summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
commit51de1d8436100f725f3576aefa24a2bd2057bc28 (patch)
treec6d1d5264b6d40a8d7ca34129f36b7d61e188af3
parentInitial commit. (diff)
downloadmpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz
mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.editorconfig15
-rw-r--r--.github/ISSUE_TEMPLATE/1_dont_ignore.md16
-rw-r--r--.github/ISSUE_TEMPLATE/2_bug_report_linux.md49
-rw-r--r--.github/ISSUE_TEMPLATE/2_bug_report_macos.md51
-rw-r--r--.github/ISSUE_TEMPLATE/2_bug_report_windows.md47
-rw-r--r--.github/ISSUE_TEMPLATE/3_bug_report.md43
-rw-r--r--.github/ISSUE_TEMPLATE/4_bug_report_build.md30
-rw-r--r--.github/ISSUE_TEMPLATE/5_feature_request.md22
-rw-r--r--.github/ISSUE_TEMPLATE/6_question.md25
-rw-r--r--.github/ISSUE_TEMPLATE/config.yml9
-rw-r--r--.github/PULL_REQUEST_TEMPLATE5
-rw-r--r--.github/workflows/build.yml292
-rw-r--r--.github/workflows/comment.yml58
-rw-r--r--.github/workflows/docs.yml24
-rw-r--r--.github/workflows/lint.yml22
-rw-r--r--.gitignore3
-rw-r--r--Copyright74
-rw-r--r--DOCS/client-api-changes.rst279
-rw-r--r--DOCS/compatibility.rst177
-rw-r--r--DOCS/compile-windows.md214
-rw-r--r--DOCS/contribute.md281
-rw-r--r--DOCS/edl-mpv.rst396
-rw-r--r--DOCS/encoding.rst155
-rw-r--r--DOCS/interface-changes.rst982
-rw-r--r--DOCS/man/af.rst267
-rw-r--r--DOCS/man/ao.rst249
-rw-r--r--DOCS/man/changes.rst20
-rw-r--r--DOCS/man/console.rst167
-rw-r--r--DOCS/man/encode.rst107
-rw-r--r--DOCS/man/input.rst3697
-rw-r--r--DOCS/man/ipc.rst387
-rw-r--r--DOCS/man/javascript.rst398
-rw-r--r--DOCS/man/libmpv.rst79
-rw-r--r--DOCS/man/lua.rst917
-rw-r--r--DOCS/man/mpv.rst1690
-rw-r--r--DOCS/man/options.rst7377
-rw-r--r--DOCS/man/osc.rst456
-rw-r--r--DOCS/man/stats.rst233
-rw-r--r--DOCS/man/vf.rst794
-rw-r--r--DOCS/man/vo.rst710
-rw-r--r--DOCS/mplayer-changes.rst455
-rw-r--r--DOCS/release-policy.md138
-rw-r--r--DOCS/tech-overview.txt656
-rw-r--r--LICENSE.GPL339
-rw-r--r--LICENSE.LGPL502
-rw-r--r--README.md208
-rw-r--r--RELEASE_NOTES214
-rwxr-xr-xTOOLS/docutils-wrapper.py67
-rwxr-xr-xTOOLS/dylib-unhell.py181
-rwxr-xr-xTOOLS/file2string.py44
-rwxr-xr-xTOOLS/gen-osd-font.sh9
-rwxr-xr-xTOOLS/idet.sh158
-rw-r--r--TOOLS/lua/README.md20
-rw-r--r--TOOLS/lua/acompressor.lua155
-rw-r--r--TOOLS/lua/ao-null-reload.lua20
-rw-r--r--TOOLS/lua/audio-hotplug-test.lua8
-rw-r--r--TOOLS/lua/autocrop.lua298
-rw-r--r--TOOLS/lua/autodeint.lua156
-rw-r--r--TOOLS/lua/autoload.lua328
-rw-r--r--TOOLS/lua/command-test.lua124
-rw-r--r--TOOLS/lua/cycle-deinterlace-pullup.lua56
-rw-r--r--TOOLS/lua/nan-test.lua37
-rw-r--r--TOOLS/lua/observe-all.lua22
-rw-r--r--TOOLS/lua/ontop-playback.lua19
-rw-r--r--TOOLS/lua/osd-test.lua35
-rw-r--r--TOOLS/lua/pause-when-minimize.lua20
-rw-r--r--TOOLS/lua/skip-logo.lua265
-rw-r--r--TOOLS/lua/status-line.lua92
-rw-r--r--TOOLS/lua/test-hooks.lua32
-rwxr-xr-xTOOLS/macos-sdk-version.py46
-rwxr-xr-xTOOLS/macos-swift-lib-directory.py42
-rwxr-xr-xTOOLS/matroska.py479
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/.notdef.glyph6
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/font.props77
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE001.glyph16
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE002.glyph22
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE003.glyph17
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE004.glyph20
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE005.glyph20
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE006.glyph47
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE007.glyph21
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE008.glyph37
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE009.glyph17
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE00A.glyph50
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE00B.glyph27
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE010.glyph21
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE011.glyph17
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE012.glyph21
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE013.glyph17
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE101.glyph16
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE104.glyph25
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE105.glyph25
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE106.glyph22
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE107.glyph56
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE108.glyph39
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE109.glyph39
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE10A.glyph36
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE10B.glyph26
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE10C.glyph33
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE10D.glyph40
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE10E.glyph53
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE110.glyph16
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE111.glyph37
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE112.glyph15
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE113.glyph20
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE114.glyph31
-rw-r--r--TOOLS/mpv-osd-symbols.sfdir/uniE115.glyph27
-rwxr-xr-xTOOLS/mpv_identify.sh149
-rwxr-xr-xTOOLS/osxbundle.py87
-rw-r--r--TOOLS/osxbundle/meson.build8
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/Info.plist874
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/MacOS/.gitkeep0
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/MacOS/lib/.gitkeep0
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/PkgInfo1
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/Resources/document.icnsbin0 -> 311266 bytes
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/Resources/icon.icnsbin0 -> 742954 bytes
-rw-r--r--TOOLS/osxbundle/mpv.app/Contents/Resources/mpv.conf2
-rwxr-xr-xTOOLS/stats-conv.py166
-rwxr-xr-xTOOLS/umpv87
-rw-r--r--TOOLS/uncrustify.cfg165
-rw-r--r--VERSION1
-rw-r--r--audio/aframe.c720
-rw-r--r--audio/aframe.h75
-rw-r--r--audio/chmap.c515
-rw-r--r--audio/chmap.h163
-rw-r--r--audio/chmap_avchannel.c51
-rw-r--r--audio/chmap_avchannel.h32
-rw-r--r--audio/chmap_sel.c389
-rw-r--r--audio/chmap_sel.h52
-rw-r--r--audio/decode/ad_lavc.c325
-rw-r--r--audio/decode/ad_spdif.c441
-rw-r--r--audio/filter/af_drop.c114
-rw-r--r--audio/filter/af_format.c143
-rw-r--r--audio/filter/af_lavcac3enc.c437
-rw-r--r--audio/filter/af_rubberband.c382
-rw-r--r--audio/filter/af_scaletempo.c626
-rw-r--r--audio/filter/af_scaletempo2.c254
-rw-r--r--audio/filter/af_scaletempo2_internals.c873
-rw-r--r--audio/filter/af_scaletempo2_internals.h134
-rw-r--r--audio/fmt-conversion.c60
-rw-r--r--audio/fmt-conversion.h24
-rw-r--r--audio/format.c267
-rw-r--r--audio/format.h77
-rw-r--r--audio/out/ao.c719
-rw-r--r--audio/out/ao.h122
-rw-r--r--audio/out/ao_alsa.c1161
-rw-r--r--audio/out/ao_audiotrack.c852
-rw-r--r--audio/out/ao_audiounit.m260
-rw-r--r--audio/out/ao_coreaudio.c435
-rw-r--r--audio/out/ao_coreaudio_chmap.c340
-rw-r--r--audio/out/ao_coreaudio_chmap.h35
-rw-r--r--audio/out/ao_coreaudio_exclusive.c472
-rw-r--r--audio/out/ao_coreaudio_properties.c103
-rw-r--r--audio/out/ao_coreaudio_properties.h61
-rw-r--r--audio/out/ao_coreaudio_utils.c539
-rw-r--r--audio/out/ao_coreaudio_utils.h79
-rw-r--r--audio/out/ao_jack.c284
-rw-r--r--audio/out/ao_lavc.c337
-rw-r--r--audio/out/ao_null.c230
-rw-r--r--audio/out/ao_openal.c401
-rw-r--r--audio/out/ao_opensles.c265
-rw-r--r--audio/out/ao_oss.c400
-rw-r--r--audio/out/ao_pcm.c248
-rw-r--r--audio/out/ao_pipewire.c883
-rw-r--r--audio/out/ao_pulse.c817
-rw-r--r--audio/out/ao_sdl.c216
-rw-r--r--audio/out/ao_sndio.c321
-rw-r--r--audio/out/ao_wasapi.c504
-rw-r--r--audio/out/ao_wasapi.h116
-rw-r--r--audio/out/ao_wasapi_changenotify.c246
-rw-r--r--audio/out/ao_wasapi_utils.c1063
-rw-r--r--audio/out/buffer.c736
-rw-r--r--audio/out/internal.h237
-rwxr-xr-xci/build-freebsd.sh29
-rwxr-xr-xci/build-macos.sh22
-rwxr-xr-xci/build-mingw64.sh306
-rwxr-xr-xci/build-msys2.sh39
-rwxr-xr-xci/build-tumbleweed.sh20
-rwxr-xr-xci/lint-commit-msg.py116
-rw-r--r--common/av_common.c404
-rw-r--r--common/av_common.h54
-rw-r--r--common/av_log.c215
-rw-r--r--common/av_log.h11
-rw-r--r--common/codecs.c107
-rw-r--r--common/codecs.h48
-rw-r--r--common/common.c413
-rw-r--r--common/common.h161
-rw-r--r--common/encode.h61
-rw-r--r--common/encode_lavc.c949
-rw-r--r--common/encode_lavc.h114
-rw-r--r--common/global.h15
-rw-r--r--common/meson.build8
-rw-r--r--common/msg.c1069
-rw-r--r--common/msg.h92
-rw-r--r--common/msg_control.h45
-rw-r--r--common/playlist.c413
-rw-r--r--common/playlist.h121
-rw-r--r--common/recorder.c422
-rw-r--r--common/recorder.h25
-rw-r--r--common/stats.c325
-rw-r--r--common/stats.h34
-rw-r--r--common/tags.c151
-rw-r--r--common/tags.h31
-rw-r--r--common/version.c23
-rw-r--r--common/version.h.in7
-rw-r--r--demux/cache.c331
-rw-r--r--demux/cache.h16
-rw-r--r--demux/codec_tags.c280
-rw-r--r--demux/codec_tags.h35
-rw-r--r--demux/cue.c270
-rw-r--r--demux/cue.h43
-rw-r--r--demux/demux.c4624
-rw-r--r--demux/demux.h361
-rw-r--r--demux/demux_cue.c304
-rw-r--r--demux/demux_disc.c360
-rw-r--r--demux/demux_edl.c651
-rw-r--r--demux/demux_lavf.c1448
-rw-r--r--demux/demux_libarchive.c120
-rw-r--r--demux/demux_mf.c373
-rw-r--r--demux/demux_mkv.c3392
-rw-r--r--demux/demux_mkv_timeline.c642
-rw-r--r--demux/demux_null.c35
-rw-r--r--demux/demux_playlist.c584
-rw-r--r--demux/demux_raw.c326
-rw-r--r--demux/demux_timeline.c719
-rw-r--r--demux/ebml.c619
-rw-r--r--demux/ebml.h93
-rw-r--r--demux/matroska.h24
-rw-r--r--demux/packet.c244
-rw-r--r--demux/packet.h86
-rw-r--r--demux/stheader.h119
-rw-r--r--demux/timeline.c41
-rw-r--r--demux/timeline.h72
-rw-r--r--etc/_mpv.zsh265
-rw-r--r--etc/builtin.conf80
-rw-r--r--etc/encoding-profiles.conf224
-rw-r--r--etc/input.conf181
-rw-r--r--etc/meson.build20
-rw-r--r--etc/mplayer-input.conf93
-rw-r--r--etc/mpv-gradient.svg198
-rw-r--r--etc/mpv-icon-8bit-128x128.pngbin0 -> 22984 bytes
-rw-r--r--etc/mpv-icon-8bit-16x16.pngbin0 -> 759 bytes
-rw-r--r--etc/mpv-icon-8bit-32x32.pngbin0 -> 2124 bytes
-rw-r--r--etc/mpv-icon-8bit-64x64.pngbin0 -> 5686 bytes
-rw-r--r--etc/mpv-icon.icobin0 -> 270345 bytes
-rw-r--r--etc/mpv-symbolic.svg68
-rw-r--r--etc/mpv.bash-completion123
-rw-r--r--etc/mpv.conf143
-rw-r--r--etc/mpv.desktop44
-rw-r--r--etc/mpv.metainfo.xml29
-rw-r--r--etc/mpv.svg86
-rw-r--r--etc/restore-old-bindings.conf59
-rw-r--r--filters/f_async_queue.c375
-rw-r--r--filters/f_async_queue.h135
-rw-r--r--filters/f_auto_filters.c431
-rw-r--r--filters/f_auto_filters.h13
-rw-r--r--filters/f_autoconvert.c576
-rw-r--r--filters/f_autoconvert.h85
-rw-r--r--filters/f_decoder_wrapper.c1326
-rw-r--r--filters/f_decoder_wrapper.h125
-rw-r--r--filters/f_demux_in.c85
-rw-r--r--filters/f_demux_in.h11
-rw-r--r--filters/f_hwtransfer.c667
-rw-r--r--filters/f_hwtransfer.h28
-rw-r--r--filters/f_lavfi.c1208
-rw-r--r--filters/f_lavfi.h42
-rw-r--r--filters/f_output_chain.c729
-rw-r--r--filters/f_output_chain.h87
-rw-r--r--filters/f_swresample.c677
-rw-r--r--filters/f_swresample.h44
-rw-r--r--filters/f_swscale.c153
-rw-r--r--filters/f_swscale.h34
-rw-r--r--filters/f_utils.c311
-rw-r--r--filters/f_utils.h84
-rw-r--r--filters/filter.c909
-rw-r--r--filters/filter.h470
-rw-r--r--filters/filter_internal.h145
-rw-r--r--filters/frame.c210
-rw-r--r--filters/frame.h59
-rw-r--r--filters/user_filters.c185
-rw-r--r--filters/user_filters.h39
-rw-r--r--input/cmd.c671
-rw-r--r--input/cmd.h156
-rw-r--r--input/event.c93
-rw-r--r--input/event.h43
-rw-r--r--input/input.c1695
-rw-r--r--input/input.h239
-rw-r--r--input/ipc-dummy.c19
-rw-r--r--input/ipc-unix.c444
-rw-r--r--input/ipc-win.c509
-rw-r--r--input/ipc.c414
-rw-r--r--input/keycodes.c379
-rw-r--r--input/keycodes.h270
-rw-r--r--input/meson.build20
-rw-r--r--input/sdl_gamepad.c287
-rw-r--r--libmpv/client.h2032
-rw-r--r--libmpv/render.h759
-rw-r--r--libmpv/render_gl.h211
-rw-r--r--libmpv/stream_cb.h247
-rw-r--r--meson.build1765
-rw-r--r--meson_options.txt117
-rw-r--r--misc/bstr.c469
-rw-r--r--misc/bstr.h231
-rw-r--r--misc/charset_conv.c235
-rw-r--r--misc/charset_conv.h22
-rw-r--r--misc/ctype.h19
-rw-r--r--misc/dispatch.c417
-rw-r--r--misc/dispatch.h32
-rw-r--r--misc/jni.c429
-rw-r--r--misc/jni.h161
-rw-r--r--misc/json.c359
-rw-r--r--misc/json.h31
-rw-r--r--misc/language.c362
-rw-r--r--misc/language.h31
-rw-r--r--misc/linked_list.h107
-rw-r--r--misc/natural_sort.c67
-rw-r--r--misc/natural_sort.h23
-rw-r--r--misc/node.c159
-rw-r--r--misc/node.h20
-rw-r--r--misc/random.c75
-rw-r--r--misc/random.h41
-rw-r--r--misc/rendezvous.c55
-rw-r--r--misc/rendezvous.h8
-rw-r--r--misc/thread_pool.c223
-rw-r--r--misc/thread_pool.h35
-rw-r--r--misc/thread_tools.c276
-rw-r--r--misc/thread_tools.h83
-rw-r--r--misc/uuid.c141
-rw-r--r--misc/uuid.h146
-rw-r--r--mpv_talloc.h7
-rw-r--r--options/m_config.h1
-rw-r--r--options/m_config_core.c876
-rw-r--r--options/m_config_core.h194
-rw-r--r--options/m_config_frontend.c1080
-rw-r--r--options/m_config_frontend.h266
-rw-r--r--options/m_option.c3866
-rw-r--r--options/m_option.h764
-rw-r--r--options/m_property.c630
-rw-r--r--options/m_property.h234
-rw-r--r--options/options.c1097
-rw-r--r--options/options.h406
-rw-r--r--options/parse_commandline.c261
-rw-r--r--options/parse_commandline.h32
-rw-r--r--options/parse_configfile.c178
-rw-r--r--options/parse_configfile.h30
-rw-r--r--options/path.c410
-rw-r--r--options/path.h98
-rw-r--r--osdep/android/strnlen.c40
-rw-r--r--osdep/android/strnlen.h33
-rw-r--r--osdep/apple_utils.c39
-rw-r--r--osdep/apple_utils.h28
-rw-r--r--osdep/compiler.h30
-rw-r--r--osdep/endian.h37
-rw-r--r--osdep/getpid.h29
-rw-r--r--osdep/glob-win.c162
-rw-r--r--osdep/io.c904
-rw-r--r--osdep/io.h232
-rw-r--r--osdep/language-apple.c45
-rw-r--r--osdep/language-posix.c72
-rw-r--r--osdep/language-win.c65
-rw-r--r--osdep/macOS_swift_bridge.h57
-rw-r--r--osdep/macos/libmpv_helper.swift250
-rw-r--r--osdep/macos/log_helper.swift47
-rw-r--r--osdep/macos/mpv_helper.swift156
-rw-r--r--osdep/macos/precise_timer.swift153
-rw-r--r--osdep/macos/remote_command_center.swift191
-rw-r--r--osdep/macos/swift_compat.swift36
-rw-r--r--osdep/macos/swift_extensions.swift58
-rw-r--r--osdep/macosx_application.h55
-rw-r--r--osdep/macosx_application.m375
-rw-r--r--osdep/macosx_application_objc.h40
-rw-r--r--osdep/macosx_events.h36
-rw-r--r--osdep/macosx_events.m408
-rw-r--r--osdep/macosx_events_objc.h45
-rw-r--r--osdep/macosx_menubar.h30
-rw-r--r--osdep/macosx_menubar.m853
-rw-r--r--osdep/macosx_menubar_objc.h25
-rw-r--r--osdep/macosx_touchbar.h46
-rw-r--r--osdep/macosx_touchbar.m334
-rw-r--r--osdep/main-fn-cocoa.c10
-rw-r--r--osdep/main-fn-unix.c6
-rw-r--r--osdep/main-fn-win.c93
-rw-r--r--osdep/main-fn.h1
-rw-r--r--osdep/meson.build51
-rw-r--r--osdep/mpv.exe.manifest41
-rw-r--r--osdep/mpv.rc50
-rw-r--r--osdep/path-darwin.c77
-rw-r--r--osdep/path-macosx.m34
-rw-r--r--osdep/path-unix.c100
-rw-r--r--osdep/path-uwp.c35
-rw-r--r--osdep/path-win.c113
-rw-r--r--osdep/path.h32
-rw-r--r--osdep/poll_wrapper.c89
-rw-r--r--osdep/poll_wrapper.h12
-rw-r--r--osdep/semaphore.h37
-rw-r--r--osdep/semaphore_osx.c117
-rw-r--r--osdep/strnlen.h31
-rw-r--r--osdep/subprocess-dummy.c7
-rw-r--r--osdep/subprocess-posix.c345
-rw-r--r--osdep/subprocess-win.c516
-rw-r--r--osdep/subprocess.c39
-rw-r--r--osdep/subprocess.h88
-rw-r--r--osdep/terminal-dummy.c35
-rw-r--r--osdep/terminal-unix.c573
-rw-r--r--osdep/terminal-win.c425
-rw-r--r--osdep/terminal.h60
-rw-r--r--osdep/threads-posix.c64
-rw-r--r--osdep/threads-posix.h247
-rw-r--r--osdep/threads-win32.h224
-rw-r--r--osdep/threads.h23
-rw-r--r--osdep/timer-darwin.c48
-rw-r--r--osdep/timer-linux.c64
-rw-r--r--osdep/timer-win32.c141
-rw-r--r--osdep/timer.c67
-rw-r--r--osdep/timer.h63
-rw-r--r--osdep/w32_keyboard.c123
-rw-r--r--osdep/w32_keyboard.h29
-rw-r--r--osdep/win32-console-wrapper.c89
-rw-r--r--osdep/windows_utils.c229
-rw-r--r--osdep/windows_utils.h49
-rw-r--r--player/audio.c985
-rw-r--r--player/client.c2248
-rw-r--r--player/client.h58
-rw-r--r--player/command.c7149
-rw-r--r--player/command.h123
-rw-r--r--player/configfiles.c472
-rw-r--r--player/core.h644
-rw-r--r--player/external_files.c359
-rw-r--r--player/external_files.h38
-rw-r--r--player/javascript.c1262
-rw-r--r--player/javascript/defaults.js782
-rw-r--r--player/javascript/meson.build6
-rw-r--r--player/loadfile.c2066
-rw-r--r--player/lua.c1341
-rw-r--r--player/lua/assdraw.lua160
-rw-r--r--player/lua/auto_profiles.lua198
-rw-r--r--player/lua/console.lua1204
-rw-r--r--player/lua/defaults.lua836
-rw-r--r--player/lua/meson.build10
-rw-r--r--player/lua/options.lua164
-rw-r--r--player/lua/osc.lua2917
-rw-r--r--player/lua/stats.lua1417
-rw-r--r--player/lua/ytdl_hook.lua1191
-rw-r--r--player/main.c467
-rw-r--r--player/meson.build10
-rw-r--r--player/misc.c334
-rw-r--r--player/osd.c580
-rw-r--r--player/playloop.c1291
-rw-r--r--player/screenshot.c611
-rw-r--r--player/screenshot.h46
-rw-r--r--player/scripting.c462
-rw-r--r--player/sub.c214
-rw-r--r--player/video.c1324
-rw-r--r--stream/cookies.c138
-rw-r--r--stream/cookies.h31
-rw-r--r--stream/dvb_tune.c652
-rw-r--r--stream/dvb_tune.h42
-rw-r--r--stream/dvbin.h143
-rw-r--r--stream/stream.c900
-rw-r--r--stream/stream.h269
-rw-r--r--stream/stream_avdevice.c32
-rw-r--r--stream/stream_bluray.c632
-rw-r--r--stream/stream_cb.c108
-rw-r--r--stream/stream_cdda.c369
-rw-r--r--stream/stream_concat.c179
-rw-r--r--stream/stream_dvb.c1161
-rw-r--r--stream/stream_dvdnav.c719
-rw-r--r--stream/stream_edl.c17
-rw-r--r--stream/stream_file.c377
-rw-r--r--stream/stream_lavf.c457
-rw-r--r--stream/stream_libarchive.c623
-rw-r--r--stream/stream_libarchive.h35
-rw-r--r--stream/stream_memory.c100
-rw-r--r--stream/stream_mf.c41
-rw-r--r--stream/stream_null.c35
-rw-r--r--stream/stream_slice.c181
-rw-r--r--sub/ass_mp.c422
-rw-r--r--sub/ass_mp.h65
-rw-r--r--sub/dec_sub.c498
-rw-r--r--sub/dec_sub.h62
-rw-r--r--sub/draw_bmp.c1035
-rw-r--r--sub/draw_bmp.h63
-rw-r--r--sub/filter_jsre.c140
-rw-r--r--sub/filter_regex.c89
-rw-r--r--sub/filter_sdh.c482
-rw-r--r--sub/img_convert.c128
-rw-r--r--sub/img_convert.h23
-rw-r--r--sub/lavc_conv.c293
-rw-r--r--sub/meson.build6
-rw-r--r--sub/osd.c559
-rw-r--r--sub/osd.h247
-rw-r--r--sub/osd_font.otfbin0 -> 4460 bytes
-rw-r--r--sub/osd_libass.c691
-rw-r--r--sub/osd_state.h94
-rw-r--r--sub/sd.h111
-rw-r--r--sub/sd_ass.c1035
-rw-r--r--sub/sd_lavc.c676
-rw-r--r--ta/README39
-rw-r--r--ta/ta.c404
-rw-r--r--ta/ta.h157
-rw-r--r--ta/ta_talloc.c77
-rw-r--r--ta/ta_talloc.h157
-rw-r--r--ta/ta_utils.c315
-rw-r--r--test/chmap.c218
-rw-r--r--test/gl_video.c25
-rw-r--r--test/img_format.c217
-rw-r--r--test/img_utils.c63
-rw-r--r--test/img_utils.h24
-rw-r--r--test/json.c87
-rw-r--r--test/libmpv_test.c271
-rw-r--r--test/linked_list.c160
-rw-r--r--test/meson.build148
-rw-r--r--test/paths.c65
-rw-r--r--test/ref/draw_bmp.txt249
-rw-r--r--test/ref/img_formats.txt2834
-rw-r--r--test/ref/repack.txt385
-rw-r--r--test/ref/repack_sws.log18
-rw-r--r--test/ref/repack_zimg.log18
-rw-r--r--test/ref/zimg_formats.txt249
-rw-r--r--test/repack.c532
-rw-r--r--test/scale_sws.c42
-rw-r--r--test/scale_test.c192
-rw-r--r--test/scale_test.h30
-rw-r--r--test/scale_zimg.c56
-rw-r--r--test/test_utils.c111
-rw-r--r--test/test_utils.h56
-rw-r--r--test/timer.c41
-rw-r--r--video/csputils.c1020
-rw-r--r--video/csputils.h290
-rw-r--r--video/cuda.c44
-rw-r--r--video/d3d.c273
-rw-r--r--video/d3d.h42
-rw-r--r--video/decode/vd_lavc.c1457
-rw-r--r--video/drmprime.c43
-rw-r--r--video/filter/refqueue.c356
-rw-r--r--video/filter/refqueue.h39
-rw-r--r--video/filter/vf_d3d11vpp.c506
-rw-r--r--video/filter/vf_fingerprint.c229
-rw-r--r--video/filter/vf_format.c245
-rw-r--r--video/filter/vf_gpu.c373
-rw-r--r--video/filter/vf_sub.c164
-rw-r--r--video/filter/vf_vapoursynth.c892
-rw-r--r--video/filter/vf_vavpp.c503
-rw-r--r--video/filter/vf_vdpaupp.c195
-rw-r--r--video/fmt-conversion.c112
-rw-r--r--video/fmt-conversion.h26
-rw-r--r--video/hwdec.c140
-rw-r--r--video/hwdec.h108
-rw-r--r--video/image_loader.c48
-rw-r--r--video/image_loader.h9
-rw-r--r--video/image_writer.c757
-rw-r--r--video/image_writer.h74
-rw-r--r--video/img_format.c824
-rw-r--r--video/img_format.h342
-rw-r--r--video/mp_image.c1289
-rw-r--r--video/mp_image.h203
-rw-r--r--video/mp_image_pool.c472
-rw-r--r--video/mp_image_pool.h47
-rw-r--r--video/out/android_common.c99
-rw-r--r--video/out/android_common.h29
-rw-r--r--video/out/aspect.c216
-rw-r--r--video/out/aspect.h33
-rw-r--r--video/out/bitmap_packer.c197
-rw-r--r--video/out/bitmap_packer.h51
-rw-r--r--video/out/cocoa_cb_common.swift230
-rw-r--r--video/out/d3d11/context.c566
-rw-r--r--video/out/d3d11/context.h9
-rw-r--r--video/out/d3d11/hwdec_d3d11va.c258
-rw-r--r--video/out/d3d11/hwdec_dxva2dxgi.c478
-rw-r--r--video/out/d3d11/ra_d3d11.c2544
-rw-r--r--video/out/d3d11/ra_d3d11.h47
-rw-r--r--video/out/dither.c175
-rw-r--r--video/out/dither.h2
-rw-r--r--video/out/dr_helper.c162
-rw-r--r--video/out/dr_helper.h37
-rw-r--r--video/out/drm_atomic.c458
-rw-r--r--video/out/drm_atomic.h100
-rw-r--r--video/out/drm_common.c1289
-rw-r--r--video/out/drm_common.h108
-rw-r--r--video/out/drm_prime.c160
-rw-r--r--video/out/drm_prime.h45
-rw-r--r--video/out/filter_kernels.c411
-rw-r--r--video/out/filter_kernels.h56
-rw-r--r--video/out/gpu/context.c277
-rw-r--r--video/out/gpu/context.h107
-rw-r--r--video/out/gpu/d3d11_helpers.c966
-rw-r--r--video/out/gpu/d3d11_helpers.h103
-rw-r--r--video/out/gpu/error_diffusion.c316
-rw-r--r--video/out/gpu/error_diffusion.h48
-rw-r--r--video/out/gpu/hwdec.c358
-rw-r--r--video/out/gpu/hwdec.h156
-rw-r--r--video/out/gpu/lcms.c526
-rw-r--r--video/out/gpu/lcms.h61
-rw-r--r--video/out/gpu/libmpv_gpu.c248
-rw-r--r--video/out/gpu/libmpv_gpu.h40
-rw-r--r--video/out/gpu/osd.c363
-rw-r--r--video/out/gpu/osd.h25
-rw-r--r--video/out/gpu/ra.c424
-rw-r--r--video/out/gpu/ra.h559
-rw-r--r--video/out/gpu/shader_cache.c1056
-rw-r--r--video/out/gpu/shader_cache.h66
-rw-r--r--video/out/gpu/spirv.c70
-rw-r--r--video/out/gpu/spirv.h41
-rw-r--r--video/out/gpu/spirv_shaderc.c125
-rw-r--r--video/out/gpu/user_shaders.c463
-rw-r--r--video/out/gpu/user_shaders.h99
-rw-r--r--video/out/gpu/utils.c349
-rw-r--r--video/out/gpu/utils.h108
-rw-r--r--video/out/gpu/video.c4364
-rw-r--r--video/out/gpu/video.h238
-rw-r--r--video/out/gpu/video_shaders.c1033
-rw-r--r--video/out/gpu/video_shaders.h59
-rw-r--r--video/out/gpu_next/context.c240
-rw-r--r--video/out/gpu_next/context.h40
-rw-r--r--video/out/hwdec/dmabuf_interop.h57
-rw-r--r--video/out/hwdec/dmabuf_interop_gl.c311
-rw-r--r--video/out/hwdec/dmabuf_interop_pl.c138
-rw-r--r--video/out/hwdec/dmabuf_interop_wl.c83
-rw-r--r--video/out/hwdec/hwdec_aimagereader.c402
-rw-r--r--video/out/hwdec/hwdec_cuda.c286
-rw-r--r--video/out/hwdec/hwdec_cuda.h59
-rw-r--r--video/out/hwdec/hwdec_cuda_gl.c174
-rw-r--r--video/out/hwdec/hwdec_cuda_vk.c344
-rw-r--r--video/out/hwdec/hwdec_drmprime.c294
-rw-r--r--video/out/hwdec/hwdec_drmprime_overlay.c334
-rw-r--r--video/out/hwdec/hwdec_ios_gl.m222
-rw-r--r--video/out/hwdec/hwdec_mac_gl.c169
-rw-r--r--video/out/hwdec/hwdec_vaapi.c557
-rw-r--r--video/out/hwdec/hwdec_vt.c141
-rw-r--r--video/out/hwdec/hwdec_vt.h63
-rw-r--r--video/out/hwdec/hwdec_vt_pl.m312
-rw-r--r--video/out/hwdec/hwdec_vulkan.c333
-rw-r--r--video/out/libmpv.h83
-rw-r--r--video/out/libmpv_sw.c208
-rw-r--r--video/out/mac/common.swift691
-rw-r--r--video/out/mac/gl_layer.swift322
-rw-r--r--video/out/mac/metal_layer.swift43
-rw-r--r--video/out/mac/title_bar.swift229
-rw-r--r--video/out/mac/view.swift297
-rw-r--r--video/out/mac/window.swift593
-rw-r--r--video/out/mac_common.swift174
-rw-r--r--video/out/meson.build51
-rw-r--r--video/out/opengl/angle_dynamic.c39
-rw-r--r--video/out/opengl/angle_dynamic.h89
-rw-r--r--video/out/opengl/common.c694
-rw-r--r--video/out/opengl/common.h258
-rw-r--r--video/out/opengl/context.c324
-rw-r--r--video/out/opengl/context.h58
-rw-r--r--video/out/opengl/context_android.c130
-rw-r--r--video/out/opengl/context_angle.c653
-rw-r--r--video/out/opengl/context_drm_egl.c744
-rw-r--r--video/out/opengl/context_dxinterop.c605
-rw-r--r--video/out/opengl/context_glx.c351
-rw-r--r--video/out/opengl/context_rpi.c327
-rw-r--r--video/out/opengl/context_wayland.c230
-rw-r--r--video/out/opengl/context_win.c378
-rw-r--r--video/out/opengl/context_x11egl.c225
-rw-r--r--video/out/opengl/egl_helpers.c381
-rw-r--r--video/out/opengl/egl_helpers.h38
-rw-r--r--video/out/opengl/formats.c196
-rw-r--r--video/out/opengl/formats.h51
-rw-r--r--video/out/opengl/gl_headers.h799
-rw-r--r--video/out/opengl/hwdec_d3d11egl.c363
-rw-r--r--video/out/opengl/hwdec_dxva2egl.c384
-rw-r--r--video/out/opengl/hwdec_dxva2gldx.c247
-rw-r--r--video/out/opengl/hwdec_rpi.c384
-rw-r--r--video/out/opengl/hwdec_vdpau.c251
-rw-r--r--video/out/opengl/libmpv_gl.c114
-rw-r--r--video/out/opengl/ra_gl.c1208
-rw-r--r--video/out/opengl/ra_gl.h17
-rw-r--r--video/out/opengl/utils.c282
-rw-r--r--video/out/opengl/utils.h57
-rw-r--r--video/out/placebo/ra_pl.c677
-rw-r--r--video/out/placebo/ra_pl.h16
-rw-r--r--video/out/placebo/utils.c263
-rw-r--r--video/out/placebo/utils.h41
-rw-r--r--video/out/present_sync.c126
-rw-r--r--video/out/present_sync.h57
-rw-r--r--video/out/vo.c1441
-rw-r--r--video/out/vo.h544
-rw-r--r--video/out/vo_caca.c314
-rw-r--r--video/out/vo_direct3d.c1247
-rw-r--r--video/out/vo_dmabuf_wayland.c872
-rw-r--r--video/out/vo_drm.c458
-rw-r--r--video/out/vo_gpu.c336
-rw-r--r--video/out/vo_gpu_next.c2104
-rw-r--r--video/out/vo_image.c165
-rw-r--r--video/out/vo_kitty.c433
-rw-r--r--video/out/vo_lavc.c262
-rw-r--r--video/out/vo_libmpv.c748
-rw-r--r--video/out/vo_mediacodec_embed.c127
-rw-r--r--video/out/vo_null.c104
-rw-r--r--video/out/vo_rpi.c938
-rw-r--r--video/out/vo_sdl.c992
-rw-r--r--video/out/vo_sixel.c627
-rw-r--r--video/out/vo_tct.c347
-rw-r--r--video/out/vo_vaapi.c877
-rw-r--r--video/out/vo_vdpau.c1139
-rw-r--r--video/out/vo_wlshm.c324
-rw-r--r--video/out/vo_x11.c447
-rw-r--r--video/out/vo_xv.c921
-rw-r--r--video/out/vulkan/common.h40
-rw-r--r--video/out/vulkan/context.c372
-rw-r--r--video/out/vulkan/context.h31
-rw-r--r--video/out/vulkan/context_android.c96
-rw-r--r--video/out/vulkan/context_display.c491
-rw-r--r--video/out/vulkan/context_mac.m119
-rw-r--r--video/out/vulkan/context_wayland.c167
-rw-r--r--video/out/vulkan/context_win.c106
-rw-r--r--video/out/vulkan/context_xlib.c143
-rw-r--r--video/out/vulkan/utils.c42
-rw-r--r--video/out/vulkan/utils.h6
-rw-r--r--video/out/w32_common.c2144
-rw-r--r--video/out/w32_common.h36
-rw-r--r--video/out/wayland_common.c2629
-rw-r--r--video/out/wayland_common.h189
-rw-r--r--video/out/win32/displayconfig.c140
-rw-r--r--video/out/win32/displayconfig.h27
-rw-r--r--video/out/win32/droptarget.c227
-rw-r--r--video/out/win32/droptarget.h35
-rw-r--r--video/out/win_state.c155
-rw-r--r--video/out/win_state.h35
-rw-r--r--video/out/wldmabuf/context_wldmabuf.c43
-rw-r--r--video/out/wldmabuf/ra_wldmabuf.c66
-rw-r--r--video/out/wldmabuf/ra_wldmabuf.h23
-rw-r--r--video/out/x11_common.c2291
-rw-r--r--video/out/x11_common.h164
-rw-r--r--video/repack.c1203
-rw-r--r--video/repack.h76
-rw-r--r--video/sws_utils.c496
-rw-r--r--video/sws_utils.h82
-rw-r--r--video/vaapi.c288
-rw-r--r--video/vaapi.h54
-rw-r--r--video/vdpau.c574
-rw-r--r--video/vdpau.h109
-rw-r--r--video/vdpau_functions.inc50
-rw-r--r--video/vdpau_mixer.c306
-rw-r--r--video/vdpau_mixer.h61
-rw-r--r--video/zimg.c730
-rw-r--r--video/zimg.h73
740 files changed, 246282 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0f4e36d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+# To use this config on you editor, follow the instructions at:
+# http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[.git/COMMIT*]
+max_line_length = 72
diff --git a/.github/ISSUE_TEMPLATE/1_dont_ignore.md b/.github/ISSUE_TEMPLATE/1_dont_ignore.md
new file mode 100644
index 0000000..578d560
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1_dont_ignore.md
@@ -0,0 +1,16 @@
+---
+name: 'README: DO NOT IGNORE OR DELETE THE ISSUE TEMPLATE'
+about: 'Chose and fill out one of the following templates!'
+title: ''
+labels: priority:ignored-issue-template
+assignees: ''
+
+---
+
+We ask you to not ignore the issue template. Fill it out as good and correct as
+possible. Issues that don't adhere to our request will be closed for ignoring
+the issue template. This is because analyzing a bug without a log file is harder
+than necessary. Low quality bug reports are noise.
+
+Please go back and chose the proper issue template. Opening issues with this
+template will be closed immediately.
diff --git a/.github/ISSUE_TEMPLATE/2_bug_report_linux.md b/.github/ISSUE_TEMPLATE/2_bug_report_linux.md
new file mode 100644
index 0000000..6faaf85
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2_bug_report_linux.md
@@ -0,0 +1,49 @@
+---
+name: 'Report a Linux Issue'
+about: 'Create a report for a runtime related Linux Issue'
+title: ''
+labels: 'os:linux'
+assignees: ''
+
+---
+
+### Important Information
+
+Provide following Information:
+- mpv version
+- Linux Distribution and Version
+- Source of the mpv binary
+- If known which version of mpv introduced the problem
+- Window Manager and version
+- GPU driver and version
+- Possible screenshot or video of visual glitches
+
+If you're not using git master or the latest release, update.
+Releases are listed here: https://github.com/mpv-player/mpv/releases
+
+### Reproduction steps
+
+Try to reproduce your issue with --no-config first. If it isn't reproducible
+with --no-config try to first find out which option or script causes your issue.
+
+Describe the reproduction steps as precise as possible. It's very likely that
+the bug you experience wasn't reproduced by the developer because the workflow
+differs from your own.
+
+### Expected behavior
+
+### Actual behavior
+
+### Log file
+
+Make a log file made with -v -v or --log-file=output.txt, paste it to
+https://0x0.st/ or attach it to the github issue, and replace this text with a
+link to it.
+
+The issue will be closed for ignoring the issue template.
+
+### Sample files
+
+Sample files needed to reproduce this issue can be uploaded to https://0x0.st/
+or similar sites. (Only needed if the issue cannot be reproduced without it.)
+Do not use garbage like "cloud storage", especially not Google Drive.
diff --git a/.github/ISSUE_TEMPLATE/2_bug_report_macos.md b/.github/ISSUE_TEMPLATE/2_bug_report_macos.md
new file mode 100644
index 0000000..809ea39
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2_bug_report_macos.md
@@ -0,0 +1,51 @@
+---
+name: 'Report a macOS Issue'
+about: 'Create a report for a runtime related macOS Issue'
+title: ''
+labels: 'os:mac'
+assignees: ''
+
+---
+
+### Important Information
+
+Provide following Information:
+- mpv version
+- macOS Version
+- Source of the mpv binary or bundle
+- If known which version of mpv introduced the problem
+- Possible screenshot or video of visual glitches
+
+If you're not using git master or the latest release, update.
+Releases are listed here: https://github.com/mpv-player/mpv/releases
+
+### Reproduction steps
+
+Try to reproduce your issue with --no-config first. If it isn't reproducible
+with --no-config try to first find out which option or script causes your issue.
+
+Describe the reproduction steps as precise as possible. It's very likely that
+the bug you experience wasn't reproduced by the developer because the workflow
+differs from your own.
+
+### Expected behavior
+
+### Actual behavior
+
+### Log file
+
+Make a log file made with -v -v or --log-file=output.txt. If you use the Bundle
+from a version later than 0.32 a default log is created for your last run at
+~/Library/Logs/mpv.log. You can jump to that file via the Help > Show log File…
+menu. Paste the log to https://0x0.st/ or attach it to the github issue, and
+replace this text with a link to it.
+
+In the case of a crash please provide the macOS Crash Report (Backtrace).
+
+The issue will be closed for ignoring the issue template.
+
+### Sample files
+
+Sample files needed to reproduce this issue can be uploaded to https://0x0.st/
+or similar sites. (Only needed if the issue cannot be reproduced without it.)
+Do not use garbage like "cloud storage", especially not Google Drive.
diff --git a/.github/ISSUE_TEMPLATE/2_bug_report_windows.md b/.github/ISSUE_TEMPLATE/2_bug_report_windows.md
new file mode 100644
index 0000000..b42c385
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2_bug_report_windows.md
@@ -0,0 +1,47 @@
+---
+name: 'Report a Windows Issue'
+about: 'Create a report for a runtime related Windows Issue'
+title: ''
+labels: 'os:win'
+assignees: ''
+
+---
+
+### Important Information
+
+Provide following Information:
+- mpv version
+- Windows Version
+- Source of the mpv binary
+- If known which version of mpv introduced the problem
+- Possible screenshot or video of visual glitches
+
+If you're not using git master or the latest release, update.
+Releases are listed here: https://github.com/mpv-player/mpv/releases
+
+### Reproduction steps
+
+Try to reproduce your issue with --no-config first. If it isn't reproducible
+with --no-config try to first find out which option or script causes your issue.
+
+Describe the reproduction steps as precise as possible. It's very likely that
+the bug you experience wasn't reproduced by the developer because the workflow
+differs from your own.
+
+### Expected behavior
+
+### Actual behavior
+
+### Log file
+
+Make a log file made with -v -v or --log-file=output.txt, paste it to
+https://0x0.st/ or attach it to the github issue, and replace this text with a
+link to it.
+
+The issue will be closed for ignoring the issue template.
+
+### Sample files
+
+Sample files needed to reproduce this issue can be uploaded to https://0x0.st/
+or similar sites. (Only needed if the issue cannot be reproduced without it.)
+Do not use garbage like "cloud storage", especially not Google Drive.
diff --git a/.github/ISSUE_TEMPLATE/3_bug_report.md b/.github/ISSUE_TEMPLATE/3_bug_report.md
new file mode 100644
index 0000000..60df58d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3_bug_report.md
@@ -0,0 +1,43 @@
+---
+name: 'Report a different Issue'
+about: 'Create a report for a runtime related Issue'
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+### Important Information
+
+Provide following Information:
+- mpv version
+- Platform and Version
+- Source of the mpv binary
+
+If you're not using git master or the latest release, update.
+Releases are listed here: https://github.com/mpv-player/mpv/releases
+
+### Reproduction steps
+
+Try to reproduce your issue with --no-config first. If it isn't reproducible
+with --no-config try to first find out which option or script causes your issue.
+
+Describe the reproduction steps as precise as possible. It's very likely that
+the bug you experience wasn't reproduced by the developer because the workflow
+differs from your own.
+
+### Expected behavior
+
+### Actual behavior
+
+### Log file
+
+Make a log file made with -v -v or --log-file=output.txt, paste it to
+https://0x0.st/ or attach it to the github issue, and replace this text with a
+link to it.
+
+### Sample files
+
+Sample files needed to reproduce this issue can be uploaded to https://0x0.st/
+or similar sites. (Only needed if the issue cannot be reproduced without it.)
+Do not use garbage like "cloud storage", especially not Google Drive.
diff --git a/.github/ISSUE_TEMPLATE/4_bug_report_build.md b/.github/ISSUE_TEMPLATE/4_bug_report_build.md
new file mode 100644
index 0000000..e8697f0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/4_bug_report_build.md
@@ -0,0 +1,30 @@
+---
+name: 'Report a build Problem'
+about: 'Create a report for a build related Issue'
+title: ''
+labels: 'core:meson'
+assignees: ''
+
+---
+
+### mpv version and platform versions
+
+If you're not using git master or the latest release, update.
+Releases are listed here: https://github.com/mpv-player/mpv/releases
+
+### Reproduction steps
+
+Describe the reproduction steps as precise as possible. It's very likely that
+the bug you experience wasn't reproduced by the developer because the workflow
+differs from your own.
+
+### Expected behavior
+
+### Actual behavior
+
+### Log file
+
+Upload meson-logs/meson-log.txt or meson setup build output to https://0x0.st/ or attach
+it to the github issue, and replace this text with a link to it.
+
+The issue will be closed for ignoring the issue template.
diff --git a/.github/ISSUE_TEMPLATE/5_feature_request.md b/.github/ISSUE_TEMPLATE/5_feature_request.md
new file mode 100644
index 0000000..2fba2ba
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/5_feature_request.md
@@ -0,0 +1,22 @@
+---
+name: 'Request a new Feature'
+about: 'Create a request for a new feature'
+title: ''
+labels: 'meta:feature-request'
+assignees: ''
+
+---
+
+Before requesting a new feature make sure it hasn't been requested yet.
+https://github.com/mpv-player/mpv/labels/meta%3Afeature-request
+
+### Expected behavior of the wanted feature
+
+### Alternative behavior of the wanted feature
+
+### Log file
+
+Even if you think it's not necessary at first, it might help us later to find
+possible issues. Make a log file made with -v -v or --log-file=output.txt, paste
+it to https://0x0.st/ or attach it to the github issue, and replace this text
+with a link to it.
diff --git a/.github/ISSUE_TEMPLATE/6_question.md b/.github/ISSUE_TEMPLATE/6_question.md
new file mode 100644
index 0000000..b6131f3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/6_question.md
@@ -0,0 +1,25 @@
+---
+name: 'Ask a Question'
+about: 'Ask a question about mpv'
+title: ''
+labels: 'meta:question'
+assignees: ''
+
+---
+
+Don't ask questions about issues, errors or problems you have and instead open
+a proper issue with the right template from the previous selection.
+
+This template is meant for questions about the workings of mpv, to clarify about
+unspecified behaviour of options or command, or anything else that is not well
+described and needs clarification.
+
+Before asking a question make sure it hasn't been asked or answered yet.
+https://github.com/mpv-player/mpv/labels/meta%3Aquestion
+
+### Log file
+
+Even if you think it's not necessary at first, it might help us later to find
+possible issues. Make a log file made with -v -v or --log-file=output.txt, paste
+it to https://0x0.st/ or attach it to the github issue, and replace this text
+with a link to it.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..536ac9c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,9 @@
+blank_issues_enabled: false
+contact_links:
+ - name: mpv IRC channel
+ url: https://mpv.io/community/
+ about: Feel free to ask questions here irc://irc.libera.chat/mpv
+ - name: mpv IRC developer channel
+ url: https://mpv.io/community/
+ about: Ask questions related to the development of mpv here
+ irc://irc.libera.chat/mpv-devel
diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
new file mode 100644
index 0000000..6c721d5
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE
@@ -0,0 +1,5 @@
+Read this before you submit this pull request:
+https://github.com/mpv-player/mpv/blob/master/DOCS/contribute.md
+
+Reading this link and following the rules will get your pull request reviewed
+and merged faster. Nobody wants lazy pull requests. \ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..ddbfd90
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,292 @@
+name: build
+
+on:
+ push:
+ branches:
+ - master
+ - ci
+ - 'release/**'
+ paths-ignore:
+ - 'DOCS/**'
+ - 'TOOLS/lua/**'
+ pull_request:
+ branches: [master]
+ paths-ignore:
+ - 'DOCS/**'
+ - 'TOOLS/lua/**'
+
+jobs:
+ mingw:
+ runs-on: ubuntu-22.04
+ env:
+ CCACHE_BASEDIR: ${{ github.workspace }}
+ CCACHE_DIR: ${{ github.workspace }}/.ccache
+ CCACHE_MAXSIZE: 500M
+ strategy:
+ fail-fast: false
+ matrix:
+ target: [i686-w64-mingw32, x86_64-w64-mingw32]
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Get time
+ id: get_time
+ run: echo "timestamp=`date +%s%N`" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v3
+ with:
+ path: ${{ env.CCACHE_DIR }}
+ key: ${{ matrix.target }}-${{ steps.get_time.outputs.timestamp }}
+ restore-keys: ${{ matrix.target }}-
+
+ - name: Install dependencies
+ run: |
+ sudo dpkg --add-architecture i386
+ sudo apt-get update
+ sudo apt-get install -y autoconf automake pkg-config g++-mingw-w64 gcc-multilib python3-pip ninja-build nasm wine wine32 wine64 ccache
+ sudo python3 -m pip install meson
+
+ - name: Build libraries
+ run: |
+ ./ci/build-mingw64.sh
+ env:
+ TARGET: ${{ matrix.target }}
+
+ - name: Build with meson
+ id: build
+ run: |
+ ./ci/build-mingw64.sh meson pack
+ env:
+ TARGET: ${{ matrix.target }}
+
+ - name: Print meson log
+ if: ${{ failure() && steps.build.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/meson-log.txt
+
+ - name: Functional test
+ id: tests
+ run: |
+ cd artifact && wine64 ./mpv.com -v --no-config
+ env:
+ WINEDEBUG: '+loaddll'
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: mpv-${{ matrix.target }}
+ path: mpv-git-*.zip
+
+ macos:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ cc:
+ - "clang"
+ cxx:
+ - "clang++"
+ os:
+ - "macos-12"
+ - "macos-13"
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Remove stray upstream python binary symlinks under /usr/local
+ run: |
+ find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete -print
+ brew unlink python && brew link --overwrite python
+
+ - name: Install dependencies
+ run: |
+ brew update
+ brew install autoconf automake pkg-config libtool python freetype fribidi little-cms2 lua@5.1 libass ffmpeg meson libplacebo
+
+ - name: Build with meson
+ id: build
+ run: |
+ ./ci/build-macos.sh
+ env:
+ CC: "${{ matrix.cc }}"
+ CXX: "${{ matrix.cxx }}"
+ TRAVIS_OS_NAME: "${{ matrix.os }}"
+
+ - name: Print meson log
+ if: ${{ failure() && steps.build.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/meson-log.txt
+
+ - name: Run meson tests
+ id: tests
+ run: |
+ meson test -C build
+
+ - name: Print meson test log
+ if: ${{ failure() && steps.tests.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/testlog.txt
+
+ linux:
+ runs-on: "ubuntu-22.04"
+ container:
+ image: "registry.opensuse.org/home/mia/images/images/mpv-ci:stable-deps"
+ env:
+ CC: "${{ matrix.config.cc }}"
+ CXX: "${{ matrix.config.cxx }}"
+ strategy:
+ matrix:
+ config:
+ - {
+ cc: "gcc",
+ cxx: "g++",
+ }
+ - {
+ cc: "clang",
+ cxx: "clang++",
+ }
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install dependencies
+ run: |
+ # workaround to avoid "fatal: unsafe repository" error
+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
+
+ - name: Build with meson
+ id: build
+ run: |
+ ./ci/build-tumbleweed.sh
+
+ - name: Print meson log
+ if: ${{ failure() && steps.build.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/meson-log.txt
+
+ - name: Run meson tests
+ id: tests
+ run: |
+ meson test -C build
+
+ - name: Print meson test log
+ if: ${{ failure() && steps.tests.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/testlog.txt
+
+ freebsd:
+ runs-on: ubuntu-latest # until https://github.com/actions/runner/issues/385
+ timeout-minutes: 30 # avoid any weirdness with the VM
+ steps:
+ - uses: actions/checkout@v3
+ - name: Test in FreeBSD VM
+ uses: cross-platform-actions/action@v0.19.1
+ with:
+ operating_system: freebsd
+ version: '13.2'
+ run: |
+ sudo pkg update
+ sudo pkg install -y \
+ alsa-lib \
+ cmake \
+ evdev-proto \
+ ffmpeg \
+ git \
+ iconv \
+ jackit \
+ libarchive \
+ libbluray \
+ libcaca \
+ libcdio-paranoia \
+ libdvdnav \
+ libplacebo \
+ libXinerama \
+ libxkbcommon \
+ libxpresent \
+ libXv \
+ luajit \
+ meson \
+ mujs \
+ openal-soft \
+ pipewire \
+ pkgconf \
+ pulseaudio \
+ python3 \
+ rubberband \
+ sekrit-twc-zimg \
+ sdl2 \
+ sndio \
+ uchardet \
+ v4l_compat \
+ vulkan-headers \
+ wayland-protocols
+ ./ci/build-freebsd.sh
+ meson test -C build
+
+ msys2:
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sys:
+ - clang32
+ - clang64
+ - mingw32
+ - mingw64
+ - ucrt64
+ defaults:
+ run:
+ shell: msys2 {0}
+ steps:
+ - name: Disable autocrlf
+ shell: pwsh
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.eol lf
+ - uses: actions/checkout@v3
+ - uses: msys2/setup-msys2@v2
+ with:
+ msystem: ${{ matrix.sys }}
+ update: true
+ install: git
+ pacboy: >-
+ angleproject:p
+ ca-certificates:p
+ cc:p
+ diffutils:p
+ ffmpeg:p
+ lcms2:p
+ libarchive:p
+ libass:p
+ libcdio-paranoia:p
+ libdvdnav:p
+ libjpeg-turbo:p
+ libplacebo:p
+ lua51:p
+ meson:p
+ ninja:p
+ pkgconf:p
+ python:p
+ rst2pdf:p
+ rubberband:p
+ shaderc:p
+ spirv-cross:p
+ uchardet:p
+ vapoursynth:p
+ vulkan:p
+
+ - name: Build with meson
+ id: build
+ run: |
+ ./ci/build-msys2.sh meson
+
+ - name: Print meson log
+ if: ${{ failure() && steps.build.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/meson-log.txt
+
+ - name: Run meson tests
+ id: tests
+ run: |
+ meson test -C build
+
+ - name: Print meson test log
+ if: ${{ failure() && steps.tests.outcome == 'failure' }}
+ run: |
+ cat ./build/meson-logs/testlog.txt
diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml
new file mode 100644
index 0000000..7f9603a
--- /dev/null
+++ b/.github/workflows/comment.yml
@@ -0,0 +1,58 @@
+name: comment
+
+on:
+ workflow_run:
+ workflows: ["build"]
+ types: [completed]
+
+jobs:
+ pr_comment:
+ if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/github-script@v6
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const run_id = ${{ github.event.workflow_run.id }};
+ const pull_head_sha = '${{github.event.workflow_run.head_sha}}';
+
+ const issue_number = await(async () => {
+ const pulls = await github.rest.pulls.list({ owner, repo });
+ for await (const { data } of github.paginate.iterator(pulls)) {
+ for (const pull of data) {
+ if (pull.head.sha === pull_head_sha) {
+ return pull.number;
+ }
+ }
+ }
+ })();
+ if (issue_number) {
+ core.info(`Using pull request ${issue_number}`);
+ } else {
+ return core.error(`No matching pull request found`);
+ }
+
+ const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id });
+ if (!artifacts.length) {
+ return core.error(`No artifacts found`);
+ }
+
+ let body = `Download the artifacts for this pull request:\n\n<details><summary>Windows</summary>\n`;
+ for (const art of artifacts) {
+ const art_link = `https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip`;
+ if (art.name.includes('w64')) {
+ body += `\n* [${art.name}](${art_link})`;
+ }
+ }
+ body += `\n</details>`;
+
+ const { data: comments } = await github.rest.issues.listComments({ repo, owner, issue_number });
+ const existing_comment = comments.find((c) => c.user.login === 'github-actions[bot]');
+ if (existing_comment) {
+ core.info(`Updating comment ${existing_comment.id}`);
+ await github.rest.issues.updateComment({ repo, owner, comment_id: existing_comment.id, body });
+ } else {
+ core.info(`Creating a comment`);
+ await github.rest.issues.createComment({ repo, owner, issue_number, body });
+ }
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..d1fcb5c
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,24 @@
+name: docs
+
+on:
+ push:
+ branches:
+ - master
+ - ci
+ - 'release/**'
+ paths:
+ - 'DOCS/**'
+ pull_request:
+ branches: [master]
+ paths:
+ - 'DOCS/**'
+
+jobs:
+ check-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Docs
+ run: |
+ sudo apt-get install python3-docutils
+ rst2man --strip-elements-with-class=contents --halt=2 ./DOCS/man/mpv.rst mpv.1
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..939255e
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,22 @@
+name: lint
+
+on:
+ push:
+ branches:
+ - master
+ - ci
+ - 'release/**'
+ pull_request:
+ branches: [master]
+
+jobs:
+ commit-msg:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 50
+
+ - name: Lint
+ run: |
+ ./ci/lint-commit-msg.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b805192
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/subprojects/*
+!/subprojects/packagefiles
+!/subprojects/*.wrap
diff --git a/Copyright b/Copyright
new file mode 100644
index 0000000..79f718b
--- /dev/null
+++ b/Copyright
@@ -0,0 +1,74 @@
+mpv is a fork of mplayer2, which is a fork of MPlayer.
+
+mpv as a whole is licensed under the GNU General Public License GPL version 2
+or later (called GPLv2+ in this document, see LICENSE.GPL for full license
+text) by default, or the GNU Lesser General Public License LGPL version 2 or
+later (LGPLv2.1+ in this document, see LICENSE.LGPL for full license text) if
+built with the -Dgpl=false configure switch.
+
+Most source files are LGPLv2.1+ or GPLv2+, but some files are available under
+more liberal licenses, such as BSD, MIT, ISC, and possibly others. Look at the
+copyright header of each source file, and grep the sources for "Copyright" if
+you need to know details. C source files without Copyright notice are usually
+licensed as LGPLv2.1+. Also see the list of files with specific licenses below
+(not all files can have a standard license header).
+
+All new contributions must be LGPLv2.1+ licensed. Using a more liberal license
+compatible to LGPLv2.1+ is also ok.
+
+Changes done to GPL code must come with the implicit/explicit agreement that the
+project can relicense the changes to LGPLv2.1+ at a later point without asking
+the contributor. This is a safeguard for making potential relicensing of
+remaining GPL code to LGPLv2.1+ easier.
+
+See DOCS/contribute.md for binding rules wrt. licensing for contributions.
+
+For information about authors and contributors, consult the git log, which
+contains the complete SVN and CVS history as well.
+
+"v2.1+" in this context means "version 2.1 or later".
+
+Some libraries are GPLv2+ or GPLv3+ only. Building mpv with Samba support makes
+it GPLv3+.
+
+mpv can be built as LGPLv2.1+ with the -Dgpl=false configure option. To add a
+LGPL mode to mpv, MPlayer code had to be relicensed from GPLv2+ to LGPLv2.1+ by
+asking the MPlayer authors for permission. Since permission could not be
+obtained from everyone, LGPL mode disables the following features, some of them
+quite central:
+- Linux X11 video output
+- BSD audio output via OSS
+- NVIDIA/Linux hardware decoding (vdpau, although nvdec usually works)
+- Linux TV input
+- minor features: jack, DVD, CDDA, SMB, CACA, legacy direct3d VO
+Some of these will be fixed in the future. The intended use for LGPL mode is
+with libmpv, and currently it's not recommended to build mpv CLI in LGPL mode
+at all.
+
+The following files are still GPL only (-Dgpl=false disables them):
+
+ audio/out/ao_jack.c will stay GPL
+ audio/out/ao_oss.c will stay GPL
+ stream/dvb* must stay GPL
+ stream/stream_cdda.c unknown
+ stream/stream_dvb.* must stay GPL
+ stream/stream_dvdnav.c unknown
+ video/out/vo_caca.c unknown
+ video/out/vo_direct3d.c unknown
+ video/out/vo_vaapi.c probably impossible (some company's code)
+ video/out/vo_vdpau.c probably impossible (nVidia's code)
+ video/out/vo_x11.c probably impossible
+ video/out/vo_xv.c probably impossible
+ video/out/x11_common.* probably impossible
+ video/vdpau.c hard (GPL-only parts must be ifdefed)
+ video/vdpau.h unknown
+ video/vdpau_mixer.* actual code must be rewritten
+ DOCS/man/ GPLv2+
+ bootstrap.py unknown license, probably GPLv2+ or LGPLv2+
+ etc/mplayer-input.conf unknown license, probably GPLv2+
+ mpv.desktop unknown license, probably GPLv2+
+ etc/restore-old-bindings.conf unknown license, probably GPLv2+
+
+None of the cases listed above affect the final binary if it's built as
+LGPL. Linked libraries still can affect the final license (for example if
+FFmpeg was built as GPL).
diff --git a/DOCS/client-api-changes.rst b/DOCS/client-api-changes.rst
new file mode 100644
index 0000000..f092647
--- /dev/null
+++ b/DOCS/client-api-changes.rst
@@ -0,0 +1,279 @@
+Introduction
+============
+
+This file lists all changes that can cause compatibility issues when using
+mpv through the client API (libmpv and ``client.h``). Since the client API
+interfaces to input handling (commands, properties) as well as command line
+options, you should also look at ``interface-changes.rst``.
+
+Normally, changes to the C API that are incompatible to previous iterations
+receive a major version bump (i.e. the first version number is increased),
+while C API additions bump the minor version (i.e. the second number is
+increased). Changes to properties/commands/options may also lead to a minor
+version bump, in particular if they are incompatible.
+
+The version number is the same as used for MPV_CLIENT_API_VERSION (see
+``client.h`` how to convert between major/minor version numbers and the flat
+32 bit integer).
+
+Also, read the section ``Compatibility`` in ``client.h``, and compatibility.rst.
+
+Options, commands, properties
+=============================
+
+Changes to these are not listed here, but in ``interface-changes.rst``. (Before
+client API version 1.17, they were listed here partially.)
+
+This listing includes changes to the bare C API and behavior only, not what
+you can access with them.
+
+API changes
+===========
+
+::
+
+ --- mpv 0.37.0 ---
+ 2.2 - add mpv_time_ns()
+ --- mpv 0.36.0 ---
+ 2.1 - add mpv_del_property()
+ --- mpv 0.35.0 ---
+ 2.0 - remove headers/functions of the obsolete opengl_cb API
+ - remove mpv_opengl_init_params.extra_exts field
+ - remove deprecated mpv_detach_destroy. Use mpv_destroy instead.
+ - remove obsolete mpv_suspend and mpv_resume
+ - remove deprecated SCRIPT_INPUT_DISPATCH, PAUSE and UNPAUSE, TRACKS_CHANGED
+ TRACK_SWITCHED, METADATA_UPDATE, CHAPTER_CHANGE events
+ --- mpv 0.33.0 ---
+ 1.109 - add MPV_RENDER_API_TYPE_SW and related (software rendering API)
+ - inactivate the opengl_cb API (always fails to initialize now)
+ The opengl_cb API was deprecated over 2 years ago. Use the render API
+ instead.
+ 1.108 - Deprecate MPV_EVENT_IDLE
+ - add mpv_event_start_file
+ - add the following fields to mpv_event_end_file: playlist_entry_id,
+ playlist_insert_id, playlist_insert_num_entries
+ - add mpv_event_to_node()
+ - add mpv_client_id()
+ 1.107 - Remove the deprecated qthelper.hpp. This was obviously not part of the
+ libmpv API, only an "additionally" provided helper, thus this is not
+ considered an API change. If you are maintaining a project that relies
+ on this header, you can simply download this file and adjust the
+ include statement to use it instead:
+
+ https://raw.githubusercontent.com/mpv-player/mpv/v0.32.0/libmpv/qthelper.hpp
+
+ It is a good idea to write better wrappers for your use, though.
+ --- mpv 0.31.0 ---
+ 1.107 - Deprecate MPV_EVENT_TICK
+ --- mpv 0.30.0 ---
+ 1.106 - Add cancel_fn to mpv_stream_cb_info
+ 1.105 - Fix deadlock problems with MPV_RENDER_PARAM_ADVANCED_CONTROL and if
+ the "vd-lavc-dr" option is enabled (which it is by default).
+ There were no actual API changes.
+ API users on older API versions and mpv releases should set
+ "vd-lavc-dr" to "no" to avoid these issues.
+ API users must still adhere to the tricky rules documented in render.h
+ to avoid other deadlocks.
+ 1.104 - Deprecate struct mpv_opengl_drm_params. Replaced by mpv_opengl_drm_params_v2
+ - Deprecate MPV_RENDER_PARAM_DRM_DISPLAY. Replaced by MPV_RENDER_PARAM_DRM_DISPLAY_V2.
+ 1.103 - redo handling of async commands
+ - add mpv_event_command and make it possible to return values from
+ commands issued with mpv_command_async() or mpv_command_node_async()
+ - add mpv_abort_async_command()
+ 1.102 - rename struct mpv_opengl_drm_osd_size to mpv_opengl_drm_draw_surface_size
+ - rename MPV_RENDER_PARAM_DRM_OSD_SIZE to MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE
+ --- mpv 0.29.0 ---
+ 1.101 - add MPV_RENDER_PARAM_ADVANCED_CONTROL and related API
+ - add MPV_RENDER_PARAM_NEXT_FRAME_INFO and related symbols
+ - add MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME
+ - add MPV_RENDER_PARAM_SKIP_RENDERING
+ - add mpv_render_context_get_info()
+ 1.100 - bump API number to avoid confusion with mpv release versions
+ - actually apply the GL_MP_MPGetNativeDisplay change for the new render
+ API. This also means compatibility for anything but x11 and wayland
+ through the old opengl-cb GL_MP_MPGetNativeDisplay method is now
+ unsupported.
+ - deprecate mpv_get_wakeup_pipe(). It's complex, but easy to replace
+ using normal API (just set a wakeup callback to a function which
+ writes to a pipe).
+ - add a 1st class hook API, which replaces the hacky mpv_command()
+ based one. The old API is deprecated and will be removed soon. The
+ old API was never meant to be stable, while the new API is.
+ 1.29 - the behavior of mpv_terminate_destroy() and mpv_detach_destroy()
+ changes subtly (see documentation in the header file). In particular,
+ mpv_detach_destroy() will not leave the player running in all
+ situations anymore (it gets closer to refcounting).
+ - rename mpv_detach_destroy() to mpv_destroy() (the old function will
+ remain valid as deprecated alias)
+ - add mpv_create_weak_client(), which makes use of above changes
+ - MPV_EVENT_SHUTDOWN is now returned exactly once if a mpv_handle
+ should terminate, instead of spamming the event queue with this event
+ 1.28 - deprecate the render opengl_cb API, and replace it with render.h
+ and render_gl.h. The goal is allowing support for APIs other than
+ OpenGL. The old API is emulated with the new API.
+ Likewise, the "opengl-cb" VO is renamed to "libmpv".
+ mpv_get_sub_api() is deprecated along the opengl_cb API.
+ The new API is relatively similar, but not the same. The rough
+ equivalents are:
+ mpv_opengl_cb_init_gl => mpv_render_context_create
+ mpv_opengl_cb_set_update_callback => mpv_render_context_set_update_callback
+ mpv_opengl_cb_draw => mpv_render_context_render
+ mpv_opengl_cb_report_flip => mpv_render_context_report_swap
+ mpv_opengl_cb_uninit_gl => mpv_render_context_free
+ The VO opengl-cb is also renamed to "libmpv".
+ Also, the GL_MP_MPGetNativeDisplay pseudo extension is not used by the
+ render API anymore, and the old opengl-cb API only handles the "x11"
+ and "wl" names anymore. Support for everything else has been removed.
+ The new render API uses proper API parameters, e.g. for X11 you pass
+ MPV_RENDER_PARAM_X11_DISPLAY directly.
+ - deprecate the qthelper.hpp header file. This provided some C++ helper
+ utility functions for Qt with use of libmpv. There is no reason to
+ keep this in the mpv git repository, nor to make it part of the libmpv
+ API. If you're using this header, you can safely copy it into your
+ project - it uses only libmpv public API. Alternatively, it could be
+ maintained in a separate repository by interested parties.
+ 1.27 - make opengl-cb the default VO. This causes a subtle behavior change
+ if the API user called mpv_opengl_cb_init_gl(), but does not set
+ the "vo" option. Before, it would still have used another VO (like
+ on the CLI, e.g. vo=gpu). Now it'll behave as if vo=opengl-cb was
+ used.
+ --- mpv 0.28.0 ---
+ 1.26 - remove glMPGetNativeDisplay("drm") support
+ - add mpv_opengl_cb_window_pos and mpv_opengl_cb_drm_params and
+ support via glMPGetNativeDisplay() for using it
+ - make --stop-playback-on-init-failure=no the default in libmpv (just
+ like in mpv CLI)
+ --- mpv 0.27.0 ---
+ 1.25 - remove setting "no-" options via mpv_set_option*(). (See corresponding
+ deprecation in 0.23.0.)
+ --- mpv 0.25.0 ---
+ 1.24 - add a MPV_ENABLE_DEPRECATED preprocessor symbol, which can be defined
+ by the user to exclude deprecated API symbols from the C headers
+ --- mpv 0.23.0 ---
+ 1.24 - the deprecated mpv_suspend() and mpv_resume() APIs now do nothing.
+ --- mpv 0.22.0 ---
+ 1.23 - deprecate setting "no-" options via mpv_set_option*(). For example,
+ instead of "no-video=" you should set "video=no".
+ - do not override the SIGPIPE signal handler anymore. This was done as
+ workaround for the FFmpeg TLS code, which has been fixed long ago.
+ - deprecate mpv_suspend() and mpv_resume(). They will be stubbed out
+ in mpv 0.23.0.
+ - make mpv_set_property() work to some degree before mpv_initialize().
+ It can now be used instead of mpv_set_option().
+ - semi-deprecate mpv_set_option()/mpv_set_option_string(). You should
+ use mpv_set_property() instead. There are some deprecated properties
+ which conflict with some options (see client.h remarks on
+ mpv_set_option()), for which mpv_set_option() might still be required.
+ In future mpv releases, the conflicting deprecated options/properties
+ will be removed, and mpv_set_option() will internally translate API
+ calls to mpv_set_property().
+ - qthelper.hpp: deprecate get_property_variant, set_property_variant,
+ set_option_variant, command_variant, and replace them with
+ get_property, set_property, command.
+ --- mpv 0.19.0 ---
+ 1.22 - add stream_cb API for custom protocols
+ --- mpv 0.18.1 ---
+ ---- - remove "status" log level from mpv_request_log_messages() docs. This
+ is 100% equivalent to "v". The behavior is still the same, thus no
+ actual API change.
+ --- mpv 0.18.0 ---
+ 1.21 - mpv_set_property() changes behavior with MPV_FORMAT_NODE. Before this
+ change it rejected mpv_nodes with format==MPV_FORMAT_STRING if the
+ property was not a string or did not have special mechanisms in place
+ the function failed. Now it always invokes the option string parser,
+ and mpv_node with a basic data type works exactly as if the function
+ is invoked with that type directly. This new behavior is equivalent
+ to mpv_set_option().
+ This also affects the mp.set_property_native() Lua function.
+ - generally, setting choice options/properties with "yes"/"no" options
+ can now be set as MPV_FORMAT_FLAG
+ - reading a choice property as MPV_FORMAT_NODE will now return a
+ MPV_FORMAT_FLAG value if the choice is "yes" (true) or "no" (false)
+ This implicitly affects Lua and JSON IPC interfaces as well.
+ - big changes to vo-cmdline on vo_opengl and vo_opengl_hq (but not
+ vo_opengl_cb): options are now normally not reset, but applied on top
+ of the current options. The special undocumented value "-" still
+ works, but now resets all options to before any vo-cmdline command
+ has been called.
+ --- mpv 0.12.0 ---
+ 1.20 - deprecate "GL_MP_D3D_interfaces"/"glMPGetD3DInterface", and introduce
+ "GL_MP_MPGetNativeDisplay"/"glMPGetNativeDisplay" (this is a
+ backwards-compatible rename)
+ --- mpv 0.11.0 ---
+ --- mpv 0.10.0 ---
+ 1.19 - add "GL_MP_D3D_interfaces" pseudo extension to make it possible to
+ use DXVA2 in OpenGL fullscreen mode in some situations
+ - mpv_request_log_messages() now accepts "terminal-default" as parameter
+ 1.18 - add MPV_END_FILE_REASON_REDIRECT, and change behavior of
+ MPV_EVENT_END_FILE accordingly
+ - a bunch of interface-changes.rst changes
+ 1.17 - mpv_initialize() now blocks SIGPIPE (details see client.h)
+ --- mpv 0.9.0 ---
+ 1.16 - add mpv_opengl_cb_report_flip()
+ - introduce mpv_opengl_cb_draw() and deprecate mpv_opengl_cb_render()
+ - add MPV_FORMAT_BYTE_ARRAY
+ 1.15 - mpv_initialize() will now load config files. This requires setting
+ the "config" and "config-dir" options. In particular, it will load
+ mpv.conf.
+ - minor backwards-compatible change to the "seek" and "screenshot"
+ commands (new flag syntax, old additional args deprecated)
+ --- mpv 0.8.0 ---
+ 1.14 - add mpv_wait_async_requests()
+ - the --msg-level option changes its native type from a flat string to
+ a key-value list (setting/reading the option as string still works)
+ 1.13 - add MPV_EVENT_QUEUE_OVERFLOW
+ 1.12 - add class Handle to qthelper.hpp
+ - improve opengl_cb.h API uninitialization behavior, and fix the qml
+ example
+ - add mpv_create_client() function
+ 1.11 - add OpenGL rendering interop API - allows an application to combine
+ its own and mpv's OpenGL rendering
+ Warning: this API is not stable yet - anything in opengl_cb.h might
+ be changed in completely incompatible ways in minor API bumps
+ --- mpv 0.7.0 ---
+ 1.10 - deprecate/disable everything directly related to script_dispatch
+ (most likely affects nobody)
+ 1.9 - add enum mpv_end_file_reason for mpv_event_end_file.reason
+ - add MPV_END_FILE_REASON_ERROR and the mpv_event_end_file.error field
+ for slightly better error reporting on playback failure
+ - add --stop-playback-on-init-failure option, and make it the default
+ behavior for libmpv only
+ - add qthelper.hpp set_option_variant()
+ - mark the following events as deprecated:
+ MPV_EVENT_TRACKS_CHANGED
+ MPV_EVENT_TRACK_SWITCHED
+ MPV_EVENT_PAUSE
+ MPV_EVENT_UNPAUSE
+ MPV_EVENT_METADATA_UPDATE
+ MPV_EVENT_CHAPTER_CHANGE
+ They are handled better with mpv_observe_property() as mentioned in
+ the documentation comments. They are not removed and still work.
+ 1.8 - add qthelper.hpp
+ 1.7 - add mpv_command_node(), mpv_command_node_async()
+ 1.6 - modify "core-idle" property behavior
+ - MPV_EVENT_LOG_MESSAGE now always sends complete lines
+ - introduce numeric log levels (mpv_log_level)
+ --- mpv 0.6.0 ---
+ 1.5 - change in X11 and "--wid" behavior again. The previous change didn't
+ work as expected, and now the behavior can be explicitly controlled
+ with the "input-x11-keyboard" option. This is only a temporary
+ measure until XEmbed is implemented and confirmed working.
+ Note: in 1.6, "input-x11-keyboard" was renamed to "input-vo-keyboard",
+ although the old option name still works.
+ 1.4 - subtle change in X11 and "--wid" behavior
+ (this change was added to 0.5.2, and broke some things, see #1090)
+ --- mpv 0.5.0 ---
+ 1.3 - add MPV_MAKE_VERSION()
+ 1.2 - remove "stream-time-pos" property (no replacement)
+ 1.1 - remap dvdnav:// to dvd://
+ - add "--cache-file", "--cache-file-size"
+ - add "--colormatrix-primaries" (and property)
+ - add "primaries" sub-field to image format properties
+ - add "playback-time" property
+ - extend the "--start" option; a leading "+", which was previously
+ insignificant is now significant
+ - add "cache-free" and "cache-used" properties
+ - OSX: the "coreaudio" AO spdif code is split into a separate AO
+ --- mpv 0.4.0 ---
+ 1.0 - the API is declared stable
+
diff --git a/DOCS/compatibility.rst b/DOCS/compatibility.rst
new file mode 100644
index 0000000..3d6ec2c
--- /dev/null
+++ b/DOCS/compatibility.rst
@@ -0,0 +1,177 @@
+CLI and API compatibility policy
+================================
+
+Human users and API users rely on the mpv/libmpv/scripting/IPC interface not
+breaking their use case. On the other hand, active development occasionally
+requires breaking things, such as removing old options or changing options in
+a way that may break.
+
+This document lists rules when, what, and how incompatible changes can be made.
+It's interesting both for mpv developers, who want to change user-visible parts,
+and mpv users, who want to know what is guaranteed to remain stable.
+
+Any of the rules below may be overridden by statements in more specific
+documentation (for example, if the manpage says that a particular option may be
+removed any time, it means that, and the option probably won't even go through
+deprecation).
+
+Additions
+---------
+
+Additions are basically always allowed. API users etc. are supposed to deal with
+the possibility that for example new API functions are added. Some parts of the
+API may document how they are extended.
+
+Options, commands, properties, events, hooks (command interface)
+----------------------------------------------------------------
+
+All of these are important for interfacing both with end users and API users
+(which include Lua scripts, libmpv, and the JSON IPC). As such, they constitute
+a large part of the user interface and APIs.
+
+All incompatible changes to this must be declared in interface-changes.rst.
+(This may also list compatible additions, but it's not a requirement.)
+
+Degrees of importance and compatibility preservation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Critical and central parts of the command interface have the strictest
+requirements. It may not be reasonable to break them, and other means to achieve
+some change have to be found. For example, the "seek" command is a bit of a
+mess, but since changing it would likely affect almost every user, it may be
+impossible to break at least the commonly used syntax. If changed anyway, there
+should be a deprecation period of at least 1 year, during which the command
+still works, and possibly a warning should remain even after this.
+
+Important/often used parts must be deprecated for at least 2 releases before
+they can be broken. There must be at least 1 release where the deprecated
+functionality still works, and a replacement is available (if technically
+reasonable). For example, a feature deprecated in mpv 0.30.0 may be removed in
+mpv 0.32.0. Minor releases do not count towards this.
+
+Less useful parts can be broken immediately, but must come with some sort of
+removal warning-
+
+Parts for debugging and testing may be removed any time, potentially even
+without any sort of documentation.
+
+Currently, the importance of a part is not documented and not even well-defined,
+which is probably a mistake.
+
+Renaming or removing options
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Typically, renaming an option can be done in a compatible way with OPT_REPLACED.
+You may need to check whether the corresponding properties still work (including
+messy details like observing properties for changes).
+
+OPT_REMOVED can be used to inform the user of alternatives or reasons for the
+removal, which is better than an option not found error. Likewise,
+m_option.deprecation_message should be set to something helpful.
+
+Both OPT_REPLACED and OPT_REMOVED can remain in the code for a long time, since
+they're unintrusive and hopefully make incompatible changes less painful for
+users.
+
+Scripting APIs
+--------------
+
+This affects internal scripting APIs (currently Lua and JavaScript).
+
+Vaguely the same rules as with the command interface apply. Since there is a
+large number of scripts, an effort should be made to provide compatibility
+for old scripts, but it does not need to be stronger than that of the command
+interface.
+
+Undocumented parts of the scripting APIs are _not_ guaranteed for compatibility.
+This applies especially for internals. Languages like Lua do not have strict
+access control (nor does the mpv code try to emulate any), so if a script
+accesses private parts, and breaks on the next mpv release, it's not mpv's
+problem.
+
+JSON IPC
+--------
+
+The JSON IPC is a thin protocol wrapping the libmpv API and the command
+interface. Compatibility-wise, it's about the same as the scripting APIs.
+The JSON protocol commands should remain as compatible as possible, and it
+should probably accept the current way commands are delimited (line breaks)
+forever.
+
+The protocol may accept non-standard JSON extensions, but only standard JSON
+(possibly with restrictions) is guaranteed for compatibility. Clients which want
+to remain compatible should not use any extensions.
+
+CLI
+---
+
+Things such as default key bindings do not necessarily require compatibility.
+However, the release notes should be extremely clear on changes to "important"
+key bindings. Bindings which restore the old behavior should be added to
+restore-old-bindings.conf.
+
+Some option parsing is CLI-only and not available from libmpv or scripting. No
+compatibility guarantees come with them. However, the rules which mpv uses to
+distinguish between options and filenames must remain consistent (if the
+non-deprecated options syntax is used).
+
+Terminal and log output
+-----------------------
+
+There are no compatibility guarantees for the terminal output, or the text
+logged via ``MPV_EVENT_LOG_MESSAGE`` and similar APIs. In particular, scripts
+invoking mpv CLI are extremely discouraged from trying to parse text output,
+and should use other mechanisms such as the JSON IPC.
+
+Protocols, filters, demuxers, etc.
+----------------------------------
+
+Which of these are present is generally not guaranteed, and can even depend
+on compile time settings.
+
+The filter list and their sub-options are considered part of the
+command-interface.
+
+libmpv C API
+------------
+
+The libmpv client API (such as ``<libmpv/client.h>``) mostly gives access to
+the command interface. The API itself (if looked at as a component separate
+from the command interface) is intended to be extremely stable.
+
+All API changes are documented in client-api-changes.rst.
+
+API compatibility
+^^^^^^^^^^^^^^^^^
+
+The API is *always* compatible. Incompatible changes are only allowed on major
+API version changes (see ``MPV_CLIENT_API_VERSION``). A major version change is
+an extremely rare event, which means usually no API symbols are ever removed.
+
+Essentially removing API functions by making them always return an error, or
+making it do nothing is allowed in cases where it is unlikely to break most
+clients, but requires a deprecation period of 2 releases. (This has happened to
+``mpv_suspend()`` for example.)
+
+API symbols can be deprecated. This should be clearly marked in the doxygen
+with ``@deprecated``, and if possible, the affected API symbols should not be
+visible if the API user defines ``MPV_ENABLE_DEPRECATED`` to 0.
+
+ABI compatibility
+^^^^^^^^^^^^^^^^^
+
+The ABI must never be broken, except on major API version changes. For example,
+constants don't change their values.
+
+Structs are tricky. If a struct can be allocated by a user (such as ``mpv_node``),
+no fields can be added. (Unless it's an union, and the addition does not change
+the offset or alignment of any of the fields or the struct itself. This has
+happened to ``mpv_node`` in the past.) If a struct is allocated by libmpv only,
+new fields can be appended to the end (for example ``mpv_event``).
+
+The ABI is only backward compatible. This means if a host application is linked
+to an older libmpv, and libmpv is updated to a newer version, it will still
+work (as in not causing any undefined behavior).
+
+Forward compatibility (an application would work with an older libmpv than it
+was linked to) is not required.
diff --git a/DOCS/compile-windows.md b/DOCS/compile-windows.md
new file mode 100644
index 0000000..04bc200
--- /dev/null
+++ b/DOCS/compile-windows.md
@@ -0,0 +1,214 @@
+Compiling for Windows
+=====================
+
+Compiling for Windows is supported with MinGW-w64. This can be used to produce
+both 32-bit and 64-bit executables, and it works for building on Windows and
+cross-compiling from Linux and Cygwin. MinGW-w64 is available from:
+https://www.mingw-w64.org/
+
+While building a complete MinGW-w64 toolchain yourself is possible, there are a
+few build environments and scripts to help ease the process, such as MSYS2 and
+MXE. Note that MinGW environments included in Linux distributions are often
+broken, outdated and useless, and usually don't use MinGW-w64.
+
+**Warning**: the original MinGW (https://osdn.net/projects/mingw/) is unsupported.
+
+Cross-compilation
+=================
+
+When cross-compiling, it is recommended to use a meson crossfile to setup
+the cross compiling environment. A minimal example is included below:
+
+```ini
+[binaries]
+c = 'x86_64-w64-mingw32-gcc'
+cpp = 'x86_64-w64-mingw32-g++'
+ar = 'x86_64-w64-mingw32-ar'
+strip = 'x86_64-w64-mingw32-strip'
+exe_wrapper = 'wine64'
+
+[host_machine]
+system = 'windows'
+cpu_family = 'x86_64'
+cpu = 'x86_64'
+endian = 'little'
+```
+
+See [meson's documentation](https://mesonbuild.com/Cross-compilation.html) for
+more information.
+
+[MXE](https://mxe.cc) makes it very easy to bootstrap a complete MingGW-w64
+environment from a Linux machine. See a working example below.
+
+Alternatively, you can try [mpv-winbuild-cmake](https://github.com/shinchiro/mpv-winbuild-cmake),
+which bootstraps a MinGW-w64 environment and builds mpv and dependencies.
+
+Example with MXE
+----------------
+
+```bash
+# Before starting, make sure you install MXE prerequisites. MXE will download
+# and build all target dependencies, but no host dependencies. For example,
+# you need a working compiler, or MXE can't build the crosscompiler.
+#
+# Refer to
+#
+# https://mxe.cc/#requirements
+#
+# Scroll down for disto/OS-specific instructions to install them.
+
+# Download MXE. Note that compiling the required packages requires about 1.4 GB
+# or more!
+
+cd /opt
+git clone https://github.com/mxe/mxe mxe
+cd mxe
+
+# Set build options.
+
+# The JOBS environment variable controls threads to use when building. DO NOT
+# use the regular `make -j4` option with MXE as it will slow down the build.
+# Alternatively, you can set this in the make command by appending "JOBS=4"
+# to the end of command:
+echo "JOBS := 4" >> settings.mk
+
+# The MXE_TARGET environment variable builds MinGW-w64 for 32 bit targets.
+# Alternatively, you can specify this in the make command by appending
+# "MXE_TARGETS=i686-w64-mingw32" to the end of command:
+echo "MXE_TARGETS := i686-w64-mingw32.static" >> settings.mk
+
+# If you want to build 64 bit version, use this:
+# echo "MXE_TARGETS := x86_64-w64-mingw32.static" >> settings.mk
+
+# Build required packages. The following provide a minimum required to build
+# a reasonable mpv binary (though not an absolute minimum).
+
+make gcc ffmpeg libass jpeg lua luajit
+
+# Add MXE binaries to $PATH
+export PATH=/opt/mxe/usr/bin/:$PATH
+
+# Build mpv. The target will be used to automatically select the name of the
+# build tools involved (e.g. it will use i686-w64-mingw32.static-gcc).
+
+cd ..
+git clone https://github.com/mpv-player/mpv.git
+cd mpv
+meson setup build --crossfile crossfile
+meson compile -C build
+```
+
+Native compilation with MSYS2
+=============================
+
+For Windows developers looking to get started quickly, MSYS2 can be used to
+compile mpv natively on a Windows machine. The MSYS2 repositories have binary
+packages for most of mpv's dependencies, so the process should only involve
+building mpv itself.
+
+To build 64-bit mpv on Windows:
+
+Installing MSYS2
+----------------
+
+1. Download an installer from https://www.msys2.org/
+
+ Both the i686 and the x86_64 version of MSYS2 can build 32-bit and 64-bit
+ mpv binaries when running on a 64-bit version of Windows, but the x86_64
+ version is preferred since the larger address space makes it less prone to
+ fork() errors.
+
+2. Start a MinGW-w64 shell (``mingw64.exe``). **Note:** This is different from
+ the MSYS2 shell that is started from the final installation dialog. You must
+ close that shell and open a new one.
+
+ For a 32-bit build, use ``mingw32.exe``.
+
+Updating MSYS2
+--------------
+
+To prevent errors during post-install, the MSYS2 core runtime must be updated
+separately.
+
+```bash
+# Check for core updates. If instructed, close the shell window and reopen it
+# before continuing.
+pacman -Syu
+
+# Update everything else
+pacman -Su
+```
+
+Installing mpv dependencies
+---------------------------
+
+```bash
+# Install MSYS2 build dependencies and a MinGW-w64 compiler
+pacman -S git $MINGW_PACKAGE_PREFIX-{python,pkgconf,gcc,meson}
+
+# Install the most important MinGW-w64 dependencies. libass and lcms2 are also
+# pulled in as dependencies of ffmpeg.
+pacman -S $MINGW_PACKAGE_PREFIX-{ffmpeg,libjpeg-turbo,luajit}
+```
+
+Building mpv
+------------
+
+Finally, compile and install mpv. Binaries will be installed to
+``/mingw64/bin`` or ``/mingw32/bin``.
+
+```bash
+meson setup build --prefix=$MSYSTEM_PREFIX
+meson compile -C build
+```
+
+Or, compile and install both libmpv and mpv:
+
+```bash
+meson setup build -Dlibmpv=true --prefix=$MSYSTEM_PREFIX
+meson compile -C build
+meson install -C build
+```
+
+Linking libmpv with MSVC programs
+---------------------------------
+
+mpv/libmpv cannot be built with Visual Studio (Microsoft is too incompetent to
+support C99/C11 properly and/or hates open source and Linux too much to
+seriously do it). But you can build C++ programs in Visual Studio and link them
+with a libmpv built with MinGW.
+
+To do this, you need a Visual Studio which supports ``stdint.h`` (recent ones do),
+and you need to create a import library for the mpv DLL:
+
+```bash
+lib /name:mpv-1.dll /out:mpv.lib /MACHINE:X64
+```
+
+The string in the ``/name:`` parameter must match the filename of the DLL (this
+is simply the filename the MSVC linker will use).
+
+Static linking is not possible.
+
+Running mpv
+-----------
+
+If you want to run mpv from the MinGW-w64 shell, you will find the experience
+much more pleasant if you use the ``winpty`` utility
+
+```bash
+pacman -S winpty
+winpty mpv.com ToS-4k-1920.mov
+```
+
+If you want to move / copy ``mpv.exe`` and ``mpv.com`` to somewhere other than
+``/mingw64/bin/`` for use outside the MinGW-w64 shell, they will still depend on
+DLLs in that folder. The simplest solution is to add ``C:\msys64\mingw64\bin``
+to the windows system ``%PATH%``. Beware though that this can cause problems or
+confusion in Cygwin if that is also installed on the machine.
+
+Use of the ANGLE OpenGL backend requires a copy of the D3D compiler DLL that
+matches the version of the D3D SDK that ANGLE was built with
+(``d3dcompiler_43.dll`` in case of MinGW-built ANGLE) in the path or in the
+same folder as mpv. It must be of the same architecture (x86_64 / i686) as the
+mpv you compiled.
diff --git a/DOCS/contribute.md b/DOCS/contribute.md
new file mode 100644
index 0000000..91422a7
--- /dev/null
+++ b/DOCS/contribute.md
@@ -0,0 +1,281 @@
+How to contribute
+=================
+
+General
+-------
+
+The main contact for mpv development is IRC, specifically #mpv
+and #mpv-devel on Libera.chat. Github is used for code review and
+long term discussions.
+
+Sending patches
+---------------
+
+- Make a github pull request, or send a link to a plaintext patch created with
+ ``git format-patch``.
+- Plain diffs posted as pastebins are not acceptable! (Especially if the http
+ link returns HTML.) They only cause extra work for everyone, because they lack
+ commit message and authorship information.
+- Never send patches to any of the developers email addresses.
+- If your changes are not supposed to be merged immediately, mark them as
+ "[RFC]" in the commit message or the pull request title.
+- Be sure to test your changes. If you didn't, please say so in the commit
+ message and the pull request text.
+
+Copyright of contributions
+--------------------------
+
+- The copyright belongs to contributors. The project is a collaborative work. By
+ sending your changes, you agree to license your contributions according to the
+ requirements of this project.
+- All new code must be LGPLv2.1+ licensed, or come with the implicit agreement
+ that it will be relicensed to LGPLv2.1+ later (see ``Copyright`` in the
+ repository root directory).
+- 100% compatible licenses are allowed too.
+- Changes in files with more liberal licenses (such as BSD, MIT, or ISC) are
+ assumed to be dual-licensed under LGPLv2.1+ and the license indicated in the
+ file header.
+- You must be either the exclusive author of the patch, or acknowledge all
+ authors involved in the commit message. If you take 3rd party code, authorship
+ and copyright must be properly acknowledged. If you're making changes on
+ behalf of your employer, and the employer owns the copyright, you must mention
+ this. If the license of the code is not LGPLv2.1+, you must mention this.
+- These license statements are legally binding.
+- Don't use fake names (something that looks like an actual name, and may be
+ someone else's name, but is not your legal name). Using a pseudonyms is
+ allowed if it can be used to identify or contact you, even if whatever
+ account you used to submit the patch dies.
+- Do not add your name to the license header. This convention is not used by
+ this project, and neither copyright law nor any of the used licenses require
+ it.
+
+Write good commit messages
+--------------------------
+
+- Write informative commit messages. Use present tense to describe the
+ situation with the patch applied, and past tense for the situation before
+ the change.
+- The subject line (the first line in a commit message) must contain a
+ prefix identifying the sub system, followed by a short description what
+ impact this commit has. This subject line and the commit message body
+ must not be longer than 72 characters per line, because it messes up the
+ output of many git tools otherwise.
+
+ For example, you fixed a crash in af_volume.c:
+
+ - Bad: ``fixed the bug (wtf?)``
+ - Good: ``af_volume: fix crash due to null pointer access``
+
+ Having a prefix gives context, and is especially useful when trying to find
+ a specific change by looking at the history, or when running ``git blame``.
+
+ Sample prefixes: ``vo_gpu: ...``, ``command: ...``, ``DOCS/input: ...``,
+ ``TOOLS/osxbundle: ...``, ``osc.lua: ...``, etc. You can always check the git
+ log for commits which modify specific files to see which prefixes are used.
+
+- The first word after the ``:`` is lower case.
+- Don't end the subject line with a ``.``.
+- Put an empty line between the subject line and the commit message.
+ If this is missing, it will break display in common git tools.
+- The body of the commit message (everything else after the subject line) must
+ be as informative as possible and contain everything that isn't obvious. Don't
+ hesitate to dump as much information as you can - it doesn't cost you
+ anything. Put some effort into it. If someone finds a bug months or years
+ later, and finds that it's caused by your commit (even though your commit was
+ supposed to fix another bug), it would be bad if there wasn't enough
+ information to test the original bug. The old bug might be reintroduced while
+ fixing the new bug.
+
+ The commit message must be wrapped on 72 characters per line, because git
+ tools usually do not break text automatically. On the other hand, you do not
+ need to break text that would be unnatural to break (like data for test cases,
+ or long URLs).
+- Another summary of good conventions: https://chris.beams.io/posts/git-commit/
+
+Split changes into multiple commits
+-----------------------------------
+
+- Follow git good practices, and split independent changes into several commits.
+ It's usually OK to put them into a single pull request.
+- Try to separate cosmetic and functional changes. It's ok to make a few
+ additional cosmetic changes in the same file you're working on. But don't do
+ something like reformatting a whole file, and hiding an actual functional
+ change in the same commit.
+- Splitting changes does _not_ mean that you should make them as fine-grained
+ as possible. Commits should form logical steps in development. The way you
+ split changes is important for code review and analyzing bugs.
+
+Always squash fixup commits when making changes to pull requests
+----------------------------------------------------------------
+
+- If you make fixup commits to your pull request, you should generally squash
+ them with "git rebase -i". We prefer to have pull requests in a merge
+ ready state.
+- We don't squash-merge (nor do we use github's feature that does this) because
+ pull requests with multiple commits are perfectly legitimate, and the only
+ thing that makes sense in non-trivial cases.
+- With complex pull requests, it *may* make sense to keep them separate, but
+ they should be clearly marked as such. Reviewing commits is generally easier
+ with fixups squashed.
+- Reviewers are encouraged to look at individual commits instead of github's
+ "changes from all commits" view (which just encourages bad git and review
+ practices).
+
+Touching user-visible parts may require updating the mpv docs
+-------------------------------------------------------------
+
+- Most user-visible things are normally documented in DOCS/man/. If your commit
+ touches documented behavior, list of sub-options, etc., you need to adjust the
+ documentation.
+- These changes usually go into the same commit that changes the code.
+- Changes to command line options (addition/modification/removal) must be
+ documented in options.rst.
+- Changes to input properties or input commands must be documented in input.rst.
+- All incompatible changes to the user interface (options, properties, commands)
+ must be documented with a small note in interface-changes.rst. (Additions may
+ be documented there as well, but this isn't required.)
+- Changes to the libmpv API must be reflected in the libmpv's headers doxygen,
+ and in client-api-changes.rst.
+
+Code formatting
+---------------
+
+mpv uses C11 with K&R formatting, with some exceptions.
+
+- Use the K&R indent style.
+- Use 4 spaces of indentation, never use tabs (except in Makefiles).
+- Add a single space between keywords and binary operators. There are some other
+ cases where spaces must be added. Example:
+
+ ```C
+ if ((a * b) > c) {
+ // code
+ some_function(a, b, c);
+ }
+ ```
+- Break lines on 80 columns. There is a hard limit of 100 columns. You may ignore
+ this limit if there's a strong case that not breaking the line will increase
+ readability. Going over 100 columns might provoke endless discussions about
+ whether such a limit is needed or not, so avoid it.
+- If the body of an if/for/while statement has more than 1 physical lines, then
+ always add braces, even if they're technically redundant.
+
+ Bad:
+
+ ```C
+ if (a)
+ // do something if b
+ if (b)
+ do_something();
+ ```
+
+ Good:
+
+ ```C
+ if (a) {
+ // do something if b
+ if (b)
+ do_something();
+ }
+ ```
+- If the if has an else branch, both branches must use braces, even if they're
+ technically redundant.
+
+ Example:
+
+ ```C
+ if (a) {
+ one_line();
+ } else {
+ one_other_line();
+ }
+ ```
+- If an if condition spans multiple physical lines, then put the opening brace
+ for the if body on the next physical line. (Also, preferably always add a
+ brace, even if technically none is needed.)
+
+ Example:
+
+ ```C
+ if (very_long_condition_a &&
+ very_long_condition_b)
+ {
+ code();
+ } else {
+ ...
+ }
+ ```
+
+ (If the if body is simple enough, this rule can be skipped.)
+- Remove any trailing whitespace.
+- Do not make stray whitespaces changes.
+
+Header #include statement order
+-------------------------------
+
+The order of ``#include`` statements in the source code is not very consistent.
+New code must follow the following conventions:
+
+- Put standard includes (``#include <stdlib.h>`` etc.) on the top,
+- then after a blank line, add library includes (``#include <zlib.h>`` etc.)
+- then after a blank line, add internal includes (``#include "player/core.h"``)
+- sort them alphabetically within these sections
+
+General coding
+--------------
+
+- Use C11. Also freely make use of C11 features if it's appropriate, but do not
+ use VLA and complex number types.
+- Don't use non-standard language (such as GNU C-only features). In some cases
+ they may be warranted, if they are optional (such as attributes enabling
+ printf-like format string checks). "#pragma once" is allowed as an exception.
+ But in general, standard C11 must be used.
+- The same applies to libc functions. We have to be Windows-compatible too. Use
+ functions guaranteed by C11 or POSIX only, unless your use is guarded by a
+ configure check. Be mindful of MinGW-specifics since C11 support is not always
+ guaranteed.
+- Prefer fusing declaration and initialization, rather than putting declarations
+ on the top of a block. Obvious data flow is more important than avoiding
+ mixing declarations and statements, which is just a C90 artifact.
+- If you add features that require intrusive changes, discuss them on the dev
+ channel first. There might be a better way to add a feature and it can avoid
+ wasted work.
+
+Code of Conduct
+---------------
+
+Please note that this project is released with a Contributor Code of Conduct.
+By participating in this project you agree to abide by its terms.
+The Contributor Code of Conduct can be found here:
+https://www.contributor-covenant.org/version/2/0/code_of_conduct/
+
+Rules for git push access
+-------------------------
+
+Push access to the main git repository is handed out on an arbitrary basis. If
+you got access, the following rules must be followed:
+
+- You are expected to follow the general development rules as outlined in this
+ whole document.
+- You must be present on the IRC dev channel when you push something.
+- Anyone can push small fixes: typo corrections, small/obvious/uncontroversial
+ bug fixes, edits to the user documentation or code comments, and so on.
+- You can freely make changes to parts of the code which you maintain. For
+ larger changes, it's recommended to let others review the changes first.
+- You automatically maintain code if you wrote or modified most of it before
+ (e.g. you made larger changes to it before, did partial or full rewrites, did
+ major bug fixes, or you're the original author of the code). If there is more
+ than one maintainer, you may need to come to an agreement with the others how
+ to handle this to avoid conflict.
+- If you make a pull requests (especially if it's to code you maintain), and you
+ want reviews, explicitly ping the people from which you expect reviews.
+- As a maintainer, you can approve pull requests by others to "your" code.
+- If you approve or merge 3rd party changes, make sure they follow the general
+ development rules.
+- Changes to user interface and public API must always be approved by the
+ project leader.
+- Seasoned project members are allowed to revert commits that broke the build,
+ or broke basic functionality in a catastrophic way, and the developer who
+ broke it is unavailable. (Depending on severity.)
+- Adhere to the CoC.
+- The project leader is not bound by these rules.
diff --git a/DOCS/edl-mpv.rst b/DOCS/edl-mpv.rst
new file mode 100644
index 0000000..0e4d2b4
--- /dev/null
+++ b/DOCS/edl-mpv.rst
@@ -0,0 +1,396 @@
+EDL files
+=========
+
+EDL files basically concatenate ranges of video/audio from multiple source
+files into a single continuous virtual file. Each such range is called a
+segment, and consists of source file, source offset, and segment length.
+
+For example::
+
+ # mpv EDL v0
+ f1.mkv,10,20
+ f2.mkv
+ f1.mkv,40,10
+
+This would skip the first 10 seconds of the file f1.mkv, then play the next
+20 seconds, then switch to the file f2.mkv and play all of it, then switch
+back to f1.mkv, skip to the 40 second mark, and play 10 seconds, and then
+stop playback. The difference to specifying the files directly on command
+line (and using ``--{ --start=10 --length=20 f1.mkv --}`` etc.) is that the
+virtual EDL file appears as a virtual timeline (like a single file), instead
+as a playlist.
+
+The general simplified syntax is::
+
+ # mpv EDL v0
+ <filename>
+ <filename>,<start in seconds>,<length in seconds>
+
+If the start time is omitted, 0 is used. If the length is omitted, the
+estimated remaining duration of the source file is used.
+
+Note::
+
+ Usage of relative or absolute paths as well as any protocol prefixes may be
+ prevented for security reasons.
+
+
+Syntax of mpv EDL files
+=======================
+
+Generally, the format is relatively strict. No superfluous whitespace (except
+empty lines and commented lines) are allowed. You must use UNIX line breaks.
+
+The first line in the file must be ``# mpv EDL v0``. This designates that the
+file uses format version 0, which is not frozen yet and may change any time.
+(If you need a stable EDL file format, make a feature request. Likewise, if
+you have suggestions for improvements, it's not too late yet.)
+
+The rest of the lines belong to one of these classes:
+
+1) An empty or commented line. A comment starts with ``#``, which must be the
+ first character in the line. The rest of the line (up until the next line
+ break) is ignored. An empty line has 0 bytes between two line feed bytes.
+2) A header entry if the line starts with ``!``.
+3) A segment entry in all other cases.
+
+Each segment entry consists of a list of named or unnamed parameters.
+Parameters are separated with ``,``. Named parameters consist of a name,
+followed by ``=``, followed by the value. Unnamed parameters have only a
+value, and the name is implicit from the parameter position.
+
+Syntax::
+
+ segment_entry ::= <param> ( <param> ',' )*
+ param ::= [ <name> '=' ] ( <value> | '%' <number> '%' <valuebytes> )
+
+The ``name`` string can consist of any characters, except ``=%,;\n!``. The
+``value`` string can consist of any characters except of ``,;\n!``.
+
+The construct starting with ``%`` allows defining any value with arbitrary
+contents inline, where ``number`` is an integer giving the number of bytes in
+``valuebytes``. If a parameter value contains disallowed characters, it has to
+be guarded by a length specifier using this syntax.
+
+The parameter name defines the meaning of the parameter:
+
+1) ``file``, the source file to use for this segment.
+2) ``start``, a time value that specifies the start offset into the source file.
+3) ``length``, a time value that specifies the length of the segment.
+
+See the section below for the format of timestamps.
+
+Unnamed parameters carry implicit names. The parameter position determines
+which of the parameters listed above is set. For example, the second parameter
+implicitly uses the name ``start``.
+
+Example::
+
+ # mpv EDL v0
+ %18%filename,with,.mkv,10,length=20,param3=%13%value,escaped,param4=value2
+
+this sets ``file`` to ``filename,with,.mkv``, ``start`` to ``10``, ``length``
+to ``20``, ``param3`` to ``value,escaped``, ``param4`` to ``value2``.
+
+Instead of line breaks, the character ``;`` can be used. Line feed bytes and
+``;`` are treated equally.
+
+Header entries start with ``!`` as first character after a line break. Header
+entries affect all other file entries in the EDL file. Their format is highly
+implementation specific. They should generally follow the file header, and come
+before any file entries.
+
+Disabling chapter generation and copying
+========================================
+
+By default, chapters from the source ranges are copied to the virtual file's
+chapters. Also, a chapter is inserted after each range. This can be disabled
+with the ``no_chapters`` header.
+
+Example::
+
+ !no_chapters
+
+
+MP4 DASH
+========
+
+This is a header that helps implementing DASH, although it only provides a low
+level mechanism.
+
+If this header is set, the given url designates an mp4 init fragment. It's
+downloaded, and every URL in the EDL is prefixed with the init fragment on the
+byte stream level. This is mostly for use by mpv's internal ytdl support. The
+ytdl script will call youtube-dl, which in turn actually processes DASH
+manifests. It may work only for this very specific purpose and fail to be
+useful in other scenarios. It can be removed or changed in incompatible ways
+at any times.
+
+Example::
+
+ !mp4_dash,init=url
+
+The ``url`` is encoded as parameter value as defined in the general EDL syntax.
+It's expected to point to an "initialization fragment", which will be prefixed
+to every entry in the EDL on the byte stream level.
+
+The current implementation will
+
+- ignore stream start times
+- use durations as hint for seeking only
+- not adjust source timestamps
+- open and close segments (i.e. fragments) as needed
+- not add segment boundaries as chapter points
+- require full compatibility between all segments (same codec etc.)
+
+Another header part of this mechanism is ``no_clip``. This header is similar
+to ``mp4_dash``, but does not include on-demand opening/closing of segments,
+and does not support init segments. It also exists solely to support internal
+ytdl requirements. Using ``no_clip`` with segments is not recommended and
+probably breaks. ``mp4_dash`` already implicitly does a variant of ``no_clip``.
+
+The ``mp4_dash`` and ``no_clip`` headers are not part of the core EDL format.
+They may be changed or removed at any time, depending on mpv's internal
+requirements.
+
+Separate files for tracks
+=========================
+
+The special ``new_stream`` header lets you specify separate parts and time
+offsets for separate tracks. This can for example be used to source audio and
+video track from separate files.
+
+Example::
+
+ # mpv EDL v0
+ video.mkv
+ !new_stream
+ audio.mkv
+
+This adds all tracks from both files to the virtual track list. Upon playback,
+the tracks will be played at the same time, instead of appending them. The files
+can contain more than 1 stream; the apparent effect is the same as if the second
+part after the ``!new_stream`` part were in a separate ``.edl`` file and added
+with ``--external-file``.
+
+Note that all metadata between the stream sets created by ``new_stream`` is
+disjoint. Global metadata is taken from the first part only.
+
+In context of mpv, this is redundant to the ``--audio-file`` and
+``--external-file`` options, but (as of this writing) has the advantage that
+this will use a unified cache for all streams.
+
+The ``new_stream`` header is not part of the core EDL format. It may be changed
+or removed at any time, depending on mpv's internal requirements.
+
+If the first ``!new_stream`` is redundant, it is ignored. This is the same
+example as above::
+
+ # mpv EDL v0
+ !new_stream
+ video.mkv
+ !new_stream
+ audio.mkv
+
+Note that ``!new_stream`` must be the first header. Whether the parser accepts
+(i.e. ignores) or rejects other headers before that is implementation specific.
+
+Track metadata
+==============
+
+The special ``track_meta`` header can set some specific metadata fields of the
+current ``!new_stream`` partition. The tags are applied to all tracks within
+the partition. It is not possible to set the metadata for individual tracks (the
+feature was needed only for single-track media).
+
+It provides following parameters change track metadata:
+
+``lang``
+ Set the language tag.
+
+``title``
+ Set the title tag.
+
+``byterate``
+ Number of bytes per second this stream uses. (Purely informational.)
+
+``index``
+ The numeric index of the track this should map to (default: -1). This is
+ the 0-based index of the virtual stream as seen by the player, enumerating
+ all audio/video/subtitle streams. If nothing matches, this is silently
+ discarded. The special index -1 (the default) has two meanings: if there
+ was a previous meta data entry (either ``!track_meta`` or ``!delay_open``
+ element since the last ``!new_stream``), then this element manipulates
+ the previous meta data entry. If there was no previous entry, a new meta
+ data entry that matches all streams is created.
+
+Example::
+
+ # mpv EDL v0
+ !track_meta,lang=bla,title=blabla
+ file.mkv
+ !new_stream
+ !track_meta,title=ducks
+ sub.srt
+
+If ``file.mkv`` has an audio and a video stream, both will use ``blabla`` as
+title. The subtitle stream will use ``ducks`` as title.
+
+The ``track_meta`` header is not part of the core EDL format. It may be changed
+or removed at any time, depending on mpv's internal requirements.
+
+Global metadata
+===============
+
+The special ``global_tags`` header can set metadata fields (aka tags) of the EDL
+file. This metadata is supposed to be informational, much like for example ID3
+tags in audio files. Due to lack of separation of different kinds of metadata it
+is unspecified what names are allowed, how they are interpreted, and whether
+some of them affect playback functionally. (Much of this is unfortunately
+inherited from FFmpeg. Another consequence of this is that FFmpeg "normalized"
+tags are recognized, or stuff like replaygain tags.)
+
+Example::
+
+ !global_tags,title=bla,something_arbitrary=even_more_arbitrary
+
+Any parameter names are allowed. Repeated use of this adds to the tag list. If
+``!new_stream`` is used, the location doesn't matter.
+
+May possibly be ignored in some cases, such as delayed media opening.
+
+Delayed media opening
+=====================
+
+The special ``delay_open`` header can be used to open the media URL of the
+stream only when the track is selected for the first time. This is supposed to
+be an optimization to speed up opening of a remote stream if there are many
+tracks for whatever reasons.
+
+This has various tricky restrictions, and also will defer failure to open a
+stream to "later". By design, it's supposed to be used for single-track streams.
+
+Using multiple segments requires you to specify all offsets and durations (also
+it was never tested whether it works at all). Interaction with ``mp4_dash`` may
+be strange.
+
+You can describe multiple sub-tracks by using multiple ``delay_open`` headers
+before the same source URL. (If there are multiple sub-tracks of the same media
+type, then the mapping to the real stream is probably rather arbitrary.) If the
+source contains tracks not described, a warning is logged when the delayed
+opening happens, and the track is hidden.
+
+This has the following parameters:
+
+``media_type``
+ Required. Must be set to ``video``, ``audio``, or ``sub``. (Other tracks in
+ the opened URL are ignored.)
+
+``codec``
+ The mpv codec name that is expected. Although mpv tries to initialize a
+ decoder with it currently (and will fail track selection if it does not
+ initialize successfully), it is not used for decoding - decoding still uses
+ the information retrieved from opening the actual media information, and may
+ be a different codec (you should try to avoid this, of course). Defaults to
+ ``null``.
+
+ Above also applies for similar fields such as ``w``. These fields are
+ mostly to help with user track pre-selection.
+
+``flags``
+ A ``+`` separated list of boolean flags. Currently defined flags:
+
+ ``default``
+ Set the default track flag.
+
+ ``forced``
+ Set the forced track flag.
+
+ Other values are ignored after triggering a warning.
+
+``w``, ``h``
+ For video codecs: expected video size. See ``codec`` for details.
+
+``fps``
+ For video codecs: expected video framerate, as integer. (The rate is usually
+ only crudely reported, and it makes no sense to expect exact values.)
+
+``samplerate``
+ For audio codecs: expected sample rate, as integer.
+
+The ``delay_open`` header is not part of the core EDL format. It may be changed
+or removed at any time, depending on mpv's internal requirements.
+
+Timestamp format
+================
+
+Currently, time values are floating point values in seconds.
+
+As an extension, you can set the ``timestamps=chapters`` option. If this option
+is set, timestamps have to be integers, and refer to chapter numbers, starting
+with 0. The default value for this parameter is ``seconds``, which means the
+time is as described in the previous paragraph.
+
+Example::
+
+ # mpv EDL v0
+ file.mkv,2,4,timestamps=chapters
+
+Plays chapter 3 and ends with the start of chapter 7 (4 chapters later).
+
+Implicit chapters
+=================
+
+mpv will add one chapter per segment entry to the virtual timeline.
+
+By default, the chapter's titles will match the entries' filenames.
+You can override set the ``title`` option to override the chapter title for
+that segment.
+
+Example::
+
+ # mpv EDL v0
+ cap.ts,5,240
+ OP.mkv,0,90,title=Show Opening
+
+The virtual timeline will have two chapters, one called "cap.ts" from 0-240s
+and a second one called "Show Opening" from 240-330s.
+
+Entry which defines the track layout
+====================================
+
+Normally, you're supposed to put only files with compatible layouts into an EDL
+file. However, at least the mpv implementation accepts entries that use
+different codecs, or even have a different number of audio/video/subtitle
+tracks. In this case, it's not obvious, which virtual tracks the EDL show should
+expose when being played.
+
+Currently, mpv will apply an arbitrary heuristic which tracks the EDL file
+should expose. (Before mpv 0.30.0, it always used the first source file in the
+segment list.)
+
+You can set the ``layout`` option to ``this`` to make a specific entry define
+the track layout.
+
+Example::
+
+ # mpv EDL v0
+ file_with_2_streams.ts,5,240
+ file_with_5_streams.mkv,0,90,layout=this
+
+The way the different virtual EDL tracks are associated with the per-segment
+ones is highly implementation-defined, and uses a heuristic. If a segment is
+missing a track, there will be a "hole", and bad behavior may result. Improving
+this is subject to further development (due to being fringe cases, they don't
+have a high priority).
+
+If future versions of mpv change this again, this option may be ignored.
+
+Syntax of EDL URIs
+==================
+
+mpv accepts inline EDL data in form of ``edl://`` URIs. Other than the
+header, the syntax is exactly the same. It's far more convenient to use ``;``
+instead of line breaks, but that is orthogonal.
+
+Example: ``edl://f1.mkv,length=5,start=10;f2.mkv,30,20;f3.mkv``
diff --git a/DOCS/encoding.rst b/DOCS/encoding.rst
new file mode 100644
index 0000000..9c6c067
--- /dev/null
+++ b/DOCS/encoding.rst
@@ -0,0 +1,155 @@
+General usage
+=============
+
+::
+
+ mpv infile --o=outfile [--of=outfileformat] [--ofopts=formatoptions] [--orawts] \
+ [(any other mpv options)] \
+ --ovc=outvideocodec [--ovcopts=outvideocodecoptions] \
+ --oac=outaudiocodec [--oacopts=outaudiocodecoptions]
+
+Help for these options is provided if giving help as parameter, as in::
+
+ mpv --ovc=help
+
+The suboptions of these generally are identical to ffmpeg's (as option parsing
+is simply delegated to ffmpeg). The option --ocopyts enables copying timestamps
+from the source as-is, instead of fixing them to match audio playback time
+(note: this doesn't work with all output container formats); --orawts even turns
+off discontinuity fixing.
+
+Note that if neither --ofps nor --oautofps is specified, VFR encoding is assumed
+and the time base is 24000fps. --oautofps sets --ofps to a guessed fps number
+from the input video. Note that not all codecs and not all formats support VFR
+encoding, and some which do have bugs when a target bitrate is specified - use
+--ofps or --oautofps to force CFR encoding in these cases.
+
+Of course, the options can be stored in a profile, like this .config/mpv/mpv.conf
+section::
+
+ [myencprofile]
+ vf-add = scale=480:-2
+ ovc = libx264
+ ovcopts-add = preset=medium
+ ovcopts-add = tune=fastdecode
+ ovcopts-add = crf=23
+ ovcopts-add = maxrate=1500k
+ ovcopts-add = bufsize=1000k
+ ovcopts-add = rc_init_occupancy=900k
+ ovcopts-add = refs=2
+ ovcopts-add = profile=baseline
+ oac = aac
+ oacopts-add = b=96k
+
+It's also possible to define default encoding options by putting them into
+the section named ``[encoding]``. (This behavior changed after mpv 0.3.x. In
+mpv 0.3.x, config options in the default section / no section were applied
+to encoding. This is not the case anymore.)
+
+One can then encode using this profile using the command::
+
+ mpv infile --o=outfile.mp4 --profile=myencprofile
+
+Some example profiles are provided in a file
+etc/encoding-profiles.conf; as for this, see below.
+
+
+Encoding examples
+=================
+
+These are some examples of encoding targets this code has been used and tested
+for.
+
+Typical MPEG-4 Part 2 ("ASP", "DivX") encoding, AVI container::
+
+ mpv infile --o=outfile.avi \
+ --vf=fps=25 \
+ --ovc=mpeg4 --ovcopts=qscale=4 \
+ --oac=libmp3lame --oacopts=b=128k
+
+Note: AVI does not support variable frame rate, so the fps filter must be used.
+The frame rate should ideally match the input (25 for PAL, 24000/1001 or
+30000/1001 for NTSC)
+
+Typical MPEG-4 Part 10 ("AVC", "H.264") encoding, Matroska (MKV) container::
+
+ mpv infile --o=outfile.mkv \
+ --ovc=libx264 --ovcopts=preset=medium,crf=23,profile=baseline \
+ --oac=libopus --oacopts=qscale=3
+
+Typical MPEG-4 Part 10 ("AVC", "H.264") encoding, MPEG-4 (MP4) container::
+
+ mpv infile --o=outfile.mp4 \
+ --ovc=libx264 --ovcopts=preset=medium,crf=23,profile=baseline \
+ --oac=aac --oacopts=b=128k
+
+Typical VP8 encoding, WebM (restricted Matroska) container::
+
+ mpv infile -o outfile.mkv \
+ --of=webm \
+ --ovc=libvpx --ovcopts=qmin=6,b=1000000k \
+ --oac=libopus --oacopts=qscale=3
+
+
+Device targets
+==============
+
+As the options for various devices can get complex, profiles can be used.
+
+An example profile file for encoding is provided in
+etc/encoding-profiles.conf in the source tree. This file is installed and loaded
+by default. If you want to modify it, you can replace and it with your own copy
+by doing::
+
+ mkdir -p ~/.mpv
+ cp /etc/mpv/encoding-profiles.conf ~/.mpv/encoding-profiles.conf
+
+Keep in mind that the default profile is the playback one. If you want to add
+options that apply only in encoding mode, put them into a ``[encoding]``
+section.
+
+Refer to the top of that file for more comments - in a nutshell, the following
+options are added by it::
+
+ --profile=enc-to-dvdpal # DVD-Video PAL, use dvdauthor -v pal+4:3 -a ac3+en
+ --profile=enc-to-dvdntsc # DVD-Video NTSC, use dvdauthor -v ntsc+4:3 -a ac3+en
+ --profile=enc-to-bb-9000 # MP4 for Blackberry Bold 9000
+ --profile=enc-to-nok-6300 # 3GP for Nokia 6300
+ --profile=enc-to-psp # MP4 for PlayStation Portable
+ --profile=enc-to-iphone # MP4 for iPhone
+ --profile=enc-to-iphone-4 # MP4 for iPhone 4 (double res)
+ --profile=enc-to-iphone-5 # MP4 for iPhone 5 (even larger res)
+
+You can encode using these with a command line like::
+
+ mpv infile --o=outfile.mp4 --profile=enc-to-bb-9000
+
+Of course, you are free to override options set by these profiles by specifying
+them after the -profile option.
+
+
+What works
+==========
+
+* Encoding at variable frame rate (default)
+* Encoding at constant frame rate using --vf=fps=RATE
+* 2-pass encoding (specify flags=+pass1 in the first pass's --ovcopts, specify
+ flags=+pass2 in the second pass)
+* Hardcoding subtitles using vobsub, ass or srt subtitle rendering (just
+ configure mpv for the subtitles as usual)
+* Hardcoding any other mpv OSD (e.g. time codes, using --osdlevel=3 and
+ --vf=expand=::::1)
+* Encoding directly from a DVD, network stream, webcam, or any other source
+ mpv supports
+* Using x264 presets/tunings/profiles (by using profile=, tune=, preset= in the
+ --ovcopts)
+* Deinterlacing/Inverse Telecine with any of mpv's filters for that
+* Audio file converting: mpv --o=outfile.m4a infile.flac --no-video
+ --oac=aac --oacopts=b=320k
+
+What does not work yet
+======================
+
+* 3-pass encoding (ensuring constant total size and bitrate constraints while
+ having VBR audio; mencoder calls this "frameno")
+* Direct stream copy
diff --git a/DOCS/interface-changes.rst b/DOCS/interface-changes.rst
new file mode 100644
index 0000000..f59f890
--- /dev/null
+++ b/DOCS/interface-changes.rst
@@ -0,0 +1,982 @@
+Introduction
+============
+
+mpv provides access to its internals via the following means:
+
+- options
+- commands
+- properties
+- events
+- hooks
+
+The sum of these mechanisms is sometimes called command interface.
+
+All of these are important for interfacing both with end users and API users
+(which include Lua scripts, libmpv, and the JSON IPC). As such, they constitute
+a large part of the user interface and APIs.
+
+Also see compatibility.rst.
+
+This document lists changes to them. New changes are added to the top. Usually,
+only incompatible or important changes are mentioned. New options/commands/etc.
+are not always listed.
+
+Interface changes
+=================
+
+::
+
+ --- mpv 0.37.0 ---
+ - `--save-position-on-quit` and its associated commands now store state files
+ in %LOCALAPPDATA% instead of %APPDATA% directory by default on Windows.
+ - change `--subs-with-matching-audio` default from `no` to `yes`
+ - change `--subs-fallback` default from `no` to `default`
+ - add the `--hdr-peak-percentile` option
+ - include `--hdr-peak-percentile` in the `gpu-hq` profile
+ - change `--audiotrack-pcm-float` default from `no` to `yes`
+ - add video-params/aspect-name
+ - change type of `--sub-pos` to float
+ - The remaining time printed in the terminal is now adjusted for speed by default.
+ You can disable this with `--no-term-remaining-playtime`.
+ - add `playlist-path` and `playlist/N/playlist-path` properties
+ - add `--x11-wid-title` option
+ - add `--libplacebo-opts` option
+ - add `--audio-file-exts`, `--cover-art-auto-exts`, and `--sub-auto-exts`
+ - change `slang` default back to NULL
+ - remove special handling of the `auto` value from `--alang/slang/vlang` options
+ - add `--subs-match-os-language` as a replacement for `--slang=auto`
+ - add `always` option to `--subs-fallback-forced`
+ - remove `auto` choice from `--sub-forced-only`
+ - remove `auto-forced-only` property
+ - rename `--sub-forced-only` to `--sub-forced-events-only`
+ - remove `sub-forced-only-cur` property (`--sub-forced-events-only` is a replacement)
+ - remove deprecated `video-aspect` property
+ - add `--video-crop`
+ - add `video-params/crop-[w,h,x,y]`
+ - remove `--tone-mapping-mode`
+ - change `--subs-fallback-forced` so that it works alongside `--slang`
+ - add `--icc-3dlut-size=auto` and make it the default
+ - add `--scale=ewa_lanczos4sharpest`
+ - remove `--scale-wblur`, `--cscale-wblur`, `--dscale-wblur`, `--tscale-wblur`
+ - remove `bcspline` filter (`bicubic` is now the same as `bcspline`)
+ - rename `--cache-dir` and `--cache-unlink-files` to `--demuxer-cache-dir` and
+ `--demuxer-cache-unlink-files`
+ - enable `--correct-downscaling`, `--linear-downscaling`, `--sigmoid-upscaling`
+ - `--cscale` defaults to `--scale` if not defined
+ - change `--tscale` default to `oversample`
+ - change `--dither-depth` to `auto`
+ - deprecate `--profile=gpu-hq`, add `--profile=<fast|high-quality>`
+ - change `--dscale` default to `hermite`
+ - update defaults to `--hdr-peak-decay-rate=20`, `--hdr-scene-threshold-low=1.0`,
+ `--hdr-scene-threshold-high=3.0`
+ - update defaults to `--deband-threshold=48`, `--deband-grain=32`
+ - add `--directory-mode=auto` and make it the default
+ - remove deprecated `--profile=opengl-hq`
+ - remove several legacy fallbacks for old deprecated options (now they will just
+ error out like normal)
+ - remove deprecated `drop-frame-count` and `vo-drop-frame-count` property aliases
+ - remove the ability to write to the `display-fps` property (use `override-display-fps`
+ instead)
+ - writing the current value to playlist-pos will no longer restart playback (use
+ `playlist-play-index` instead)
+ - remove deprecated `--oaoffset`, `--oafirst`, `--ovoffset`, `--ovfirst`,
+ `--demuxer-force-retry-on-eof`, `--fit-border` options
+ - remove deprecated `--record-file` option
+ - remove deprecated `--vf-defaults` and `--af-defaults` options
+ - `--drm-connector` no longer allows selecting the card number (use `--drm-device`
+ instead)
+ - add `--title-bar` option
+ - add `--window-corners` option
+ - rename `--cdrom-device` to `--cdda-device`
+ - remove `--scale-cutoff`, `--cscale-cutoff`, `--dscale-cutoff`, `--tscale-cutoff`
+ - remove `--scaler-lut-size`
+ - deprecate shared-script-properties (user-data is a replacement)
+ - add `--backdrop-type` option
+ - add `--window-affinity` option
+ - `--config-dir` no longer forces cache and state files to also reside in there
+ - deprecate `--demuxer-cue-codepage` in favor of `--metadata-codepage`
+ - change the default of `metadata-codepage` to `auto`
+ - add `playlist-next-playlist` and `playlist-prev-playlist` commands
+ - change `video-codec` to show description or name, not both
+ - deprecate `--cdda-toc-bias` option, offsets are always checked now
+ - disable `--allow-delayed-peak-detect` by default
+ - rename `--fps` to `--container-fps-override`
+ - rename `--override-display-fps` to `--display-fps-override`
+ - rename `--sub-ass-force-style` to `--sub-ass-style-overrides`
+ - alias `--screenshot-directory` to `--screenshot-dir`
+ - alias `--watch-later-directory` to `--watch-later-dir`
+ - rename `--play-dir` to `--play-direction`
+ - `--js-memory-report` is now used for enabling memory reporting for javascript
+ scripts
+ - drop support for `-del` syntax for list options
+ - `--demuxer-hysteresis-secs` now respects `--cache-secs` and/or
+ `--demuxer-readahead-secs` as well
+ - add hdr metadata to `video-params` property
+ - add `--target-gamut`
+ - change the way display names are retrieved on macOS, usage of options and properties
+ `--fs-screen-name`, `--screen-name` and `display-names` needs to be adjusted
+ - remove OpenGL cocoa backend that was deprecated in 0.29
+ - remove `border`, `fullscreen`, `ontop`, `osd-level` and `pause`
+ from default `--watch-later-options`
+ - add `video-*` and `secondary-sub-visibility` to default `--watch-later-options`
+ --- mpv 0.36.0 ---
+ - add `--target-contrast`
+ - Target luminance value is now also applied when ICC profile is used.
+ `--icc-use-luma` has been added to use ICC profile luminance value.
+ If target luminance and ICC luminance is not used, old behavior apply,
+ defaulting to 203 nits. (Only applies for `--vo=gpu-next`)
+ - `playlist/N/title` gets set upon opening the file if it wasn't already set
+ and a title is available.
+ - add the `--vo=kitty` video output driver, as well as the options
+ `--vo-kitty-cols`, `--vo-kitty-rows`, `--vo-kitty-width`,
+ `--vo-kitty-height`, `--vo-kitty-left`, `--vo-kitty-top`,
+ `--vo-kitty-config-clear`, `--vo-kitty-alt-screen` and
+ `--vo-kitty-use-shm`
+ - add `--force-render`
+ - add `--vo-sixel-config-clear`, `--vo-sixel-alt-screen` and
+ `--vo-sixel-buffered`
+ - add `--wayland-content-type`
+ - deprecate `--vo-sixel-exit-clear` and alias it to
+ `--vo-sixel-alt-screen`
+ - deprecate `--drm-atomic`
+ - add `--demuxer-hysteresis-secs`
+ - add `--video-sync=display-tempo`
+ - the `start` option is no longer unconditionally written by
+ watch-later. It is still written by default but you may
+ need to explicitly add `start` depending on how you have
+ `--watch-later-options` configured.
+ - add `--vd-lavc-dr=auto` and make it the default
+ - add support for the fractional scale protocol in wayland
+ - in wayland, hidpi window scaling now scales the window by the compositor's
+ dpi scale factor by default (can be disabled with --no-hidpi-window-scale
+ if fractional scaling support exists).
+ - change --screenshot-tag-colorspace default value from `no` to `yes`
+ - undeprecate vf_sub
+ - add `--tone-mapping=st2094-40` and `--tone-mapping=st2094-10`
+ - change `--screenshot-jxl-effort` default from `3` to `4`.
+ - add `--tone-mapping-visualize`
+ - change type of `--brightness`, `--saturation`, `--contrast`, `--hue` and
+ `--gamma` to float.
+ - add `platform` property
+ - add `--auto-window-resize`
+ - `--save-position-on-quit` and its associated commands now store state files in
+ the XDG_STATE_HOME directory by default. This only has an effect on linux/bsd
+ systems.
+ - mpv now implictly saves cache files in XDG_CACHE_HOME by default. This only has
+ an effect if the user enables options that would lead to cache being stored and
+ only makes a difference on linux/bsd systems.
+ - `--cache-on-disk` no longer requires explictly setting the `--cache-dir` option
+ - add `--icc-cache` and `--gpu-shader-cache` options to control whether or not to
+ save cache files for these features; explictly setting `--icc-cache-dir` and
+ `--gpu-shader-cache` is no longer required
+ - remove the `--tone-mapping-crosstalk` option
+ - add `--gamut-mapping-mode=perceptual|relative|saturation|absolute|linear`
+ - add `--corner-rounding` option
+ - change `--subs-with-matching-audio` default from `yes` to `no`
+ - change `--slang` default from blank to `auto`
+ - add `--input-cursor-passthrough` option to allow pointer events to completely
+ passthrough the mpv window
+ - icc and gpu-shader cache are now saved by default (use --no-icc-shader-cache and
+ --no-gpu-shader-cache to disable)
+ - add `--directory-mode=recursive|lazy|ignore`
+ - `--hwdec=yes` is now mapped to `auto-safe` rather than `auto` (also used
+ by ctrl+h keybind)
+ - add `--hdr-contrast-recovery` and `--hdr-contrast-smoothness`
+ - include `--hdr-contrast-recovery` in the `gpu-hq` profile
+ --- mpv 0.35.0 ---
+ - add the `--vo=gpu-next` video output driver, as well as the options
+ `--allow-delayed-peak-detect`, `--builtin-scalers`,
+ `--interpolation-preserve` `--lut`, `--lut-type`, `--image-lut`,
+ `--image-lut-type` and `--target-lut` along with it.
+ - add `--target-colorspace-hint`
+ - add `--tone-mapping-crosstalk`
+ - add `--tone-mapping` options `auto`, `spline` and `bt.2446a`
+ - add `--inverse-tone-mapping`
+ - add `--gamut-mapping-mode`, replacing `--gamut-clipping` and `--gamut-warning`
+ - add `--tone-mapping-mode`, replacing `--tone-mapping-desaturate` and
+ `--tone-mapping-desaturate-exponent`.
+ - add `dolbyvision` sub-parameter to `format` video filter
+ - `--sub-visibility` no longer has any effect on secondary subtitles
+ - add `film-grain` sub-parameter to `format` video filter
+ - add experimental `--vo=dmabuf-wayland` video output driver
+ - add `--x11-present` for controlling whether to use xorg's present extension
+ - add `engine` option to the `rubberband` audio filter to support the new
+ engine introduced in rubberband 3.0.0. Defaults to `finer` (new engine).
+ - add `--wayland-configure-bounds` option
+ - deprecate `--gamma-factor`
+ - deprecate `--gamma-auto`
+ - remove `--vulkan-disable-events`
+ - add `--glsl-shader-opts`
+ --- mpv 0.34.0 ---
+ - deprecate selecting by card number with `--drm-connector`, add
+ `--drm-device` which can be used instead
+ - add `--screen-name` and `--fs-screen-name` flags to allow selecting the
+ screen by its name instead of the index
+ - add `--macos-geometry-calculation` to change the rectangle used for screen
+ position and size calculation. the old behavior used the whole screen,
+ which didn't take the menu bar and Dock into account. The new default
+ behaviour includes both. To revert to the old behavior set this to
+ `whole`.
+ - add an additional optional `albumart` argument to the `video-add` command,
+ which tells mpv to load the given video as album art.
+ - undeprecate `--cache-secs` option
+ - remove `--icc-contrast` and introduce `--icc-force-contrast`. The latter
+ defaults to the equivalent of the old `--icc-contrast=inf`, and can
+ instead be used to specifically set the contrast to any value.
+ - add a `--watch-later-options` option to allow configuring which
+ options quit-watch-later saves
+ - make `current-window-scale` writeable and use it in the default input.conf
+ - add `--input-builtin-bindings` flag to control loading of built-in key
+ bindings during start-up (default: yes).
+ - add ``track-list/N/image`` sub-property
+ - remove `--opengl-restrict` option
+ - js custom-init: use filename ~~/init.js instead of ~~/.init.js (dot)
+ --- mpv 0.33.0 ---
+ - add `--d3d11-exclusive-fs` flag to enable D3D11 exclusive fullscreen mode
+ when the player enters fullscreen.
+ - directories in ~/.mpv/scripts/ (or equivalent) now have special semantics
+ (see mpv Lua scripting docs)
+ - names starting with "." in ~/.mpv/scripts/ (or equivalent) are now ignored
+ - js modules: ~~/scripts/modules.js/ is no longer used, global paths can be
+ set with custom init (see docs), dir-scripts first look at <dir>/modules/
+ - the OSX bundle now logs to "~/Library/Logs/mpv.log" by default
+ - deprecate the --cache-secs option (once removed, the cache cannot be
+ limited by time anymore)
+ - remove deprecated legacy hook API ("hook-add", "hook-ack"). Use either the
+ libmpv API (mpv_hook_add(), mpv_hook_continue()), or the Lua scripting
+ wrappers (mp.add_hook()).
+ - improve how property change notifications are delivered on events and on
+ hooks. In particular, a hook event is only returned to a client after all
+ changes initiated before the hook point were delivered to the same client.
+ In addition, it should no longer happen that events and property change
+ notifications were interleaved in bad ways (it could happen that a
+ property notification delivered after an event contained a value that was
+ valid only before the event happened).
+ - the playlist-pos and playlist-pos-1 properties now can return and accept
+ -1, and are never unavailable. Out of range indexes are now accepted, but
+ behave like writing -1.
+ - the playlist-pos and playlist-pos-1 properties deprecate the current
+ behavior when writing back the current value to the property: currently,
+ this restarts playback, but in the future, it will do nothing.
+ Using the "playlist-play-index" command is recommended instead.
+ - add "playlist-play-index" command
+ - add playlist-current-pos, playlist-playing-pos properties
+ - Lua end-file events set the "error" field; this is deprecated; use the
+ "file_error" instead for this specific event. Scripts relying on the
+ "error" field for end-file will silently break at some point in the
+ future.
+ - remove deprecated --input-file option, add --input-ipc-client, which is
+ vaguely a replacement of the removed option, but not the same
+ - change another detail for track selection options (see --aid manpage
+ entry)
+ - reading loop-file property as native property or mpv_node will now return
+ "inf" instead of boolean true (also affects loop option)
+ - remove some --vo-direct3d-... options (it got dumbed down; use --vo=gpu)
+ - remove video-params/plane-depth property (was too vaguely defined)
+ - remove --video-sync-adrop-size option (implementation was changed, no
+ replacement for what this option did)
+ - undeprecate --video-sync=display-adrop
+ - deprecate legacy auto profiles (profiles starting with "extension." and
+ "protocol."). Use conditional auto profiles instead.
+ - the "subprocess" command does not connect spawned processes' stdin to
+ mpv's stdin anymore. Instead, stdin is connected to /dev/null by default.
+ To get the old behavior, set the "passthrough_stdin" argument to true.
+ - key/value list options do not accept ":" as item separator anymore,
+ only ",". This means ":" is always considered part of the value.
+ - remove deprecated --vo-vdpau-deint option
+ - add `delete-watch-later-config` command to complement
+ `write-watch-later-config`
+ --- mpv 0.32.0 ---
+ - change behavior when using legacy option syntax with options that start
+ with two dashes (``--`` instead of a ``-``). Now, using the recommended
+ syntax is required for options starting with ``--``, which means an option
+ value must be strictly passed after a ``=``, instead of as separate
+ argument. For example, ``--log-file f.txt`` was previously accepted and
+ behaved like ``--log-file=f.txt``, but now causes an error. Use of legacy
+ syntax that is still supported now prints a deprecation warning.
+ --- mpv 0.31.0 ---
+ - add `--resume-playback-check-mtime` to check consistent mtime when
+ restoring playback state.
+ - add `--d3d11-output-csp` to enable explicit selection of a D3D11
+ swap chain color space.
+ - the --sws- options and similar now affect vo_image and screenshot
+ conversion (does not matter as much for vo_gpu, which does most of this
+ with shaders)
+ - add a builtin "sw-fast" profile, which restores performance settings
+ for software video conversion. These were switched to higher quality since
+ mpv 0.30.0 (related to the previous changelog entry). This affects video
+ outputs like vo_x11 and vo_drm, and screenshots, but not much else.
+ - deprecate --input-file (there are no plans to remove this short-term,
+ but it will probably eventually go away <- that was a lie)
+ - deprecate --video-sync=display-adrop (might be removed if it's in the way;
+ undeprecated or readded if it's not too much of a problem)
+ - deprecate all input section commands (these will be changed/removed, as
+ soon as mpv internals do not require them anymore)
+ - remove deprecated --playlist-pos alias (use --playlist-start)
+ - deprecate --display-fps, introduce --override-display-fps. The display-fps
+ property now is unavailable if no VO exists (or the VO did not return a
+ display FPS), instead of returning the option value in this case. The
+ property will keep existing, but writing to it is deprecated.
+ - the vf/af properties now do not reject the set value anymore, even if
+ filter chain initialization fails. Instead, the vf/af options are always
+ set to the user's value, even if it does not reflect the "runtime" vf/af
+ chain.
+ - the vid/aid/sid/secondary-sid properties (and their aliases: "audio",
+ "video", "sub") will now allow setting any track ID; before this change,
+ only IDs of actually existing tracks could be set (the restriction was
+ active the MPV_EVENT_FILE_LOADED/"file-loaded" event was sent). Setting
+ an ID for which no track exists is equivalent to disabling it. Note that
+ setting the properties to non-existing tracks may report it as selected
+ track for a small time window, until it's forced back to "no". The exact
+ details how this is handled may change in the future.
+ - remove old Apple Remote support, including --input-appleremote
+ - add MediaPlayer support and remove the old Media Key event tap on macOS.
+ this possibly also re-adds the Apple Remote support
+ - the "edition" property now strictly returns the value of the option,
+ instead of the runtime value. The new "current-edition" property needs to
+ be queried to read the runtime-chosen edition. This is a breaking change
+ for any users which expected "edition" to return the runtime-chosen
+ edition at default settings (--edition=auto).
+ - the "window-scale" property now strictly returns the value of the option,
+ instead of the actual size of the window. The new "current-window-scale"
+ property needs to be queried to read the value as indicated by the current
+ window size. This is a breaking change.
+ - explicitly deprecate passing more than 1 item to "-add" suffix in key/value
+ options (for example --script-opts-add). This was actually always
+ deprecated, like with other list options, but the option parser did not
+ print a warning in this particular case.
+ - deprecate -del for list options (use -remove instead, which is by content
+ instead of by integer index)
+ - if `--fs` is used but `--fs-screen` is not set, mpv will now use `--screen`
+ instead.
+ - change the default of --hwdec to "no" on RPI. The default used to be "mmal"
+ specifically if 'Raspberry Pi support' was enabled at configure time
+ (equivalent to --enable-rpi). Use --hwdec=mmal to get the old behavior.
+ --- mpv 0.30.0 ---
+ - add `--d3d11-output-format` to enable explicit selection of a D3D11
+ swap chain format.
+ - rewrite DVB channel switching to use an integer value
+ `--dvbin-channel-switch-offset` for switching instead of the old
+ stream controls which are now gone. Cycling this property up or down will
+ change the offset to the channel which was initially tuned to.
+ Example for `input.conf`: `H cycle dvbin-channel-switch-offset up`,
+ `K cycle dvbin-channel-switch-offset down`.
+ - adapt `stream_dvb` to support writing to `dvbin-prog` at runtime
+ and also to consistently use dvbin-configuration over URI parameters
+ when provided
+ - add `--d3d11-adapter` to enable explicit selection of a D3D11 rendering
+ adapter by name.
+ - rename `--drm-osd-plane-id` to `--drm-draw-plane`, `--drm-video-plane-id` to
+ `--drm-drmprime-video-plane` and `--drm-osd-size` to `--drm-draw-surface-size`
+ to better reflect what the options actually control, that the values they
+ accept aren't actually internal DRM ID's (like with similar options in
+ ffmpeg's KMS support), and that the video plane is only used when the drmprime
+ overlay hwdec interop is active, with the video being drawn to the draw plane
+ otherwise.
+ - in addition to the above, the `--drm-draw-plane` and `--drm-drmprime-video-plane`
+ options now accept either an integer index, or the values primary or overlay.
+ `--drm-draw-plane` now defaults to primary and `--drm-drmprime-video-plane`
+ defaults to overlay. This should be similar to previous behavior on most drivers
+ due to how planes are usually sorted.
+ - rename --opensles-frames-per-buffer to --opensles-frames-per-enqueue to
+ better reflect its purpose. In the past it overrides the buffer size the AO
+ requests (but not the default/value of the generic --audio-buffer option).
+ Now it only guarantees that the soft buffer size will not be smaller than
+ itself while setting the size per Enqueue.
+ - add --opensles-buffer-size-in-ms, allowing user to tune the soft buffer size.
+ It overrides the --audio-buffer option unless it's set to 0 (with the default
+ being 250).
+ - remove `--linear-scaling`, replaced by `--linear-upscaling` and
+ `--linear-downscaling`. This means that `--sigmoid-upscaling` no longer
+ implies linear light downscaling as well, which was confusing.
+ - the built-in `gpu-hq` profile now includes` --linear-downscaling`.
+ - support for `--spirv-compiler=nvidia` has been removed, leaving `shaderc`
+ as the only option. The `--spirv-compiler` option itself has been marked
+ as deprecated, and may be removed in the future.
+ - split up `--tone-mapping-desaturate`` into strength + exponent, instead of
+ only using a single value (which previously just controlled the exponent).
+ The strength now linearly blends between the linear and nonlinear tone
+ mapped versions of a color.
+ - add --hdr-peak-decay-rate and --hdr-scene-threshold-low/high
+ - add --tone-mapping-max-boost
+ - ipc: require that "request_id" fields are integers. Other types are still
+ accepted for compatibility, but this will stop in the future. Also, if no
+ request_id is provided, 0 will be assumed.
+ - mpv_command_node() and mp.command_native() now support named arguments
+ (see manpage). If you want to use them, use a new version of the manpage
+ as reference, which lists the definitive names.
+ - edition and disc title switching will now fully reload playback (may have
+ consequences for scripts, client API, or when using file-local options)
+ - with the removal of the stream cache, the following properties and options were
+ dropped: `cache`, `cache-size`, `cache-free`, `cache-used`, `--cache-default`,
+ `--cache-initial`, `--cache-seek-min`, `--cache-backbuffer`, `--cache-file`,
+ `--cache-file-size`
+ - the --cache option does not take a number value anymore
+ - remove async playback abort hack. This may make it impossible to abort
+ playback if --demuxer-thread=no is forced.
+ - remove `--macos-title-bar-style`, replaced by `--macos-title-bar-material`
+ and `--macos-title-bar-appearance`.
+ - The default for `--vulkan-async-compute` has changed to `yes` from `no`
+ with the move to libplacebo as the back-end for vulkan rendering.
+ - Remove "disc-titles", "disc-title", "disc-title-list", and "angle"
+ properties. dvd:// does not support title ranges anymore.
+ - Remove all "tv-..." options and properties, along with the classic Linux
+ analog TV support.
+ - remove "program" property (no replacement)
+ - always prefer EGL over GLX, which helps with AMD/vaapi, but will break
+ vdpau with --vo=gpu - use --gpu-context=x11 to be able to use vdpau. This
+ does not affect --vo=vdpau or --hwdec=vdpau-copy.
+ - remove deprecated --chapter option
+ - deprecate --record-file
+ - add `--demuxer-cue-codepage`
+ - add ``track-list/N/demux-bitrate``, ``track-list/N/demux-rotation`` and
+ ``track-list/N/demux-par`` property
+ - Deprecate ``--video-aspect`` and add ``--video-aspect-override`` to
+ replace it. (The `video-aspect` option remains unchanged.)
+ --- mpv 0.29.0 ---
+ - drop --opensles-sample-rate, as --audio-samplerate should be used if desired
+ - drop deprecated --videotoolbox-format, --ff-aid, --ff-vid, --ff-sid,
+ --ad-spdif-dtshd, --softvol options
+ - fix --external-files: strictly never select any tracks from them, unless
+ explicitly selected (this may or may not be expected)
+ - --ytdl is now always enabled, even for libmpv
+ - add a number of --audio-resample-* options, which should from now on be
+ used instead of --af-defaults=lavrresample:...
+ - deprecate --vf-defaults and --af-defaults. These didn't work with the
+ lavfi bridge, so they have very little use left. The only potential use
+ is with af_lavrresample (going to be deprecated, --audio-resample-... set
+ its defaults), and various hw deinterlacing filters (like vf_vavpp), for
+ which you will have to stop using --deinterlace=yes, and instead use the
+ vf toggle commands and the filter enable/disable flag to customize it.
+ - deprecate --af=lavrresample. Use the ``--audio-resample-...`` options to
+ customize resampling, or the libavfilter ``--af=aresample`` filter.
+ - add --osd-on-seek
+ - remove outfmt sub-parameter from "format" video filter (no replacement)
+ - some behavior changes in the video filter chain, including:
+ - before, using an incompatible filter with hwdec would disable hwdec;
+ now it disables the filter at runtime instead
+ - inserting an incompatible filter with hwdec at runtime would refuse
+ to insert the filter; now it will add it successfully, but disables
+ the filter slightly later
+ - some behavior changes in the audio filter chain, including:
+ - a manually inserted lavrresample filter is not necessarily used for
+ sample format conversion anymore, so it's pretty useless
+ - changing playback speed will not respect --af-defaults anymore
+ - having libavfilter based filters after the scaletempo or rubberband
+ filters is not supported anymore, and may desync if playback speed is
+ changed (libavfilter does not support the metadata for playback speed)
+ - the lavcac3enc filter does not auto detach itself anymore; instead it
+ passes through the data after converting it to the sample rate and
+ channel configuration the ac3 encoder expects; also, if the audio
+ format changes midstream in a way that causes the filter to switch
+ between PCM and AC3 output, the audio output won't be reconfigured,
+ and audio playback will fail due to libswresample being unable to
+ convert between PCM and AC3 (Note: the responsible developer didn't
+ give a shit. Later changes might have improved or worsened this.)
+ - inserting a filter that changes the output sample format will not
+ reconfigure the AO - you need to run an additional "ao-reload"
+ command to force this if you want that
+ - using "strong" gapless audio (--gapless-audio=yes) can fail if the
+ audio formats are not convertible (such as switching between PCM and
+ AC3 passthrough)
+ - if filters do not pass through PTS values correctly, A/V sync can
+ result over time. Some libavfilter filters are known to be affected by
+ this, such as af_loudnorm, which can desync over time, depending on
+ how the audio track was muxed (af_lavfi's fix-pts suboption can help).
+ - remove out-format sub-parameter from "format" audio filter (no replacement)
+ - --lavfi-complex now requires uniquely named filter pads. In addition,
+ unconnected filter pads are not allowed anymore (that means every filter
+ pad must be connected either to another filter, or to a video/audio track
+ or video/audio output). If they are disconnected at runtime, the stream
+ will probably stall.
+ - rename --vo=opengl-cb to --vo=libmpv (goes in hand with the opengl-cb
+ API deprecation, see client-api-changes.rst)
+ - deprecate the OpenGL cocoa backend, option choice --gpu-context=cocoa
+ when used with --gpu-api=opengl (use --vo=libmpv)
+ - make --deinterlace=yes always deinterlace, instead of trying to check
+ certain unreliable video metadata. Also flip the defaults of all builtin
+ HW deinterlace filters to always deinterlace.
+ - change vf_vavpp default to use the best deinterlace algorithm by default
+ - remove a compatibility hack that allowed CLI aliases to be set as property
+ (such as "sub-file"), deprecated in mpv 0.26.0
+ - deprecate the old command based hook API, and introduce a proper C API
+ (the high level Lua API for this does not change)
+ - rename the the lua-settings/ config directory to script-opts/
+ - the way the player waits for scripts getting loaded changes slightly. Now
+ scripts are loaded in parallel, and block the player from continuing
+ playback only in the player initialization phase. It could change again in
+ the future. (This kind of waiting was always a feature to prevent that
+ playback is started while scripts are only half-loaded.)
+ - deprecate --ovoffset, --oaoffset, --ovfirst, --oafirst
+ - remove the following encoding options: --ocopyts (now the default, old
+ timestamp handling is gone), --oneverdrop (now default), --oharddup (you
+ need to use --vf=fps=VALUE), --ofps, --oautofps, --omaxfps
+ - remove --video-stereo-mode. This option was broken out of laziness, and
+ nobody wants to fix it. Automatic 3D down-conversion to 2D is also broken,
+ although you can just insert the stereo3d filter manually. The obscurity
+ of 3D content doesn't justify such an option anyway.
+ - change cycle-values command to use the current value, instead of an
+ internal counter that remembered the current position.
+ - remove deprecated ao/vo auto profiles. Consider using scripts like
+ auto-profiles.lua instead.
+ - --[c]scale-[w]param[1|2] and --tone-mapping-param now accept "default",
+ and if set to that value, reading them as property will also return
+ "default", instead of float nan as in previous versions
+ --- mpv 0.28.0 ---
+ - rename --hwdec=mediacodec option to mediacodec-copy, to reflect
+ conventions followed by other hardware video decoding APIs
+ - drop previously deprecated --heartbeat-cmd and --heartbeat--interval
+ options
+ - rename --vo=opengl to --vo=gpu
+ - rename --opengl-backend to --gpu-context
+ - rename --opengl-shaders to --glsl-shaders
+ - rename --opengl-shader-cache-dir to --gpu-shader-cache-dir
+ - rename --opengl-tex-pad-x/y to --gpu-tex-pad-x/y
+ - rename --opengl-fbo-format to --fbo-format
+ - rename --opengl-gamma to --gamma-factor
+ - rename --opengl-debug to --gpu-debug
+ - rename --opengl-sw to --gpu-sw
+ - rename --opengl-vsync-fences to --swapchain-depth, and the interpretation
+ slightly changed. Now defaults to 3.
+ - rename the built-in profile `opengl-hq` to `gpu-hq`
+ - the semantics of --opengl-es=yes are slightly changed -> now requires GLES
+ - remove the (deprecated) alias --gpu-context=drm-egl
+ - remove the (deprecated) --vo=opengl-hq
+ - remove --opengl-es=force2 (use --opengl-es=yes --opengl-restrict=300)
+ - the --msg-level option now affects --log-file
+ - drop "audio-out-detected-device" property - this was unavailable on all
+ audio output drivers for quite a while (coreaudio used to provide it)
+ - deprecate --videotoolbox-format (use --hwdec-image-format, which affects
+ most other hwaccels)
+ - remove deprecated --demuxer-max-packets
+ - remove most of the deprecated audio and video filters
+ - remove the deprecated --balance option/property
+ - rename the --opengl-hwdec-interop option to --gpu-hwdec-interop, and
+ change some of its semantics: extend it take the strings "auto" and
+ "all". "all" loads all backends. "auto" behaves like "all" for
+ vo_opengl_cb, while on vo_gpu it loads nothing, but allows on demand
+ loading by the decoder. The empty string as option value behaves like
+ "auto". Old --hwdec values do not work anymore.
+ This option is hereby declared as unstable and may change any time - its
+ old use is deprecated, and it has very little use outside of debugging
+ now.
+ - change the --hwdec option from a choice to a plain string (affects
+ introspection of the option/property), also affects some properties
+ - rename --hwdec=rpi to --hwdec=mmal, same for the -copy variant (no
+ backwards compatibility)
+ - deprecate the --ff-aid, --ff-vid, --ff-sid options and properties (there is
+ no replacement, but you can manually query the track property and use the
+ "ff-index" field to find the mpv track ID to imitate this behavior)
+ - rename --no-ometadata to --no-ocopy-metadata
+ --- mpv 0.27.0 ---
+ - drop previously deprecated --field-dominance option
+ - drop previously deprecated "osd" command
+ - remove client API compatibility handling for "script", "sub-file",
+ "audio-file", "external-file" (these cases used to log a deprecation
+ warning)
+ - drop deprecated --video-aspect-method=hybrid option choice
+ - rename --hdr-tone-mapping to --tone-mapping (and generalize it)
+ - --opengl-fbo-format changes from a choice to a string. Also, its value
+ will be checked only on renderer initialization, rather than when the
+ option is set.
+ - Using opengl-cb now always assumes 8 bit per component depth, and dithers
+ to this size. Before, it tried to figure out the depth of the first
+ framebuffer that was ever passed to the renderer. Having GL framebuffers
+ with a size larger than 8 bit per component is quite rare. If you need
+ it, set the --dither-depth option instead.
+ - --lavfi-complex can now be set during runtime. If you set this in
+ expectation it would be applied only after a reload, you might observe
+ weird behavior.
+ - add --track-auto-selection to help with scripts/applications that
+ make exclusive use of --lavfi-complex.
+ - undeprecate --loop, and map it from --loop-playlist to --loop-file (the
+ deprecation was to make sure no API user gets broken by a sudden behavior
+ change)
+ - remove previously deprecated vf_eq
+ - remove that hardware deinterlace filters (vavpp, d3d11vpp, vdpaupp)
+ changed their deinterlacing-enabled setting depending on what the
+ --deinterlace option or property was set to. Now, a filter always does
+ what its filter options and defaults imply. The --deinterlace option and
+ property strictly add/remove its own filters. For example, if you run
+ "mpv --vf=vavpp --deinterlace=yes", this will insert another, redundant
+ filter, which is probably not what you want. For toggling a deinterlace
+ filter manually, use the "vf toggle" command, and do not set the
+ deinterlace option/property. To customize the filter that will be
+ inserted automatically, use --vf-defaults. Details how this works will
+ probably change in the future.
+ - remove deinterlace=auto (this was not deprecated, but had only a very
+ obscure use that stopped working with the change above. It was also
+ prone to be confused with a feature not implemented by it: auto did _not_
+ mean that deinterlacing was enabled on demand.)
+ - add shortened mnemonic names for mouse button bindings, eg. mbtn_left
+ the old numeric names (mouse_btn0) are deprecated
+ - remove mouse_btn3_dbl and up, since they are only generated for buttons
+ 0-2 (these now print an error when sent from the 'mouse' command)
+ - rename the axis bindings to wheel_up/down/etc. axis scrolling and mouse
+ wheel scrolling are now conceptually the same thing
+ the old axis_up/down names remain as deprecated aliases
+ --- mpv 0.26.0 ---
+ - remove remaining deprecated audio device options, like --alsa-device
+ Some of them were removed in earlier releases.
+ - introduce --replaygain... options, which replace the same functionality
+ provided by the deprecated --af=volume:replaygain... mechanism.
+ - drop the internal "mp-rawvideo" codec (used by --demuxer=rawvideo)
+ - rename --sub-ass-style-override to --sub-ass-override, and rename the
+ `--sub-ass-override=signfs` setting to `--sub-ass-override=scale`.
+ - change default of --video-aspect-method to "bitstream". The "hybrid"
+ method (old default) is deprecated.
+ - remove property "video-params/nom-peak"
+ - remove option --target-brightness
+ - replace vf_format's `peak` suboption by `sig-peak`, which is relative to
+ the reference white level instead of in cd/m^2
+ - renamed the TRCs `st2084` and `std-b67` to `pq` and `hlg` respectively
+ - the "osd" command is deprecated (use "cycle osd-level")
+ - --field-dominance is deprecated (use --vf=setfield=bff or tff)
+ - --really-quiet subtle behavior change
+ - the deprecated handling of setting "no-" options via client API is dropped
+ - the following options change to append-by-default (and possibly separator):
+ --script
+ also, the following options are deprecated:
+ --sub-paths => --sub-file-paths
+ the following options are deprecated for setting via API:
+ "script" (use "scripts")
+ "sub-file" (use "sub-files")
+ "audio-file" (use "audio-files")
+ "external-file" (use "external-files")
+ (the compatibility hacks for this will be removed after this release)
+ - remove property `vo-performance`, and add `vo-passes` as a more general
+ replacement
+ - deprecate passing multiple arguments to -add/-pre options (affects the
+ vf/af commands too)
+ - remove --demuxer-lavf-cryptokey. Use --demux-lavf-o=cryptokey=<hex> or
+ --demux-lavf-o=decryption_key=<hex> instead (whatever fits your situation).
+ - rename --opengl-dumb-mode=no to --opengl-dumb-mode=auto, and make `no`
+ always disable it (unless forced on by hardware limitation).
+ - generalize --scale-clamp, --cscale-clamp etc. to accept a float between
+ 0.0 and 1.0 instead of just being a flag. A value of 1.0 corresponds to
+ the old `yes`, and a value of 0.0 corresponds to the old `no`.
+ --- mpv 0.25.0 ---
+ - remove opengl-cb dxva2 dummy hwdec interop
+ (see git "vo_opengl: remove dxva2 dummy hwdec backend")
+ - remove ppm, pgm, pgmyuv, tga choices from the --screenshot-format and
+ --vo-image-format options
+ - the "jpeg" choice in the option above now leads to a ".jpg" file extension
+ - --af=drc is gone (you can use e.g. lavfi/acompressor instead)
+ - remove image_size predefined uniform from OpenGL user shaders. Use
+ input_size instead
+ - add --sub-filter-sdh
+ - add --sub-filter-sdh-harder
+ - remove --input-app-events option (macOS)
+ - deprecate most --vf and --af filters. Only some filters not in libavfilter
+ will be kept.
+ Also, you can use libavfilter filters directly (e.g. you can use
+ --vf=name=opts instead of --vf=lavfi=[name=opts]), as long as the
+ libavfilter filter's name doesn't clash with a mpv builtin filter.
+ In the long term, --vf/--af syntax might change again, but if it does, it
+ will switch to libavfilter's native syntax. (The above mentioned direct
+ support for lavfi filters still has some differences, such as how strings
+ are escaped.) If this happens, the non-deprecated builtin filters might be
+ moved to "somewhere else" syntax-wise.
+ - deprecate --loop - after a deprecation period, it will be undeprecated,
+ but changed to alias --loop-file
+ - add --keep-open-pause=no
+ - deprecate --demuxer-max-packets
+ - change --audio-file-auto default from "exact" to "no" (mpv won't load
+ files with the same filename as the video, but different extension, as
+ audio track anymore)
+ --- mpv 0.24.0 ---
+ - deprecate --hwdec-api and replace it with --opengl-hwdec-interop.
+ The new option accepts both --hwdec values, as well as named backends.
+ A minor difference is that --hwdec-api=no (which used to be the default)
+ now actually does not preload any interop layer, while the new default
+ ("") uses the value of --hwdec.
+ - drop deprecated --ad/--vd features
+ - drop deprecated --sub-codepage syntax
+ - rename properties:
+ - "drop-frame-count" to "decoder-frame-drop-count"
+ - "vo-drop-frame-count" to "frame-drop-count"
+ The old names still work, but are deprecated.
+ - remove the --stream-capture option and property. No replacement.
+ (--record-file might serve as alternative)
+ - add --sub-justify
+ - add --sub-ass-justify
+ - internally there's a different way to enable the demuxer cache now
+ it can be auto-enabled even if the stream cache remains disabled
+ --- mpv 0.23.0 ---
+ - remove deprecated vf_vdpaurb (use "--hwdec=vdpau-copy" instead)
+ - the following properties now have new semantics:
+ - "demuxer" (use "current-demuxer")
+ - "fps" (use "container-fps")
+ - "idle" (use "idle-active")
+ - "cache" (use "cache-percent")
+ - "audio-samplerate" (use "audio-params/samplerate")
+ - "audio-channels" (use "audio-params/channel-count")
+ - "audio-format" (use "audio-codec-name")
+ (the properties equivalent to the old semantics are in parentheses)
+ - remove deprecated --vo and --ao sub-options (like --vo=opengl:...), and
+ replace them with global options. A somewhat complete list can be found
+ here: https://github.com/mpv-player/mpv/wiki/Option-replacement-list#mpv-0210
+ - remove --vo-defaults and --ao-defaults as well
+ - remove deprecated global sub-options (like -demuxer-rawaudio format=...),
+ use flat options (like --demuxer-rawaudio-format=...)
+ - the --sub-codepage option changes in incompatible ways:
+ - detector-selection and fallback syntax is deprecated
+ - enca/libguess are removed and deprecated (behaves as if they hadn't
+ been compiled-in)
+ - --sub-codepage=<codepage> does not force the codepage anymore
+ (this requires different and new syntax)
+ - remove --fs-black-out-screens option for macOS
+ - change how spdif codecs are selected. You can't enable spdif passthrough
+ with --ad anymore. This was deprecated; use --audio-spdif instead.
+ - deprecate the "family" selection with --ad/--vd
+ forcing/excluding codecs with "+", "-", "-" is deprecated as well
+ - explicitly mark --ad-spdif-dtshd as deprecated (it was done so a long time
+ ago, but it didn't complain when using the option)
+ --- mpv 0.22.0 ---
+ - the "audio-device-list" property now sets empty device description to the
+ device name as a fallback
+ - add --hidpi-window-scale option for macOS
+ - add audiounit audio output for iOS
+ - make --start-time work with --rebase-start-time=no
+ - add --opengl-early-flush=auto mode
+ - add --hwdec=vdpau-copy, deprecate vf_vdpaurb
+ - add tct video output for true-color and 256-color terminals
+ --- mpv 0.21.0 ---
+ - unlike in older versions, setting options at runtime will now take effect
+ immediately (see for example issue #3281). On the other hand, it will also
+ do runtime verification and reject option changes that do not work
+ (example: setting the "vf" option to a filter during playback, which fails
+ to initialize - the option value will remain at its old value). In general,
+ "set name value" should be mostly equivalent to "set options/name value"
+ in cases where the "name" property is not deprecated and "options/name"
+ exists - deviations from this are either bugs, or documented as caveats
+ in the "Inconsistencies between options and properties" manpage section.
+ - deprecate _all_ --vo and --ao suboptions. Generally, all suboptions are
+ replaced by global options, which do exactly the same. For example,
+ "--vo=opengl:scale=nearest" turns into "--scale=nearest". In some cases,
+ the global option is prefixed, e.g. "--vo=opengl:pbo" turns into
+ "--opengl-pbo".
+ Most of the exact replacements are documented here:
+ https://github.com/mpv-player/mpv/wiki/Option-replacement-list
+ - remove --vo=opengl-hq. Set --profile=opengl-hq instead. Note that this
+ profile does not force the VO. This means if you use the --vo option to
+ set another VO, it won't work. But this also means it can be used with
+ opengl-cb.
+ - remove the --vo=opengl "pre-shaders", "post-shaders" and "scale-shader"
+ sub-options: they were deprecated in favor of "user-shaders"
+ - deprecate --vo-defaults (no replacement)
+ - remove the vo-cmdline command. You can set OpenGL renderer options
+ directly via properties instead.
+ - deprecate the device/sink options on all AOs. Use --audio-device instead.
+ - deprecate "--ao=wasapi:exclusive" and "--ao=coreaudio:exclusive",
+ use --audio-exclusive instead.
+ - subtle changes in how "--no-..." options are treated mean that they are
+ not accessible under "options/..." anymore (instead, these are resolved
+ at parsing time). This does not affect options which start with "--no-",
+ but do not use the mechanism for negation options.
+ (Also see client API change for API version 1.23.)
+ - rename the following properties
+ - "demuxer" -> "current-demuxer"
+ - "fps" -> "container-fps"
+ - "idle" -> "idle-active"
+ - "cache" -> "cache-percent"
+ the old names are deprecated and will change behavior in mpv 0.23.0.
+ - remove deprecated "hwdec-active" and "hwdec-detected" properties
+ - deprecate the ao and vo auto-profiles (they never made any sense)
+ - deprecate "--vo=direct3d_shaders" - use "--vo=direct3d" instead.
+ Change "--vo=direct3d" to always use shaders by default.
+ - deprecate --playlist-pos option, renamed to --playlist-start
+ - deprecate the --chapter option, as it is redundant with --start/--end,
+ and conflicts with the semantics of the "chapter" property
+ - rename --sub-text-* to --sub-* and --ass-* to --sub-ass-* (old options
+ deprecated)
+ - incompatible change to cdda:// protocol options: the part after cdda://
+ now always sets the device, not the span or speed to be played. No
+ separating extra "/" is needed. The hidden --cdda-device options is also
+ deleted (it was redundant with the documented --cdrom-device).
+ - deprecate --vo=rpi. It will be removed in mpv 0.23.0. Its functionality
+ was folded into --vo=opengl, which now uses RPI hardware decoding by
+ treating it as a hardware overlay (without applying GL filtering). Also
+ to be changed in 0.23.0: the --fs flag will be reset to "no" by default
+ (like on the other platforms).
+ - deprecate --mute=auto (informally has been since 0.18.1)
+ - deprecate "resume" and "suspend" IPC commands. They will be completely
+ removed in 0.23.0.
+ - deprecate mp.suspend(), mp.resume(), mp.resume_all() Lua scripting
+ commands, as well as setting mp.use_suspend. They will be completely
+ removed in 0.23.0.
+ - the "seek" command's absolute seek mode will now interpret negative
+ seek times as relative from the end of the file (and clamps seeks that
+ still go before 0)
+ - add almost all options to the property list, meaning you can change
+ options without adding "options/" to the property name (a new section
+ has been added to the manpage describing some conflicting behavior
+ between options and properties)
+ - implement changing sub-speed during playback
+ - make many previously fixed options changeable at runtime (for example
+ --terminal, --osc, --ytdl, can all be enable/disabled after
+ mpv_initialize() - this can be extended to other still fixed options
+ on user requests)
+ --- mpv 0.20.0 ---
+ - add --image-display-duration option - this also means that image duration
+ is not influenced by --mf-fps anymore in the general case (this is an
+ incompatible change)
+ --- mpv 0.19.0 ---
+ - deprecate "balance" option/property (no replacement)
+ --- mpv 0.18.1 ---
+ - deprecate --heartbeat-cmd
+ - remove --softvol=no capability:
+ - deprecate --softvol, it now does nothing
+ - --volume, --mute, and the corresponding properties now always control
+ softvol, and behave as expected without surprises (e.g. you can set
+ them normally while no audio is initialized)
+ - rename --softvol-max to --volume-max (deprecated alias is added)
+ - the --volume-restore-data option and property are removed without
+ replacement. They were _always_ internal, and used for watch-later
+ resume/restore. Now --volume/--mute are saved directly instead.
+ - the previous point means resuming files with older watch-later configs
+ will print an error about missing --volume-restore-data (which you can
+ ignore), and will not restore the previous value
+ - as a consequence, volume controls will no longer control PulseAudio
+ per-application value, or use the system mixer's per-application
+ volume processing
+ - system or per-application volume can still be controlled with the
+ ao-volume and ao-mute properties (there are no command line options)
+ --- mpv 0.18.0 ---
+ - now ab-loops are active even if one of the "ab-loop-a"/"-b" properties is
+ unset ("no"), in which case the start of the file is used if the A loop
+ point is unset, and the end of the file for an unset B loop point
+ - deprecate --sub-ass=no option by --ass-style-override=strip
+ (also needs --embeddedfonts=no)
+ - add "hwdec-interop" and "hwdec-current" properties
+ - deprecated "hwdec-active" and "hwdec-detected" properties (to be removed
+ in mpv 0.20.0)
+ - choice option/property values that are "yes" or "no" will now be returned
+ as booleans when using the mpv_node functions in the client API, the
+ "native" property accessors in Lua, and the JSON API. They can be set as
+ such as well.
+ - the VO opengl fbo-format sub-option does not accept "rgb" or "rgba"
+ anymore
+ - all VO opengl prescalers have been removed (replaced by user scripts)
+ --- mpv 0.17.0 ---
+ - deprecate "track-list/N/audio-channels" property (use
+ "track-list/N/demux-channel-count" instead)
+ - remove write access to "stream-pos", and change semantics for read access
+ - Lua scripts now don't suspend mpv by default while script code is run
+ - add "cache-speed" property
+ - rename --input-unix-socket to --input-ipc-server, and make it work on
+ Windows too
+ - change the exact behavior of the "video-zoom" property
+ - --video-unscaled no longer disables --video-zoom and --video-aspect
+ To force the old behavior, set --video-zoom=0 and --video-aspect=0
+ --- mpv 0.16.0 ---
+ - change --audio-channels default to stereo (use --audio-channels=auto to
+ get the old default)
+ - add --audio-normalize-downmix
+ - change the default downmix behavior (--audio-normalize-downmix=yes to get
+ the old default)
+ - VO opengl custom shaders must now use "sample_pixel" as function name,
+ instead of "sample"
+ - change VO opengl scaler-resizes-only default to enabled
+ - add VO opengl "interpolation-threshold" suboption (introduces new default
+ behavior, which can change e.g. ``--video-sync=display-vdrop`` to the
+ worse, but is usually what you want)
+ - make "volume" and "mute" properties changeable even if no audio output is
+ active (this gives not-ideal behavior if --softvol=no is used)
+ - add "volume-max" and "mixer-active" properties
+ - ignore --input-cursor option for events injected by input commands like
+ "mouse", "keydown", etc.
+ --- mpv 0.15.0 ---
+ - change "yadif" video filter defaults
+ --- mpv 0.14.0 ---
+ - vo_opengl interpolation now requires --video-sync=display-... to be set
+ - change some vo_opengl defaults (including changing tscale)
+ - add "vsync-ratio", "estimated-display-fps" properties
+ - add --rebase-start-time option
+ This is a breaking change to start time handling. Instead of making start
+ time handling an aspect of different options and properties (like
+ "time-pos" vs. "playback-time"), make it dependent on the new option. For
+ compatibility, the "time-start" property now always returns 0, so code
+ which attempted to handle rebasing manually will not break.
+ --- mpv 0.13.0 ---
+ - remove VO opengl-cb frame queue suboptions (no replacement)
+ --- mpv 0.12.0 ---
+ - remove --use-text-osd (useless; fontconfig isn't a requirement anymore,
+ and text rendering is also lazily initialized)
+ - some time properties (at least "playback-time", "time-pos",
+ "time-remaining", "playtime-remaining") now are unavailable if the time
+ is unknown, instead of just assuming that the internal playback position
+ is 0
+ - add --audio-fallback-to-null option
+ - replace vf_format outputlevels suboption with "video-output-levels" global
+ property/option; also remove "colormatrix-output-range" property
+ - vo_opengl: remove sharpen3/sharpen5 scale filters, add sharpen sub-option
+ --- mpv 0.11.0 ---
+ - add "af-metadata" property
+ --- mpv 0.10.0 ---
+ - add --video-aspect-method option
+ - add --playlist-pos option
+ - add --video-sync* options
+ "display-sync-active" property
+ "vo-missed-frame-count" property
+ "audio-speed-correction" and "video-speed-correction" properties
+ - remove --demuxer-readahead-packets and --demuxer-readahead-bytes
+ add --demuxer-max-packets and --demuxer-max-bytes
+ (the new options are not replacement and have very different semantics)
+ - change "video-aspect" property: always settable, even if no video is
+ running; always return the override - if no override is set, return
+ the video's aspect ratio
+ - remove disc-nav (DVD, BD) related properties and commands
+ - add "option-info/<name>/set-locally" property
+ - add --cache-backbuffer; change --cache-default default to 75MB
+ the new total cache size is the sum of backbuffer and the cache size
+ specified by --cache-default or --cache
+ - add ``track-list/N/audio-channels`` property
+ - change --screenshot-tag-colorspace default value
+ - add --stretch-image-subs-to-screen
+ - add "playlist/N/title" property
+ - add --video-stereo-mode=no to disable auto-conversions
+ - add --force-seekable, and change default seekability in some cases
+ - add vf yadif/vavpp/vdpaupp interlaced-only suboptions
+ Also, the option is enabled by default (Except vf_yadif, which has
+ it enabled only if it's inserted by the deinterlace property.)
+ - add --hwdec-preload
+ - add ao coreaudio exclusive suboption
+ - add ``track-list/N/forced`` property
+ - add audio-params/channel-count and ``audio-params-out/channel-count props.
+ - add af volume replaygain-fallback suboption
+ - add video-params/stereo-in property
+ - add "keypress", "keydown", and "keyup" commands
+ - deprecate --ad-spdif-dtshd and enabling passthrough via --ad
+ add --audio-spdif as replacement
+ - remove "get_property" command
+ - remove --slave-broken
+ - add vo opengl custom shader suboptions (source-shader, scale-shader,
+ pre-shaders, post-shaders)
+ - completely change how the hwdec properties work:
+ - "hwdec" now reflects the --hwdec option
+ - "hwdec-detected" does partially what the old "hwdec" property did
+ (and also, "detected-hwdec" is removed)
+ - "hwdec-active" is added
+ - add protocol-list property
+ - deprecate audio-samplerate and audio-channels properties
+ (audio-params sub-properties are the replacement)
+ - add audio-params and audio-out-params properties
+ - deprecate "audio-format" property, replaced with "audio-codec-name"
+ - deprecate --media-title, replaced with --force-media-title
+ - deprecate "length" property, replaced with "duration"
+ - change volume property:
+ - the value 100 is now always "unchanged volume" - with softvol, the
+ range is 0 to --softvol-max, without it is 0-100
+ - the minimum value of --softvol-max is raised to 100
+ - remove vo opengl npot suboption
+ - add relative seeking by percentage to "seek" command
+ - add playlist_shuffle command
+ - add --force-window=immediate
+ - add ao coreaudio change-physical-format suboption
+ - remove vo opengl icc-cache suboption, add icc-cache-dir suboption
+ - add --screenshot-directory
+ - add --screenshot-high-bit-depth
+ - add --screenshot-jpeg-source-chroma
+ - default action for "rescan_external_files" command changes
+ --- mpv 0.9.0 ---
diff --git a/DOCS/man/af.rst b/DOCS/man/af.rst
new file mode 100644
index 0000000..98f9a95
--- /dev/null
+++ b/DOCS/man/af.rst
@@ -0,0 +1,267 @@
+AUDIO FILTERS
+=============
+
+Audio filters allow you to modify the audio stream and its properties. The
+syntax is:
+
+``--af=...``
+ Setup a chain of audio filters. See ``--vf`` (`VIDEO FILTERS`_) for the
+ full syntax.
+
+.. note::
+
+ To get a full list of available audio filters, see ``--af=help``.
+
+ Also, keep in mind that most actual filters are available via the ``lavfi``
+ wrapper, which gives you access to most of libavfilter's filters. This
+ includes all filters that have been ported from MPlayer to libavfilter.
+
+ The ``--vf`` description describes how libavfilter can be used and how to
+ workaround deprecated mpv filters.
+
+See ``--vf`` group of options for info on how ``--af-add``, ``--af-pre``,
+``--af-clr``, and possibly others work.
+
+Available filters are:
+
+``lavcac3enc[=options]``
+ Encode multi-channel audio to AC-3 at runtime using libavcodec. Supports
+ 16-bit native-endian input format, maximum 6 channels. The output is
+ big-endian when outputting a raw AC-3 stream, native-endian when
+ outputting to S/PDIF. If the input sample rate is not 48 kHz, 44.1 kHz or
+ 32 kHz, it will be resampled to 48 kHz.
+
+ ``tospdif=<yes|no>``
+ Output raw AC-3 stream if ``no``, output to S/PDIF for
+ pass-through if ``yes`` (default).
+
+ ``bitrate=<rate>``
+ The bitrate use for the AC-3 stream. Set it to 384 to get 384 kbps.
+
+ The default is 640. Some receivers might not be able to handle this.
+
+ Valid values: 32, 40, 48, 56, 64, 80, 96, 112, 128,
+ 160, 192, 224, 256, 320, 384, 448, 512, 576, 640.
+
+ The special value ``auto`` selects a default bitrate based on the
+ input channel number:
+
+ :1ch: 96
+ :2ch: 192
+ :3ch: 224
+ :4ch: 384
+ :5ch: 448
+ :6ch: 448
+
+ ``minch=<n>``
+ If the input channel number is less than ``<minch>``, the filter will
+ detach itself (default: 3).
+
+ ``encoder=<name>``
+ Select the libavcodec encoder used. Currently, this should be an AC-3
+ encoder, and using another codec will fail horribly.
+
+``format=format:srate:channels:out-srate:out-channels``
+ Does not do any format conversion itself. Rather, it may cause the
+ filter system to insert necessary conversion filters before or after this
+ filter if needed. It is primarily useful for controlling the audio format
+ going into other filters. To specify the format for audio output, see
+ ``--audio-format``, ``--audio-samplerate``, and ``--audio-channels``. This
+ filter is able to force a particular format, whereas ``--audio-*``
+ may be overridden by the ao based on output compatibility.
+
+ All parameters are optional. The first 3 parameters restrict what the filter
+ accepts as input. They will therefore cause conversion filters to be
+ inserted before this one. The ``out-`` parameters tell the filters or audio
+ outputs following this filter how to interpret the data without actually
+ doing a conversion. Setting these will probably just break things unless you
+ really know you want this for some reason, such as testing or dealing with
+ broken media.
+
+ ``<format>``
+ Force conversion to this format. Use ``--af=format=format=help`` to get
+ a list of valid formats.
+
+ ``<srate>``
+ Force conversion to a specific sample rate. The rate is an integer,
+ 48000 for example.
+
+ ``<channels>``
+ Force mixing to a specific channel layout. See ``--audio-channels`` option
+ for possible values.
+
+ ``<out-srate>``
+
+ ``<out-channels>``
+
+ *NOTE*: this filter used to be named ``force``. The old ``format`` filter
+ used to do conversion itself, unlike this one which lets the filter system
+ handle the conversion.
+
+``scaletempo[=option1:option2:...]``
+ Scales audio tempo without altering pitch, optionally synced to playback
+ speed.
+
+ This works by playing 'stride' ms of audio at normal speed then consuming
+ 'stride*scale' ms of input audio. It pieces the strides together by
+ blending 'overlap'% of stride with audio following the previous stride. It
+ optionally performs a short statistical analysis on the next 'search' ms
+ of audio to determine the best overlap position.
+
+ ``scale=<amount>``
+ Nominal amount to scale tempo. Scales this amount in addition to
+ speed. (default: 1.0)
+ ``stride=<amount>``
+ Length in milliseconds to output each stride. Too high of a value will
+ cause noticeable skips at high scale amounts and an echo at low scale
+ amounts. Very low values will alter pitch. Increasing improves
+ performance. (default: 60)
+ ``overlap=<factor>``
+ Factor of stride to overlap. Decreasing improves performance.
+ (default: .20)
+ ``search=<amount>``
+ Length in milliseconds to search for best overlap position. Decreasing
+ improves performance greatly. On slow systems, you will probably want
+ to set this very low. (default: 14)
+ ``speed=<tempo|pitch|both|none>``
+ Set response to speed change.
+
+ tempo
+ Scale tempo in sync with speed (default).
+ pitch
+ Reverses effect of filter. Scales pitch without altering tempo.
+ Add this to your ``input.conf`` to step by musical semi-tones::
+
+ [ multiply speed 0.9438743126816935
+ ] multiply speed 1.059463094352953
+
+ .. warning::
+
+ Loses sync with video.
+ both
+ Scale both tempo and pitch.
+ none
+ Ignore speed changes.
+
+ .. admonition:: Examples
+
+ ``mpv --af=scaletempo --speed=1.2 media.ogg``
+ Would play media at 1.2x normal speed, with audio at normal
+ pitch. Changing playback speed would change audio tempo to match.
+
+ ``mpv --af=scaletempo=scale=1.2:speed=none --speed=1.2 media.ogg``
+ Would play media at 1.2x normal speed, with audio at normal
+ pitch, but changing playback speed would have no effect on audio
+ tempo.
+
+ ``mpv --af=scaletempo=stride=30:overlap=.50:search=10 media.ogg``
+ Would tweak the quality and performance parameters.
+
+ ``mpv --af=scaletempo=scale=1.2:speed=pitch audio.ogg``
+ Would play media at 1.2x normal speed, with audio at normal pitch.
+ Changing playback speed would change pitch, leaving audio tempo at
+ 1.2x.
+
+``scaletempo2[=option1:option2:...]``
+ Scales audio tempo without altering pitch.
+ The algorithm is ported from chromium and uses the
+ Waveform Similarity Overlap-and-add (WSOLA) method.
+ It seems to achieves higher audio quality than scaletempo, and rubberband R2
+ engine, or ``engine=faster``. This filter is inserted automatically if
+ ``audio-pitch-correction`` option is used (on by default) when the playback
+ speed is changed.
+
+ By default, the ``search-interval`` and ``window-size`` parameters
+ have the same values as in chromium.
+
+ ``min-speed=<speed>``
+ Mute audio if the playback speed is below ``<speed>``. (default: 0.25)
+
+ ``max-speed=<speed>``
+ Mute audio if the playback speed is above ``<speed>``
+ and ``<speed> != 0``. (default: 8.0)
+
+ ``search-interval=<amount>``
+ Length in milliseconds to search for best overlap position. (default: 40)
+
+ ``window-size=<amount>``
+ Length in milliseconds of the overlap-and-add window. (default: 12)
+
+``rubberband``
+ High quality pitch correction with librubberband. This can be used in place
+ of ``scaletempo`` and ``scaletempo2``, and will be used to adjust audio pitch
+ when playing at speed different from normal. It can also be used to adjust
+ audio pitch without changing playback speed.
+
+ ``pitch-scale=<amount>``
+ Sets the pitch scaling factor. Frequencies are multiplied by this value.
+ (default: 1.0)
+
+ ``engine=<faster|finer>``
+ Select the core Rubberband engine to be used. There are two available:
+
+ :Faster: This is the Rubberband R2 engine. It uses significantly less
+ CPU than the Finer (R3) engine.
+ :Finer: This is the Rubberband R3 engine. This engine is only available
+ with librubberband version 3 or newer. This produces significantly
+ higher quality output, at the cost of higher CPU usage. (Default
+ if available)
+
+ This filter has a number of additional sub-options. You can list them with
+ ``mpv --af=rubberband=help``. This will also show the default values
+ for each option. The options are not documented here, because they are
+ merely passed to librubberband. Look at the librubberband documentation
+ to learn what each option does:
+ https://breakfastquay.com/rubberband/code-doc/classRubberBand_1_1RubberBandStretcher.html
+ Do note that certain options are only applicable to one of R2 (faster) and
+ R3 (finer) engines.
+ (The mapping of the mpv rubberband filter sub-option names and values to
+ those of librubberband follows a simple pattern: ``"Option" + Name + Value``.)
+
+ This filter supports the following ``af-command`` commands:
+
+ ``set-pitch``
+ Set the ``<pitch-scale>`` argument dynamically. This can be used to
+ change the playback pitch at runtime. Note that speed is controlled
+ using the standard ``speed`` property, not ``af-command``.
+
+ ``multiply-pitch <factor>``
+ Multiply the current value of ``<pitch-scale>`` dynamically. For
+ example: 0.5 to go down by an octave, 1.5 to go up by a perfect fifth.
+ If you want to go up or down by semi-tones, use 1.059463094352953 and
+ 0.9438743126816935
+
+``lavfi=graph``
+ Filter audio using FFmpeg's libavfilter.
+
+ ``<graph>``
+ Libavfilter graph. See ``lavfi`` video filter for details - the graph
+ syntax is the same.
+
+ .. warning::
+
+ Don't forget to quote libavfilter graphs as described in the lavfi
+ video filter section.
+
+ ``o=<string>``
+ AVOptions.
+
+ ``fix-pts=<yes|no>``
+ Determine PTS based on sample count (default: no). If this is enabled,
+ the player won't rely on libavfilter passing through PTS accurately.
+ Instead, it pass a sample count as PTS to libavfilter, and compute the
+ PTS used by mpv based on that and the input PTS. This helps with filters
+ which output a recomputed PTS instead of the original PTS (including
+ filters which require the PTS to start at 0). mpv normally expects
+ filters to not touch the PTS (or only to the extent of changing frame
+ boundaries), so this is not the default, but it will be needed to use
+ broken filters. In practice, these broken filters will either cause slow
+ A/V desync over time (with some files), or break playback completely if
+ you seek or start playback from the middle of a file.
+
+``drop``
+ This filter drops or repeats audio frames to adapt to playback speed. It
+ always operates on full audio frames, because it was made to handle SPDIF
+ (compressed audio passthrough). This is used automatically if the
+ ``--video-sync=display-adrop`` option is used. Do not use this filter (or
+ the given option); they are extremely low quality.
diff --git a/DOCS/man/ao.rst b/DOCS/man/ao.rst
new file mode 100644
index 0000000..4e4e454
--- /dev/null
+++ b/DOCS/man/ao.rst
@@ -0,0 +1,249 @@
+AUDIO OUTPUT DRIVERS
+====================
+
+Audio output drivers are interfaces to different audio output facilities. The
+syntax is:
+
+``--ao=<driver1,driver2,...[,]>``
+ Specify a priority list of audio output drivers to be used.
+
+If the list has a trailing ',', mpv will fall back on drivers not contained
+in the list.
+
+.. note::
+
+ See ``--ao=help`` for a list of compiled-in audio output drivers. The
+ driver ``--ao=alsa`` is preferred. ``--ao=pulse`` is preferred on systems
+ where PulseAudio is used. On BSD systems, ``--ao=oss`` is preferred.
+
+Available audio output drivers are:
+
+``alsa`` (Linux only)
+ ALSA audio output driver
+
+ See `ALSA audio output options`_ for options specific to this AO.
+
+ .. warning::
+
+ To get multichannel/surround audio, use ``--audio-channels=auto``. The
+ default for this option is ``auto-safe``, which makes this audio output
+ explicitly reject multichannel output, as there is no way to detect
+ whether a certain channel layout is actually supported.
+
+ You can also try `using the upmix plugin
+ <https://github.com/mpv-player/mpv/wiki/ALSA-Surround-Sound-and-Upmixing>`_.
+ This setup enables multichannel audio on the ``default`` device
+ with automatic upmixing with shared access, so playing stereo
+ and multichannel audio at the same time will work as expected.
+
+``oss``
+ OSS audio output driver
+
+``jack``
+ JACK (Jack Audio Connection Kit) audio output driver.
+
+ The following global options are supported by this audio output:
+
+ ``--jack-port=<name>``
+ Connects to the ports with the given name (default: physical ports).
+ ``--jack-name=<client>``
+ Client name that is passed to JACK (default: ``mpv``). Useful
+ if you want to have certain connections established automatically.
+ ``--jack-autostart=<yes|no>``
+ Automatically start jackd if necessary (default: disabled). Note that
+ this tends to be unreliable and will flood stdout with server messages.
+ ``--jack-connect=<yes|no>``
+ Automatically create connections to output ports (default: enabled).
+ When enabled, the maximum number of output channels will be limited to
+ the number of available output ports.
+ ``--jack-std-channel-layout=<waveext|any>``
+ Select the standard channel layout (default: waveext). JACK itself has no
+ notion of channel layouts (i.e. assigning which speaker a given
+ channel is supposed to map to) - it just takes whatever the application
+ outputs, and reroutes it to whatever the user defines. This means the
+ user and the application are in charge of dealing with the channel
+ layout. ``waveext`` uses WAVE_FORMAT_EXTENSIBLE order, which, even
+ though it was defined by Microsoft, is the standard on many systems.
+ The value ``any`` makes JACK accept whatever comes from the audio
+ filter chain, regardless of channel layout and without reordering. This
+ mode is probably not very useful, other than for debugging or when used
+ with fixed setups.
+
+``coreaudio`` (macOS only)
+ Native macOS audio output driver using AudioUnits and the CoreAudio
+ sound server.
+
+ Automatically redirects to ``coreaudio_exclusive`` when playing compressed
+ formats.
+
+ The following global options are supported by this audio output:
+
+ ``--coreaudio-change-physical-format=<yes|no>``
+ Change the physical format to one similar to the requested audio format
+ (default: no). This has the advantage that multichannel audio output
+ will actually work. The disadvantage is that it will change the
+ system-wide audio settings. This is equivalent to changing the ``Format``
+ setting in the ``Audio Devices`` dialog in the ``Audio MIDI Setup``
+ utility. Note that this does not affect the selected speaker setup.
+
+ ``--coreaudio-spdif-hack=<yes|no>``
+ Try to pass through AC3/DTS data as PCM. This is useful for drivers
+ which do not report AC3 support. It converts the AC3 data to float,
+ and assumes the driver will do the inverse conversion, which means
+ a typical A/V receiver will pick it up as compressed IEC framed AC3
+ stream, ignoring that it's marked as PCM. This disables normal AC3
+ passthrough (even if the device reports it as supported). Use with
+ extreme care.
+
+
+``coreaudio_exclusive`` (macOS only)
+ Native macOS audio output driver using direct device access and
+ exclusive mode (bypasses the sound server).
+
+``openal``
+ OpenAL audio output driver.
+
+ ``--openal-num-buffers=<2-128>``
+ Specify the number of audio buffers to use. Lower values are better for
+ lower CPU usage. Default: 4.
+
+ ``--openal-num-samples=<256-32768>``
+ Specify the number of complete samples to use for each buffer. Higher
+ values are better for lower CPU usage. Default: 8192.
+
+ ``--openal-direct-channels=<yes|no>``
+ Enable OpenAL Soft's direct channel extension when available to avoid
+ tinting the sound with ambisonics or HRTF. Default: yes.
+
+``pulse``
+ PulseAudio audio output driver
+
+ The following global options are supported by this audio output:
+
+ ``--pulse-host=<host>``
+ Specify the host to use. An empty <host> string uses a local connection,
+ "localhost" uses network transfer (most likely not what you want).
+
+ ``--pulse-buffer=<1-2000|native>``
+ Set the audio buffer size in milliseconds. A higher value buffers
+ more data, and has a lower probability of buffer underruns. A smaller
+ value makes the audio stream react faster, e.g. to playback speed
+ changes. "native" lets the sound server determine buffers.
+
+ ``--pulse-latency-hacks=<yes|no>``
+ Enable hacks to workaround PulseAudio timing bugs (default: no). If
+ enabled, mpv will do elaborate latency calculations on its own. If
+ disabled, it will use PulseAudio automatically updated timing
+ information. Disabling this might help with e.g. networked audio or
+ some plugins, while enabling it might help in some unknown situations
+ (it used to be required to get good behavior on old PulseAudio versions).
+
+ If you have stuttering video when using pulse, try to enable this
+ option. (Or try to update PulseAudio.)
+
+ ``--pulse-allow-suspended=<yes|no>``
+ Allow mpv to use PulseAudio even if the sink is suspended (default: no).
+ Can be useful if PulseAudio is running as a bridge to jack and mpv has its sink-input set to the one jack is using.
+
+``pipewire``
+ PipeWire audio output driver
+
+ The following global options are supported by this audio output:
+
+ ``--pipewire-buffer=<1-2000|native>``
+ Set the audio buffer size in milliseconds. A higher value buffers
+ more data, and has a lower probability of buffer underruns. A smaller
+ value makes the audio stream react faster, e.g. to playback speed
+ changes. "native" lets the sound server determine buffers.
+
+ ``--pipewire-remote=<remote>``
+ Specify the PipeWire remote daemon name to connect to via local UNIX
+ sockets.
+ An empty <remote> string uses the default remote named ``pipewire-0``.
+
+ ``--pipewire-volume-mode=<channel|global>``
+ Specify if the ``ao-volume`` property should apply to the channel
+ volumes or the global volume.
+ By default the channel volumes are used.
+
+``sdl``
+ SDL 1.2+ audio output driver. Should work on any platform supported by SDL
+ 1.2, but may require the ``SDL_AUDIODRIVER`` environment variable to be set
+ appropriately for your system.
+
+ .. note:: This driver is for compatibility with extremely foreign
+ environments, such as systems where none of the other drivers
+ are available.
+
+ The following global options are supported by this audio output:
+
+ ``--sdl-buflen=<length>``
+ Sets the audio buffer length in seconds. Is used only as a hint by the
+ sound system. Playing a file with ``-v`` will show the requested and
+ obtained exact buffer size. A value of 0 selects the sound system
+ default.
+
+``null``
+ Produces no audio output but maintains video playback speed. You can use
+ ``--ao=null --ao-null-untimed`` for benchmarking.
+
+ The following global options are supported by this audio output:
+
+ ``--ao-null-untimed``
+ Do not simulate timing of a perfect audio device. This means audio
+ decoding will go as fast as possible, instead of timing it to the
+ system clock.
+
+ ``--ao-null-buffer``
+ Simulated buffer length in seconds.
+
+ ``--ao-null-outburst``
+ Simulated chunk size in samples.
+
+ ``--ao-null-speed``
+ Simulated audio playback speed as a multiplier. Usually, a real audio
+ device will not go exactly as fast as the system clock. It will deviate
+ just a little, and this option helps to simulate this.
+
+ ``--ao-null-latency``
+ Simulated device latency. This is additional to EOF.
+
+ ``--ao-null-broken-eof``
+ Simulate broken audio drivers, which always add the fixed device
+ latency to the reported audio playback position.
+
+ ``--ao-null-broken-delay``
+ Simulate broken audio drivers, which don't report latency correctly.
+
+ ``--ao-null-channel-layouts``
+ If not empty, this is a ``,`` separated list of channel layouts the
+ AO allows. This can be used to test channel layout selection.
+
+ ``--ao-null-format``
+ Force the audio output format the AO will accept. If unset accepts any.
+
+``pcm``
+ Raw PCM/WAVE file writer audio output
+
+ The following global options are supported by this audio output:
+
+ ``--ao-pcm-waveheader=<yes|no>``
+ Include or do not include the WAVE header (default: included). When
+ not included, raw PCM will be generated.
+ ``--ao-pcm-file=<filename>``
+ Write the sound to ``<filename>`` instead of the default
+ ``audiodump.wav``. If ``no-waveheader`` is specified, the default is
+ ``audiodump.pcm``.
+ ``--ao-pcm-append=<yes|no>``
+ Append to the file, instead of overwriting it. Always use this with the
+ ``no-waveheader`` option - with ``waveheader`` it's broken, because
+ it will write a WAVE header every time the file is opened.
+
+``sndio``
+ Audio output to the OpenBSD sndio sound system
+
+ (Note: only supports mono, stereo, 4.0, 5.1 and 7.1 channel
+ layouts.)
+
+``wasapi``
+ Audio output to the Windows Audio Session API.
diff --git a/DOCS/man/changes.rst b/DOCS/man/changes.rst
new file mode 100644
index 0000000..63de41c
--- /dev/null
+++ b/DOCS/man/changes.rst
@@ -0,0 +1,20 @@
+CHANGELOG
+=========
+
+There is no real changelog, but you can look at the following things:
+
+* The release changelog, which should contain most user-visible changes,
+ including new features and bug fixes:
+
+ https://github.com/mpv-player/mpv/releases
+* The git log, which is the "real" changelog
+* The file https://github.com/mpv-player/mpv/blob/master/DOCS/interface-changes.rst
+ documents changes to the command and user interface, such as options and
+ properties. (It usually documents breaking changes only, additions and
+ enhancements are often not listed.)
+* C API changes are listed in
+ https://github.com/mpv-player/mpv/blob/master/DOCS/client-api-changes.rst
+* The file ``mplayer-changes.rst`` in the ``DOCS`` sub directory on the git
+ repository, which used to be in place of this section. It documents some
+ changes that happened since mplayer2 forked off MPlayer. (Not updated
+ anymore.)
diff --git a/DOCS/man/console.rst b/DOCS/man/console.rst
new file mode 100644
index 0000000..49502b3
--- /dev/null
+++ b/DOCS/man/console.rst
@@ -0,0 +1,167 @@
+CONSOLE
+=======
+
+The console is a REPL for mpv input commands. It is displayed on the video
+window. It also shows log messages. It can be disabled entirely using the
+``--load-osd-console=no`` option.
+
+Keybindings
+-----------
+
+\`
+ Show the console.
+
+ESC and Ctrl+[
+ Hide the console.
+
+ENTER, Ctrl+j and Ctrl+m
+ Run the typed command.
+
+Shift+ENTER
+ Type a literal newline character.
+
+LEFT and Ctrl+b
+ Move the cursor to the previous character.
+
+RIGHT and Ctrl+f
+ Move the cursor to the next character.
+
+Ctrl+LEFT and Alt+b
+ Move the cursor to the beginning of the current word, or if between words,
+ to the beginning of the previous word.
+
+Ctrl+RIGHT and Alt+f
+ Move the cursor to the end of the current word, or if between words, to the
+ end of the next word.
+
+HOME and Ctrl+a
+ Move the cursor to the start of the current line.
+
+END and Ctrl+e
+ Move the cursor to the end of the current line.
+
+BACKSPACE and Ctrl+h
+ Delete the previous character.
+
+Ctrl+d
+ Hide the console if the current line is empty, otherwise delete the next
+ character.
+
+Ctrl+BACKSPACE and Ctrl+w
+ Delete text from the cursor to the beginning of the current word, or if
+ between words, to the beginning of the previous word.
+
+Ctrl+DEL and Alt+d
+ Delete text from the cursor to the end of the current word, or if between
+ words, to the end of the next word.
+
+Ctrl+u
+ Delete text from the cursor to the beginning of the current line.
+
+Ctrl+k
+ Delete text from the cursor to the end of the current line.
+
+Ctrl+c
+ Clear the current line.
+
+UP and Ctrl+p
+ Move back in the command history.
+
+DOWN and Ctrl+n
+ Move forward in the command history.
+
+PGUP
+ Go to the first command in the history.
+
+PGDN
+ Stop navigating the command history.
+
+INSERT
+ Toggle insert mode.
+
+Ctrl+v
+ Paste text (uses the clipboard on X11 and Wayland).
+
+Shift+INSERT
+ Paste text (uses the primary selection on X11 and Wayland).
+
+TAB and Ctrl+i
+ Complete the command or property name at the cursor.
+
+Ctrl+l
+ Clear all log messages from the console.
+
+Commands
+--------
+
+``script-message-to console type <text> [<cursor_pos>]``
+ Show the console and pre-fill it with the provided text, optionally
+ specifying the initial cursor position as a positive integer starting from
+ 1.
+
+ .. admonition:: Examples for input.conf
+
+ ``% script-message-to console type "seek absolute-percent; keypress ESC" 6``
+ Enter a percent position to seek to and close the console.
+
+ ``Ctrl+o script-message-to console type "loadfile ''; keypress ESC" 11``
+ Enter a file or URL to play. Tab completes paths in the filesystem.
+
+Known issues
+------------
+
+- Pasting text is slow on Windows
+- Non-ASCII keyboard input has restrictions
+- The cursor keys move between Unicode code-points, not grapheme clusters
+
+Configuration
+-------------
+
+This script can be customized through a config file ``script-opts/console.conf``
+placed in mpv's user directory and through the ``--script-opts`` command-line
+option. The configuration syntax is described in `ON SCREEN CONTROLLER`_.
+
+Key bindings can be changed in a standard way, see for example stats.lua
+documentation.
+
+Configurable Options
+~~~~~~~~~~~~~~~~~~~~
+
+``scale``
+ Default: 1
+
+ All drawing is scaled by this value, including the text borders and the
+ cursor.
+
+ If the VO backend in use has HiDPI scale reporting implemented, the option
+ value is scaled with the reported HiDPI scale.
+
+``font``
+ Default: unset (picks a hardcoded font depending on detected platform)
+
+ Set the font used for the REPL and the console.
+ This has to be a monospaced font for the completion suggestions to be
+ aligned correctly.
+
+``font_size``
+ Default: 16
+
+ Set the font size used for the REPL and the console. This will be
+ multiplied by "scale".
+
+``border_size``
+ Default: 1
+
+ Set the font border size used for the REPL and the console.
+
+``history_dedup``
+ Default: true
+
+ Remove duplicate entries in history as to only keep the latest one.
+ multiplied by "scale."
+
+``font_hw_ratio``
+ Default: 2.0
+
+ The ratio of font height to font width.
+ Adjusts table width of completion suggestions.
diff --git a/DOCS/man/encode.rst b/DOCS/man/encode.rst
new file mode 100644
index 0000000..399eba2
--- /dev/null
+++ b/DOCS/man/encode.rst
@@ -0,0 +1,107 @@
+ENCODING
+========
+
+You can encode files from one format/codec to another using this facility.
+
+``--o=<filename>``
+ Enables encoding mode and specifies the output file name.
+
+``--of=<format>``
+ Specifies the output format (overrides autodetection by the file name
+ extension of the file specified by ``-o``). See ``--of=help`` for a full
+ list of supported formats.
+
+``--ofopts=<options>``
+ Specifies the output format options for libavformat.
+ See ``--ofopts=help`` for a full list of supported options.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ ``--ofopts-add=<option>``
+ Appends the option given as an argument to the options list. (Passing
+ multiple options is currently still possible, but deprecated.)
+
+ ``--ofopts=""``
+ Completely empties the options list.
+
+``--oac=<codec>``
+ Specifies the output audio codec. See ``--oac=help`` for a full list of
+ supported codecs.
+
+``--oacopts=<options>``
+ Specifies the output audio codec options for libavcodec.
+ See ``--oacopts=help`` for a full list of supported options.
+
+ .. admonition:: Example
+
+ "``--oac=libmp3lame --oacopts=b=128000``"
+ selects 128 kbps MP3 encoding.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ ``--oacopts-add=<option>``
+ Appends the option given as an argument to the options list. (Passing
+ multiple options is currently still possible, but deprecated.)
+
+ ``--oacopts=""``
+ Completely empties the options list.
+
+``--ovc=<codec>``
+ Specifies the output video codec. See ``--ovc=help`` for a full list of
+ supported codecs.
+
+``--ovcopts=<options>``
+ Specifies the output video codec options for libavcodec.
+ See --ovcopts=help for a full list of supported options.
+
+ .. admonition:: Examples
+
+ ``"--ovc=mpeg4 --ovcopts=qscale=5"``
+ selects constant quantizer scale 5 for MPEG-4 encoding.
+
+ ``"--ovc=libx264 --ovcopts=crf=23"``
+ selects VBR quality factor 23 for H.264 encoding.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ ``--ovcopts-add=<option>``
+ Appends the option given as an argument to the options list. (Passing
+ multiple options is currently still possible, but deprecated.)
+
+ ``--ovcopts=""``
+ Completely empties the options list.
+
+``--orawts``
+ Copies input pts to the output video (not supported by some output
+ container formats, e.g. AVI). In this mode, discontinuities are not fixed
+ and all pts are passed through as-is. Never seek backwards or use multiple
+ input files in this mode!
+
+``--no-ocopy-metadata``
+ Turns off copying of metadata from input files to output files when
+ encoding (which is enabled by default).
+
+``--oset-metadata=<metadata-tag[,metadata-tag,...]>``
+ Specifies metadata to include in the output file.
+ Supported keys vary between output formats. For example, Matroska (MKV) and
+ FLAC allow almost arbitrary keys, while support in MP4 and MP3 is more
+ limited.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ "``--oset-metadata=title="Output title",comment="Another tag"``"
+ adds a title and a comment to the output file.
+
+``--oremove-metadata=<metadata-tag[,metadata-tag,...]>``
+ Specifies metadata to exclude from the output file when copying from the
+ input file.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ "``--oremove-metadata=comment,genre``"
+ excludes copying of the the comment and genre tags to the output
+ file.
diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst
new file mode 100644
index 0000000..8dbf58b
--- /dev/null
+++ b/DOCS/man/input.rst
@@ -0,0 +1,3697 @@
+COMMAND INTERFACE
+=================
+
+The mpv core can be controlled with commands and properties. A number of ways
+to interact with the player use them: key bindings (``input.conf``), OSD
+(showing information with properties), JSON IPC, the client API (``libmpv``),
+and the classic slave mode.
+
+input.conf
+----------
+
+The input.conf file consists of a list of key bindings, for example::
+
+ s screenshot # take a screenshot with the s key
+ LEFT seek 15 # map the left-arrow key to seeking forward by 15 seconds
+
+Each line maps a key to an input command. Keys are specified with their literal
+value (upper case if combined with ``Shift``), or a name for special keys. For
+example, ``a`` maps to the ``a`` key without shift, and ``A`` maps to ``a``
+with shift.
+
+The file is located in the mpv configuration directory (normally at
+``~/.config/mpv/input.conf`` depending on platform). The default bindings are
+defined here::
+
+ https://github.com/mpv-player/mpv/blob/master/etc/input.conf
+
+A list of special keys can be obtained with
+
+ ``mpv --input-keylist``
+
+In general, keys can be combined with ``Shift``, ``Ctrl`` and ``Alt``::
+
+ ctrl+q quit
+
+**mpv** can be started in input test mode, which displays key bindings and the
+commands they're bound to on the OSD, instead of executing the commands::
+
+ mpv --input-test --force-window --idle
+
+(Only closing the window will make **mpv** exit, pressing normal keys will
+merely display the binding, even if mapped to quit.)
+
+Also see `Key names`_.
+
+input.conf syntax
+-----------------
+
+``[Shift+][Ctrl+][Alt+][Meta+]<key> [{<section>}] <command> ( ; <command> )*``
+
+Note that by default, the right Alt key can be used to create special
+characters, and thus does not register as a modifier. The option
+``--no-input-right-alt-gr`` changes this behavior.
+
+Newlines always start a new binding. ``#`` starts a comment (outside of quoted
+string arguments). To bind commands to the ``#`` key, ``SHARP`` can be used.
+
+``<key>`` is either the literal character the key produces (ASCII or Unicode
+character), or a symbolic name (as printed by ``--input-keylist``).
+
+``<section>`` (braced with ``{`` and ``}``) is the input section for this
+command.
+
+``<command>`` is the command itself. It consists of the command name and
+multiple (or none) arguments, all separated by whitespace. String arguments
+should be quoted, typically with ``"``. See ``Flat command syntax``.
+
+You can bind multiple commands to one key. For example:
+
+| a show-text "command 1" ; show-text "command 2"
+
+It's also possible to bind a command to a sequence of keys:
+
+| a-b-c show-text "command run after a, b, c have been pressed"
+
+(This is not shown in the general command syntax.)
+
+If ``a`` or ``a-b`` or ``b`` are already bound, this will run the first command
+that matches, and the multi-key command will never be called. Intermediate keys
+can be remapped to ``ignore`` in order to avoid this issue. The maximum number
+of (non-modifier) keys for combinations is currently 4.
+
+Key names
+---------
+
+All mouse and keyboard input is to converted to mpv-specific key names. Key
+names are either special symbolic identifiers representing a physical key, or a
+text key names, which are unicode code points encoded as UTF-8. These are what
+keyboard input would normally produce, for example ``a`` for the A key. As a
+consequence, mpv uses input translated by the current OS keyboard layout, rather
+than physical scan codes.
+
+Currently there is the hardcoded assumption that every text key can be
+represented as a single unicode code point (in NFKC form).
+
+All key names can be combined with the modifiers ``Shift``, ``Ctrl``, ``Alt``,
+``Meta``. They must be prefixed to the actual key name, where each modifier
+is followed by a ``+`` (for example ``ctrl+q``).
+
+The ``Shift`` modifier requires some attention. For instance ``Shift+2`` should
+usually be specified as key-name ``@`` at ``input.conf``, and similarly the
+combination ``Alt+Shift+2`` is usually ``Alt+@``, etc. Special key names like
+``Shift+LEFT`` work as expected. If in doubt - use ``--input-test`` to check
+how a key/combination is seen by mpv.
+
+Symbolic key names and modifier names are case-insensitive. Unicode key names
+are case-sensitive because input bindings typically respect the shift key.
+
+Another type of key names are hexadecimal key names, that serve as fallback
+for special keys that are neither unicode, nor have a special mpv defined name.
+They will break as soon as mpv adds proper names for them, but can enable you
+to use a key at all if that does not happen.
+
+All symbolic names are listed by ``--input-keylist``. ``--input-test`` is a
+special mode that prints all input on the OSD.
+
+Comments on some symbolic names:
+
+``KP*``
+ Keypad names. Behavior varies by backend (whether they implement this, and
+ on how they treat numlock), but typically, mpv tries to map keys on the
+ keypad to separate names, even if they produce the same text as normal keys.
+
+``MOUSE_BTN*``, ``MBTN*``
+ Various mouse buttons.
+
+ Depending on backend, the mouse wheel might also be represented as a button.
+ In addition, ``MOUSE_BTN3`` to ``MOUSE_BTN6`` are deprecated aliases for
+ ``WHEEL_UP``, ``WHEEL_DOWN``, ``WHEEL_LEFT``, ``WHEEL_RIGHT``.
+
+ ``MBTN*`` are aliases for ``MOUSE_BTN*``.
+
+``WHEEL_*``
+ Mouse wheels (typically).
+
+``AXIS_*``
+ Deprecated aliases for ``WHEEL_*``.
+
+``*_DBL``
+ Mouse button double clicks.
+
+``MOUSE_MOVE``, ``MOUSE_ENTER``, ``MOUSE_LEAVE``
+ Emitted by mouse move events. Enter/leave happens when the mouse enters or
+ leave the mpv window (or the current mouse region, using the deprecated
+ mouse region input section mechanism).
+
+``CLOSE_WIN``
+ Pseudo key emitted when closing the mpv window using the OS window manager
+ (for example, by clicking the close button in the window title bar).
+
+``GAMEPAD_*``
+ Keys emitted by the SDL gamepad backend.
+
+``UNMAPPED``
+ Pseudo-key that matches any unmapped key. (You should probably avoid this
+ if possible, because it might change behavior or get removed in the future.)
+
+``ANY_UNICODE``
+ Pseudo-key that matches any key that produces text. (You should probably
+ avoid this if possible, because it might change behavior or get removed in
+ the future.)
+
+Flat command syntax
+-------------------
+
+This is the syntax used in input.conf, and referred to "input.conf syntax" in
+a number of other places.
+
+|
+| ``<command> ::= [<prefixes>] <command_name> (<argument>)*``
+| ``<argument> ::= (<unquoted> | " <double_quoted> " | ' <single_quoted> ' | `X <custom_quoted> X`)``
+
+``command_name`` is an unquoted string with the command name itself. See
+`List of Input Commands`_ for a list.
+
+Arguments are separated by whitespaces even if the command expects only one
+argument. Arguments with whitespaces or other special characters must be quoted,
+or the command cannot be parsed correctly.
+
+Double quotes interpret JSON/C-style escaping, like ``\t`` or ``\"`` or ``\\``.
+JSON escapes according to RFC 8259, minus surrogate pair escapes. This is the
+only form which allows newlines at the value - as ``\n``.
+
+Single quotes take the content literally, and cannot include the single-quote
+character at the value.
+
+Custom quotes also take the content literally, but are more flexible than single
+quotes. They start with ````` (back-quote) followed by any ASCII character,
+and end at the first occurrence of the same pair in reverse order, e.g.
+```-foo-``` or ````bar````. The final pair sequence is not allowed at the
+value - in these examples ``-``` and `````` respectively. In the second
+example the last character of the value also can't be a back-quote.
+
+Mixed quoting at the same argument, like ``'foo'"bar"``, is not supported.
+
+Note that argument parsing and property expansion happen at different stages.
+First, arguments are determined as described above, and then, where applicable,
+properties are expanded - regardless of argument quoting. However, expansion
+can still be prevented with the ``raw`` prefix or ``$>``. See `Input Command
+Prefixes`_ and `Property Expansion`_.
+
+Commands specified as arrays
+----------------------------
+
+This applies to certain APIs, such as ``mp.commandv()`` or
+``mp.command_native()`` (with array parameters) in Lua scripting, or
+``mpv_command()`` or ``mpv_command_node()`` (with MPV_FORMAT_NODE_ARRAY) in the
+C libmpv client API.
+
+The command as well as all arguments are passed as a single array. Similar to
+the `Flat command syntax`_, you can first pass prefixes as strings (each as
+separate array item), then the command name as string, and then each argument
+as string or a native value.
+
+Since these APIs pass arguments as separate strings or native values, they do
+not expect quotes, and do support escaping. Technically, there is the input.conf
+parser, which first splits the command string into arguments, and then invokes
+argument parsers for each argument. The input.conf parser normally handles
+quotes and escaping. The array command APIs mentioned above pass strings
+directly to the argument parsers, or can sidestep them by the ability to pass
+non-string values.
+
+Property expansion is disabled by default for these APIs. This can be changed
+with the ``expand-properties`` prefix. See `Input Command Prefixes`_.
+
+Sometimes commands have string arguments, that in turn are actually parsed by
+other components (e.g. filter strings with ``vf add``) - in these cases, you
+you would have to double-escape in input.conf, but not with the array APIs.
+
+For complex commands, consider using `Named arguments`_ instead, which should
+give slightly more compatibility. Some commands do not support named arguments
+and inherently take an array, though.
+
+Named arguments
+---------------
+
+This applies to certain APIs, such as ``mp.command_native()`` (with tables that
+have string keys) in Lua scripting, or ``mpv_command_node()`` (with
+MPV_FORMAT_NODE_MAP) in the C libmpv client API.
+
+The name of the command is provided with a ``name`` string field. The name of
+each command is defined in each command description in the
+`List of Input Commands`_. ``--input-cmdlist`` also lists them. See the
+``subprocess`` command for an example.
+
+Some commands do not support named arguments (e.g. ``run`` command). You need
+to use APIs that pass arguments as arrays.
+
+Named arguments are not supported in the "flat" input.conf syntax, which means
+you cannot use them for key bindings in input.conf at all.
+
+Property expansion is disabled by default for these APIs. This can be changed
+with the ``expand-properties`` prefix. See `Input Command Prefixes`_.
+
+List of Input Commands
+----------------------
+
+Commands with parameters have the parameter name enclosed in ``<`` / ``>``.
+Don't add those to the actual command. Optional arguments are enclosed in
+``[`` / ``]``. If you don't pass them, they will be set to a default value.
+
+Remember to quote string arguments in input.conf (see `Flat command syntax`_).
+
+``ignore``
+ Use this to "block" keys that should be unbound, and do nothing. Useful for
+ disabling default bindings, without disabling all bindings with
+ ``--no-input-default-bindings``.
+
+``seek <target> [<flags>]``
+ Change the playback position. By default, seeks by a relative amount of
+ seconds.
+
+ The second argument consists of flags controlling the seek mode:
+
+ relative (default)
+ Seek relative to current position (a negative value seeks backwards).
+ absolute
+ Seek to a given time (a negative value starts from the end of the file).
+ absolute-percent
+ Seek to a given percent position.
+ relative-percent
+ Seek relative to current position in percent.
+ keyframes
+ Always restart playback at keyframe boundaries (fast).
+ exact
+ Always do exact/hr/precise seeks (slow).
+
+ Multiple flags can be combined, e.g.: ``absolute+keyframes``.
+
+ By default, ``keyframes`` is used for ``relative``, ``relative-percent``,
+ and ``absolute-percent`` seeks, while ``exact`` is used for ``absolute``
+ seeks.
+
+ Before mpv 0.9, the ``keyframes`` and ``exact`` flags had to be passed as
+ 3rd parameter (essentially using a space instead of ``+``). The 3rd
+ parameter is still parsed, but is considered deprecated.
+
+``revert-seek [<flags>]``
+ Undoes the ``seek`` command, and some other commands that seek (but not
+ necessarily all of them). Calling this command once will jump to the
+ playback position before the seek. Calling it a second time undoes the
+ ``revert-seek`` command itself. This only works within a single file.
+
+ The first argument is optional, and can change the behavior:
+
+ mark
+ Mark the current time position. The next normal ``revert-seek`` command
+ will seek back to this point, no matter how many seeks happened since
+ last time.
+ mark-permanent
+ If set, mark the current position, and do not change the mark position
+ before the next ``revert-seek`` command that has ``mark`` or
+ ``mark-permanent`` set (or playback of the current file ends). Until
+ this happens, ``revert-seek`` will always seek to the marked point. This
+ flag cannot be combined with ``mark``.
+
+ Using it without any arguments gives you the default behavior.
+
+``frame-step``
+ Play one frame, then pause. Does nothing with audio-only playback.
+
+``frame-back-step``
+ Go back by one frame, then pause. Note that this can be very slow (it tries
+ to be precise, not fast), and sometimes fails to behave as expected. How
+ well this works depends on whether precise seeking works correctly (e.g.
+ see the ``--hr-seek-demuxer-offset`` option). Video filters or other video
+ post-processing that modifies timing of frames (e.g. deinterlacing) should
+ usually work, but might make backstepping silently behave incorrectly in
+ corner cases. Using ``--hr-seek-framedrop=no`` should help, although it
+ might make precise seeking slower.
+
+ This does not work with audio-only playback.
+
+``set <name> <value>``
+ Set the given property or option to the given value.
+
+``del <name>``
+ Delete the given property. Most properties cannot be deleted.
+
+``add <name> [<value>]``
+ Add the given value to the property or option. On overflow or underflow,
+ clamp the property to the maximum. If ``<value>`` is omitted, assume ``1``.
+
+``cycle <name> [<value>]``
+ Cycle the given property or option. The second argument can be ``up`` or
+ ``down`` to set the cycle direction. On overflow, set the property back to
+ the minimum, on underflow set it to the maximum. If ``up`` or ``down`` is
+ omitted, assume ``up``.
+
+ Whether or not key-repeat is enabled by default depends on the property.
+ Currently properties with continuous values are repeatable by default (like
+ ``volume``), while discrete values are not (like ``osd-level``).
+
+``multiply <name> <value>``
+ Similar to ``add``, but multiplies the property or option with the numeric
+ value.
+
+``screenshot <flags>``
+ Take a screenshot.
+
+ Multiple flags are available (some can be combined with ``+``):
+
+ <subtitles> (default)
+ Save the video image, in its original resolution, and with subtitles.
+ Some video outputs may still include the OSD in the output under certain
+ circumstances.
+ <video>
+ Like ``subtitles``, but typically without OSD or subtitles. The exact
+ behavior depends on the selected video output.
+ <window>
+ Save the contents of the mpv window. Typically scaled, with OSD and
+ subtitles. The exact behavior depends on the selected video output.
+ <each-frame>
+ Take a screenshot each frame. Issue this command again to stop taking
+ screenshots. Note that you should disable frame-dropping when using
+ this mode - or you might receive duplicate images in cases when a
+ frame was dropped. This flag can be combined with the other flags,
+ e.g. ``video+each-frame``.
+
+ Older mpv versions required passing ``single`` and ``each-frame`` as
+ second argument (and did not have flags). This syntax is still understood,
+ but deprecated and might be removed in the future.
+
+ If you combine this command with another one using ``;``, you can use the
+ ``async`` flag to make encoding/writing the image file asynchronous. For
+ normal standalone commands, this is always asynchronous, and the flag has
+ no effect. (This behavior changed with mpv 0.29.0.)
+
+ On success, returns a ``mpv_node`` with a ``filename`` field set to the
+ saved screenshot location.
+
+``screenshot-to-file <filename> <flags>``
+ Take a screenshot and save it to a given file. The format of the file will
+ be guessed by the extension (and ``--screenshot-format`` is ignored - the
+ behavior when the extension is missing or unknown is arbitrary).
+
+ The second argument is like the first argument to ``screenshot`` and
+ supports ``subtitles``, ``video``, ``window``.
+
+ If the file already exists, it's overwritten.
+
+ Like all input command parameters, the filename is subject to property
+ expansion as described in `Property Expansion`_.
+
+``playlist-next <flags>``
+ Go to the next entry on the playlist.
+
+ First argument:
+
+ weak (default)
+ If the last file on the playlist is currently played, do nothing.
+ force
+ Terminate playback if there are no more files on the playlist.
+
+``playlist-prev <flags>``
+ Go to the previous entry on the playlist.
+
+ First argument:
+
+ weak (default)
+ If the first file on the playlist is currently played, do nothing.
+ force
+ Terminate playback if the first file is being played.
+
+``playlist-next-playlist``
+ Go to the next entry on the playlist with a different ``playlist-path``.
+
+``playlist-prev-playlist``
+ Go to the first of the previous entries on the playlist with a different
+ ``playlist-path``.
+
+``playlist-play-index <integer|current|none>``
+ Start (or restart) playback of the given playlist index. In addition to the
+ 0-based playlist entry index, it supports the following values:
+
+ <current>
+ The current playlist entry (as in ``playlist-current-pos``) will be
+ played again (unload and reload). If none is set, playback is stopped.
+ (In corner cases, ``playlist-current-pos`` can point to a playlist entry
+ even if playback is currently inactive,
+
+ <none>
+ Playback is stopped. If idle mode (``--idle``) is enabled, the player
+ will enter idle mode, otherwise it will exit.
+
+ This command is similar to ``loadfile`` in that it only manipulates the
+ state of what to play next, without waiting until the current file is
+ unloaded, and the next one is loaded.
+
+ Setting ``playlist-pos`` or similar properties can have a similar effect to
+ this command. However, it's more explicit, and guarantees that playback is
+ restarted if for example the new playlist entry is the same as the previous
+ one.
+
+``loadfile <url> [<flags> [<options>]]``
+ Load the given file or URL and play it. Technically, this is just a playlist
+ manipulation command (which either replaces the playlist or appends an entry
+ to it). Actual file loading happens independently. For example, a
+ ``loadfile`` command that replaces the current file with a new one returns
+ before the current file is stopped, and the new file even begins loading.
+
+ Second argument:
+
+ <replace> (default)
+ Stop playback of the current file, and play the new file immediately.
+ <append>
+ Append the file to the playlist.
+ <append-play>
+ Append the file, and if nothing is currently playing, start playback.
+ (Always starts with the added file, even if the playlist was not empty
+ before running this command.)
+
+ The third argument is a list of options and values which should be set
+ while the file is playing. It is of the form ``opt1=value1,opt2=value2,..``.
+ When using the client API, this can be a ``MPV_FORMAT_NODE_MAP`` (or a Lua
+ table), however the values themselves must be strings currently. These
+ options are set during playback, and restored to the previous value at end
+ of playback (see `Per-File Options`_).
+
+``loadlist <url> [<flags>]``
+ Load the given playlist file or URL (like ``--playlist``).
+
+ Second argument:
+
+ <replace> (default)
+ Stop playback and replace the internal playlist with the new one.
+ <append>
+ Append the new playlist at the end of the current internal playlist.
+ <append-play>
+ Append the new playlist, and if nothing is currently playing, start
+ playback. (Always starts with the new playlist, even if the internal
+ playlist was not empty before running this command.)
+
+``playlist-clear``
+ Clear the playlist, except the currently played file.
+
+``playlist-remove <index>``
+ Remove the playlist entry at the given index. Index values start counting
+ with 0. The special value ``current`` removes the current entry. Note that
+ removing the current entry also stops playback and starts playing the next
+ entry.
+
+``playlist-move <index1> <index2>``
+ Move the playlist entry at index1, so that it takes the place of the
+ entry index2. (Paradoxically, the moved playlist entry will not have
+ the index value index2 after moving if index1 was lower than index2,
+ because index2 refers to the target entry, not the index the entry
+ will have after moving.)
+
+``playlist-shuffle``
+ Shuffle the playlist. This is similar to what is done on start if the
+ ``--shuffle`` option is used.
+
+``playlist-unshuffle``
+ Attempt to revert the previous ``playlist-shuffle`` command. This works
+ only once (multiple successive ``playlist-unshuffle`` commands do nothing).
+ May not work correctly if new recursive playlists have been opened since
+ a ``playlist-shuffle`` command.
+
+``run <command> [<arg1> [<arg2> [...]]]``
+ Run the given command. Unlike in MPlayer/mplayer2 and earlier versions of
+ mpv (0.2.x and older), this doesn't call the shell. Instead, the command
+ is run directly, with each argument passed separately. Each argument is
+ expanded like in `Property Expansion`_.
+
+ This command has a variable number of arguments, and cannot be used with
+ named arguments.
+
+ The program is run in a detached way. mpv doesn't wait until the command
+ is completed, but continues playback right after spawning it.
+
+ To get the old behavior, use ``/bin/sh`` and ``-c`` as the first two
+ arguments.
+
+ .. admonition:: Example
+
+ ``run "/bin/sh" "-c" "echo ${title} > /tmp/playing"``
+
+ This is not a particularly good example, because it doesn't handle
+ escaping, and a specially prepared file might allow an attacker to
+ execute arbitrary shell commands. It is recommended to write a small
+ shell script, and call that with ``run``.
+
+``subprocess``
+ Similar to ``run``, but gives more control about process execution to the
+ caller, and does does not detach the process.
+
+ You can avoid blocking until the process terminates by running this command
+ asynchronously. (For example ``mp.command_native_async()`` in Lua scripting.)
+
+ This has the following named arguments. The order of them is not guaranteed,
+ so you should always call them with named arguments, see `Named arguments`_.
+
+ ``args`` (``MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING]``)
+ Array of strings with the command as first argument, and subsequent
+ command line arguments following. This is just like the ``run`` command
+ argument list.
+
+ The first array entry is either an absolute path to the executable, or
+ a filename with no path components, in which case the executable is
+ searched in the directories in the ``PATH`` environment variable. On
+ Unix, this is equivalent to ``posix_spawnp`` and ``execvp`` behavior.
+
+ ``playback_only`` (``MPV_FORMAT_FLAG``)
+ Boolean indicating whether the process should be killed when playback
+ of the current playlist entry terminates (optional, default: true). If
+ enabled, stopping playback will automatically kill the process, and you
+ can't start it outside of playback.
+
+ ``capture_size`` (``MPV_FORMAT_INT64``)
+ Integer setting the maximum number of stdout plus stderr bytes that can
+ be captured (optional, default: 64MB). If the number of bytes exceeds
+ this, capturing is stopped. The limit is per captured stream.
+
+ ``capture_stdout`` (``MPV_FORMAT_FLAG``)
+ Capture all data the process outputs to stdout and return it once the
+ process ends (optional, default: no).
+
+ ``capture_stderr`` (``MPV_FORMAT_FLAG``)
+ Same as ``capture_stdout``, but for stderr.
+
+ ``detach`` (``MPV_FORMAT_FLAG``)
+ Whether to run the process in detached mode (optional, default: no). In
+ this mode, the process is run in a new process session, and the command
+ does not wait for the process to terminate. If neither
+ ``capture_stdout`` nor ``capture_stderr`` have been set to true,
+ the command returns immediately after the new process has been started,
+ otherwise the command will read as long as the pipes are open.
+
+ ``env`` (``MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING]``)
+ Set a list of environment variables for the new process (default: empty).
+ If an empty list is passed, the environment of the mpv process is used
+ instead. (Unlike the underlying OS mechanisms, the mpv command cannot
+ start a process with empty environment. Fortunately, that is completely
+ useless.) The format of the list is as in the ``execle()`` syscall. Each
+ string item defines an environment variable as in ``NAME=VALUE``.
+
+ On Lua, you may use ``utils.get_env_list()`` to retrieve the current
+ environment if you e.g. simply want to add a new variable.
+
+ ``stdin_data`` (``MPV_FORMAT_STRING``)
+ Feed the given string to the new process' stdin. Since this is a string,
+ you cannot pass arbitrary binary data. If the process terminates or
+ closes the pipe before all data is written, the remaining data is
+ silently discarded. Probably does not work on win32.
+
+ ``passthrough_stdin`` (``MPV_FORMAT_FLAG``)
+ If enabled, wire the new process' stdin to mpv's stdin (default: no).
+ Before mpv 0.33.0, this argument did not exist, but the behavior was as
+ if this was set to true.
+
+ The command returns the following result (as ``MPV_FORMAT_NODE_MAP``):
+
+ ``status`` (``MPV_FORMAT_INT64``)
+ Typically this is the process exit code (0 or positive) if the process
+ terminates normally, or negative for other errors (failed to start,
+ terminated by mpv, and others). The meaning of negative values is
+ undefined, other than meaning error (and does not correspond to OS low
+ level exit status values).
+
+ On Windows, it can happen that a negative return value is returned even
+ if the process terminates normally, because the win32 ``UINT`` exit
+ code is assigned to an ``int`` variable before being set as ``int64_t``
+ field in the result map. This might be fixed later.
+
+ ``stdout`` (``MPV_FORMAT_BYTE_ARRAY``)
+ Captured stdout stream, limited to ``capture_size``.
+
+ ``stderr`` (``MPV_FORMAT_BYTE_ARRAY``)
+ Same as ``stdout``, but for stderr.
+
+ ``error_string`` (``MPV_FORMAT_STRING``)
+ Empty string if the process terminated normally. The string ``killed``
+ if the process was terminated in an unusual way. The string ``init`` if
+ the process could not be started.
+
+ On Windows, ``killed`` is only returned when the process has been
+ killed by mpv as a result of ``playback_only`` being set to true.
+
+ ``killed_by_us`` (``MPV_FORMAT_FLAG``)
+ Whether the process has been killed by mpv, for example as a result of
+ ``playback_only`` being set to true, aborting the command (e.g. by
+ ``mp.abort_async_command()``), or if the player is about to exit.
+
+ Note that the command itself will always return success as long as the
+ parameters are correct. Whether the process could be spawned or whether
+ it was somehow killed or returned an error status has to be queried from
+ the result value.
+
+ This command can be asynchronously aborted via API. Also see `Asynchronous
+ command details`_. Only the ``run`` command can start processes in a truly
+ detached way.
+
+ .. note:: The subprocess will always be terminated on player exit if it
+ wasn't started in detached mode, even if ``playback_only`` is
+ false.
+
+ .. admonition:: Warning
+
+ Don't forget to set the ``playback_only`` field to false if you want
+ the command to run while the player is in idle mode, or if you don't
+ want the end of playback to kill the command.
+
+ .. admonition:: Example
+
+ ::
+
+ local r = mp.command_native({
+ name = "subprocess",
+ playback_only = false,
+ capture_stdout = true,
+ args = {"cat", "/proc/cpuinfo"},
+ })
+ if r.status == 0 then
+ print("result: " .. r.stdout)
+ end
+
+ This is a fairly useless Lua example, which demonstrates how to run
+ a process in a blocking manner, and retrieving its stdout output.
+
+``quit [<code>]``
+ Exit the player. If an argument is given, it's used as process exit code.
+
+``quit-watch-later [<code>]``
+ Exit player, and store current playback position. Playing that file later
+ will seek to the previous position on start. The (optional) argument is
+ exactly as in the ``quit`` command. See `RESUMING PLAYBACK`_.
+
+``sub-add <url> [<flags> [<title> [<lang>]]]``
+ Load the given subtitle file or stream. By default, it is selected as
+ current subtitle after loading.
+
+ The ``flags`` argument is one of the following values:
+
+ <select>
+
+ Select the subtitle immediately (default).
+
+ <auto>
+
+ Don't select the subtitle. (Or in some special situations, let the
+ default stream selection mechanism decide.)
+
+ <cached>
+
+ Select the subtitle. If a subtitle with the same filename was already
+ added, that one is selected, instead of loading a duplicate entry.
+ (In this case, title/language are ignored, and if the was changed since
+ it was loaded, these changes won't be reflected.)
+
+ The ``title`` argument sets the track title in the UI.
+
+ The ``lang`` argument sets the track language, and can also influence
+ stream selection with ``flags`` set to ``auto``.
+
+``sub-remove [<id>]``
+ Remove the given subtitle track. If the ``id`` argument is missing, remove
+ the current track. (Works on external subtitle files only.)
+
+``sub-reload [<id>]``
+ Reload the given subtitle tracks. If the ``id`` argument is missing, reload
+ the current track. (Works on external subtitle files only.)
+
+ This works by unloading and re-adding the subtitle track.
+
+``sub-step <skip> <flags>``
+ Change subtitle timing such, that the subtitle event after the next
+ ``<skip>`` subtitle events is displayed. ``<skip>`` can be negative to step
+ backwards.
+
+ Secondary argument:
+
+ primary (default)
+ Steps through the primary subtitles.
+ secondary
+ Steps through the secondary subtitles.
+
+``sub-seek <skip> <flags>``
+ Seek to the next (skip set to 1) or the previous (skip set to -1) subtitle.
+ This is similar to ``sub-step``, except that it seeks video and audio
+ instead of adjusting the subtitle delay.
+
+ Secondary argument:
+
+ primary (default)
+ Seeks through the primary subtitles.
+ secondary
+ Seeks through the secondary subtitles.
+
+ For embedded subtitles (like with Matroska), this works only with subtitle
+ events that have already been displayed, or are within a short prefetch
+ range.
+
+``print-text <text>``
+ Print text to stdout. The string can contain properties (see
+ `Property Expansion`_). Take care to put the argument in quotes.
+
+``show-text <text> [<duration>|-1 [<level>]]``
+ Show text on the OSD. The string can contain properties, which are expanded
+ as described in `Property Expansion`_. This can be used to show playback
+ time, filename, and so on. ``no-osd`` has no effect on this command.
+
+ <duration>
+ The time in ms to show the message for. By default, it uses the same
+ value as ``--osd-duration``.
+
+ <level>
+ The minimum OSD level to show the text at (see ``--osd-level``).
+
+``expand-text <string>``
+ Property-expand the argument and return the expanded string. This can be
+ used only through the client API or from a script using
+ ``mp.command_native``. (see `Property Expansion`_).
+
+``expand-path "<string>"``
+ Expand a path's double-tilde placeholders into a platform-specific path.
+ As ``expand-text``, this can only be used through the client API or from
+ a script using ``mp.command_native``.
+
+ .. admonition:: Example
+
+ ``mp.osd_message(mp.command_native({"expand-path", "~~home/"}))``
+
+ This line of Lua would show the location of the user's mpv
+ configuration directory on the OSD.
+
+``show-progress``
+ Show the progress bar, the elapsed time and the total duration of the file
+ on the OSD. ``no-osd`` has no effect on this command.
+
+``write-watch-later-config``
+ Write the resume config file that the ``quit-watch-later`` command writes,
+ but continue playback normally.
+
+``delete-watch-later-config [<filename>]``
+ Delete any existing resume config file that was written by
+ ``quit-watch-later`` or ``write-watch-later-config``. If a filename is
+ specified, then the deleted config is for that file; otherwise, it is the
+ same one as would be written by ``quit-watch-later`` or
+ ``write-watch-later-config`` in the current circumstance.
+
+``stop [<flags>]``
+ Stop playback and clear playlist. With default settings, this is
+ essentially like ``quit``. Useful for the client API: playback can be
+ stopped without terminating the player.
+
+ The first argument is optional, and supports the following flags:
+
+ keep-playlist
+ Do not clear the playlist.
+
+
+``mouse <x> <y> [<button> [<mode>]]``
+ Send a mouse event with given coordinate (``<x>``, ``<y>``).
+
+ Second argument:
+
+ <button>
+ The button number of clicked mouse button. This should be one of 0-19.
+ If ``<button>`` is omitted, only the position will be updated.
+
+ Third argument:
+
+ <single> (default)
+ The mouse event represents regular single click.
+
+ <double>
+ The mouse event represents double-click.
+
+``keypress <name>``
+ Send a key event through mpv's input handler, triggering whatever
+ behavior is configured to that key. ``name`` uses the ``input.conf``
+ naming scheme for keys and modifiers. Useful for the client API: key events
+ can be sent to libmpv to handle internally.
+
+``keydown <name>``
+ Similar to ``keypress``, but sets the ``KEYDOWN`` flag so that if the key is
+ bound to a repeatable command, it will be run repeatedly with mpv's key
+ repeat timing until the ``keyup`` command is called.
+
+``keyup [<name>]``
+ Set the ``KEYUP`` flag, stopping any repeated behavior that had been
+ triggered. ``name`` is optional. If ``name`` is not given or is an
+ empty string, ``KEYUP`` will be set on all keys. Otherwise, ``KEYUP`` will
+ only be set on the key specified by ``name``.
+
+``keybind <name> <command>``
+ Binds a key to an input command. ``command`` must be a complete command
+ containing all the desired arguments and flags. Both ``name`` and
+ ``command`` use the ``input.conf`` naming scheme. This is primarily
+ useful for the client API.
+
+``audio-add <url> [<flags> [<title> [<lang>]]]``
+ Load the given audio file. See ``sub-add`` command.
+
+``audio-remove [<id>]``
+ Remove the given audio track. See ``sub-remove`` command.
+
+``audio-reload [<id>]``
+ Reload the given audio tracks. See ``sub-reload`` command.
+
+``video-add <url> [<flags> [<title> [<lang> [<albumart>]]]]``
+ Load the given video file. See ``sub-add`` command for common options.
+
+ ``albumart`` (``MPV_FORMAT_FLAG``)
+ If enabled, mpv will load the given video as album art.
+
+``video-remove [<id>]``
+ Remove the given video track. See ``sub-remove`` command.
+
+``video-reload [<id>]``
+ Reload the given video tracks. See ``sub-reload`` command.
+
+``rescan-external-files [<mode>]``
+ Rescan external files according to the current ``--sub-auto``,
+ ``--audio-file-auto`` and ``--cover-art-auto`` settings. This can be used
+ to auto-load external files *after* the file was loaded.
+
+ The ``mode`` argument is one of the following:
+
+ <reselect> (default)
+ Select the default audio and subtitle streams, which typically selects
+ external files with the highest preference. (The implementation is not
+ perfect, and could be improved on request.)
+
+ <keep-selection>
+ Do not change current track selections.
+
+
+Input Commands that are Possibly Subject to Change
+--------------------------------------------------
+
+``af <operation> <value>``
+ Change audio filter chain. See ``vf`` command.
+
+``vf <operation> <value>``
+ Change video filter chain.
+
+ The semantics are exactly the same as with option parsing (see
+ `VIDEO FILTERS`_). As such the text below is a redundant and incomplete
+ summary.
+
+ The first argument decides what happens:
+
+ <set>
+ Overwrite the previous filter chain with the new one.
+
+ <add>
+ Append the new filter chain to the previous one.
+
+ <toggle>
+ Check if the given filter (with the exact parameters) is already in the
+ video chain. If it is, remove the filter. If it isn't, add the filter.
+ (If several filters are passed to the command, this is done for
+ each filter.)
+
+ A special variant is combining this with labels, and using ``@name``
+ without filter name and parameters as filter entry. This toggles the
+ enable/disable flag.
+
+ <remove>
+ Like ``toggle``, but always remove the given filter from the chain.
+
+ <clr>
+ Remove all filters. Note that like the other sub-commands, this does
+ not control automatically inserted filters.
+
+ The argument is always needed. E.g. in case of ``clr`` use ``vf clr ""``.
+
+ You can assign labels to filter by prefixing them with ``@name:`` (where
+ ``name`` is a user-chosen arbitrary identifier). Labels can be used to
+ refer to filters by name in all of the filter chain modification commands.
+ For ``add``, using an already used label will replace the existing filter.
+
+ The ``vf`` command shows the list of requested filters on the OSD after
+ changing the filter chain. This is roughly equivalent to
+ ``show-text ${vf}``. Note that auto-inserted filters for format conversion
+ are not shown on the list, only what was requested by the user.
+
+ Normally, the commands will check whether the video chain is recreated
+ successfully, and will undo the operation on failure. If the command is run
+ before video is configured (can happen if the command is run immediately
+ after opening a file and before a video frame is decoded), this check can't
+ be run. Then it can happen that creating the video chain fails.
+
+ .. admonition:: Example for input.conf
+
+ - ``a vf set vflip`` turn the video upside-down on the ``a`` key
+ - ``b vf set ""`` remove all video filters on ``b``
+ - ``c vf toggle gradfun`` toggle debanding on ``c``
+
+ .. admonition:: Example how to toggle disabled filters at runtime
+
+ - Add something like ``vf-add=@deband:!gradfun`` to ``mpv.conf``.
+ The ``@deband:`` is the label, an arbitrary, user-given name for this
+ filter entry. The ``!`` before the filter name disables the filter by
+ default. Everything after this is the normal filter name and possibly
+ filter parameters, like in the normal ``--vf`` syntax.
+ - Add ``a vf toggle @deband`` to ``input.conf``. This toggles the
+ "disabled" flag for the filter with the label ``deband`` when the
+ ``a`` key is hit.
+
+``cycle-values [<"!reverse">] <property> <value1> [<value2> [...]]``
+ Cycle through a list of values. Each invocation of the command will set the
+ given property to the next value in the list. The command will use the
+ current value of the property/option, and use it to determine the current
+ position in the list of values. Once it has found it, it will set the
+ next value in the list (wrapping around to the first item if needed).
+
+ This command has a variable number of arguments, and cannot be used with
+ named arguments.
+
+ The special argument ``!reverse`` can be used to cycle the value list in
+ reverse. The only advantage is that you don't need to reverse the value
+ list yourself when adding a second key binding for cycling backwards.
+
+``enable-section <name> [<flags>]``
+ This command is deprecated, except for mpv-internal uses.
+
+ Enable all key bindings in the named input section.
+
+ The enabled input sections form a stack. Bindings in sections on the top of
+ the stack are preferred to lower sections. This command puts the section
+ on top of the stack. If the section was already on the stack, it is
+ implicitly removed beforehand. (A section cannot be on the stack more than
+ once.)
+
+ The ``flags`` parameter can be a combination (separated by ``+``) of the
+ following flags:
+
+ <exclusive>
+ All sections enabled before the newly enabled section are disabled.
+ They will be re-enabled as soon as all exclusive sections above them
+ are removed. In other words, the new section shadows all previous
+ sections.
+ <allow-hide-cursor>
+ This feature can't be used through the public API.
+ <allow-vo-dragging>
+ Same.
+
+``disable-section <name>``
+ This command is deprecated, except for mpv-internal uses.
+
+ Disable the named input section. Undoes ``enable-section``.
+
+``define-section <name> <contents> [<flags>]``
+ This command is deprecated, except for mpv-internal uses.
+
+ Create a named input section, or replace the contents of an already existing
+ input section. The ``contents`` parameter uses the same syntax as the
+ ``input.conf`` file (except that using the section syntax in it is not
+ allowed), including the need to separate bindings with a newline character.
+
+ If the ``contents`` parameter is an empty string, the section is removed.
+
+ The section with the name ``default`` is the normal input section.
+
+ In general, input sections have to be enabled with the ``enable-section``
+ command, or they are ignored.
+
+ The last parameter has the following meaning:
+
+ <default> (also used if parameter omitted)
+ Use a key binding defined by this section only if the user hasn't
+ already bound this key to a command.
+ <force>
+ Always bind a key. (The input section that was made active most recently
+ wins if there are ambiguities.)
+
+ This command can be used to dispatch arbitrary keys to a script or a client
+ API user. If the input section defines ``script-binding`` commands, it is
+ also possible to get separate events on key up/down, and relatively detailed
+ information about the key state. The special key name ``unmapped`` can be
+ used to match any unmapped key.
+
+``overlay-add <id> <x> <y> <file> <offset> <fmt> <w> <h> <stride>``
+ Add an OSD overlay sourced from raw data. This might be useful for scripts
+ and applications controlling mpv, and which want to display things on top
+ of the video window.
+
+ Overlays are usually displayed in screen resolution, but with some VOs,
+ the resolution is reduced to that of the video's. You can read the
+ ``osd-width`` and ``osd-height`` properties. At least with ``--vo-xv`` and
+ anamorphic video (such as DVD), ``osd-par`` should be read as well, and the
+ overlay should be aspect-compensated.
+
+ This has the following named arguments. The order of them is not guaranteed,
+ so you should always call them with named arguments, see `Named arguments`_.
+
+ ``id`` is an integer between 0 and 63 identifying the overlay element. The
+ ID can be used to add multiple overlay parts, update a part by using this
+ command with an already existing ID, or to remove a part with
+ ``overlay-remove``. Using a previously unused ID will add a new overlay,
+ while reusing an ID will update it.
+
+ ``x`` and ``y`` specify the position where the OSD should be displayed.
+
+ ``file`` specifies the file the raw image data is read from. It can be
+ either a numeric UNIX file descriptor prefixed with ``@`` (e.g. ``@4``),
+ or a filename. The file will be mapped into memory with ``mmap()``,
+ copied, and unmapped before the command returns (changed in mpv 0.18.1).
+
+ It is also possible to pass a raw memory address for use as bitmap memory
+ by passing a memory address as integer prefixed with an ``&`` character.
+ Passing the wrong thing here will crash the player. This mode might be
+ useful for use with libmpv. The ``offset`` parameter is simply added to the
+ memory address (since mpv 0.8.0, ignored before).
+
+ ``offset`` is the byte offset of the first pixel in the source file.
+ (The current implementation always mmap's the whole file from position 0 to
+ the end of the image, so large offsets should be avoided. Before mpv 0.8.0,
+ the offset was actually passed directly to ``mmap``, but it was changed to
+ make using it easier.)
+
+ ``fmt`` is a string identifying the image format. Currently, only ``bgra``
+ is defined. This format has 4 bytes per pixels, with 8 bits per component.
+ The least significant 8 bits are blue, and the most significant 8 bits
+ are alpha (in little endian, the components are B-G-R-A, with B as first
+ byte). This uses premultiplied alpha: every color component is already
+ multiplied with the alpha component. This means the numeric value of each
+ component is equal to or smaller than the alpha component. (Violating this
+ rule will lead to different results with different VOs: numeric overflows
+ resulting from blending broken alpha values is considered something that
+ shouldn't happen, and consequently implementations don't ensure that you
+ get predictable behavior in this case.)
+
+ ``w``, ``h``, and ``stride`` specify the size of the overlay. ``w`` is the
+ visible width of the overlay, while ``stride`` gives the width in bytes in
+ memory. In the simple case, and with the ``bgra`` format, ``stride==4*w``.
+ In general, the total amount of memory accessed is ``stride * h``.
+ (Technically, the minimum size would be ``stride * (h - 1) + w * 4``, but
+ for simplicity, the player will access all ``stride * h`` bytes.)
+
+ .. note::
+
+ Before mpv 0.18.1, you had to do manual "double buffering" when updating
+ an overlay by replacing it with a different memory buffer. Since mpv
+ 0.18.1, the memory is simply copied and doesn't reference any of the
+ memory indicated by the command's arguments after the command returns.
+ If you want to use this command before mpv 0.18.1, reads the old docs
+ to see how to handle this correctly.
+
+``overlay-remove <id>``
+ Remove an overlay added with ``overlay-add`` and the same ID. Does nothing
+ if no overlay with this ID exists.
+
+``osd-overlay``
+ Add/update/remove an OSD overlay.
+
+ (Although this sounds similar to ``overlay-add``, ``osd-overlay`` is for
+ text overlays, while ``overlay-add`` is for bitmaps. Maybe ``overlay-add``
+ will be merged into ``osd-overlay`` to remove this oddity.)
+
+ You can use this to add text overlays in ASS format. ASS has advanced
+ positioning and rendering tags, which can be used to render almost any kind
+ of vector graphics.
+
+ This command accepts the following parameters:
+
+ ``id``
+ Arbitrary integer that identifies the overlay. Multiple overlays can be
+ added by calling this command with different ``id`` parameters. Calling
+ this command with the same ``id`` replaces the previously set overlay.
+
+ There is a separate namespace for each libmpv client (i.e. IPC
+ connection, script), so IDs can be made up and assigned by the API user
+ without conflicting with other API users.
+
+ If the libmpv client is destroyed, all overlays associated with it are
+ also deleted. In particular, connecting via ``--input-ipc-server``,
+ adding an overlay, and disconnecting will remove the overlay immediately
+ again.
+
+ ``format``
+ String that gives the type of the overlay. Accepts the following values
+ (HTML rendering of this is broken, view the generated manpage instead,
+ or the raw RST source):
+
+ ``ass-events``
+ The ``data`` parameter is a string. The string is split on the
+ newline character. Every line is turned into the ``Text`` part of
+ a ``Dialogue`` ASS event. Timing is unused (but behavior of timing
+ dependent ASS tags may change in future mpv versions).
+
+ Note that it's better to put multiple lines into ``data``, instead
+ of adding multiple OSD overlays.
+
+ This provides 2 ASS ``Styles``. ``OSD`` contains the text style as
+ defined by the current ``--osd-...`` options. ``Default`` is
+ similar, and contains style that ``OSD`` would have if all options
+ were set to the default.
+
+ In addition, the ``res_x`` and ``res_y`` options specify the value
+ of the ASS ``PlayResX`` and ``PlayResY`` header fields. If ``res_y``
+ is set to 0, ``PlayResY`` is initialized to an arbitrary default
+ value (but note that the default for this command is 720, not 0).
+ If ``res_x`` is set to 0, ``PlayResX`` is set based on ``res_y``
+ such that a virtual ASS pixel has a square pixel aspect ratio.
+
+ ``none``
+ Special value that causes the overlay to be removed. Most parameters
+ other than ``id`` and ``format`` are mostly ignored.
+
+ ``data``
+ String defining the overlay contents according to the ``format``
+ parameter.
+
+ ``res_x``, ``res_y``
+ Used if ``format`` is set to ``ass-events`` (see description there).
+ Optional, defaults to 0/720.
+
+ ``z``
+ The Z order of the overlay. Optional, defaults to 0.
+
+ Note that Z order between different overlays of different formats is
+ static, and cannot be changed (currently, this means that bitmap
+ overlays added by ``overlay-add`` are always on top of the ASS overlays
+ added by ``osd-overlay``). In addition, the builtin OSD components are
+ always below any of the custom OSD. (This includes subtitles of any kind
+ as well as text rendered by ``show-text``.)
+
+ It's possible that future mpv versions will randomly change how Z order
+ between different OSD formats and builtin OSD is handled.
+
+ ``hidden``
+ If set to true, do not display this (default: false).
+
+ ``compute_bounds``
+ If set to true, attempt to determine bounds and write them to the
+ command's result value as ``x0``, ``x1``, ``y0``, ``y1`` rectangle
+ (default: false). If the rectangle is empty, not known, or somehow
+ degenerate, it is not set. ``x1``/``y1`` is the coordinate of the
+ bottom exclusive corner of the rectangle.
+
+ The result value may depend on the VO window size, and is based on the
+ last known window size at the time of the call. This means the results
+ may be different from what is actually rendered.
+
+ For ``ass-events``, the result rectangle is recomputed to ``PlayRes``
+ coordinates (``res_x``/``res_y``). If window size is not known, a
+ fallback is chosen.
+
+ You should be aware that this mechanism is very inefficient, as it
+ renders the full result, and then uses the bounding box of the rendered
+ bitmap list (even if ``hidden`` is set). It will flush various caches.
+ Its results also depend on the used libass version.
+
+ This feature is experimental, and may change in some way again.
+
+ .. note::
+
+ Always use named arguments (``mpv_command_node()``). Lua scripts should
+ use the ``mp.create_osd_overlay()`` helper instead of invoking this
+ command directly.
+
+``script-message [<arg1> [<arg2> [...]]]``
+ Send a message to all clients, and pass it the following list of arguments.
+ What this message means, how many arguments it takes, and what the arguments
+ mean is fully up to the receiver and the sender. Every client receives the
+ message, so be careful about name clashes (or use ``script-message-to``).
+
+ This command has a variable number of arguments, and cannot be used with
+ named arguments.
+
+``script-message-to <target> [<arg1> [<arg2> [...]]]``
+ Same as ``script-message``, but send it only to the client named
+ ``<target>``. Each client (scripts etc.) has a unique name. For example,
+ Lua scripts can get their name via ``mp.get_script_name()``. Note that
+ client names only consist of alphanumeric characters and ``_``.
+
+ This command has a variable number of arguments, and cannot be used with
+ named arguments.
+
+``script-binding <name>``
+ Invoke a script-provided key binding. This can be used to remap key
+ bindings provided by external Lua scripts.
+
+ The argument is the name of the binding.
+
+ It can optionally be prefixed with the name of the script, using ``/`` as
+ separator, e.g. ``script-binding scriptname/bindingname``. Note that script
+ names only consist of alphanumeric characters and ``_``.
+
+ For completeness, here is how this command works internally. The details
+ could change any time. On any matching key event, ``script-message-to``
+ or ``script-message`` is called (depending on whether the script name is
+ included), with the following arguments:
+
+ 1. The string ``key-binding``.
+ 2. The name of the binding (as established above).
+ 3. The key state as string (see below).
+ 4. The key name (since mpv 0.15.0).
+ 5. The text the key would produce, or empty string if not applicable.
+
+ The 5th argument is only set if no modifiers are present (using the shift
+ key with a letter is normally not emitted as having a modifier, and results
+ in upper case text instead, but some backends may mess up).
+
+ The key state consists of 2 characters:
+
+ 1. One of ``d`` (key was pressed down), ``u`` (was released), ``r`` (key
+ is still down, and was repeated; only if key repeat is enabled for this
+ binding), ``p`` (key was pressed; happens if up/down can't be tracked).
+ 2. Whether the event originates from the mouse, either ``m`` (mouse button)
+ or ``-`` (something else).
+
+ Future versions can add more arguments and more key state characters to
+ support more input peculiarities.
+
+``ab-loop``
+ Cycle through A-B loop states. The first command will set the ``A`` point
+ (the ``ab-loop-a`` property); the second the ``B`` point, and the third
+ will clear both points.
+
+``drop-buffers``
+ Drop audio/video/demuxer buffers, and restart from fresh. Might help with
+ unseekable streams that are going out of sync.
+ This command might be changed or removed in the future.
+
+``screenshot-raw [<flags>]``
+ Return a screenshot in memory. This can be used only through the client
+ API. The MPV_FORMAT_NODE_MAP returned by this command has the ``w``, ``h``,
+ ``stride`` fields set to obvious contents. The ``format`` field is set to
+ ``bgr0`` by default. This format is organized as ``B8G8R8X8`` (where ``B``
+ is the LSB). The contents of the padding ``X`` are undefined. The ``data``
+ field is of type MPV_FORMAT_BYTE_ARRAY with the actual image data. The image
+ is freed as soon as the result mpv_node is freed. As usual with client API
+ semantics, you are not allowed to write to the image data.
+
+ The ``stride`` is the number of bytes from a pixel at ``(x0, y0)`` to the
+ pixel at ``(x0, y0 + 1)``. This can be larger than ``w * 4`` if the image
+ was cropped, or if there is padding. This number can be negative as well.
+ You access a pixel with ``byte_index = y * stride + x * 4`` (assuming the
+ ``bgr0`` format).
+
+ The ``flags`` argument is like the first argument to ``screenshot`` and
+ supports ``subtitles``, ``video``, ``window``.
+
+``vf-command <label> <command> <argument> [<target>]``
+ Send a command to the filter. Note that currently, this only works with
+ the ``lavfi`` filter. Refer to the libavfilter documentation for the list
+ of supported commands for each filter.
+
+ ``<label>`` is a mpv filter label, use ``all`` to send it to all filters
+ at once.
+
+ ``<command>`` and ``<argument>`` are filter-specific strings.
+
+ ``<target>`` is a filter or filter instance name and defaults to ``all``.
+ Note that the target is an additional specifier for filters that
+ support them, such as complex ``lavfi`` filter chains.
+
+``af-command <label> <command> <argument> [<target>]``
+ Same as ``vf-command``, but for audio filters.
+
+``apply-profile <name> [<mode>]``
+ Apply the contents of a named profile. This is like using ``profile=name``
+ in a config file, except you can map it to a key binding to change it at
+ runtime.
+
+ The mode argument:
+
+ ``default``
+ Apply the profile. Default if the argument is omitted.
+
+ ``restore``
+ Restore options set by a previous ``apply-profile`` command for this
+ profile. Only works if the profile has ``profile-restore`` set to a
+ relevant mode. Prints a warning if nothing could be done. See
+ `Runtime profiles`_ for details.
+
+``load-script <filename>``
+ Load a script, similar to the ``--script`` option. Whether this waits for
+ the script to finish initialization or not changed multiple times, and the
+ future behavior is left undefined.
+
+ On success, returns a ``mpv_node`` with a ``client_id`` field set to the
+ return value of the ``mpv_client_id()`` API call of the newly created script
+ handle.
+
+``change-list <name> <operation> <value>``
+ This command changes list options as described in `List Options`_. The
+ ``<name>`` parameter is the normal option name, while ``<operation>`` is
+ the suffix or action used on the option.
+
+ Some operations take no value, but the command still requires the value
+ parameter. In these cases, the value must be an empty string.
+
+ .. admonition:: Example
+
+ ``change-list glsl-shaders append file.glsl``
+
+ Add a filename to the ``glsl-shaders`` list. The command line
+ equivalent is ``--glsl-shaders-append=file.glsl`` or alternatively
+ ``--glsl-shader=file.glsl``.
+
+``dump-cache <start> <end> <filename>``
+ Dump the current cache to the given filename. The ``<filename>`` file is
+ overwritten if it already exists. ``<start>`` and ``<end>`` give the
+ time range of what to dump. If no data is cached at the given time range,
+ nothing may be dumped (creating a file with no packets).
+
+ Dumping a larger part of the cache will freeze the player. No effort was
+ made to fix this, as this feature was meant mostly for creating small
+ excerpts.
+
+ See ``--stream-record`` for various caveats that mostly apply to this
+ command too, as both use the same underlying code for writing the output
+ file.
+
+ If ``<filename>`` is an empty string, an ongoing ``dump-cache`` is stopped.
+
+ If ``<end>`` is ``no``, then continuous dumping is enabled. Then, after
+ dumping the existing parts of the cache, anything read from network is
+ appended to the cache as well. This behaves similar to ``--stream-record``
+ (although it does not conflict with that option, and they can be both active
+ at the same time).
+
+ If the ``<end>`` time is after the cache, the command will _not_ wait and
+ write newly received data to it.
+
+ The end of the resulting file may be slightly damaged or incomplete at the
+ end. (Not enough effort was made to ensure that the end lines up properly.)
+
+ Note that this command will finish only once dumping ends. That means it
+ works similar to the ``screenshot`` command, just that it can block much
+ longer. If continuous dumping is used, the command will not finish until
+ playback is stopped, an error happens, another ``dump-cache`` command is
+ run, or an API like ``mp.abort_async_command`` was called to explicitly stop
+ the command. See `Synchronous vs. Asynchronous`_.
+
+ .. note::
+
+ This was mostly created for network streams. For local files, there may
+ be much better methods to create excerpts and such. There are tons of
+ much more user-friendly Lua scripts, that will re-encode parts of a file
+ by spawning a separate instance of ``ffmpeg``. With network streams,
+ this is not that easily possible, as the stream would have to be
+ downloaded again. Even if ``--stream-record`` is used to record the
+ stream to the local filesystem, there may be problems, because the
+ recorded file is still written to.
+
+ This command is experimental, and all details about it may change in the
+ future.
+
+``ab-loop-dump-cache <filename>``
+ Essentially calls ``dump-cache`` with the current AB-loop points as
+ arguments. Like ``dump-cache``, this will overwrite the file at
+ ``<filename>``. Likewise, if the B point is set to ``no``, it will enter
+ continuous dumping after the existing cache was dumped.
+
+ The author reserves the right to remove this command if enough motivation
+ is found to move this functionality to a trivial Lua script.
+
+``ab-loop-align-cache``
+ Re-adjust the A/B loop points to the start and end within the cache the
+ ``ab-loop-dump-cache`` command will (probably) dump. Basically, it aligns
+ the times on keyframes. The guess might be off especially at the end (due to
+ granularity issues due to remuxing). If the cache shrinks in the meantime,
+ the points set by the command will not be the effective parameters either.
+
+ This command has an even more uncertain future than ``ab-loop-dump-cache``
+ and might disappear without replacement if the author decides it's useless.
+
+Undocumented commands: ``ao-reload`` (experimental/internal).
+
+List of events
+~~~~~~~~~~~~~~
+
+This is a partial list of events. This section describes what
+``mpv_event_to_node()`` returns, and which is what scripting APIs and the JSON
+IPC sees. Note that the C API has separate C-level declarations with
+``mpv_event``, which may be slightly different.
+
+Note that events are asynchronous: the player core continues running while
+events are delivered to scripts and other clients. In some cases, you can use
+hooks to enforce synchronous execution.
+
+All events can have the following fields:
+
+``event``
+ Name as the event (as returned by ``mpv_event_name()``).
+
+``id``
+ The ``reply_userdata`` field (opaque user value). If ``reply_userdata`` is 0,
+ the field is not added.
+
+``error``
+ Set to an error string (as returned by ``mpv_error_string()``). This field
+ is missing if no error happened, or the event type does not report error.
+ Most events leave this unset.
+
+This list uses the event name field value, and the C API symbol in brackets:
+
+``start-file`` (``MPV_EVENT_START_FILE``)
+ Happens right before a new file is loaded. When you receive this, the
+ player is loading the file (or possibly already done with it).
+
+ This has the following fields:
+
+ ``playlist_entry_id``
+ Playlist entry ID of the file being loaded now.
+
+``end-file`` (``MPV_EVENT_END_FILE``)
+ Happens after a file was unloaded. Typically, the player will load the
+ next file right away, or quit if this was the last file.
+
+ The event has the following fields:
+
+ ``reason``
+ Has one of these values:
+
+ ``eof``
+ The file has ended. This can (but doesn't have to) include
+ incomplete files or broken network connections under
+ circumstances.
+
+ ``stop``
+ Playback was ended by a command.
+
+ ``quit``
+ Playback was ended by sending the quit command.
+
+ ``error``
+ An error happened. In this case, an ``error`` field is present with
+ the error string.
+
+ ``redirect``
+ Happens with playlists and similar. Details see
+ ``MPV_END_FILE_REASON_REDIRECT`` in the C API.
+
+ ``unknown``
+ Unknown. Normally doesn't happen, unless the Lua API is out of sync
+ with the C API. (Likewise, it could happen that your script gets
+ reason strings that did not exist yet at the time your script was
+ written.)
+
+ ``playlist_entry_id``
+ Playlist entry ID of the file that was being played or attempted to be
+ played. This has the same value as the ``playlist_entry_id`` field in the
+ corresponding ``start-file`` event.
+
+ ``file_error``
+ Set to mpv error string describing the approximate reason why playback
+ failed. Unset if no error known. (In Lua scripting, this value was set
+ on the ``error`` field directly. This is deprecated since mpv 0.33.0.
+ In the future, this ``error`` field will be unset for this specific
+ event.)
+
+ ``playlist_insert_id``
+ If loading ended, because the playlist entry to be played was for example
+ a playlist, and the current playlist entry is replaced with a number of
+ other entries. This may happen at least with MPV_END_FILE_REASON_REDIRECT
+ (other event types may use this for similar but different purposes in the
+ future). In this case, playlist_insert_id will be set to the playlist
+ entry ID of the first inserted entry, and playlist_insert_num_entries to
+ the total number of inserted playlist entries. Note this in this specific
+ case, the ID of the last inserted entry is playlist_insert_id+num-1.
+ Beware that depending on circumstances, you may observe the new playlist
+ entries before seeing the event (e.g. reading the "playlist" property or
+ getting a property change notification before receiving the event).
+ If this is 0 in the C API, this field isn't added.
+
+ ``playlist_insert_num_entries``
+ See playlist_insert_id. Only present if playlist_insert_id is present.
+
+``file-loaded`` (``MPV_EVENT_FILE_LOADED``)
+ Happens after a file was loaded and begins playback.
+
+``seek`` (``MPV_EVENT_SEEK``)
+ Happens on seeking. (This might include cases when the player seeks
+ internally, even without user interaction. This includes e.g. segment
+ changes when playing ordered chapters Matroska files.)
+
+``playback-restart`` (``MPV_EVENT_PLAYBACK_RESTART``)
+ Start of playback after seek or after file was loaded.
+
+``shutdown`` (``MPV_EVENT_SHUTDOWN``)
+ Sent when the player quits, and the script should terminate. Normally
+ handled automatically. See `Details on the script initialization and lifecycle`_.
+
+``log-message`` (``MPV_EVENT_LOG_MESSAGE``)
+ Receives messages enabled with ``mpv_request_log_messages()`` (Lua:
+ ``mp.enable_messages``).
+
+ This contains, in addition to the default event fields, the following
+ fields:
+
+ ``prefix``
+ The module prefix, identifies the sender of the message. This is what
+ the terminal player puts in front of the message text when using the
+ ``--v`` option, and is also what is used for ``--msg-level``.
+
+ ``level``
+ The log level as string. See ``msg.log`` for possible log level names.
+ Note that later versions of mpv might add new levels or remove
+ (undocumented) existing ones.
+
+ ``text``
+ The log message. The text will end with a newline character. Sometimes
+ it can contain multiple lines.
+
+ Keep in mind that these messages are meant to be hints for humans. You
+ should not parse them, and prefix/level/text of messages might change
+ any time.
+
+``hook``
+ The event has the following fields:
+
+ ``hook_id``
+ ID to pass to ``mpv_hook_continue()``. The Lua scripting wrapper
+ provides a better API around this with ``mp.add_hook()``.
+
+``get-property-reply`` (``MPV_EVENT_GET_PROPERTY_REPLY``)
+ See C API.
+
+``set-property-reply`` (``MPV_EVENT_SET_PROPERTY_REPLY``)
+ See C API.
+
+``command-reply`` (``MPV_EVENT_COMMAND_REPLY``)
+ This is one of the commands for which the ```error`` field is meaningful.
+
+ JSON IPC and Lua and possibly other backends treat this specially and may
+ not pass the actual event to the user. See C API.
+
+ The event has the following fields:
+
+ ``result``
+ The result (on success) of any ``mpv_node`` type, if any.
+
+``client-message`` (``MPV_EVENT_CLIENT_MESSAGE``)
+ Lua and possibly other backends treat this specially and may not pass the
+ actual event to the user.
+
+ The event has the following fields:
+
+ ``args``
+ Array of strings with the message data.
+
+``video-reconfig`` (``MPV_EVENT_VIDEO_RECONFIG``)
+ Happens on video output or filter reconfig.
+
+``audio-reconfig`` (``MPV_EVENT_AUDIO_RECONFIG``)
+ Happens on audio output or filter reconfig.
+
+``property-change`` (``MPV_EVENT_PROPERTY_CHANGE``)
+ Happens when a property that is being observed changes value.
+
+ The event has the following fields:
+
+ ``name``
+ The name of the property.
+
+ ``data``
+ The new value of the property.
+
+The following events also happen, but are deprecated: ``idle``, ``tick``
+Use ``mpv_observe_property()`` (Lua: ``mp.observe_property()``) instead.
+
+Hooks
+~~~~~
+
+Hooks are synchronous events between player core and a script or similar. This
+applies to client API (including the Lua scripting interface). Normally,
+events are supposed to be asynchronous, and the hook API provides an awkward
+and obscure way to handle events that require stricter coordination. There are
+no API stability guarantees made. Not following the protocol exactly can make
+the player freeze randomly. Basically, nobody should use this API.
+
+The C API is described in the header files. The Lua API is described in the
+Lua section.
+
+Before a hook is actually invoked on an API clients, it will attempt to return
+new values for all observed properties that were changed before the hook. This
+may make it easier for an application to set defined "barriers" between property
+change notifications by registering hooks. (That means these hooks will have an
+effect, even if you do nothing and make them continue immediately.)
+
+The following hooks are currently defined:
+
+``on_load``
+ Called when a file is to be opened, before anything is actually done.
+ For example, you could read and write the ``stream-open-filename``
+ property to redirect an URL to something else (consider support for
+ streaming sites which rarely give the user a direct media URL), or
+ you could set per-file options with by setting the property
+ ``file-local-options/<option name>``. The player will wait until all
+ hooks are run.
+
+ Ordered after ``start-file`` and before ``playback-restart``.
+
+``on_load_fail``
+ Called after after a file has been opened, but failed to. This can be
+ used to provide a fallback in case native demuxers failed to recognize
+ the file, instead of always running before the native demuxers like
+ ``on_load``. Demux will only be retried if ``stream-open-filename``
+ was changed. If it fails again, this hook is _not_ called again, and
+ loading definitely fails.
+
+ Ordered after ``on_load``, and before ``playback-restart`` and ``end-file``.
+
+``on_preloaded``
+ Called after a file has been opened, and before tracks are selected and
+ decoders are created. This has some usefulness if an API users wants
+ to select tracks manually, based on the set of available tracks. It's
+ also useful to initialize ``--lavfi-complex`` in a specific way by API,
+ without having to "probe" the available streams at first.
+
+ Note that this does not yet apply default track selection. Which operations
+ exactly can be done and not be done, and what information is available and
+ what is not yet available yet, is all subject to change.
+
+ Ordered after ``on_load_fail`` etc. and before ``playback-restart``.
+
+``on_unload``
+ Run before closing a file, and before actually uninitializing
+ everything. It's not possible to resume playback in this state.
+
+ Ordered before ``end-file``. Will also happen in the error case (then after
+ ``on_load_fail``).
+
+``on_before_start_file``
+ Run before a ``start-file`` event is sent. (If any client changes the
+ current playlist entry, or sends a quit command to the player, the
+ corresponding event will not actually happen after the hook returns.)
+ Useful to drain property changes before a new file is loaded.
+
+``on_after_end_file``
+ Run after an ``end-file`` event. Useful to drain property changes after a
+ file has finished.
+
+Input Command Prefixes
+----------------------
+
+These prefixes are placed between key name and the actual command. Multiple
+prefixes can be specified. They are separated by whitespace.
+
+``osd-auto``
+ Use the default behavior for this command. This is the default for
+ ``input.conf`` commands. Some libmpv/scripting/IPC APIs do not use this as
+ default, but use ``no-osd`` instead.
+``no-osd``
+ Do not use any OSD for this command.
+``osd-bar``
+ If possible, show a bar with this command. Seek commands will show the
+ progress bar, property changing commands may show the newly set value.
+``osd-msg``
+ If possible, show an OSD message with this command. Seek command show
+ the current playback time, property changing commands show the newly set
+ value as text.
+``osd-msg-bar``
+ Combine osd-bar and osd-msg.
+``raw``
+ Do not expand properties in string arguments. (Like ``"${property-name}"``.)
+ This is the default for some libmpv/scripting/IPC APIs.
+``expand-properties``
+ All string arguments are expanded as described in `Property Expansion`_.
+ This is the default for ``input.conf`` commands.
+``repeatable``
+ For some commands, keeping a key pressed doesn't run the command repeatedly.
+ This prefix forces enabling key repeat in any case. For a list of commands:
+ the first command determines the repeatability of the whole list (up to and
+ including version 0.33 - a list was always repeatable).
+``async``
+ Allow asynchronous execution (if possible). Note that only a few commands
+ will support this (usually this is explicitly documented). Some commands
+ are asynchronous by default (or rather, their effects might manifest
+ after completion of the command). The semantics of this flag might change
+ in the future. Set it only if you don't rely on the effects of this command
+ being fully realized when it returns. See `Synchronous vs. Asynchronous`_.
+``sync``
+ Allow synchronous execution (if possible). Normally, all commands are
+ synchronous by default, but some are asynchronous by default for
+ compatibility with older behavior.
+
+All of the osd prefixes are still overridden by the global ``--osd-level``
+settings.
+
+Synchronous vs. Asynchronous
+----------------------------
+
+The ``async`` and ``sync`` prefix matter only for how the issuer of the command
+waits on the completion of the command. Normally it does not affect how the
+command behaves by itself. There are the following cases:
+
+- Normal input.conf commands are always run asynchronously. Slow running
+ commands are queued up or run in parallel.
+- "Multi" input.conf commands (1 key binding, concatenated with ``;``) will be
+ executed in order, except for commands that are async (either prefixed with
+ ``async``, or async by default for some commands). The async commands are
+ run in a detached manner, possibly in parallel to the remaining sync commands
+ in the list.
+- Normal Lua and libmpv commands (e.g. ``mpv_command()``) are run in a blocking
+ manner, unless the ``async`` prefix is used, or the command is async by
+ default. This means in the sync case the caller will block, even if the core
+ continues playback. Async mode runs the command in a detached manner.
+- Async libmpv command API (e.g. ``mpv_command_async()``) never blocks the
+ caller, and always notify their completion with a message. The ``sync`` and
+ ``async`` prefixes make no difference.
+- Lua also provides APIs for running async commands, which behave similar to the
+ C counterparts.
+- In all cases, async mode can still run commands in a synchronous manner, even
+ in detached mode. This can for example happen in cases when a command does not
+ have an asynchronous implementation. The async libmpv API still never blocks
+ the caller in these cases.
+
+Before mpv 0.29.0, the ``async`` prefix was only used by screenshot commands,
+and made them run the file saving code in a detached manner. This is the
+default now, and ``async`` changes behavior only in the ways mentioned above.
+
+Currently the following commands have different waiting characteristics with
+sync vs. async: sub-add, audio-add, sub-reload, audio-reload,
+rescan-external-files, screenshot, screenshot-to-file, dump-cache,
+ab-loop-dump-cache.
+
+Asynchronous command details
+----------------------------
+
+On the API level, every asynchronous command is bound to the context which
+started it. For example, an asynchronous command started by ``mpv_command_async``
+is bound to the ``mpv_handle`` passed to the function. Only this ``mpv_handle``
+receives the completion notification (``MPV_EVENT_COMMAND_REPLY``), and only
+this handle can abort a still running command directly. If the ``mpv_handle`` is
+destroyed, any still running async. commands started by it are terminated.
+
+The scripting APIs and JSON IPC give each script/connection its own implicit
+``mpv_handle``.
+
+If the player is closed, the core may abort all pending async. commands on its
+own (like a forced ``mpv_abort_async_command()`` call for each pending command
+on behalf of the API user). This happens at the same time ``MPV_EVENT_SHUTDOWN``
+is sent, and there is no way to prevent this.
+
+Input Sections
+--------------
+
+Input sections group a set of bindings, and enable or disable them at once.
+In ``input.conf``, each key binding is assigned to an input section, rather
+than actually having explicit text sections.
+
+See also: ``enable-section`` and ``disable-section`` commands.
+
+Predefined bindings:
+
+``default``
+ Bindings without input section are implicitly assigned to this section. It
+ is enabled by default during normal playback.
+``encode``
+ Section which is active in encoding mode. It is enabled exclusively, so
+ that bindings in the ``default`` sections are ignored.
+
+Properties
+----------
+
+Properties are used to set mpv options during runtime, or to query arbitrary
+information. They can be manipulated with the ``set``/``add``/``cycle``
+commands, and retrieved with ``show-text``, or anything else that uses property
+expansion. (See `Property Expansion`_.)
+
+The property name is annotated with RW to indicate whether the property is
+generally writable.
+
+If an option is referenced, the property will normally take/return exactly the
+same values as the option. In these cases, properties are merely a way to change
+an option at runtime.
+
+Property list
+-------------
+
+.. note::
+
+ Most options can be set at runtime via properties as well. Just remove the
+ leading ``--`` from the option name. These are not documented below, see
+ `OPTIONS`_ instead. Only properties which do not exist as option with the
+ same name, or which have very different behavior from the options are
+ documented below.
+
+ Properties marked as (RW) are writeable, while those that aren't are
+ read-only.
+
+``audio-speed-correction``, ``video-speed-correction``
+ Factor multiplied with ``speed`` at which the player attempts to play the
+ file. Usually it's exactly 1. (Display sync mode will make this useful.)
+
+ OSD formatting will display it in the form of ``+1.23456%``, with the number
+ being ``(raw - 1) * 100`` for the given raw property value.
+
+``display-sync-active``
+ Whether ``--video-sync=display`` is actually active.
+
+``filename``
+ Currently played file, with path stripped. If this is an URL, try to undo
+ percent encoding as well. (The result is not necessarily correct, but
+ looks better for display purposes. Use the ``path`` property to get an
+ unmodified filename.)
+
+ This has a sub-property:
+
+ ``filename/no-ext``
+ Like the ``filename`` property, but if the text contains a ``.``, strip
+ all text after the last ``.``. Usually this removes the file extension.
+
+``file-size``
+ Length in bytes of the source file/stream. (This is the same as
+ ``${stream-end}``. For segmented/multi-part files, this will return the
+ size of the main or manifest file, whatever it is.)
+
+``estimated-frame-count``
+ Total number of frames in current file.
+
+ .. note:: This is only an estimate. (It's computed from two unreliable
+ quantities: fps and stream length.)
+
+``estimated-frame-number``
+ Number of current frame in current stream.
+
+ .. note:: This is only an estimate. (It's computed from two unreliable
+ quantities: fps and possibly rounded timestamps.)
+
+``pid``
+ Process-id of mpv.
+
+``path``
+ Full path of the currently played file. Usually this is exactly the same
+ string you pass on the mpv command line or the ``loadfile`` command, even
+ if it's a relative path. If you expect an absolute path, you will have to
+ determine it yourself, for example by using the ``working-directory``
+ property.
+
+``stream-open-filename``
+ The full path to the currently played media. This is different from
+ ``path`` only in special cases. In particular, if ``--ytdl=yes`` is used,
+ and the URL is detected by ``youtube-dl``, then the script will set this
+ property to the actual media URL. This property should be set only during
+ the ``on_load`` or ``on_load_fail`` hooks, otherwise it will have no effect
+ (or may do something implementation defined in the future). The property is
+ reset if playback of the current media ends.
+
+``media-title``
+ If the currently played file has a ``title`` tag, use that.
+
+ Otherwise, return the ``filename`` property.
+
+``file-format``
+ Symbolic name of the file format. In some cases, this is a comma-separated
+ list of format names, e.g. mp4 is ``mov,mp4,m4a,3gp,3g2,mj2`` (the list
+ may grow in the future for any format).
+
+``current-demuxer``
+ Name of the current demuxer. (This is useless.)
+
+ (Renamed from ``demuxer``.)
+
+``stream-path``
+ Filename (full path) of the stream layer filename. (This is probably
+ useless and is almost never different from ``path``.)
+
+``stream-pos``
+ Raw byte position in source stream. Technically, this returns the position
+ of the most recent packet passed to a decoder.
+
+``stream-end``
+ Raw end position in bytes in source stream.
+
+``duration``
+ Duration of the current file in seconds. If the duration is unknown, the
+ property is unavailable. Note that the file duration is not always exactly
+ known, so this is an estimate.
+
+ This replaces the ``length`` property, which was deprecated after the
+ mpv 0.9 release. (The semantics are the same.)
+
+ This has a sub-property:
+
+ ``duration/full``
+ ``duration`` with milliseconds.
+
+``avsync``
+ Last A/V synchronization difference. Unavailable if audio or video is
+ disabled.
+
+``total-avsync-change``
+ Total A-V sync correction done. Unavailable if audio or video is
+ disabled.
+
+``decoder-frame-drop-count``
+ Video frames dropped by decoder, because video is too far behind audio (when
+ using ``--framedrop=decoder``). Sometimes, this may be incremented in other
+ situations, e.g. when video packets are damaged, or the decoder doesn't
+ follow the usual rules. Unavailable if video is disabled.
+
+``frame-drop-count``
+ Frames dropped by VO (when using ``--framedrop=vo``).
+
+``mistimed-frame-count``
+ Number of video frames that were not timed correctly in display-sync mode
+ for the sake of keeping A/V sync. This does not include external
+ circumstances, such as video rendering being too slow or the graphics
+ driver somehow skipping a vsync. It does not include rounding errors either
+ (which can happen especially with bad source timestamps). For example,
+ using the ``display-desync`` mode should never change this value from 0.
+
+``vsync-ratio``
+ For how many vsyncs a frame is displayed on average. This is available if
+ display-sync is active only. For 30 FPS video on a 60 Hz screen, this will
+ be 2. This is the moving average of what actually has been scheduled, so
+ 24 FPS on 60 Hz will never remain exactly on 2.5, but jitter depending on
+ the last frame displayed.
+
+``vo-delayed-frame-count``
+ Estimated number of frames delayed due to external circumstances in
+ display-sync mode. Note that in general, mpv has to guess that this is
+ happening, and the guess can be inaccurate.
+
+``percent-pos`` (RW)
+ Position in current file (0-100). The advantage over using this instead of
+ calculating it out of other properties is that it properly falls back to
+ estimating the playback position from the byte position, if the file
+ duration is not known.
+
+``time-pos`` (RW)
+ Position in current file in seconds.
+
+ This has a sub-property:
+
+ ``time-pos/full``
+ ``time-pos`` with milliseconds.
+
+``time-start``
+ Deprecated. Always returns 0. Before mpv 0.14, this used to return the start
+ time of the file (could affect e.g. transport streams). See
+ ``--rebase-start-time`` option.
+
+``time-remaining``
+ Remaining length of the file in seconds. Note that the file duration is not
+ always exactly known, so this is an estimate.
+
+ This has a sub-property:
+
+ ``time-remaining/full``
+ ``time-remaining`` with milliseconds.
+
+``audio-pts``
+ Current audio playback position in current file in seconds. Unlike time-pos,
+ this updates more often than once per frame. For audio-only files, it is
+ mostly equivalent to time-pos, while for video-only files this property is
+ not available.
+
+ This has a sub-property:
+
+ ``audio-pts/full``
+ ``audio-pts`` with milliseconds.
+
+``playtime-remaining``
+ ``time-remaining`` scaled by the current ``speed``.
+
+ This has a sub-property:
+
+ ``playtime-remaining/full``
+ ``playtime-remaining`` with milliseconds.
+
+``playback-time`` (RW)
+ Position in current file in seconds. Unlike ``time-pos``, the time is
+ clamped to the range of the file. (Inaccurate file durations etc. could
+ make it go out of range. Useful on attempts to seek outside of the file,
+ as the seek target time is considered the current position during seeking.)
+
+ This has a sub-property:
+
+ ``playback-time/full``
+ ``playback-time`` with milliseconds.
+
+``chapter`` (RW)
+ Current chapter number. The number of the first chapter is 0.
+
+``edition`` (RW)
+ Current MKV edition number. Setting this property to a different value will
+ restart playback. The number of the first edition is 0.
+
+ Before mpv 0.31.0, this showed the actual edition selected at runtime, if
+ you didn't set the option or property manually. With mpv 0.31.0 and later,
+ this strictly returns the user-set option or property value, and the
+ ``current-edition`` property was added to return the runtime selected
+ edition (this matters with ``--edition=auto``, the default).
+
+``current-edition``
+ Currently selected edition. This property is unavailable if no file is
+ loaded, or the file has no editions. (Matroska files make a difference
+ between having no editions and a single edition, which will be reflected by
+ the property, although in practice it does not matter.)
+
+``chapters``
+ Number of chapters.
+
+``editions``
+ Number of MKV editions.
+
+``edition-list``
+ List of editions, current entry marked. Currently, the raw property value
+ is useless.
+
+ This has a number of sub-properties. Replace ``N`` with the 0-based edition
+ index.
+
+ ``edition-list/count``
+ Number of editions. If there are no editions, this can be 0 or 1 (1
+ if there's a useless dummy edition).
+
+ ``edition-list/N/id`` (RW)
+ Edition ID as integer. Use this to set the ``edition`` property.
+ Currently, this is the same as the edition index.
+
+ ``edition-list/N/default``
+ Whether this is the default edition.
+
+ ``edition-list/N/title``
+ Edition title as stored in the file. Not always available.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each edition)
+ "id" MPV_FORMAT_INT64
+ "title" MPV_FORMAT_STRING
+ "default" MPV_FORMAT_FLAG
+
+``metadata``
+ Metadata key/value pairs.
+
+ If the property is accessed with Lua's ``mp.get_property_native``, this
+ returns a table with metadata keys mapping to metadata values. If it is
+ accessed with the client API, this returns a ``MPV_FORMAT_NODE_MAP``,
+ with tag keys mapping to tag values.
+
+ For OSD, it returns a formatted list. Trying to retrieve this property as
+ a raw string doesn't work.
+
+ This has a number of sub-properties:
+
+ ``metadata/by-key/<key>``
+ Value of metadata entry ``<key>``.
+
+ ``metadata/list/count``
+ Number of metadata entries.
+
+ ``metadata/list/N/key``
+ Key name of the Nth metadata entry. (The first entry is ``0``).
+
+ ``metadata/list/N/value``
+ Value of the Nth metadata entry.
+
+ ``metadata/<key>``
+ Old version of ``metadata/by-key/<key>``. Use is discouraged, because
+ the metadata key string could conflict with other sub-properties.
+
+ The layout of this property might be subject to change. Suggestions are
+ welcome how exactly this property should work.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_MAP
+ (key and string value for each metadata entry)
+
+``filtered-metadata``
+ Like ``metadata``, but includes only fields listed in the ``--display-tags``
+ option. This is the same set of tags that is printed to the terminal.
+
+``chapter-metadata``
+ Metadata of current chapter. Works similar to ``metadata`` property. It
+ also allows the same access methods (using sub-properties).
+
+ Per-chapter metadata is very rare. Usually, only the chapter name
+ (``title``) is set.
+
+ For accessing other information, like chapter start, see the
+ ``chapter-list`` property.
+
+``vf-metadata/<filter-label>``
+ Metadata added by video filters. Accessed by the filter label,
+ which, if not explicitly specified using the ``@filter-label:`` syntax,
+ will be ``<filter-name>NN``.
+
+ Works similar to ``metadata`` property. It allows the same access
+ methods (using sub-properties).
+
+ An example of this kind of metadata are the cropping parameters
+ added by ``--vf=lavfi=cropdetect``.
+
+``af-metadata/<filter-label>``
+ Equivalent to ``vf-metadata/<filter-label>``, but for audio filters.
+
+``idle-active``
+ Returns ``yes``/true if no file is loaded, but the player is staying around
+ because of the ``--idle`` option.
+
+ (Renamed from ``idle``.)
+
+``core-idle``
+ Whether the playback core is paused. This can differ from ``pause`` in
+ special situations, such as when the player pauses itself due to low
+ network cache.
+
+ This also returns ``yes``/true if playback is restarting or if nothing is
+ playing at all. In other words, it's only ``no``/false if there's actually
+ video playing. (Behavior since mpv 0.7.0.)
+
+``cache-speed``
+ Current I/O read speed between the cache and the lower layer (like network).
+ This gives the number bytes per seconds over a 1 second window (using
+ the type ``MPV_FORMAT_INT64`` for the client API).
+
+ This is the same as ``demuxer-cache-state/raw-input-rate``.
+
+``demuxer-cache-duration``
+ Approximate duration of video buffered in the demuxer, in seconds. The
+ guess is very unreliable, and often the property will not be available
+ at all, even if data is buffered.
+
+``demuxer-cache-time``
+ Approximate time of video buffered in the demuxer, in seconds. Same as
+ ``demuxer-cache-duration`` but returns the last timestamp of buffered
+ data in demuxer.
+
+``demuxer-cache-idle``
+ Whether the demuxer is idle, which means that the demuxer cache is filled
+ to the requested amount, and is currently not reading more data.
+
+``demuxer-cache-state``
+ Each entry in ``seekable-ranges`` represents a region in the demuxer cache
+ that can be seeked to, with a ``start`` and ``end`` fields containing the
+ respective timestamps. If there are multiple demuxers active, this only
+ returns information about the "main" demuxer, but might be changed in
+ future to return unified information about all demuxers. The ranges are in
+ arbitrary order. Often, ranges will overlap for a bit, before being joined.
+ In broken corner cases, ranges may overlap all over the place.
+
+ The end of a seek range is usually smaller than the value returned by the
+ ``demuxer-cache-time`` property, because that property returns the guessed
+ buffering amount, while the seek ranges represent the buffered data that
+ can actually be used for cached seeking.
+
+ ``bof-cached`` indicates whether the seek range with the lowest timestamp
+ points to the beginning of the stream (BOF). This implies you cannot seek
+ before this position at all. ``eof-cached`` indicates whether the seek range
+ with the highest timestamp points to the end of the stream (EOF). If both
+ ``bof-cached`` and ``eof-cached`` are true, and there's only 1 cache range,
+ the entire stream is cached.
+
+ ``fw-bytes`` is the number of bytes of packets buffered in the range
+ starting from the current decoding position. This is a rough estimate
+ (may not account correctly for various overhead), and stops at the
+ demuxer position (it ignores seek ranges after it).
+
+ ``file-cache-bytes`` is the number of bytes stored in the file cache. This
+ includes all overhead, and possibly unused data (like pruned data). This
+ member is missing if the file cache wasn't enabled with
+ ``--cache-on-disk=yes``.
+
+ ``cache-end`` is ``demuxer-cache-time``. Missing if unavailable.
+
+ ``reader-pts`` is the approximate timestamp of the start of the buffered
+ range. Missing if unavailable.
+
+ ``cache-duration`` is ``demuxer-cache-duration``. Missing if unavailable.
+
+ ``raw-input-rate`` is the estimated input rate of the network layer (or any
+ other byte-oriented input layer) in bytes per second. May be inaccurate or
+ missing.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_MAP
+ "seekable-ranges" MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP
+ "start" MPV_FORMAT_DOUBLE
+ "end" MPV_FORMAT_DOUBLE
+ "bof-cached" MPV_FORMAT_FLAG
+ "eof-cached" MPV_FORMAT_FLAG
+ "fw-bytes" MPV_FORMAT_INT64
+ "file-cache-bytes" MPV_FORMAT_INT64
+ "cache-end" MPV_FORMAT_DOUBLE
+ "reader-pts" MPV_FORMAT_DOUBLE
+ "cache-duration" MPV_FORMAT_DOUBLE
+ "raw-input-rate" MPV_FORMAT_INT64
+
+ Other fields (might be changed or removed in the future):
+
+ ``eof``
+ Whether the reader thread has hit the end of the file.
+
+ ``underrun``
+ Whether the reader thread could not satisfy a decoder's request for a
+ new packet.
+
+ ``idle``
+ Whether the thread is currently not reading.
+
+ ``total-bytes``
+ Sum of packet bytes (plus some overhead estimation) of the entire packet
+ queue, including cached seekable ranges.
+
+``demuxer-via-network``
+ Whether the stream demuxed via the main demuxer is most likely played via
+ network. What constitutes "network" is not always clear, might be used for
+ other types of untrusted streams, could be wrong in certain cases, and its
+ definition might be changing. Also, external files (like separate audio
+ files or streams) do not influence the value of this property (currently).
+
+``demuxer-start-time``
+ The start time reported by the demuxer in fractional seconds.
+
+``paused-for-cache``
+ Whether playback is paused because of waiting for the cache.
+
+``cache-buffering-state``
+ The percentage (0-100) of the cache fill status until the player will
+ unpause (related to ``paused-for-cache``).
+
+``eof-reached``
+ Whether the end of playback was reached. Note that this is usually
+ interesting only if ``--keep-open`` is enabled, since otherwise the player
+ will immediately play the next file (or exit or enter idle mode), and in
+ these cases the ``eof-reached`` property will logically be cleared
+ immediately after it's set.
+
+``seeking``
+ Whether the player is currently seeking, or otherwise trying to restart
+ playback. (It's possible that it returns ``yes``/true while a file is
+ loaded. This is because the same underlying code is used for seeking and
+ resyncing.)
+
+``mixer-active``
+ Whether the audio mixer is active.
+
+ This option is relatively useless. Before mpv 0.18.1, it could be used to
+ infer behavior of the ``volume`` property.
+
+``ao-volume`` (RW)
+ System volume. This property is available only if mpv audio output is
+ currently active, and only if the underlying implementation supports volume
+ control. What this option does depends on the API. For example, on ALSA
+ this usually changes system-wide audio, while with PulseAudio this controls
+ per-application volume.
+
+``ao-mute`` (RW)
+ Similar to ``ao-volume``, but controls the mute state. May be unimplemented
+ even if ``ao-volume`` works.
+
+``audio-codec``
+ Audio codec selected for decoding.
+
+``audio-codec-name``
+ Audio codec.
+
+``audio-params``
+ Audio format as output by the audio decoder.
+ This has a number of sub-properties:
+
+ ``audio-params/format``
+ The sample format as string. This uses the same names as used in other
+ places of mpv.
+
+ ``audio-params/samplerate``
+ Samplerate.
+
+ ``audio-params/channels``
+ The channel layout as a string. This is similar to what the
+ ``--audio-channels`` accepts.
+
+ ``audio-params/hr-channels``
+ As ``channels``, but instead of the possibly cryptic actual layout
+ sent to the audio device, return a hopefully more human readable form.
+ (Usually only ``audio-out-params/hr-channels`` makes sense.)
+
+ ``audio-params/channel-count``
+ Number of audio channels. This is redundant to the ``channels`` field
+ described above.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_MAP
+ "format" MPV_FORMAT_STRING
+ "samplerate" MPV_FORMAT_INT64
+ "channels" MPV_FORMAT_STRING
+ "channel-count" MPV_FORMAT_INT64
+ "hr-channels" MPV_FORMAT_STRING
+
+``audio-out-params``
+ Same as ``audio-params``, but the format of the data written to the audio
+ API.
+
+``colormatrix``
+ Redirects to ``video-params/colormatrix``. This parameter (as well as
+ similar ones) can be overridden with the ``format`` video filter.
+
+``colormatrix-input-range``
+ See ``colormatrix``.
+
+``colormatrix-primaries``
+ See ``colormatrix``.
+
+``hwdec`` (RW)
+ Reflects the ``--hwdec`` option.
+
+ Writing to it may change the currently used hardware decoder, if possible.
+ (Internally, the player may reinitialize the decoder, and will perform a
+ seek to refresh the video properly.) You can watch the other hwdec
+ properties to see whether this was successful.
+
+ Unlike in mpv 0.9.x and before, this does not return the currently active
+ hardware decoder. Since mpv 0.18.0, ``hwdec-current`` is available for
+ this purpose.
+
+``hwdec-current``
+ The current hardware decoding in use. If decoding is active, return one of
+ the values used by the ``hwdec`` option/property. ``no``/false indicates
+ software decoding. If no decoder is loaded, the property is unavailable.
+
+``hwdec-interop``
+ This returns the currently loaded hardware decoding/output interop driver.
+ This is known only once the VO has opened (and possibly later). With some
+ VOs (like ``gpu``), this might be never known in advance, but only when
+ the decoder attempted to create the hw decoder successfully. (Using
+ ``--gpu-hwdec-interop`` can load it eagerly.) If there are multiple
+ drivers loaded, they will be separated by ``,``.
+
+ If no VO is active or no interop driver is known, this property is
+ unavailable.
+
+ This does not necessarily use the same values as ``hwdec``. There can be
+ multiple interop drivers for the same hardware decoder, depending on
+ platform and VO.
+
+``video-format``
+ Video format as string.
+
+``video-codec``
+ Video codec selected for decoding.
+
+``width``, ``height``
+ Video size. This uses the size of the video as decoded, or if no video
+ frame has been decoded yet, the (possibly incorrect) container indicated
+ size.
+
+``video-params``
+ Video parameters, as output by the decoder (with overrides like aspect
+ etc. applied). This has a number of sub-properties:
+
+ ``video-params/pixelformat``
+ The pixel format as string. This uses the same names as used in other
+ places of mpv.
+
+ ``video-params/hw-pixelformat``
+ The underlying pixel format as string. This is relevant for some cases
+ of hardware decoding and unavailable otherwise.
+
+ ``video-params/average-bpp``
+ Average bits-per-pixel as integer. Subsampled planar formats use a
+ different resolution, which is the reason this value can sometimes be
+ odd or confusing. Can be unavailable with some formats.
+
+ ``video-params/w``, ``video-params/h``
+ Video size as integers, with no aspect correction applied.
+
+ ``video-params/dw``, ``video-params/dh``
+ Video size as integers, scaled for correct aspect ratio.
+
+ ``video-params/crop-x``, ``video-params/crop-y``
+ Crop offset of the source video frame.
+
+ ``video-params/crop-w``, ``video-params/crop-h``
+ Video size after cropping.
+
+ ``video-params/aspect``
+ Display aspect ratio as float.
+
+ ``video-params/aspect-name``
+ Display aspect ratio name as string. The name coresponds to motion
+ picture film format that introduced given aspect ratio in film.
+
+ ``video-params/par``
+ Pixel aspect ratio.
+
+ ``video-params/sar``
+ Storage aspect ratio.
+
+ ``video-params/sar-name``
+ Storage aspect ratio name as string.
+
+ ``video-params/colormatrix``
+ The colormatrix in use as string. (Exact values subject to change.)
+
+ ``video-params/colorlevels``
+ The colorlevels as string. (Exact values subject to change.)
+
+ ``video-params/primaries``
+ The primaries in use as string. (Exact values subject to change.)
+
+ ``video-params/gamma``
+ The gamma function in use as string. (Exact values subject to change.)
+
+ ``video-params/sig-peak`` (deprecated)
+ The video file's tagged signal peak as float.
+
+ ``video-params/light``
+ The light type in use as a string. (Exact values subject to change.)
+
+ ``video-params/chroma-location``
+ Chroma location as string. (Exact values subject to change.)
+
+ ``video-params/rotate``
+ Intended display rotation in degrees (clockwise).
+
+ ``video-params/stereo-in``
+ Source file stereo 3D mode. (See the ``format`` video filter's
+ ``stereo-in`` option.)
+
+ ``video-params/alpha``
+ Alpha type. If the format has no alpha channel, this will be unavailable
+ (but in future releases, it could change to ``no``). If alpha is
+ present, this is set to ``straight`` or ``premul``.
+
+ ``video-params/min-luma``
+ Minimum luminance, as reported by HDR10 metadata (in cd/m²)
+
+ ``video-params/max-luma``
+ Maximum luminance, as reported by HDR10 metadata (in cd/m²)
+
+ ``video-params/max-cll``
+ Maximum content light level, as reported by HDR10 metadata (in cd/m²)
+
+ ``video-params/max-fall``
+ Maximum frame average light level, as reported by HDR10 metadata (in cd/m²)
+
+ ``video-params/scene-max-r``
+ MaxRGB of a scene for R component, as reported by HDR10+ metadata (in cd/m²)
+
+ ``video-params/scene-max-g``
+ MaxRGB of a scene for G component, as reported by HDR10+ metadata (in cd/m²)
+
+ ``video-params/scene-max-b``
+ MaxRGB of a scene for B component, as reported by HDR10+ metadata (in cd/m²)
+
+ ``video-params/max-pq-y``
+ Maximum PQ luminance of a frame, as reported by peak detection (in PQ, 0-1)
+
+ ``video-params/avg-pq-y``
+ Average PQ luminance of a frame, as reported by peak detection (in PQ, 0-1)
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_MAP
+ "pixelformat" MPV_FORMAT_STRING
+ "hw-pixelformat" MPV_FORMAT_STRING
+ "w" MPV_FORMAT_INT64
+ "h" MPV_FORMAT_INT64
+ "dw" MPV_FORMAT_INT64
+ "dh" MPV_FORMAT_INT64
+ "aspect" MPV_FORMAT_DOUBLE
+ "par" MPV_FORMAT_DOUBLE
+ "colormatrix" MPV_FORMAT_STRING
+ "colorlevels" MPV_FORMAT_STRING
+ "primaries" MPV_FORMAT_STRING
+ "gamma" MPV_FORMAT_STRING
+ "sig-peak" MPV_FORMAT_DOUBLE
+ "light" MPV_FORMAT_STRING
+ "chroma-location" MPV_FORMAT_STRING
+ "rotate" MPV_FORMAT_INT64
+ "stereo-in" MPV_FORMAT_STRING
+ "average-bpp" MPV_FORMAT_INT64
+ "alpha" MPV_FORMAT_STRING
+ "min-luma" MPV_FORMAT_DOUBLE
+ "max-luma" MPV_FORMAT_DOUBLE
+ "max-cll" MPV_FORMAT_DOUBLE
+ "max-fall" MPV_FORMAT_DOUBLE
+ "scene-max-r" MPV_FORMAT_DOUBLE
+ "scene-max-g" MPV_FORMAT_DOUBLE
+ "scene-max-b" MPV_FORMAT_DOUBLE
+ "max-pq-y" MPV_FORMAT_DOUBLE
+ "avg-pq-y" MPV_FORMAT_DOUBLE
+
+``dwidth``, ``dheight``
+ Video display size. This is the video size after filters and aspect scaling
+ have been applied. The actual video window size can still be different
+ from this, e.g. if the user resized the video window manually.
+
+ These have the same values as ``video-out-params/dw`` and
+ ``video-out-params/dh``.
+
+``video-dec-params``
+ Exactly like ``video-params``, but no overrides applied.
+
+``video-out-params``
+ Same as ``video-params``, but after video filters have been applied. If
+ there are no video filters in use, this will contain the same values as
+ ``video-params``. Note that this is still not necessarily what the video
+ window uses, since the user can change the window size, and all real VOs
+ do their own scaling independently from the filter chain.
+
+ Has the same sub-properties as ``video-params``.
+
+``video-frame-info``
+ Approximate information of the current frame. Note that if any of these
+ are used on OSD, the information might be off by a few frames due to OSD
+ redrawing and frame display being somewhat disconnected, and you might
+ have to pause and force a redraw.
+
+ This has a number of sub-properties:
+
+ ``video-frame-info/picture-type``
+ The type of the picture. It can be "I" (intra), "P" (predicted), "B"
+ (bi-dir predicted) or unavailable.
+
+ ``video-frame-info/interlaced``
+ Whether the content of the frame is interlaced.
+
+ ``video-frame-info/tff``
+ If the content is interlaced, whether the top field is displayed first.
+
+ ``video-frame-info/repeat``
+ Whether the frame must be delayed when decoding.
+
+``container-fps``
+ Container FPS. This can easily contain bogus values. For videos that use
+ modern container formats or video codecs, this will often be incorrect.
+
+ (Renamed from ``fps``.)
+
+``estimated-vf-fps``
+ Estimated/measured FPS of the video filter chain output. (If no filters
+ are used, this corresponds to decoder output.) This uses the average of
+ the 10 past frame durations to calculate the FPS. It will be inaccurate
+ if frame-dropping is involved (such as when framedrop is explicitly
+ enabled, or after precise seeking). Files with imprecise timestamps (such
+ as Matroska) might lead to unstable results.
+
+``window-scale`` (RW)
+ Window size multiplier. Setting this will resize the video window to the
+ values contained in ``dwidth`` and ``dheight`` multiplied with the value
+ set with this property. Setting ``1`` will resize to original video size
+ (or to be exact, the size the video filters output). ``2`` will set the
+ double size, ``0.5`` halves the size.
+
+ Note that setting a value identical to its previous value will not resize
+ the window. That's because this property mirrors the ``window-scale``
+ option, and setting an option to its previous value is ignored. If this
+ value is set while the window is in a fullscreen, the multiplier is not
+ applied until the window is taken out of that state. Writing this property
+ to a maximized window can unmaximize the window depending on the OS and
+ window manager. If the window does not unmaximize, the multiplier will be
+ applied if the user unmaximizes the window later.
+
+ See ``current-window-scale`` for the value derived from the actual window
+ size.
+
+ Since mpv 0.31.0, this always returns the previously set value (or the
+ default value), instead of the value implied by the actual window size.
+ Before mpv 0.31.0, this returned what ``current-window-scale`` returns now,
+ after the window was created.
+
+``current-window-scale`` (RW)
+ The ``window-scale`` value calculated from the current window size. This
+ has the same value as ``window-scale`` if the window size was not changed
+ since setting the option, and the window size was not restricted in other
+ ways. If the window is fullscreened, this will return the scale value
+ calculated from the last non-fullscreen size of the window. The property
+ is unavailable if no video is active.
+
+ When setting this property in the fullscreen or maximized state, the behavior
+ is the same as window-scale. In all other cases, setting the value of this
+ property will always resize the window. This does not affect the value of
+ ``window-scale``.
+
+``focused``
+ Whether the window has focus. Might not be supported by all VOs.
+
+``display-names``
+ Names of the displays that the mpv window covers. On X11, these
+ are the xrandr names (LVDS1, HDMI1, DP1, VGA1, etc.). On Windows, these
+ are the GDI names (\\.\DISPLAY1, \\.\DISPLAY2, etc.) and the first display
+ in the list will be the one that Windows considers associated with the
+ window (as determined by the MonitorFromWindow API.) On macOS these are the
+ Display Product Names as used in the System Information and only one display
+ name is returned since a window can only be on one screen.
+
+``display-fps``
+ The refresh rate of the current display. Currently, this is the lowest FPS
+ of any display covered by the video, as retrieved by the underlying system
+ APIs (e.g. xrandr on X11). It is not the measured FPS. It's not necessarily
+ available on all platforms. Note that any of the listed facts may change
+ any time without a warning.
+
+``estimated-display-fps``
+ The actual rate at which display refreshes seem to occur, measured by
+ system time. Only available if display-sync mode (as selected by
+ ``--video-sync``) is active.
+
+``vsync-jitter``
+ Estimated deviation factor of the vsync duration.
+
+``display-width``, ``display-height``
+ The current display's horizontal and vertical resolution in pixels. Whether
+ or not these values update as the mpv window changes displays depends on
+ the windowing backend. It may not be available on all platforms.
+
+``display-hidpi-scale``
+ The HiDPI scale factor as reported by the windowing backend. If no VO is
+ active, or if the VO does not report a value, this property is unavailable.
+ It may be saner to report an absolute DPI, however, this is the way HiDPI
+ support is implemented on most OS APIs. See also ``--hidpi-window-scale``.
+
+``osd-width``, ``osd-height``
+ Last known OSD width (can be 0). This is needed if you want to use the
+ ``overlay-add`` command. It gives you the actual OSD/window size (not
+ including decorations drawn by the OS window manager).
+
+ Alias to ``osd-dimensions/w`` and ``osd-dimensions/h``.
+
+``osd-par``
+ Last known OSD display pixel aspect (can be 0).
+
+ Alias to ``osd-dimensions/osd-par``.
+
+``osd-dimensions``
+ Last known OSD dimensions.
+
+ Has the following sub-properties (which can be read as ``MPV_FORMAT_NODE``
+ or Lua table with ``mp.get_property_native``):
+
+ ``osd-dimensions/w``
+ Size of the VO window in OSD render units (usually pixels, but may be
+ scaled pixels with VOs like ``xv``).
+
+ ``osd-dimensions/h``
+ Size of the VO window in OSD render units,
+
+ ``osd-dimensions/par``
+ Pixel aspect ratio of the OSD (usually 1).
+
+ ``osd-dimensions/aspect``
+ Display aspect ratio of the VO window. (Computing from the properties
+ above.)
+
+ ``osd-dimensions/mt``, ``osd-dimensions/mb``, ``osd-dimensions/ml``, ``osd-dimensions/mr``
+ OSD to video margins (top, bottom, left, right). This describes the
+ area into which the video is rendered.
+
+ Any of these properties may be unavailable or set to dummy values if the
+ VO window is not created or visible.
+
+``window-id``
+ Read-only - mpv's window id. May not always be available, i.e due to window
+ not being opened yet or not being supported by the VO.
+
+``mouse-pos``
+ Read-only - last known mouse position, normalizd to OSD dimensions.
+
+ Has the following sub-properties (which can be read as ``MPV_FORMAT_NODE``
+ or Lua table with ``mp.get_property_native``):
+
+ ``mouse-pos/x``, ``mouse-pos/y``
+ Last known coordinates of the mouse pointer.
+
+ ``mouse-pos/hover``
+ Boolean - whether the mouse pointer hovers the video window. The
+ coordinates should be ignored when this value is false, because the
+ video backends update them only when the pointer hovers the window.
+
+``sub-ass-extradata``
+ The current ASS subtitle track's extradata. There is no formatting done.
+ The extradata is returned as a string as-is. This property is not
+ available for non-ASS subtitle tracks.
+
+``sub-text``
+ The current subtitle text regardless of sub visibility. Formatting is
+ stripped. If the subtitle is not text-based (i.e. DVD/BD subtitles), an
+ empty string is returned.
+
+``sub-text-ass``
+ Like ``sub-text``, but return the text in ASS format. Text subtitles in
+ other formats are converted. For native ASS subtitles, events that do
+ not contain any text (but vector drawings etc.) are not filtered out. If
+ multiple events match with the current playback time, they are concatenated
+ with line breaks. Contains only the "Text" part of the events.
+
+ This property is not enough to render ASS subtitles correctly, because ASS
+ header and per-event metadata are not returned. You likely need to do
+ further filtering on the returned string to make it useful.
+
+``secondary-sub-text``
+ Same as ``sub-text``, but for the secondary subtitles.
+
+``sub-start``
+ The current subtitle start time (in seconds). If there's multiple current
+ subtitles, returns the first start time. If no current subtitle is present
+ null is returned instead.
+
+``secondary-sub-start``
+ Same as ``sub-start``, but for the secondary subtitles.
+
+``sub-end``
+ The current subtitle end time (in seconds). If there's multiple current
+ subtitles, return the last end time. If no current subtitle is present, or
+ if it's present but has unknown or incorrect duration, null is returned
+ instead.
+
+``secondary-sub-end``
+ Same as ``sub-end``, but for the secondary subtitles.
+
+``playlist-pos`` (RW)
+ Current position on playlist. The first entry is on position 0. Writing to
+ this property may start playback at the new position.
+
+ In some cases, this is not necessarily the currently playing file. See
+ explanation of ``current`` and ``playing`` flags in ``playlist``.
+
+ If there the playlist is empty, or if it's non-empty, but no entry is
+ "current", this property returns -1. Likewise, writing -1 will put the
+ player into idle mode (or exit playback if idle mode is not enabled). If an
+ out of range index is written to the property, this behaves as if writing -1.
+ (Before mpv 0.33.0, instead of returning -1, this property was unavailable
+ if no playlist entry was current.)
+
+ Writing the current value back to the property will have no effect.
+ Use ``playlist-play-index`` to restart the playback of the current entry if
+ desired.
+
+``playlist-pos-1`` (RW)
+ Same as ``playlist-pos``, but 1-based.
+
+``playlist-current-pos`` (RW)
+ Index of the "current" item on playlist. This usually, but not necessarily,
+ the currently playing item (see ``playlist-playing-pos``). Depending on the
+ exact internal state of the player, it may refer to the playlist item to
+ play next, or the playlist item used to determine what to play next.
+
+ For reading, this is exactly the same as ``playlist-pos``.
+
+ For writing, this *only* sets the position of the "current" item, without
+ stopping playback of the current file (or starting playback, if this is done
+ in idle mode). Use -1 to remove the current flag.
+
+ This property is only vaguely useful. If set during playback, it will
+ typically cause the playlist entry *after* it to be played next. Another
+ possibly odd observable state is that if ``playlist-next`` is run during
+ playback, this property is set to the playlist entry to play next (unlike
+ the previous case). There is an internal flag that decides whether the
+ current playlist entry or the next one should be played, and this flag is
+ currently inaccessible for API users. (Whether this behavior will kept is
+ possibly subject to change.)
+
+``playlist-playing-pos``
+ Index of the "playing" item on playlist. A playlist item is "playing" if
+ it's being loaded, actually playing, or being unloaded. This property is set
+ during the ``MPV_EVENT_START_FILE`` (``start-file``) and the
+ ``MPV_EVENT_START_END`` (``end-file``) events. Outside of that, it returns
+ -1. If the playlist entry was somehow removed during playback, but playback
+ hasn't stopped yet, or is in progress of being stopped, it also returns -1.
+ (This can happen at least during state transitions.)
+
+ In the "playing" state, this is usually the same as ``playlist-pos``, except
+ during state changes, or if ``playlist-current-pos`` was written explicitly.
+
+``playlist-count``
+ Number of total playlist entries.
+
+``playlist-path``
+ The original path of the playlist for the current entry before mpv expanded
+ the entries. Unavailable if the file was not originally associated with a
+ playlist in some way.
+
+``playlist``
+ Playlist, current entry marked. Currently, the raw property value is
+ useless.
+
+ This has a number of sub-properties. Replace ``N`` with the 0-based playlist
+ entry index.
+
+ ``playlist/count``
+ Number of playlist entries (same as ``playlist-count``).
+
+ ``playlist/N/filename``
+ Filename of the Nth entry.
+
+ ``playlist/N/playing``
+ ``yes``/true if the ``playlist-playing-pos`` property points to this
+ entry, ``no``/false or unavailable otherwise.
+
+ ``playlist/N/current``
+ ``yes``/true if the ``playlist-current-pos`` property points to this
+ entry, ``no``/false or unavailable otherwise.
+
+ ``playlist/N/title``
+ Name of the Nth entry. Available if the playlist file contains
+ such fields and mpv's parser supports it for the given
+ playlist format, or if the playlist entry has been opened before and a
+ media-title other then then filename has been acquired.
+
+ ``playlist/N/id``
+ Unique ID for this entry. This is an automatically assigned integer ID
+ that is unique for the entire life time of the current mpv core
+ instance. Other commands, events, etc. use this as ``playlist_entry_id``
+ fields.
+
+ ``playlist/N/playlist-path``
+ The original path of the playlist for this entry before mpv expanded
+ it. Unavailable if the file was not originally associated with a playlist
+ in some way.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each playlist entry)
+ "filename" MPV_FORMAT_STRING
+ "current" MPV_FORMAT_FLAG (might be missing; since mpv 0.7.0)
+ "playing" MPV_FORMAT_FLAG (same)
+ "title" MPV_FORMAT_STRING (optional)
+ "id" MPV_FORMAT_INT64
+
+``track-list``
+ List of audio/video/sub tracks, current entry marked. Currently, the raw
+ property value is useless.
+
+ This has a number of sub-properties. Replace ``N`` with the 0-based track
+ index.
+
+ ``track-list/count``
+ Total number of tracks.
+
+ ``track-list/N/id``
+ The ID as it's used for ``-sid``/``--aid``/``--vid``. This is unique
+ within tracks of the same type (sub/audio/video), but otherwise not.
+
+ ``track-list/N/type``
+ String describing the media type. One of ``audio``, ``video``, ``sub``.
+
+ ``track-list/N/src-id``
+ Track ID as used in the source file. Not always available. (It is
+ missing if the format has no native ID, if the track is a pseudo-track
+ that does not exist in this way in the actual file, or if the format
+ is handled by libavformat, and the format was not whitelisted as having
+ track IDs.)
+
+ ``track-list/N/title``
+ Track title as it is stored in the file. Not always available.
+
+ ``track-list/N/lang``
+ Track language as identified by the file. Not always available.
+
+ ``track-list/N/image``
+ ``yes``/true if this is a video track that consists of a single
+ picture, ``no``/false or unavailable otherwise. The heuristic used to
+ determine if a stream is an image doesn't attempt to detect images in
+ codecs normally used for videos. Otherwise, it is reliable.
+
+ ``track-list/N/albumart``
+ ``yes``/true if this is an image embedded in an audio file or external
+ cover art, ``no``/false or unavailable otherwise.
+
+ ``track-list/N/default``
+ ``yes``/true if the track has the default flag set in the file,
+ ``no``/false or unavailable otherwise.
+
+ ``track-list/N/forced``
+ ``yes``/true if the track has the forced flag set in the file,
+ ``no``/false or unavailable otherwise.
+
+ ``track-list/N/codec``
+ The codec name used by this track, for example ``h264``. Unavailable
+ in some rare cases.
+
+ ``track-list/N/external``
+ ``yes``/true if the track is an external file, ``no``/false or
+ unavailable otherwise. This is set for separate subtitle files.
+
+ ``track-list/N/external-filename``
+ The filename if the track is from an external file, unavailable
+ otherwise.
+
+ ``track-list/N/selected``
+ ``yes``/true if the track is currently decoded, ``no``/false or
+ unavailable otherwise.
+
+ ``track-list/N/main-selection``
+ It indicates the selection order of tracks for the same type.
+ If a track is not selected, or is selected by the ``--lavfi-complex``,
+ it is not available. For subtitle tracks, ``0`` represents the ``sid``,
+ and ``1`` represents the ``secondary-sid``.
+
+ ``track-list/N/ff-index``
+ The stream index as usually used by the FFmpeg utilities. Note that
+ this can be potentially wrong if a demuxer other than libavformat
+ (``--demuxer=lavf``) is used. For mkv files, the index will usually
+ match even if the default (builtin) demuxer is used, but there is
+ no hard guarantee.
+
+ ``track-list/N/decoder-desc``
+ If this track is being decoded, the human-readable decoder name,
+
+ ``track-list/N/demux-w``, ``track-list/N/demux-h``
+ Video size hint as indicated by the container. (Not always accurate.)
+
+ ``track-list/N/demux-crop-x``, ``track-list/N/demux-crop-y``
+ Crop offset of the source video frame.
+
+ ``track-list/N/demux-crop-w``, ``track-list/N/demux-crop-h``
+ Video size after cropping.
+
+ ``track-list/N/demux-channel-count``
+ Number of audio channels as indicated by the container. (Not always
+ accurate - in particular, the track could be decoded as a different
+ number of channels.)
+
+ ``track-list/N/demux-channels``
+ Channel layout as indicated by the container. (Not always accurate.)
+
+ ``track-list/N/demux-samplerate``
+ Audio sample rate as indicated by the container. (Not always accurate.)
+
+ ``track-list/N/demux-fps``
+ Video FPS as indicated by the container. (Not always accurate.)
+
+ ``track-list/N/demux-bitrate``
+ Audio average bitrate, in bits per second. (Not always accurate.)
+
+ ``track-list/N/demux-rotation``
+ Video clockwise rotation metadata, in degrees.
+
+ ``track-list/N/demux-par``
+ Pixel aspect ratio.
+
+ ``track-list/N/audio-channels`` (deprecated)
+ Deprecated alias for ``track-list/N/demux-channel-count``.
+
+ ``track-list/N/replaygain-track-peak``, ``track-list/N/replaygain-track-gain``
+ Per-track replaygain values. Only available for audio tracks with
+ corresponding information stored in the source file.
+
+ ``track-list/N/replaygain-album-peak``, ``track-list/N/replaygain-album-gain``
+ Per-album replaygain values. If the file has per-track but no per-album
+ information, the per-album values will be copied from the per-track
+ values currently. It's possible that future mpv versions will make
+ these properties unavailable instead in this case.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each track)
+ "id" MPV_FORMAT_INT64
+ "type" MPV_FORMAT_STRING
+ "src-id" MPV_FORMAT_INT64
+ "title" MPV_FORMAT_STRING
+ "lang" MPV_FORMAT_STRING
+ "image" MPV_FORMAT_FLAG
+ "albumart" MPV_FORMAT_FLAG
+ "default" MPV_FORMAT_FLAG
+ "forced" MPV_FORMAT_FLAG
+ "selected" MPV_FORMAT_FLAG
+ "main-selection" MPV_FORMAT_INT64
+ "external" MPV_FORMAT_FLAG
+ "external-filename" MPV_FORMAT_STRING
+ "codec" MPV_FORMAT_STRING
+ "ff-index" MPV_FORMAT_INT64
+ "decoder-desc" MPV_FORMAT_STRING
+ "demux-w" MPV_FORMAT_INT64
+ "demux-h" MPV_FORMAT_INT64
+ "demux-crop-x" MPV_FORMAT_INT64
+ "demux-crop-y" MPV_FORMAT_INT64
+ "demux-crop-w" MPV_FORMAT_INT64
+ "demux-crop-h" MPV_FORMAT_INT64
+ "demux-channel-count" MPV_FORMAT_INT64
+ "demux-channels" MPV_FORMAT_STRING
+ "demux-samplerate" MPV_FORMAT_INT64
+ "demux-fps" MPV_FORMAT_DOUBLE
+ "demux-bitrate" MPV_FORMAT_INT64
+ "demux-rotation" MPV_FORMAT_INT64
+ "demux-par" MPV_FORMAT_DOUBLE
+ "audio-channels" MPV_FORMAT_INT64
+ "replaygain-track-peak" MPV_FORMAT_DOUBLE
+ "replaygain-track-gain" MPV_FORMAT_DOUBLE
+ "replaygain-album-peak" MPV_FORMAT_DOUBLE
+ "replaygain-album-gain" MPV_FORMAT_DOUBLE
+
+``current-tracks/...``
+ This gives access to currently selected tracks. It redirects to the correct
+ entry in ``track-list``.
+
+ The following sub-entries are defined: ``video``, ``audio``, ``sub``,
+ ``sub2``
+
+ For example, ``current-tracks/audio/lang`` returns the current audio track's
+ language field (the same value as ``track-list/N/lang``).
+
+ If tracks of the requested type are selected via ``--lavfi-complex``, the
+ first one is returned.
+
+``chapter-list`` (RW)
+ List of chapters, current entry marked. Currently, the raw property value
+ is useless.
+
+ This has a number of sub-properties. Replace ``N`` with the 0-based chapter
+ index.
+
+ ``chapter-list/count``
+ Number of chapters.
+
+ ``chapter-list/N/title``
+ Chapter title as stored in the file. Not always available.
+
+ ``chapter-list/N/time``
+ Chapter start time in seconds as float.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each chapter)
+ "title" MPV_FORMAT_STRING
+ "time" MPV_FORMAT_DOUBLE
+
+``af``, ``vf`` (RW)
+ See ``--vf``/``--af`` and the ``vf``/``af`` command.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each filter entry)
+ "name" MPV_FORMAT_STRING
+ "label" MPV_FORMAT_STRING [optional]
+ "enabled" MPV_FORMAT_FLAG [optional]
+ "params" MPV_FORMAT_NODE_MAP [optional]
+ "key" MPV_FORMAT_STRING
+ "value" MPV_FORMAT_STRING
+
+ It's also possible to write the property using this format.
+
+``seekable``
+ Whether it's generally possible to seek in the current file.
+
+``partially-seekable``
+ Whether the current file is considered seekable, but only because the cache
+ is active. This means small relative seeks may be fine, but larger seeks
+ may fail anyway. Whether a seek will succeed or not is generally not known
+ in advance.
+
+ If this property returns ``yes``/true, so will ``seekable``.
+
+``playback-abort``
+ Whether playback is stopped or is to be stopped. (Useful in obscure
+ situations like during ``on_load`` hook processing, when the user can stop
+ playback, but the script has to explicitly end processing.)
+
+``cursor-autohide`` (RW)
+ See ``--cursor-autohide``. Setting this to a new value will always update
+ the cursor, and reset the internal timer.
+
+``osd-sym-cc``
+ Inserts the current OSD symbol as opaque OSD control code (cc). This makes
+ sense only with the ``show-text`` command or options which set OSD messages.
+ The control code is implementation specific and is useless for anything else.
+
+``osd-ass-cc``
+ ``${osd-ass-cc/0}`` disables escaping ASS sequences of text in OSD,
+ ``${osd-ass-cc/1}`` enables it again. By default, ASS sequences are
+ escaped to avoid accidental formatting, and this property can disable
+ this behavior. Note that the properties return an opaque OSD control
+ code, which only makes sense for the ``show-text`` command or options
+ which set OSD messages.
+
+ .. admonition:: Example
+
+ - ``--osd-msg3='This is ${osd-ass-cc/0}{\\b1}bold text'``
+ - ``show-text "This is ${osd-ass-cc/0}{\\b1}bold text"``
+
+ Any ASS override tags as understood by libass can be used.
+
+ Note that you need to escape the ``\`` character, because the string is
+ processed for C escape sequences before passing it to the OSD code. See
+ `Flat command syntax`_ for details.
+
+ A list of tags can be found here:
+ https://aegisub.org/docs/latest/ass_tags/
+
+``vo-configured``
+ Whether the VO is configured right now. Usually this corresponds to whether
+ the video window is visible. If the ``--force-window`` option is used, this
+ usually always returns ``yes``/true.
+
+``vo-passes``
+ Contains introspection about the VO's active render passes and their
+ execution times. Not implemented by all VOs.
+
+ This is further subdivided into two frame types, ``vo-passes/fresh`` for
+ fresh frames (which have to be uploaded, scaled, etc.) and
+ ``vo-passes/redraw`` for redrawn frames (which only have to be re-painted).
+ The number of passes for any given subtype can change from frame to frame,
+ and should not be relied upon.
+
+ Each frame type has a number of further sub-properties. Replace ``TYPE``
+ with the frame type, ``N`` with the 0-based pass index, and ``M`` with the
+ 0-based sample index.
+
+ ``vo-passes/TYPE/count``
+ Number of passes.
+
+ ``vo-passes/TYPE/N/desc``
+ Human-friendy description of the pass.
+
+ ``vo-passes/TYPE/N/last``
+ Last measured execution time, in nanoseconds.
+
+ ``vo-passes/TYPE/N/avg``
+ Average execution time of this pass, in nanoseconds. The exact
+ timeframe varies, but it should generally be a handful of seconds.
+
+ ``vo-passes/TYPE/N/peak``
+ The peak execution time (highest value) within this averaging range, in
+ nanoseconds.
+
+ ``vo-passes/TYPE/N/count``
+ The number of samples for this pass.
+
+ ``vo-passes/TYPE/N/samples/M``
+ The raw execution time of a specific sample for this pass, in
+ nanoseconds.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_MAP
+ "TYPE" MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP
+ "desc" MPV_FORMAT_STRING
+ "last" MPV_FORMAT_INT64
+ "avg" MPV_FORMAT_INT64
+ "peak" MPV_FORMAT_INT64
+ "count" MPV_FORMAT_INT64
+ "samples" MPV_FORMAT_NODE_ARRAY
+ MP_FORMAT_INT64
+
+ Note that directly accessing this structure via subkeys is not supported,
+ the only access is through aforementioned ``MPV_FORMAT_NODE``.
+
+``perf-info``
+ Further performance data. Querying this property triggers internal
+ collection of some data, and may slow down the player. Each query will reset
+ some internal state. Property change notification doesn't and won't work.
+ All of this may change in the future, so don't use this. The builtin
+ ``stats`` script is supposed to be the only user; since it's bundled and
+ built with the source code, it can use knowledge of mpv internal to render
+ the information properly. See ``stats`` script description for some details.
+
+``video-bitrate``, ``audio-bitrate``, ``sub-bitrate``
+ Bitrate values calculated on the packet level. This works by dividing the
+ bit size of all packets between two keyframes by their presentation
+ timestamp distance. (This uses the timestamps are stored in the file, so
+ e.g. playback speed does not influence the returned values.) In particular,
+ the video bitrate will update only per keyframe, and show the "past"
+ bitrate. To make the property more UI friendly, updates to these properties
+ are throttled in a certain way.
+
+ The unit is bits per second. OSD formatting turns these values in kilobits
+ (or megabits, if appropriate), which can be prevented by using the
+ raw property value, e.g. with ``${=video-bitrate}``.
+
+ Note that the accuracy of these properties is influenced by a few factors.
+ If the underlying demuxer rewrites the packets on demuxing (done for some
+ file formats), the bitrate might be slightly off. If timestamps are bad
+ or jittery (like in Matroska), even constant bitrate streams might show
+ fluctuating bitrate.
+
+ How exactly these values are calculated might change in the future.
+
+ In earlier versions of mpv, these properties returned a static (but bad)
+ guess using a completely different method.
+
+``packet-video-bitrate``, ``packet-audio-bitrate``, ``packet-sub-bitrate``
+ Old and deprecated properties for ``video-bitrate``, ``audio-bitrate``,
+ ``sub-bitrate``. They behave exactly the same, but return a value in
+ kilobits. Also, they don't have any OSD formatting, though the same can be
+ achieved with e.g. ``${=video-bitrate}``.
+
+ These properties shouldn't be used anymore.
+
+``audio-device-list``
+ The list of discovered audio devices. This is mostly for use with the
+ client API, and reflects what ``--audio-device=help`` with the command line
+ player returns.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each device entry)
+ "name" MPV_FORMAT_STRING
+ "description" MPV_FORMAT_STRING
+
+ The ``name`` is what is to be passed to the ``--audio-device`` option (and
+ often a rather cryptic audio API-specific ID), while ``description`` is
+ human readable free form text. The description is set to the device name
+ (minus mpv-specific ``<driver>/`` prefix) if no description is available
+ or the description would have been an empty string.
+
+ The special entry with the name set to ``auto`` selects the default audio
+ output driver and the default device.
+
+ The property can be watched with the property observation mechanism in
+ the client API and in Lua scripts. (Technically, change notification is
+ enabled the first time this property is read.)
+
+``audio-device`` (RW)
+ Set the audio device. This directly reads/writes the ``--audio-device``
+ option, but on write accesses, the audio output will be scheduled for
+ reloading.
+
+ Writing this property while no audio output is active will not automatically
+ enable audio. (This is also true in the case when audio was disabled due to
+ reinitialization failure after a previous write access to ``audio-device``.)
+
+ This property also doesn't tell you which audio device is actually in use.
+
+ How these details are handled may change in the future.
+
+``current-vo``
+ Current video output driver (name as used with ``--vo``).
+
+``current-ao``
+ Current audio output driver (name as used with ``--ao``).
+
+``shared-script-properties`` (RW)
+ This is a key/value map of arbitrary strings shared between scripts for
+ general use. The player itself does not use any data in it (although some
+ builtin scripts may). The property is not preserved across player restarts.
+
+ This is very primitive, inefficient, and annoying to use. It's a makeshift
+ solution which could go away any time (for example, when a better solution
+ becomes available). This is also why this property has an annoying name. You
+ should avoid using it, unless you absolutely have to.
+
+ Lua scripting has helpers starting with ``utils.shared_script_property_``.
+ They are undocumented because you should not use this property. If you still
+ think you must, you should use the helpers instead of the property directly.
+
+ You are supposed to use the ``change-list`` command to modify the contents.
+ Reading, modifying, and writing the property manually could data loss if two
+ scripts update different keys at the same time due to lack of
+ synchronization. The Lua helpers take care of this.
+
+ (There is no way to ensure synchronization if two scripts try to update the
+ same key at the same time.)
+
+``user-data`` (RW)
+ This is a recursive key/value map of arbitrary nodes shared between clients for
+ general use (i.e. scripts, IPC clients, host applications, etc).
+ The player itself does not use any data in it (although some builtin scripts may).
+ The property is not preserved across player restarts.
+
+ This is a more powerful replacement for ``shared-script-properties``.
+
+ Sub-paths can be accessed directly; e.g. ``user-data/my-script/state/a`` can be
+ read, written, or observed.
+
+ The top-level object itself cannot be written directly; write to sub-paths instead.
+
+ Converting this property or its sub-properties to strings will give a JSON
+ representation. If converting a leaf-level object (i.e. not a map or array)
+ and not using raw mode, the underlying content will be given (e.g. strings will be
+ printed directly, rather than quoted and JSON-escaped).
+
+``working-directory``
+ The working directory of the mpv process. Can be useful for JSON IPC users,
+ because the command line player usually works with relative paths.
+
+``protocol-list``
+ List of protocol prefixes potentially recognized by the player. They are
+ returned without trailing ``://`` suffix (which is still always required).
+ In some cases, the protocol will not actually be supported (consider
+ ``https`` if ffmpeg is not compiled with TLS support).
+
+``decoder-list``
+ List of decoders supported. This lists decoders which can be passed to
+ ``--vd`` and ``--ad``.
+
+ ``codec``
+ Canonical codec name, which identifies the format the decoder can
+ handle.
+
+ ``driver``
+ The name of the decoder itself. Often, this is the same as ``codec``.
+ Sometimes it can be different. It is used to distinguish multiple
+ decoders for the same codec.
+
+ ``description``
+ Human readable description of the decoder and codec.
+
+ When querying the property with the client API using ``MPV_FORMAT_NODE``,
+ or with Lua ``mp.get_property_native``, this will return a mpv_node with
+ the following contents:
+
+ ::
+
+ MPV_FORMAT_NODE_ARRAY
+ MPV_FORMAT_NODE_MAP (for each decoder entry)
+ "codec" MPV_FORMAT_STRING
+ "driver" MPV_FORMAT_STRING
+ "description" MPV_FORMAT_STRING
+
+``encoder-list``
+ List of libavcodec encoders. This has the same format as ``decoder-list``.
+ The encoder names (``driver`` entries) can be passed to ``--ovc`` and
+ ``--oac`` (without the ``lavc:`` prefix required by ``--vd`` and ``--ad``).
+
+``demuxer-lavf-list``
+ List of available libavformat demuxers' names. This can be used to check
+ for support for a specific format or use with ``--demuxer-lavf-format``.
+
+``input-key-list``
+ List of `Key names`_, same as output by ``--input-keylist``.
+
+``mpv-version``
+ The mpv version/copyright string. Depending on how the binary was built, it
+ might contain either a release version, or just a git hash.
+
+``mpv-configuration``
+ The configuration arguments that were passed to the build system. If the
+ meson version used to compile mpv is older than 1.1.0, then a hardcoded
+ string of a few, arbitrary options is displayed instead.
+
+``ffmpeg-version``
+ The contents of the ``av_version_info()`` API call. This is a string which
+ identifies the build in some way, either through a release version number,
+ or a git hash. This applies to Libav as well (the property is still named
+ the same.) This property is unavailable if mpv is linked against older
+ FFmpeg and Libav versions.
+
+``libass-version``
+ The value of ``ass_library_version()``. This is an integer, encoded in a
+ somewhat weird form (apparently "hex BCD"), indicating the release version
+ of the libass library linked to mpv.
+
+``platform``
+ Returns a string describing what target platform mpv was built for. The value
+ of this is dependent on what the underlying build system detects. Some of the
+ most common values are: ``windows``, ``darwin`` (macos or ios), ``linux``,
+ ``android``, and ``freebsd``. Note that this is not a complete listing.
+
+``options/<name>`` (RW)
+ The value of option ``--<name>``. Most options can be changed at runtime by
+ writing to this property. Note that many options require reloading the file
+ for changes to take effect. If there is an equivalent property, prefer
+ setting the property instead.
+
+ There shouldn't be any reason to access ``options/<name>`` instead of
+ ``<name>``, except in situations in which the properties have different
+ behavior or conflicting semantics.
+
+``file-local-options/<name>`` (RW)
+ Similar to ``options/<name>``, but when setting an option through this
+ property, the option is reset to its old value once the current file has
+ stopped playing. Trying to write an option while no file is playing (or
+ is being loaded) results in an error.
+
+ (Note that if an option is marked as file-local, even ``options/`` will
+ access the local value, and the ``old`` value, which will be restored on
+ end of playback, cannot be read or written until end of playback.)
+
+``option-info/<name>``
+ Additional per-option information.
+
+ This has a number of sub-properties. Replace ``<name>`` with the name of
+ a top-level option. No guarantee of stability is given to any of these
+ sub-properties - they may change radically in the feature.
+
+ ``option-info/<name>/name``
+ The name of the option.
+
+ ``option-info/<name>/type``
+ The name of the option type, like ``String`` or ``Integer``. For many
+ complex types, this isn't very accurate.
+
+ ``option-info/<name>/set-from-commandline``
+ Whether the option was set from the mpv command line. What this is set
+ to if the option is e.g. changed at runtime is left undefined (meaning
+ it could change in the future).
+
+ ``option-info/<name>/set-locally``
+ Whether the option was set per-file. This is the case with
+ automatically loaded profiles, file-dir configs, and other cases. It
+ means the option value will be restored to the value before playback
+ start when playback ends.
+
+ ``option-info/<name>/default-value``
+ The default value of the option. May not always be available.
+
+ ``option-info/<name>/min``, ``option-info/<name>/max``
+ Integer minimum and maximum values allowed for the option. Only
+ available if the options are numeric, and the minimum/maximum has been
+ set internally. It's also possible that only one of these is set.
+
+ ``option-info/<name>/choices``
+ If the option is a choice option, the possible choices. Choices that
+ are integers may or may not be included (they can be implied by ``min``
+ and ``max``). Note that options which behave like choice options, but
+ are not actual choice options internally, may not have this info
+ available.
+
+``property-list``
+ The list of top-level properties.
+
+``profile-list``
+ The list of profiles and their contents. This is highly
+ implementation-specific, and may change any time. Currently, it returns an
+ array of options for each profile. Each option has a name and a value, with
+ the value currently always being a string. Note that the options array is
+ not a map, as order matters and duplicate entries are possible. Recursive
+ profiles are not expanded, and show up as special ``profile`` options.
+
+ The ``profile-restore`` field is currently missing if it holds the default
+ value (either because it was not set, or set explicitly to ``default``),
+ but in the future it might hold the value ``default``.
+
+``command-list``
+ The list of input commands. This returns an array of maps, where each map
+ node represents a command. This map currently only has a single entry:
+ ``name`` for the name of the command. (This property is supposed to be a
+ replacement for ``--input-cmdlist``. The option dumps some more
+ information, but it's a valid feature request to extend this property if
+ needed.)
+
+``input-bindings``
+ The list of current input key bindings. This returns an array of maps,
+ where each map node represents a binding for a single key/command. This map
+ has the following entries:
+
+ ``key``
+ The key name. This is normalized and may look slightly different from
+ how it was specified in the source (e.g. in input.conf).
+
+ ``cmd``
+ The command mapped to the key. (Currently, this is exactly the same
+ string as specified in the source, other than stripping whitespace and
+ comments. It's possible that it will be normalized in the future.)
+
+ ``is_weak``
+ If set to true, any existing and active user bindings will take priority.
+
+ ``owner``
+ If this entry exists, the name of the script (or similar) which added
+ this binding.
+
+ ``section``
+ Name of the section this binding is part of. This is a rarely used
+ mechanism. This entry may be removed or change meaning in the future.
+
+ ``priority``
+ A number. Bindings with a higher value are preferred over bindings
+ with a lower value. If the value is negative, this binding is inactive
+ and will not be triggered by input. Note that mpv does not use this
+ value internally, and matching of bindings may work slightly differently
+ in some cases. In addition, this value is dynamic and can change around
+ at runtime.
+
+ ``comment``
+ If available, the comment following the command on the same line. (For
+ example, the input.conf entry ``f cycle bla # toggle bla`` would
+ result in an entry with ``comment = "toggle bla", cmd = "cycle bla"``.)
+
+ This property is read-only, and change notification is not supported.
+ Currently, there is no mechanism to change key bindings at runtime, other
+ than scripts adding or removing their own bindings.
+
+Inconsistencies between options and properties
+----------------------------------------------
+
+You can access (almost) all options as properties, though there are some
+caveats with some properties (due to historical reasons):
+
+``vid``, ``aid``, ``sid``
+ While playback is active, these return the actually active tracks. For
+ example, if you set ``aid=5``, and the currently played file contains no
+ audio track with ID 5, the ``aid`` property will return ``no``.
+
+ Before mpv 0.31.0, you could set existing tracks at runtime only.
+
+``display-fps``
+ This inconsistent behavior is deprecated. Post-deprecation, the reported
+ value and the option value are cleanly separated (``override-display-fps``
+ for the option value).
+
+``vf``, ``af``
+ If you set the properties during playback, and the filter chain fails to
+ reinitialize, the option will be set, but the runtime filter chain does not
+ change. On the other hand, the next video to be played will fail, because
+ the initial filter chain cannot be created.
+
+ This behavior changed in mpv 0.31.0. Before this, the new value was rejected
+ *iff* a video (for ``vf``) or an audio (for ``af``) track was active. If
+ playback was not active, the behavior was the same as the current one.
+
+``playlist``
+ The property is read-only and returns the current internal playlist. The
+ option is for loading playlist during command line parsing. For client API
+ uses, you should use the ``loadlist`` command instead.
+
+``profile``, ``include``
+ These are write-only, and will perform actions as they are written to,
+ exactly as if they were used on the mpv CLI commandline. Their only use is
+ when using libmpv before ``mpv_initialize()``, which in turn is probably
+ only useful in encoding mode. Normal libmpv users should use other
+ mechanisms, such as the ``apply-profile`` command, and the
+ ``mpv_load_config_file`` API function. Avoid these properties.
+
+Property Expansion
+------------------
+
+All string arguments to input commands as well as certain options (like
+``--term-playing-msg``) are subject to property expansion. Note that property
+expansion does not work in places where e.g. numeric parameters are expected.
+(For example, the ``add`` command does not do property expansion. The ``set``
+command is an exception and not a general rule.)
+
+.. admonition:: Example for input.conf
+
+ ``i show-text "Filename: ${filename}"``
+ shows the filename of the current file when pressing the ``i`` key
+
+Whether property expansion is enabled by default depends on which API is used
+(see `Flat command syntax`_, `Commands specified as arrays`_ and `Named
+arguments`_), but it can always be enabled with the ``expand-properties``
+prefix or disabled with the ``raw`` prefix, as described in `Input Command
+Prefixes`_.
+
+The following expansions are supported:
+
+``${NAME}``
+ Expands to the value of the property ``NAME``. If retrieving the property
+ fails, expand to an error string. (Use ``${NAME:}`` with a trailing
+ ``:`` to expand to an empty string instead.)
+ If ``NAME`` is prefixed with ``=``, expand to the raw value of the property
+ (see section below).
+``${NAME:STR}``
+ Expands to the value of the property ``NAME``, or ``STR`` if the
+ property cannot be retrieved. ``STR`` is expanded recursively.
+``${?NAME:STR}``
+ Expands to ``STR`` (recursively) if the property ``NAME`` is available.
+``${!NAME:STR}``
+ Expands to ``STR`` (recursively) if the property ``NAME`` cannot be
+ retrieved.
+``${?NAME==VALUE:STR}``
+ Expands to ``STR`` (recursively) if the property ``NAME`` expands to a
+ string equal to ``VALUE``. You can prefix ``NAME`` with ``=`` in order to
+ compare the raw value of a property (see section below). If the property
+ is unavailable, or other errors happen when retrieving it, the value is
+ never considered equal.
+ Note that ``VALUE`` can't contain any of the characters ``:`` or ``}``.
+ Also, it is possible that escaping with ``"`` or ``%`` might be added in
+ the future, should the need arise.
+``${!NAME==VALUE:STR}``
+ Same as with the ``?`` variant, but ``STR`` is expanded if the value is
+ not equal. (Using the same semantics as with ``?``.)
+``$$``
+ Expands to ``$``.
+``$}``
+ Expands to ``}``. (To produce this character inside recursive
+ expansion.)
+``$>``
+ Disable property expansion and special handling of ``$`` for the rest
+ of the string.
+
+In places where property expansion is allowed, C-style escapes are often
+accepted as well. Example:
+
+ - ``\n`` becomes a newline character
+ - ``\\`` expands to ``\``
+
+Raw and Formatted Properties
+----------------------------
+
+Normally, properties are formatted as human-readable text, meant to be
+displayed on OSD or on the terminal. It is possible to retrieve an unformatted
+(raw) value from a property by prefixing its name with ``=``. These raw values
+can be parsed by other programs and follow the same conventions as the options
+associated with the properties.
+
+.. admonition:: Examples
+
+ - ``${time-pos}`` expands to ``00:14:23`` (if playback position is at 14
+ minutes 23 seconds)
+ - ``${=time-pos}`` expands to ``863.4`` (same time, plus 400 milliseconds -
+ milliseconds are normally not shown in the formatted case)
+
+Sometimes, the difference in amount of information carried by raw and formatted
+property values can be rather big. In some cases, raw values have more
+information, like higher precision than seconds with ``time-pos``. Sometimes
+it is the other way around, e.g. ``aid`` shows track title and language in the
+formatted case, but only the track number if it is raw.
diff --git a/DOCS/man/ipc.rst b/DOCS/man/ipc.rst
new file mode 100644
index 0000000..fbb0b01
--- /dev/null
+++ b/DOCS/man/ipc.rst
@@ -0,0 +1,387 @@
+JSON IPC
+========
+
+mpv can be controlled by external programs using the JSON-based IPC protocol.
+It can be enabled by specifying the path to a unix socket or a named pipe using
+the option ``--input-ipc-server``. Clients can connect to this socket and send
+commands to the player or receive events from it.
+
+.. warning::
+
+ This is not intended to be a secure network protocol. It is explicitly
+ insecure: there is no authentication, no encryption, and the commands
+ themselves are insecure too. For example, the ``run`` command is exposed,
+ which can run arbitrary system commands. The use-case is controlling the
+ player locally. This is not different from the MPlayer slave protocol.
+
+Socat example
+-------------
+
+You can use the ``socat`` tool to send commands (and receive replies) from the
+shell. Assuming mpv was started with:
+
+::
+
+ mpv file.mkv --input-ipc-server=/tmp/mpvsocket
+
+Then you can control it using socat:
+
+::
+
+ > echo '{ "command": ["get_property", "playback-time"] }' | socat - /tmp/mpvsocket
+ {"data":190.482000,"error":"success"}
+
+In this case, socat copies data between stdin/stdout and the mpv socket
+connection.
+
+See the ``--idle`` option how to make mpv start without exiting immediately or
+playing a file.
+
+It's also possible to send input.conf style text-only commands:
+
+::
+
+ > echo 'show-text ${playback-time}' | socat - /tmp/mpvsocket
+
+But you won't get a reply over the socket. (This particular command shows the
+playback time on the player's OSD.)
+
+Command Prompt example
+----------------------
+
+Unfortunately, it's not as easy to test the IPC protocol on Windows, since
+Windows ports of socat (in Cygwin and MSYS2) don't understand named pipes. In
+the absence of a simple tool to send and receive from bidirectional pipes, the
+``echo`` command can be used to send commands, but not receive replies from the
+command prompt.
+
+Assuming mpv was started with:
+
+::
+
+ mpv file.mkv --input-ipc-server=\\.\pipe\mpvsocket
+
+You can send commands from a command prompt:
+
+::
+
+ echo show-text ${playback-time} >\\.\pipe\mpvsocket
+
+To be able to simultaneously read and write from the IPC pipe, like on Linux,
+it's necessary to write an external program that uses overlapped file I/O (or
+some wrapper like .NET's NamedPipeClientStream.)
+
+You can open the pipe in PuTTY as "serial" device. This is not very
+comfortable, but gives a way to test interactively without having to write code.
+
+Protocol
+--------
+
+The protocol uses UTF-8-only JSON as defined by RFC-8259. Unlike standard JSON,
+"\u" escape sequences are not allowed to construct surrogate pairs. To avoid
+getting conflicts, encode all text characters including and above codepoint
+U+0020 as UTF-8. mpv might output broken UTF-8 in corner cases (see "UTF-8"
+section below).
+
+Clients can execute commands on the player by sending JSON messages of the
+following form:
+
+::
+
+ { "command": ["command_name", "param1", "param2", ...] }
+
+where ``command_name`` is the name of the command to be executed, followed by a
+list of parameters. Parameters must be formatted as native JSON values
+(integers, strings, booleans, ...). Every message **must** be terminated with
+``\n``. Additionally, ``\n`` must not appear anywhere inside the message. In
+practice this means that messages should be minified before being sent to mpv.
+
+mpv will then send back a reply indicating whether the command was run
+correctly, and an additional field holding the command-specific return data (it
+can also be null).
+
+::
+
+ { "error": "success", "data": null }
+
+mpv will also send events to clients with JSON messages of the following form:
+
+::
+
+ { "event": "event_name" }
+
+where ``event_name`` is the name of the event. Additional event-specific fields
+can also be present. See `List of events`_ for a list of all supported events.
+
+Because events can occur at any time, it may be difficult at times to determine
+which response goes with which command. Commands may optionally include a
+``request_id`` which, if provided in the command request, will be copied
+verbatim into the response. mpv does not interpret the ``request_id`` in any
+way; it is solely for the use of the requester. The only requirement is that
+the ``request_id`` field must be an integer (a number without fractional parts
+in the range ``-2^63..2^63-1``). Using other types is deprecated and will
+currently show a warning. In the future, this will raise an error.
+
+For example, this request:
+
+::
+
+ { "command": ["get_property", "time-pos"], "request_id": 100 }
+
+Would generate this response:
+
+::
+
+ { "error": "success", "data": 1.468135, "request_id": 100 }
+
+If you don't specify a ``request_id``, command replies will set it to 0.
+
+All commands, replies, and events are separated from each other with a line
+break character (``\n``).
+
+If the first character (after skipping whitespace) is not ``{``, the command
+will be interpreted as non-JSON text command, as they are used in input.conf
+(or ``mpv_command_string()`` in the client API). Additionally, lines starting
+with ``#`` and empty lines are ignored.
+
+Currently, embedded 0 bytes terminate the current line, but you should not
+rely on this.
+
+Data flow
+---------
+
+Currently, the mpv-side IPC implementation does not service the socket while a
+command is executed and the reply is written. It is for example not possible
+that other events, that happened during the execution of the command, are
+written to the socket before the reply is written.
+
+This might change in the future. The only guarantee is that replies to IPC
+messages are sent in sequence.
+
+Also, since socket I/O is inherently asynchronous, it is possible that you read
+unrelated event messages from the socket, before you read the reply to the
+previous command you sent. In this case, these events were queued by the mpv
+side before it read and started processing your command message.
+
+If the mpv-side IPC implementation switches away from blocking writes and
+blocking command execution, it may attempt to send events at any time.
+
+You can also use asynchronous commands, which can return in any order, and
+which do not block IPC protocol interaction at all while the command is
+executed in the background.
+
+Asynchronous commands
+---------------------
+
+Command can be run asynchronously. This behaves exactly as with normal command
+execution, except that execution is not blocking. Other commands can be sent
+while it's executing, and command completion can be arbitrarily reordered.
+
+The ``async`` field controls this. If present, it must be a boolean. If missing,
+``false`` is assumed.
+
+For example, this initiates an asynchronous command:
+
+::
+
+ { "command": ["screenshot"], "request_id": 123, "async": true }
+
+And this is the completion:
+
+::
+
+ {"request_id":123,"error":"success","data":null}
+
+By design, you will not get a confirmation that the command was started. If a
+command is long running, sending the message will not lead to any reply until
+much later when the command finishes.
+
+Some commands execute synchronously, but these will behave like asynchronous
+commands that finished execution immediately.
+
+Cancellation of asynchronous commands is available in the libmpv API, but has
+not yet been implemented in the IPC protocol.
+
+Commands with named arguments
+-----------------------------
+
+If the ``command`` field is a JSON object, named arguments are expected. This
+is described in the C API ``mpv_command_node()`` documentation (the
+``MPV_FORMAT_NODE_MAP`` case). In some cases, this may make commands more
+readable, while some obscure commands basically require using named arguments.
+
+Currently, only "proper" commands (as listed by `List of Input Commands`_)
+support named arguments.
+
+Commands
+--------
+
+In addition to the commands described in `List of Input Commands`_, a few
+extra commands can also be used as part of the protocol:
+
+``client_name``
+ Return the name of the client as string. This is the string ``ipc-N`` with
+ N being an integer number.
+
+``get_time_us``
+ Return the current mpv internal time in microseconds as a number. This is
+ basically the system time, with an arbitrary offset.
+
+``get_property``
+ Return the value of the given property. The value will be sent in the data
+ field of the replay message.
+
+ Example:
+
+ ::
+
+ { "command": ["get_property", "volume"] }
+ { "data": 50.0, "error": "success" }
+
+``get_property_string``
+ Like ``get_property``, but the resulting data will always be a string.
+
+ Example:
+
+ ::
+
+ { "command": ["get_property_string", "volume"] }
+ { "data": "50.000000", "error": "success" }
+
+``set_property``
+ Set the given property to the given value. See `Properties`_ for more
+ information about properties.
+
+ Example:
+
+ ::
+
+ { "command": ["set_property", "pause", true] }
+ { "error": "success" }
+
+``set_property_string``
+ Alias for ``set_property``. Both commands accept native values and strings.
+
+``observe_property``
+ Watch a property for changes. If the given property is changed, then an
+ event of type ``property-change`` will be generated
+
+ Example:
+
+ ::
+
+ { "command": ["observe_property", 1, "volume"] }
+ { "error": "success" }
+ { "event": "property-change", "id": 1, "data": 52.0, "name": "volume" }
+
+ .. warning::
+
+ If the connection is closed, the IPC client is destroyed internally,
+ and the observed properties are unregistered. This happens for example
+ when sending commands to a socket with separate ``socat`` invocations.
+ This can make it seem like property observation does not work. You must
+ keep the IPC connection open to make it work.
+
+``observe_property_string``
+ Like ``observe_property``, but the resulting data will always be a string.
+
+ Example:
+
+ ::
+
+ { "command": ["observe_property_string", 1, "volume"] }
+ { "error": "success" }
+ { "event": "property-change", "id": 1, "data": "52.000000", "name": "volume" }
+
+``unobserve_property``
+ Undo ``observe_property`` or ``observe_property_string``. This requires the
+ numeric id passed to the observed command as argument.
+
+ Example:
+
+ ::
+
+ { "command": ["unobserve_property", 1] }
+ { "error": "success" }
+
+``request_log_messages``
+ Enable output of mpv log messages. They will be received as events. The
+ parameter to this command is the log-level (see ``mpv_request_log_messages``
+ C API function).
+
+ Log message output is meant for humans only (mostly for debugging).
+ Attempting to retrieve information by parsing these messages will just
+ lead to breakages with future mpv releases. Instead, make a feature request,
+ and ask for a proper event that returns the information you need.
+
+``enable_event``, ``disable_event``
+ Enables or disables the named event. Mirrors the ``mpv_request_event`` C
+ API function. If the string ``all`` is used instead of an event name, all
+ events are enabled or disabled.
+
+ By default, most events are enabled, and there is not much use for this
+ command.
+
+``get_version``
+ Returns the client API version the C API of the remote mpv instance
+ provides.
+
+ See also: ``DOCS/client-api-changes.rst``.
+
+UTF-8
+-----
+
+Normally, all strings are in UTF-8. Sometimes it can happen that strings are
+in some broken encoding (often happens with file tags and such, and filenames
+on many Unixes are not required to be in UTF-8 either). This means that mpv
+sometimes sends invalid JSON. If that is a problem for the client application's
+parser, it should filter the raw data for invalid UTF-8 sequences and perform
+the desired replacement, before feeding the data to its JSON parser.
+
+mpv will not attempt to construct invalid UTF-8 with broken "\u" escape
+sequences. This includes surrogate pairs.
+
+JSON extensions
+---------------
+
+The following non-standard extensions are supported:
+
+ - a list or object item can have a trailing ","
+ - object syntax accepts "=" in addition of ":"
+ - object keys can be unquoted, if they start with a character in "A-Za-z\_"
+ and contain only characters in "A-Za-z0-9\_"
+ - byte escapes with "\xAB" are allowed (with AB being a 2 digit hex number)
+
+Example:
+
+::
+
+ { objkey = "value\x0A" }
+
+Is equivalent to:
+
+::
+
+ { "objkey": "value\n" }
+
+Alternative ways of starting clients
+------------------------------------
+
+You can create an anonymous IPC connection without having to set
+``--input-ipc-server``. This is achieved through a mpv pseudo scripting backend
+that starts processes.
+
+You can put ``.run`` file extension in the mpv scripts directory in its config
+directory (see the `FILES`_ section for details), or load them through other
+means (see `Script location`_). These scripts are simply executed with the OS
+native mechanism (as if you ran them in the shell). They must have a proper
+shebang and have the executable bit set.
+
+When executed, a socket (the IPC connection) is passed to them through file
+descriptor inheritance. The file descriptor is indicated as the special command
+line argument ``--mpv-ipc-fd=N``, where ``N`` is the numeric file descriptor.
+
+The rest is the same as with a normal ``--input-ipc-server`` IPC connection. mpv
+does not attempt to observe or other interact with the started script process.
+
+This does not work in Windows yet.
diff --git a/DOCS/man/javascript.rst b/DOCS/man/javascript.rst
new file mode 100644
index 0000000..bdbb04b
--- /dev/null
+++ b/DOCS/man/javascript.rst
@@ -0,0 +1,398 @@
+JAVASCRIPT
+==========
+
+JavaScript support in mpv is near identical to its Lua support. Use this section
+as reference on differences and availability of APIs, but otherwise you should
+refer to the Lua documentation for API details and general scripting in mpv.
+
+Example
+-------
+
+JavaScript code which leaves fullscreen mode when the player is paused:
+
+::
+
+ function on_pause_change(name, value) {
+ if (value == true)
+ mp.set_property("fullscreen", "no");
+ }
+ mp.observe_property("pause", "bool", on_pause_change);
+
+
+Similarities with Lua
+---------------------
+
+mpv tries to load a script file as JavaScript if it has a ``.js`` extension, but
+otherwise, the documented Lua options, script directories, loading, etc apply to
+JavaScript files too.
+
+Script initialization and lifecycle is the same as with Lua, and most of the Lua
+functions at the modules ``mp``, ``mp.utils``, ``mp.msg`` and ``mp.options`` are
+available to JavaScript with identical APIs - including running commands,
+getting/setting properties, registering events/key-bindings/hooks, etc.
+
+Differences from Lua
+--------------------
+
+No need to load modules. ``mp``, ``mp.utils``, ``mp.msg`` and ``mp.options``
+are preloaded, and you can use e.g. ``var cwd = mp.utils.getcwd();`` without
+prior setup.
+
+Errors are slightly different. Where the Lua APIs return ``nil`` for error,
+the JavaScript ones return ``undefined``. Where Lua returns ``something, error``
+JavaScript returns only ``something`` - and makes ``error`` available via
+``mp.last_error()``. Note that only some of the functions have this additional
+``error`` value - typically the same ones which have it in Lua.
+
+Standard APIs are preferred. For instance ``setTimeout`` and ``JSON.stringify``
+are available, but ``mp.add_timeout`` and ``mp.utils.format_json`` are not.
+
+No standard library. This means that interaction with anything outside of mpv is
+limited to the available APIs, typically via ``mp.utils``. However, some file
+functions were added, and CommonJS ``require`` is available too - where the
+loaded modules have the same privileges as normal scripts.
+
+Language features - ECMAScript 5
+--------------------------------
+
+The scripting backend which mpv currently uses is MuJS - a compatible minimal
+ES5 interpreter. As such, ``String.substring`` is implemented for instance,
+while the common but non-standard ``String.substr`` is not. Please consult the
+MuJS pages on language features and platform support - https://mujs.com .
+
+Unsupported Lua APIs and their JS alternatives
+----------------------------------------------
+
+``mp.add_timeout(seconds, fn)`` JS: ``id = setTimeout(fn, ms)``
+
+``mp.add_periodic_timer(seconds, fn)`` JS: ``id = setInterval(fn, ms)``
+
+``utils.parse_json(str [, trail])`` JS: ``JSON.parse(str)``
+
+``utils.format_json(v)`` JS: ``JSON.stringify(v)``
+
+``utils.to_string(v)`` see ``dump`` below.
+
+``mp.get_next_timeout()`` see event loop below.
+
+``mp.dispatch_events([allow_wait])`` see event loop below.
+
+Scripting APIs - identical to Lua
+---------------------------------
+
+(LE) - Last-Error, indicates that ``mp.last_error()`` can be used after the
+call to test for success (empty string) or failure (non empty reason string).
+Where the Lua APIs use ``nil`` to indicate error, JS APIs use ``undefined``.
+
+``mp.command(string)`` (LE)
+
+``mp.commandv(arg1, arg2, ...)`` (LE)
+
+``mp.command_native(table [,def])`` (LE)
+
+``id = mp.command_native_async(table [,fn])`` (LE) Notes: ``id`` is true-thy on
+success, ``error`` is empty string on success.
+
+``mp.abort_async_command(id)``
+
+``mp.del_property(name)`` (LE)
+
+``mp.get_property(name [,def])`` (LE)
+
+``mp.get_property_osd(name [,def])`` (LE)
+
+``mp.get_property_bool(name [,def])`` (LE)
+
+``mp.get_property_number(name [,def])`` (LE)
+
+``mp.get_property_native(name [,def])`` (LE)
+
+``mp.set_property(name, value)`` (LE)
+
+``mp.set_property_bool(name, value)`` (LE)
+
+``mp.set_property_number(name, value)`` (LE)
+
+``mp.set_property_native(name, value)`` (LE)
+
+``mp.get_time()``
+
+``mp.add_key_binding(key, name|fn [,fn [,flags]])``
+
+``mp.add_forced_key_binding(...)``
+
+``mp.remove_key_binding(name)``
+
+``mp.register_event(name, fn)``
+
+``mp.unregister_event(fn)``
+
+``mp.observe_property(name, type, fn)``
+
+``mp.unobserve_property(fn)``
+
+``mp.get_opt(key)``
+
+``mp.get_script_name()``
+
+``mp.get_script_directory()``
+
+``mp.osd_message(text [,duration])``
+
+``mp.get_wakeup_pipe()``
+
+``mp.register_idle(fn)``
+
+``mp.unregister_idle(fn)``
+
+``mp.enable_messages(level)``
+
+``mp.register_script_message(name, fn)``
+
+``mp.unregister_script_message(name)``
+
+``mp.create_osd_overlay(format)``
+
+``mp.get_osd_size()`` (returned object has properties: width, height, aspect)
+
+``mp.msg.log(level, ...)``
+
+``mp.msg.fatal(...)``
+
+``mp.msg.error(...)``
+
+``mp.msg.warn(...)``
+
+``mp.msg.info(...)``
+
+``mp.msg.verbose(...)``
+
+``mp.msg.debug(...)``
+
+``mp.msg.trace(...)``
+
+``mp.utils.getcwd()`` (LE)
+
+``mp.utils.readdir(path [, filter])`` (LE)
+
+``mp.utils.file_info(path)`` (LE) Note: like lua - this does NOT expand
+meta-paths like ``~~/foo`` (other JS file functions do expand meta paths).
+
+``mp.utils.split_path(path)``
+
+``mp.utils.join_path(p1, p2)``
+
+``mp.utils.subprocess(t)``
+
+``mp.utils.subprocess_detached(t)``
+
+``mp.utils.get_env_list()``
+
+``mp.utils.getpid()`` (LE)
+
+``mp.add_hook(type, priority, fn(hook))``
+
+``mp.options.read_options(obj [, identifier [, on_update]])`` (types:
+string/boolean/number)
+
+Additional utilities
+--------------------
+
+``mp.last_error()``
+ If used after an API call which updates last error, returns an empty string
+ if the API call succeeded, or a non-empty error reason string otherwise.
+
+``Error.stack`` (string)
+ When using ``try { ... } catch(e) { ... }``, then ``e.stack`` is the stack
+ trace of the error - if it was created using the ``Error(...)`` constructor.
+
+``print`` (global)
+ A convenient alias to ``mp.msg.info``.
+
+``dump`` (global)
+ Like ``print`` but also expands objects and arrays recursively.
+
+``mp.utils.getenv(name)``
+ Returns the value of the host environment variable ``name``, or
+ ``undefined`` if the variable is not defined.
+
+``mp.utils.get_user_path(path)``
+ Trivial wrapper of the ``expand-path`` mpv command, returns a string.
+ ``read_file``, ``write_file``, ``append_file`` and ``require`` already
+ expand the path internally and accept mpv meta-paths like ``~~desktop/foo``.
+
+``mp.utils.read_file(fname [,max])``
+ Returns the content of file ``fname`` as string. If ``max`` is provided and
+ not negative, limit the read to ``max`` bytes.
+
+``mp.utils.write_file(fname, str)``
+ (Over)write file ``fname`` with text content ``str``. ``fname`` must be
+ prefixed with ``file://`` as simple protection against accidental arguments
+ switch, e.g. ``mp.utils.write_file("file://~/abc.txt", "hello world")``.
+
+``mp.utils.append_file(fname, str)``
+ Same as ``mp.utils.write_file`` if the file ``fname`` does not exist. If it
+ does exist then append instead of overwrite.
+
+Note: ``read_file``, ``write_file`` and ``append_file`` throw on errors, allow
+text content only.
+
+``mp.get_time_ms()``
+ Same as ``mp.get_time()`` but in ms instead of seconds.
+
+``mp.get_script_file()``
+ Returns the file name of the current script.
+
+``exit()`` (global)
+ Make the script exit at the end of the current event loop iteration.
+ Note: please remove added key bindings before calling ``exit()``.
+
+``mp.utils.compile_js(fname, content_str)``
+ Compiles the JS code ``content_str`` as file name ``fname`` (without loading
+ anything from the filesystem), and returns it as a function. Very similar
+ to a ``Function`` constructor, but shows at stack traces as ``fname``.
+
+``mp.module_paths``
+ Global modules search paths array for the ``require`` function (see below).
+
+Timers (global)
+---------------
+
+The standard HTML/node.js timers are available:
+
+``id = setTimeout(fn [,duration [,arg1 [,arg2...]]])``
+
+``id = setTimeout(code_string [,duration])``
+
+``clearTimeout(id)``
+
+``id = setInterval(fn [,duration [,arg1 [,arg2...]]])``
+
+``id = setInterval(code_string [,duration])``
+
+``clearInterval(id)``
+
+``setTimeout`` and ``setInterval`` return id, and later call ``fn`` (or execute
+``code_string``) after ``duration`` ms. Interval also repeat every ``duration``.
+
+``duration`` has a minimum and default value of 0, ``code_string`` is
+a plain string which is evaluated as JS code, and ``[,arg1 [,arg2..]]`` are used
+as arguments (if provided) when calling back ``fn``.
+
+The ``clear...(id)`` functions cancel timer ``id``, and are irreversible.
+
+Note: timers always call back asynchronously, e.g. ``setTimeout(fn)`` will never
+call ``fn`` before returning. ``fn`` will be called either at the end of this
+event loop iteration or at a later event loop iteration. This is true also for
+intervals - which also never call back twice at the same event loop iteration.
+
+Additionally, timers are processed after the event queue is empty, so it's valid
+to use ``setTimeout(fn)`` as a one-time idle observer.
+
+CommonJS modules and ``require(id)``
+------------------------------------
+
+CommonJS Modules are a standard system where scripts can export common functions
+for use by other scripts. Specifically, a module is a script which adds
+properties (functions, etc) to its pre-existing ``exports`` object, which
+another script can access with ``require(module-id)``. This runs the module and
+returns its ``exports`` object. Further calls to ``require`` for the same module
+will return its cached ``exports`` object without running the module again.
+
+Modules and ``require`` are supported, standard compliant, and generally similar
+to node.js. However, most node.js modules won't run due to missing modules such
+as ``fs``, ``process``, etc, but some node.js modules with minimal dependencies
+do work. In general, this is for mpv modules and not a node.js replacement.
+
+A ``.js`` file extension is always added to ``id``, e.g. ``require("./foo")``
+will load the file ``./foo.js`` and return its ``exports`` object.
+
+An id which starts with ``./`` or ``../`` is relative to the script or module
+which ``require`` it. Otherwise it's considered a top-level id (CommonJS term).
+
+Top-level id is evaluated as absolute filesystem path if possible, e.g. ``/x/y``
+or ``~/x``. Otherwise it's considered a global module id and searched according
+to ``mp.module_paths`` in normal array order, e.g. ``require("x")`` tries to
+load ``x.js`` at one of the array paths, and id ``foo/x`` tries to load ``x.js``
+inside dir ``foo`` at one of the paths.
+
+The ``mp.module_paths`` array is empty by default except for scripts which are
+loaded as a directory where it contains one item - ``<directory>/modules/`` .
+The array may be updated from a script (or using custom init - see below) which
+will affect future calls to ``require`` for global module id's which are not
+already loaded/cached.
+
+No ``global`` variable, but a module's ``this`` at its top lexical scope is the
+global object - also in strict mode. If you have a module which needs ``global``
+as the global object, you could do ``this.global = this;`` before ``require``.
+
+Functions and variables declared at a module don't pollute the global object.
+
+Custom initialization
+---------------------
+
+After mpv initializes the JavaScript environment for a script but before it
+loads the script - it tries to run the file ``init.js`` at the root of the mpv
+configuration directory. Code at this file can update the environment further
+for all scripts. E.g. if it contains ``mp.module_paths.push("/foo")`` then
+``require`` at all scripts will search global module id's also at ``/foo``
+(do NOT do ``mp.module_paths = ["/foo"];`` because this will remove existing
+paths - like ``<script-dir>/modules`` for scripts which load from a directory).
+
+The custom-init file is ignored if mpv is invoked with ``--no-config``.
+
+Before mpv 0.34, the file name was ``.init.js`` (with dot) at the same dir.
+
+The event loop
+--------------
+
+The event loop poll/dispatch mpv events as long as the queue is not empty, then
+processes the timers, then waits for the next event, and repeats this forever.
+
+You could put this code at your script to replace the built-in event loop, and
+also print every event which mpv sends to your script:
+
+::
+
+ function mp_event_loop() {
+ var wait = 0;
+ do {
+ var e = mp.wait_event(wait);
+ dump(e); // there could be a lot of prints...
+ if (e.event != "none") {
+ mp.dispatch_event(e);
+ wait = 0;
+ } else {
+ wait = mp.process_timers() / 1000;
+ if (wait != 0) {
+ mp.notify_idle_observers();
+ wait = mp.peek_timers_wait() / 1000;
+ }
+ }
+ } while (mp.keep_running);
+ }
+
+
+``mp_event_loop`` is a name which mpv tries to call after the script loads.
+The internal implementation is similar to this (without ``dump`` though..).
+
+``e = mp.wait_event(wait)`` returns when the next mpv event arrives, or after
+``wait`` seconds if positive and no mpv events arrived. ``wait`` value of 0
+returns immediately (with ``e.event == "none"`` if the queue is empty).
+
+``mp.dispatch_event(e)`` calls back the handlers registered for ``e.event``,
+if there are such (event handlers, property observers, script messages, etc).
+
+``mp.process_timers()`` calls back the already-added, non-canceled due timers,
+and returns the duration in ms till the next due timer (possibly 0), or -1 if
+there are no pending timers. Must not be called recursively.
+
+``mp.notify_idle_observers()`` calls back the idle observers, which we do when
+we're about to sleep (wait != 0), but the observers may add timers or take
+non-negligible duration to complete, so we re-calculate ``wait`` afterwards.
+
+``mp.peek_timers_wait()`` returns the same values as ``mp.process_timers()``
+but without doing anything. Invalid result if called from a timer callback.
+
+Note: ``exit()`` is also registered for the ``shutdown`` event, and its
+implementation is a simple ``mp.keep_running = false``.
diff --git a/DOCS/man/libmpv.rst b/DOCS/man/libmpv.rst
new file mode 100644
index 0000000..e00f8b9
--- /dev/null
+++ b/DOCS/man/libmpv.rst
@@ -0,0 +1,79 @@
+EMBEDDING INTO OTHER PROGRAMS (LIBMPV)
+======================================
+
+mpv can be embedded into other programs as video/audio playback backend. The
+recommended way to do so is using libmpv. See ``libmpv/client.h`` in the mpv
+source code repository. This provides a C API. Bindings for other languages
+might be available (see wiki).
+
+Since libmpv merely allows access to underlying mechanisms that can control
+mpv, further documentation is spread over a few places:
+
+- https://github.com/mpv-player/mpv/blob/master/libmpv/client.h
+- https://mpv.io/manual/master/#options
+- https://mpv.io/manual/master/#list-of-input-commands
+- https://mpv.io/manual/master/#properties
+- https://github.com/mpv-player/mpv-examples/tree/master/libmpv
+
+C PLUGINS
+=========
+
+You can write C plugins for mpv. These use the libmpv API, although they do not
+use the libmpv library itself.
+
+They are enabled by default if compiler supports linking with the ``-rdynamic``
+flag on Linux/BSD platforms. On Windows the are always enabled.
+
+C plugins location
+------------------
+
+C plugins are put into the mpv scripts directory in its config directory
+(see the `FILES`_ section for details). They must have a ``.so`` or ``.dll``
+file extension. They can also be explicitly loaded with the ``--script`` option.
+
+API
+---
+
+A C plugin must export the following function::
+
+ int mpv_open_cplugin(mpv_handle *handle)
+
+The plugin function will be called on loading time. This function does not
+return as long as your plugin is loaded (it runs in its own thread). The
+``handle`` will be deallocated as soon as the plugin function returns.
+
+The return value is interpreted as error status. A value of ``0`` is
+interpreted as success, while ``-1`` signals an error. In the latter case,
+the player prints an uninformative error message that loading failed.
+
+Return values other than ``0`` and ``-1`` are reserved, and trigger undefined
+behavior.
+
+Within the plugin function, you can call libmpv API functions. The ``handle``
+is created by ``mpv_create_client()`` (or actually an internal equivalent),
+and belongs to you. You can call ``mpv_wait_event()`` to wait for things
+happening, and so on.
+
+Note that the player might block until your plugin calls ``mpv_wait_event()``
+for the first time. This gives you a chance to install initial hooks etc.
+before playback begins.
+
+The details are quite similar to Lua scripts.
+
+Linkage to libmpv
+-----------------
+
+The current implementation requires that your plugins are **not** linked against
+libmpv. What your plugins use are not symbols from a libmpv binary, but
+symbols from the mpv host binary.
+
+On Windows to make symbols from the host binary available, you have to define
+MPV_CPLUGIN_DYNAMIC_SYM when compiling cplugin. This will load symbols
+dynamically, before calling ``mpv_open_cplugin()``.
+
+Examples
+--------
+
+See:
+
+- https://github.com/mpv-player/mpv-examples/tree/master/cplugins
diff --git a/DOCS/man/lua.rst b/DOCS/man/lua.rst
new file mode 100644
index 0000000..5708e19
--- /dev/null
+++ b/DOCS/man/lua.rst
@@ -0,0 +1,917 @@
+LUA SCRIPTING
+=============
+
+mpv can load Lua scripts. (See `Script location`_.)
+
+mpv provides the built-in module ``mp``, which contains functions to send
+commands to the mpv core and to retrieve information about playback state, user
+settings, file information, and so on.
+
+These scripts can be used to control mpv in a similar way to slave mode.
+Technically, the Lua code uses the client API internally.
+
+Example
+-------
+
+A script which leaves fullscreen mode when the player is paused:
+
+::
+
+ function on_pause_change(name, value)
+ if value == true then
+ mp.set_property("fullscreen", "no")
+ end
+ end
+ mp.observe_property("pause", "bool", on_pause_change)
+
+
+Script location
+---------------
+
+Scripts can be passed to the ``--script`` option, and are automatically loaded
+from the ``scripts`` subdirectory of the mpv configuration directory (usually
+``~/.config/mpv/scripts/``).
+
+A script can be a single file. The file extension is used to select the
+scripting backend to use for it. For Lua, it is ``.lua``. If the extension is
+not recognized, an error is printed. (If an error happens, the extension is
+either mistyped, or the backend was not compiled into your mpv binary.)
+
+mpv internally loads the script's name by stripping the ``.lua`` extension and
+replacing all nonalphanumeric characters with ``_``. E.g., ``my-tools.lua``
+becomes ``my_tools``. If there are several scripts with the same name, it is
+made unique by appending a number. This is the name returned by
+``mp.get_script_name()``.
+
+Entries with ``.disable`` extension are always ignored.
+
+If a script is a directory (either if a directory is passed to ``--script``,
+or any sub-directories in the script directory, such as for example
+``~/.config/mpv/scripts/something/``), then the directory represents a single
+script. The player will try to load a file named ``main.x``, where ``x`` is
+replaced with the file extension. For example, if ``main.lua`` exists, it is
+loaded with the Lua scripting backend.
+
+You must not put any other files or directories that start with ``main.`` into
+the script's top level directory. If the script directory contains for example
+both ``main.lua`` and ``main.js``, only one of them will be loaded (and which
+one depends on mpv internals that may change any time). Likewise, if there is
+for example ``main.foo``, your script will break as soon as mpv adds a backend
+that uses the ``.foo`` file extension.
+
+mpv also appends the top level directory of the script to the start of Lua's
+package path so you can import scripts from there too. Be aware that this will
+shadow Lua libraries that use the same package path. (Single file scripts do not
+include mpv specific directories in the Lua package path. This was silently
+changed in mpv 0.32.0.)
+
+Using a script directory is the recommended way to package a script that
+consists of multiple source files, or requires other files (you can use
+``mp.get_script_directory()`` to get the location and e.g. load data files).
+
+Making a script a git repository, basically a repository which contains a
+``main.lua`` file in the root directory, makes scripts easily updateable
+(without the dangers of auto-updates). Another suggestion is to use git
+submodules to share common files or libraries.
+
+Details on the script initialization and lifecycle
+--------------------------------------------------
+
+Your script will be loaded by the player at program start from the ``scripts``
+configuration subdirectory, or from a path specified with the ``--script``
+option. Some scripts are loaded internally (like ``--osc``). Each script runs in
+its own thread. Your script is first run "as is", and once that is done, the event loop
+is entered. This event loop will dispatch events received by mpv and call your
+own event handlers which you have registered with ``mp.register_event``, or
+timers added with ``mp.add_timeout`` or similar. Note that since the
+script starts execution concurrently with player initialization, some properties
+may not be populated with meaningful values until the relevant subsystems have
+initialized.
+
+When the player quits, all scripts will be asked to terminate. This happens via
+a ``shutdown`` event, which by default will make the event loop return. If your
+script got into an endless loop, mpv will probably behave fine during playback,
+but it won't terminate when quitting, because it's waiting on your script.
+
+Internally, the C code will call the Lua function ``mp_event_loop`` after
+loading a Lua script. This function is normally defined by the default prelude
+loaded before your script (see ``player/lua/defaults.lua`` in the mpv sources).
+The event loop will wait for events and dispatch events registered with
+``mp.register_event``. It will also handle timers added with ``mp.add_timeout``
+and similar (by waiting with a timeout).
+
+Since mpv 0.6.0, the player will wait until the script is fully loaded before
+continuing normal operation. The player considers a script as fully loaded as
+soon as it starts waiting for mpv events (or it exits). In practice this means
+the player will more or less hang until the script returns from the main chunk
+(and ``mp_event_loop`` is called), or the script calls ``mp_event_loop`` or
+``mp.dispatch_events`` directly. This is done to make it possible for a script
+to fully setup event handlers etc. before playback actually starts. In older
+mpv versions, this happened asynchronously. With mpv 0.29.0, this changes
+slightly, and it merely waits for scripts to be loaded in this manner before
+starting playback as part of the player initialization phase. Scripts run though
+initialization in parallel. This might change again.
+
+mp functions
+------------
+
+The ``mp`` module is preloaded, although it can be loaded manually with
+``require 'mp'``. It provides the core client API.
+
+``mp.command(string)``
+ Run the given command. This is similar to the commands used in input.conf.
+ See `List of Input Commands`_.
+
+ By default, this will show something on the OSD (depending on the command),
+ as if it was used in ``input.conf``. See `Input Command Prefixes`_ how
+ to influence OSD usage per command.
+
+ Returns ``true`` on success, or ``nil, error`` on error.
+
+``mp.commandv(arg1, arg2, ...)``
+ Similar to ``mp.command``, but pass each command argument as separate
+ parameter. This has the advantage that you don't have to care about
+ quoting and escaping in some cases.
+
+ Example:
+
+ ::
+
+ mp.command("loadfile " .. filename .. " append")
+ mp.commandv("loadfile", filename, "append")
+
+ These two commands are equivalent, except that the first version breaks
+ if the filename contains spaces or certain special characters.
+
+ Note that properties are *not* expanded. You can use either ``mp.command``,
+ the ``expand-properties`` prefix, or the ``mp.get_property`` family of
+ functions.
+
+ Unlike ``mp.command``, this will not use OSD by default either (except
+ for some OSD-specific commands).
+
+``mp.command_native(table [,def])``
+ Similar to ``mp.commandv``, but pass the argument list as table. This has
+ the advantage that in at least some cases, arguments can be passed as
+ native types. It also allows you to use named argument.
+
+ If the table is an array, each array item is like an argument in
+ ``mp.commandv()`` (but can be a native type instead of a string).
+
+ If the table contains string keys, it's interpreted as command with named
+ arguments. This requires at least an entry with the key ``name`` to be
+ present, which must be a string, and contains the command name. The special
+ entry ``_flags`` is optional, and if present, must be an array of
+ `Input Command Prefixes`_ to apply. All other entries are interpreted as
+ arguments.
+
+ Returns a result table on success (usually empty), or ``def, error`` on
+ error. ``def`` is the second parameter provided to the function, and is
+ nil if it's missing.
+
+``mp.command_native_async(table [,fn])``
+ Like ``mp.command_native()``, but the command is ran asynchronously (as far
+ as possible), and upon completion, fn is called. fn has three arguments:
+ ``fn(success, result, error)``:
+
+ ``success``
+ Always a Boolean and is true if the command was successful,
+ otherwise false.
+
+ ``result``
+ The result value (can be nil) in case of success, nil otherwise (as
+ returned by ``mp.command_native()``).
+
+ ``error``
+ The error string in case of an error, nil otherwise.
+
+ Returns a table with undefined contents, which can be used as argument for
+ ``mp.abort_async_command``.
+
+ If starting the command failed for some reason, ``nil, error`` is returned,
+ and ``fn`` is called indicating failure, using the same error value.
+
+ ``fn`` is always called asynchronously, even if the command failed to start.
+
+``mp.abort_async_command(t)``
+ Abort a ``mp.command_native_async`` call. The argument is the return value
+ of that command (which starts asynchronous execution of the command).
+ Whether this works and how long it takes depends on the command and the
+ situation. The abort call itself is asynchronous. Does not return anything.
+
+``mp.del_property(name)``
+ Delete the given property. See ``mp.get_property`` and `Properties`_ for more
+ information about properties. Most properties cannot be deleted.
+
+ Returns true on success, or ``nil, error`` on error.
+
+``mp.get_property(name [,def])``
+ Return the value of the given property as string. These are the same
+ properties as used in input.conf. See `Properties`_ for a list of
+ properties. The returned string is formatted similar to ``${=name}``
+ (see `Property Expansion`_).
+
+ Returns the string on success, or ``def, error`` on error. ``def`` is the
+ second parameter provided to the function, and is nil if it's missing.
+
+``mp.get_property_osd(name [,def])``
+ Similar to ``mp.get_property``, but return the property value formatted for
+ OSD. This is the same string as printed with ``${name}`` when used in
+ input.conf.
+
+ Returns the string on success, or ``def, error`` on error. ``def`` is the
+ second parameter provided to the function, and is an empty string if it's
+ missing. Unlike ``get_property()``, assigning the return value to a variable
+ will always result in a string.
+
+``mp.get_property_bool(name [,def])``
+ Similar to ``mp.get_property``, but return the property value as Boolean.
+
+ Returns a Boolean on success, or ``def, error`` on error.
+
+``mp.get_property_number(name [,def])``
+ Similar to ``mp.get_property``, but return the property value as number.
+
+ Note that while Lua does not distinguish between integers and floats,
+ mpv internals do. This function simply request a double float from mpv,
+ and mpv will usually convert integer property values to float.
+
+ Returns a number on success, or ``def, error`` on error.
+
+``mp.get_property_native(name [,def])``
+ Similar to ``mp.get_property``, but return the property value using the best
+ Lua type for the property. Most time, this will return a string, Boolean,
+ or number. Some properties (for example ``chapter-list``) are returned as
+ tables.
+
+ Returns a value on success, or ``def, error`` on error. Note that ``nil``
+ might be a possible, valid value too in some corner cases.
+
+``mp.set_property(name, value)``
+ Set the given property to the given string value. See ``mp.get_property``
+ and `Properties`_ for more information about properties.
+
+ Returns true on success, or ``nil, error`` on error.
+
+``mp.set_property_bool(name, value)``
+ Similar to ``mp.set_property``, but set the given property to the given
+ Boolean value.
+
+``mp.set_property_number(name, value)``
+ Similar to ``mp.set_property``, but set the given property to the given
+ numeric value.
+
+ Note that while Lua does not distinguish between integers and floats,
+ mpv internals do. This function will test whether the number can be
+ represented as integer, and if so, it will pass an integer value to mpv,
+ otherwise a double float.
+
+``mp.set_property_native(name, value)``
+ Similar to ``mp.set_property``, but set the given property using its native
+ type.
+
+ Since there are several data types which cannot represented natively in
+ Lua, this might not always work as expected. For example, while the Lua
+ wrapper can do some guesswork to decide whether a Lua table is an array
+ or a map, this would fail with empty tables. Also, there are not many
+ properties for which it makes sense to use this, instead of
+ ``set_property``, ``set_property_bool``, ``set_property_number``.
+ For these reasons, this function should probably be avoided for now, except
+ for properties that use tables natively.
+
+``mp.get_time()``
+ Return the current mpv internal time in seconds as a number. This is
+ basically the system time, with an arbitrary offset.
+
+``mp.add_key_binding(key, name|fn [,fn [,flags]])``
+ Register callback to be run on a key binding. The binding will be mapped to
+ the given ``key``, which is a string describing the physical key. This uses
+ the same key names as in input.conf, and also allows combinations
+ (e.g. ``ctrl+a``). If the key is empty or ``nil``, no physical key is
+ registered, but the user still can create own bindings (see below).
+
+ After calling this function, key presses will cause the function ``fn`` to
+ be called (unless the user remapped the key with another binding).
+
+ The ``name`` argument should be a short symbolic string. It allows the user
+ to remap the key binding via input.conf using the ``script-message``
+ command, and the name of the key binding (see below for
+ an example). The name should be unique across other bindings in the same
+ script - if not, the previous binding with the same name will be
+ overwritten. You can omit the name, in which case a random name is generated
+ internally. (Omitting works as follows: either pass ``nil`` for ``name``,
+ or pass the ``fn`` argument in place of the name. The latter is not
+ recommended and is handled for compatibility only.)
+
+ The last argument is used for optional flags. This is a table, which can
+ have the following entries:
+
+ ``repeatable``
+ If set to ``true``, enables key repeat for this specific binding.
+
+ ``complex``
+ If set to ``true``, then ``fn`` is called on both key up and down
+ events (as well as key repeat, if enabled), with the first
+ argument being a table. This table has the following entries (and
+ may contain undocumented ones):
+
+ ``event``
+ Set to one of the strings ``down``, ``repeat``, ``up`` or
+ ``press`` (the latter if key up/down can't be tracked).
+
+ ``is_mouse``
+ Boolean Whether the event was caused by a mouse button.
+
+ ``key_name``
+ The name of they key that triggered this, or ``nil`` if
+ invoked artificially. If the key name is unknown, it's an
+ empty string.
+
+ ``key_text``
+ Text if triggered by a text key, otherwise ``nil``. See
+ description of ``script-binding`` command for details (this
+ field is equivalent to the 5th argument).
+
+ Internally, key bindings are dispatched via the ``script-message-to`` or
+ ``script-binding`` input commands and ``mp.register_script_message``.
+
+ Trying to map multiple commands to a key will essentially prefer a random
+ binding, while the other bindings are not called. It is guaranteed that
+ user defined bindings in the central input.conf are preferred over bindings
+ added with this function (but see ``mp.add_forced_key_binding``).
+
+ Example:
+
+ ::
+
+ function something_handler()
+ print("the key was pressed")
+ end
+ mp.add_key_binding("x", "something", something_handler)
+
+ This will print the message ``the key was pressed`` when ``x`` was pressed.
+
+ The user can remap these key bindings. Then the user has to put the
+ following into their input.conf to remap the command to the ``y`` key:
+
+ ::
+
+ y script-binding something
+
+
+ This will print the message when the key ``y`` is pressed. (``x`` will
+ still work, unless the user remaps it.)
+
+ You can also explicitly send a message to a named script only. Assume the
+ above script was using the filename ``fooscript.lua``:
+
+ ::
+
+ y script-binding fooscript/something
+
+``mp.add_forced_key_binding(...)``
+ This works almost the same as ``mp.add_key_binding``, but registers the
+ key binding in a way that will overwrite the user's custom bindings in their
+ input.conf. (``mp.add_key_binding`` overwrites default key bindings only,
+ but not those by the user's input.conf.)
+
+``mp.remove_key_binding(name)``
+ Remove a key binding added with ``mp.add_key_binding`` or
+ ``mp.add_forced_key_binding``. Use the same name as you used when adding
+ the bindings. It's not possible to remove bindings for which you omitted
+ the name.
+
+``mp.register_event(name, fn)``
+ Call a specific function when an event happens. The event name is a string,
+ and the function fn is a Lua function value.
+
+ Some events have associated data. This is put into a Lua table and passed
+ as argument to fn. The Lua table by default contains a ``name`` field,
+ which is a string containing the event name. If the event has an error
+ associated, the ``error`` field is set to a string describing the error,
+ on success it's not set.
+
+ If multiple functions are registered for the same event, they are run in
+ registration order, which the first registered function running before all
+ the other ones.
+
+ Returns true if such an event exists, false otherwise.
+
+ See `Events`_ and `List of events`_ for details.
+
+``mp.unregister_event(fn)``
+ Undo ``mp.register_event(..., fn)``. This removes all event handlers that
+ are equal to the ``fn`` parameter. This uses normal Lua ``==`` comparison,
+ so be careful when dealing with closures.
+
+``mp.observe_property(name, type, fn)``
+ Watch a property for changes. If the property ``name`` is changed, then
+ the function ``fn(name)`` will be called. ``type`` can be ``nil``, or be
+ set to one of ``none``, ``native``, ``bool``, ``string``, or ``number``.
+ ``none`` is the same as ``nil``. For all other values, the new value of
+ the property will be passed as second argument to ``fn``, using
+ ``mp.get_property_<type>`` to retrieve it. This means if ``type`` is for
+ example ``string``, ``fn`` is roughly called as in
+ ``fn(name, mp.get_property_string(name))``.
+
+ If possible, change events are coalesced. If a property is changed a bunch
+ of times in a row, only the last change triggers the change function. (The
+ exact behavior depends on timing and other things.)
+
+ If a property is unavailable, or on error, the value argument to ``fn`` is
+ ``nil``. (The ``observe_property()`` call always succeeds, even if a
+ property does not exist.)
+
+ In some cases the function is not called even if the property changes.
+ This depends on the property, and it's a valid feature request to ask for
+ better update handling of a specific property.
+
+ If the ``type`` is ``none`` or ``nil``, sporadic property change events are
+ possible. This means the change function ``fn`` can be called even if the
+ property doesn't actually change.
+
+ You always get an initial change notification. This is meant to initialize
+ the user's state to the current value of the property.
+
+``mp.unobserve_property(fn)``
+ Undo ``mp.observe_property(..., fn)``. This removes all property handlers
+ that are equal to the ``fn`` parameter. This uses normal Lua ``==``
+ comparison, so be careful when dealing with closures.
+
+``mp.add_timeout(seconds, fn [, disabled])``
+ Call the given function fn when the given number of seconds has elapsed.
+ Note that the number of seconds can be fractional. For now, the timer's
+ resolution may be as low as 50 ms, although this will be improved in the
+ future.
+
+ If the ``disabled`` argument is set to ``true`` or a truthy value, the
+ timer will wait to be manually started with a call to its ``resume()``
+ method.
+
+ This is a one-shot timer: it will be removed when it's fired.
+
+ Returns a timer object. See ``mp.add_periodic_timer`` for details.
+
+``mp.add_periodic_timer(seconds, fn [, disabled])``
+ Call the given function periodically. This is like ``mp.add_timeout``, but
+ the timer is re-added after the function fn is run.
+
+ Returns a timer object. The timer object provides the following methods:
+ ``stop()``
+ Disable the timer. Does nothing if the timer is already disabled.
+ This will remember the current elapsed time when stopping, so that
+ ``resume()`` essentially unpauses the timer.
+
+ ``kill()``
+ Disable the timer. Resets the elapsed time. ``resume()`` will
+ restart the timer.
+
+ ``resume()``
+ Restart the timer. If the timer was disabled with ``stop()``, this
+ will resume at the time it was stopped. If the timer was disabled
+ with ``kill()``, or if it's a previously fired one-shot timer (added
+ with ``add_timeout()``), this starts the timer from the beginning,
+ using the initially configured timeout.
+
+ ``is_enabled()``
+ Whether the timer is currently enabled or was previously disabled
+ (e.g. by ``stop()`` or ``kill()``).
+
+ ``timeout`` (RW)
+ This field contains the current timeout period. This value is not
+ updated as time progresses. It's only used to calculate when the
+ timer should fire next when the timer expires.
+
+ If you write this, you can call ``t:kill() ; t:resume()`` to reset
+ the current timeout to the new one. (``t:stop()`` won't use the
+ new timeout.)
+
+ ``oneshot`` (RW)
+ Whether the timer is periodic (``false``) or fires just once
+ (``true``). This value is used when the timer expires (but before
+ the timer callback function fn is run).
+
+ Note that these are methods, and you have to call them using ``:`` instead
+ of ``.`` (Refer to https://www.lua.org/manual/5.2/manual.html#3.4.9 .)
+
+ Example:
+
+ ::
+
+ seconds = 0
+ timer = mp.add_periodic_timer(1, function()
+ print("called every second")
+ # stop it after 10 seconds
+ seconds = seconds + 1
+ if seconds >= 10 then
+ timer:kill()
+ end
+ end)
+
+
+``mp.get_opt(key)``
+ Return a setting from the ``--script-opts`` option. It's up to the user and
+ the script how this mechanism is used. Currently, all scripts can access
+ this equally, so you should be careful about collisions.
+
+``mp.get_script_name()``
+ Return the name of the current script. The name is usually made of the
+ filename of the script, with directory and file extension removed. If
+ there are several scripts which would have the same name, it's made unique
+ by appending a number. Any nonalphanumeric characters are replaced with ``_``.
+
+ .. admonition:: Example
+
+ The script ``/path/to/foo-script.lua`` becomes ``foo_script``.
+
+``mp.get_script_directory()``
+ Return the directory if this is a script packaged as directory (see
+ `Script location`_ for a description). Return nothing if this is a single
+ file script.
+
+``mp.osd_message(text [,duration])``
+ Show an OSD message on the screen. ``duration`` is in seconds, and is
+ optional (uses ``--osd-duration`` by default).
+
+Advanced mp functions
+---------------------
+
+These also live in the ``mp`` module, but are documented separately as they
+are useful only in special situations.
+
+``mp.get_wakeup_pipe()``
+ Calls ``mpv_get_wakeup_pipe()`` and returns the read end of the wakeup
+ pipe. This is deprecated, but still works. (See ``client.h`` for details.)
+
+``mp.get_next_timeout()``
+ Return the relative time in seconds when the next timer (``mp.add_timeout``
+ and similar) expires. If there is no timer, return ``nil``.
+
+``mp.dispatch_events([allow_wait])``
+ This can be used to run custom event loops. If you want to have direct
+ control what the Lua script does (instead of being called by the default
+ event loop), you can set the global variable ``mp_event_loop`` to your
+ own function running the event loop. From your event loop, you should call
+ ``mp.dispatch_events()`` to dequeue and dispatch mpv events.
+
+ If the ``allow_wait`` parameter is set to ``true``, the function will block
+ until the next event is received or the next timer expires. Otherwise (and
+ this is the default behavior), it returns as soon as the event loop is
+ emptied. It's strongly recommended to use ``mp.get_next_timeout()`` and
+ ``mp.get_wakeup_pipe()`` if you're interested in properly working
+ notification of new events and working timers.
+
+``mp.register_idle(fn)``
+ Register an event loop idle handler. Idle handlers are called before the
+ script goes to sleep after handling all new events. This can be used for
+ example to delay processing of property change events: if you're observing
+ multiple properties at once, you might not want to act on each property
+ change, but only when all change notifications have been received.
+
+``mp.unregister_idle(fn)``
+ Undo ``mp.register_idle(fn)``. This removes all idle handlers that
+ are equal to the ``fn`` parameter. This uses normal Lua ``==`` comparison,
+ so be careful when dealing with closures.
+
+``mp.enable_messages(level)``
+ Set the minimum log level of which mpv message output to receive. These
+ messages are normally printed to the terminal. By calling this function,
+ you can set the minimum log level of messages which should be received with
+ the ``log-message`` event. See the description of this event for details.
+ The level is a string, see ``msg.log`` for allowed log levels.
+
+``mp.register_script_message(name, fn)``
+ This is a helper to dispatch ``script-message`` or ``script-message-to``
+ invocations to Lua functions. ``fn`` is called if ``script-message`` or
+ ``script-message-to`` (with this script as destination) is run
+ with ``name`` as first parameter. The other parameters are passed to ``fn``.
+ If a message with the given name is already registered, it's overwritten.
+
+ Used by ``mp.add_key_binding``, so be careful about name collisions.
+
+``mp.unregister_script_message(name)``
+ Undo a previous registration with ``mp.register_script_message``. Does
+ nothing if the ``name`` wasn't registered.
+
+``mp.create_osd_overlay(format)``
+ Create an OSD overlay. This is a very thin wrapper around the ``osd-overlay``
+ command. The function returns a table, which mostly contains fields that
+ will be passed to ``osd-overlay``. The ``format`` parameter is used to
+ initialize the ``format`` field. The ``data`` field contains the text to
+ be used as overlay. For details, see the ``osd-overlay`` command.
+
+ In addition, it provides the following methods:
+
+ ``update()``
+ Commit the OSD overlay to the screen, or in other words, run the
+ ``osd-overlay`` command with the current fields of the overlay table.
+ Returns the result of the ``osd-overlay`` command itself.
+
+ ``remove()``
+ Remove the overlay from the screen. A ``update()`` call will add it
+ again.
+
+ Example:
+
+ ::
+
+ ov = mp.create_osd_overlay("ass-events")
+ ov.data = "{\\an5}{\\b1}hello world!"
+ ov:update()
+
+ The advantage of using this wrapper (as opposed to running ``osd-overlay``
+ directly) is that the ``id`` field is allocated automatically.
+
+``mp.get_osd_size()``
+ Returns a tuple of ``osd_width, osd_height, osd_par``. The first two give
+ the size of the OSD in pixels (for video outputs like ``--vo=xv``, this may
+ be "scaled" pixels). The third is the display pixel aspect ratio.
+
+ May return invalid/nonsense values if OSD is not initialized yet.
+
+mp.msg functions
+----------------
+
+This module allows outputting messages to the terminal, and can be loaded
+with ``require 'mp.msg'``.
+
+``msg.log(level, ...)``
+ The level parameter is the message priority. It's a string and one of
+ ``fatal``, ``error``, ``warn``, ``info``, ``v``, ``debug``, ``trace``. The
+ user's settings will determine which of these messages will be
+ visible. Normally, all messages are visible, except ``v``, ``debug`` and
+ ``trace``.
+
+ The parameters after that are all converted to strings. Spaces are inserted
+ to separate multiple parameters.
+
+ You don't need to add newlines.
+
+``msg.fatal(...)``, ``msg.error(...)``, ``msg.warn(...)``, ``msg.info(...)``, ``msg.verbose(...)``, ``msg.debug(...)``, ``msg.trace(...)``
+ All of these are shortcuts and equivalent to the corresponding
+ ``msg.log(level, ...)`` call.
+
+mp.options functions
+--------------------
+
+mpv comes with a built-in module to manage options from config-files and the
+command-line. All you have to do is to supply a table with default options to
+the read_options function. The function will overwrite the default values
+with values found in the config-file and the command-line (in that order).
+
+``options.read_options(table [, identifier [, on_update]])``
+ A ``table`` with key-value pairs. The type of the default values is
+ important for converting the values read from the config file or
+ command-line back. Do not use ``nil`` as a default value!
+
+ The ``identifier`` is used to identify the config-file and the command-line
+ options. These needs to unique to avoid collisions with other scripts.
+ Defaults to ``mp.get_script_name()`` if the parameter is ``nil`` or missing.
+
+ The ``on_update`` parameter enables run-time updates of all matching option
+ values via the ``script-opts`` option/property. If any of the matching
+ options changes, the values in the ``table`` (which was originally passed to
+ the function) are changed, and ``on_update(list)`` is called. ``list`` is
+ a table where each updated option has a ``list[option_name] = true`` entry.
+ There is no initial ``on_update()`` call. This never re-reads the config file.
+ ``script-opts`` is always applied on the original config file, ignoring
+ previous ``script-opts`` values (for example, if an option is removed from
+ ``script-opts`` at runtime, the option will have the value in the config
+ file). ``table`` entries are only written for option values whose values
+ effectively change (this is important if the script changes ``table``
+ entries independently).
+
+
+Example implementation::
+
+ local options = {
+ optionA = "defaultvalueA",
+ optionB = -0.5,
+ optionC = true,
+ }
+
+ require "mp.options".read_options(options, "myscript")
+ print(options.optionA)
+
+
+The config file will be stored in ``script-opts/identifier.conf`` in mpv's user
+folder. Comment lines can be started with # and stray spaces are not removed.
+Boolean values will be represented with yes/no.
+
+Example config::
+
+ # comment
+ optionA=Hello World
+ optionB=9999
+ optionC=no
+
+
+Command-line options are read from the ``--script-opts`` parameter. To avoid
+collisions, all keys have to be prefixed with ``identifier-``.
+
+Example command-line::
+
+ --script-opts=myscript-optionA=TEST,myscript-optionB=0,myscript-optionC=yes
+
+
+mp.utils functions
+------------------
+
+This built-in module provides generic helper functions for Lua, and have
+strictly speaking nothing to do with mpv or video/audio playback. They are
+provided for convenience. Most compensate for Lua's scarce standard library.
+
+Be warned that any of these functions might disappear any time. They are not
+strictly part of the guaranteed API.
+
+``utils.getcwd()``
+ Returns the directory that mpv was launched from. On error, ``nil, error``
+ is returned.
+
+``utils.readdir(path [, filter])``
+ Enumerate all entries at the given path on the filesystem, and return them
+ as array. Each entry is a directory entry (without the path).
+ The list is unsorted (in whatever order the operating system returns it).
+
+ If the ``filter`` argument is given, it must be one of the following
+ strings:
+
+ ``files``
+ List regular files only. This excludes directories, special files
+ (like UNIX device files or FIFOs), and dead symlinks. It includes
+ UNIX symlinks to regular files.
+
+ ``dirs``
+ List directories only, or symlinks to directories. ``.`` and ``..``
+ are not included.
+
+ ``normal``
+ Include the results of both ``files`` and ``dirs``. (This is the
+ default.)
+
+ ``all``
+ List all entries, even device files, dead symlinks, FIFOs, and the
+ ``.`` and ``..`` entries.
+
+ On error, ``nil, error`` is returned.
+
+``utils.file_info(path)``
+ Stats the given path for information and returns a table with the
+ following entries:
+
+ ``mode``
+ protection bits (on Windows, always 755 (octal) for directories
+ and 644 (octal) for files)
+ ``size``
+ size in bytes
+ ``atime``
+ time of last access
+ ``mtime``
+ time of last modification
+ ``ctime``
+ time of last metadata change
+ ``is_file``
+ Whether ``path`` is a regular file (boolean)
+ ``is_dir``
+ Whether ``path`` is a directory (boolean)
+
+ ``mode`` and ``size`` are integers.
+ Timestamps (``atime``, ``mtime`` and ``ctime``) are integer seconds since
+ the Unix epoch (Unix time).
+ The booleans ``is_file`` and ``is_dir`` are provided as a convenience;
+ they can be and are derived from ``mode``.
+
+ On error (e.g. path does not exist), ``nil, error`` is returned.
+
+``utils.split_path(path)``
+ Split a path into directory component and filename component, and return
+ them. The first return value is always the directory. The second return
+ value is the trailing part of the path, the directory entry.
+
+``utils.join_path(p1, p2)``
+ Return the concatenation of the 2 paths. Tries to be clever. For example,
+ if ``p2`` is an absolute path, ``p2`` is returned without change.
+
+``utils.subprocess(t)``
+ Runs an external process and waits until it exits. Returns process status
+ and the captured output. This is a legacy wrapper around calling the
+ ``subprocess`` command with ``mp.command_native``. It does the following
+ things:
+
+ - copy the table ``t``
+ - rename ``cancellable`` field to ``playback_only``
+ - rename ``max_size`` to ``capture_size``
+ - set ``capture_stdout`` field to ``true`` if unset
+ - set ``name`` field to ``subprocess``
+ - call ``mp.command_native(copied_t)``
+ - if the command failed, create a dummy result table
+ - copy ``error_string`` to ``error`` field if the string is non-empty
+ - return the result table
+
+ It is recommended to use ``mp.command_native`` or ``mp.command_native_async``
+ directly, instead of calling this legacy wrapper. It is for compatibility
+ only.
+
+ See the ``subprocess`` documentation for semantics and further parameters.
+
+``utils.subprocess_detached(t)``
+ Runs an external process and detaches it from mpv's control.
+
+ The parameter ``t`` is a table. The function reads the following entries:
+
+ ``args``
+ Array of strings of the same semantics as the ``args`` used in the
+ ``subprocess`` function.
+
+ The function returns ``nil``.
+
+ This is a legacy wrapper around calling the ``run`` command with
+ ``mp.commandv`` and other functions.
+
+``utils.getpid()``
+ Returns the process ID of the running mpv process. This can be used to identify
+ the calling mpv when launching (detached) subprocesses.
+
+``utils.get_env_list()``
+ Returns the C environment as a list of strings. (Do not confuse this with
+ the Lua "environment", which is an unrelated concept.)
+
+``utils.parse_json(str [, trail])``
+ Parses the given string argument as JSON, and returns it as a Lua table. On
+ error, returns ``nil, error``. (Currently, ``error`` is just a string
+ reading ``error``, because there is no fine-grained error reporting of any
+ kind.)
+
+ The returned value uses similar conventions as ``mp.get_property_native()``
+ to distinguish empty objects and arrays.
+
+ If the ``trail`` parameter is ``true`` (or any value equal to ``true``),
+ then trailing non-whitespace text is tolerated by the function, and the
+ trailing text is returned as 3rd return value. (The 3rd return value is
+ always there, but with ``trail`` set, no error is raised.)
+
+``utils.format_json(v)``
+ Format the given Lua table (or value) as a JSON string and return it. On
+ error, returns ``nil, error``. (Errors usually only happen on value types
+ incompatible with JSON.)
+
+ The argument value uses similar conventions as ``mp.set_property_native()``
+ to distinguish empty objects and arrays.
+
+``utils.to_string(v)``
+ Turn the given value into a string. Formats tables and their contents. This
+ doesn't do anything special; it is only needed because Lua is terrible.
+
+Events
+------
+
+Events are notifications from player core to scripts. You can register an
+event handler with ``mp.register_event``.
+
+Note that all scripts (and other parts of the player) receive events equally,
+and there's no such thing as blocking other scripts from receiving events.
+
+Example:
+
+::
+
+ function my_fn(event)
+ print("start of playback!")
+ end
+
+ mp.register_event("file-loaded", my_fn)
+
+For the existing event types, see `List of events`_.
+
+Extras
+------
+
+This documents experimental features, or features that are "too special" to
+guarantee a stable interface.
+
+``mp.add_hook(type, priority, fn)``
+ Add a hook callback for ``type`` (a string identifying a certain kind of
+ hook). These hooks allow the player to call script functions and wait for
+ their result (normally, the Lua scripting interface is asynchronous from
+ the point of view of the player core). ``priority`` is an arbitrary integer
+ that allows ordering among hooks of the same kind. Using the value 50 is
+ recommended as neutral default value.
+
+ ``fn(hook)`` is the function that will be called during execution of the
+ hook. The parameter passed to it (``hook``) is a Lua object that can control
+ further aspects about the currently invoked hook. It provides the following
+ methods:
+
+ ``defer()``
+ Returning from the hook function should not automatically continue
+ the hook. Instead, the API user wants to call ``hook:cont()`` on its
+ own at a later point in time (before or after the function has
+ returned).
+
+ ``cont()``
+ Continue the hook. Doesn't need to be called unless ``defer()`` was
+ called.
+
+ See `Hooks`_ for currently existing hooks and what they do - only the hook
+ list is interesting; handling hook execution is done by the Lua script
+ function automatically.
diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst
new file mode 100644
index 0000000..e97422d
--- /dev/null
+++ b/DOCS/man/mpv.rst
@@ -0,0 +1,1690 @@
+mpv
+###
+
+##############
+a media player
+##############
+
+:Copyright: GPLv2+
+:Manual section: 1
+:Manual group: multimedia
+
+.. contents:: Table of Contents
+
+SYNOPSIS
+========
+
+| **mpv** [options] [file|URL|PLAYLIST|-]
+| **mpv** [options] files
+
+DESCRIPTION
+===========
+
+**mpv** is a media player based on MPlayer and mplayer2. It supports a wide variety of video
+file formats, audio and video codecs, and subtitle types. Special input URL
+types are available to read input from a variety of sources other than disk
+files. Depending on platform, a variety of different video and audio output
+methods are supported.
+
+Usage examples to get you started quickly can be found at the end of this man
+page.
+
+
+INTERACTIVE CONTROL
+===================
+
+mpv has a fully configurable, command-driven control layer which allows you
+to control mpv using keyboard, mouse, or remote control (there is no
+LIRC support - configure remotes as input devices instead).
+
+See the ``--input-`` options for ways to customize it.
+
+The following listings are not necessarily complete. See ``etc/input.conf``
+in the mpv source files for a list of default bindings. User ``input.conf``
+files and Lua scripts can define additional key bindings.
+
+See `COMMAND INTERFACE`_ and `Key names`_ sections for more details on
+configuring keybindings.
+
+See also ``--input-test`` for interactive binding details by key, and the
+`stats`_ built-in script for key bindings list (including print to terminal).
+
+Keyboard Control
+----------------
+
+LEFT and RIGHT
+ Seek backward/forward 5 seconds. Shift+arrow does a 1 second exact seek
+ (see ``--hr-seek``).
+
+UP and DOWN
+ Seek forward/backward 1 minute. Shift+arrow does a 5 second exact seek (see
+ ``--hr-seek``).
+
+Ctrl+LEFT and Ctrl+RIGHT
+ Seek to the previous/next subtitle. Subject to some restrictions and
+ might not always work; see ``sub-seek`` command.
+
+Ctrl+Shift+LEFT and Ctrl+Shift+RIGHT
+ Adjust subtitle delay so that the next or previous subtitle is displayed
+ now. This is especially useful to sync subtitles to audio.
+
+[ and ]
+ Decrease/increase current playback speed by 10%.
+
+{ and }
+ Halve/double current playback speed.
+
+BACKSPACE
+ Reset playback speed to normal.
+
+Shift+BACKSPACE
+ Undo the last seek. This works only if the playlist entry was not changed.
+ Hitting it a second time will go back to the original position.
+ See ``revert-seek`` command for details.
+
+Shift+Ctrl+BACKSPACE
+ Mark the current position. This will then be used by ``Shift+BACKSPACE``
+ as revert position (once you seek back, the marker will be reset). You can
+ use this to seek around in the file and then return to the exact position
+ where you left off.
+
+< and >
+ Go backward/forward in the playlist.
+
+ENTER
+ Go forward in the playlist.
+
+p and SPACE
+ Pause (pressing again unpauses).
+
+\.
+ Step forward. Pressing once will pause, every consecutive press will
+ play one frame and then go into pause mode again.
+
+,
+ Step backward. Pressing once will pause, every consecutive press will
+ play one frame in reverse and then go into pause mode again.
+
+q
+ Stop playing and quit.
+
+Q
+ Like ``q``, but store the current playback position. Playing the same file
+ later will resume at the old playback position if possible. See
+ `RESUMING PLAYBACK`_.
+
+/ and *
+ Decrease/increase volume.
+
+9 and 0
+ Decrease/increase volume.
+
+m
+ Mute sound.
+
+\_
+ Cycle through the available video tracks.
+
+\#
+ Cycle through the available audio tracks.
+
+E
+ Cycle through the available Editions.
+
+f
+ Toggle fullscreen (see also ``--fs``).
+
+ESC
+ Exit fullscreen mode.
+
+T
+ Toggle stay-on-top (see also ``--ontop``).
+
+w and W
+ Decrease/increase pan-and-scan range. The ``e`` key does the same as
+ ``W`` currently, but use is discouraged.
+
+o and P
+ Show progression bar, elapsed time and total duration on the OSD.
+
+O
+ Toggle OSD states between normal and playback time/duration.
+
+v
+ Toggle subtitle visibility.
+
+j and J
+ Cycle through the available subtitles.
+
+z and Z
+ Adjust subtitle delay by +/- 0.1 seconds. The ``x`` key does the same as
+ ``Z`` currently, but use is discouraged.
+
+l
+ Set/clear A-B loop points. See ``ab-loop`` command for details.
+
+L
+ Toggle infinite looping.
+
+Ctrl++ and Ctrl+-
+ Adjust audio delay (A/V sync) by +/- 0.1 seconds.
+
+Shift+g and Shift+f
+ Adjust subtitle font size by +/- 10%.
+
+u
+ Switch between applying only ``--sub-ass-*`` overrides (default) to SSA/ASS
+ subtitles, and overriding them almost completely with the normal subtitle
+ style. See ``--sub-ass-override`` for more info.
+
+V
+ Toggle subtitle VSFilter aspect compatibility mode. See
+ ``--sub-ass-vsfilter-aspect-compat`` for more info.
+
+r and R
+ Move subtitles up/down. The ``t`` key does the same as ``R`` currently, but
+ use is discouraged.
+
+s
+ Take a screenshot.
+
+S
+ Take a screenshot, without subtitles. (Whether this works depends on VO
+ driver support.)
+
+Ctrl+s
+ Take a screenshot, as the window shows it (with subtitles, OSD, and scaled
+ video).
+
+PGUP and PGDWN
+ Seek to the beginning of the previous/next chapter. In most cases,
+ "previous" will actually go to the beginning of the current chapter; see
+ ``--chapter-seek-threshold``.
+
+Shift+PGUP and Shift+PGDWN
+ Seek backward or forward by 10 minutes. (This used to be mapped to
+ PGUP/PGDWN without Shift.)
+
+d
+ Activate/deactivate deinterlacer.
+
+A
+ Cycle aspect ratio override.
+
+Ctrl+h
+ Toggle hardware video decoding on/off.
+
+Alt+LEFT, Alt+RIGHT, Alt+UP, Alt+DOWN
+ Move the video rectangle (panning).
+
+Alt++ and Alt+-
+ Combining ``Alt`` with the ``+`` or ``-`` keys changes video zoom.
+
+Alt+BACKSPACE
+ Reset the pan/zoom settings.
+
+F8
+ Show the playlist and the current position in it (useful only if a UI window
+ is used, broken on the terminal).
+
+F9
+ Show the list of audio and subtitle streams (useful only if a UI window is
+ used, broken on the terminal).
+
+i and I
+ Show/toggle an overlay displaying statistics about the currently playing
+ file such as codec, framerate, number of dropped frames and so on. See
+ `STATS`_ for more information.
+
+DEL
+ Cycle OSC visibility between never / auto (mouse-move) / always
+
+\`
+ Show the console. (ESC closes it again. See `CONSOLE`_.)
+
+(The following keys are valid only when using a video output that supports the
+corresponding adjustment.)
+
+1 and 2
+ Adjust contrast.
+
+3 and 4
+ Adjust brightness.
+
+5 and 6
+ Adjust gamma.
+
+7 and 8
+ Adjust saturation.
+
+Alt+0 (and Command+0 on macOS)
+ Resize video window to half its original size.
+
+Alt+1 (and Command+1 on macOS)
+ Resize video window to its original size.
+
+Alt+2 (and Command+2 on macOS)
+ Resize video window to double its original size.
+
+Command + f (macOS only)
+ Toggle fullscreen (see also ``--fs``).
+
+(The following keys are valid if you have a keyboard with multimedia keys.)
+
+PAUSE
+ Pause.
+
+STOP
+ Stop playing and quit.
+
+PREVIOUS and NEXT
+ Seek backward/forward 1 minute.
+
+ZOOMIN and ZOOMOUT
+ Changes video zoom.
+
+If you miss some older key bindings, look at ``etc/restore-old-bindings.conf``
+in the mpv git repository.
+
+Mouse Control
+-------------
+
+Left double click
+ Toggle fullscreen on/off.
+
+Right click
+ Toggle pause on/off.
+
+Forward/Back button
+ Skip to next/previous entry in playlist.
+
+Wheel up/down
+ Decrease/increase volume.
+
+Wheel left/right
+ Seek forward/backward 10 seconds.
+
+
+USAGE
+=====
+
+Command line arguments starting with ``-`` are interpreted as options,
+everything else as filenames or URLs. All options except *flag* options (or
+choice options which include ``yes``) require a parameter in the form
+``--option=value``.
+
+One exception is the lone ``-`` (without anything else), which means media data
+will be read from stdin. Also, ``--`` (without anything else) will make the
+player interpret all following arguments as filenames, even if they start with
+``-``. (To play a file named ``-``, you need to use ``./-``.)
+
+Every *flag* option has a *no-flag* counterpart, e.g. the opposite of the
+``--fs`` option is ``--no-fs``. ``--fs=yes`` is same as ``--fs``, ``--fs=no``
+is the same as ``--no-fs``.
+
+If an option is marked as *(XXX only)*, it will only work in combination with
+the *XXX* option or if *XXX* is compiled in.
+
+Legacy option syntax
+--------------------
+
+The ``--option=value`` syntax is not strictly enforced, and the alternative
+legacy syntax ``-option value`` and ``-option=value`` will also work. This is
+mostly for compatibility with MPlayer. Using these should be avoided. Their
+semantics can change any time in the future.
+
+For example, the alternative syntax will consider an argument following the
+option a filename. ``mpv -fs no`` will attempt to play a file named ``no``,
+because ``--fs`` is a flag option that requires no parameter. If an option
+changes and its parameter becomes optional, then a command line using the
+alternative syntax will break.
+
+Until mpv 0.31.0, there was no difference whether an option started with ``--``
+or a single ``-``. Newer mpv releases strictly expect that you pass the option
+value after a ``=``. For example, before ``mpv --log-file f.txt`` would write
+a log to ``f.txt``, but now this command line fails, as ``--log-file`` expects
+an option value, and ``f.txt`` is simply considered a normal file to be played
+(as in ``mpv f.txt``).
+
+The future plan is that ``-option value`` will not work anymore, and options
+with a single ``-`` behave the same as ``--`` options.
+
+Escaping spaces and other special characters
+--------------------------------------------
+
+Keep in mind that the shell will partially parse and mangle the arguments you
+pass to mpv. For example, you might need to quote or escape options and
+filenames:
+
+ ``mpv "filename with spaces.mkv" --title="window title"``
+
+It gets more complicated if the suboption parser is involved. The suboption
+parser puts several options into a single string, and passes them to a
+component at once, instead of using multiple options on the level of the
+command line.
+
+The suboption parser can quote strings with ``"`` and ``[...]``.
+Additionally, there is a special form of quoting with ``%n%`` described below.
+
+For example, assume the hypothetical ``foo`` filter can take multiple options:
+
+ ``mpv test.mkv --vf=foo:option1=value1:option2:option3=value3,bar``
+
+This passes ``option1`` and ``option3`` to the ``foo`` filter, with ``option2``
+as flag (implicitly ``option2=yes``), and adds a ``bar`` filter after that. If
+an option contains spaces or characters like ``,`` or ``:``, you need to quote
+them:
+
+ ``mpv '--vf=foo:option1="option value with spaces",bar'``
+
+Shells may actually strip some quotes from the string passed to the commandline,
+so the example quotes the string twice, ensuring that mpv receives the ``"``
+quotes.
+
+The ``[...]`` form of quotes wraps everything between ``[`` and ``]``. It's
+useful with shells that don't interpret these characters in the middle of
+an argument (like bash). These quotes are balanced (since mpv 0.9.0): the ``[``
+and ``]`` nest, and the quote terminates on the last ``]`` that has no matching
+``[`` within the string. (For example, ``[a[b]c]`` results in ``a[b]c``.)
+
+The fixed-length quoting syntax is intended for use with external
+scripts and programs.
+
+It is started with ``%`` and has the following format::
+
+ %n%string_of_length_n
+
+.. admonition:: Examples
+
+ ``mpv '--vf=foo:option1=%11%quoted text' test.avi``
+
+ Or in a script:
+
+ ``mpv --vf=foo:option1=%`expr length "$NAME"`%"$NAME" test.avi``
+
+Note: where applicable with JSON-IPC, ``%n%`` is the length in UTF-8 bytes,
+after decoding the JSON data.
+
+Suboptions passed to the client API are also subject to escaping. Using
+``mpv_set_option_string()`` is exactly like passing ``--name=data`` to the
+command line (but without shell processing of the string). Some options
+support passing values in a more structured way instead of flat strings, and
+can avoid the suboption parsing mess. For example, ``--vf`` supports
+``MPV_FORMAT_NODE``, which lets you pass suboptions as a nested data structure
+of maps and arrays.
+
+Paths
+-----
+
+Some care must be taken when passing arbitrary paths and filenames to mpv. For
+example, paths starting with ``-`` will be interpreted as options. Likewise,
+if a path contains the sequence ``://``, the string before that might be
+interpreted as protocol prefix, even though ``://`` can be part of a legal
+UNIX path. To avoid problems with arbitrary paths, you should be sure that
+absolute paths passed to mpv start with ``/``, and prefix relative paths with
+``./``.
+
+Using the ``file://`` pseudo-protocol is discouraged, because it involves
+strange URL unescaping rules.
+
+The name ``-`` itself is interpreted as stdin, and will cause mpv to disable
+console controls. (Which makes it suitable for playing data piped to stdin.)
+
+The special argument ``--`` can be used to stop mpv from interpreting the
+following arguments as options.
+
+When using the client API, you should strictly avoid using ``mpv_command_string``
+for invoking the ``loadfile`` command, and instead prefer e.g. ``mpv_command``
+to avoid the need for filename escaping.
+
+For paths passed to suboptions, the situation is further complicated by the
+need to escape special characters. To work this around, the path can be
+additionally wrapped in the fixed-length syntax, e.g. ``%n%string_of_length_n``
+(see above).
+
+Some mpv options interpret paths starting with ``~``.
+Currently, the prefix ``~~home/`` expands to the mpv configuration directory
+(usually ``~/.config/mpv/``).
+``~/`` expands to the user's home directory. (The trailing ``/`` is always
+required.) The following paths are currently recognized:
+
+================ ===============================================================
+Name Meaning
+================ ===============================================================
+``~~/`` If the subpath exists in any of the mpv's config directories
+ the path of the existing file/dir is returned. Otherwise this
+ is equivalent to ``~~home/``.
+ Note that if --no-config is used ``~~/foobar`` will resolve to
+ ``foobar`` which can be unexpected.
+``~/`` user home directory root (similar to shell, ``$HOME``)
+``~~home/`` mpv config dir (for example ``~/.config/mpv/``)
+``~~global/`` the global config path, if available (not on win32)
+``~~osxbundle/`` the macOS bundle resource path (macOS only)
+``~~desktop/`` the path to the desktop (win32, macOS)
+``~~exe_dir/`` win32 only: the path to the directory containing the exe (for
+ config file purposes; ``$MPV_HOME`` overrides it)
+``~~cache/`` the path to application cache data (``~/.cache/mpv/``)
+ On some platforms, this will be the same as ``~~home/``.
+``~~state/`` the path to application state data (``~/.local/state/mpv/``)
+ On some platforms, this will be the same as ``~~home/``.
+``~~old_home/`` do not use
+================ ===============================================================
+
+
+Per-File Options
+----------------
+
+When playing multiple files, any option given on the command line usually
+affects all files. Example::
+
+ mpv --a file1.mkv --b file2.mkv --c
+
+=============== ===========================
+File Active options
+=============== ===========================
+file1.mkv ``--a --b --c``
+file2.mkv ``--a --b --c``
+=============== ===========================
+
+(This is different from MPlayer and mplayer2.)
+
+Also, if any option is changed at runtime (via input commands), they are not
+reset when a new file is played.
+
+Sometimes, it is useful to change options per-file. This can be achieved by
+adding the special per-file markers ``--{`` and ``--}``. (Note that you must
+escape these on some shells.) Example::
+
+ mpv --a file1.mkv --b --\{ --c file2.mkv --d file3.mkv --e --\} file4.mkv --f
+
+=============== ===========================
+File Active options
+=============== ===========================
+file1.mkv ``--a --b --f``
+file2.mkv ``--a --b --f --c --d --e``
+file3.mkv ``--a --b --f --c --d --e``
+file4.mkv ``--a --b --f``
+=============== ===========================
+
+Additionally, any file-local option changed at runtime is reset when the current
+file stops playing. If option ``--c`` is changed during playback of
+``file2.mkv``, it is reset when advancing to ``file3.mkv``. This only affects
+file-local options. The option ``--a`` is never reset here.
+
+
+List Options
+------------
+
+Some options which store lists of option values can have action suffixes. For
+example, the ``--display-tags`` option takes a ``,``-separated list of tags, but
+the option also allows you to append a single tag with ``--display-tags-append``,
+and the tag name can for example contain a literal ``,`` without the need for
+escaping.
+
+String list and path list options
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+String lists are separated by ``,``. The strings are not parsed or interpreted
+by the option system itself. However, most path or file list options use ``:``
+(Unix) or ``;`` (Windows) as separator, instead of ``,``.
+
+They support the following operations:
+
+============= ===============================================
+Suffix Meaning
+============= ===============================================
+-set Set a list of items (using the list separator, escaped with backslash)
+-append Append single item (does not interpret escapes)
+-add Append 1 or more items (same syntax as -set)
+-pre Prepend 1 or more items (same syntax as -set)
+-clr Clear the option (remove all items)
+-remove Delete item if present (does not interpret escapes)
+-toggle Append an item, or remove if if it already exists (no escapes)
+============= ===============================================
+
+``-append`` is meant as a simple way to append a single item without having
+to escape the argument (you may still need to escape on the shell level).
+
+Key/value list options
+~~~~~~~~~~~~~~~~~~~~~~
+
+A key/value list is a list of key/value string pairs. In programming languages,
+this type of data structure is often called a map or a dictionary. The order
+normally does not matter, although in some cases the order might matter.
+
+They support the following operations:
+
+============= ===============================================
+Suffix Meaning
+============= ===============================================
+-set Set a list of items (using ``,`` as separator)
+-append Append a single item (escapes for the key, no escapes for the value)
+-add Append 1 or more items (same syntax as -set)
+-remove Delete item by key if present (does not interpret escapes)
+============= ===============================================
+
+Keys are unique within the list. If an already present key is set, the existing
+key is removed before the new value is appended.
+
+If you want to pass a value without interpreting it for escapes or ``,``, it is
+recommended to use the ``-append`` variant. When using libmpv, prefer using
+``MPV_FORMAT_NODE_MAP``; when using a scripting backend or the JSON IPC, use an
+appropriate structured data type.
+
+Prior to mpv 0.33, ``:`` was also recognized as separator by ``-set``.
+
+Filter options
+~~~~~~~~~~~~~~
+
+This is a very complex option type for the ``--af`` and ``--vf`` options only.
+They often require complicated escaping. See `VIDEO FILTERS`_ for details. They
+support the following operations:
+
+============= ===============================================
+Suffix Meaning
+============= ===============================================
+-set Set a list of filters (using ``,`` as separator)
+-append Append single filter
+-add Append 1 or more filters (same syntax as -set)
+-pre Prepend 1 or more filters (same syntax as -set)
+-clr Clear the option (remove all filters)
+-remove Delete filter if present
+-toggle Append a filter, or remove if if it already exists
+-help Pseudo operation that prints a help text to the terminal
+============= ===============================================
+
+General
+~~~~~~~
+
+Without suffix, the operation used is normally ``-set``.
+
+Although some operations allow specifying multiple items, using this is strongly
+discouraged and deprecated, except for ``-set``. There is a chance that
+operations like ``-add`` and ``-pre`` will work like ``-append`` and accept a
+single, unescaped item only (so the ``,`` separator will not be interpreted and
+is passed on as part of the value).
+
+Some options (like ``--sub-file``, ``--audio-file``, ``--glsl-shader``) are
+aliases for the proper option with ``-append`` action. For example,
+``--sub-file`` is an alias for ``--sub-files-append``.
+
+Options of this type can be changed at runtime using the ``change-list``
+command, which takes the suffix (without the ``-``) as separate operation
+parameter.
+
+CONFIGURATION FILES
+===================
+
+Location and Syntax
+-------------------
+
+You can put all of the options in configuration files which will be read every
+time mpv is run. The system-wide configuration file 'mpv.conf' is in your
+configuration directory (e.g. ``/etc/mpv`` or ``/usr/local/etc/mpv``), the
+user-specific one is ``~/.config/mpv/mpv.conf``. For details and platform
+specifics (in particular Windows paths) see the `FILES`_ section.
+
+User-specific options override system-wide options and options given on the
+command line override both. The syntax of the configuration files is
+``option=value``. Everything after a *#* is considered a comment. Options that
+work without values can be enabled by setting them to *yes* and disabled by
+setting them to *no*, and if the value is omitted, *yes* is implied. Even
+suboptions can be specified in this way.
+
+.. admonition:: Example configuration file
+
+ ::
+
+ # Don't allow new windows to be larger than the screen.
+ autofit-larger=100%x100%
+ # Enable hardware decoding if available, =yes is implied.
+ hwdec
+ # Spaces don't have to be escaped.
+ osd-playing-msg=File: ${filename}
+
+Escaping special characters
+--------------------------------------
+
+This is done like with command line options. A config entry can be quoted with
+``"``, ``'``, as well as with the fixed-length syntax (``%n%``) mentioned
+before. This is like passing the exact contents of the quoted string as a
+command line option. C-style escapes are currently _not_ interpreted on this
+level, although some options do this manually (this is a mess and should
+probably be changed at some point). The shell is not involved here, so option
+values only need to be quoted to escape ``#`` and ``\``, ``"``, ``'`` or ``%``
+at the beginning of the value, and leading and trailing whitespace.
+
+Putting Command Line Options into the Configuration File
+--------------------------------------------------------
+
+Almost all command line options can be put into the configuration file. Here
+is a small guide:
+
+======================= ========================
+Option Configuration file entry
+======================= ========================
+``--flag`` ``flag``
+``-opt val`` ``opt=val``
+``--opt=val`` ``opt=val``
+``-opt "has spaces"`` ``opt=has spaces``
+======================= ========================
+
+File-specific Configuration Files
+---------------------------------
+
+You can also write file-specific configuration files. If you wish to have a
+configuration file for a file called 'video.avi', create a file named
+'video.avi.conf' with the file-specific options in it and put it in
+``~/.config/mpv/``. You can also put the configuration file in the same directory
+as the file to be played. Both require you to set the ``--use-filedir-conf``
+option (either on the command line or in your global config file). If a
+file-specific configuration file is found in the same directory, no
+file-specific configuration is loaded from ``~/.config/mpv``. In addition, the
+``--use-filedir-conf`` option enables directory-specific configuration files.
+For this, mpv first tries to load a mpv.conf from the same directory
+as the file played and then tries to load any file-specific configuration.
+
+
+Profiles
+--------
+
+To ease working with different configurations, profiles can be defined in the
+configuration files. A profile starts with its name in square brackets,
+e.g. ``[my-profile]``. All following options will be part of the profile. A
+description (shown by ``--profile=help``) can be defined with the
+``profile-desc`` option. To end the profile, start another one or use the
+profile name ``default`` to continue with normal options.
+
+You can list profiles with ``--profile=help``, and show the contents of a
+profile with ``--show-profile=<name>`` (replace ``<name>`` with the profile
+name). You can apply profiles on start with the ``--profile=<name>`` option,
+or at runtime with the ``apply-profile <name>`` command.
+
+.. admonition:: Example mpv config file with profiles
+
+ ::
+
+ # normal top-level option
+ fullscreen=yes
+
+ # a profile that can be enabled with --profile=big-cache
+ [big-cache]
+ cache=yes
+ demuxer-max-bytes=123400KiB
+ demuxer-readahead-secs=20
+
+ [slow]
+ profile-desc="some profile name"
+ # reference a builtin profile
+ profile=high-quality
+
+ [fast]
+ vo=vdpau
+
+ # using a profile again extends it
+ [slow]
+ framedrop=no
+ # you can also include other profiles
+ profile=big-cache
+
+Runtime profiles
+----------------
+
+Profiles can be set at runtime with ``apply-profile`` command. Since this
+operation is "destructive" (every item in a profile is simply set as an
+option, overwriting the previous value), you can't just enable and disable
+profiles again.
+
+As a partial remedy, there is a way to make profiles save old option values
+before overwriting them with the profile values, and then restoring the old
+values at a later point using ``apply-profile <profile-name> restore``.
+
+This can be enabled with the ``profile-restore`` option, which takes one of
+the following options:
+
+ ``default``
+ Does nothing, and nothing can be restored (default).
+
+ ``copy``
+ When applying a profile, copy the old values of all profile options to a
+ backup before setting them from the profile. These options are reset to
+ their old values using the backup when restoring.
+
+ Every profile has its own list of backed up values. If the backup
+ already exists (e.g. if ``apply-profile name`` was called more than
+ once in a row), the existing backup is no changed. The restore operation
+ will remove the backup.
+
+ It's important to know that restoring does not "undo" setting an option,
+ but simply copies the old option value. Consider for example ``vf-add``,
+ appends an entry to ``vf``. This mechanism will simply copy the entire
+ ``vf`` list, and does _not_ execute the inverse of ``vf-add`` (that
+ would be ``vf-remove``) on restoring.
+
+ Note that if a profile contains recursive profiles (via the ``profile``
+ option), the options in these recursive profiles are treated as if they
+ were part of this profile. The referenced profile's backup list is not
+ used when creating or using the backup. Restoring a profile does not
+ restore referenced profiles, only the options of referenced profiles (as
+ if they were part of the main profile).
+
+ ``copy-equal``
+ Similar to ``copy``, but restore an option only if it has the same value
+ as the value effectively set by the profile. This tries to deal with
+ the situation when the user does not want the option to be reset after
+ interactively changing it.
+
+.. admonition:: Example
+
+ ::
+
+ [something]
+ profile-restore=copy-equal
+ vf-add=rotate=PI/2 # rotate by 90 degrees
+
+ Then running these commands will result in behavior as commented:
+
+ ::
+
+ set vf vflip
+ apply-profile something
+ vf add hflip
+ apply-profile something
+ # vf == vflip,rotate=PI/2,hflip,rotate=PI/2
+ apply-profile something restore
+ # vf == vflip
+
+Conditional auto profiles
+-------------------------
+
+Profiles which have the ``profile-cond`` option set are applied automatically
+if the associated condition matches (unless auto profiles are disabled). The
+option takes a string, which is interpreted as Lua expression. If the
+expression evaluates as truthy, the profile is applied. If the expression
+errors or evaluates as falsy, the profile is not applied. This Lua code
+execution is not sandboxed.
+
+Any variables in condition expressions can reference properties. If an
+identifier is not already defined by Lua or mpv, it is interpreted as property.
+For example, ``pause`` would return the current pause status. You cannot
+reference properties with ``-`` this way since that would denote a subtraction,
+but if the variable name contains any ``_`` characters, they are turned into
+``-``. For example, ``playback_time`` would return the property
+``playback-time``.
+
+A more robust way to access properties is using ``p.property_name`` or
+``get("property-name", default_value)``. The automatic variable to property
+magic will break if a new identifier with the same name is introduced (for
+example, if a function named ``pause()`` were added, ``pause`` would return a
+function value instead of the value of the ``pause`` property).
+
+Note that if a property is not available, it will return ``nil``, which can
+cause errors if used in expressions. These are logged in verbose mode, and the
+expression is considered to be false.
+
+Whenever a property referenced by a profile condition changes, the condition
+is re-evaluated. If the return value of the condition changes from falsy or
+error to truthy, the profile is applied.
+
+This mechanism tries to "unapply" profiles once the condition changes from
+truthy to falsy or error. If you want to use this, you need to set
+``profile-restore`` for the profile. Another possibility it to create another
+profile with an inverse condition to undo the other profile.
+
+Recursive profiles can be used. But it is discouraged to reference other
+conditional profiles in a conditional profile, since this can lead to tricky
+and unintuitive behavior.
+
+.. admonition:: Example
+
+ Make only HD video look funny:
+
+ ::
+
+ [something]
+ profile-desc=HD video sucks
+ profile-cond=width >= 1280
+ hue=-50
+
+ Make only videos containing "youtube" or "youtu.be" in their path brighter:
+
+ ::
+
+ [youtube]
+ profile-cond=path:find('youtu%.?be')
+ gamma=20
+
+ If you want the profile to be reverted if the condition goes to false again,
+ you can set ``profile-restore``:
+
+ ::
+
+ [something]
+ profile-desc=Mess up video when entering fullscreen
+ profile-cond=fullscreen
+ profile-restore=copy
+ vf-add=rotate=PI/2 # rotate by 90 degrees
+
+ This appends the ``rotate`` filter to the video filter chain when entering
+ fullscreen. When leaving fullscreen, the ``vf`` option is set to the value
+ it had before entering fullscreen. Note that this would also remove any
+ other filters that were added during fullscreen mode by the user. Avoiding
+ this is trickier, and could for example be solved by adding a second profile
+ with an inverse condition and operation:
+
+ ::
+
+ [something]
+ profile-cond=fullscreen
+ vf-add=@rot:rotate=PI/2
+
+ [something-inv]
+ profile-cond=not fullscreen
+ vf-remove=@rot
+
+.. warning::
+
+ Every time an involved property changes, the condition is evaluated again.
+ If your condition uses ``p.playback_time`` for example, the condition is
+ re-evaluated approximately on every video frame. This is probably slow.
+
+This feature is managed by an internal Lua script. Conditions are executed as
+Lua code within this script. Its environment contains at least the following
+things:
+
+``(function environment table)``
+ Every Lua function has an environment table. This is used for identifier
+ access. There is no named Lua symbol for it; it is implicit.
+
+ The environment does "magic" accesses to mpv properties. If an identifier
+ is not already defined in ``_G``, it retrieves the mpv property of the same
+ name. Any occurrences of ``_`` in the name are replaced with ``-`` before
+ reading the property. The returned value is as retrieved by
+ ``mp.get_property_native(name)``. Internally, a cache of property values,
+ updated by observing the property is used instead, so properties that are
+ not observable will be stuck at the initial value forever.
+
+ If you want to access properties, that actually contain ``_`` in the name,
+ use ``get()`` (which does not perform transliteration).
+
+ Internally, the environment table has a ``__index`` meta method set, which
+ performs the access logic.
+
+``p``
+ A "magic" table similar to the environment table. Unlike the latter, this
+ does not prefer accessing variables defined in ``_G`` - it always accesses
+ properties.
+
+``get(name [, def])``
+ Read a property and return its value. If the property value is ``nil`` (e.g.
+ if the property does not exist), ``def`` is returned.
+
+ This is superficially similar to ``mp.get_property_native(name)``. An
+ important difference is that this accesses the property cache, and enables
+ the change detection logic (which is essential to the dynamic runtime
+ behavior of auto profiles). Also, it does not return an error value as
+ second return value.
+
+ The "magic" tables mentioned above use this function as backend. It does not
+ perform the ``_`` transliteration.
+
+In addition, the same environment as in a blank mpv Lua script is present. For
+example, ``math`` is defined and gives access to the Lua standard math library.
+
+.. warning::
+
+ This feature is subject to change indefinitely. You might be forced to
+ adjust your profiles on mpv updates.
+
+Legacy auto profiles
+--------------------
+
+Some profiles are loaded automatically using a legacy mechanism. The following
+example demonstrates this:
+
+.. admonition:: Auto profile loading
+
+ ::
+
+ [extension.mkv]
+ profile-desc="profile for .mkv files"
+ vf=vflip
+
+The profile name follows the schema ``type.name``, where type can be
+``protocol`` for the input/output protocol in use (see ``--list-protocols``),
+and ``extension`` for the extension of the path of the currently played file
+(*not* the file format).
+
+This feature is very limited, and is considered soft-deprecated. Use conditional
+auto profiles.
+
+Using mpv from other programs or scripts
+========================================
+
+There are three choices for using mpv from other programs or scripts:
+
+ 1. Calling it as UNIX process. If you do this, *do not parse terminal output*.
+ The terminal output is intended for humans, and may change any time. In
+ addition, terminal behavior itself may change any time. Compatibility
+ cannot be guaranteed.
+
+ Your code should work even if you pass ``--no-terminal``. Do not attempt
+ to simulate user input by sending terminal control codes to mpv's stdin.
+ If you need interactive control, using ``--input-ipc-server`` is
+ recommended. This gives you access to the `JSON IPC`_ over unix domain
+ sockets (or named pipes on Windows).
+
+ Depending on what you do, passing ``--no-config`` or ``--config-dir`` may
+ be a good idea to avoid conflicts with the normal mpv user configuration
+ intended for CLI playback.
+
+ Using ``--input-ipc-server`` is also suitable for purposes like remote
+ control (however, the IPC protocol itself is not "secure" and not
+ intended to be so).
+
+ 2. Using libmpv. This is generally recommended when mpv is used as playback
+ backend for a completely different application. The provided C API is
+ very close to CLI mechanisms and the scripting API.
+
+ Note that even though libmpv has different defaults, it can be configured
+ to work exactly like the CLI player (except command line parsing is
+ unavailable).
+
+ See `EMBEDDING INTO OTHER PROGRAMS (LIBMPV)`_.
+
+ 3. As a user script (`LUA SCRIPTING`_, `JAVASCRIPT`_, `C PLUGINS`_). This is
+ recommended when the goal is to "enhance" the CLI player. Scripts get
+ access to the entire client API of mpv.
+
+ This is the standard way to create third-party extensions for the player.
+
+All these access the client API, which is the sum of the various mechanisms
+provided by the player core, as documented here: `OPTIONS`_,
+`List of Input Commands`_, `Properties`_, `List of events`_ (also see C API),
+`Hooks`_.
+
+TAKING SCREENSHOTS
+==================
+
+Screenshots of the currently played file can be taken using the 'screenshot'
+input mode command, which is by default bound to the ``s`` key. Files named
+``mpv-shotNNNN.jpg`` will be saved in the working directory, using the first
+available number - no files will be overwritten. In pseudo-GUI mode, the
+screenshot will be saved somewhere else. See `PSEUDO GUI MODE`_.
+
+A screenshot will usually contain the unscaled video contents at the end of the
+video filter chain and subtitles. By default, ``S`` takes screenshots without
+subtitles, while ``s`` includes subtitles.
+
+Unlike with MPlayer, the ``screenshot`` video filter is not required. This
+filter was never required in mpv, and has been removed.
+
+TERMINAL STATUS LINE
+====================
+
+During playback, mpv shows the playback status on the terminal. It looks like
+something like this:
+
+ ``AV: 00:03:12 / 00:24:25 (13%) A-V: -0.000``
+
+The status line can be overridden with the ``--term-status-msg`` option.
+
+The following is a list of things that can show up in the status line. Input
+properties, that can be used to get the same information manually, are also
+listed.
+
+- ``AV:`` or ``V:`` (video only) or ``A:`` (audio only)
+- The current time position in ``HH:MM:SS`` format (``playback-time`` property)
+- The total file duration (absent if unknown) (``duration`` property)
+- Playback speed, e.g. ``x2.0``. Only visible if the speed is not normal. This
+ is the user-requested speed, and not the actual speed (usually they should
+ be the same, unless playback is too slow). (``speed`` property.)
+- Playback percentage, e.g. ``(13%)``. How much of the file has been played.
+ Normally calculated out of playback position and duration, but can fallback
+ to other methods (like byte position) if these are not available.
+ (``percent-pos`` property.)
+- The audio/video sync as ``A-V: 0.000``. This is the difference between
+ audio and video time. Normally it should be 0 or close to 0. If it's growing,
+ it might indicate a playback problem. (``avsync`` property.)
+- Total A/V sync change, e.g. ``ct: -0.417``. Normally invisible. Can show up
+ if there is audio "missing", or not enough frames can be dropped. Usually
+ this will indicate a problem. (``total-avsync-change`` property.)
+- Encoding state in ``{...}``, only shown in encoding mode.
+- Display sync state. If display sync is active (``display-sync-active``
+ property), this shows ``DS: 2.500/13``, where the first number is average
+ number of vsyncs per video frame (e.g. 2.5 when playing 24Hz videos on 60Hz
+ screens), which might jitter if the ratio doesn't round off, or there are
+ mistimed frames (``vsync-ratio``), and the second number of estimated number
+ of vsyncs which took too long (``vo-delayed-frame-count`` property). The
+ latter is a heuristic, as it's generally not possible to determine this with
+ certainty.
+- Dropped frames, e.g. ``Dropped: 4``. Shows up only if the count is not 0. Can
+ grow if the video framerate is higher than that of the display, or if video
+ rendering is too slow. May also be incremented on "hiccups" and when the video
+ frame couldn't be displayed on time. (``frame-drop-count`` property.)
+ If the decoder drops frames, the number of decoder-dropped frames is appended
+ to the display as well, e.g.: ``Dropped: 4/34``. This happens only if
+ decoder frame dropping is enabled with the ``--framedrop`` options.
+ (``decoder-frame-drop-count`` property.)
+- Cache state, e.g. ``Cache: 2s/134KB``. Visible if the stream cache is enabled.
+ The first value shows the amount of video buffered in the demuxer in seconds,
+ the second value shows the estimated size of the buffered amount in kilobytes.
+ (``demuxer-cache-duration`` and ``demuxer-cache-state`` properties.)
+
+
+LOW LATENCY PLAYBACK
+====================
+
+mpv is optimized for normal video playback, meaning it actually tries to buffer
+as much data as it seems to make sense. This will increase latency. Reducing
+latency is possible only by specifically disabling features which increase
+latency.
+
+The builtin ``low-latency`` profile tries to apply some of the options which can
+reduce latency. You can use ``--profile=low-latency`` to apply all of them. You
+can list the contents with ``--show-profile=low-latency`` (some of the options
+are quite obscure, and may change every mpv release).
+
+Be aware that some of the options can reduce playback quality.
+
+Most latency is actually caused by inconvenient timing behavior. You can disable
+this with ``--untimed``, but it will likely break, unless the stream has no
+audio, and the input feeds data to the player at a constant rate.
+
+Another common problem is with MJPEG streams. These do not signal the correct
+framerate. Using ``--untimed`` or ``--no-correct-pts --container-fps-override=60``
+might help.
+
+For livestreams, data can build up due to pausing the stream, due to slightly
+lower playback rate, or "buffering" pauses. If the demuxer cache is enabled,
+these can be skipped manually. The experimental ``drop-buffers`` command can
+be used to discard any buffered data, though it's very disruptive.
+
+In some cases, manually tuning TCP buffer sizes and such can help to reduce
+latency.
+
+Additional options that can be tried:
+
+- ``--opengl-glfinish=yes``, can reduce buffering in the graphics driver
+- ``--opengl-swapinterval=0``, same
+- ``--vo=xv``, same
+- without audio ``--framedrop=no --speed=1.01`` may help for live sources
+ (results can be mixed)
+
+RESUMING PLAYBACK
+=================
+
+mpv is capable of storing the playback position of the currently playing file
+and resume from there the next time that file is played. This is done with the
+commands ``quit-watch-later`` (bound to Shift+Q by default) and
+``write-watch-later-config``, and with the ``--save-position-on-quit`` option.
+
+The difference between always quitting with a key bound to ``quit-watch-later``
+and using ``--save-position-on-quit`` is that the latter will save the playback
+position even when mpv is closed with a method other than a keybinding, for
+example if you shutdown your system without closing mpv beforehand, unless of
+course mpv is terminated abruptly and doesn't have the time to save (e.g. with
+the KILL Unix signal).
+
+mpv also stores options other than the playback position when they have been
+modified after playback began, for example the volume and selected audio/subtitles,
+and restores their values the next time the file is played. Which options are
+saved can be configured with the ``--watch-later-options`` option.
+
+When playing multiple playlist entries, mpv checks if one them has a resume
+config file associated, and if it finds one it restarts playback from it. For
+example, if you use ``quit-watch-later`` on the 5th episode of a show, and
+later play all the episodes, mpv will automatically resume playback from
+episode 5.
+
+More options to configure this functionality are listed in `Watch Later`_.
+
+PROTOCOLS
+=========
+
+``http://...``, ``https://``, ...
+
+ Many network protocols are supported, but the protocol prefix must always
+ be specified. mpv will never attempt to guess whether a filename is
+ actually a network address. A protocol prefix is always required.
+
+ Note that not all prefixes are documented here. Undocumented prefixes are
+ either aliases to documented protocols, or are just redirections to
+ protocols implemented and documented in FFmpeg.
+
+ ``data:`` is supported in FFmpeg (not in Libav), but needs to be in the
+ format ``data://``. This is done to avoid ambiguity with filenames. You
+ can also prefix it with ``lavf://`` or ``ffmpeg://``.
+
+``ytdl://...``
+
+ By default, the youtube-dl hook script only looks at http(s) URLs. Prefixing
+ an URL with ``ytdl://`` forces it to be always processed by the script. This
+ can also be used to invoke special youtube-dl functionality like playing a
+ video by ID or invoking search.
+
+ Keep in mind that you can't pass youtube-dl command line options by this,
+ and you have to use ``--ytdl-raw-options`` instead.
+
+``-``
+
+ Play data from stdin.
+
+``smb://PATH``
+
+ Play a path from Samba share. (Requires FFmpeg support.)
+
+``bd://[title][/device]`` ``--bluray-device=PATH``
+
+ Play a Blu-ray disc. Since libbluray 1.0.1, you can read from ISO files
+ by passing them to ``--bluray-device``.
+
+ ``title`` can be: ``longest`` or ``first`` (selects the default
+ playlist); ``mpls/<number>`` (selects <number>.mpls playlist);
+ ``<number>`` (select playlist with the same index). mpv will list
+ the available playlists on loading.
+
+ ``bluray://`` is an alias.
+
+``dvd://[title][/device]`` ``--dvd-device=PATH``
+
+ Play a DVD. DVD menus are not supported. If no title is given, the longest
+ title is auto-selected. Without ``--dvd-device``, it will probably try
+ to open an actual optical drive, if available and implemented for the OS.
+
+ ``dvdnav://`` is an old alias for ``dvd://`` and does exactly the same
+ thing.
+
+``dvb://[cardnumber@]channel`` ``--dvbin-...``
+
+ Digital TV via DVB. (Linux only.)
+
+``mf://[filemask|@listfile]`` ``--mf-...``
+
+ Play a series of images as video.
+
+``cdda://[device]`` ``--cdrom-device=PATH`` ``--cdda-...``
+
+ Play CD.
+
+``lavf://...``
+
+ Access any FFmpeg/Libav libavformat protocol. Basically, this passed the
+ string after the ``//`` directly to libavformat.
+
+``av://type:options``
+
+ This is intended for using libavdevice inputs. ``type`` is the libavdevice
+ demuxer name, and ``options`` is the (pseudo-)filename passed to the
+ demuxer.
+
+ .. admonition:: Example
+
+ ::
+
+ mpv av://v4l2:/dev/video0 --profile=low-latency --untimed
+
+ This plays video from the first v4l input with nearly the lowest latency
+ possible. It's a good replacement for the removed ``tv://`` input.
+ Using ``--untimed`` is a hack to output a captured frame immediately,
+ instead of respecting the input framerate. (There may be better ways to
+ handle this in the future.)
+
+ ``avdevice://`` is an alias.
+
+``file://PATH``
+
+ A local path as URL. Might be useful in some special use-cases. Note that
+ ``PATH`` itself should start with a third ``/`` to make the path an
+ absolute path.
+
+``appending://PATH``
+
+ Play a local file, but assume it's being appended to. This is useful for
+ example for files that are currently being downloaded to disk. This will
+ block playback, and stop playback only if no new data was appended after
+ a timeout of about 2 seconds.
+
+ Using this is still a bit of a bad idea, because there is no way to detect
+ if a file is actually being appended, or if it's still written. If you're
+ trying to play the output of some program, consider using a pipe
+ (``something | mpv -``). If it really has to be a file on disk, use tail to
+ make it wait forever, e.g. ``tail -f -c +0 file.mkv | mpv -``.
+
+``fd://123``
+
+ Read data from the given file descriptor (for example 123). This is similar
+ to piping data to stdin via ``-``, but can use an arbitrary file descriptor.
+ mpv may modify some file descriptor properties when the stream layer "opens"
+ it.
+
+``fdclose://123``
+
+ Like ``fd://``, but the file descriptor is closed after use. When using this
+ you need to ensure that the same fd URL will only be used once.
+
+``edl://[edl specification as in edl-mpv.rst]``
+
+ Stitch together parts of multiple files and play them.
+
+``slice://start[-end]@URL``
+
+ Read a slice of a stream.
+
+ ``start`` and ``end`` represent a byte range and accept
+ suffixes such as ``KiB`` and ``MiB``. ``end`` is optional.
+
+ if ``end`` starts with ``+``, it is considered as offset from ``start``.
+
+ Only works with seekable streams.
+
+ Examples::
+
+ mpv slice://1g-2g@cap.ts
+
+ This starts reading from cap.ts after seeking 1 GiB, then
+ reads until reaching 2 GiB or end of file.
+
+ mpv slice://1g-+2g@cap.ts
+
+ This starts reading from cap.ts after seeking 1 GiB, then
+ reads until reaching 3 GiB or end of file.
+
+ mpv slice://100m@appending://cap.ts
+
+ This starts reading from cap.ts after seeking 100MiB, then
+ reads until end of file.
+
+``null://``
+
+ Simulate an empty file. If opened for writing, it will discard all data.
+ The ``null`` demuxer will specifically pass autoprobing if this protocol
+ is used (while it's not automatically invoked for empty files).
+
+``memory://data``
+
+ Use the ``data`` part as source data.
+
+``hex://data``
+
+ Like ``memory://``, but the string is interpreted as hexdump.
+
+PSEUDO GUI MODE
+===============
+
+mpv has no official GUI, other than the OSC (`ON SCREEN CONTROLLER`_), which
+is not a full GUI and is not meant to be. However, to compensate for the lack
+of expected GUI behavior, mpv will in some cases start with some settings
+changed to behave slightly more like a GUI mode.
+
+Currently this happens only in the following cases:
+
+- if started using the ``mpv.desktop`` file on Linux (e.g. started from menus
+ or file associations provided by desktop environments)
+- if started from explorer.exe on Windows (technically, if it was started on
+ Windows, and all of the stdout/stderr/stdin handles are unset)
+- started out of the bundle on macOS
+- if you manually use ``--player-operation-mode=pseudo-gui`` on the command line
+
+This mode applies options from the builtin profile ``builtin-pseudo-gui``, but
+only if these haven't been set in the user's config file or on the command line,
+which is the main difference to using ``--profile=builtin-pseudo-gui``.
+
+The profile is currently defined as follows:
+
+::
+
+ [builtin-pseudo-gui]
+ terminal=no
+ force-window=yes
+ idle=once
+ screenshot-directory=~~desktop/
+
+The ``pseudo-gui`` profile exists for compatibility. The options in the
+``pseudo-gui`` profile are applied unconditionally. In addition, the profile
+makes sure to enable the pseudo-GUI mode, so that ``--profile=pseudo-gui``
+works like in older mpv releases:
+
+::
+
+ [pseudo-gui]
+ player-operation-mode=pseudo-gui
+
+.. warning::
+
+ Currently, you can extend the ``pseudo-gui`` profile in the config file the
+ normal way. This is deprecated. In future mpv releases, the behavior might
+ change, and not apply your additional settings, and/or use a different
+ profile name.
+
+Linux desktop issues
+====================
+
+This subsection describes common problems on the Linux desktop. None of these
+problems exist on systems like Windows or macOS.
+
+Disabling Screensaver
+---------------------
+
+By default, mpv tries to disable the OS screensaver during playback (only if
+a VO using the OS GUI API is active). ``--stop-screensaver=no`` disables this.
+
+A common problem is that Linux desktop environments ignore the standard
+screensaver APIs on which mpv relies. In particular, mpv uses the Screen Saver
+extension (XSS) on X11, and the idle-inhibit protocol on Wayland.
+
+Before mpv 0.33.0, the X11 backend ran ``xdg-screensaver reset`` in 10 second
+intervals when not paused in order to support screensaver inhibition in these
+environments. This functionality was removed in 0.33.0, but it is possible to
+call the ``xdg-screensaver`` command line program from a user script instead.
+
+
+.. include:: options.rst
+
+.. include:: ao.rst
+
+.. include:: vo.rst
+
+.. include:: af.rst
+
+.. include:: vf.rst
+
+.. include:: encode.rst
+
+.. include:: input.rst
+
+.. include:: osc.rst
+
+.. include:: stats.rst
+
+.. include:: console.rst
+
+.. include:: lua.rst
+
+.. include:: javascript.rst
+
+.. include:: ipc.rst
+
+.. include:: changes.rst
+
+.. include:: libmpv.rst
+
+ENVIRONMENT VARIABLES
+=====================
+
+There are a number of environment variables that can be used to control the
+behavior of mpv.
+
+``HOME``, ``XDG_CONFIG_HOME``
+ Used to determine mpv config directory. If ``XDG_CONFIG_HOME`` is not set,
+ ``$HOME/.config/mpv`` is used.
+
+ ``$HOME/.mpv`` is always added to the list of config search paths with a
+ lower priority.
+
+``MPV_HOME``
+ Directory where mpv looks for user settings. Overrides ``HOME``, and mpv
+ will try to load the config file as ``$MPV_HOME/mpv.conf``.
+
+``MPV_VERBOSE`` (see also ``-v`` and ``--msg-level``)
+ Set the initial verbosity level across all message modules (default: 0).
+ This is an integer, and the resulting verbosity corresponds to the number
+ of ``--v`` options passed to the command line.
+
+``MPV_LEAK_REPORT``
+ If set to ``1``, enable internal talloc leak reporting. If set to another
+ value, disable leak reporting. If unset, use the default, which normally is
+ ``0``. If mpv was built with ``--enable-ta-leak-report``, the default is
+ ``1``. If leak reporting was disabled at compile time (``NDEBUG`` in
+ custom ``CFLAGS``), this environment variable is ignored.
+
+``LADSPA_PATH``
+ Specifies the search path for LADSPA plugins. If it is unset, fully
+ qualified path names must be used.
+
+``DISPLAY``
+ Standard X11 display name to use.
+
+FFmpeg/Libav:
+ This library accesses various environment variables. However, they are not
+ centrally documented, and documenting them is not our job. Therefore, this
+ list is incomplete.
+
+ Notable environment variables:
+
+ ``http_proxy``
+ URL to proxy for ``http://`` and ``https://`` URLs.
+
+ ``no_proxy``
+ List of domain patterns for which no proxy should be used.
+ List entries are separated by ``,``. Patterns can include ``*``.
+
+libdvdcss:
+ ``DVDCSS_CACHE``
+ Specify a directory in which to store title key values. This will
+ speed up descrambling of DVDs which are in the cache. The
+ ``DVDCSS_CACHE`` directory is created if it does not exist, and a
+ subdirectory is created named after the DVD's title or manufacturing
+ date. If ``DVDCSS_CACHE`` is not set or is empty, libdvdcss will use
+ the default value which is ``${HOME}/.dvdcss/`` under Unix and
+ the roaming application data directory (``%APPDATA%``) under
+ Windows. The special value "off" disables caching.
+
+ ``DVDCSS_METHOD``
+ Sets the authentication and decryption method that libdvdcss will use
+ to read scrambled discs. Can be one of ``title``, ``key`` or ``disc``.
+
+ key
+ is the default method. libdvdcss will use a set of calculated
+ player keys to try to get the disc key. This can fail if the drive
+ does not recognize any of the player keys.
+
+ disc
+ is a fallback method when key has failed. Instead of using player
+ keys, libdvdcss will crack the disc key using a brute force
+ algorithm. This process is CPU intensive and requires 64 MB of
+ memory to store temporary data.
+
+ title
+ is the fallback when all other methods have failed. It does not
+ rely on a key exchange with the DVD drive, but rather uses a crypto
+ attack to guess the title key. On rare cases this may fail because
+ there is not enough encrypted data on the disc to perform a
+ statistical attack, but on the other hand it is the only way to
+ decrypt a DVD stored on a hard disc, or a DVD with the wrong region
+ on an RPC2 drive.
+
+ ``DVDCSS_RAW_DEVICE``
+ Specify the raw device to use. Exact usage will depend on your
+ operating system, the Linux utility to set up raw devices is raw(8)
+ for instance. Please note that on most operating systems, using a raw
+ device requires highly aligned buffers: Linux requires a 2048 bytes
+ alignment (which is the size of a DVD sector).
+
+ ``DVDCSS_VERBOSE``
+ Sets the libdvdcss verbosity level.
+
+ :0: Outputs no messages at all.
+ :1: Outputs error messages to stderr.
+ :2: Outputs error messages and debug messages to stderr.
+
+ ``DVDREAD_NOKEYS``
+ Skip retrieving all keys on startup. Currently disabled.
+
+ ``HOME``
+ FIXME: Document this.
+
+
+EXIT CODES
+==========
+
+Normally **mpv** returns 0 as exit code after finishing playback successfully.
+If errors happen, the following exit codes can be returned:
+
+ :1: Error initializing mpv. This is also returned if unknown options are
+ passed to mpv.
+ :2: The file passed to mpv couldn't be played. This is somewhat fuzzy:
+ currently, playback of a file is considered to be successful if
+ initialization was mostly successful, even if playback fails
+ immediately after initialization.
+ :3: There were some files that could be played, and some files which
+ couldn't (using the definition of success from above).
+ :4: Quit due to a signal, Ctrl+c in a VO window (by default), or from the
+ default quit key bindings in encoding mode.
+
+Note that quitting the player manually will always lead to exit code 0,
+overriding the exit code that would be returned normally. Also, the ``quit``
+input command can take an exit code: in this case, that exit code is returned.
+
+FILES
+=====
+
+Note that this section assumes Linux/BSD. On other platforms the paths may be different.
+For Windows-specifics, see `FILES ON WINDOWS`_ section.
+
+``/usr/local/etc/mpv/mpv.conf``
+ mpv system-wide settings (depends on ``--prefix`` passed to configure - mpv
+ in default configuration will use ``/usr/local/etc/mpv/`` as config
+ directory, while most Linux distributions will set it to ``/etc/mpv/``).
+
+``~/.cache/mpv``
+ The standard cache directory. Certain options within mpv may cause it to write
+ cache files to disk. This can be overridden by environment variables, in
+ ascending order:
+
+ :1: If ``$XDG_CACHE_HOME`` is set, then the derived cache directory
+ will be ``$XDG_CACHE_HOME/mpv``.
+ :2: If ``$MPV_HOME`` is set, then the derived cache directory will be
+ ``$MPV_HOME``.
+
+ If the directory does not exist, mpv will try to create it automatically.
+
+``~/.config/mpv``
+ The standard configuration directory. This can be overridden by environment
+ variables, in ascending order:
+
+ :1: If ``$XDG_CONFIG_HOME`` is set, then the derived configuration directory
+ will be ``$XDG_CONFIG_HOME/mpv``.
+ :2: If ``$MPV_HOME`` is set, then the derived configuration directory will be
+ ``$MPV_HOME``.
+
+ If this directory, nor the original configuration directory (see below) do
+ not exist, mpv tries to create this directory automatically.
+
+``~/.mpv/``
+ The original (pre 0.5.0) configuration directory. It will continue to be
+ read if present. If this directory is present and the standard configuration
+ directory is not present, then cache files and watch later config files will
+ also be written to this directory.
+
+ If both this directory and the standard configuration directory are
+ present, configuration will be read from both with the standard
+ configuration directory content taking precedence. However, you should
+ fully migrate to the standard directory and a warning will be shown in
+ this situation.
+
+``~/.config/mpv/mpv.conf``
+ mpv user settings (see `CONFIGURATION FILES`_ section)
+
+``~/.config/mpv/input.conf``
+ key bindings (see `INPUT.CONF`_ section)
+
+``~/.config/mpv/fonts.conf``
+ Fontconfig fonts.conf that is customized for mpv. You should include system
+ fonts.conf in this file or mpv would not know about fonts that you already
+ have in the system.
+
+ Only available when libass is built with fontconfig.
+
+``~/.config/mpv/subfont.ttf``
+ fallback subtitle font
+
+``~/.config/mpv/fonts/``
+ Default location for ``--sub-fonts-dir`` (see `Subtitles`_) and
+ ``--osd-fonts-dir`` (see `OSD`_).
+
+``~/.config/mpv/scripts/``
+ All files in this directory are loaded as if they were passed to the
+ ``--script`` option. They are loaded in alphabetical order.
+
+ The ``--load-scripts=no`` option disables loading these files.
+
+ See `Script location`_ for details.
+
+``~/.local/state/mpv/watch_later/``
+ Contains temporary config files needed for resuming playback of files with
+ the watch later feature. See for example the ``Q`` key binding, or the
+ ``quit-watch-later`` input command.
+
+ This can be overridden by environment variables, in ascending order:
+
+ :1: If ``$XDG_STATE_HOME`` is set, then the derived watch later directory
+ will be ``$XDG_STATE_HOME/mpv/watch_later``.
+ :2: If ``$MPV_HOME`` is set, then the derived watch later directory will be
+ ``$MPV_HOME/watch_later``.
+
+ Each file is a small config file which is loaded if the corresponding media
+ file is loaded. It contains the playback position and some (not necessarily
+ all) settings that were changed during playback. The filenames are hashed
+ from the full paths of the media files. It's in general not possible to
+ extract the media filename from this hash. However, you can set the
+ ``--write-filename-in-watch-later-config`` option, and the player will
+ add the media filename to the contents of the resume config file.
+
+``~/.config/mpv/script-opts/osc.conf``
+ This is loaded by the OSC script. See the `ON SCREEN CONTROLLER`_ docs
+ for details.
+
+ Other files in this directory are specific to the corresponding scripts
+ as well, and the mpv core doesn't touch them.
+
+FILES ON WINDOWS
+================
+
+On win32 (if compiled with MinGW, but not Cygwin), the default config file
+locations are different. They are generally located under ``%APPDATA%/mpv/``.
+For example, the path to mpv.conf is ``%APPDATA%/mpv/mpv.conf``, which maps to
+a system and user-specific path, for example
+
+ ``C:\users\USERNAME\AppData\Roaming\mpv\mpv.conf``
+
+You can find the exact path by running ``echo %APPDATA%\mpv\mpv.conf`` in cmd.exe.
+
+Other config files (such as ``input.conf``) are in the same directory. See the
+`FILES`_ section above.
+
+The cache directory is located at ``%LOCALAPPDATA%/mpv/cache``.
+
+The watch_later directory is located at ``%LOCALAPPDATA%/mpv/watch_later``.
+
+The environment variable ``$MPV_HOME`` completely overrides these, like on
+UNIX.
+
+If a directory named ``portable_config`` next to the mpv.exe exists, all
+config will be loaded from this directory only. Watch later config files and
+cache files are written to this directory as well. (This exists on Windows
+only and is redundant with ``$MPV_HOME``. However, since Windows is very
+scripting unfriendly, a wrapper script just setting ``$MPV_HOME``, like you
+could do it on other systems, won't work. ``portable_config`` is provided for
+convenience to get around this restriction.)
+
+Config files located in the same directory as ``mpv.exe`` are loaded with
+lower priority. Some config files are loaded only once, which means that
+e.g. of 2 ``input.conf`` files located in two config directories, only the
+one from the directory with higher priority will be loaded.
+
+A third config directory with the lowest priority is the directory named ``mpv``
+in the same directory as ``mpv.exe``. This used to be the directory with the
+highest priority, but is now discouraged to use and might be removed in the
+future.
+
+Note that mpv likes to mix ``/`` and ``\`` path separators for simplicity.
+kernel32.dll accepts this, but cmd.exe does not.
+
+FILES ON MACOS
+==============
+
+On macOS the watch later directory is located at ``~/.config/mpv/watch_later/``
+and the cache directory is set to ``~/Library/Caches/io.mpv/``. These directories
+can't be overwritten by enviroment variables.
+Everything else is the same as `FILES`_.
diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst
new file mode 100644
index 0000000..e0445d4
--- /dev/null
+++ b/DOCS/man/options.rst
@@ -0,0 +1,7377 @@
+OPTIONS
+=======
+
+Track Selection
+---------------
+
+``--alang=<languagecode[,languagecode,...]>``
+ Specify a priority list of audio languages to use, as IETF language tags.
+ Equivalent ISO 639-1 two-letter and ISO 639-2 three-letter codes are treated the same.
+ The first tag in the list whose language matches a track in the file will be used.
+ A track that matches more subtags will be preferred over one that matches fewer,
+ with preference given to earlier subtags over later ones. See also ``--aid``.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Examples
+
+ - ``mpv dvd://1 --alang=hu,en`` chooses the Hungarian language track
+ on a DVD and falls back on English if Hungarian is not available.
+ - ``mpv --alang=jpn example.mkv`` plays a Matroska file with Japanese
+ audio.
+
+``--slang=<languagecode[,languagecode,...]>``
+ Equivalent to ``--alang``, for subtitle tracks.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Examples
+
+ - ``mpv dvd://1 --slang=hu,en`` chooses the Hungarian subtitle track on
+ a DVD and falls back on English if Hungarian is not available.
+ - ``mpv --slang=jpn example.mkv`` plays a Matroska file with Japanese
+ subtitles.
+ - ``mpv --slang=pt-BR example.mkv`` plays a Matroska file with Brazilian
+ Portuguese subtitles if available, and otherwise any Portuguese subtitles.
+
+``--vlang=<...>``
+ Equivalent to ``--alang`` and ``--slang``, for video tracks.
+
+ This is a string list option. See `List Options`_ for details.
+
+``--aid=<ID|auto|no>``
+ Select audio track. ``auto`` selects the default, ``no`` disables audio.
+ See also ``--alang``. mpv normally prints available audio tracks on the
+ terminal when starting playback of a file.
+
+ ``--audio`` is an alias for ``--aid``.
+
+ ``--aid=no`` or ``--audio=no`` or ``--no-audio`` disables audio playback.
+ (The latter variant does not work with the client API.)
+
+ .. note::
+
+ The track selection options (``--aid`` but also ``--sid`` and the
+ others) sometimes expose behavior that may appear strange. Also, the
+ behavior tends to change around with each mpv release.
+
+ The track selection properties will return the option value outside of
+ playback (as expected), but during playback, the affective track
+ selection is returned. For example, with ``--aid=auto``, the ``aid``
+ property will suddenly return ``2`` after playback initialization
+ (assuming the file has at least 2 audio tracks, and the second is the
+ default).
+
+ At mpv 0.32.0 (and some releases before), if you passed a track value
+ for which a corresponding track didn't exist (e.g. ``--aid=2`` and there
+ was only 1 audio track), the ``aid`` property returned ``no``. However if
+ another audio track was added during playback, and you tried to set the
+ ``aid`` property to ``2``, nothing happened, because the ``aid`` option
+ still had the value ``2``, and writing the same value has no effect.
+
+ With mpv 0.33.0, the behavior was changed. Now track selection options
+ are reset to ``auto`` at playback initialization, if the option had
+ tries to select a track that does not exist. The same is done if the
+ track exists, but fails to initialize. The consequence is that unlike
+ before mpv 0.33.0, the user's track selection parameters are clobbered
+ in certain situations.
+
+ Also since mpv 0.33.0, trying to select a track by number will strictly
+ select this track. Before this change, trying to select a track which
+ did not exist would fall back to track default selection at playback
+ initialization. The new behavior is more consistent.
+
+ Setting a track selection property at runtime, and then playing a new
+ file might reset the track selection to defaults, if the fingerprint
+ of the track list of the new file is different.
+
+ Be aware of tricky combinations of all of all of the above: for example,
+ ``mpv --aid=2 file_with_2_audio_tracks.mkv file_with_1_audio_track.mkv``
+ would first play the correct track, and the second file without audio.
+ If you then go back the first file, its first audio track will be played,
+ and the second file is played with audio. If you do the same thing again
+ but instead of using ``--aid=2`` you run ``set aid 2`` while the file is
+ playing, then changing to the second file will play its audio track.
+ This is because runtime selection enables the fingerprint heuristic.
+
+ Most likely this is not the end.
+
+``--sid=<ID|auto|no>``
+ Display the subtitle stream specified by ``<ID>``. ``auto`` selects
+ the default, ``no`` disables subtitles.
+
+ ``--sub`` is an alias for ``--sid``.
+
+ ``--sid=no`` or ``--sub=no`` or ``--no-sub`` disables subtitle decoding.
+ (The latter variant does not work with the client API.)
+
+``--vid=<ID|auto|no>``
+ Select video channel. ``auto`` selects the default, ``no`` disables video.
+
+ ``--video`` is an alias for ``--vid``.
+
+ ``--vid=no`` or ``--video=no`` or ``--no-video`` disables video playback.
+ (The latter variant does not work with the client API.)
+
+ If video is disabled, mpv will try to download the audio only if media is
+ streamed with youtube-dl, because it saves bandwidth. This is done by
+ setting the ytdl_format to "bestaudio/best" in the ytdl_hook.lua script.
+
+``--edition=<ID|auto>``
+ (Matroska files only)
+ Specify the edition (set of chapters) to use, where 0 is the first. If set
+ to ``auto`` (the default), mpv will choose the first edition declared as a
+ default, or if there is no default, the first edition defined.
+
+``--track-auto-selection=<yes|no>``
+ Enable the default track auto-selection (default: yes). Enabling this will
+ make the player select streams according to ``--aid``, ``--alang``, and
+ others. If it is disabled, no tracks are selected. In addition, the player
+ will not exit if no tracks are selected, and wait instead (this wait mode
+ is similar to pausing, but the pause option is not set).
+
+ This is useful with ``--lavfi-complex``: you can start playback in this
+ mode, and then set select tracks at runtime by setting the filter graph.
+ Note that if ``--lavfi-complex`` is set before playback is started, the
+ referenced tracks are always selected.
+
+``--subs-with-matching-audio=<yes|no>``
+ When autoselecting a subtitle track, select a full/non-forced one even if the selected
+ audio stream matches your preferred subtitle language (default: yes). If this option is
+ set to ``no``, a non-forced subtitle track that matches the audio language will never be
+ autoselected by mpv regardless of the value of ``--slang`` or ``--subs-fallback``.
+
+``--subs-match-os-language=<yes|no>``
+ When autoselecting a subtitle track, select the track that matches the language of your OS
+ if the audio stream is in a different language if suitable (default track or a forced track
+ under the right conditions). Note that if ``-slang`` is set, this will be completely ignored
+ (default: yes).
+
+``--subs-fallback=<yes|default|no>``
+ When autoselecting a subtitle track, if no tracks match your preferred languages,
+ select a full track even if it doesn't match your preferred subtitle language (default: default).
+ Setting this to `default` means that only streams flagged as `default` will be selected.
+
+``--subs-fallback-forced=<yes|no|always>``
+ When autoselecting a subtitle track, the default value of `yes` will prefer using a forced
+ subtitle track if the subtitle language matches the audio language and matches your list of
+ preferred languages. The special value `always` will only select forced subtitle tracks and
+ never fallback on a non-forced track. Conversely, `no` will never select a forced subtitle
+ track.
+
+
+Playback Control
+----------------
+
+``--start=<relative time>``
+ Seek to given time position.
+
+ The general format for times is ``[+|-][[hh:]mm:]ss[.ms]``. If the time is
+ prefixed with ``-``, the time is considered relative from the end of the
+ file (as signaled by the demuxer/the file). A ``+`` is usually ignored (but
+ see below).
+
+ The following alternative time specifications are recognized:
+
+ ``pp%`` seeks to percent position pp (0-100).
+
+ ``#c`` seeks to chapter number c. (Chapters start from 1.)
+
+ ``none`` resets any previously set option (useful for libmpv).
+
+ If ``--rebase-start-time=no`` is given, then prefixing times with ``+``
+ makes the time relative to the start of the file. A timestamp without
+ prefix is considered an absolute time, i.e. should seek to a frame with a
+ timestamp as the file contains it. As a bug, but also a hidden feature,
+ putting 1 or more spaces before the ``+`` or ``-`` always interprets the
+ time as absolute, which can be used to seek to negative timestamps (useful
+ for debugging at most).
+
+ .. admonition:: Examples
+
+ ``--start=+56``, ``--start=00:56``
+ Seeks to the start time + 56 seconds.
+ ``--start=-56``, ``--start=-00:56``
+ Seeks to the end time - 56 seconds.
+ ``--start=01:10:00``
+ Seeks to 1 hour 10 min.
+ ``--start=50%``
+ Seeks to the middle of the file.
+ ``--start=30 --end=40``
+ Seeks to 30 seconds, plays 10 seconds, and exits.
+ ``--start=-3:20 --length=10``
+ Seeks to 3 minutes and 20 seconds before the end of the file, plays
+ 10 seconds, and exits.
+ ``--start='#2' --end='#4'``
+ Plays chapters 2 and 3, and exits.
+
+``--end=<relative time>``
+ Stop at given time. Use ``--length`` if the time should be relative
+ to ``--start``. See ``--start`` for valid option values and examples.
+
+``--length=<relative time>``
+ Stop after a given time relative to the start time.
+ See ``--start`` for valid option values and examples.
+
+ If both ``--end`` and ``--length`` are provided, playback will stop when it
+ reaches either of the two endpoints.
+
+ Obscurity note: this does not work correctly if ``--rebase-start-time=no``,
+ and the specified time is not an "absolute" time, as defined in the
+ ``--start`` option description.
+
+``--rebase-start-time=<yes|no>``
+ Whether to move the file start time to ``00:00:00`` (default: yes). This
+ is less awkward for files which start at a random timestamp, such as
+ transport streams. On the other hand, if there are timestamp resets, the
+ resulting behavior can be rather weird. For this reason, and in case you
+ are actually interested in the real timestamps, this behavior can be
+ disabled with ``no``.
+
+``--speed=<0.01-100>``
+ Slow down or speed up playback by the factor given as parameter.
+
+ If ``--audio-pitch-correction`` (on by default) is used, playing with a
+ speed higher than normal automatically inserts the ``scaletempo2`` audio
+ filter.
+
+``--pause``
+ Start the player in paused state.
+
+``--shuffle``
+ Play files in random order.
+
+``--playlist-start=<auto|index>``
+ Set which file on the internal playlist to start playback with. The index
+ is an integer, with 0 meaning the first file. The value ``auto`` means that
+ the selection of the entry to play is left to the playback resume mechanism
+ (default). If an entry with the given index doesn't exist, the behavior is
+ unspecified and might change in future mpv versions. The same applies if
+ the playlist contains further playlists (don't expect any reasonable
+ behavior). Passing a playlist file to mpv should work with this option,
+ though. E.g. ``mpv playlist.m3u --playlist-start=123`` will work as expected,
+ as long as ``playlist.m3u`` does not link to further playlists.
+
+ The value ``no`` is a deprecated alias for ``auto``.
+
+``--playlist=<filename>``
+ Play files according to a playlist file. Supports some common formats. If
+ no format is detected, it will be treated as list of files, separated by
+ newline characters. You may need this option to load plaintext files as
+ a playlist. Note that XML playlist formats are not supported.
+
+ This option forces ``--demuxer=playlist`` to interpret the playlist file.
+ Some playlist formats, notably CUE and optical disc formats, need to use
+ different demuxers and will not work with this option. They still can be
+ played directly, without using this option.
+
+ You can play playlists directly, without this option. Before mpv version
+ 0.31.0, this option disabled any security mechanisms that might be in
+ place, but since 0.31.0 it uses the same security mechanisms as playing a
+ playlist file directly. If you trust the playlist file, you can disable
+ any security checks with ``--load-unsafe-playlists``. Because playlists
+ can load other playlist entries, consider applying this option only to the
+ playlist itself and not its entries, using something along these lines:
+
+ ``mpv --{ --playlist=filename --load-unsafe-playlists --}``
+
+ .. warning::
+
+ The way older versions of mpv played playlist files via ``--playlist``
+ was not safe against maliciously constructed files. Such files may
+ trigger harmful actions. This has been the case for all versions of
+ mpv prior to 0.31.0, and all MPlayer versions, but unfortunately this
+ fact was not well documented earlier, and some people have even
+ misguidedly recommended the use of ``--playlist`` with untrusted
+ sources. Do NOT use ``--playlist`` with random internet sources or
+ files you do not trust if you are not sure your mpv is at least 0.31.0.
+
+ In particular, playlists can contain entries using protocols other than
+ local files, such as special protocols like ``avdevice://`` (which are
+ inherently unsafe).
+
+``--chapter-merge-threshold=<number>``
+ Threshold for merging almost consecutive ordered chapter parts in
+ milliseconds (default: 100). Some Matroska files with ordered chapters
+ have inaccurate chapter end timestamps, causing a small gap between the
+ end of one chapter and the start of the next one when they should match.
+ If the end of one playback part is less than the given threshold away from
+ the start of the next one then keep playing video normally over the
+ chapter change instead of doing a seek.
+
+``--chapter-seek-threshold=<seconds>``
+ Distance in seconds from the beginning of a chapter within which a backward
+ chapter seek will go to the previous chapter (default: 5.0). Past this
+ threshold, a backward chapter seek will go to the beginning of the current
+ chapter instead. A negative value means always go back to the previous
+ chapter.
+
+``--hr-seek=<no|absolute|yes|default>``
+ Select when to use precise seeks that are not limited to keyframes. Such
+ seeks require decoding video from the previous keyframe up to the target
+ position and so can take some time depending on decoding performance. For
+ some video formats, precise seeks are disabled. This option selects the
+ default choice to use for seeks; it is possible to explicitly override that
+ default in the definition of key bindings and in input commands.
+
+ :no: Never use precise seeks.
+ :absolute: Use precise seeks if the seek is to an absolute position in the
+ file, such as a chapter seek, but not for relative seeks like
+ the default behavior of arrow keys.
+ :default: Like ``absolute``, but enable hr-seeks in audio-only cases. The
+ exact behavior is implementation specific and may change with
+ new releases (default).
+ :yes: Use precise seeks whenever possible.
+ :always: Same as ``yes`` (for compatibility).
+
+``--hr-seek-demuxer-offset=<seconds>``
+ This option exists to work around failures to do precise seeks (as in
+ ``--hr-seek``) caused by bugs or limitations in the demuxers for some file
+ formats. Some demuxers fail to seek to a keyframe before the given target
+ position, going to a later position instead. The value of this option is
+ subtracted from the time stamp given to the demuxer. Thus, if you set this
+ option to 1.5 and try to do a precise seek to 60 seconds, the demuxer is
+ told to seek to time 58.5, which hopefully reduces the chance that it
+ erroneously goes to some time later than 60 seconds. The downside of
+ setting this option is that precise seeks become slower, as video between
+ the earlier demuxer position and the real target may be unnecessarily
+ decoded.
+
+``--hr-seek-framedrop=<yes|no>``
+ Allow the video decoder to drop frames during seek, if these frames are
+ before the seek target. If this is enabled, precise seeking can be faster,
+ but if you're using video filters which modify timestamps or add new
+ frames, it can lead to precise seeking skipping the target frame. This
+ e.g. can break frame backstepping when deinterlacing is enabled.
+
+ Default: ``yes``
+
+``--index=<mode>``
+ Controls how to seek in files. Note that if the index is missing from a
+ file, it will be built on the fly by default, so you don't need to change
+ this. But it might help with some broken files.
+
+ :default: use an index if the file has one, or build it if missing
+ :recreate: don't read or use the file's index
+
+ .. note::
+
+ This option only works if the underlying media supports seeking
+ (i.e. not with stdin, pipe, etc).
+
+``--load-unsafe-playlists``
+ Load URLs from playlists which are considered unsafe (default: no). This
+ includes special protocols and anything that doesn't refer to normal files.
+ Local files and HTTP links on the other hand are always considered safe.
+
+ In addition, if a playlist is loaded while this is set, the added playlist
+ entries are not marked as originating from network or potentially unsafe
+ location. (Instead, the behavior is as if the playlist entries were provided
+ directly to mpv command line or ``loadfile`` command.)
+
+``--access-references=<yes|no>``
+ Follow any references in the file being opened (default: yes). Disabling
+ this is helpful if the file is automatically scanned (e.g. thumbnail
+ generation). If the thumbnail scanner for example encounters a playlist
+ file, which contains network URLs, and the scanner should not open these,
+ enabling this option will prevent it. This option also disables ordered
+ chapters, mov reference files, opening of archives, and a number of other
+ features.
+
+ On older FFmpeg versions, this will not work in some cases. Some FFmpeg
+ demuxers might not respect this option.
+
+ This option does not prevent opening of paired subtitle files and such. Use
+ ``--autoload-files=no`` to prevent this.
+
+ This option does not always work if you open non-files (for example using
+ ``dvd://directory`` would open a whole bunch of files in the given
+ directory). Prefixing the filename with ``./`` if it doesn't start with
+ a ``/`` will avoid this.
+
+``--loop-playlist=<N|inf|force|no>``, ``--loop-playlist``
+ Loops playback ``N`` times. A value of ``1`` plays it one time (default),
+ ``2`` two times, etc. ``inf`` means forever. ``no`` is the same as ``1`` and
+ disables looping. If several files are specified on command line, the
+ entire playlist is looped. ``--loop-playlist`` is the same as
+ ``--loop-playlist=inf``.
+
+ The ``force`` mode is like ``inf``, but does not skip playlist entries
+ which have been marked as failing. This means the player might waste CPU
+ time trying to loop a file that doesn't exist. But it might be useful for
+ playing webradios under very bad network conditions.
+
+``--loop-file=<N|inf|no>``, ``--loop=<N|inf|no>``
+ Loop a single file N times. ``inf`` means forever, ``no`` means normal
+ playback. For compatibility, ``--loop-file`` and ``--loop-file=yes`` are
+ also accepted, and are the same as ``--loop-file=inf``.
+
+ The difference to ``--loop-playlist`` is that this doesn't loop the playlist,
+ just the file itself. If the playlist contains only a single file, the
+ difference between the two option is that this option performs a seek on
+ loop, instead of reloading the file.
+
+ .. note::
+
+ ``--loop-file`` counts the number of times it causes the player to
+ seek to the beginning of the file, not the number of full playthroughs. This
+ means ``--loop-file=1`` will end up playing the file twice. Contrast with
+ ``--loop-playlist``, which counts the number of full playthroughs.
+
+ ``--loop`` is an alias for this option.
+
+``--ab-loop-a=<time>``, ``--ab-loop-b=<time>``
+ Set loop points. If playback passes the ``b`` timestamp, it will seek to
+ the ``a`` timestamp. Seeking past the ``b`` point doesn't loop (this is
+ intentional).
+
+ If ``a`` is after ``b``, the behavior is as if the points were given in
+ the right order, and the player will seek to ``b`` after crossing through
+ ``a``. This is different from old behavior, where looping was disabled (and
+ as a bug, looped back to ``a`` on the end of the file).
+
+ If either options are set to ``no`` (or unset), looping is disabled. This
+ is different from old behavior, where an unset ``a`` implied the start of
+ the file, and an unset ``b`` the end of the file.
+
+ The loop-points can be adjusted at runtime with the corresponding
+ properties. See also ``ab-loop`` command.
+
+``--ab-loop-count=<N|inf>``
+ Run A-B loops only N times, then ignore the A-B loop points (default: inf).
+ Every finished loop iteration will decrement this option by 1 (unless it is
+ set to ``inf`` or 0). ``inf`` means that looping goes on forever. If this
+ option is set to 0, A-B looping is ignored, and even the ``ab-loop`` command
+ will not enable looping again (the command will show ``(disabled)`` on the
+ OSD message if both loop points are set, but ``ab-loop-count`` is 0).
+
+``--ordered-chapters``, ``--no-ordered-chapters``
+ Enabled by default.
+ Disable support for Matroska ordered chapters. mpv will not load or
+ search for video segments from other files, and will also ignore any
+ chapter order specified for the main file.
+
+``--ordered-chapters-files=<playlist-file>``
+ Loads the given file as playlist, and tries to use the files contained in
+ it as reference files when opening a Matroska file that uses ordered
+ chapters. This overrides the normal mechanism for loading referenced
+ files by scanning the same directory the main file is located in.
+
+ Useful for loading ordered chapter files that are not located on the local
+ filesystem, or if the referenced files are in different directories.
+
+ Note: a playlist can be as simple as a text file containing filenames
+ separated by newlines.
+
+``--chapters-file=<filename>``
+ Load chapters from this file, instead of using the chapter metadata found
+ in the main file.
+
+ This accepts a media file (like mkv) or even a pseudo-format like ffmetadata
+ and uses its chapters to replace the current file's chapters. This doesn't
+ work with OGM or XML chapters directly.
+
+``--sstep=<sec>``
+ Skip <sec> seconds after every frame.
+
+ .. note::
+
+ Without ``--hr-seek``, skipping will snap to keyframes.
+
+``--stop-playback-on-init-failure=<yes|no>``
+ Stop playback if either audio or video fails to initialize (default: no).
+ With ``no``, playback will continue in video-only or audio-only mode if one
+ of them fails. This doesn't affect playback of audio-only or video-only
+ files.
+
+``--play-direction=<forward|+|backward|->``
+ Control the playback direction (default: forward). Setting ``backward``
+ will attempt to play the file in reverse direction, with decreasing
+ playback time. If this is set on playback starts, playback will start from
+ the end of the file. If this is changed at during playback, a hr-seek will
+ be issued to change the direction.
+
+ ``+`` and ``-`` are aliases for ``forward`` and ``backward``.
+
+ The rest of this option description pertains to the ``backward`` mode.
+
+ .. note::
+
+ Backward playback is extremely fragile. It may not always work, is much
+ slower than forward playback, and breaks certain other features. How
+ well it works depends mainly on the file being played. Generally, it
+ will show good results (or results at all) only if the stars align.
+
+ mpv, as well as most media formats, were designed for forward playback
+ only. Backward playback is bolted on top of mpv, and tries to make a medium
+ effort to make backward playback work. Depending on your use-case, another
+ tool may work much better.
+
+ Backward playback is not exactly a 1st class feature. Implementation
+ tradeoffs were made, that are bad for backward playback, but in turn do not
+ cause disadvantages for normal playback. Various possible optimizations are
+ not implemented in order to keep the complexity down. Normally, a media
+ player is highly pipelined (future data is prepared in separate threads, so
+ it is available in realtime when the next stage needs it), but backward
+ playback will essentially stall the pipeline at various random points.
+
+ For example, for intra-only codecs are trivially backward playable, and
+ tools built around them may make efficient use of them (consider video
+ editors or camera viewers). mpv won't be efficient in this case, because it
+ uses its generic backward playback algorithm, that on top of it is not very
+ optimized.
+
+ If you just want to quickly go backward through the video and just show
+ "keyframes", just use forward playback, and hold down the left cursor key
+ (which on CLI with default config sends many small relative seek commands).
+
+ The implementation consists of mostly 3 parts:
+
+ - Backward demuxing. This relies on the demuxer cache, so the demuxer cache
+ should (or must, didn't test it) be enabled, and its size will affect
+ performance. If the cache is too small or too large, quadratic runtime
+ behavior may result.
+
+ - Backward decoding. The decoder library used (libavcodec) does not support
+ this. It is emulated by feeding bits of data in forward, putting the
+ result in a queue, returning the queue data to the VO in reverse, and
+ then starting over at an earlier position. This can require buffering an
+ extreme amount of decoded data, and also completely breaks pipelining.
+
+ - Backward output. This is relatively simple, because the decoder returns
+ the frames in the needed order. However, this may cause various problems
+ because filters see audio and video going backward.
+
+ Known problems:
+
+ - It's fragile. If anything doesn't work, random non-useful behavior may
+ occur. In simple cases, the player will just play nonsense and artifacts.
+ In other cases, it may get stuck or heat the CPU. (Exceeding memory usage
+ significantly beyond the user-set limits would be a bug, though.)
+
+ - Performance and resource usage isn't good. In part this is inherent to
+ backward playback of normal media formats, and in parts due to
+ implementation choices and tradeoffs.
+
+ - This is extremely reliant on good demuxer behavior. Although backward
+ demuxing requires no special demuxer support, it is required that the
+ demuxer performs seeks reliably, fulfills some specific requirements
+ about packet metadata, and has deterministic behavior.
+
+ - Starting playback exactly from the end may or may not work, depending on
+ seeking behavior and file duration detection.
+
+ - Some container formats, audio, and video codecs are not supported due to
+ their behavior. There is no list, and the player usually does not detect
+ them. Certain live streams (including TV captures) may exhibit problems
+ in particular, as well as some lossy audio codecs. h264 intra-refresh is
+ known not to work due to problems with libavcodec. WAV and some other raw
+ audio formats tend to have problems - there are hacks for dealing with
+ them, which may or may not work.
+
+ - Backward demuxing of subtitles is not supported. Subtitle display still
+ works for some external text subtitle formats. (These are fully read into
+ memory, and only backward display is needed.) Text subtitles that are
+ cached in the subtitle renderer also have a chance to be displayed
+ correctly.
+
+ - Some features dealing with playback of broken or hard to deal with files
+ will not work fully (such as timestamp correction).
+
+ - If demuxer low level seeks (i.e. seeking the actual demuxer instead of
+ just within the demuxer cache) are performed by backward playback, the
+ created seek ranges may not join, because not enough overlap is achieved.
+
+ - Trying to use this with hardware video decoding will probably exhaust all
+ your GPU memory and then crash a thing or two. Or it will fail because
+ ``--hwdec-extra-frames`` will certainly be set too low.
+
+ - Stream recording is broken. ``--stream-record`` may keep working if you
+ backward play within a cached region only.
+
+ - Relative seeks may behave weird. Small seeks backward (towards smaller
+ time, i.e. ``seek -1``) may not really seek properly, and audio will
+ remain muted for a while. Using hr-seek is recommended, which should have
+ none of these problems.
+
+ - Some things are just weird. For example, while seek commands manipulate
+ playback time in the expected way (provided they work correctly), the
+ framestep commands are transposed. Backstepping will perform very
+ expensive work to step forward by 1 frame.
+
+ Tuning:
+
+ - Remove all ``--vf``/``--af`` filters you have set. Disable hardware
+ decoding. Disable functions like SPDIF passthrough.
+
+ - Increasing ``--video-reversal-buffer`` might help if reversal queue
+ overflow is reported, which may happen in high bitrate video, or video
+ with large GOP. Hardware decoding mostly ignores this, and you need to
+ increase ``--hwdec-extra-frames`` instead (until you get playback without
+ logged errors).
+
+ - The demuxer cache is essential for backward demuxing. Make sure to set
+ ``--cache=yes``. The cache size might matter. If it's too small, a queue
+ overflow will be logged, and backward playback cannot continue, or it
+ performs too many low level seeks. If it's too large, implementation
+ tradeoffs may cause general performance issues. Use
+ ``--demuxer-max-bytes`` to potentially increase the amount of packets the
+ demuxer layer can queue for reverse demuxing (basically it's the
+ ``--video-reversal-buffer`` equivalent for the demuxer layer).
+
+ - Setting ``--vd-queue-enable=yes`` can help a lot to make playback smooth
+ (once it works).
+
+ - ``--demuxer-backward-playback-step`` also factors into how many seeks may
+ be performed, and whether backward demuxing could break due to queue
+ overflow. If it's set too high, the backstep operation needs to search
+ through more packets all the time, even if the cache is large enough.
+
+ - Setting ``--demuxer-cache-wait`` may be useful to cache the entire file
+ into the demuxer cache. Set ``--demuxer-max-bytes`` to a large size to
+ make sure it can read the entire cache; ``--demuxer-max-back-bytes``
+ should also be set to a large size to prevent that tries to trim the
+ cache.
+
+ - If audio artifacts are audible, even though the AO does not underrun,
+ increasing ``--audio-backward-overlap`` might help in some cases.
+
+``--video-reversal-buffer=<bytesize>``, ``--audio-reversal-buffer=<bytesize>``
+ For backward decoding. Backward decoding decodes forward in steps, and then
+ reverses the decoder output. These options control the approximate maximum
+ amount of bytes that can be buffered. The main use of this is to avoid
+ unbounded resource usage; during normal backward playback, it's not supposed
+ to hit the limit, and if it does, it will drop frames and complain about it.
+
+ Use this option if you get reversal queue overflow errors during backward
+ playback. Increase the size until the warning disappears. Usually, the video
+ buffer will overflow first, especially if it's high resolution video.
+
+ This does not work correctly if video hardware decoding is used. The video
+ frame size will not include the referenced GPU and driver memory. Some
+ hardware decoders may also be limited by ``--hwdec-extra-frames``.
+
+ How large the queue size needs to be depends entirely on the way the media
+ was encoded. Audio typically requires a very small buffer, while video can
+ require excessively large buffers.
+
+ (Technically, this allows the last frame to exceed the limit. Also, this
+ does not account for other buffered frames, such as inside the decoder or
+ the video output.)
+
+ This does not affect demuxer cache behavior at all.
+
+ See ``--list-options`` for defaults and value range. ``<bytesize>`` options
+ accept suffixes such as ``KiB`` and ``MiB``.
+
+``--video-backward-overlap=<auto|number>``, ``--audio-backward-overlap=<auto|number>``
+ Number of overlapping keyframe ranges to use for backward decoding (default:
+ auto) ("keyframe" to be understood as in the mpv/ffmpeg specific meaning).
+ Backward decoding works by forward decoding in small steps. Some codecs
+ cannot restart decoding from any packet (even if it's marked as seek point),
+ which becomes noticeable with backward decoding (in theory this is a problem
+ with seeking too, but ``--hr-seek-demuxer-offset`` can fix it for seeking).
+ In particular, MDCT based audio codecs are affected.
+
+ The solution is to feed a previous packet to the decoder each time, and then
+ discard the output. This option controls how many packets to feed. The
+ ``auto`` choice is currently hardcoded to 0 for video, and uses 1 for lossy
+ audio, 0 for lossless audio. For some specific lossy audio codecs, this is
+ set to 2.
+
+ ``--video-backward-overlap`` can potentially handle intra-refresh video,
+ depending on the exact conditions. You may have to use the
+ ``--vd-lavc-show-all`` option as well.
+
+``--video-backward-batch=<number>``, ``--audio-backward-batch=<number>``
+ Number of keyframe ranges to decode at once when backward decoding (default:
+ 1 for video, 10 for audio). Another pointless tuning parameter nobody should
+ use. This should affect performance only. In theory, setting a number higher
+ than 1 for audio will reduce overhead due to less frequent backstep
+ operations and less redundant decoding work due to fewer decoded overlap
+ frames (see ``--audio-backward-overlap``). On the other hand, it requires
+ a larger reversal buffer, and could make playback less smooth due to
+ breaking pipelining (e.g. by decoding a lot, and then doing nothing for a
+ while).
+
+ It probably never makes sense to set ``--video-backward-batch``. But in
+ theory, it could help with intra-only video codecs by reducing backstep
+ operations.
+
+``--demuxer-backward-playback-step=<seconds>``
+ Number of seconds the demuxer should seek back to get new packets during
+ backward playback (default: 60). This is useful for tuning backward
+ playback, see ``--play-direction`` for details.
+
+ Setting this to a very low value or 0 may make the player think seeking is
+ broken, or may make it perform multiple seeks.
+
+ Setting this to a high value may lead to quadratic runtime behavior.
+
+Program Behavior
+----------------
+
+``--help``, ``--h``
+ Show short summary of options.
+
+ You can also pass a string to this option, which will list all top-level
+ options which contain the string in the name, e.g. ``--h=scale`` for all
+ options that contain the word ``scale``. The special string ``*`` lists
+ all top-level options.
+
+``-v``
+ Increment verbosity level, one level for each ``-v`` found on the command
+ line.
+
+``--version, -V``
+ Print version string and exit.
+
+``--no-config``
+ Do not load default configuration or any user files. This prevents loading of
+ both the user-level and system-wide ``mpv.conf`` and ``input.conf`` files. Other
+ user files are blocked as well, such as resume playback files and cache files.
+ This option only takes effect when used as a command line flag.
+
+ .. note::
+
+ Files explicitly requested by command line options, like
+ ``--include`` or ``--use-filedir-conf``, will still be loaded.
+
+ See also: ``--config-dir``.
+
+``--list-options``
+ Prints all available options.
+
+``--list-properties``
+ Print a list of the available properties.
+
+``--list-protocols``
+ Print a list of the supported protocols.
+
+``--log-file=<path>``
+ Opens the given path for writing, and print log messages to it. Existing
+ files will be truncated. The log level is at least ``-v -v``, but
+ can be raised via ``--msg-level`` (the option cannot lower it below the
+ forced minimum log level).
+
+ A special case is the macOS bundle, it will create a log file at
+ ``~/Library/Logs/mpv.log`` by default.
+
+``--config-dir=<path>``
+ Force a different configuration directory. If this is set, the given
+ directory is used to load configuration files, and all other configuration
+ directories are ignored. This means the global mpv configuration directory
+ as well as per-user directories are ignored, and overrides through
+ environment variables (``MPV_HOME``) are also ignored.
+
+ Note that the cache and state paths (``~~/cache``, ``~~/state``) are not
+ considered "configuration" and keep their auto-detection logic.
+
+ Note that the ``--no-config`` option takes precedence over this option.
+
+``--dump-stats=<filename>``
+ Write certain statistics to the given file. The file is truncated on
+ opening. The file will contain raw samples, each with a timestamp. To
+ make this file into a readable, the script ``TOOLS/stats-conv.py`` can be
+ used (which currently displays it as a graph).
+
+ This option is useful for debugging only.
+
+``--idle=<no|yes|once>``
+ Makes mpv wait idly instead of quitting when there is no file to play.
+ Mostly useful in input mode, where mpv can be controlled through input
+ commands. (Default: ``no``)
+
+ ``once`` will only idle at start and let the player close once the
+ first playlist has finished playing back.
+
+``--include=<configuration-file>``
+ Specify configuration file to be parsed after the default ones.
+
+``--load-scripts=<yes|no>``
+ If set to ``no``, don't auto-load scripts from the ``scripts``
+ configuration subdirectory (usually ``~/.config/mpv/scripts/``).
+ (Default: ``yes``)
+
+``--script=<filename>``, ``--scripts=file1.lua:file2.lua:...``
+ Load a Lua script. The second option allows you to load multiple scripts by
+ separating them with the path separator (``:`` on Unix, ``;`` on Windows).
+
+ ``--scripts`` is a path list option. See `List Options`_ for details.
+
+``--script-opts=key1=value1,key2=value2,...``
+ Set options for scripts. A script can query an option by key. If an
+ option is used and what semantics the option value has depends entirely on
+ the loaded scripts. Values not claimed by any scripts are ignored.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+``--merge-files``
+ Pretend that all files passed to mpv are concatenated into a single, big
+ file. This uses timeline/EDL support internally.
+
+``--profile=<profile1,profile2,...>``
+ Use the given profile(s), ``--profile=help`` displays a list of the
+ defined profiles.
+
+``--reset-on-next-file=<all|option1,option2,...>``
+ Normally, mpv will try to keep all settings when playing the next file on
+ the playlist, even if they were changed by the user during playback. (This
+ behavior is the opposite of MPlayer's, which tries to reset all settings
+ when starting next file.)
+
+ Default: Do not reset anything.
+
+ This can be changed with this option. It accepts a list of options, and
+ mpv will reset the value of these options on playback start to the initial
+ value. The initial value is either the default value, or as set by the
+ config file or command line.
+
+ The special name ``all`` resets as many options as possible.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Examples
+
+ - ``--reset-on-next-file=pause``
+ Reset pause mode when switching to the next file.
+ - ``--reset-on-next-file=fullscreen,speed``
+ Reset fullscreen and playback speed settings if they were changed
+ during playback.
+ - ``--reset-on-next-file=all``
+ Try to reset all settings that were changed during playback.
+
+``--show-profile=<profile>``
+ Show the description and content of a profile. Lists all profiles if no
+ parameter is provided.
+
+``--use-filedir-conf``
+ Look for a file-specific configuration file in the same directory as the
+ file that is being played. See `File-specific Configuration Files`_.
+
+ .. warning::
+
+ May be dangerous if playing from untrusted media.
+
+``--ytdl``, ``--no-ytdl``
+ Enable the youtube-dl hook-script. It will look at the input URL, and will
+ play the video located on the website. This works with many streaming sites,
+ not just the one that the script is named after. This requires a recent
+ version of youtube-dl to be installed on the system. (Enabled by default.)
+
+ If the script can't do anything with an URL, it will do nothing.
+
+ This accepts a set of options, which can be passed to it with the
+ ``--script-opts`` option (using ``ytdl_hook-`` as prefix):
+
+ ``try_ytdl_first=<yes|no>``
+ If 'yes' will try parsing the URL with youtube-dl first, instead of the
+ default where it's only after mpv failed to open it. This mostly depends
+ on whether most of your URLs need youtube-dl parsing.
+
+ ``exclude=<URL1|URL2|...``
+ A ``|``-separated list of URL patterns which mpv should not use with
+ youtube-dl. The patterns are matched after the ``http(s)://`` part of
+ the URL.
+
+ ``^`` matches the beginning of the URL, ``$`` matches its end, and you
+ should use ``%`` before any of the characters ``^$()%|,.[]*+-?`` to
+ match that character.
+
+ .. admonition:: Examples
+
+ - ``--script-opts=ytdl_hook-exclude='^youtube%.com'``
+ will exclude any URL that starts with ``http://youtube.com`` or
+ ``https://youtube.com``.
+ - ``--script-opts=ytdl_hook-exclude='%.mkv$|%.mp4$'``
+ will exclude any URL that ends with ``.mkv`` or ``.mp4``.
+
+ See more lua patterns here: https://www.lua.org/manual/5.1/manual.html#5.4.1
+
+ ``all_formats=<yes|no>``
+ If 'yes' will attempt to add all formats found reported by youtube-dl
+ (default: no). Each format is added as a separate track. In addition,
+ they are delay-loaded, and actually opened only when a track is selected
+ (this should keep load times as low as without this option).
+
+ It adds average bitrate metadata, if available, which means you can use
+ ``--hls-bitrate`` to decide which track to select. (HLS used to be the
+ only format whose alternative quality streams were exposed in a similar
+ way, thus the option name.)
+
+ Tracks which represent formats that were selected by youtube-dl as
+ default will have the default flag set. This means mpv should generally
+ still select formats chosen with ``--ytdl-format`` by default.
+
+ Although this mechanism makes it possible to switch streams at runtime,
+ it's not suitable for this purpose for various technical reasons. (It's
+ slow, which can't be really fixed.) In general, this option is not
+ useful, and was only added to show that it's possible.
+
+ There are two cases that must be considered when doing quality/bandwidth
+ selection:
+
+ 1. Completely separate audio and video streams (DASH-like). Each of
+ these streams contain either only audio or video, so you can
+ mix and combine audio/video bandwidths without restriction. This
+ intuitively matches best with the concept of selecting quality
+ by track (what ``all_formats`` is supposed to do).
+
+ 2. Separate sets of muxed audio and video streams. Each version of
+ the media contains both an audio and video stream, and they are
+ interleaved. In order not to waste bandwidth, you should only
+ select one of these versions (if, for example, you select an
+ audio stream, then video will be downloaded, even if you selected
+ video from a different stream).
+
+ mpv will still represent them as separate tracks, but will set
+ the title of each track to ``muxed-N``, where ``N`` is replaced
+ with the youtube-dl format ID of the originating stream.
+
+ Some sites will mix 1. and 2., but we assume that they do so for
+ compatibility reasons, and there is no reason to use them at all.
+
+ ``force_all_formats=<yes|no>``
+ If set to 'yes', and ``all_formats`` is also set to 'yes', this will
+ try to represent all youtube-dl reported formats as tracks, even if
+ mpv would normally use the direct URL reported by it (default: yes).
+
+ It appears this normally makes a difference if youtube-dl works on a
+ master HLS playlist.
+
+ If this is set to 'no', this specific kind of stream is treated like
+ ``all_formats`` is set to 'no', and the stream selection as done by
+ youtube-dl (via ``--ytdl-format``) is used.
+
+ ``thumbnails=<all|best|none>``
+ Add thumbnails as video tracks (default: none).
+
+ Thumbnails get downloaded when they are added as tracks, so 'all' can
+ have a noticable impact on how long it takes to open the video when
+ there are a lot of thumbnails.
+
+ ``use_manifests=<yes|no>``
+ Make mpv use the master manifest URL for formats like HLS and DASH,
+ if available, allowing for video/audio selection in runtime (default:
+ no). It's disabled ("no") by default for performance reasons.
+
+ ``ytdl_path=youtube-dl``
+ Configure paths to youtube-dl's executable or a compatible fork's. The
+ paths should be separated by : on Unix and ; on Windows. mpv looks in
+ order for the configured paths in PATH and in mpv's config directory.
+ The defaults are "yt-dlp", "yt-dlp_x86" and "youtube-dl". On Windows
+ the suffix extension is not necessary, but only ".exe" is acceptable.
+
+ .. admonition:: Why do the option names mix ``_`` and ``-``?
+
+ I have no idea.
+
+``--ytdl-format=<ytdl|best|worst|mp4|webm|...>``
+ Video format/quality that is directly passed to youtube-dl. The possible
+ values are specific to the website and the video, for a given url the
+ available formats can be found with the command
+ ``youtube-dl --list-formats URL``. See youtube-dl's documentation for
+ available aliases.
+ (Default: ``bestvideo+bestaudio/best``)
+
+ The ``ytdl`` value does not pass a ``--format`` option to youtube-dl at all,
+ and thus does not override its default. Note that sometimes youtube-dl
+ returns a format that mpv cannot use, and in these cases the mpv default
+ may work better.
+
+``--ytdl-raw-options=<key>=<value>[,<key>=<value>[,...]]``
+ Pass arbitrary options to youtube-dl. Parameter and argument should be
+ passed as a key-value pair. Options without argument must include ``=``.
+
+ There is no sanity checking so it's possible to break things (i.e.
+ passing invalid parameters to youtube-dl).
+
+ A proxy URL can be passed for youtube-dl to use it in parsing the website.
+ This is useful for geo-restricted URLs. After youtube-dl parsing, some
+ URLs also require a proxy for playback, so this can pass that proxy
+ information to mpv. Take note that SOCKS proxies aren't supported and
+ https URLs also bypass the proxy. This is a limitation in FFmpeg.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ - ``--ytdl-raw-options=username=user,password=pass``
+ - ``--ytdl-raw-options=force-ipv6=``
+ - ``--ytdl-raw-options=proxy=[http://127.0.0.1:3128]``
+ - ``--ytdl-raw-options-append=proxy=http://127.0.0.1:3128``
+
+``--js-memory-report=<yes|no>``
+ Enable memory reporting for javascript scripts in the stats overlay.
+ This is disabled by default because it has an overhead and increases
+ memory usage. This option will only work if it is enabled before mpv is
+ started.
+
+``--load-stats-overlay=<yes|no>``
+ Enable the builtin script that shows useful playback information on a key
+ binding (default: yes). By default, the ``i`` key is used (``I`` to make
+ the overlay permanent).
+
+``--load-osd-console=<yes|no>``
+ Enable the built-in script that shows a console on a key binding and lets
+ you enter commands (default: yes). The ````` key is used to show the
+ console by default, and ``ESC`` to hide it again.
+
+``--load-auto-profiles=<yes|no|auto>``
+ Enable the builtin script that does auto profiles (default: auto). See
+ `Conditional auto profiles`_ for details. ``auto`` will load the script,
+ but immediately unload it if there are no conditional profiles.
+
+``--player-operation-mode=<cplayer|pseudo-gui>``
+ For enabling "pseudo GUI mode", which means that the defaults for some
+ options are changed. This option should not normally be used directly, but
+ only by mpv internally, or mpv-provided scripts, config files, or .desktop
+ files. See `PSEUDO GUI MODE`_ for details.
+
+Watch Later
+-----------
+
+``--save-position-on-quit``
+ Always save the current playback position on quit. When this file is
+ played again later, the player will seek to the old playback position on
+ start. This does not happen if playback of a file is stopped in any other
+ way than quitting. For example, going to the next file in the playlist
+ will not save the position, and start playback at beginning the next time
+ the file is played.
+
+ This behavior is disabled by default, but is always available when quitting
+ the player with Shift+Q.
+
+ See `RESUMING PLAYBACK`_.
+
+``--watch-later-dir=<path>``
+ The directory in which to store the "watch later" temporary files.
+
+ ``--watch-later-directory`` is an alias for ``--watch-later-dir``.
+
+ If this option is unset, the files will be stored in a subdirectory
+ named "watch_later" underneath the local state directory
+ (usually ``~/.local/state/mpv/``).
+
+``--no-resume-playback``
+ Do not restore playback position from the ``watch_later`` configuration
+ subdirectory (usually ``~/.config/mpv/watch_later/``).
+
+``--resume-playback-check-mtime``
+ Only restore the playback position from the ``watch_later`` configuration
+ subdirectory (usually ``~/.config/mpv/watch_later/``) if the file's
+ modification time is the same as at the time of saving. This may prevent
+ skipping forward in files with the same name which have different content.
+ (Default: ``no``)
+
+``--watch-later-options=option1,option2,...``
+ The options that are saved in "watch later" files if they have been changed
+ since when mpv started. These values will be restored the next time the
+ files are played. Note that the playback position is saved via the ``start``
+ option.
+
+ When removing options, existing watch later data won't be modified and will
+ still be applied fully, but new watch later data won't contain these
+ options.
+
+ See ``--help=watch-later-options`` for the list of the properties that are
+ restored by default.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Examples
+
+ - ``--watch-later-options-remove=sid``
+ The subtitle track selection will not be restored.
+ - ``--watch-later-options-remove=volume``
+ ``--watch-later-options-remove=mute``
+ The volume and mute state won't be saved to watch later files.
+ - ``--watch-later-options=start``
+ No option will be saved to watch later files, except the playback
+ position.
+
+``--write-filename-in-watch-later-config``
+ Prepend the watch later config files with the name of the file they refer
+ to. This is simply written as comment on the top of the file.
+
+ .. warning::
+
+ This option may expose privacy-sensitive information and is thus
+ disabled by default.
+
+``--ignore-path-in-watch-later-config``
+ Ignore path (i.e. use filename only) when using watch later feature.
+ (Default: disabled)
+
+Video
+-----
+
+``--vo=<driver>``
+ Specify the video output backend to be used. See `VIDEO OUTPUT DRIVERS`_ for
+ details and descriptions of available drivers.
+
+``--vd=<...>``
+ Specify a priority list of video decoders to be used, according to their
+ family and name. See ``--ad`` for further details. Both of these options
+ use the same syntax and semantics; the only difference is that they
+ operate on different codec lists.
+
+ .. note::
+
+ See ``--vd=help`` for a full list of available decoders.
+
+``--vf=<filter1[=parameter1:parameter2:...],filter2,...>``
+ Specify a list of video filters to apply to the video stream. See
+ `VIDEO FILTERS`_ for details and descriptions of the available filters.
+ The option variants ``--vf-add``, ``--vf-pre``, and ``--vf-clr`` exist
+ to modify a previously specified list, but you should not need these for
+ typical use.
+
+``--untimed``
+ Do not sleep when outputting video frames. Useful for benchmarks when used
+ with ``--no-audio.``
+
+``--framedrop=<mode>``
+ Skip displaying some frames to maintain A/V sync on slow systems, or
+ playing high framerate video on video outputs that have an upper framerate
+ limit.
+
+ The argument selects the drop methods, and can be one of the following:
+
+ <no>
+ Disable any frame dropping. Not recommended, for testing only.
+ <vo>
+ Drop late frames on video output (default). This still decodes and
+ filters all frames, but doesn't render them on the VO. Drops are
+ indicated in the terminal status line as ``Dropped:`` field.
+
+ In audio sync. mode, this drops frames that are outdated at the time of
+ display. If the decoder is too slow, in theory all frames would have to
+ be dropped (because all frames are too late) - to avoid this, frame
+ dropping stops if the effective framerate is below 10 FPS.
+
+ In display-sync. modes (see ``--video-sync``), this affects only how
+ A/V drops or repeats frames. If this mode is disabled, A/V desync will
+ in theory not affect video scheduling anymore (much like the
+ ``display-resample-desync`` mode). However, even if disabled, frames
+ will still be skipped (i.e. dropped) according to the ratio between
+ video and display frequencies.
+
+ This is the recommended mode, and the default.
+ <decoder>
+ Old, decoder-based framedrop mode. (This is the same as ``--framedrop=yes``
+ in mpv 0.5.x and before.) This tells the decoder to skip frames (unless
+ they are needed to decode future frames). May help with slow systems,
+ but can produce unwatchable choppy output, or even freeze the display
+ completely.
+
+ This uses a heuristic which may not make sense, and in general cannot
+ achieve good results, because the decoder's frame dropping cannot be
+ controlled in a predictable manner. Not recommended.
+
+ Even if you want to use this, prefer ``decoder+vo`` for better results.
+
+ The ``--vd-lavc-framedrop`` option controls what frames to drop.
+ <decoder+vo>
+ Enable both modes. Not recommended. Better than just ``decoder`` mode.
+
+ .. note::
+
+ ``--vo=vdpau`` has its own code for the ``vo`` framedrop mode. Slight
+ differences to other VOs are possible.
+
+``--video-latency-hacks=<yes|no>``
+ Enable some things which tend to reduce video latency by 1 or 2 frames
+ (default: no). Note that this option might be removed without notice once
+ the player's timing code does not inherently need to do these things
+ anymore. Using this option is known to break other options such as
+ interpolation, so it is not recommended to enable this.
+
+ This does:
+
+ - Use the demuxer reported FPS for frame dropping. This avoids the
+ player needing to decode 1 frame in advance, lowering total latency in
+ effect. This also means that if the demuxer reported FPS is wrong, or
+ the video filter chain changes FPS (e.g. deinterlacing), then it could
+ drop too many or not enough frames.
+ - Disable waiting for the first video frame. Normally the player waits for
+ the first video frame to be fully rendered before starting playback
+ properly. Some VOs will lazily initialize stuff when rendering the first
+ frame, so if this is not done, there is some likeliness that the VO has
+ to drop some frames if rendering the first frame takes longer than needed.
+
+``--display-fps-override=<fps>``
+ Set the display FPS used with the ``--video-sync=display-*`` modes. By
+ default, a detected value is used. Keep in mind that setting an incorrect
+ value (even if slightly incorrect) can ruin video playback. On multi-monitor
+ systems, there is a chance that the detected value is from the wrong
+ monitor.
+
+ Set this option only if you have reason to believe the automatically
+ determined value is wrong.
+
+``--hwdec=<api1,api2,...|no|auto|auto-safe|auto-copy>``
+ Specify the hardware video decoding API that should be used if possible.
+ Whether hardware decoding is actually done depends on the video codec. If
+ hardware decoding is not possible, mpv will fall back on software decoding.
+
+ Hardware decoding is not enabled by default, to keep the out-of-the-box
+ configuration as reliable as possible. However, when using modern hardware,
+ hardware video decoding should work correctly, offering reduced CPU usage,
+ and possibly lower power consumption. On older systems, it may be necessary
+ to use hardware decoding due to insufficient CPU resources; and even on
+ modern systems, sufficiently complex content (eg: 4K60 AV1) may require it.
+
+ .. note::
+
+ Use the ``Ctrl+h`` shortcut to toggle hardware decoding at runtime. It
+ toggles this option between ``auto-safe`` and ``no``.
+
+ If you decide you want to use hardware decoding by default, the general
+ recommendation is to try out decoding with the command line option, and
+ prove to yourself that it works as desired for the content you care
+ about. After that, you can add it to your config file.
+
+ When testing, you should start by using ``hwdec=auto-safe`` as it will
+ limit itself to choosing from hwdecs that are actively supported by the
+ development team. If that doesn't result in working hardware decoding,
+ you can try ``hwdec=auto`` to have it attempt to load every possible
+ hwdec, but if ``auto-safe`` didn't work, you will probably need to know
+ exactly which hwdec matches your hardware and read up on that entry
+ below.
+
+ If ``auto-safe`` or ``auto`` produced the desired results, we recommend
+ just sticking with that and only setting a specific hwdec in your config
+ file if it is really necessary.
+
+ If you use the Ubuntu package, keep in mind that their
+ ``/etc/mpv/mpv.conf`` contains ``hwdec=vaapi``, which is less than
+ ideal as it may not be the right choice for your system, and it may end
+ up using an inefficient wrapper library under the covers. We recommend
+ removing this line or deleting the file altogether.
+
+ .. note::
+
+ Even if enabled, hardware decoding is still only white-listed for some
+ codecs. See ``--hwdec-codecs`` to enable hardware decoding in more cases.
+
+ .. admonition:: Which method to choose?
+
+ - If you only want to enable hardware decoding at runtime, don't set the
+ parameter, or put ``hwdec=no`` into your ``mpv.conf`` (relevant on
+ distros which force-enable it by default, such as on Ubuntu). Use the
+ ``Ctrl+h`` default binding to enable it at runtime.
+ - If you're not sure, but want hardware decoding always enabled by
+ default, put ``hwdec=auto-safe`` into your ``mpv.conf``, and
+ acknowledge that this may cause problems.
+ - If you want to test available hardware decoding methods, pass
+ ``--hwdec=auto --hwdec-codecs=all`` and look at the terminal output.
+ - If you're a developer, or want to perform elaborate tests, you may
+ need any of the other possible option values.
+
+ This option accepts a comma delimited list of ``api`` types, along with certain
+ special values:
+
+ :no: always use software decoding (default)
+ :auto-safe: enable any whitelisted hw decoder (see below)
+ :auto: forcibly enable any hw decoder found (see below)
+ :yes: exactly the same as ``auto-safe``
+ :auto-copy: enable best hw decoder with copy-back (see below)
+
+ .. note::
+
+ Special values can be mixed with api names. eg: ``vaapi,auto`` will try
+ and use the ``vaapi`` hwdec, and if that fails, will run through the
+ normal ``auto`` logic.
+
+ Actively supported hwdecs:
+
+ :d3d11va: requires ``--vo=gpu`` with ``--gpu-context=d3d11`` or
+ ``--gpu-context=angle`` (Windows 8+ only)
+ :d3d11va-copy: copies video back to system RAM (Windows 8+ only)
+ :videotoolbox: requires ``--vo=gpu`` (macOS 10.8 and up),
+ or ``--vo=libmpv`` (iOS 9.0 and up)
+ :videotoolbox-copy: copies video back into system RAM (macOS 10.8 or iOS 9.0 and up)
+ :vaapi: requires ``--vo=gpu``, ``--vo=vaapi`` or ``--vo=dmabuf-wayland`` (Linux only)
+ :vaapi-copy: copies video back into system RAM (Linux with some GPUs only)
+ :nvdec: requires ``--vo=gpu`` (Any platform CUDA is available)
+ :nvdec-copy: copies video back to system RAM (Any platform CUDA is available)
+ :drm: requires ``--vo=gpu`` (Linux only)
+ :drm-copy: copies video back to system RAM (Linux only)
+ :vulkan: requires ``--vo=gpu-next`` (Any platform with Vulkan Video Decoding)
+ :vulkan-copy: copies video back to system RAM (Any platform with Vulkan Video Decoding)
+
+ Other hwdecs (only use if you know you have to):
+
+ :dxva2: requires ``--vo=gpu`` with ``--gpu-context=d3d11``,
+ ``--gpu-context=angle`` or ``--gpu-context=dxinterop``
+ (Windows only)
+ :dxva2-copy: copies video back to system RAM (Windows only)
+ :vdpau: requires ``--vo=gpu`` with ``--gpu-context=x11``, or
+ ``--vo=vdpau`` (Linux only)
+ :vdpau-copy: copies video back into system RAM (Linux with some GPUs only)
+ :mediacodec: requires ``--vo=gpu --gpu-context=android``
+ or ``--vo=mediacodec_embed`` (Android only)
+ :mediacodec-copy: copies video back to system RAM (Android only)
+ :mmal: requires ``--vo=gpu`` (Raspberry Pi only - default if available)
+ :mmal-copy: copies video back to system RAM (Raspberry Pi only)
+ :cuda: requires ``--vo=gpu`` (Any platform CUDA is available)
+ :cuda-copy: copies video back to system RAM (Any platform CUDA is available)
+ :crystalhd: copies video back to system RAM (Any platform supported by hardware)
+ :rkmpp: requires ``--vo=gpu`` (some RockChip devices only)
+
+ ``auto`` tries to automatically enable hardware decoding using the first
+ available method. This still depends what VO you are using. For example,
+ if you are not using ``--vo=gpu`` or ``--vo=vdpau``, vdpau decoding will
+ never be enabled. Also note that if the first found method doesn't actually
+ work, it will always fall back to software decoding, instead of trying the
+ next method (might matter on some Linux systems).
+
+ ``auto-safe`` is similar to ``auto``, but allows only whitelisted methods
+ that are considered "safe". This is supposed to be a reasonable way to
+ enable hardware decdoding by default in a config file (even though you
+ shouldn't do that anyway; prefer runtime enabling with ``Ctrl+h``). Unlike
+ ``auto``, this will not try to enable unknown or known-to-be-bad methods. In
+ addition, this may disable hardware decoding in other situations when it's
+ known to cause problems, but currently this mechanism is quite primitive.
+ (As an example for something that still causes problems: certain
+ combinations of HEVC and Intel chips on Windows tend to cause mpv to crash,
+ most likely due to driver bugs.)
+
+ ``auto-copy-safe`` selects the union of methods selected with ``auto-safe``
+ and ``auto-copy``.
+
+ ``auto-copy`` selects only modes that copy the video data back to system
+ memory after decoding. This selects modes like ``vaapi-copy`` (and so on).
+ If none of these work, hardware decoding is disabled. This mode is usually
+ guaranteed to incur no additional quality loss compared to software
+ decoding (assuming modern codecs and an error free video stream), and will
+ allow CPU processing with video filters. This mode works with all video
+ filters and VOs.
+
+ Because these copy the decoded video back to system RAM, they're often less
+ efficient than the direct modes, and may not help too much over software
+ decoding if you are short on CPU resources.
+
+ .. note::
+
+ Most non-copy methods only work with the OpenGL GPU backend. Currently,
+ only the ``vaapi``, ``nvdec``, ``cuda`` and ``vulkan`` methods work with
+ Vulkan.
+
+ The ``vaapi`` mode, if used with ``--vo=gpu``, requires Mesa 11, and most
+ likely works with Intel and AMD GPUs only. It also requires the opengl EGL
+ backend.
+
+ ``nvdec`` and ``nvdec-copy`` are the newest, and recommended method to do
+ hardware decoding on Nvidia GPUs.
+
+ ``cuda`` and ``cuda-copy`` are an older implementation of hardware decoding
+ on Nvidia GPUs that uses Nvidia's bitstream parsers rather than FFmpeg's.
+ This can lead to feature deficiencies, such as incorrect playback of HDR
+ content, and ``nvdec``/``nvdec-copy`` should always be preferred unless you
+ specifically need Nvidia's deinterlacing algorithms. To use this
+ deinterlacing you must pass the option:
+ ``vd-lavc-o=deint=[weave|bob|adaptive]``.
+ Pass ``weave`` (or leave the option unset) to not attempt any
+ deinterlacing.
+
+ .. admonition:: Quality reduction with hardware decoding
+
+ In theory, hardware decoding does not reduce video quality (at least
+ for the codecs h264 and HEVC). However, due to restrictions in video
+ output APIs, as well as bugs in the actual hardware decoders, there can
+ be some loss, or even blatantly incorrect results. This has largely
+ ceased to be a problem with modern hardware, but there is a lot of
+ hardware out there, so caveat emptor. Known problems are discussed
+ below, but the list cannot be considered exhaustive, as even hwdecs that
+ work well on certain hardware generations may be problematic on other
+ ones.
+
+ In some cases, RGB conversion is forced, which means the RGB conversion
+ is performed by the hardware decoding API, instead of the shaders
+ used by ``--vo=gpu``. This means certain colorspaces may not display
+ correctly, and certain filtering (such as debanding) cannot be applied
+ in an ideal way. This will also usually force the use of low quality
+ chroma scalers instead of the one specified by ``--cscale``. In other
+ cases, hardware decoding can also reduce the bit depth of the decoded
+ image, which can introduce banding or precision loss for 10-bit files.
+
+ ``vdpau`` always does RGB conversion in hardware, which does not
+ support newer colorspaces like BT.2020 correctly. However, ``vdpau``
+ doesn't support 10 bit or HDR encodings, so these limitations are
+ unlikely to be relevant.
+
+ ``dxva2`` is not safe. It appears to always use BT.601 for forced RGB
+ conversion, but actual behavior depends on the GPU drivers. Some drivers
+ appear to convert to limited range RGB, which gives a faded appearance.
+ In addition to driver-specific behavior, global system settings might
+ affect this additionally. This can give incorrect results even with
+ completely ordinary video sources.
+
+ ``rpi`` always uses the hardware overlay renderer, even with
+ ``--vo=gpu``.
+
+ ``mediacodec`` is not safe. It forces RGB conversion (not with ``-copy``)
+ and how well it handles non-standard colorspaces is not known.
+ In the rare cases where 10-bit is supported the bit depth of the output
+ will be reduced to 8.
+
+ ``cuda`` should usually be safe, but depending on how a file/stream
+ has been mixed, it has been reported to corrupt the timestamps causing
+ glitched, flashing frames. It can also sometimes cause massive
+ framedrops for unknown reasons. Caution is advised, and ``nvdec``
+ should always be preferred.
+
+ ``crystalhd`` is not safe. It always converts to 4:2:2 YUV, which
+ may be lossy, depending on how chroma sub-sampling is done during
+ conversion. It also discards the top left pixel of each frame for
+ some reason.
+
+ If you run into any weird decoding issues, frame glitches or
+ discoloration, and you have ``--hwdec`` turned on, the first thing you
+ should try is disabling it.
+
+``--gpu-hwdec-interop=<auto|all|no|name>``
+ This option is for troubleshooting hwdec interop issues. Since it's a
+ debugging option, its semantics may change at any time.
+
+ This is useful for the ``gpu`` and ``libmpv`` VOs for selecting which
+ hwdec interop context to use exactly. Effectively it also can be used
+ to block loading of certain backends.
+
+ If set to ``auto`` (default), the behavior depends on the VO: for ``gpu``,
+ it does nothing, and the interop context is loaded on demand (when the
+ decoder probes for ``--hwdec`` support). For ``libmpv``, which has
+ has no on-demand loading, this is equivalent to ``all``.
+
+ The empty string is equivalent to ``auto``.
+
+ If set to ``all``, it attempts to load all interop contexts at GL context
+ creation time.
+
+ Other than that, a specific backend can be set, and the list of them can
+ be queried with ``help`` (mpv CLI only).
+
+ Runtime changes to this are ignored (the current option value is used
+ whenever the renderer is created).
+
+``--hwdec-extra-frames=<N>``
+ Number of GPU frames hardware decoding should preallocate (default: see
+ ``--list-options`` output). If this is too low, frame allocation may fail
+ during decoding, and video frames might get dropped and/or corrupted.
+ Setting it too high simply wastes GPU memory and has no advantages.
+
+ This value is used only for hardware decoding APIs which require
+ preallocating surfaces (known examples include ``d3d11va`` and ``vaapi``).
+ For other APIs, frames are allocated as needed. The details depend on the
+ libavcodec implementations of the hardware decoders.
+
+ The required number of surfaces depends on dynamic runtime situations. The
+ default is a fixed value that is thought to be sufficient for most uses. But
+ in certain situations, it may not be enough.
+
+``--hwdec-image-format=<name>``
+ Set the internal pixel format used by hardware decoding via ``--hwdec``
+ (default ``no``). The special value ``no`` selects an implementation
+ specific standard format. Most decoder implementations support only one
+ format, and will fail to initialize if the format is not supported.
+
+ Some implementations might support multiple formats. In particular,
+ videotoolbox is known to require ``uyvy422`` for good performance on some
+ older hardware. d3d11va can always use ``yuv420p``, which uses an opaque
+ format, with likely no advantages.
+
+``--cuda-decode-device=<auto|0..>``
+ Choose the GPU device used for decoding when using the ``cuda`` or
+ ``nvdec`` hwdecs with the OpenGL GPU backend, and with the ``cuda-copy``
+ or ``nvdec-copy`` hwdecs in all cases.
+
+ For the OpenGL GPU backend, the default device used for decoding is the one
+ being used to provide ``gpu`` output (and in the vast majority of cases,
+ only one GPU will be present).
+
+ For the ``copy`` hwdecs, the default device will be the first device
+ enumerated by the CUDA libraries - however that is done.
+
+ For the Vulkan GPU backend, decoding must always happen on the display
+ device, and this option has no effect.
+
+``--vaapi-device=<device file>``
+ Choose the DRM device for ``vaapi-copy``. This should be the path to a
+ DRM device file. (Default: ``/dev/dri/renderD128``)
+
+``--panscan=<0.0-1.0>``
+ Enables pan-and-scan functionality (cropping the sides of e.g. a 16:9
+ video to make it fit a 4:3 display without black bands). The range
+ controls how much of the image is cropped. May not work with all video
+ output drivers.
+
+ This option has no effect if ``--video-unscaled`` option is used.
+
+``--video-aspect-override=<ratio|no>``
+ Override video aspect ratio, in case aspect information is incorrect or
+ missing in the file being played.
+
+ These values have special meaning:
+
+ :0: disable aspect ratio handling, pretend the video has square pixels
+ :no: same as ``0``
+ :-1: use the video stream or container aspect (default)
+
+ But note that handling of these special values might change in the future.
+
+ .. admonition:: Examples
+
+ - ``--video-aspect-override=4:3`` or ``--video-aspect-override=1.3333``
+ - ``--video-aspect-override=16:9`` or ``--video-aspect-override=1.7777``
+ - ``--no-video-aspect-override`` or ``--video-aspect-override=no``
+
+``--video-aspect-method=<bitstream|container>``
+ This sets the default video aspect determination method (if the aspect is
+ _not_ overridden by the user with ``--video-aspect-override`` or others).
+
+ :container: Strictly prefer the container aspect ratio. This is apparently
+ the default behavior with VLC, at least with Matroska. Note that
+ if the container has no aspect ratio set, the behavior is the
+ same as with bitstream.
+ :bitstream: Strictly prefer the bitstream aspect ratio, unless the bitstream
+ aspect ratio is not set. This is apparently the default behavior
+ with XBMC/kodi, at least with Matroska.
+
+ The current default for mpv is ``container``.
+
+ Normally you should not set this. Try the various choices if you encounter
+ video that has the wrong aspect ratio in mpv, but seems to be correct in
+ other players.
+
+``--video-unscaled=<no|yes|downscale-big>``
+ Disable scaling of the video. If the window is larger than the video,
+ black bars are added. Otherwise, the video is cropped, unless the option
+ is set to ``downscale-big``, in which case the video is fit to window. The
+ video still can be influenced by the other ``--video-...`` options. This
+ option disables the effect of ``--panscan``.
+
+ Note that the scaler algorithm may still be used, even if the video isn't
+ scaled. For example, this can influence chroma conversion. The video will
+ also still be scaled in one dimension if the source uses non-square pixels
+ (e.g. anamorphic widescreen DVDs).
+
+ This option is disabled if the ``--no-keepaspect`` option is used.
+
+``--video-pan-x=<value>``, ``--video-pan-y=<value>``
+ Moves the displayed video rectangle by the given value in the X or Y
+ direction. The unit is in fractions of the size of the scaled video (the
+ full size, even if parts of the video are not visible due to panscan or
+ other options).
+
+ For example, displaying a video fullscreen on a 1920x1080 screen with
+ ``--video-pan-x=-0.1`` would move the video 192 pixels to the left and
+ ``--video-pan-y=-0.1`` would move the video 108 pixels up.
+
+ This option is disabled if the ``--no-keepaspect`` option is used.
+
+``--video-rotate=<0-359|no>``
+ Rotate the video clockwise, in degrees. If ``no`` is given, the video is
+ never rotated, even if the file has rotation metadata. (The rotation value
+ is added to the rotation metadata, which means the value ``0`` would rotate
+ the video according to the rotation metadata.)
+
+ When using hardware decoding without copy-back, only 90° steps work, while
+ software decoding and hardware decoding methods that copy the video back to
+ system memory support all values between 0 and 359.
+
+``--video-crop=<[W[xH]][+x+y]>``, ``--video-crop=<x:y>``
+ Crop the video by starting at the x, y offset for w, h pixels. The crop is
+ applied to the source video rectangle (before anamorphic stretch) by the VO.
+ A crop rectangle that is not within the video rectangle will be ignored.
+ This works with hwdec, unlike the equivalent 'lavfi-crop'. When offset is
+ omitted, the central area will be cropped. Setting the crop to empty one
+ ``--video-crop=0x0+0+0`` overrides container crop and disables cropping.
+ Setting the crop to ``--video-crop=""`` disables manual cropping and restores
+ the container crop if it's specified.
+
+``--video-zoom=<value>``
+ Adjust the video display scale factor by the given value. The parameter is
+ given log 2. For example, ``--video-zoom=0`` is unscaled,
+ ``--video-zoom=1`` is twice the size, ``--video-zoom=-2`` is one fourth of
+ the size, and so on.
+
+ This option is disabled if the ``--no-keepaspect`` option is used.
+
+``--video-scale-x=<value>``, ``--video-scale-y=<value>``
+ Multiply the video display size with the given value (default: 1.0). If a
+ non-default value is used, this will be different from the window size, so
+ video will be either cut off, or black bars are added.
+
+ This value is multiplied with the value derived from ``--video-zoom`` and
+ the normal video aspect ratio. This option is disabled if the
+ ``--no-keepaspect`` option is used.
+
+``--video-align-x=<-1-1>``, ``--video-align-y=<-1-1>``
+ Moves the video rectangle within the black borders, which are usually added
+ to pad the video to screen if video and screen aspect ratios are different.
+ ``--video-align-y=-1`` would move the video to the top of the screen
+ (leaving a border only on the bottom), a value of ``0`` centers it
+ (default), and a value of ``1`` would put the video at the bottom of the
+ screen.
+
+ If video and screen aspect match perfectly, these options do nothing.
+
+ This option is disabled if the ``--no-keepaspect`` option is used.
+
+``--video-margin-ratio-left=<val>``, ``--video-margin-ratio-right=<val>``, ``--video-margin-ratio-top=<val>``, ``--video-margin-ratio-bottom=<val>``
+ Set extra video margins on each border (default: 0). Each value is a ratio
+ of the window size, using a range 0.0-1.0. For example, setting the option
+ ``--video-margin-ratio-right=0.2`` at a window size of 1000 pixels will add
+ a 200 pixels border on the right side of the window.
+
+ The video is "boxed" by these margins. The window size is not changed. In
+ particular it does not enlarge the window, and the margins will cause the
+ video to be downscaled by default. This may or may not change in the future.
+
+ The margins are applied after 90° video rotation, but before any other video
+ transformations.
+
+ This option is disabled if the ``--no-keepaspect`` option is used.
+
+ Subtitles still may use the margins, depending on ``--sub-use-margins`` and
+ similar options.
+
+ These options were created for the OSC. Some odd decisions, such as making
+ the margin values a ratio (instead of pixels), were made for the sake of
+ the OSC. It's possible that these options may be replaced by ones that are
+ more generally useful. The behavior of these options may change to fit
+ OSC requirements better, too.
+
+``--correct-pts``, ``--no-correct-pts``
+ ``--no-correct-pts`` switches mpv to a mode where video timing is
+ determined using a fixed framerate value (either using the
+ ``--container-fps-override`` option, or using file information). Sometimes,
+ files with very broken timestamps can be played somewhat well in this mode.
+ Note that video filters, subtitle rendering, seeking (including hr-seeks and
+ backstepping), and audio synchronization can be completely broken in this mode.
+
+``--container-fps-override=<float>``
+ Override video framerate. Useful if the original value is wrong or missing.
+
+ .. note::
+
+ Works in ``--no-correct-pts`` mode only.
+
+``--deinterlace=<yes|no>``
+ Enable or disable interlacing (default: no).
+ Interlaced video shows ugly comb-like artifacts, which are visible on
+ fast movement. Enabling this typically inserts the yadif video filter in
+ order to deinterlace the video, or lets the video output apply deinterlacing
+ if supported.
+
+ This behaves exactly like the ``deinterlace`` input property (usually
+ mapped to ``d``).
+
+ Keep in mind that this **will** conflict with manually inserted
+ deinterlacing filters, unless you take care. (Since mpv 0.27.0, even the
+ hardware deinterlace filters will conflict. Also since that version,
+ ``--deinterlace=auto`` was removed, which used to mean that the default
+ interlacing option of possibly inserted video filters was used.)
+
+ Note that this will make video look worse if it's not actually interlaced.
+
+``--frames=<number>``
+ Play/convert only first ``<number>`` video frames, then quit.
+
+ ``--frames=0`` loads the file, but immediately quits before initializing
+ playback. (Might be useful for scripts which just want to determine some
+ file properties.)
+
+ For audio-only playback, any value greater than 0 will quit playback
+ immediately after initialization. The value 0 works as with video.
+
+``--video-output-levels=<outputlevels>``
+ RGB color levels used with YUV to RGB conversion. Normally, output devices
+ such as PC monitors use full range color levels. However, some TVs and
+ video monitors expect studio RGB levels. Providing full range output to a
+ device expecting studio level input results in crushed blacks and whites,
+ the reverse in dim gray blacks and dim whites.
+
+ Not all VOs support this option. Some will silently ignore it.
+
+ Available color ranges are:
+
+ :auto: automatic selection (equals to full range) (default)
+ :limited: limited range (16-235 per component), studio levels
+ :full: full range (0-255 per component), PC levels
+
+ .. note::
+
+ It is advisable to use your graphics driver's color range option
+ instead, if available.
+
+``--hwdec-codecs=<codec1,codec2,...|all>``
+ Allow hardware decoding for a given list of codecs only. The special value
+ ``all`` always allows all codecs.
+
+ You can get the list of allowed codecs with ``mpv --vd=help``. Remove the
+ prefix, e.g. instead of ``lavc:h264`` use ``h264``.
+
+ By default, this is set to ``h264,vc1,hevc,vp8,vp9,av1``. Note that
+ the hardware acceleration special codecs like ``h264_vdpau`` are not
+ relevant anymore, and in fact have been removed from Libav in this form.
+
+ This is usually only needed with broken GPUs, where a codec is reported
+ as supported, but decoding causes more problems than it solves.
+
+ .. admonition:: Example
+
+ ``mpv --hwdec=vdpau --vo=vdpau --hwdec-codecs=h264,mpeg2video``
+ Enable vdpau decoding for h264 and mpeg2 only.
+
+``--vd-lavc-check-hw-profile=<yes|no>``
+ Check hardware decoder profile (default: yes). If ``no`` is set, the
+ highest profile of the hardware decoder is unconditionally selected, and
+ decoding is forced even if the profile of the video is higher than that.
+ The result is most likely broken decoding, but may also help if the
+ detected or reported profiles are somehow incorrect.
+
+``--vd-lavc-software-fallback=<yes|no|N>``
+ Fallback to software decoding if the hardware-accelerated decoder fails
+ (default: 3). If this is a number, then fallback will be triggered if
+ N frames fail to decode in a row. 1 is equivalent to ``yes``.
+
+ Setting this to a higher number might break the playback start fallback: if
+ a fallback happens, parts of the file will be skipped, approximately by to
+ the number of packets that could not be decoded. Values below an unspecified
+ count will not have this problem, because mpv retains the packets.
+
+``--vd-lavc-film-grain=<auto|cpu|gpu>``
+ Enables film grain application on the GPU. If video decoding is done on
+ the CPU, doing film grain application on the GPU can speed up decoding.
+ This option can also help hardware decoding, as it can reduce the number
+ of frame copies done.
+
+ By default, it's set to ``auto``, so if the VO supports film grain
+ application, then it will be treated as ``gpu``. If the VO does not
+ support this, then it will be treated as ``cpu``, regardless of the setting.
+ Currently, only ``gpu-next`` supports film grain application.
+
+``--vd-lavc-dr=<auto|yes|no>``
+ Enable direct rendering (default: auto). If this is set to ``yes``, the
+ video will be decoded directly to GPU video memory (or staging buffers).
+ This can speed up video upload, and may help with large resolutions or
+ slow hardware. This works only with the following VOs:
+
+ - ``gpu``: requires at least OpenGL 4.4 or Vulkan.
+ - ``libmpv``: The libmpv render API has optional support.
+
+ The ``auto`` option will try to guess whether DR can improve performance
+ on your particular hardware. Currently this enables it on AMD or NVIDIA
+ if using OpenGL or unconditionally if using Vulkan.
+
+ Using video filters of any kind that write to the image data (or output
+ newly allocated frames) will silently disable the DR code path.
+
+``--vd-lavc-bitexact``
+ Only use bit-exact algorithms in all decoding steps (for codec testing).
+
+``--vd-lavc-fast`` (MPEG-1/2 and H.264 only)
+ Enable optimizations which do not comply with the format specification and
+ potentially cause problems, like simpler dequantization, simpler motion
+ compensation, assuming use of the default quantization matrix, assuming YUV
+ 4:2:0 and skipping a few checks to detect damaged bitstreams.
+
+``--vd-lavc-o=<key>=<value>[,<key>=<value>[,...]]``
+ Pass AVOptions to libavcodec decoder. Note, a patch to make the ``o=``
+ unneeded and pass all unknown options through the AVOption system is
+ welcome. A full list of AVOptions can be found in the FFmpeg manual.
+
+ Some options which used to be direct options can be set with this
+ mechanism, like ``bug``, ``gray``, ``idct``, ``ec``, ``vismv``,
+ ``skip_top`` (was ``st``), ``skip_bottom`` (was ``sb``), ``debug``.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ ``--vd-lavc-o=debug=pict``
+
+``--vd-lavc-show-all=<yes|no>``
+ Show even broken/corrupt frames (default: no). If this option is set to
+ no, libavcodec won't output frames that were either decoded before an
+ initial keyframe was decoded, or frames that are recognized as corrupted.
+
+``--vd-lavc-skiploopfilter=<skipvalue>`` (H.264, HEVC only)
+ Skips the loop filter (AKA deblocking) during decoding. Since
+ the filtered frame is supposed to be used as reference for decoding
+ dependent frames, this has a worse effect on quality than not doing
+ deblocking on e.g. MPEG-2 video. But at least for high bitrate HDTV,
+ this provides a big speedup with little visible quality loss.
+ Codecs other than H.264 or HEVC may have partial support for this option
+ (often only ``all`` and ``none``).
+
+ ``<skipvalue>`` can be one of the following:
+
+ :none: Never skip.
+ :default: Skip useless processing steps (e.g. 0 size packets in AVI).
+ :nonref: Skip frames that are not referenced (i.e. not used for
+ decoding other frames, the error cannot "build up").
+ :bidir: Skip B-Frames.
+ :nonkey: Skip all frames except keyframes.
+ :all: Skip all frames.
+
+``--vd-lavc-skipidct=<skipvalue>`` (MPEG-1/2/4 only)
+ Skips the IDCT step. This degrades quality a lot in almost all cases
+ (see skiploopfilter for available skip values).
+
+``--vd-lavc-skipframe=<skipvalue>``
+ Skips decoding of frames completely. Big speedup, but jerky motion and
+ sometimes bad artifacts (see skiploopfilter for available skip values).
+
+``--vd-lavc-framedrop=<skipvalue>``
+ Set framedropping mode used with ``--framedrop`` (see skiploopfilter for
+ available skip values).
+
+``--vd-lavc-threads=<N>``
+ Number of threads to use for decoding. Whether threading is actually
+ supported depends on codec (default: 0). 0 means autodetect number of cores
+ on the machine and use that, up to the maximum of 16. You can set more than
+ 16 threads manually.
+
+``--vd-lavc-assume-old-x264=<yes|no>``
+ Assume the video was encoded by an old, buggy x264 version (default: no).
+ Normally, this is autodetected by libavcodec. But if the bitstream contains
+ no x264 version info (or it was somehow skipped), and the stream was in fact
+ encoded by an old x264 version (build 150 or earlier), and if the stream
+ uses 4:4:4 chroma, then libavcodec will by default show corrupted video.
+ This option sets the libavcodec ``x264_build`` option to ``150``, which
+ means that if the stream contains no version info, or was not encoded by
+ x264 at all, it assumes it was encoded by the old version. Enabling this
+ option is pretty safe if you want your broken files to work, but in theory
+ this can break on streams not encoded by x264, or if a stream encoded by a
+ newer x264 version contains no version info.
+
+``--vd-apply-cropping``
+ Certain video codecs support cropping, meaning that only a sub-rectangle of
+ the decoded frame is intended for display. This option controls how cropping
+ is handled by libavcodec. Cropping during decoding has certain limitations
+ with regards to alignment and hardware decoding. If this option is enabled,
+ decoder will apply the crop, else VO will handle it. Enabled by default.
+
+``--swapchain-depth=<N>``
+ Allow up to N in-flight frames. This essentially controls the frame
+ latency. Increasing the swapchain depth can improve pipelining and prevent
+ missed vsyncs, but increases visible latency. This option only mandates an
+ upper limit, the implementation can use a lower latency than requested
+ internally. A setting of 1 means that the VO will wait for every frame to
+ become visible before starting to render the next frame. (Default: 3)
+
+Audio
+-----
+
+``--audio-pitch-correction=<yes|no>``
+ If this is enabled (default), playing with a speed different from normal
+ automatically inserts the ``scaletempo2`` audio filter. You can insert
+ filters besides ``scaletempo2`` and modify their params using
+ `Conditional auto profiles`:
+
+ ::
+
+ [af_insert]
+ profile-cond=speed ~= 1
+ profile-restore=copy
+ af-add=scaletempo2=search-interval=50 # Insert filter and params here.
+
+ Filters set this way replace the ``scaletempo2`` default, instead of
+ overlapping with it. If there are multiple audio filters inserted that can do
+ pitch correction, then only the last one in the filter chain is used.
+ For details on the specifics of each available filter, see the audio filter
+ section.
+
+``--audio-device=<name>``
+ Use the given audio device. This consists of the audio output name, e.g.
+ ``alsa``, followed by ``/``, followed by the audio output specific device
+ name. The default value for this option is ``auto``, which tries every audio
+ output in preference order with the default device.
+
+ You can list audio devices with ``--audio-device=help``. This outputs the
+ device name in quotes, followed by a description. The device name is what
+ you have to pass to the ``--audio-device`` option. The list of audio devices
+ can be retrieved by API by using the ``audio-device-list`` property.
+
+ While the option normally takes one of the strings as indicated by the
+ methods above, you can also force the device for most AOs by building it
+ manually. For example ``name/foobar`` forces the AO ``name`` to use the
+ device ``foobar``. However, the ``--ao`` option will strictly force a
+ specific AO. To avoid confusion, don't use ``--ao`` and ``--audio-device``
+ together.
+
+ .. admonition:: Example for ALSA
+
+ MPlayer and mplayer2 required you to replace any ',' with '.' and
+ any ':' with '=' in the ALSA device name. For example, to use the
+ device named ``dmix:default``, you had to do:
+
+ ``-ao alsa:device=dmix=default``
+
+ In mpv you could instead use:
+
+ ``--audio-device=alsa/dmix:default``
+
+
+``--audio-exclusive=<yes|no>``
+ Enable exclusive output mode. In this mode, the system is usually locked
+ out, and only mpv will be able to output audio.
+
+ This only works for some audio outputs, such as ``wasapi``, ``coreaudio``
+ and ``pipewire``. Other audio outputs silently ignore this option.
+ They either have no concept of exclusive mode, or the mpv side of the
+ implementation is missing.
+
+``--audio-fallback-to-null=<yes|no>``
+ If no audio device can be opened, behave as if ``--ao=null`` was given. This
+ is useful in combination with ``--audio-device``: instead of causing an
+ error if the selected device does not exist, the client API user (or a
+ Lua script) could let playback continue normally, and check the
+ ``current-ao`` and ``audio-device-list`` properties to make high-level
+ decisions about how to continue.
+
+``--ao=<driver>``
+ Specify the audio output drivers to be used. See `AUDIO OUTPUT DRIVERS`_ for
+ details and descriptions of available drivers.
+
+``--af=<filter1[=parameter1:parameter2:...],filter2,...>``
+ Specify a list of audio filters to apply to the audio stream. See
+ `AUDIO FILTERS`_ for details and descriptions of the available filters.
+ The option variants ``--af-add``, ``--af-pre``, and ``--af-clr`` exist
+ to modify a previously specified list, but you should not need these for
+ typical use.
+
+``--audio-spdif=<codecs>``
+ List of codecs for which compressed audio passthrough should be used. This
+ works for both classic S/PDIF and HDMI.
+
+ Possible codecs are ``ac3``, ``dts``, ``dts-hd``, ``eac3``, ``truehd``.
+ Multiple codecs can be specified by separating them with ``,``. ``dts``
+ refers to low bitrate DTS core, while ``dts-hd`` refers to DTS MA (receiver
+ and OS support varies). If both ``dts`` and ``dts-hd`` are specified, it
+ behaves equivalent to specifying ``dts-hd`` only.
+
+ In earlier mpv versions you could use ``--ad`` to force the spdif wrapper.
+ This does not work anymore.
+
+ .. admonition:: Warning
+
+ There is not much reason to use this. HDMI supports uncompressed
+ multichannel PCM, and mpv supports lossless DTS-HD decoding via
+ FFmpeg's new DCA decoder (based on libdcadec).
+
+``--ad=<decoder1,decoder2,...[-]>``
+ Specify a priority list of audio decoders to be used, according to their
+ decoder name. When determining which decoder to use, the first decoder that
+ matches the audio format is selected. If that is unavailable, the next
+ decoder is used. Finally, it tries all other decoders that are not
+ explicitly selected or rejected by the option.
+
+ ``-`` at the end of the list suppresses fallback on other available
+ decoders not on the ``--ad`` list. ``+`` in front of an entry forces the
+ decoder. Both of these should not normally be used, because they break
+ normal decoder auto-selection! Both of these methods are deprecated.
+
+ .. admonition:: Examples
+
+ ``--ad=mp3float``
+ Prefer the FFmpeg/Libav ``mp3float`` decoder over all other MP3
+ decoders.
+
+ ``--ad=help``
+ List all available decoders.
+
+ .. admonition:: Warning
+
+ Enabling compressed audio passthrough (AC3 and DTS via SPDIF/HDMI) with
+ this option is not possible. Use ``--audio-spdif`` instead.
+
+``--volume=<value>``
+ Set the startup volume. 0 means silence, 100 means no volume reduction or
+ amplification. Negative values can be passed for compatibility, but are
+ treated as 0.
+
+ Since mpv 0.18.1, this always controls the internal mixer (aka "softvol").
+
+``--replaygain=<no|track|album>``
+ Adjust volume gain according to replaygain values stored in the file
+ metadata. With ``--replaygain=no`` (the default), perform no adjustment.
+ With ``--replaygain=track``, apply track gain. With ``--replaygain=album``,
+ apply album gain if present and fall back to track gain otherwise.
+
+``--replaygain-preamp=<db>``
+ Pre-amplification gain in dB to apply to the selected replaygain gain
+ (default: 0).
+
+``--replaygain-clip=<yes|no>``
+ Prevent clipping caused by replaygain by automatically lowering the
+ gain (default). Use ``--replaygain-clip=no`` to disable this.
+
+``--replaygain-fallback=<db>``
+ Gain in dB to apply if the file has no replay gain tags. This option
+ is always applied if the replaygain logic is somehow inactive. If this
+ is applied, no other replaygain options are applied.
+
+``--audio-delay=<sec>``
+ Audio delay in seconds (positive or negative float value). Positive values
+ delay the audio, and negative values delay the video.
+
+``--mute=<yes|no|auto>``
+ Set startup audio mute status (default: no).
+
+ ``auto`` is a deprecated possible value that is equivalent to ``no``.
+
+ See also: ``--volume``.
+
+``--softvol=<no|yes|auto>``
+ Deprecated/unfunctional. Before mpv 0.18.1, this used to control whether
+ to use the volume controls of the audio output driver or the internal mpv
+ volume filter.
+
+ The current behavior is that softvol is always enabled, i.e. as if this
+ option is set to ``yes``. The other behaviors are not available anymore,
+ although ``auto`` almost matches current behavior in most cases.
+
+ The ``no`` behavior is still partially available through the ``ao-volume``
+ and ``ao-mute`` properties. But there are no options to reset these.
+
+``--audio-demuxer=<[+]name>``
+ Use this audio demuxer type when using ``--audio-file``. Use a '+' before
+ the name to force it; this will skip some checks. Give the demuxer name as
+ printed by ``--audio-demuxer=help``.
+
+``--ad-lavc-ac3drc=<level>``
+ Select the Dynamic Range Compression level for AC-3 audio streams.
+ ``<level>`` is a float value ranging from 0 to 1, where 0 means no
+ compression (which is the default) and 1 means full compression (make loud
+ passages more silent and vice versa). Values up to 6 are also accepted, but
+ are purely experimental. This option only shows an effect if the AC-3 stream
+ contains the required range compression information.
+
+ The standard mandates that DRC is enabled by default, but mpv (and some
+ other players) ignore this for the sake of better audio quality.
+
+``--ad-lavc-downmix=<yes|no>``
+ Whether to request audio channel downmixing from the decoder (default: no).
+ Some decoders, like AC-3, AAC and DTS, can remix audio on decoding. The
+ requested number of output channels is set with the ``--audio-channels`` option.
+ Useful for playing surround audio on a stereo system.
+
+``--ad-lavc-threads=<0-16>``
+ Number of threads to use for decoding. Whether threading is actually
+ supported depends on codec. As of this writing, it's supported for some
+ lossless codecs only. 0 means autodetect number of cores on the
+ machine and use that, up to the maximum of 16 (default: 1).
+
+``--ad-lavc-o=<key>=<value>[,<key>=<value>[,...]]``
+ Pass AVOptions to libavcodec decoder. Note, a patch to make the o=
+ unneeded and pass all unknown options through the AVOption system is
+ welcome. A full list of AVOptions can be found in the FFmpeg manual.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+``--ad-spdif-dtshd=<yes|no>``, ``--dtshd``, ``--no-dtshd``
+ If DTS is passed through, use DTS-HD.
+
+ .. admonition:: Warning
+
+ This and enabling passthrough via ``--ad`` are deprecated in favor of
+ using ``--audio-spdif=dts-hd``.
+
+``--audio-channels=<auto-safe|auto|layouts>``
+ Control which audio channels are output (e.g. surround vs. stereo). There
+ are the following possibilities:
+
+ - ``--audio-channels=auto-safe``
+ Use the system's preferred channel layout. If there is none (such
+ as when accessing a hardware device instead of the system mixer),
+ force stereo. Some audio outputs might simply accept any layout and
+ do downmixing on their own.
+
+ This is the default.
+ - ``--audio-channels=auto``
+ Send the audio device whatever it accepts, preferring the audio's
+ original channel layout. Can cause issues with HDMI (see the warning
+ below).
+ - ``--audio-channels=layout1,layout2,...``
+ List of ``,``-separated channel layouts which should be allowed.
+ Technically, this only adjusts the filter chain output to the best
+ matching layout in the list, and passes the result to the audio API.
+ It's possible that the audio API will select a different channel
+ layout.
+
+ Using this mode is recommended for direct hardware output, especially
+ over HDMI (see HDMI warning below).
+ - ``--audio-channels=<stereo|mono>``
+ Force a downmix to stereo or mono. These are special-cases of the
+ previous item. (See paragraphs below for implications.)
+
+ If a list of layouts is given, each item can be either an explicit channel
+ layout name (like ``5.1``), or a channel number. Channel numbers refer to
+ default layouts, e.g. 2 channels refer to stereo, 6 refers to 5.1.
+
+ See ``--audio-channels=help`` output for defined default layouts. This also
+ lists speaker names, which can be used to express arbitrary channel
+ layouts (e.g. ``fl-fr-lfe`` is 2.1).
+
+ If the list of channel layouts has only 1 item, the decoder is asked to
+ produce according output. This sometimes triggers decoder-downmix, which
+ might be different from the normal mpv downmix. (Only some decoders support
+ remixing audio, like AC-3, AAC or DTS. You can use ``--ad-lavc-downmix=no``
+ to make the decoder always output its native layout.) One consequence is
+ that ``--audio-channels=stereo`` triggers decoder downmix, while ``auto``
+ or ``auto-safe`` never will, even if they end up selecting stereo. This
+ happens because the decision whether to use decoder downmix happens long
+ before the audio device is opened.
+
+ If the channel layout of the media file (i.e. the decoder) and the AO's
+ channel layout don't match, mpv will attempt to insert a conversion filter.
+ You may need to change the channel layout of the system mixer to achieve
+ your desired output as mpv does not have control over it. Another
+ work-around for this on some AOs is to use ``--audio-exclusive=yes`` to
+ circumvent the system mixer entirely.
+
+ .. admonition:: Warning
+
+ Using ``auto`` can cause issues when using audio over HDMI. The OS will
+ typically report all channel layouts that _can_ go over HDMI, even if
+ the receiver does not support them. If a receiver gets an unsupported
+ channel layout, random things can happen, such as dropping the
+ additional channels, or adding noise.
+
+ You are recommended to set an explicit whitelist of the layouts you
+ want. For example, most A/V receivers connected via HDMI and that can
+ do 7.1 would be served by: ``--audio-channels=7.1,5.1,stereo``
+
+``--audio-display=<no|embedded-first|external-first>``
+ Determines whether to display cover art when playing audio files and with
+ what priority. It will display the first image found, and additional images
+ are available as video tracks.
+
+ :no: Disable display of video entirely when playing audio
+ files.
+ :embedded-first: Display embedded images and external cover art, giving
+ priority to embedded images (default).
+ :external-first: Display embedded images and external cover art, giving
+ priority to external files.
+
+ This option has no influence on files with normal video tracks.
+
+``--audio-files=<files>``
+ Play audio from an external file while viewing a video.
+
+ This is a path list option. See `List Options`_ for details.
+
+``--audio-file=<file>``
+ CLI/config file only alias for ``--audio-files-append``. Each use of this
+ option will add a new audio track. The details are similar to how
+ ``--sub-file`` works.
+
+``--audio-format=<format>``
+ Select the sample format used for output from the audio filter layer to
+ the sound card. The values that ``<format>`` can adopt are listed below in
+ the description of the ``format`` audio filter.
+
+``--audio-samplerate=<Hz>``
+ Select the output sample rate to be used (of course sound cards have
+ limits on this). If the sample frequency selected is different from that
+ of the current media, the lavrresample audio filter will be inserted into
+ the audio filter layer to compensate for the difference.
+
+``--gapless-audio=<no|yes|weak>``
+ Try to play consecutive audio files with no silence or disruption at the
+ point of file change. Default: ``weak``.
+
+ :no: Disable gapless audio.
+ :yes: The audio device is opened using parameters chosen for the first
+ file played and is then kept open for gapless playback. This
+ means that if the first file for example has a low sample rate, then
+ the following files may get resampled to the same low sample rate,
+ resulting in reduced sound quality. If you play files with different
+ parameters, consider using options such as ``--audio-samplerate``
+ and ``--audio-format`` to explicitly select what the shared output
+ format will be.
+ :weak: Normally, the audio device is kept open (using the format it was
+ first initialized with). If the audio format the decoder output
+ changes, the audio device is closed and reopened. This means that
+ you will normally get gapless audio with files that were encoded
+ using the same settings, but might not be gapless in other cases.
+ The exact conditions under which the audio device is kept open is
+ an implementation detail, and can change from version to version.
+ Currently, the device is kept even if the sample format changes,
+ but the sample formats are convertible.
+ If video is still going on when there is still audio, trying to use
+ gapless is also explicitly given up.
+
+ .. note::
+
+ This feature is implemented in a simple manner and relies on audio
+ output device buffering to continue playback while moving from one file
+ to another. If playback of the new file starts slowly, for example
+ because it is played from a remote network location or because you have
+ specified cache settings that require time for the initial cache fill,
+ then the buffered audio may run out before playback of the new file
+ can start.
+
+``--initial-audio-sync``, ``--no-initial-audio-sync``
+ When starting a video file or after events such as seeking, mpv will by
+ default modify the audio stream to make it start from the same timestamp
+ as video, by either inserting silence at the start or cutting away the
+ first samples. Disabling this option makes the player behave like older
+ mpv versions did: video and audio are both started immediately even if
+ their start timestamps differ, and then video timing is gradually adjusted
+ if necessary to reach correct synchronization later.
+
+``--volume-max=<100.0-1000.0>``
+ Set the maximum amplification level in percent (default: 130). A value of
+ 130 will allow you to adjust the volume up to about double the normal level.
+
+``--audio-file-auto=<no|exact|fuzzy|all>``, ``--no-audio-file-auto``
+ Load additional audio files matching the video filename. The parameter
+ specifies how external audio files are matched.
+
+ :no: Don't automatically load external audio files (default).
+ :exact: Load the media filename with audio file extension.
+ :fuzzy: Load all audio files containing the media filename.
+ :all: Load all audio files in the current and ``--audio-file-paths``
+ directories.
+
+``--audio-file-auto-exts=ext1,ext2,...``
+ Audio file extentions to try and match when using ``audio-file-auto``.
+
+ This is a string list option. See `List Options`_ for details.
+
+``--audio-file-paths=<path1:path2:...>``
+ Equivalent to ``--sub-file-paths`` option, but for auto-loaded audio files.
+
+ This is a path list option. See `List Options`_ for details.
+
+``--audio-client-name=<name>``
+ The application name the player reports to the audio API. Can be useful
+ if you want to force a different audio profile (e.g. with PulseAudio),
+ or to set your own application name when using libmpv.
+
+``--audio-buffer=<seconds>``
+ Set the audio output minimum buffer. The audio device might actually create
+ a larger buffer if it pleases. If the device creates a smaller buffer,
+ additional audio is buffered in an additional software buffer.
+
+ Making this larger will make soft-volume and other filters react slower,
+ introduce additional issues on playback speed change, and block the
+ player on audio format changes. A smaller buffer might lead to audio
+ dropouts.
+
+ This option should be used for testing only. If a non-default value helps
+ significantly, the mpv developers should be contacted.
+
+ Default: 0.2 (200 ms).
+
+``--audio-stream-silence=<yes|no>``
+ Cash-grab consumer audio hardware (such as A/V receivers) often ignore
+ initial audio sent over HDMI. This can happen every time audio over HDMI
+ is stopped and resumed. In order to compensate for this, you can enable
+ this option to not to stop and restart audio on seeks, and fill the gaps
+ with silence. Likewise, when pausing playback, audio is not stopped, and
+ silence is played while paused. Note that if no audio track is selected,
+ the audio device will still be closed immediately.
+
+ Not all AOs support this.
+
+ .. admonition:: Warning
+
+ This modifies certain subtle player behavior, like A/V-sync and underrun
+ handling. Enabling this option is strongly discouraged.
+
+``--audio-wait-open=<secs>``
+ This makes sense for use with ``--audio-stream-silence=yes``. If this option
+ is given, the player will wait for the given amount of seconds after opening
+ the audio device before sending actual audio data to it. Useful if your
+ expensive hardware discards the first 1 or 2 seconds of audio data sent to
+ it. If ``--audio-stream-silence=yes`` is not set, this option will likely
+ just waste time.
+
+Subtitles
+---------
+
+.. note::
+
+ Changing styling and position does not work with all subtitles. Image-based
+ subtitles (DVD, Bluray/PGS, DVB) cannot changed for fundamental reasons.
+ Subtitles in ASS format are normally not changed intentionally, but
+ overriding them can be controlled with ``--sub-ass-override``.
+
+``--sub-demuxer=<[+]name>``
+ Force subtitle demuxer type for ``--sub-file``. Give the demuxer name as
+ printed by ``--sub-demuxer=help``.
+
+``--sub-delay=<sec>``
+ Delays subtitles by ``<sec>`` seconds. Can be negative.
+
+``--sub-files=<file-list>``, ``--sub-file=<filename>``
+ Add a subtitle file to the list of external subtitles.
+
+ If you use ``--sub-file`` only once, this subtitle file is displayed by
+ default.
+
+ If ``--sub-file`` is used multiple times, the subtitle to use can be
+ switched at runtime by cycling subtitle tracks. It's possible to show
+ two subtitles at once: use ``--sid`` to select the first subtitle index,
+ and ``--secondary-sid`` to select the second index. (The index is printed
+ on the terminal output after the ``--sid=`` in the list of streams.)
+
+ ``--sub-files`` is a path list option (see `List Options`_ for details), and
+ can take multiple file names separated by ``:`` (Unix) or ``;`` (Windows),
+ while ``--sub-file`` takes a single filename, but can be used multiple
+ times to add multiple files. Technically, ``--sub-file`` is a CLI/config
+ file only alias for ``--sub-files-append``.
+
+``--secondary-sid=<ID|auto|no>``
+ Select a secondary subtitle stream. This is similar to ``--sid``. If a
+ secondary subtitle is selected, it will be rendered as toptitle (i.e. on
+ the top of the screen) alongside the normal subtitle, and provides a way
+ to render two subtitles at once.
+
+ There are some caveats associated with this feature. For example, bitmap
+ subtitles will always be rendered in their usual position, so selecting a
+ bitmap subtitle as secondary subtitle will result in overlapping subtitles.
+ Secondary subtitles are never shown on the terminal if video is disabled.
+
+ .. note::
+
+ Styling and interpretation of any formatting tags is disabled for the
+ secondary subtitle. Internally, the same mechanism as ``--no-sub-ass``
+ is used to strip the styling.
+
+ .. note::
+
+ If the main subtitle stream contains formatting tags which display the
+ subtitle at the top of the screen, it will overlap with the secondary
+ subtitle. To prevent this, you could use ``--no-sub-ass`` to disable
+ styling in the main subtitle stream.
+
+``--sub-scale=<0-100>``
+ Factor for the text subtitle font size (default: 1).
+
+ .. note::
+
+ This affects ASS subtitles as well, and may lead to incorrect subtitle
+ rendering. Use with care, or use ``--sub-font-size`` instead.
+
+``--sub-scale-by-window=<yes|no>``
+ Whether to scale subtitles with the window size (default: yes). If this is
+ disabled, changing the window size won't change the subtitle font size.
+
+ Like ``--sub-scale``, this can break ASS subtitles.
+
+``--sub-scale-with-window=<yes|no>``
+ Make the subtitle font size relative to the window, instead of the video.
+ This is useful if you always want the same font size, even if the video
+ doesn't cover the window fully, e.g. because screen aspect and window
+ aspect mismatch (and the player adds black bars).
+
+ Default: yes.
+
+ This option is misnamed. The difference to the confusingly similar sounding
+ option ``--sub-scale-by-window`` is that ``--sub-scale-with-window`` still
+ scales with the approximate window size, while the other option disables
+ this scaling.
+
+ Affects plain text subtitles only (or ASS if ``--sub-ass-override`` is set
+ high enough).
+
+``--sub-ass-scale-with-window=<yes|no>``
+ Like ``--sub-scale-with-window``, but affects subtitles in ASS format only.
+ Like ``--sub-scale``, this can break ASS subtitles.
+
+ Default: no.
+
+``--embeddedfonts=<yes|no>``
+ Use fonts embedded in Matroska container files and ASS scripts (default:
+ yes). These fonts can be used for SSA/ASS subtitle rendering.
+
+``--sub-pos=<0-150>``
+ Specify the position of subtitles on the screen. The value is the vertical
+ position of the subtitle in % of the screen height. 100 is the original
+ position, which is often not the absolute bottom of the screen, but with
+ some margin between the bottom and the subtitle. Values above 100 move the
+ subtitle further down.
+
+ .. admonition:: Warning
+
+ Text subtitles (as opposed to image subtitles) may be cut off if the
+ value of the option is above 100. This is a libass restriction.
+
+ This affects ASS subtitles as well, and may lead to incorrect subtitle
+ rendering in addition to the problem above.
+
+ Using ``--sub-margin-y`` can achieve this in a better way.
+
+``--sub-speed=<0.1-10.0>``
+ Multiply the subtitle event timestamps with the given value. Can be used
+ to fix the playback speed for frame-based subtitle formats. Affects text
+ subtitles only.
+
+ .. admonition:: Example
+
+ ``--sub-speed=25/23.976`` plays frame based subtitles which have been
+ loaded assuming a framerate of 23.976 at 25 FPS.
+
+``--sub-ass-style-overrides=<[Style.]Param=Value[,...]>``
+ Override some style or script info parameters.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Examples
+
+ - ``--sub-ass-style-overrides=FontName=Arial,Default.Bold=1``
+ - ``--sub-ass-style-overrides=PlayResY=768``
+
+ .. note::
+
+ Using this option may lead to incorrect subtitle rendering.
+
+``--sub-ass-hinting=<none|light|normal|native>``
+ Set font hinting type. <type> can be:
+
+ :none: no hinting (default)
+ :light: FreeType autohinter, light mode
+ :normal: FreeType autohinter, normal mode
+ :native: font native hinter
+
+ .. admonition:: Warning
+
+ Enabling hinting can lead to mispositioned text (in situations it's
+ supposed to match up video background), or reduce the smoothness
+ of animations with some badly authored ASS scripts. It is recommended
+ to not use this option, unless really needed.
+
+``--sub-ass-line-spacing=<value>``
+ Set line spacing value for SSA/ASS renderer.
+
+``--sub-ass-shaper=<simple|complex>``
+ Set the text layout engine used by libass.
+
+ :simple: uses Fribidi only, fast, doesn't render some languages correctly
+ :complex: uses HarfBuzz, slower, wider language support
+
+ ``complex`` is the default. If libass hasn't been compiled against HarfBuzz,
+ libass silently reverts to ``simple``.
+
+``--sub-ass-styles=<filename>``
+ Load all SSA/ASS styles found in the specified file and use them for
+ rendering text subtitles. The syntax of the file is exactly like the ``[V4
+ Styles]`` / ``[V4+ Styles]`` section of SSA/ASS.
+
+ .. note::
+
+ Using this option may lead to incorrect subtitle rendering.
+
+``--sub-ass-override=<yes|no|force|scale|strip>``
+ Control whether user style overrides should be applied. Note that all of
+ these overrides try to be somewhat smart about figuring out whether or not
+ a subtitle is considered a "sign".
+
+ :no: Render subtitles as specified by the subtitle scripts, without
+ overrides.
+ :yes: Apply all the ``--sub-ass-*`` style override options. Changing the
+ default for any of these options can lead to incorrect subtitle
+ rendering (default).
+ :force: Like ``yes``, but also force all ``--sub-*`` options. Can break
+ rendering easily.
+ :scale: Like ``yes``, but also apply ``--sub-scale``.
+ :strip: Radically strip all ASS tags and styles from the subtitle. This
+ is equivalent to the old ``--no-ass`` / ``--no-sub-ass`` options.
+
+ This also controls some bitmap subtitle overrides, as well as HTML tags in
+ formats like SRT, despite the name of the option.
+
+``--sub-ass-force-margins``
+ Enables placing toptitles and subtitles in black borders when they are
+ available, if the subtitles are in the ASS format.
+
+ Default: no.
+
+``--sub-use-margins``
+ Enables placing toptitles and subtitles in black borders when they are
+ available, if the subtitles are in a plain text format (or ASS if
+ ``--sub-ass-override`` is set high enough).
+
+ Default: yes.
+
+``--sub-ass-vsfilter-aspect-compat=<yes|no>``
+ Stretch SSA/ASS subtitles when playing anamorphic videos for compatibility
+ with traditional VSFilter behavior. This switch has no effect when the
+ video is stored with square pixels.
+
+ The renderer historically most commonly used for the SSA/ASS subtitle
+ formats, VSFilter, had questionable behavior that resulted in subtitles
+ being stretched too if the video was stored in anamorphic format that
+ required scaling for display. This behavior is usually undesirable and
+ newer VSFilter versions may behave differently. However, many existing
+ scripts compensate for the stretching by modifying things in the opposite
+ direction. Thus, if such scripts are displayed "correctly", they will not
+ appear as intended. This switch enables emulation of the old VSFilter
+ behavior (undesirable but expected by many existing scripts).
+
+ Enabled by default.
+
+``--sub-ass-vsfilter-blur-compat=<yes|no>``
+ Scale ``\blur`` tags by video resolution instead of script resolution
+ (enabled by default). This is bug in VSFilter, which according to some,
+ can't be fixed anymore in the name of compatibility.
+
+ Note that this uses the actual video resolution for calculating the
+ offset scale factor, not what the video filter chain or the video output
+ use.
+
+``--sub-ass-vsfilter-color-compat=<basic|full|force-601|no>``
+ Mangle colors like (xy-)vsfilter do (default: basic). Historically, VSFilter
+ was not color space aware. This was no problem as long as the color space
+ used for SD video (BT.601) was used. But when everything switched to HD
+ (BT.709), VSFilter was still converting RGB colors to BT.601, rendered
+ them into the video frame, and handled the frame to the video output, which
+ would use BT.709 for conversion to RGB. The result were mangled subtitle
+ colors. Later on, bad hacks were added on top of the ASS format to control
+ how colors are to be mangled.
+
+ :basic: Handle only BT.601->BT.709 mangling, if the subtitles seem to
+ indicate that this is required (default).
+ :full: Handle the full ``YCbCr Matrix`` header with all video color spaces
+ supported by libass and mpv. This might lead to bad breakages in
+ corner cases and is not strictly needed for compatibility
+ (hopefully), which is why this is not default.
+ :force-601: Force BT.601->BT.709 mangling, regardless of subtitle headers
+ or video color space.
+ :no: Disable color mangling completely. All colors are RGB.
+
+ Choosing anything other than ``no`` will make the subtitle color depend on
+ the video color space, and it's for example in theory not possible to reuse
+ a subtitle script with another video file. The ``--sub-ass-override``
+ option doesn't affect how this option is interpreted.
+
+``--stretch-dvd-subs=<yes|no>``
+ Stretch DVD subtitles when playing anamorphic videos for better looking
+ fonts on badly mastered DVDs. This switch has no effect when the
+ video is stored with square pixels - which for DVD input cannot be the case
+ though.
+
+ Many studios tend to use bitmap fonts designed for square pixels when
+ authoring DVDs, causing the fonts to look stretched on playback on DVD
+ players. This option fixes them, however at the price of possibly
+ misaligning some subtitles (e.g. sign translations).
+
+ Disabled by default.
+
+``--stretch-image-subs-to-screen=<yes|no>``
+ Stretch DVD and other image subtitles to the screen, ignoring the video
+ margins. This has a similar effect as ``--sub-use-margins`` for text
+ subtitles, except that the text itself will be stretched, not only just
+ repositioned. (At least in general it is unavoidable, as an image bitmap
+ can in theory consist of a single bitmap covering the whole screen, and
+ the player won't know where exactly the text parts are located.)
+
+ This option does not display subtitles correctly. Use with care.
+
+ Disabled by default.
+
+``--image-subs-video-resolution=<yes|no>``
+ Override the image subtitle resolution with the video resolution
+ (default: no). Normally, the subtitle canvas is fit into the video canvas
+ (e.g. letterboxed). Setting this option uses the video size as subtitle
+ canvas size. Can be useful to test broken subtitles, which often happen
+ when the video was trancoded, while attempting to keep the old subtitles.
+
+``--sub-ass``, ``--no-sub-ass``
+ Render ASS subtitles natively (enabled by default).
+
+ .. note::
+
+ This has been deprecated by ``--sub-ass-override=strip``. You also
+ may need ``--embeddedfonts=no`` to get the same behavior. Also,
+ using ``--sub-ass-override=style`` should give better results
+ without breaking subtitles too much.
+
+ If ``--no-sub-ass`` is specified, all tags and style declarations are
+ stripped and ignored on display. The subtitle renderer uses the font style
+ as specified by the ``--sub-`` options instead.
+
+ .. note::
+
+ Using ``--no-sub-ass`` may lead to incorrect or completely broken
+ rendering of ASS/SSA subtitles. It can sometimes be useful to forcibly
+ override the styling of ASS subtitles, but should be avoided in general.
+
+``--sub-auto=<no|exact|fuzzy|all>``, ``--no-sub-auto``
+ Load additional subtitle files matching the video filename. The parameter
+ specifies how external subtitle files are matched. ``exact`` is enabled by
+ default.
+
+ :no: Don't automatically load external subtitle files.
+ :exact: Load the media filename with subtitle file extension and possibly
+ language suffixes (default).
+ :fuzzy: Load all subs containing the media filename.
+ :all: Load all subs in the current and ``--sub-file-paths`` directories.
+
+``--sub-auto-exts=ext1,ext2,...``
+ Subtitle extentions to try and match when using ``--sub-auto``. Note that
+ modifying this list will also affect what mpv recognizes as subtitles when
+ using drag and drop.
+
+ This is a string list option. See `List Options`_ for details.
+
+``--sub-codepage=<codepage>``
+ You can use this option to specify the subtitle codepage. uchardet will be
+ used to guess the charset. (If mpv was not compiled with uchardet, then
+ ``utf-8`` is the effective default.)
+
+ The default value for this option is ``auto``, which enables autodetection.
+
+ The following steps are taken to determine the final codepage, in order:
+
+ - if the specific codepage has a ``+``, use that codepage
+ - if the data looks like UTF-8, assume it is UTF-8
+ - if ``--sub-codepage`` is set to a specific codepage, use that
+ - run uchardet, and if successful, use that
+ - otherwise, use ``UTF-8-BROKEN``
+
+ .. admonition:: Examples
+
+ - ``--sub-codepage=latin2`` Use Latin 2 if input is not UTF-8.
+ - ``--sub-codepage=+cp1250`` Always force recoding to cp1250.
+
+ The pseudo codepage ``UTF-8-BROKEN`` is used internally. If it's set,
+ subtitles are interpreted as UTF-8 with "Latin 1" as fallback for bytes
+ which are not valid UTF-8 sequences. iconv is never involved in this mode.
+
+ .. note::
+
+ This works for text subtitle files only. Other types of subtitles (in
+ particular subtitles in mkv files) are always assumed to be UTF-8.
+
+
+``--sub-stretch-durations=<yes|no>``
+ Stretch a subtitle duration so it ends when the next one starts.
+ Should help with subtitles which erroneously have zero durations.
+
+ .. note::
+
+ Only applies to text subtitles.
+
+``--sub-fix-timing=<yes|no>``
+ Adjust subtitle timing is to remove minor gaps or overlaps between
+ subtitles (if the difference is smaller than 210 ms, the gap or overlap
+ is removed).
+
+``--sub-forced-events-only=<yes|no>``
+ Enabling this displays only forced events within subtitle streams. Only
+ some bitmap subtitle formats (such as DVD or PGS) are capable of having a
+ mixture of forced and unforced events within the stream. Enabling this on
+ text subtitles will cause no subtitles to be displayed (default: ``no``).
+
+``--sub-fps=<rate>``
+ Specify the framerate of the subtitle file (default: video fps). Affects
+ text subtitles only.
+
+ .. note::
+
+ ``<rate>`` > video fps speeds the subtitles up for frame-based
+ subtitle files and slows them down for time-based ones.
+
+ See also: ``--sub-speed``.
+
+``--sub-gauss=<0.0-3.0>``
+ Apply Gaussian blur to image subtitles (default: 0). This can help to make
+ pixelated DVD/Vobsubs look nicer. A value other than 0 also switches to
+ software subtitle scaling. Might be slow.
+
+ .. note::
+
+ Never applied to text subtitles.
+
+``--sub-gray``
+ Convert image subtitles to grayscale. Can help to make yellow DVD/Vobsubs
+ look nicer.
+
+ .. note::
+
+ Never applied to text subtitles.
+
+``--sub-file-paths=<path-list>``
+ Specify extra directories to search for subtitles matching the video.
+ Multiple directories can be separated by ":" (";" on Windows).
+ Paths can be relative or absolute. Relative paths are interpreted relative
+ to video file directory.
+ If the file is a URL, only absolute paths and ``sub`` configuration
+ subdirectory will be scanned.
+
+ .. admonition:: Example
+
+ Assuming that ``/path/to/video/video.avi`` is played and
+ ``--sub-file-paths=sub:subtitles`` is specified, mpv
+ searches for subtitle files in these directories:
+
+ - ``/path/to/video/``
+ - ``/path/to/video/sub/``
+ - ``/path/to/video/subtitles/``
+ - the ``sub`` configuration subdirectory (usually ``~/.config/mpv/sub/``)
+
+ This is a path list option. See `List Options`_ for details.
+
+``--sub-visibility``, ``--no-sub-visibility``
+ Can be used to disable display of subtitles, but still select and decode
+ them.
+
+``--secondary-sub-visibility``, ``--no-secondary-sub-visibility``
+ Can be used to disable display of secondary subtitles, but still select and
+ decode them.
+
+``--sub-clear-on-seek``
+ (Obscure, rarely useful.) Can be used to play broken mkv files with
+ duplicate ReadOrder fields. ReadOrder is the first field in a
+ Matroska-style ASS subtitle packets. It should be unique, and libass
+ uses it for fast elimination of duplicates. This option disables caching
+ of subtitles across seeks, so after a seek libass can't eliminate subtitle
+ packets with the same ReadOrder as earlier packets.
+
+``--teletext-page=<1-999>``
+ This works for ``dvb_teletext`` subtitle streams, and if FFmpeg has been
+ compiled with support for it.
+
+``--sub-past-video-end``
+ After the last frame of video, if this option is enabled, subtitles will
+ continue to update based on audio timestamps. Otherwise, the subtitles
+ for the last video frame will stay onscreen.
+
+ Default: disabled
+
+``--sub-font=<name>``
+ Specify font to use for subtitles that do not themselves
+ specify a particular font. The default is ``sans-serif``.
+
+ .. admonition:: Examples
+
+ - ``--sub-font='Bitstream Vera Sans'``
+ - ``--sub-font='Comic Sans MS'``
+
+ .. note::
+
+ The ``--sub-font`` option (and many other style related ``--sub-``
+ options) are ignored when ASS-subtitles are rendered, unless the
+ ``--no-sub-ass`` option is specified.
+
+ This used to support fontconfig patterns. Starting with libass 0.13.0,
+ this stopped working.
+
+``--sub-font-size=<size>``
+ Specify the sub font size. The unit is the size in scaled pixels at a
+ window height of 720. The actual pixel size is scaled with the window
+ height: if the window height is larger or smaller than 720, the actual size
+ of the text increases or decreases as well.
+
+ Default: 55.
+
+``--sub-back-color=<color>``
+ See ``--sub-color``. Color used for sub text background. You can use
+ ``--sub-shadow-offset`` to change its size relative to the text.
+
+``--sub-blur=<0..20.0>``
+ Gaussian blur factor. 0 means no blur applied (default).
+
+``--sub-bold=<yes|no>``
+ Format text on bold.
+
+``--sub-italic=<yes|no>``
+ Format text on italic.
+
+``--sub-border-color=<color>``
+ See ``--sub-color``. Color used for the sub font border.
+
+``--sub-border-size=<size>``
+ Size of the sub font border in scaled pixels (see ``--sub-font-size``
+ for details). A value of 0 disables borders.
+
+ Default: 3.
+
+``--sub-color=<color>``
+ Specify the color used for unstyled text subtitles.
+
+ The color is specified in the form ``r/g/b``, where each color component
+ is specified as number in the range 0.0 to 1.0. It's also possible to
+ specify the transparency by using ``r/g/b/a``, where the alpha value 0
+ means fully transparent, and 1.0 means opaque. If the alpha component is
+ not given, the color is 100% opaque.
+
+ Passing a single number to the option sets the sub to gray, and the form
+ ``gray/a`` lets you specify alpha additionally.
+
+ .. admonition:: Examples
+
+ - ``--sub-color=1.0/0.0/0.0`` set sub to opaque red
+ - ``--sub-color=1.0/0.0/0.0/0.75`` set sub to opaque red with 75% alpha
+ - ``--sub-color=0.5/0.75`` set sub to 50% gray with 75% alpha
+
+ Alternatively, the color can be specified as a RGB hex triplet in the form
+ ``#RRGGBB``, where each 2-digit group expresses a color value in the
+ range 0 (``00``) to 255 (``FF``). For example, ``#FF0000`` is red.
+ This is similar to web colors. Alpha is given with ``#AARRGGBB``.
+
+ .. admonition:: Examples
+
+ - ``--sub-color='#FF0000'`` set sub to opaque red
+ - ``--sub-color='#C0808080'`` set sub to 50% gray with 75% alpha
+
+``--sub-margin-x=<size>``
+ Left and right screen margin for the subs in scaled pixels (see
+ ``--sub-font-size`` for details).
+
+ This option specifies the distance of the sub to the left, as well as at
+ which distance from the right border long sub text will be broken.
+
+ Default: 25.
+
+``--sub-margin-y=<size>``
+ Top and bottom screen margin for the subs in scaled pixels (see
+ ``--sub-font-size`` for details).
+
+ This option specifies the vertical margins of unstyled text subtitles.
+ If you just want to raise the vertical subtitle position, use ``--sub-pos``.
+
+ Default: 22.
+
+``--sub-align-x=<left|center|right>``
+ Control to which corner of the screen text subtitles should be
+ aligned to (default: ``center``).
+
+ Never applied to ASS subtitles, except in ``--no-sub-ass`` mode. Likewise,
+ this does not apply to image subtitles.
+
+``--sub-align-y=<top|center|bottom>``
+ Vertical position (default: ``bottom``).
+ Details see ``--sub-align-x``.
+
+``--sub-justify=<auto|left|center|right>``
+ Control how multi line subs are justified irrespective of where they
+ are aligned (default: ``auto`` which justifies as defined by
+ ``--sub-align-x``).
+ Left justification is recommended to make the subs easier to read
+ as it is easier for the eyes.
+
+``--sub-ass-justify=<yes|no>``
+ Applies justification as defined by ``--sub-justify`` on ASS subtitles
+ if ``--sub-ass-override`` is not set to ``no``.
+ Default: ``no``.
+
+``--sub-shadow-color=<color>``
+ See ``--sub-color``. Color used for sub text shadow.
+
+ .. note::
+
+ ignored when ``--sub-back-color`` is
+ specified (or more exactly: when that option is not set to completely
+ transparent).
+
+``--sub-shadow-offset=<size>``
+ Displacement of the sub text shadow in scaled pixels (see
+ ``--sub-font-size`` for details). A value of 0 disables shadows.
+
+ Default: 0.
+
+``--sub-spacing=<size>``
+ Horizontal sub font spacing in scaled pixels (see ``--sub-font-size``
+ for details). This value is added to the normal letter spacing. Negative
+ values are allowed.
+
+ Default: 0.
+
+``--sub-filter-sdh=<yes|no>``
+ Applies filter removing subtitle additions for the deaf or hard-of-hearing (SDH).
+ This is intended for English, but may in part work for other languages too.
+ The intention is that it can be always enabled so may not remove
+ all parts added.
+ It removes speaker labels (like MAN:), upper case text in parentheses and
+ any text in brackets.
+
+ Default: ``no``.
+
+``--sub-filter-sdh-harder=<yes|no>``
+ Do harder SDH filtering (if enabled by ``--sub-filter-sdh``).
+ Will also remove speaker labels and text within parentheses using both
+ lower and upper case letters.
+
+ Default: ``no``.
+
+``--sub-filter-regex-...=...``
+ Set a list of regular expressions to match on text subtitles, and remove any
+ lines that match (default: empty). This is a string list option. See
+ `List Options`_ for details. Normally, you should use
+ ``--sub-filter-regex-append=<regex>``, where each option use will append a
+ new regular expression, without having to fight escaping problems.
+
+ List items are matched in order. If a regular expression matches, the
+ process is stopped, and the subtitle line is discarded. The text matched
+ against is, by default, the ``Text`` field of ASS events (if the
+ subtitle format is different, it is always converted). This may include
+ formatting tags. Matching is case-insensitive, but how this is done depends
+ on the libc, and most likely works in ASCII only. It does not work on
+ bitmap/image subtitles. Unavailable on inferior OSes (requires POSIX regex
+ support).
+
+ .. admonition:: Example
+
+ ``--sub-filter-regex-append=opensubtitles\.org`` filters some ads.
+
+ Technically, using a list for matching is redundant, since you could just
+ use a single combined regular expression. But it helps with diagnosis,
+ ease of use, and temporarily disabling or enabling individual filters.
+
+ .. warning::
+
+ This is experimental. The semantics most likely will change, and if you
+ use this, you should be prepared to update the option later. Ideas
+ include replacing the regexes with a very primitive and small subset of
+ sed, or some method to control case-sensitivity.
+
+``--sub-filter-jsre-...=...``
+ Same as ``--sub-filter-regex`` but with JavaScript regular expressions.
+ Shares/affected-by all ``--sub-filter-regex-*`` control options (see below),
+ and also experimental. Requires only JavaScript support.
+
+``--sub-filter-regex-plain=<yes|no>``
+ Whether to first convert the ASS "Text" field to plain-text (default: no).
+ This strips ASS tags and applies ASS directives, like ``\N`` to new-line.
+ If the result is multi-line then the regexp anchors ``^`` and ``$`` match
+ each line, but still any match discards all lines.
+
+``--sub-filter-regex-warn=<yes|no>``
+ Log dropped lines with warning log level, instead of verbose (default: no).
+ Helpful for testing.
+
+``--sub-filter-regex-enable=<yes|no>``
+ Whether to enable regex filtering (default: yes). Note that if no regexes
+ are added to the ``--sub-filter-regex`` list, setting this option to ``yes``
+ has no effect. It's meant to easily disable or enable filtering
+ temporarily.
+
+``--sub-create-cc-track=<yes|no>``
+ For every video stream, create a closed captions track (default: no). The
+ only purpose is to make the track available for selection at the start of
+ playback, instead of creating it lazily. This applies only to
+ ``ATSC A53 Part 4 Closed Captions`` (displayed by mpv as subtitle tracks
+ using the codec ``eia_608``). The CC track is marked "default" and selected
+ according to the normal subtitle track selection rules. You can then use
+ ``--sid`` to explicitly select the correct track too.
+
+ If the video stream contains no closed captions, or if no video is being
+ decoded, the CC track will remain empty and will not show any text.
+
+``--sub-font-provider=<auto|none|fontconfig>``
+ Which libass font provider backend to use (default: auto). ``auto`` will
+ attempt to use the native font provider: fontconfig on Linux, CoreText on
+ macOS, DirectWrite on Windows. ``fontconfig`` forces fontconfig, if libass
+ was built with support (if not, it behaves like ``none``).
+
+ The ``none`` font provider effectively disables system fonts. It will still
+ attempt to use embedded fonts (unless ``--embeddedfonts=no`` is set; this is
+ the same behavior as with all other font providers), ``subfont.ttf`` if
+ provided, and fonts in the ``fonts`` sub-directory if provided. (The
+ fallback is more strict than that of other font providers, and if a font
+ name does not match, it may prefer not to render any text that uses the
+ missing font.)
+
+``--sub-fonts-dir=<path>``
+ Font files in this directory are used by mpv/libass for subtitles. Useful
+ if you do not want to install fonts to your system. Note that files in this
+ directory are loaded into memory before being used by mpv. If you have a
+ lot of fonts, consider using fonts.conf (see `FILES`_ section) to include
+ additional mpv user settings.
+
+ If this option is not specified, ``~~/fonts`` will be used by default.
+
+Window
+------
+
+``--title=<string>``
+ Set the window title. This is used for the video window, and if possible,
+ also sets the audio stream title.
+
+ Properties are expanded. (See `Property Expansion`_.)
+
+ .. warning::
+
+ There is a danger of this causing significant CPU usage, depending on
+ the properties used. Changing the window title is often a slow
+ operation, and if the title changes every frame, playback can be ruined.
+
+``--screen=<default|0-32>``
+ In multi-monitor configurations (i.e. a single desktop that spans across
+ multiple displays), this option tells mpv which screen to display the
+ video on.
+
+ .. admonition:: Note (X11)
+
+ This option does not work properly with all window managers. In these
+ cases, you can try to use ``--geometry`` to position the window
+ explicitly. It's also possible that the window manager provides native
+ features to control which screens application windows should use.
+
+ .. admonition:: Note (Wayland)
+
+ This option does not actually work on wayland since window placement is
+ not allowed. However setting this option does influence mpv's initial
+ guess at finding an output which may be useful for options like
+ ``--geometry`` or ``--autofit`` which depend on the monitor resolution.
+
+ See also ``--fs-screen``.
+
+``--screen-name=<string>``
+ In multi-monitor configurations, this option tells mpv which screen to
+ display the video on based on the screen name from the video backend. The
+ same caveats in the ``--screen`` option also apply here. This option is
+ ignored and does nothing if ``--screen`` is explicitly set.
+
+``--fullscreen``, ``--fs``
+ Fullscreen playback.
+
+``--fs-screen=<all|current|0-32>``
+ In multi-monitor configurations (i.e. a single desktop that spans across
+ multiple displays), this option tells mpv which screen to go fullscreen to.
+ If ``current`` is used mpv will fallback on what the user provided with
+ the ``screen`` option.
+
+ .. admonition:: Note (X11)
+
+ This option works properly only with window managers which
+ understand the EWMH ``_NET_WM_FULLSCREEN_MONITORS`` hint.
+
+ .. admonition:: Note (macOS)
+
+ ``all`` does not work on macOS and will behave like ``current``.
+
+ See also ``--screen``.
+
+``--fs-screen-name=<string>``
+ In multi-monitor configurations, this option tells mpv which screen to go
+ fullscreen to based on the screen name from the video backend. The same
+ caveats in the ``--fs-screen`` option also apply here. This option is
+ ignored and does nothing if ``--fs-screen`` is explicitly set.
+
+``--keep-open=<yes|no|always>``
+ Do not terminate when playing or seeking beyond the end of the file, and
+ there is no next file to be played (and ``--loop`` is not used).
+ Instead, pause the player. When trying to seek beyond end of the file, the
+ player will attempt to seek to the last frame.
+
+ Normally, this will act like ``set pause yes`` on EOF, unless the
+ ``--keep-open-pause=no`` option is set.
+
+ The following arguments can be given:
+
+ :no: If the current file ends, go to the next file or terminate.
+ (Default.)
+ :yes: Don't terminate if the current file is the last playlist entry.
+ Equivalent to ``--keep-open`` without arguments.
+ :always: Like ``yes``, but also applies to files before the last playlist
+ entry. This means playback will never automatically advance to
+ the next file.
+
+ .. note::
+
+ This option is not respected when using ``--frames``. Explicitly
+ skipping to the next file if the binding uses ``force`` will terminate
+ playback as well.
+
+ Also, if errors or unusual circumstances happen, the player can quit
+ anyway.
+
+ Since mpv 0.6.0, this doesn't pause if there is a next file in the playlist,
+ or the playlist is looped. Approximately, this will pause when the player
+ would normally exit, but in practice there are corner cases in which this
+ is not the case (e.g. ``mpv --keep-open file.mkv /dev/null`` will play
+ file.mkv normally, then fail to open ``/dev/null``, then exit). (In
+ mpv 0.8.0, ``always`` was introduced, which restores the old behavior.)
+
+``--keep-open-pause=<yes|no>``
+ If set to ``no``, instead of pausing when ``--keep-open`` is active, just
+ stop at end of file and continue playing forward when you seek backwards
+ until end where it stops again. Default: ``yes``.
+
+``--image-display-duration=<seconds|inf>``
+ If the current file is an image, play the image for the given amount of
+ seconds (default: 1). ``inf`` means the file is kept open forever (until
+ the user stops playback manually).
+
+ Unlike ``--keep-open``, the player is not paused, but simply continues
+ playback until the time has elapsed. (It should not use any resources
+ during "playback".)
+
+ This affects image files, which are defined as having only 1 video frame
+ and no audio. The player may recognize certain non-images as images, for
+ example if ``--length`` is used to reduce the length to 1 frame, or if
+ you seek to the last frame.
+
+ This option does not affect the framerate used for ``mf://`` or
+ ``--merge-files``. For that, use ``--mf-fps`` instead.
+
+ Setting ``--image-display-duration`` hides the OSC and does not track
+ playback time on the command-line output, and also does not duplicate
+ the image frame when encoding. To force the player into "dumb mode"
+ and actually count out seconds, or to duplicate the image when
+ encoding, you need to use ``--demuxer=lavf --demuxer-lavf-o=loop=1``,
+ and use ``--length`` or ``--frames`` to stop after a particular time.
+
+``--force-window=<yes|no|immediate>``
+ Create a video output window even if there is no video. This can be useful
+ when pretending that mpv is a GUI application. Currently, the window
+ always has the size 640x480, and is subject to ``--geometry``,
+ ``--autofit``, and similar options.
+
+ .. warning::
+
+ The window is created only after initialization (to make sure default
+ window placement still works if the video size is different from the
+ ``--force-window`` default window size). This can be a problem if
+ initialization doesn't work perfectly, such as when opening URLs with
+ bad network connection, or opening broken video files. The ``immediate``
+ mode can be used to create the window always on program start, but this
+ may cause other issues.
+
+``--taskbar-progress``, ``--no-taskbar-progress``
+ (Windows only)
+ Enable/disable playback progress rendering in taskbar (Windows 7 and above).
+
+ Enabled by default.
+
+``--snap-window``
+ (Windows only) Snap the player window to screen edges.
+
+``--drag-and-drop=<no|auto|replace|append>``
+ (X11, Wayland and Windows only)
+ Controls the default behavior of drag and drop on platforms that support this.
+ ``auto`` will obey what the underlying os/platform gives mpv. Typically, holding
+ shift during the drag and drop will append the item to the playlist. Otherwise,
+ it will completely replace it. ``replace`` and ``append`` always force replacing
+ and appending to the playlist respectively. ``no`` disables all drag and drop
+ behavior.
+
+``--ontop``
+ Makes the player window stay on top of other windows.
+
+ On Windows, if combined with fullscreen mode, this causes mpv to be
+ treated as exclusive fullscreen window that bypasses the Desktop Window
+ Manager.
+
+``--ontop-level=<window|system|desktop|level>``
+ (macOS only)
+ Sets the level of an ontop window (default: window).
+
+ :window: On top of all other windows.
+ :system: On top of system elements like Taskbar, Menubar and Dock.
+ :desktop: On top of the Desktop behind windows and Desktop icons.
+ :level: A level as integer.
+
+``--focus-on-open``, ``--no-focus-on-open``
+ (macOS only)
+ Focus the video window on creation and makes it the front most window. This
+ is on by default.
+
+``--window-corners=<default|donotround|round|roundsmall>``
+ (Windows only)
+ Set the preference for window corner rounding.
+
+ :default: Let the system decide whether or not to round window corners
+ :donotround: Never round window corners
+ :round: Round the corners if appropriate
+ :roundsmall: Round the corners if appropriate, with a small radius
+
+``--border``, ``--no-border``
+ Play video with window border and decorations. Since this is on by
+ default, use ``--no-border`` to disable the standard window decorations.
+
+``--title-bar``, ``--no-title-bar``
+ (Windows only)
+ Play video with the window title bar. Since this is on by default,
+ use --no-title-bar to hide the title bar. The --no-border option takes
+ precedence.
+
+``--on-all-workspaces``
+ (X11 and macOS only)
+ Show the video window on all virtual desktops.
+
+``--geometry=<[W[xH]][+-x+-y][/WS]>``, ``--geometry=<x:y>``
+ Adjust the initial window position or size. ``W`` and ``H`` set the window
+ size in pixels. ``x`` and ``y`` set the window position, measured in pixels
+ from the top-left corner of the screen to the top-left corner of the image
+ being displayed. If a percentage sign (``%``) is given after the argument,
+ it turns the value into a percentage of the screen size in that direction.
+ Positions are specified similar to the standard X11 ``--geometry`` option
+ format, in which e.g. +10-50 means "place 10 pixels from the left border and
+ 50 pixels from the lower border" and "--20+-10" means "place 20 pixels
+ beyond the right and 10 pixels beyond the top border". A trailing ``/``
+ followed by an integer denotes on which workspace (virtual desktop) the
+ window should appear (X11 only).
+
+ If an external window is specified using the ``--wid`` option, this
+ option is ignored.
+
+ The coordinates are relative to the screen given with ``--screen`` for the
+ video output drivers that fully support ``--screen``.
+
+ .. note::
+
+ Generally only supported by GUI VOs. Ignored for encoding.
+
+ .. admonition:: Note (macOS)
+
+ On macOS, the origin of the screen coordinate system is located on the
+ bottom-left corner. For instance, ``0:0`` will place the window at the
+ bottom-left of the screen.
+
+ .. admonition:: Note (X11)
+
+ This option does not work properly with all window managers.
+
+ .. admonition:: Examples
+
+ ``50:40``
+ Places the window at x=50, y=40.
+ ``50%:50%``
+ Places the window in the middle of the screen.
+ ``100%:100%``
+ Places the window at the bottom right corner of the screen.
+ ``50%``
+ Sets the window width to half the screen width. Window height is set
+ so that the window has the video aspect ratio.
+ ``50%x50%``
+ Forces the window width and height to half the screen width and
+ height. Will show black borders to compensate for the video aspect
+ ratio (with most VOs and without ``--no-keepaspect``).
+ ``50%+10+10/2``
+ Sets the window to half the screen widths, and positions it 10
+ pixels below/left of the top left corner of the screen, on the
+ second workspace.
+
+ See also ``--autofit`` and ``--autofit-larger`` for fitting the window into
+ a given size without changing aspect ratio.
+
+``--autofit=<[W[xH]]>``
+ Set the initial window size to a maximum size specified by ``WxH``, without
+ changing the window's aspect ratio. The size is measured in pixels, or if
+ a number is followed by a percentage sign (``%``), in percents of the
+ screen size.
+
+ This option never changes the aspect ratio of the window. If the aspect
+ ratio mismatches, the window's size is reduced until it fits into the
+ specified size.
+
+ Window position is not taken into account, nor is it modified by this
+ option (the window manager still may place the window differently depending
+ on size). Use ``--geometry`` to change the window position. Its effects
+ are applied after this option.
+
+ See ``--geometry`` for details how this is handled with multi-monitor
+ setups.
+
+ Use ``--autofit-larger`` instead if you just want to limit the maximum size
+ of the window, rather than always forcing a window size.
+
+ Use ``--geometry`` if you want to force both window width and height to a
+ specific size.
+
+ .. note::
+
+ Generally only supported by GUI VOs. Ignored for encoding.
+
+ .. admonition:: Examples
+
+ ``70%``
+ Make the window width 70% of the screen size, keeping aspect ratio.
+ ``1000``
+ Set the window width to 1000 pixels, keeping aspect ratio.
+ ``70%x60%``
+ Make the window as large as possible, without being wider than 70%
+ of the screen width, or higher than 60% of the screen height.
+
+``--autofit-larger=<[W[xH]]>``
+ This option behaves exactly like ``--autofit``, except the window size is
+ only changed if the window would be larger than the specified size.
+
+ .. admonition:: Example
+
+ ``90%x80%``
+ If the video is larger than 90% of the screen width or 80% of the
+ screen height, make the window smaller until either its width is 90%
+ of the screen, or its height is 80% of the screen.
+
+``--autofit-smaller=<[W[xH]]>``
+ This option behaves exactly like ``--autofit``, except that it sets the
+ minimum size of the window (just as ``--autofit-larger`` sets the maximum).
+
+ .. admonition:: Example
+
+ ``500x500``
+ Make the window at least 500 pixels wide and 500 pixels high
+ (depending on the video aspect ratio, the width or height will be
+ larger than 500 in order to keep the aspect ratio the same).
+
+``--window-scale=<factor>``
+ Resize the video window to a multiple (or fraction) of the video size. This
+ option is applied before ``--autofit`` and other options are applied (so
+ they override this option).
+
+ For example, ``--window-scale=0.5`` would show the window at half the
+ video size.
+
+``--window-minimized=<yes|no>``
+ Whether the video window is minimized or not. Setting this will minimize,
+ or unminimize, the video window if the current VO supports it. Note that
+ some VOs may support minimization while not supporting unminimization
+ (eg: Wayland).
+
+ Whether this option and ``--window-maximized`` work on program start or
+ at runtime, and whether they're (at runtime) updated to reflect the actual
+ window state, heavily depends on the VO and the windowing system. Some VOs
+ simply do not implement them or parts of them, while other VOs may be
+ restricted by the windowing systems (especially Wayland).
+
+``--window-maximized=<yes|no>``
+ Whether the video window is maximized or not. Setting this will maximize,
+ or unmaximize, the video window if the current VO supports it. See
+ ``--window-minimized`` for further remarks.
+
+``--cursor-autohide=<number|no|always>``
+ Make mouse cursor automatically hide after given number of milliseconds
+ (default: 1000 ms). ``no`` will disable cursor autohide. ``always``
+ means the cursor will stay hidden.
+
+``--cursor-autohide-fs-only``
+ If this option is given, the cursor is always visible in windowed mode. In
+ fullscreen mode, the cursor is shown or hidden according to
+ ``--cursor-autohide``.
+
+``--force-rgba-osd-rendering``
+ Change how some video outputs render the OSD and text subtitles. This
+ does not change appearance of the subtitles and only has performance
+ implications. For VOs which support native ASS rendering (like ``gpu``,
+ ``vdpau``, ``direct3d``), this can be slightly faster or slower,
+ depending on GPU drivers and hardware. For other VOs, this just makes
+ rendering slower.
+
+``--force-render``
+ Forces mpv to always render frames regardless of the visibility of the
+ window. Currently only affects X11 and Wayland VOs since they are the
+ only ones that have this optimization (i.e. everything else always renders
+ regardless of visibility).
+
+``--force-window-position``
+ Forcefully move mpv's video output window to default location whenever
+ there is a change in video parameters, video stream or file. This used to
+ be the default behavior. Currently only affects X11 and SDL VOs.
+
+``--auto-window-resize=<yes|no>``
+ (Wayland, Win32, and X11)
+ By default, mpv will automatically resize itself if the video's size changes
+ (i.e. advancing forward in a playlist). Setting this to ``no`` disables this
+ behavior so the window size never changes automatically. This option does
+ not have any impact on the ``--autofit`` or ``--geometry`` options.
+
+``--no-keepaspect``, ``--keepaspect``
+ ``--no-keepaspect`` will always stretch the video to window size, and will
+ disable the window manager hints that force the window aspect ratio.
+ (Ignored in fullscreen mode.)
+
+``--no-keepaspect-window``, ``--keepaspect-window``
+ ``--keepaspect-window`` (the default) will lock the window size to the
+ video aspect. ``--no-keepaspect-window`` disables this behavior, and will
+ instead add black bars if window aspect and video aspect mismatch. Whether
+ this actually works depends on the VO backend.
+ (Ignored in fullscreen mode.)
+
+``--monitoraspect=<ratio>``
+ Set the aspect ratio of your monitor or TV screen. A value of 0 disables a
+ previous setting (e.g. in the config file). Overrides the
+ ``--monitorpixelaspect`` setting if enabled.
+
+ See also ``--monitorpixelaspect`` and ``--video-aspect-override``.
+
+ .. admonition:: Examples
+
+ - ``--monitoraspect=4:3`` or ``--monitoraspect=1.3333``
+ - ``--monitoraspect=16:9`` or ``--monitoraspect=1.7777``
+
+``--hidpi-window-scale``, ``--no-hidpi-window-scale``
+ (macOS, Windows, X11, and Wayland only)
+ Scale the window size according to the backing scale factor (default: yes).
+ On regular HiDPI resolutions the window opens with double the size but appears
+ as having the same size as on non-HiDPI resolutions.
+
+``--native-fs``, ``--no-native-fs``
+ (macOS only)
+ Uses the native fullscreen mechanism of the OS (default: yes).
+
+``--monitorpixelaspect=<ratio>``
+ Set the aspect of a single pixel of your monitor or TV screen (default:
+ 1). A value of 1 means square pixels (correct for (almost?) all LCDs). See
+ also ``--monitoraspect`` and ``--video-aspect-override``.
+
+``--stop-screensaver=<yes|no|always>``
+ Turns off the screensaver (or screen blanker and similar mechanisms) at
+ startup and turns it on again on exit (default: yes). When using ``yes``,
+ the screensaver will re-enable when playback is not active. ``always`` will
+ always disable the screensaver. Note that stopping the screensaver is only
+ possible if a video output is available (i.e. there is an open mpv window).
+
+ This is not supported on all video outputs or platforms. Sometimes it is
+ implemented, but does not work (especially with Linux "desktops"). Read the
+ `Disabling Screensaver`_ section very carefully.
+
+``--wid=<ID>``
+ This tells mpv to attach to an existing window. If a VO is selected that
+ supports this option, it will use that window for video output. mpv will
+ scale the video to the size of this window, and will add black bars to
+ compensate if the aspect ratio of the video is different.
+
+ On X11, the ID is interpreted as a ``Window`` on X11. Unlike
+ MPlayer/mplayer2, mpv always creates its own window, and sets the wid
+ window as parent. The window will always be resized to cover the parent
+ window fully. The value ``0`` is interpreted specially, and mpv will
+ draw directly on the root window.
+
+ On win32, the ID is interpreted as ``HWND``. Pass it as value cast to
+ ``uint32_t`` (all Windows handles are 32-bit), this is important as mpv will
+ not accept negative values. mpv will create its own window and set the
+ wid window as parent, like with X11.
+
+ On macOS/Cocoa, the ID is interpreted as ``NSView*``. Pass it as value cast
+ to ``intptr_t``. mpv will create its own sub-view. Because macOS does not
+ support window embedding of foreign processes, this works only with libmpv,
+ and will crash when used from the command line.
+
+ On Android, the ID is interpreted as ``android.view.Surface``. Pass it as a
+ value cast to ``intptr_t``. Use with ``--vo=mediacodec_embed`` and
+ ``--hwdec=mediacodec`` for direct rendering using MediaCodec, or with
+ ``--vo=gpu --gpu-context=android`` (with or without ``--hwdec=mediacodec``).
+
+``--no-window-dragging``
+ Don't move the window when clicking on it and moving the mouse pointer.
+
+``--x11-name=<string>``
+ Set the window class name for X11-based video output methods.
+
+``--x11-netwm=<yes|no|auto>``
+ (X11 only)
+ Control the use of NetWM protocol features.
+
+ This may or may not help with broken window managers. This provides some
+ functionality that was implemented by the now removed ``--fstype`` option.
+ Actually, it is not known to the developers to which degree this option
+ was needed, so feedback is welcome.
+
+ Specifically, ``yes`` will force use of NetWM fullscreen support, even if
+ not advertised by the WM. This can be useful for WMs that are broken on
+ purpose, like XMonad. (XMonad supposedly doesn't advertise fullscreen
+ support, because Flash uses it. Apparently, applications which want to
+ use fullscreen anyway are supposed to either ignore the NetWM support hints,
+ or provide a workaround. Shame on XMonad for deliberately breaking X
+ protocols (as if X isn't bad enough already).
+
+ By default, NetWM support is autodetected (``auto``).
+
+ This option might be removed in the future.
+
+``--x11-bypass-compositor=<yes|no|fs-only|never>``
+ If set to ``yes``, then ask the compositor to unredirect the mpv window
+ (default: ``fs-only``). This uses the ``_NET_WM_BYPASS_COMPOSITOR`` hint.
+
+ ``fs-only`` asks the window manager to disable the compositor only in
+ fullscreen mode.
+
+ ``no`` sets ``_NET_WM_BYPASS_COMPOSITOR`` to 0, which is the default value
+ as declared by the EWMH specification, i.e. no change is done.
+
+ ``never`` asks the window manager to never disable the compositor.
+
+``--x11-present=<no|auto|yes>``
+ Whether or not to use presentation statistics from X11's presentation
+ extension (default: ``auto``).
+
+ mpv asks X11 for present events which it then may use for more accurate
+ frame presentation. This only has an effect if ``--video-sync=display-...``
+ is being used.
+
+ The auto option enumerates XRandr providers for autodetection. If amd, radeon,
+ intel, or nouveau (the standard x86 Mesa drivers) is found and nvidia is NOT
+ found, presentation feedback is enabled. Other drivers are not assumed to
+ work, so they are not enabled automatically.
+
+ ``yes`` or ``no`` can still be passed regardless to enable/disable this
+ mechanism in case there is good/bad behavior with whatever your combination
+ of hardware/drivers/etc. happens to be.
+
+``--x11-wid-title`` ``--no-x11-wid-title``
+ Whether or not to set the window title when mpv is embedded on X11 (default:
+ ``no``).
+
+
+Disc Devices
+------------
+
+``--cdda-device=<path>``
+ Specify the CD device for CDDA playback (default: ``/dev/cdrom``).
+
+``--dvd-device=<path>``
+ Specify the DVD device or .iso filename (default: ``/dev/dvd``). You can
+ also specify a directory that contains files previously copied directly
+ from a DVD (with e.g. vobcopy).
+
+ .. admonition:: Example
+
+ ``mpv dvd:// --dvd-device=/path/to/dvd/``
+
+``--bluray-device=<path>``
+ (Blu-ray only)
+ Specify the Blu-ray disc location. Must be a directory with Blu-ray
+ structure.
+
+ .. admonition:: Example
+
+ ``mpv bd:// --bluray-device=/path/to/bd/``
+
+``--cdda-...``
+ These options can be used to tune the CD Audio reading feature of mpv.
+
+``--cdda-speed=<value>``
+ Set CD spin speed.
+
+``--cdda-paranoia=<0-2>``
+ Set paranoia level. Values other than 0 seem to break playback of
+ anything but the first track.
+
+ :0: disable checking (default)
+ :1: overlap checking only
+ :2: full data correction and verification
+
+``--cdda-sector-size=<value>``
+ Set atomic read size.
+
+``--cdda-overlap=<value>``
+ Force minimum overlap search during verification to <value> sectors.
+
+``--cdda-toc-offset=<value>``
+ Add ``<value>`` sectors to the values reported when addressing tracks.
+ May be negative.
+
+``--cdda-skip=<yes|no>``
+ (Never) accept imperfect data reconstruction.
+
+``--cdda-cdtext=<yes|no>``
+ Print CD text. This is disabled by default, because it ruins performance
+ with CD-ROM drives for unknown reasons.
+
+``--dvd-speed=<speed>``
+ Try to limit DVD speed (default: 0, no change). DVD base speed is 1385
+ kB/s, so an 8x drive can read at speeds up to 11080 kB/s. Slower speeds
+ make the drive more quiet. For watching DVDs, 2700 kB/s should be quiet and
+ fast enough. mpv resets the speed to the drive default value on close.
+ Values of at least 100 mean speed in kB/s. Values less than 100 mean
+ multiples of 1385 kB/s, i.e. ``--dvd-speed=8`` selects 11080 kB/s.
+
+ .. note::
+
+ You need write access to the DVD device to change the speed.
+
+``--dvd-angle=<ID>``
+ Some DVDs contain scenes that can be viewed from multiple angles.
+ This option tells mpv which angle to use (default: 1).
+
+
+
+Equalizer
+---------
+
+``--brightness=<-100-100>``
+ Adjust the brightness of the video signal (default: 0). Not supported by
+ all video output drivers.
+
+``--contrast=<-100-100>``
+ Adjust the contrast of the video signal (default: 0). Not supported by all
+ video output drivers.
+
+``--saturation=<-100-100>``
+ Adjust the saturation of the video signal (default: 0). You can get
+ grayscale output with this option. Not supported by all video output
+ drivers.
+
+``--gamma=<-100-100>``
+ Adjust the gamma of the video signal (default: 0). Not supported by all
+ video output drivers.
+
+``--hue=<-100-100>``
+ Adjust the hue of the video signal (default: 0). You can get a colored
+ negative of the image with this option. Not supported by all video output
+ drivers.
+
+Demuxer
+-------
+
+``--demuxer=<[+]name>``
+ Force demuxer type. Use a '+' before the name to force it; this will skip
+ some checks. Give the demuxer name as printed by ``--demuxer=help``.
+
+``--demuxer-lavf-analyzeduration=<value>``
+ Maximum length in seconds to analyze the stream properties.
+
+``--demuxer-lavf-probe-info=<yes|no|auto|nostreams>``
+ Whether to probe stream information (default: auto). Technically, this
+ controls whether libavformat's ``avformat_find_stream_info()`` function
+ is called. Usually it's safer to call it, but it can also make startup
+ slower.
+
+ The ``auto`` choice (the default) tries to skip this for a few know-safe
+ whitelisted formats, while calling it for everything else.
+
+ The ``nostreams`` choice only calls it if and only if the file seems to
+ contain no streams after opening (helpful in cases when calling the function
+ is needed to detect streams at all, such as with FLV files).
+
+``--demuxer-lavf-probescore=<1-100>``
+ Minimum required libavformat probe score. Lower values will require
+ less data to be loaded (makes streams start faster), but makes file
+ format detection less reliable. Can be used to force auto-detected
+ libavformat demuxers, even if libavformat considers the detection not
+ reliable enough. (Default: 26.)
+
+``--demuxer-lavf-allow-mimetype=<yes|no>``
+ Allow deriving the format from the HTTP MIME type (default: yes). Set
+ this to no in case playing things from HTTP mysteriously fails, even
+ though the same files work from local disk.
+
+ This is default in order to reduce latency when opening HTTP streams.
+
+``--demuxer-lavf-format=<name>``
+ Force a specific libavformat demuxer.
+
+``--demuxer-lavf-hacks=<yes|no>``
+ By default, some formats will be handled differently from other formats
+ by explicitly checking for them. Most of these compensate for weird or
+ imperfect behavior from libavformat demuxers. Passing ``no`` disables
+ these. For debugging and testing only.
+
+``--demuxer-lavf-o=<key>=<value>[,<key>=<value>[,...]]``
+ Pass AVOptions to libavformat demuxer.
+
+ Note, a patch to make the *o=* unneeded and pass all unknown options
+ through the AVOption system is welcome. A full list of AVOptions can
+ be found in the FFmpeg manual. Note that some options may conflict
+ with mpv options.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ ``--demuxer-lavf-o=fflags=+ignidx``
+
+``--demuxer-lavf-probesize=<value>``
+ Maximum amount of data to probe during the detection phase. In the
+ case of MPEG-TS this value identifies the maximum number of TS packets
+ to scan.
+
+``--demuxer-lavf-buffersize=<value>``
+ Size of the stream read buffer allocated for libavformat in bytes
+ (default: 32768). Lowering the size could lower latency. Note that
+ libavformat might reallocate the buffer internally, or not fully use all
+ of it.
+
+``--demuxer-lavf-linearize-timestamps=<yes|no|auto>``
+ Attempt to linearize timestamp resets in demuxed streams (default: auto).
+ This was tested only for single audio streams. It's unknown whether it
+ works correctly for video (but likely won't). Note that the implementation
+ is slightly incorrect either way, and will introduce a discontinuity by
+ about 1 codec frame size.
+
+ The ``auto`` mode enables this for OGG audio stream. This covers the common
+ and annoying case of OGG web radio streams. Some of these will reset
+ timestamps to 0 every time a new song begins. This breaks the mpv seekable
+ cache, which can't deal with timestamp resets. Note that FFmpeg/libavformat's
+ seeking API can't deal with this either; it's likely that if this option
+ breaks this even more, while if it's disabled, you can at least seek within
+ the first song in the stream. Well, you won't get anything useful either
+ way if the seek is outside of mpv's cache.
+
+``--demuxer-lavf-propagate-opts=<yes|no>``
+ Propagate FFmpeg-level options to recursively opened connections (default:
+ yes). This is needed because FFmpeg will apply these settings to nested
+ AVIO contexts automatically. On the other hand, this could break in certain
+ situations - it's the FFmpeg API, you just can't win.
+
+ This affects in particular the ``--timeout`` option and anything passed
+ with ``--demuxer-lavf-o``.
+
+ If this option is deemed unnecessary at some point in the future, it will
+ be removed without notice.
+
+``--demuxer-mkv-subtitle-preroll=<yes|index|no>``
+ Try harder to show embedded soft subtitles when seeking somewhere. Normally,
+ it can happen that the subtitle at the seek target is not shown due to how
+ some container file formats are designed. The subtitles appear only if
+ seeking before or exactly to the position a subtitle first appears. To
+ make this worse, subtitles are often timed to appear a very small amount
+ before the associated video frame, so that seeking to the video frame
+ typically does not demux the subtitle at that position.
+
+ Enabling this option makes the demuxer start reading data a bit before the
+ seek target, so that subtitles appear correctly. Note that this makes
+ seeking slower, and is not guaranteed to always work. It only works if the
+ subtitle is close enough to the seek target.
+
+ Works with the internal Matroska demuxer only. Always enabled for absolute
+ and hr-seeks, and this option changes behavior with relative or imprecise
+ seeks only.
+
+ You can use the ``--demuxer-mkv-subtitle-preroll-secs`` option to specify
+ how much data the demuxer should pre-read at most in order to find subtitle
+ packets that may overlap. Setting this to 0 will effectively disable this
+ preroll mechanism. Setting a very large value can make seeking very slow,
+ and an extremely large value would completely reread the entire file from
+ start to seek target on every seek - seeking can become slower towards the
+ end of the file. The details are messy, and the value is actually rounded
+ down to the cluster with the previous video keyframe.
+
+ Some files, especially files muxed with newer mkvmerge versions, have
+ information embedded that can be used to determine what subtitle packets
+ overlap with a seek target. In these cases, mpv will reduce the amount
+ of data read to a minimum. (Although it will still read *all* data between
+ the cluster that contains the first wanted subtitle packet, and the seek
+ target.) If the ``index`` choice (which is the default) is specified, then
+ prerolling will be done only if this information is actually available. If
+ this method is used, the maximum amount of data to skip can be additionally
+ controlled by ``--demuxer-mkv-subtitle-preroll-secs-index`` (it still uses
+ the value of the option without ``-index`` if that is higher).
+
+ See also ``--hr-seek-demuxer-offset`` option. This option can achieve a
+ similar effect, but only if hr-seek is active. It works with any demuxer,
+ but makes seeking much slower, as it has to decode audio and video data
+ instead of just skipping over it.
+
+``--demuxer-mkv-subtitle-preroll-secs=<value>``
+ See ``--demuxer-mkv-subtitle-preroll``.
+
+``--demuxer-mkv-subtitle-preroll-secs-index=<value>``
+ See ``--demuxer-mkv-subtitle-preroll``.
+
+``--demuxer-mkv-probe-start-time=<yes|no>``
+ Check the start time of Matroska files (default: yes). This simply reads the
+ first cluster timestamps and assumes it is the start time. Technically, this
+ also reads the first timestamp, which may increase latency by one frame
+ (which may be relevant for live streams).
+
+``--demuxer-mkv-probe-video-duration=<yes|no|full>``
+ When opening the file, seek to the end of it, and check what timestamp the
+ last video packet has, and report that as file duration. This is strictly
+ for compatibility with Haali only. In this mode, it's possible that opening
+ will be slower (especially when playing over http), or that behavior with
+ broken files is much worse. So don't use this option.
+
+ The ``yes`` mode merely uses the index and reads a small number of blocks
+ from the end of the file. The ``full`` mode actually traverses the entire
+ file and can make a reliable estimate even without an index present (such
+ as partial files).
+
+``--demuxer-rawaudio-channels=<value>``
+ Number of channels (or channel layout) if ``--demuxer=rawaudio`` is used
+ (default: stereo).
+
+``--demuxer-rawaudio-format=<value>``
+ Sample format for ``--demuxer=rawaudio`` (default: s16le).
+ Use ``--demuxer-rawaudio-format=help`` to get a list of all formats.
+
+``--demuxer-rawaudio-rate=<value>``
+ Sample rate for ``--demuxer=rawaudio`` (default: 44 kHz).
+
+``--demuxer-rawvideo-fps=<value>``
+ Rate in frames per second for ``--demuxer=rawvideo`` (default: 25.0).
+
+``--demuxer-rawvideo-w=<value>``, ``--demuxer-rawvideo-h=<value>``
+ Image dimension in pixels for ``--demuxer=rawvideo``.
+
+ .. admonition:: Example
+
+ Play a raw YUV sample::
+
+ mpv sample-720x576.yuv --demuxer=rawvideo \
+ --demuxer-rawvideo-w=720 --demuxer-rawvideo-h=576
+
+``--demuxer-rawvideo-format=<value>``
+ Color space (fourcc) in hex or string for ``--demuxer=rawvideo``
+ (default: ``YV12``).
+
+``--demuxer-rawvideo-mp-format=<value>``
+ Color space by internal video format for ``--demuxer=rawvideo``. Use
+ ``--demuxer-rawvideo-mp-format=help`` for a list of possible formats.
+
+``--demuxer-rawvideo-codec=<value>``
+ Set the video codec instead of selecting the rawvideo codec when using
+ ``--demuxer=rawvideo``. This uses the same values as codec names in
+ ``--vd`` (but it does not accept decoder names).
+
+``--demuxer-rawvideo-size=<value>``
+ Frame size in bytes when using ``--demuxer=rawvideo``.
+
+``--demuxer-max-bytes=<bytesize>``
+ This controls how much the demuxer is allowed to buffer ahead. The demuxer
+ will normally try to read ahead as much as necessary, or as much is
+ requested with ``--demuxer-readahead-secs``. The option can be used to
+ restrict the maximum readahead. This limits excessive readahead in case of
+ broken files or desynced playback. The demuxer will stop reading additional
+ packets as soon as one of the limits is reached. (The limits still can be
+ slightly overstepped due to technical reasons.)
+
+ Set these limits higher if you get a packet queue overflow warning, and
+ you think normal playback would be possible with a larger packet queue.
+
+ See ``--list-options`` for defaults and value range. ``<bytesize>`` options
+ accept suffixes such as ``KiB`` and ``MiB``.
+
+``--demuxer-max-back-bytes=<bytesize>``
+ This controls how much past data the demuxer is allowed to preserve. This
+ is useful only if the cache is enabled.
+
+ Unlike the forward cache, there is no control how many seconds are actually
+ cached - it will simply use as much memory this option allows. Setting this
+ option to 0 will strictly disable any back buffer, but this will lead to
+ the situation that the forward seek range starts after the current playback
+ position (as it removes past packets that are seek points).
+
+ If the end of the file is reached, the remaining unused forward buffer space
+ is "donated" to the backbuffer (unless the backbuffer size is set to 0, or
+ ``--demuxer-donate-buffer`` is set to ``no``).
+ This still limits the total cache usage to the sum of the forward and
+ backward cache, and effectively makes better use of the total allowed memory
+ budget. (The opposite does not happen: free backward buffer is never
+ "donated" to the forward buffer.)
+
+ Keep in mind that other buffers in the player (like decoders) will cause the
+ demuxer to cache "future" frames in the back buffer, which can skew the
+ impression about how much data the backbuffer contains.
+
+ See ``--list-options`` for defaults and value range.
+
+``--demuxer-donate-buffer=<yes|no>``
+ Whether to let the back buffer use part of the forward buffer (default: yes).
+ If set to ``yes``, the "donation" behavior described in the option
+ description for ``--demuxer-max-back-bytes`` is enabled. This means the
+ back buffer may use up memory up to the sum of the forward and back buffer
+ options, minus the active size of the forward buffer. If set to ``no``, the
+ options strictly limit the forward and back buffer sizes separately.
+
+ Note that if the end of the file is reached, the buffered data stays the
+ same, even if you seek back within the cache. This is because the back
+ buffer is only reduced when new data is read.
+
+``--demuxer-seekable-cache=<yes|no|auto>``
+ Debugging option to control whether seeking can use the demuxer cache
+ (default: auto). Normally you don't ever need to set this; the default
+ ``auto`` does the right thing and enables cache seeking it if ``--cache``
+ is set to ``yes`` (or is implied ``yes`` if ``--cache=auto``).
+
+ If enabled, short seek offsets will not trigger a low level demuxer seek
+ (which means for example that slow network round trips or FFmpeg seek bugs
+ can be avoided). If a seek cannot happen within the cached range, a low
+ level seek will be triggered. Seeking outside of the cache will start a new
+ cached range, but can discard the old cache range if the demuxer exhibits
+ certain unsupported behavior.
+
+ The special value ``auto`` means ``yes`` in the same situation as
+ ``--cache-secs`` is used (i.e. when the stream appears to be a network
+ stream or the stream cache is enabled).
+
+``--demuxer-thread=<yes|no>``
+ Run the demuxer in a separate thread, and let it prefetch a certain amount
+ of packets (default: yes). Having this enabled leads to smoother playback,
+ enables features like prefetching, and prevents that stuck network freezes
+ the player. On the other hand, it can add overhead, or the background
+ prefetching can hog CPU resources.
+
+ Disabling this option is not recommended. Use it for debugging only.
+
+``--demuxer-termination-timeout=<seconds>``
+ Number of seconds the player should wait to shutdown the demuxer (default:
+ 0.1). The player will wait up to this much time before it closes the
+ stream layer forcefully. Forceful closing usually means the network I/O is
+ given no chance to close its connections gracefully (of course the OS can
+ still close TCP connections properly), and might result in annoying messages
+ being logged, and in some cases, confused remote servers.
+
+ This timeout is usually only applied when loading has finished properly. If
+ loading is aborted by the user, or in some corner cases like removing
+ external tracks sourced from network during playback, forceful closing is
+ always used.
+
+``--demuxer-readahead-secs=<seconds>``
+ If ``--demuxer-thread`` is enabled, this controls how much the demuxer
+ should buffer ahead in seconds (default: 1). As long as no packet has
+ a timestamp difference higher than the readahead amount relative to the
+ last packet returned to the decoder, the demuxer keeps reading.
+
+ Note that enabling the cache (such as ``--cache=yes``, or if the input
+ is considered a network stream, and ``--cache=auto`` is used), this option
+ is mostly ignored. (``--cache-secs`` will override this. Technically, the
+ maximum of both options is used.)
+
+ The main purpose of this option is to limit the readhead for local playback,
+ since a large readahead value is not overly useful in this case.
+
+ (This value tends to be fuzzy, because many file formats don't store linear
+ timestamps.)
+
+``--demuxer-hysteresis-secs=<seconds>``
+ Once the demuxer limit is reached (``--demuxer-max-bytes``,
+ ``--demuxer-readahead-secs`` or ``--cache-secs``), this value can be used
+ to specify a hysteresis before the demuxer will buffer ahead again. This
+ specifies the maximum number of seconds from the current playback position
+ that needs to be remaining in the cache before the demuxer will continue
+ buffering ahead.
+
+ For example, with a value of 10 seconds specified, the demuxer will buffer
+ ahead up to the demuxer limit and won't start buffering ahead again until
+ there is only 10 seconds of content left in the cache.
+
+ This can provide significant power savings and reduce load by making the
+ demuxer only buffer ahead in chunks at a time rather than buffering ahead
+ nonstop to keep the cache filled.
+
+ If you want to save power and reduce load, configure this to a small number
+ that's much lower than ``--cache-secs`` or ``--demuxer-readahead-secs``.
+ If it takes a long time to buffer anything at all for a given stream (like
+ when reading from a very slow disk is involved), then the hysteresis value
+ should be larger to compensate.
+
+ The default value is 0 seconds, which disables the caching hysteresis. A
+ value of 10 seconds probably works well for most usecases.
+
+``--prefetch-playlist=<yes|no>``
+ Prefetch next playlist entry while playback of the current entry is ending
+ (default: no).
+
+ This does not prefill the cache with the video data of the next URL.
+ Prefetching video data is supported only for the current playlist entry,
+ and depends on the demuxer cache settings (on by default). This merely
+ opens the URL of the next playlist entry as soon the current URL is fully
+ read.
+
+ This does **not** work with URLs resolved by the ``youtube-dl`` wrapper,
+ and it won't.
+
+ This can give subtly wrong results if per-file options are used, or if
+ options are changed in the time window between prefetching start and next
+ file played.
+
+ This can occasionally make wrong prefetching decisions. For example, it
+ can't predict whether you go backwards in the playlist, and assumes you
+ won't edit the playlist.
+
+ Highly experimental.
+
+``--force-seekable=<yes|no>``
+ If the player thinks that the media is not seekable (e.g. playing from a
+ pipe, or it's an http stream with a server that doesn't support range
+ requests), seeking will be disabled. This option can forcibly enable it.
+ For seeks within the cache, there's a good chance of success.
+
+``--demuxer-cache-wait=<yes|no>``
+ Before starting playback, read data until either the end of the file was
+ reached, or the demuxer cache has reached maximum capacity. Only once this
+ is done, playback starts. This intentionally happens before the initial
+ seek triggered with ``--start``. This does not change any runtime behavior
+ after the initial caching. This option is useless if the file cannot be
+ cached completely.
+
+``--rar-list-all-volumes=<yes|no>``
+ When opening multi-volume rar files, open all volumes to create a full list
+ of contained files (default: no). If disabled, only the archive entries
+ whose headers are located within the first volume are listed (and thus
+ played when opening a .rar file with mpv). Doing so speeds up opening, and
+ the typical idiotic use-case of playing uncompressed multi-volume rar files
+ that contain a single media file is made faster.
+
+ Opening is still slow, because for unknown, idiotic, and unnecessary reasons
+ libarchive opens all volumes anyway when playing the main file, even though
+ mpv iterated no archive entries yet.
+
+``--directory-mode=<auto|lazy|recursive|ignore>``
+ When opening a directory, open subdirectories lazily, recursively or not at
+ all. The default is ``auto``, which behaves like ``recursive`` with
+ ``--shuffle``, and like ``lazy`` otherwise.
+
+Input
+-----
+
+``--native-keyrepeat``
+ Use system settings for keyrepeat delay and rate, instead of
+ ``--input-ar-delay`` and ``--input-ar-rate``. (Whether this applies
+ depends on the VO backend and how it handles keyboard input. Does not
+ apply to terminal input.)
+
+``--input-ar-delay``
+ Delay in milliseconds before we start to autorepeat a key (0 to disable).
+
+``--input-ar-rate``
+ Number of key presses to generate per second on autorepeat.
+
+``--input-conf=<filename>``
+ Specify input configuration file other than the default location in the mpv
+ configuration directory (usually ``~/.config/mpv/input.conf``).
+
+``--no-input-default-bindings``
+ Disable default-level ("weak") key bindings. These are bindings which config
+ files like ``input.conf`` can override. It currently affects the builtin key
+ bindings, and keys which scripts bind using ``mp.add_key_binding`` (but not
+ ``mp.add_forced_key_binding`` because this overrides ``input.conf``).
+
+``--no-input-builtin-bindings``
+ Disable loading of built-in key bindings during start-up. This option is
+ applied only during (lib)mpv initialization, and if used then it will not
+ be not possible to enable them later. May be useful to libmpv clients.
+
+``--input-cmdlist``
+ Prints all commands that can be bound to keys.
+
+``--input-doubleclick-time=<milliseconds>``
+ Time in milliseconds to recognize two consecutive button presses as a
+ double-click (default: 300).
+
+``--input-keylist``
+ Prints all keys that can be bound to commands.
+
+``--input-key-fifo-size=<2-65000>``
+ Specify the size of the FIFO that buffers key events (default: 7). If it
+ is too small, some events may be lost. The main disadvantage of setting it
+ to a very large value is that if you hold down a key triggering some
+ particularly slow command then the player may be unresponsive while it
+ processes all the queued commands.
+
+``--input-test``
+ Input test mode. Instead of executing commands on key presses, mpv
+ will show the keys and the bound commands on the OSD. Has to be used
+ with a dummy video, and the normal ways to quit the player will not
+ work (key bindings that normally quit will be shown on OSD only, just
+ like any other binding). See `INPUT.CONF`_.
+
+``--input-terminal``, ``--no-input-terminal``
+ ``--no-input-terminal`` prevents the player from reading key events from
+ standard input. Useful when reading data from standard input. This is
+ automatically enabled when ``-`` is found on the command line. There are
+ situations where you have to set it manually, e.g. if you open
+ ``/dev/stdin`` (or the equivalent on your system), use stdin in a playlist
+ or intend to read from stdin later on via the loadfile or loadlist input
+ commands.
+
+``--input-ipc-server=<filename>``
+ Enable the IPC support and create the listening socket at the given path.
+
+ On Linux and Unix, the given path is a regular filesystem path. On Windows,
+ named pipes are used, so the path refers to the pipe namespace
+ (``\\.\pipe\<name>``). If the ``\\.\pipe\`` prefix is missing, mpv will add
+ it automatically before creating the pipe, so
+ ``--input-ipc-server=/tmp/mpv-socket`` and
+ ``--input-ipc-server=\\.\pipe\tmp\mpv-socket`` are equivalent for IPC on
+ Windows.
+
+ See `JSON IPC`_ for details.
+
+``--input-ipc-client=fd://<N>``
+ Connect a single IPC client to the given FD. This is somewhat similar to
+ ``--input-ipc-server``, except no socket is created, and instead the passed
+ FD is treated like a socket connection received from ``accept()``. In
+ practice, you could pass either a FD created by ``socketpair()``, or a pipe.
+ In both cases, you must sure the FD is actually inherited by mpv (do not
+ set the POSIX ``CLOEXEC`` flag).
+
+ The player quits when the connection is closed.
+
+ This is somewhat similar to the removed ``--input-file`` option, except it
+ supports only integer FDs, and cannot open actual paths.
+
+ .. admonition:: Example
+
+ ``--input-ipc-client=fd://123``
+
+ .. note::
+
+ Does not and will not work on Windows.
+
+ .. warning::
+
+ Writing to the ``input-ipc-server`` option at runtime will start another
+ instance of an IPC client handler for the ``input-ipc-client`` option,
+ because initialization is bundled, and this thing is stupid. This is a
+ bug. Writing to ``input-ipc-client`` at runtime will start another IPC
+ client handler for the new value, without stopping the old one, even if
+ the FD value is the same (but the string is different e.g. due to
+ whitespace). This is not a bug.
+
+``--input-gamepad=<yes|no>``
+ Enable/disable SDL2 Gamepad support. Disabled by default.
+
+``--input-cursor``, ``--no-input-cursor``
+ Permit mpv to receive pointer events reported by the video output
+ driver. Necessary to use the OSC, or to select the buttons in DVD menus.
+ Support depends on the VO in use.
+
+``--input-cursor-passthrough``, ``--no-input-cursor-passthrough``
+ (X11 and Wayland only)
+ Tell the backend windowing system to allow pointer events to passthrough
+ the mpv window. This allows windows under mpv to instead receive pointer
+ events as if the mpv window was never there.
+
+``--input-media-keys=<yes|no>``
+ On systems where mpv can choose between receiving media keys or letting
+ the system handle them - this option controls whether mpv should receive
+ them.
+
+ Default: yes (except for libmpv). macOS and Windows only, because elsewhere
+ mpv doesn't have a choice - the system decides whether to send media keys
+ to mpv. For instance, on X11 or Wayland, system-wide media keys are not
+ implemented. Whether media keys work when the mpv window is focused is
+ implementation-defined.
+
+``--input-right-alt-gr``, ``--no-input-right-alt-gr``
+ (Cocoa and Windows only)
+ Use the right Alt key as Alt Gr to produce special characters. If disabled,
+ count the right Alt as an Alt modifier key. Enabled by default.
+
+``--input-vo-keyboard=<yes|no>``
+ Disable all keyboard input on for VOs which can't participate in proper
+ keyboard input dispatching. May not affect all VOs. Generally useful for
+ embedding only.
+
+ On X11, a sub-window with input enabled grabs all keyboard input as long
+ as it is 1. a child of a focused window, and 2. the mouse is inside of
+ the sub-window. It can steal away all keyboard input from the
+ application embedding the mpv window, and on the other hand, the mpv
+ window will receive no input if the mouse is outside of the mpv window,
+ even though mpv has focus. Modern toolkits work around this weird X11
+ behavior, but naively embedding foreign windows breaks it.
+
+ The only way to handle this reasonably is using the XEmbed protocol, which
+ was designed to solve these problems. GTK provides ``GtkSocket``, which
+ supports XEmbed. Qt doesn't seem to provide anything working in newer
+ versions.
+
+ If the embedder supports XEmbed, input should work with default settings
+ and with this option disabled. Note that ``input-default-bindings`` is
+ disabled by default in libmpv as well - it should be enabled if you want
+ the mpv default key bindings.
+
+OSD
+---
+
+``--osc``, ``--no-osc``
+ Whether to load the on-screen-controller (default: yes).
+
+``--no-osd-bar``, ``--osd-bar``
+ Disable display of the OSD bar.
+
+ You can configure this on a per-command basis in input.conf using ``osd-``
+ prefixes, see ``Input Command Prefixes``. If you want to disable the OSD
+ completely, use ``--osd-level=0``.
+
+``--osd-on-seek=<no,bar,msg,msg-bar>``
+ Set what is displayed on the OSD during seeks. The default is ``bar``.
+
+ You can configure this on a per-command basis in input.conf using ``osd-``
+ prefixes, see ``Input Command Prefixes``.
+
+``--osd-duration=<time>``
+ Set the duration of the OSD messages in ms (default: 1000).
+
+``--osd-font=<name>``
+ Specify font to use for OSD. The default is ``sans-serif``.
+
+ .. admonition:: Examples
+
+ - ``--osd-font='Bitstream Vera Sans'``
+ - ``--osd-font='Comic Sans MS'``
+
+``--osd-font-size=<size>``
+ Specify the OSD font size. See ``--sub-font-size`` for details.
+
+ Default: 55.
+
+``--osd-msg1=<string>``
+ Show this string as message on OSD with OSD level 1 (visible by default).
+ The message will be visible by default, and as long as no other message
+ covers it, and the OSD level isn't changed (see ``--osd-level``).
+ Expands properties; see `Property Expansion`_.
+
+``--osd-msg2=<string>``
+ Similar to ``--osd-msg1``, but for OSD level 2. If this is an empty string
+ (default), then the playback time is shown.
+
+``--osd-msg3=<string>``
+ Similar to ``--osd-msg1``, but for OSD level 3. If this is an empty string
+ (default), then the playback time, duration, and some more information is
+ shown.
+
+ This is used for the ``show-progress`` command (by default mapped to ``P``),
+ and when seeking if enabled with ``--osd-on-seek`` or by ``osd-`` prefixes
+ in input.conf (see ``Input Command Prefixes``).
+
+ ``--osd-status-msg`` is a legacy equivalent (but with a minor difference).
+
+``--osd-status-msg=<string>``
+ Show a custom string during playback instead of the standard status text.
+ This overrides the status text used for ``--osd-level=3``, when using the
+ ``show-progress`` command (by default mapped to ``P``), and when seeking if
+ enabled with ``--osd-on-seek`` or ``osd-`` prefixes in input.conf (see
+ ``Input Command Prefixes``). Expands properties. See `Property Expansion`_.
+
+ This option has been replaced with ``--osd-msg3``. The only difference is
+ that this option implicitly includes ``${osd-sym-cc}``. This option is
+ ignored if ``--osd-msg3`` is not empty.
+
+``--osd-playing-msg=<string>``
+ Show a message on OSD when playback starts. The string is expanded for
+ properties, e.g. ``--osd-playing-msg='file: ${filename}'`` will show the
+ message ``file:`` followed by a space and the currently played filename.
+
+ See `Property Expansion`_.
+
+``--osd-playing-msg-duration=<time>``
+ Set the duration of ``osd-playing-msg`` in ms. If this is unset,
+ ``osd-playing-msg`` stays on screen for the duration of ``osd-duration``.
+
+``--osd-bar-align-x=<-1-1>``
+ Position of the OSD bar. -1 is far left, 0 is centered, 1 is far right.
+ Fractional values (like 0.5) are allowed.
+
+``--osd-bar-align-y=<-1-1>``
+ Position of the OSD bar. -1 is top, 0 is centered, 1 is bottom.
+ Fractional values (like 0.5) are allowed.
+
+``--osd-bar-w=<1-100>``
+ Width of the OSD bar, in percentage of the screen width (default: 75).
+ A value of 50 means the bar is half the screen wide.
+
+``--osd-bar-h=<0.1-50>``
+ Height of the OSD bar, in percentage of the screen height (default: 3.125).
+
+``--osd-back-color=<color>``
+ See ``--sub-color``. Color used for OSD text background.
+
+``--osd-blur=<0..20.0>``
+ Gaussian blur factor. 0 means no blur applied (default).
+
+``--osd-bold=<yes|no>``
+ Format text on bold.
+
+``--osd-italic=<yes|no>``
+ Format text on italic.
+
+``--osd-border-color=<color>``
+ See ``--sub-color``. Color used for the OSD font border.
+
+``--osd-border-size=<size>``
+ Size of the OSD font border in scaled pixels (see ``--sub-font-size``
+ for details). A value of 0 disables borders.
+
+ Default: 3.
+
+``--osd-color=<color>``
+ Specify the color used for OSD.
+ See ``--sub-color`` for details.
+
+``--osd-fractions``
+ Show OSD times with fractions of seconds (in millisecond precision). Useful
+ to see the exact timestamp of a video frame.
+
+``--osd-level=<0-3>``
+ Specifies which mode the OSD should start in.
+
+ :0: OSD completely disabled (subtitles only)
+ :1: enabled (shows up only on user interaction)
+ :2: enabled + current time visible by default
+ :3: enabled + ``--osd-status-msg`` (current time and status by default)
+
+``--osd-margin-x=<size>``
+ Left and right screen margin for the OSD in scaled pixels (see
+ ``--sub-font-size`` for details).
+
+ This option specifies the distance of the OSD to the left, as well as at
+ which distance from the right border long OSD text will be broken.
+
+ Default: 25.
+
+``--osd-margin-y=<size>``
+ Top and bottom screen margin for the OSD in scaled pixels (see
+ ``--sub-font-size`` for details).
+
+ This option specifies the vertical margins of the OSD.
+
+ Default: 22.
+
+``--osd-align-x=<left|center|right>``
+ Control to which corner of the screen OSD should be
+ aligned to (default: ``left``).
+
+``--osd-align-y=<top|center|bottom>``
+ Vertical position (default: ``top``).
+ Details see ``--osd-align-x``.
+
+``--osd-scale=<factor>``
+ OSD font size multiplier, multiplied with ``--osd-font-size`` value.
+
+``--osd-scale-by-window=<yes|no>``
+ Whether to scale the OSD with the window size (default: yes). If this is
+ disabled, ``--osd-font-size`` and other OSD options that use scaled pixels
+ are always in actual pixels. The effect is that changing the window size
+ won't change the OSD font size.
+
+``--osd-shadow-color=<color>``
+ See ``--sub-color``. Color used for OSD shadow.
+
+ .. note::
+
+ ignored when ``--osd-back-color`` is specified (or more exactly: when
+ that option is not set to completely transparent).
+
+``--osd-shadow-offset=<size>``
+ Displacement of the OSD shadow in scaled pixels (see
+ ``--sub-font-size`` for details). A value of 0 disables shadows.
+
+ Default: 0.
+
+``--osd-spacing=<size>``
+ Horizontal OSD/sub font spacing in scaled pixels (see ``--sub-font-size``
+ for details). This value is added to the normal letter spacing. Negative
+ values are allowed.
+
+ Default: 0.
+
+``--video-osd=<yes|no>``
+ Enabled OSD rendering on the video window (default: yes). This can be used
+ in situations where terminal OSD is preferred. If you just want to disable
+ all OSD rendering, use ``--osd-level=0``.
+
+ It does not affect subtitles or overlays created by scripts (in particular,
+ the OSC needs to be disabled with ``--no-osc``).
+
+ This option is somewhat experimental and could be replaced by another
+ mechanism in the future.
+
+``--osd-font-provider=<...>``
+ See ``--sub-font-provider`` for details and accepted values. Note that
+ unlike subtitles, OSD never uses embedded fonts from media files.
+
+``--osd-fonts-dir=<path>``
+ See ``--sub-fonts-dir`` for details. Defaults to ``~~/fonts``.
+
+Screenshot
+----------
+
+``--screenshot-format=<type>``
+ Set the image file type used for saving screenshots.
+
+ Available choices:
+
+ :png: PNG
+ :jpg: JPEG (default)
+ :jpeg: JPEG (alias for jpg)
+ :webp: WebP
+ :jxl: JPEG XL
+ :avif: AVIF
+
+``--screenshot-tag-colorspace=<yes|no>``
+ Tag screenshots with the appropriate colorspace (default: yes).
+
+ Note that not all formats support this. When it is unsupported, or when
+ this option is disabled, screenshots will be converted to sRGB before
+ being written.
+
+``--screenshot-high-bit-depth=<yes|no>``
+ If possible, write screenshots with a bit depth similar to the source
+ video (default: yes). This is interesting in particular for PNG, as this
+ sometimes triggers writing 16 bit PNGs with huge file sizes. This will also
+ include an unused alpha channel in the resulting files if 16 bit is used.
+
+``--screenshot-template=<template>``
+ Specify the filename template used to save screenshots. The template
+ specifies the filename without file extension, and can contain format
+ specifiers, which will be substituted when taking a screenshot.
+ By default, the template is ``mpv-shot%n``, which results in filenames like
+ ``mpv-shot0012.png`` for example.
+
+ The template can start with a relative or absolute path, in order to
+ specify a directory location where screenshots should be saved.
+
+ If the final screenshot filename points to an already existing file, the
+ file will not be overwritten. The screenshot will either not be saved, or if
+ the template contains ``%n``, saved using different, newly generated
+ filename.
+
+ Allowed format specifiers:
+
+ ``%[#][0X]n``
+ A sequence number, padded with zeros to length X (default: 04). E.g.
+ passing the format ``%04n`` will yield ``0012`` on the 12th screenshot.
+ The number is incremented every time a screenshot is taken or if the
+ file already exists. The length ``X`` must be in the range 0-9. With
+ the optional # sign, mpv will use the lowest available number. For
+ example, if you take three screenshots--0001, 0002, 0003--and delete
+ the first two, the next two screenshots will not be 0004 and 0005, but
+ 0001 and 0002 again.
+ ``%f``
+ Filename of the currently played video.
+ ``%F``
+ Same as ``%f``, but strip the file extension, including the dot.
+ ``%x``
+ Directory path of the currently played video. If the video is not on
+ the filesystem (but e.g. ``http://``), this expand to an empty string.
+ ``%X{fallback}``
+ Same as ``%x``, but if the video file is not on the filesystem, return
+ the fallback string inside the ``{...}``.
+ ``%p``
+ Current playback time, in the same format as used in the OSD. The
+ result is a string of the form "HH:MM:SS". For example, if the video is
+ at the time position 5 minutes and 34 seconds, ``%p`` will be replaced
+ with "00:05:34".
+ ``%P``
+ Similar to ``%p``, but extended with the playback time in milliseconds.
+ It is formatted as "HH:MM:SS.mmm", with "mmm" being the millisecond
+ part of the playback time.
+
+ .. note::
+
+ This is a simple way for getting unique per-frame timestamps. (Frame
+ numbers would be more intuitive, but are not easily implementable
+ because container formats usually use time stamps for identifying
+ frames.)
+ ``%wX``
+ Specify the current playback time using the format string ``X``.
+ ``%p`` is like ``%wH:%wM:%wS``, and ``%P`` is like ``%wH:%wM:%wS.%wT``.
+
+ Valid format specifiers:
+ ``%wH``
+ hour (padded with 0 to two digits)
+ ``%wh``
+ hour (not padded)
+ ``%wM``
+ minutes (00-59)
+ ``%wm``
+ total minutes (includes hours, unlike ``%wM``)
+ ``%wS``
+ seconds (00-59)
+ ``%ws``
+ total seconds (includes hours and minutes)
+ ``%wf``
+ like ``%ws``, but as float
+ ``%wT``
+ milliseconds (000-999)
+
+ ``%tX``
+ Specify the current local date/time using the format ``X``. This format
+ specifier uses the UNIX ``strftime()`` function internally, and inserts
+ the result of passing "%X" to ``strftime``. For example, ``%tm`` will
+ insert the number of the current month as number. You have to use
+ multiple ``%tX`` specifiers to build a full date/time string.
+ ``%{prop[:fallback text]}``
+ Insert the value of the input property 'prop'. E.g. ``%{filename}`` is
+ the same as ``%f``. If the property does not exist or is not available,
+ an error text is inserted, unless a fallback is specified.
+ ``%%``
+ Replaced with the ``%`` character itself.
+
+``--screenshot-dir=<path>``
+ Store screenshots in this directory. This path is joined with the filename
+ generated by ``--screenshot-template``. If the template filename is already
+ absolute, the directory is ignored.
+
+ ``--screenshot-directory`` is an alias for ``--screenshot-dir``.
+
+ If the directory does not exist, it is created on the first screenshot. If
+ it is not a directory, an error is generated when trying to write a
+ screenshot.
+
+ This option is not set by default, and thus will write screenshots to the
+ directory from which mpv was started. In pseudo-gui mode
+ (see `PSEUDO GUI MODE`_), this is set to the desktop.
+
+``--screenshot-jpeg-quality=<0-100>``
+ Set the JPEG quality level. Higher means better quality. The default is 90.
+
+``--screenshot-jpeg-source-chroma=<yes|no>``
+ Write JPEG files with the same chroma subsampling as the video
+ (default: yes). If disabled, the libjpeg default is used.
+
+``--screenshot-png-compression=<0-9>``
+ Set the PNG compression level. Higher means better compression. This will
+ affect the file size of the written screenshot file and the time it takes
+ to write a screenshot. Too high compression might occupy enough CPU time to
+ interrupt playback. The default is 7.
+
+``--screenshot-png-filter=<0-5>``
+ Set the filter applied prior to PNG compression. 0 is none, 1 is "sub", 2 is
+ "up", 3 is "average", 4 is "Paeth", and 5 is "mixed". This affects the level
+ of compression that can be achieved. For most images, "mixed" achieves the
+ best compression ratio, hence it is the default.
+
+``--screenshot-webp-lossless=<yes|no>``
+ Write lossless WebP files. ``--screenshot-webp-quality`` is ignored if this
+ is set. The default is no.
+
+``--screenshot-webp-quality=<0-100>``
+ Set the WebP quality level. Higher means better quality. The default is 75.
+
+``--screenshot-webp-compression=<0-6>``
+ Set the WebP compression level. Higher means better compression, but takes
+ more CPU time. Note that this also affects the screenshot quality when used
+ with lossy WebP files. The default is 4.
+
+``--screenshot-jxl-distance=<0-15>``
+ Set the JPEG XL Butteraugli distance. Lower means better quality. Lossless
+ is 0.0, and 1.0 is approximately equivalent to JPEG quality 90 for
+ photographic content. Use 0.1 for "visually lossless" screenshots. The
+ default is 1.0.
+
+``--screenshot-jxl-effort=<1-9>``
+ Set the JPEG XL compression effort. Higher effort (usually) means better
+ compression, but takes more CPU time. The default is 4.
+
+``--screenshot-avif-encoder=<encoder>``
+ Specify the AV1 encoder to be used by libavcodec for encoding avif
+ screenshots.
+
+ Default: ``libaom-av1``
+
+``--screenshot-avif-pixfmt=<format>``
+ Specify the pixel format to the libavcodec encoder.
+
+ Default: ``yuv420p``
+
+``--screenshot-avif-opts=key1=value1,key2=value2,...``
+ Specifies libavcodec options for selected encoder. For more information,
+ consult the FFmpeg documentation.
+
+ Default: ``usage=allintra,crf=32,cpu-used=8,tune=ssim``
+
+ Note: the default is only guaranteed to work with the libaom-av1 encoder.
+ Above options may not be valid and or optimal for other encoders.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ "``--screenshot-avif-opts=crf=32,aq-mode=complexity``"
+ sets the crf to 32 and quantization (aq-mode) to complexity based.
+
+``--screenshot-sw=<yes|no>``
+ Whether to use software rendering for screenshots (default: no).
+
+ If set to no, the screenshot will be rendered by the current VO (only vo_gpu
+ or vo_gpu_next currently). The advantage is that this will (probably) always
+ show up as in the video window, because the same code is used for rendering.
+ But since the renderer needs to be reinitialized, this can be slow and
+ interrupt playback.
+
+ If set to yes, the software scaler is used to convert the video to RGB (or
+ whatever the target screenshot requires). In this case, conversion will
+ run in a separate thread and will probably not interrupt playback. The
+ software renderer may lack some capabilities, such as HDR rendering.
+ If ``window`` mode is used, the image will also be scaled in software
+ which may not accurately reflect the actual visible result.
+
+Software Scaler
+---------------
+
+``--sws-scaler=<name>``
+ Specify the software scaler algorithm to be used with ``--vf=scale``. This
+ also affects video output drivers which lack hardware acceleration,
+ e.g. ``x11``. See also ``--vf=scale``.
+
+ To get a list of available scalers, run ``--sws-scaler=help``.
+
+ Default: ``bicubic``.
+
+``--sws-lgb=<0-100>``
+ Software scaler Gaussian blur filter (luma). See ``--sws-scaler``.
+
+``--sws-cgb=<0-100>``
+ Software scaler Gaussian blur filter (chroma). See ``--sws-scaler``.
+
+``--sws-ls=<-100-100>``
+ Software scaler sharpen filter (luma). See ``--sws-scaler``.
+
+``--sws-cs=<-100-100>``
+ Software scaler sharpen filter (chroma). See ``--sws-scaler``.
+
+``--sws-chs=<h>``
+ Software scaler chroma horizontal shifting. See ``--sws-scaler``.
+
+``--sws-cvs=<v>``
+ Software scaler chroma vertical shifting. See ``--sws-scaler``.
+
+``--sws-bitexact=<yes|no>``
+ Unknown functionality (default: no). Consult libswscale source code. The
+ primary purpose of this, as far as libswscale API goes), is to produce
+ exactly the same output for the same input on all platforms (output has the
+ same "bits" everywhere, thus "bitexact"). Typically disables optimizations.
+
+``--sws-fast=<yes|no>``
+ Allow optimizations that help with performance, but reduce quality (default:
+ no).
+
+ VOs like ``drm`` and ``x11`` will benefit a lot from using ``--sws-fast``.
+ You may need to set other options, like ``--sws-scaler``. The builtin
+ ``sws-fast`` profile sets this option and some others to gain performance
+ for reduced quality. Also see ``--sws-allow-zimg``.
+
+``--sws-allow-zimg=<yes|no>``
+ Allow using zimg (if the component using the internal swscale wrapper
+ explicitly allows so) (default: yes). In this case, zimg *may* be used, if
+ the internal zimg wrapper supports the input and output formats. It will
+ silently or noisily fall back to libswscale if one of these conditions does
+ not apply.
+
+ If zimg is used, the other ``--sws-`` options are ignored, and the
+ ``--zimg-`` options are used instead.
+
+ If the internal component using the swscale wrapper hooks up logging
+ correctly, a verbose priority log message will indicate whether zimg is
+ being used.
+
+ Most things which need software conversion can make use of this.
+
+ .. note::
+
+ Do note that zimg *may* be slower than libswscale. Usually,
+ it's faster on x86 platforms, but slower on ARM (due to lack of ARM
+ specific optimizations). The mpv zimg wrapper uses unoptimized repacking
+ for some formats, for which zimg cannot be blamed.
+
+``--zimg-scaler=<point|bilinear|bicubic|spline16|spline36|lanczos>``
+ Zimg luma scaler to use (default: lanczos).
+
+``--zimg-scaler-param-a=<default|float>``, ``--zimg-scaler-param-b=<default|float>``
+ Set scaler parameters. By default, these are set to the special string
+ ``default``, which maps to a scaler-specific default value. Ignored if the
+ scaler is not tunable.
+
+ ``lanczos``
+ ``--zimg-scaler-param-a`` is the number of taps.
+
+ ``bicubic``
+ a and b are the bicubic b and c parameters.
+
+``--zimg-scaler-chroma=...``
+ Same as ``--zimg-scaler``, for for chroma interpolation (default: bilinear).
+
+``--zimg-scaler-chroma-param-a``, ``--zimg-scaler-chroma-param-b``
+ Same as ``--zimg-scaler-param-a`` / ``--zimg-scaler-param-b``, for chroma.
+
+``--zimg-dither=<no|ordered|random|error-diffusion>``
+ Dithering (default: random).
+
+``--zimg-threads=<auto|integer>``
+ Set the maximum number of threads to use for scaling (default: auto).
+ ``auto`` uses the number of logical cores on the current machine. Note that
+ the scaler may use less threads (or even just 1 thread) depending on stuff.
+ Passing a value of 1 disables threading and always scales the image in a
+ single operation. Higher thread counts waste resources, but make it
+ typically faster.
+
+ Note that some zimg git versions had bugs that will corrupt the output if
+ threads are used.
+
+``--zimg-fast=<yes|no>``
+ Allow optimizations that help with performance, but reduce quality (default:
+ yes). Currently, this may simplify gamma conversion operations.
+
+
+Audio Resampler
+---------------
+
+This controls the default options of any resampling done by mpv (but not within
+libavfilter, within the system audio API resampler, or any other places).
+
+It also sets the defaults for the ``lavrresample`` audio filter.
+
+``--audio-resample-filter-size=<length>``
+ Length of the filter with respect to the lower sampling rate. (default:
+ 16)
+
+``--audio-resample-phase-shift=<count>``
+ Log2 of the number of polyphase entries. (..., 10->1024, 11->2048,
+ 12->4096, ...) (default: 10->1024)
+
+``--audio-resample-cutoff=<cutoff>``
+ Cutoff frequency (0.0-1.0), default set depending upon filter length.
+
+``--audio-resample-linear=<yes|no>``
+ If set then filters will be linearly interpolated between polyphase
+ entries. (default: no)
+
+``--audio-normalize-downmix=<yes|no>``
+ Enable/disable normalization if surround audio is downmixed to stereo
+ (default: no). If this is disabled, downmix can cause clipping. If it's
+ enabled, the output might be too quiet. It depends on the source audio.
+
+ Technically, this changes the ``normalize`` suboption of the
+ ``lavrresample`` audio filter, which performs the downmixing.
+
+ If downmix happens outside of mpv for some reason, or in the decoder
+ (decoder downmixing), or in the audio output (system mixer), this has no
+ effect.
+
+``--audio-resample-max-output-size=<length>``
+ Limit maximum size of audio frames filtered at once, in ms (default: 40).
+ The output size size is limited in order to make resample speed changes
+ react faster. This is necessary especially if decoders or filters output
+ very large frame sizes (like some lossless codecs or some DRC filters).
+ This option does not affect the resampling algorithm in any way.
+
+ For testing/debugging only. Can be removed or changed any time.
+
+``--audio-swresample-o=<string>``
+ Set AVOptions on the SwrContext or AVAudioResampleContext. These should
+ be documented by FFmpeg or Libav.
+
+ This is a key/value list option. See `List Options`_ for details.
+
+Terminal
+--------
+
+``--quiet``
+ Make console output less verbose; in particular, prevents the status line
+ (i.e. AV: 3.4 (00:00:03.37) / 5320.6 ...) from being displayed.
+ Particularly useful on slow terminals or broken ones which do not properly
+ handle carriage return (i.e. ``\r``).
+
+ See also: ``--really-quiet`` and ``--msg-level``.
+
+``--really-quiet``
+ Display even less output and status messages than with ``--quiet``.
+
+``--no-terminal``, ``--terminal``
+ Disable any use of the terminal and stdin/stdout/stderr. This completely
+ silences any message output.
+
+ Unlike ``--really-quiet``, this disables input and terminal initialization
+ as well.
+
+``--no-msg-color``
+ Disable colorful console output on terminals.
+
+``--msg-level=<module1=level1,module2=level2,...>``
+ Control verbosity directly for each module. The ``all`` module changes the
+ verbosity of all the modules. The verbosity changes from this option are
+ applied in order from left to right, and each item can override a previous
+ one.
+
+ Run mpv with ``--msg-level=all=trace`` to see all messages mpv outputs. You
+ can use the module names printed in the output (prefixed to each line in
+ ``[...]``) to limit the output to interesting modules.
+
+ This also affects ``--log-file``, and in certain cases libmpv API logging.
+
+ .. note::
+
+ Some messages are printed before the command line is parsed and are
+ therefore not affected by ``--msg-level``. To control these messages,
+ you have to use the ``MPV_VERBOSE`` environment variable; see
+ `ENVIRONMENT VARIABLES`_ for details.
+
+ Available levels:
+
+ :no: complete silence
+ :fatal: fatal messages only
+ :error: error messages
+ :warn: warning messages
+ :info: informational messages
+ :status: status messages (default)
+ :v: verbose messages
+ :debug: debug messages
+ :trace: very noisy debug messages
+
+ .. admonition:: Example
+
+ ::
+
+ mpv --msg-level=ao/sndio=no
+
+ Completely silences the output of ao_sndio, which uses the log
+ prefix ``[ao/sndio]``.
+
+ ::
+
+ mpv --msg-level=all=warn,ao/alsa=error
+
+ Only show warnings or worse, and let the ao_alsa output show errors
+ only.
+
+``--term-osd=<auto|no|force>``
+ Control whether OSD messages are shown on the console when no video output
+ is available (default: auto).
+
+ :auto: use terminal OSD if no video output active
+ :no: disable terminal OSD
+ :force: use terminal OSD even if video output active
+
+ The ``auto`` mode also enables terminal OSD if ``--video-osd=no`` was set.
+
+``--term-osd-bar``, ``--no-term-osd-bar``
+ Enable printing a progress bar under the status line on the terminal.
+ (Disabled by default.)
+
+``--term-osd-bar-chars=<string>``
+ Customize the ``--term-osd-bar`` feature. The string is expected to
+ consist of 5 characters (start, left space, position indicator,
+ right space, end). You can use Unicode characters, but note that double-
+ width characters will not be treated correctly.
+
+ Default: ``[-+-]``.
+
+``--term-playing-msg=<string>``
+ Print out a string after starting playback. The string is expanded for
+ properties, e.g. ``--term-playing-msg='file: ${filename}'`` will print the string
+ ``file:`` followed by a space and the currently played filename.
+
+ See `Property Expansion`_.
+
+``--term-remaining-playtime``, ``--no-term-remaining-playtime``
+ When printing out the time on the terminal, show the remaining time adjusted by
+ playback speed. Default: ``yes``
+
+``--term-status-msg=<string>``
+ Print out a custom string during playback instead of the standard status
+ line. Expands properties. See `Property Expansion`_.
+
+``--term-title=<string>``
+ Set the terminal title. Currently, this simply concatenates the escape
+ sequence setting the window title with the provided (property expanded)
+ string. This will mess up if the expanded string contain bytes that end the
+ escape sequence, or if the terminal does not understand the sequence. The
+ latter probably includes the regrettable win32.
+
+ Expands properties. See `Property Expansion`_.
+
+``--msg-module``
+ Prepend module name to each console message.
+
+``--msg-time``
+ Prepend timing information to each console message. The time is in
+ seconds since the player process was started (technically, slightly
+ later actually), using a monotonic time source depending on the OS. This
+ is ``CLOCK_MONOTONIC`` on sane UNIX variants.
+
+Cache
+-----
+
+``--cache=<yes|no|auto>``
+ Decide whether to use network cache settings (default: auto).
+
+ If enabled, use up to ``--cache-secs`` for the cache size (but still limited
+ to ``--demuxer-max-bytes``), and make the cached data seekable (if possible).
+ If disabled, ``--cache-pause`` and related are implicitly disabled.
+
+ The ``auto`` choice enables this depending on whether the stream is thought
+ to involve network accesses or other slow media (this is an imperfect
+ heuristic).
+
+ Before mpv 0.30.0, this used to accept a number, which specified the size
+ of the cache in kilobytes. Use e.g. ``--cache --demuxer-max-bytes=123k``
+ instead.
+
+``--no-cache``
+ Turn off input stream caching. See ``--cache``.
+
+``--cache-secs=<seconds>``
+ How many seconds of audio/video to prefetch if the cache is active. This
+ overrides the ``--demuxer-readahead-secs`` option if and only if the cache
+ is enabled and the value is larger. The default value is set to something
+ very high, so the actually achieved readahead will usually be limited by
+ the value of the ``--demuxer-max-bytes`` option. Setting this option is
+ usually only useful for limiting readahead.
+
+``--cache-on-disk=<yes|no>``
+ Write packet data to a temporary file, instead of keeping them in memory.
+ This makes sense only with ``--cache``. If the normal cache is disabled,
+ this option is ignored.
+
+ The cache file is append-only. Even if the player appears to prune data, the
+ file space freed by it is not reused. The cache file is deleted when
+ playback is closed.
+
+ Note that packet metadata is still kept in memory. ``--demuxer-max-bytes``
+ and related options are applied to metadata *only*. The size of this
+ metadata varies, but 50 MB per hour of media is typical. The cache
+ statistics will report this metadats size, instead of the size of the cache
+ file. If the metadata hits the size limits, the metadata is pruned (but not
+ the cache file).
+
+ When the media is closed, the cache file is deleted. A cache file is
+ generally worthless after the media is closed, and it's hard to retrieve
+ any media data from it (it's not supported by design).
+
+ If the option is enabled at runtime, the cache file is created, but old data
+ will remain in the memory cache. If the option is disabled at runtime, old
+ data remains in the disk cache, and the cache file is not closed until the
+ media is closed. If the option is disabled and enabled again, it will
+ continue to use the cache file that was opened first.
+
+``--demuxer-cache-dir=<path>``
+ Directory where to create temporary files. Cache is stored in the system's
+ cache directory (usually ``~/.cache/mpv``) if this is unset.
+
+ Currently, this is used for ``--cache-on-disk`` only.
+
+``--cache-pause=<yes|no>``
+ Whether the player should automatically pause when the cache runs out of
+ data and stalls decoding/playback (default: yes). If enabled, it will
+ pause and unpause once more data is available, aka "buffering".
+
+``--cache-pause-wait=<seconds>``
+ Number of seconds the packet cache should have buffered before starting
+ playback again if "buffering" was entered (default: 1). This can be used
+ to control how long the player rebuffers if ``--cache-pause`` is enabled,
+ and the demuxer underruns. If the given time is higher than the maximum
+ set with ``--cache-secs`` or ``--demuxer-readahead-secs``, or prefetching
+ ends before that for some other reason (like file end or maximum configured
+ cache size reached), playback resumes earlier.
+
+``--cache-pause-initial=<yes|no>``
+ Enter "buffering" mode before starting playback (default: no). This can be
+ used to ensure playback starts smoothly, in exchange for waiting some time
+ to prefetch network data (as controlled by ``--cache-pause-wait``). For
+ example, some common behavior is that playback starts, but network caches
+ immediately underrun when trying to decode more data as playback progresses.
+
+ Another thing that can happen is that the network prefetching is so CPU
+ demanding (due to demuxing in the background) that playback drops frames
+ at first. In these cases, it helps enabling this option, and setting
+ ``--cache-secs`` and ``--cache-pause-wait`` to roughly the same value.
+
+ This option also triggers when playback is restarted after seeking.
+
+``--demuxer-cache-unlink-files=<immediate|whendone|no>``
+ Whether or when to unlink cache files (default: immediate). This affects
+ cache files which are inherently temporary, and which make no sense to
+ remain on disk after the player terminates. This is a debugging option.
+
+ ``immediate``
+ Unlink cache file after they were created. The cache files won't be
+ visible anymore, even though they're in use. This ensures they are
+ guaranteed to be removed from disk when the player terminates, even if
+ it crashes.
+
+ ``whendone``
+ Delete cache files after they are closed.
+
+ ``no``
+ Don't delete cache files. They will consume disk space without having a
+ use.
+
+ Currently, this is used for ``--cache-on-disk`` only.
+
+``--stream-buffer-size=<bytesize>``
+ Size of the low level stream byte buffer (default: 128KB). This is used as
+ buffer between demuxer and low level I/O (e.g. sockets). Generally, this
+ can be very small, and the main purpose is similar to the internal buffer
+ FILE in the C standard library will have.
+
+ Half of the buffer is always used for guaranteed seek back, which is
+ important for unseekable input.
+
+ There are known cases where this can help performance to set a large buffer:
+
+ 1. mp4 files. libavformat may trigger many small seeks in both
+ directions, depending on how the file was muxed.
+
+ 2. Certain network filesystems, which do not have a cache, and where
+ small reads can be inefficient.
+
+ In other cases, setting this to a large value can reduce performance.
+
+ Usually, read accesses are at half the buffer size, but it may happen that
+ accesses are done alternating with smaller and larger sizes (this is due to
+ the internal ring buffer wrap-around).
+
+ See ``--list-options`` for defaults and value range. ``<bytesize>`` options
+ accept suffixes such as ``KiB`` and ``MiB``.
+
+``--vd-queue-enable=<yes|no>, --ad-queue-enable``
+ Enable running the video/audio decoder on a separate thread (default: no).
+ If enabled, the decoder is run on a separate thread, and a frame queue is
+ put between decoder and higher level playback logic. The size of the frame
+ queue is defined by the other options below.
+
+ This is probably quite pointless. libavcodec already has multithreaded
+ decoding (enabled by default), which makes this largely unnecessary. It
+ might help in some corner cases with high bandwidth video that is slow to
+ decode (in these cases libavcodec would block the playback logic, while
+ using a decoding thread would distribute the decoding time evenly without
+ affecting the playback logic). In other situations, it will simply make
+ seeking slower and use significantly more memory.
+
+ The queue size is restricted by the other ``--vd-queue-...`` options. The
+ final queue size is the minimum as indicated by the option with the lowest
+ limit. Each decoder/track has its own queue that may use the full configured
+ queue size.
+
+ Most queue options can be changed at runtime. ``--vd-queue-enable`` itself
+ (and the audio equivalent) update only if decoding is completely
+ reinitialized. However, setting ``--vd-queue-max-samples=1`` should almost
+ lead to the same behavior as ``--vd-queue-enable=no``, so that value can
+ be used for effectively runtime enabling/disabling the queue.
+
+ This should not be used with hardware decoding. It is possible to enable
+ this for audio, but it makes even less sense.
+
+``--vd-queue-max-bytes=<bytesize>``, ``--ad-queue-max-bytes``
+ Maximum approximate allowed size of the queue. If exceeded, decoding will
+ be stopped. The maximum size can be exceeded by about 1 frame.
+
+ See ``--list-options`` for defaults and value range. ``<bytesize>`` options
+ accept suffixes such as ``KiB`` and ``MiB``.
+
+``--vd-queue-max-samples=<int>``, ``--ad-queue-max-samples``
+ Maximum number of frames (video) or samples (audio) of the queue. The audio
+ size may be exceeded by about 1 frame.
+
+ See ``--list-options`` for defaults and value range.
+
+``--vd-queue-max-secs=<seconds>``, ``--ad-queue-max-secs``
+ Maximum number of seconds of media in the queue. The special value 0 means
+ no limit is set. The queue size may be exceeded by about 2 frames. Timestamp
+ resets may lead to random queue size usage.
+
+ See ``--list-options`` for defaults and value range.
+
+Network
+-------
+
+``--user-agent=<string>``
+ Use ``<string>`` as user agent for HTTP streaming.
+
+``--cookies``, ``--no-cookies``
+ Support cookies when making HTTP requests. Disabled by default.
+
+``--cookies-file=<filename>``
+ Read HTTP cookies from <filename>. The file is assumed to be in Netscape
+ format.
+
+``--http-header-fields=<field1,field2>``
+ Set custom HTTP fields when accessing HTTP stream.
+
+ This is a string list option. See `List Options`_ for details.
+
+ .. admonition:: Example
+
+ ::
+
+ mpv --http-header-fields='Field1: value1','Field2: value2' \
+ http://localhost:1234
+
+ Will generate HTTP request::
+
+ GET / HTTP/1.0
+ Host: localhost:1234
+ User-Agent: MPlayer
+ Icy-MetaData: 1
+ Field1: value1
+ Field2: value2
+ Connection: close
+
+``--http-proxy=<proxy>``
+ URL of the HTTP/HTTPS proxy. If this is set, the ``http_proxy`` environment
+ is ignored. The ``no_proxy`` environment variable is still respected. This
+ option is silently ignored if it does not start with ``http://``. Proxies
+ are not used for https URLs. Setting this option does not try to make the
+ ytdl script use the proxy.
+
+``--tls-ca-file=<filename>``
+ Certificate authority database file for use with TLS. (Silently fails with
+ older FFmpeg or Libav versions.)
+
+``--tls-verify``
+ Verify peer certificates when using TLS (e.g. with ``https://...``).
+ (Silently fails with older FFmpeg or Libav versions.)
+
+``--tls-cert-file``
+ A file containing a certificate to use in the handshake with the
+ peer.
+
+``--tls-key-file``
+ A file containing the private key for the certificate.
+
+``--referrer=<string>``
+ Specify a referrer path or URL for HTTP requests.
+
+``--network-timeout=<seconds>``
+ Specify the network timeout in seconds (default: 60 seconds). This affects
+ at least HTTP. The special value 0 uses the FFmpeg/Libav defaults. If a
+ protocol is used which does not support timeouts, this option is silently
+ ignored.
+
+ .. warning::
+
+ This breaks the RTSP protocol, because of inconsistent FFmpeg API
+ regarding its internal timeout option. Not only does the RTSP timeout
+ option accept different units (seconds instead of microseconds, causing
+ mpv to pass it huge values), it will also overflow FFmpeg internal
+ calculations. The worst is that merely setting the option will put RTSP
+ into listening mode, which breaks any client uses. At time of this
+ writing, the fix was not made effective yet. For this reason, this
+ option is ignored (or should be ignored) on RTSP URLs. You can still
+ set the timeout option directly with ``--demuxer-lavf-o``.
+
+``--rtsp-transport=<lavf|udp|udp_multicast|tcp|http>``
+ Select RTSP transport method (default: tcp). This selects the underlying
+ network transport when playing ``rtsp://...`` URLs. The value ``lavf``
+ leaves the decision to libavformat.
+
+``--hls-bitrate=<no|min|max|<rate>>``
+ If HLS streams are played, this option controls what streams are selected
+ by default. The option allows the following parameters:
+
+ :no: Don't do anything special. Typically, this will simply pick the
+ first audio/video streams it can find.
+ :min: Pick the streams with the lowest bitrate.
+ :max: Same, but highest bitrate. (Default.)
+
+ Additionally, if the option is a number, the stream with the highest rate
+ equal or below the option value is selected.
+
+ The bitrate as used is sent by the server, and there's no guarantee it's
+ actually meaningful.
+
+DVB
+---
+
+``--dvbin-prog=<string>``
+ This defines the program to tune to. Usually, you may specify this
+ by using a stream URI like ``"dvb://ZDF HD"``, but you can tune to a
+ different channel by writing to this property at runtime.
+ Also see ``dvbin-channel-switch-offset`` for more useful channel
+ switching functionality.
+
+``--dvbin-card=<0-15>``
+ Specifies using card number 0-15 (default: 0).
+
+``--dvbin-file=<filename>``
+ Instructs mpv to read the channels list from ``<filename>``. The default is
+ in the mpv configuration directory (usually ``~/.config/mpv``) with the
+ filename ``channels.conf.{sat,ter,cbl,atsc,isdbt}`` (based on your card
+ type) or ``channels.conf`` as a last resort.
+ Please note that using specific file name with card type is recommended,
+ since the legacy channel format is not fully standardized
+ so autodetection of the delivery system may fail otherwise.
+ For DVB-S/2 cards, a VDR 1.7.x format channel list is recommended
+ as it allows tuning to DVB-S2 channels, enabling subtitles and
+ decoding the PMT (which largely improves the demuxing).
+ Classic mplayer format channel lists are still supported (without
+ these improvements), and for other card types, only limited VDR
+ format channel list support is implemented (patches welcome).
+ For channels with dynamic PID switching or incomplete
+ ``channels.conf``, ``--dvbin-full-transponder`` or the magic PID
+ ``8192`` are recommended.
+
+``--dvbin-timeout=<1-30>``
+ Maximum number of seconds to wait when trying to tune a frequency before
+ giving up (default: 30).
+
+``--dvbin-full-transponder=<yes|no>``
+ Apply no filters on program PIDs, only tune to frequency and pass full
+ transponder to demuxer.
+ The player frontend selects the streams from the full TS in this case,
+ so the program which is shown initially may not match the chosen channel.
+ Switching between the programs is possible by cycling the ``program``
+ property.
+ This is useful to record multiple programs on a single transponder,
+ or to work around issues in the ``channels.conf``.
+ It is also recommended to use this for channels which switch PIDs
+ on-the-fly, e.g. for regional news.
+
+ Default: ``no``
+
+``--dvbin-channel-switch-offset=<integer>``
+ This value is not meant for setting via configuration, but used in channel
+ switching. An ``input.conf`` can ``cycle`` this value ``up`` and ``down``
+ to perform channel switching. This number effectively gives the offset
+ to the initially tuned to channel in the channel list.
+
+ An example ``input.conf`` could contain:
+ ``H cycle dvbin-channel-switch-offset up``, ``K cycle dvbin-channel-switch-offset down``
+
+ALSA audio output options
+-------------------------
+
+
+``--alsa-device=<device>``
+ Deprecated, use ``--audio-device`` (requires ``alsa/`` prefix).
+
+``--alsa-resample=yes``
+ Enable ALSA resampling plugin. (This is disabled by default, because
+ some drivers report incorrect audio delay in some cases.)
+
+``--alsa-mixer-device=<device>``
+ Set the mixer device used with ``ao-volume`` (default: ``default``).
+
+``--alsa-mixer-name=<name>``
+ Set the name of the mixer element (default: ``Master``). This is for
+ example ``PCM`` or ``Master``.
+
+``--alsa-mixer-index=<number>``
+ Set the index of the mixer channel (default: 0). Consider the output of
+ "``amixer scontrols``", then the index is the number that follows the
+ name of the element.
+
+``--alsa-non-interleaved``
+ Allow output of non-interleaved formats (if the audio decoder uses
+ this format). Currently disabled by default, because some popular
+ ALSA plugins are utterly broken with non-interleaved formats.
+
+``--alsa-ignore-chmap``
+ Don't read or set the channel map of the ALSA device - only request the
+ required number of channels, and then pass the audio as-is to it. This
+ option most likely should not be used. It can be useful for debugging,
+ or for static setups with a specially engineered ALSA configuration (in
+ this case you should always force the same layout with ``--audio-channels``,
+ or it will work only for files which use the layout implicit to your
+ ALSA device).
+
+``--alsa-buffer-time=<microseconds>``
+ Set the requested buffer time in microseconds. A value of 0 skips requesting
+ anything from the ALSA API. This and the ``--alsa-periods`` option uses the
+ ALSA ``near`` functions to set the requested parameters. If doing so results
+ in an empty configuration set, setting these parameters is skipped.
+
+ Both options control the buffer size. A low buffer size can lead to higher
+ CPU usage and audio dropouts, while a high buffer size can lead to higher
+ latency in volume changes and other filtering.
+
+``--alsa-periods=<number>``
+ Number of periods requested from the ALSA API. See ``--alsa-buffer-time``
+ for further remarks.
+
+
+GPU renderer options
+-----------------------
+
+The following video options are currently all specific to ``--vo=gpu``,
+``--vo=libmpv`` and ``--vo=gpu-next``, which are the only VOs that implement
+them.
+
+``--scale=<filter>``
+ The filter function to use when upscaling video.
+
+ ``bilinear``
+ Bilinear hardware texture filtering (fastest, very low quality). This is
+ the default when using the ``fast`` profile.
+
+ ``lanczos``
+ Lanczos scaling. Provides good balance between quality and performance.
+ This is the default for ``scale``. The number of taps can be controlled
+ with ``scale-radius``, but is best left unchanged.
+
+ (This filter is an alias for ``sinc``-windowed ``sinc``)
+
+ ``ewa_lanczos``
+ Elliptic weighted average Lanczos scaling. Also known as Jinc.
+ Relatively slow, but very good quality. The radius can be controlled
+ with ``scale-radius``. Increasing the radius makes the filter sharper
+ but adds more ringing.
+
+ (This filter is an alias for ``jinc``-windowed ``jinc``)
+
+ ``ewa_lanczossharp``
+ A slightly sharpened version of ewa_lanczos. This is the default when
+ using the ``high-quality`` profile.
+
+ ``ewa_lanczos4sharpest``
+ Very sharp scaler, but also slightly slower than ``ewa_lanczossharp``.
+ Prone to ringing, so it's recommended to combine this with an
+ anti-ringing shader. On ``--vo=gpu-next``, setting this filter enables
+ built-in anti-ringing, so no extra action needs to be taken.
+
+ ``mitchell``
+ Mitchell-Netravali. The ``B`` and ``C`` parameters can be set with
+ ``--scale-param1`` and ``--scale-param2``.
+
+ ``hermite``
+ Hermite spline. Similar to ``bicubic`` but with ``B`` set to ``0.0``.
+ This filter has the special property of having a support of radius 1.0,
+ making it very fast in comparison, but prone to blocking. This is the
+ default for ``--dscale``.
+
+ ``catmull_rom``
+ Catmull-Rom. A Cubic filter in the same vein as ``mitchell``, where
+ the ``B`` and ``C`` parameters are ``0.0`` and ``0.5`` respectively.
+ This filter is sharper than ``mitchell``, but it results in more
+ ringing.
+
+ ``oversample``
+ A version of nearest neighbour that (naively) oversamples pixels, so
+ that pixels overlapping edges get linearly interpolated instead of
+ rounded. This essentially removes the small imperfections and judder
+ artifacts caused by nearest-neighbour interpolation, in exchange for
+ adding some blur. This can also be used for frame mixing, where it
+ is commonly known as "smoothmotion" (see ``--tscale``).
+
+ ``linear``
+ A ``--tscale`` filter.
+
+ There are some more filters, but most are not as useful. For a complete
+ list, pass ``help`` as value, e.g.::
+
+ mpv --scale=help
+
+``--cscale=<filter>``
+ As ``--scale``, but for interpolating chroma information. If the image is
+ not subsampled, this option is ignored entirely. If this option is unset,
+ the filter implied by ``--scale`` will be applied.
+
+``--dscale=<filter>``
+ Like ``--scale``, but apply these filters on downscaling instead.
+
+``--tscale=<filter>``
+ The filter used for interpolating the temporal axis (frames). This is only
+ used if ``--interpolation`` is enabled. The only valid choices for
+ ``--tscale`` are separable convolution filters (use ``--tscale=help`` to
+ get a list). The default is ``oversample``.
+
+ Common ``--tscale`` choices include ``oversample``, ``linear``,
+ ``catmull_rom``, ``mitchell``, ``gaussian``, or ``bicubic``. These are
+ listed in increasing order of smoothness/blurriness, with ``bicubic``
+ being the smoothest/blurriest and ``oversample`` being the sharpest/least
+ smooth.
+
+``--scale-param1=<value>``, ``--scale-param2=<value>``, ``--cscale-param1=<value>``, ``--cscale-param2=<value>``, ``--dscale-param1=<value>``, ``--dscale-param2=<value>``, ``--tscale-param1=<value>``, ``--tscale-param2=<value>``
+ Set filter parameters. By default, these are set to the special string
+ ``default``, which maps to a scaler-specific default value. Ignored if the
+ filter is not tunable. Currently, this affects the following filter
+ parameters:
+
+ bicubic
+ Spline parameters (``B`` and ``C``). Defaults to B=1 and C=0.
+
+ gaussian
+ Scale parameter (``t``). Increasing this makes the result blurrier.
+ Defaults to 1.
+
+ oversample
+ Minimum distance to an edge before interpolation is used. Setting this
+ to 0 will always interpolate edges, whereas setting it to 0.5 will
+ never interpolate, thus behaving as if the regular nearest neighbour
+ algorithm was used. Defaults to 0.0.
+
+``--scale-blur=<value>``, ``--cscale-blur=<value>``, ``--dscale-blur=<value>``, ``--tscale-blur=<value>``
+ Kernel scaling factor (also known as a blur factor). Decreasing this makes
+ the result sharper, increasing it makes it blurrier (default 0). If set to
+ 0, the kernel's preferred blur factor is used. Note that setting this too
+ low (eg. 0.5) leads to bad results. It's generally recommended to stick to
+ values between 0.8 and 1.2.
+
+``--scale-clamp=<0.0-1.0>``, ``--cscale-clamp``, ``--dscale-clamp``, ``--tscale-clamp``
+ Specifies a weight bias to multiply into negative coefficients. Specifying
+ ``--scale-clamp=1`` has the effect of removing negative weights completely,
+ thus effectively clamping the value range to [0-1]. Values between 0.0 and
+ 1.0 can be specified to apply only a moderate diminishment of negative
+ weights. This is especially useful for ``--tscale``, where it reduces
+ excessive ringing artifacts in the temporal domain (which typically
+ manifest themselves as short flashes or fringes of black, mostly around
+ moving edges) in exchange for potentially adding more blur. The default for
+ ``--tscale-clamp`` is 1.0, the others default to 0.0.
+
+``--scale-taper=<value>``, ``--scale-wtaper=<value>``, ``--dscale-taper=<value>``, ``--dscale-wtaper=<value>``, ``--cscale-taper=<value>``, ``--cscale-wtaper=<value>``, ``--tscale-taper=<value>``, ``--tscale-wtaper=<value>``
+ Kernel/window taper factor. Increasing this flattens the filter function.
+ Value range is 0 to 1. A value of 0 (the default) means no flattening, a
+ value of 1 makes the filter completely flat (equivalent to a box function).
+ Values in between mean that some portion will be flat and the actual filter
+ function will be squeezed into the space in between.
+
+``--scale-radius=<value>``, ``--cscale-radius=<value>``, ``--dscale-radius=<value>``, ``--tscale-radius=<value>``
+ Set radius for tunable filters, must be a float number between 0.5 and
+ 16.0. Defaults to the filter's preferred radius if not specified. Doesn't
+ work for every scaler and VO combination.
+
+ Note that depending on filter implementation details and video scaling
+ ratio, the radius that actually being used might be different (most likely
+ being increased a bit).
+
+``--scale-antiring=<value>``, ``--cscale-antiring=<value>``, ``--dscale-antiring=<value>``, ``--tscale-antiring=<value>``
+ Set the antiringing strength. This tries to eliminate ringing, but can
+ introduce other artifacts in the process. Must be a float number between
+ 0.0 and 1.0. The default value of 0.0 disables antiringing entirely.
+
+ Note that this doesn't affect the special filters ``bilinear`` and
+ ``bicubic_fast``, nor does it affect any polar (EWA) scalers.
+
+``--scale-window=<window>``, ``--cscale-window=<window>``, ``--dscale-window=<window>``, ``--tscale-window=<window>``
+ (Advanced users only) Choose a custom windowing function for the kernel.
+ Defaults to the filter's preferred window if unset. Use
+ ``--scale-window=help`` to get a list of supported windowing functions.
+
+``--scale-wparam=<window>``, ``--cscale-wparam=<window>``, ``--cscale-wparam=<window>``, ``--tscale-wparam=<window>``
+ (Advanced users only) Configure the parameter for the window function given
+ by ``--scale-window`` etc. By default, these are set to the special string
+ ``default``, which maps to a window-specific default value. Ignored if the
+ window is not tunable. Currently, this affects the following window
+ parameters:
+
+ kaiser
+ Window parameter (alpha). Defaults to 6.33.
+ blackman
+ Window parameter (alpha). Defaults to 0.16.
+ gaussian
+ Scale parameter (t). Increasing this makes the window wider. Defaults
+ to 1.
+
+``--scaler-resizes-only``
+ Disable the scaler if the video image is not resized. In that case,
+ ``bilinear`` is used instead of whatever is set with ``--scale``. Bilinear
+ will reproduce the source image perfectly if no scaling is performed.
+ Enabled by default. Note that this option never affects ``--cscale``.
+
+``--correct-downscaling``
+ When using convolution based filters, extend the filter size when
+ downscaling. Increases quality, but reduces performance while downscaling.
+ Enabled by default.
+
+ This will perform slightly sub-optimally for anamorphic video (but still
+ better than without it) since it will extend the size to match only the
+ milder of the scale factors between the axes.
+
+ Note: this option is ignored when using bilinear downscaling with ``--vo=gpu``.
+
+``--linear-downscaling``
+ Scale in linear light when downscaling. It should only be used with a
+ ``--fbo-format`` that has at least 16 bit precision. This option
+ has no effect on HDR content. Enabled by default.
+
+``--linear-upscaling``
+ Scale in linear light when upscaling. Like ``--linear-downscaling``, it
+ should only be used with a ``--fbo-format`` that has at least 16 bits
+ precisions. This is not usually recommended except for testing/specific
+ purposes. Users are advised to either enable ``--sigmoid-upscaling`` or
+ keep both options disabled (i.e. scaling in gamma light).
+
+``--sigmoid-upscaling``
+ When upscaling, use a sigmoidal color transform to avoid emphasizing
+ ringing artifacts. Enabled by default. This is incompatible with and replaces
+ ``--linear-upscaling``. (Note that sigmoidization also requires
+ linearization, so the ``LINEAR`` rendering step fires in both cases)
+
+``--sigmoid-center``
+ The center of the sigmoid curve used for ``--sigmoid-upscaling``, must be a
+ float between 0.0 and 1.0. Defaults to 0.75 if not specified.
+
+``--sigmoid-slope``
+ The slope of the sigmoid curve used for ``--sigmoid-upscaling``, must be a
+ float between 1.0 and 20.0. Defaults to 6.5 if not specified.
+
+``--interpolation``
+ Reduce stuttering caused by mismatches in the video fps and display refresh
+ rate (also known as judder).
+
+ .. warning:: This requires setting the ``--video-sync`` option to one
+ of the ``display-`` modes, or it will be silently disabled.
+ This was not required before mpv 0.14.0.
+
+ This essentially attempts to interpolate the missing frames by convoluting
+ the video along the temporal axis. The filter used can be controlled using
+ the ``--tscale`` setting.
+
+``--interpolation-threshold=<0..1,-1>``
+ Threshold below which frame ratio interpolation gets disabled (default:
+ ``0.01``). This is calculated as ``abs(disphz/vfps - 1) < threshold``,
+ where ``vfps`` is the speed-adjusted video FPS, and ``disphz`` the
+ display refresh rate. (The speed-adjusted video FPS is roughly equal to
+ the normal video FPS, but with slowdown and speedup applied. This matters
+ if you use ``--video-sync=display-resample`` to make video run synchronously
+ to the display FPS, or if you change the ``speed`` property.)
+
+ The default is intended to enable interpolation in scenarios where
+ retiming with the ``--video-sync=display-*`` cannot adjust the speed of
+ the video sufficiently for smooth playback. For example if a video is
+ 60.00 FPS and your display refresh rate is 59.94 Hz, interpolation will
+ never be activated, since the mismatch is within 1% of the refresh
+ rate. The default also handles the scenario when mpv cannot determine the
+ container FPS, such as during certain live streams, and may dynamically
+ toggle interpolation on and off. In this scenario, the default would be to
+ not use interpolation but rather to allow ``--video-sync=display-*`` to
+ retime the video to match display refresh rate. See
+ ``--video-sync-max-video-change`` for more information about how mpv
+ will retime video.
+
+ Also note that if you use e.g. ``--video-sync=display-vdrop``, small
+ deviations in the rate can disable interpolation and introduce a
+ discontinuity every other minute.
+
+ Set this to ``-1`` to disable this logic.
+
+``--interpolation-preserve``
+ Preserve the previous frames' interpolated results even when renderer
+ parameters are changed - with the exception of options related to
+ cropping and video placement, which always invalidate the cache. Enabling
+ this option makes dynamic updates of renderer settings slightly smoother at
+ the cost of slightly higher latency in response to such changes. Defaults
+ to on. (Only affects ``--vo=gpu-next``, note that ``--vo=gpu`` always
+ invalidates interpolated frames)
+
+``--opengl-pbo``
+ Enable use of PBOs. On some drivers this can be faster, especially if the
+ source video size is huge (e.g. so called "4K" video). On other drivers it
+ might be slower or cause latency issues.
+
+``--dither-depth=<N|no|auto>``
+ Set dither target depth to N. Default: auto.
+
+ no
+ Disable any dithering done by mpv.
+ auto
+ Automatic selection. If output bit depth cannot be detected, 8 bits per
+ component are assumed.
+ 8
+ Dither to 8 bit output.
+
+ Note that the depth of the connected video display device cannot be
+ detected. Often, LCD panels will do dithering on their own, which conflicts
+ with this option and leads to ugly output.
+
+``--dither-size-fruit=<2-8>``
+ Set the size of the dither matrix (default: 6). The actual size of the
+ matrix is ``(2^N) x (2^N)`` for an option value of ``N``, so a value of 6
+ gives a size of 64x64. The matrix is generated at startup time, and a large
+ matrix can take rather long to compute (seconds).
+
+ Used in ``--dither=fruit`` mode only.
+
+``--dither=<fruit|ordered|error-diffusion|no>``
+ Select dithering algorithm (default: fruit). (Normally, the
+ ``--dither-depth`` option controls whether dithering is enabled.)
+
+ The ``error-diffusion`` option requires compute shader support. It also
+ requires large amount of shared memory to run, the size of which depends on
+ both the kernel (see ``--error-diffusion`` option below) and the height of
+ video window. It will fallback to ``fruit`` dithering if there is no enough
+ shared memory to run the shader.
+
+``--temporal-dither``
+ Enable temporal dithering. (Only active if dithering is enabled in
+ general.) This changes between 8 different dithering patterns on each frame
+ by changing the orientation of the tiled dithering matrix. Unfortunately,
+ this can lead to flicker on LCD displays, since these have a high reaction
+ time.
+
+``--temporal-dither-period=<1-128>``
+ Determines how often the dithering pattern is updated when
+ ``--temporal-dither`` is in use. 1 (the default) will update on every video
+ frame, 2 on every other frame, etc.
+
+``--error-diffusion=<kernel>``
+ The error diffusion kernel to use when ``--dither=error-diffusion`` is set.
+
+ ``simple``
+ Propagate error to only two adjacent pixels. Fastest but low quality.
+
+ ``sierra-lite``
+ Fast with reasonable quality. This is the default.
+
+ ``floyd-steinberg``
+ Most notable error diffusion kernel.
+
+ ``atkinson``
+ Looks different from other kernels because only fraction of errors will
+ be propagated during dithering. A typical use case of this kernel is
+ saving dithered screenshot (in window mode). This kernel produces
+ slightly smaller file, with still reasonable dithering quality.
+
+ There are other kernels (use ``--error-diffusion=help`` to list) but most of
+ them are much slower and demanding even larger amount of shared memory.
+ Among these kernels, ``burkes`` achieves a good balance between performance
+ and quality, and probably is the one you want to try first.
+
+``--gpu-debug``
+ Enables GPU debugging. What this means depends on the API type. For OpenGL,
+ it calls ``glGetError()``, and requests a debug context. For Vulkan, it
+ enables validation layers.
+
+``--opengl-swapinterval=<n>``
+ Interval in displayed frames between two buffer swaps. 1 is equivalent to
+ enable VSYNC, 0 to disable VSYNC. Defaults to 1 if not specified.
+
+ Note that this depends on proper OpenGL vsync support. On some platforms
+ and drivers, this only works reliably when in fullscreen mode. It may also
+ require driver-specific hacks if using multiple monitors, to ensure mpv
+ syncs to the right one. Compositing window managers can also lead to bad
+ results, as can missing or incorrect display FPS information (see
+ ``--display-fps-override``).
+
+``--vulkan-device=<device name|UUID>``
+ The name or UUID of the Vulkan device to use for rendering and presentation. Use
+ ``--vulkan-device=help`` to see the list of available devices and their
+ names and UUIDs. If left unspecified, the first enumerated hardware Vulkan
+ device will be used.
+
+``--vulkan-swap-mode=<mode>``
+ Controls the presentation mode of the vulkan swapchain. This is similar
+ to the ``--opengl-swapinterval`` option.
+
+ auto
+ Use the preferred swapchain mode for the vulkan context. (Default)
+ fifo
+ Non-tearing, vsync blocked. Similar to "VSync on".
+ fifo-relaxed
+ Tearing, vsync blocked. Late frames will tear instead of stuttering.
+ mailbox
+ Non-tearing, not vsync blocked. Similar to "triple buffering".
+ immediate
+ Tearing, not vsync blocked. Similar to "VSync off".
+
+``--vulkan-queue-count=<1..8>``
+ Controls the number of VkQueues used for rendering (limited by how many
+ your device supports). In theory, using more queues could enable some
+ parallelism between frames (when using a ``--swapchain-depth`` higher than
+ 1), but it can also slow things down on hardware where there's no true
+ parallelism between queues. (Default: 1)
+
+``--vulkan-async-transfer``
+ Enables the use of async transfer queues on supported vulkan devices. Using
+ them allows transfer operations like texture uploads and blits to happen
+ concurrently with the actual rendering, thus improving overall throughput
+ and power consumption. Enabled by default, and should be relatively safe.
+
+``--vulkan-async-compute``
+ Enables the use of async compute queues on supported vulkan devices. Using
+ this, in theory, allows out-of-order scheduling of compute shaders with
+ graphics shaders, thus enabling the hardware to do more effective work while
+ waiting for pipeline bubbles and memory operations. Not beneficial on all
+ GPUs. It's worth noting that if async compute is enabled, and the device
+ supports more compute queues than graphics queues (bound by the restrictions
+ set by ``--vulkan-queue-count``), mpv will internally try and prefer the
+ use of compute shaders over fragment shaders wherever possible. Enabled by
+ default, although Nvidia users may want to disable it.
+
+``--vulkan-display-display=<n>``
+ The index of the display, on the selected Vulkan device, to present on when
+ using the ``displayvk`` GPU context. Use ``--vulkan-display-display=help``
+ to see the list of available displays. If left unspecified, the first
+ enumerated display will be used.
+
+
+``--vulkan-display-mode=<n>``
+ The index of the display mode, of the selected Vulkan display, to use when
+ using the ``displayvk`` GPU context. Use ``--vulkan-display-mode=help``
+ to see the list of available modes. If left unspecified, the first
+ enumerated mode will be used.
+
+``--vulkan-display-plane=<n>``
+ The index of the plane, on the selected Vulkan device, to present on when
+ using the ``displayvk`` GPU context. Use ``--vulkan-display-plane=help``
+ to see the list of available planes. If left unspecified, the first
+ enumerated plane will be used.
+
+``--d3d11-exclusive-fs=<yes|no>``
+ Switches the D3D11 swap chain fullscreen state to 'fullscreen' when
+ fullscreen video is requested. Also known as "exclusive fullscreen" or
+ "D3D fullscreen" in other applications. Gives mpv full control of
+ rendering on the swap chain's screen. Off by default.
+
+``--d3d11-warp=<yes|no|auto>``
+ Use WARP (Windows Advanced Rasterization Platform) with the D3D11 GPU
+ backend (default: auto). This is a high performance software renderer. By
+ default, it is only used when the system has no hardware adapters that
+ support D3D11. While the extended GPU features will work with WARP, they
+ can be very slow.
+
+``--d3d11-feature-level=<12_1|12_0|11_1|11_0|10_1|10_0|9_3|9_2|9_1>``
+ Select a specific feature level when using the D3D11 GPU backend. By
+ default, the highest available feature level is used. This option can be
+ used to select a lower feature level, which is mainly useful for debugging.
+ Most extended GPU features will not work at 9_x feature levels.
+
+``--d3d11-flip=<yes|no>``
+ Enable flip-model presentation, which avoids unnecessarily copying the
+ backbuffer by sharing surfaces with the DWM (default: yes). This may cause
+ performance issues with older drivers. If flip-model presentation is not
+ supported (for example, on Windows 7 without the platform update), mpv will
+ automatically fall back to the older bitblt presentation model.
+
+``--d3d11-sync-interval=<0..4>``
+ Schedule each frame to be presented for this number of VBlank intervals.
+ (default: 1) Setting to 1 will enable VSync, setting to 0 will disable it.
+
+``--d3d11-adapter=<adapter name|help>``
+ Select a specific D3D11 adapter to utilize for D3D11 rendering.
+ Will pick the default adapter if unset. Alternatives are listed
+ when the name "help" is given.
+
+ Checks for matches based on the start of the string, case
+ insensitive. Thus, if the description of the adapter starts with
+ the vendor name, that can be utilized as the selection parameter.
+
+ Hardware decoders utilizing the D3D11 rendering abstraction's helper
+ functionality to receive a device, such as D3D11VA or DXVA2's DXGI
+ mode, will be affected by this choice.
+
+``--d3d11-output-format=<auto|rgba8|bgra8|rgb10_a2|rgba16f>``
+ Select a specific D3D11 output format to utilize for D3D11 rendering.
+ "auto" is the default, which will pick either rgba8 or rgb10_a2 depending
+ on the configured desktop bit depth. rgba16f and bgra8 are left out of
+ the autodetection logic, and are available for manual testing.
+
+ .. note::
+
+ Desktop bit depth querying is only available from an API available
+ from Windows 10. Thus on older systems it will only automatically
+ utilize the rgba8 output format.
+
+``--d3d11-output-csp=<auto|srgb|linear|pq|bt.2020>``
+ Select a specific D3D11 output color space to utilize for D3D11 rendering.
+ "auto" is the default, which will select the color space of the desktop
+ on which the swap chain is located.
+
+ Values other than "srgb" and "pq" have had issues in testing, so they
+ are mostly available for manual testing.
+
+ .. note::
+
+ Swap chain color space configuration is only available from an API
+ available from Windows 10. Thus on older systems it will not work.
+
+``--d3d11va-zero-copy=<yes|no>``
+ By default, when using hardware decoding with ``--gpu-api=d3d11``, the
+ video image will be copied (GPU-to-GPU) from the decoder surface to a
+ shader resource. Set this option to avoid that copy by sampling directly
+ from the decoder image. This may increase performance and reduce power
+ usage, but can cause the image to be sampled incorrectly on the bottom and
+ right edges due to padding, and may invoke driver bugs, since Direct3D 11
+ technically does not allow sampling from a decoder surface (though most
+ drivers support it.)
+
+ Currently only relevant for ``--gpu-api=d3d11``.
+
+``--wayland-app-id=<string>``
+ Set the client app id for Wayland-based video output methods (default: ``mpv``).
+
+``--wayland-configure-bounds=<auto|yes|no>``
+ Controls whether or not mpv opts into the configure bounds event if sent by the
+ compositor (default: auto). This restricts the initial size of the mpv window to
+ a certain maximum size intended by the compositor. In most cases, this simply
+ just prevents the mpv window from being larger than the size of the monitor when
+ it first renders. With the default value of ``auto``, configure-bounds will
+ silently be ignored if any ``autofit`` or ``geometry`` type option is also set.
+
+``--wayland-content-type=<auto|none|photo|video|game>``
+ If supported by the compositor, mpv will send a hint using the content-type
+ protocol telling the compositor what type of content is being displayed. ``auto``
+ (default) will automatically switch between telling the compositor the content
+ is a photo, video or possibly none depending on internal heuristics.
+
+``--wayland-disable-vsync=<yes|no>``
+ Disable mpv's internal vsync for Wayland-based video output (default: no).
+ This is mainly useful for benchmarking wayland VOs when combined with
+ ``video-sync=display-desync``, ``--no-audio``, and ``--untimed=yes``.
+
+``--wayland-edge-pixels-pointer=<value>``
+ Defines the size of an edge border (default: 16) to initiate client side
+ resize events in the wayland contexts with the mouse. This is only active if
+ there are no server side decorations from the compositor.
+
+``--wayland-edge-pixels-touch=<value>``
+ Defines the size of an edge border (default: 32) to initiate client side
+ resizes events in the wayland contexts with touch events.
+
+``--spirv-compiler=<compiler>``
+ Controls which compiler is used to translate GLSL to SPIR-V. This is
+ (currently) only relevant for ``--gpu-api=vulkan`` and `--gpu-api=d3d11`.
+ The possible choices are currently only:
+
+ auto
+ Use the first available compiler. (Default)
+ shaderc
+ Use libshaderc, which is an API wrapper around glslang. This is
+ generally the most preferred, if available.
+
+ .. note::
+
+ This option is deprecated, since there is only one reasonable value.
+ It may be removed in the future.
+
+``--glsl-shader=<file>``, ``--glsl-shaders=<file-list>``
+ Custom GLSL hooks. These are a flexible way to add custom fragment shaders,
+ which can be injected at almost arbitrary points in the rendering pipeline,
+ and access all previous intermediate textures.
+
+ Each use of the ``--glsl-shader`` option will add another file to the
+ internal list of shaders, while ``--glsl-shaders`` takes a list of files,
+ and overwrites the internal list with it. The latter is a path list option
+ (see `List Options`_ for details).
+
+ .. admonition:: Warning
+
+ The syntax is not stable yet and may change any time.
+
+ The general syntax of a user shader looks like this::
+
+ //!METADATA ARGS...
+ //!METADATA ARGS...
+
+ vec4 hook() {
+ ...
+ return something;
+ }
+
+ //!METADATA ARGS...
+ //!METADATA ARGS...
+
+ ...
+
+ Each section of metadata, along with the non-metadata lines after it,
+ defines a single block. There are currently two types of blocks, HOOKs and
+ TEXTUREs.
+
+ A ``TEXTURE`` block can set the following options:
+
+ TEXTURE <name> (required)
+ The name of this texture. Hooks can then bind the texture under this
+ name using BIND. This must be the first option of the texture block.
+
+ SIZE <width> [<height>] [<depth>] (required)
+ The dimensions of the texture. The height and depth are optional. The
+ type of texture (1D, 2D or 3D) depends on the number of components
+ specified.
+
+ FORMAT <name> (required)
+ The texture format for the samples. Supported texture formats are listed
+ in debug logging when the ``gpu`` VO is initialized (look for
+ ``Texture formats:``). Usually, this follows OpenGL naming conventions.
+ For example, ``rgb16`` provides 3 channels with normalized 16 bit
+ components. One oddity are float formats: for example, ``rgba16f`` has
+ 16 bit internal precision, but the texture data is provided as 32 bit
+ floats, and the driver converts the data on texture upload.
+
+ Although format names follow a common naming convention, not all of them
+ are available on all hardware, drivers, GL versions, and so on.
+
+ FILTER <LINEAR|NEAREST>
+ The min/magnification filter used when sampling from this texture.
+
+ BORDER <CLAMP|REPEAT|MIRROR>
+ The border wrapping mode used when sampling from this texture.
+
+ Following the metadata is a string of bytes in hexadecimal notation that
+ define the raw texture data, corresponding to the format specified by
+ `FORMAT`, on a single line with no extra whitespace.
+
+ A ``HOOK`` block can set the following options:
+
+ HOOK <name> (required)
+ The texture which to hook into. May occur multiple times within a
+ metadata block, up to a predetermined limit. See below for a list of
+ hookable textures.
+
+ DESC <title>
+ User-friendly description of the pass. This is the name used when
+ representing this shader in the list of passes for property
+ `vo-passes`.
+
+ BIND <name>
+ Loads a texture (either coming from mpv or from a ``TEXTURE`` block)
+ and makes it available to the pass. When binding textures from mpv,
+ this will also set up macros to facilitate accessing it properly. See
+ below for a list. By default, no textures are bound. The special name
+ HOOKED can be used to refer to the texture that triggered this pass.
+
+ SAVE <name>
+ Gives the name of the texture to save the result of this pass into. By
+ default, this is set to the special name HOOKED which has the effect of
+ overwriting the hooked texture.
+
+ WIDTH <szexpr>, HEIGHT <szexpr>
+ Specifies the size of the resulting texture for this pass. ``szexpr``
+ refers to an expression in RPN (reverse polish notation), using the
+ operators + - * / > < !, floating point literals, and references to
+ sizes of existing texture (such as MAIN.width or CHROMA.height),
+ OUTPUT, or NATIVE_CROPPED (size of an input texture cropped after
+ pan-and-scan, video-align-x/y, video-pan-x/y, etc. and possibly
+ prescaled). By default, these are set to HOOKED.w and HOOKED.h,
+ espectively.
+
+ WHEN <szexpr>
+ Specifies a condition that needs to be true (non-zero) for the shader
+ stage to be evaluated. If it fails, it will silently be omitted. (Note
+ that a shader stage like this which has a dependency on an optional
+ hook point can still cause that hook point to be saved, which has some
+ minor overhead)
+
+ OFFSET <ox oy | ALIGN>
+ Indicates a pixel shift (offset) introduced by this pass. These pixel
+ offsets will be accumulated and corrected during the next scaling pass
+ (``cscale`` or ``scale``). The default values are 0 0 which correspond
+ to no shift. Note that offsets are ignored when not overwriting the
+ hooked texture.
+
+ A special value of ``ALIGN`` will attempt to fix existing offset of
+ HOOKED by align it with reference. It requires HOOKED to be resizable
+ (see below). It works transparently with fragment shader. For compute
+ shader, the predefined ``texmap`` macro is required to handle coordinate
+ mapping.
+
+ COMPONENTS <n>
+ Specifies how many components of this pass's output are relevant and
+ should be stored in the texture, up to 4 (rgba). By default, this value
+ is equal to the number of components in HOOKED.
+
+ COMPUTE <bw> <bh> [<tw> <th>]
+ Specifies that this shader should be treated as a compute shader, with
+ the block size bw and bh. The compute shader will be dispatched with
+ however many blocks are necessary to completely tile over the output.
+ Within each block, there will be tw*th threads, forming a single work
+ group. In other words: tw and th specify the work group size, which can
+ be different from the block size. So for example, a compute shader with
+ bw, bh = 32 and tw, th = 8 running on a 500x500 texture would dispatch
+ 16x16 blocks (rounded up), each with 8x8 threads.
+
+ Compute shaders in mpv are treated a bit different from fragment
+ shaders. Instead of defining a ``vec4 hook`` that produces an output
+ sample, you directly define ``void hook`` which writes to a fixed
+ writeonly image unit named ``out_image`` (this is bound by mpv) using
+ `imageStore`. To help translate texture coordinates in the absence of
+ vertices, mpv provides a special function ``NAME_map(id)`` to map from
+ the texel space of the output image to the texture coordinates for all
+ bound textures. In particular, ``NAME_pos`` is equivalent to
+ ``NAME_map(gl_GlobalInvocationID)``, although using this only really
+ makes sense if (tw,th) == (bw,bh).
+
+ Each bound mpv texture (via ``BIND``) will make available the following
+ definitions to that shader pass, where NAME is the name of the bound
+ texture:
+
+ vec4 NAME_tex(vec2 pos)
+ The sampling function to use to access the texture at a certain spot
+ (in texture coordinate space, range [0,1]). This takes care of any
+ necessary normalization conversions.
+ vec4 NAME_texOff(vec2 offset)
+ Sample the texture at a certain offset in pixels. This works like
+ NAME_tex but additionally takes care of necessary rotations, so that
+ sampling at e.g. vec2(-1,0) is always one pixel to the left.
+ vec2 NAME_pos
+ The local texture coordinate of that texture, range [0,1].
+ vec2 NAME_size
+ The (rotated) size in pixels of the texture.
+ mat2 NAME_rot
+ The rotation matrix associated with this texture. (Rotates pixel space
+ to texture coordinates)
+ vec2 NAME_pt
+ The (unrotated) size of a single pixel, range [0,1].
+ float NAME_mul
+ The coefficient that needs to be multiplied into the texture contents
+ in order to normalize it to the range [0,1].
+ sampler NAME_raw
+ The raw bound texture itself. The use of this should be avoided unless
+ absolutely necessary.
+
+ Normally, users should use either NAME_tex or NAME_texOff to read from the
+ texture. For some shaders however , it can be better for performance to do
+ custom sampling from NAME_raw, in which case care needs to be taken to
+ respect NAME_mul and NAME_rot.
+
+ In addition to these parameters, the following uniforms are also globally
+ available:
+
+ float random
+ A random number in the range [0-1], different per frame.
+ int frame
+ A simple count of frames rendered, increases by one per frame and never
+ resets (regardless of seeks).
+ vec2 input_size
+ The size in pixels of the input image (possibly cropped and prescaled).
+ vec2 target_size
+ The size in pixels of the visible part of the scaled (and possibly
+ cropped) image.
+ vec2 tex_offset
+ Texture offset introduced by user shaders or options like panscan, video-align-x/y, video-pan-x/y.
+
+ Internally, vo_gpu may generate any number of the following textures.
+ Whenever a texture is rendered and saved by vo_gpu, all of the passes
+ that have hooked into it will run, in the order they were added by the
+ user. This is a list of the legal hook points:
+
+ RGB, LUMA, CHROMA, ALPHA, XYZ (resizable)
+ Source planes (raw). Which of these fire depends on the image format of
+ the source.
+
+ CHROMA_SCALED, ALPHA_SCALED (fixed)
+ Source planes (upscaled). These only fire on subsampled content.
+
+ NATIVE (resizable)
+ The combined image, in the source colorspace, before conversion to RGB.
+
+ MAINPRESUB (resizable)
+ The image, after conversion to RGB, but before
+ ``--blend-subtitles=video`` is applied.
+
+ MAIN (resizable)
+ The main image, after conversion to RGB but before upscaling.
+
+ LINEAR (fixed)
+ Linear light image, before scaling. This only fires when
+ ``--linear-upscaling``, ``--linear-downscaling`` or
+ ``--sigmoid-upscaling`` is in effect.
+
+ SIGMOID (fixed)
+ Sigmoidized light, before scaling. This only fires when
+ ``--sigmoid-upscaling`` is in effect.
+
+ PREKERNEL (fixed)
+ The image immediately before the scaler kernel runs.
+
+ POSTKERNEL (fixed)
+ The image immediately after the scaler kernel runs.
+
+ SCALED (fixed)
+ The final upscaled image, before color management.
+
+ OUTPUT (fixed)
+ The final output image, after color management but before dithering and
+ drawing to screen.
+
+ Only the textures labelled with ``resizable`` may be transformed by the
+ pass. When overwriting a texture marked ``fixed``, the WIDTH, HEIGHT and
+ OFFSET must be left at their default values.
+
+``--glsl-shader=<file>``
+ CLI/config file only alias for ``--glsl-shaders-append``.
+
+``--glsl-shader-opts=param1=value1,param2=value2,...``
+ Specifies the options to use for tunable shader parameters. You can target
+ specific named shaders by prefixing the shader name with a ``/``, e.g.
+ ``shader/param=value``. Without a prefix, parameters affect all shaders.
+ The shader name is the base part of the shader filename, without the
+ extension. (``--vo=gpu-next`` only)
+
+``--deband``
+ Enable the debanding algorithm. This greatly reduces the amount of visible
+ banding, blocking and other quantization artifacts, at the expense of
+ very slightly blurring some of the finest details. In practice, it's
+ virtually always an improvement - the only reason to disable it would be
+ for performance.
+
+``--deband-iterations=<0..16>``
+ The number of debanding steps to perform per sample. Each step reduces a
+ bit more banding, but takes time to compute. Note that the strength of each
+ step falls off very quickly, so high numbers (>4) are practically useless.
+ (Default 1)
+
+``--deband-threshold=<0..4096>``
+ The debanding filter's cut-off threshold. Higher numbers increase the
+ debanding strength dramatically but progressively diminish image details.
+ (Default 48)
+
+``--deband-range=<1..64>``
+ The debanding filter's initial radius. The radius increases linearly for
+ each iteration. A higher radius will find more gradients, but a lower
+ radius will smooth more aggressively. (Default 16)
+
+ If you increase the ``--deband-iterations``, you should probably decrease
+ this to compensate.
+
+``--deband-grain=<0..4096>``
+ Add some extra noise to the image. This significantly helps cover up
+ remaining quantization artifacts. Higher numbers add more noise. (Default
+ 32)
+
+``--corner-rounding=<0..1>``
+ If set to a value above 0.0, the output will be rendered with rounded
+ corners, as if an alpha transparency mask had been applied. The value
+ indicates the relative fraction of the side length to round - a value of
+ 1.0 rounds the corners as much as possible. (``--vo=gpu-next`` only)
+
+``--sharpen=<value>``
+ If set to a value other than 0, enable an unsharp masking filter. Positive
+ values will sharpen the image (but add more ringing and aliasing). Negative
+ values will blur the image. If your GPU is powerful enough, consider
+ alternatives like the ``ewa_lanczossharp`` scale filter, or the
+ ``--scale-blur`` option. (Only for ``--vo=gpu``)
+
+``--opengl-glfinish``
+ Call ``glFinish()`` before swapping buffers (default: disabled). Slower,
+ but might improve results when doing framedropping. Can completely ruin
+ performance. The details depend entirely on the OpenGL driver.
+
+``--opengl-waitvsync``
+ Call ``glXWaitVideoSyncSGI`` after each buffer swap (default: disabled).
+ This may or may not help with video timing accuracy and frame drop. It's
+ possible that this makes video output slower, or has no effect at all.
+
+ X11/GLX only.
+
+``--opengl-dwmflush=<no|windowed|yes|auto>``
+ (Windows only)
+ Calls ``DwmFlush`` after swapping buffers on Windows (default: auto). It
+ also sets ``SwapInterval(0)`` to ignore the OpenGL timing. Values are: no
+ (disabled), windowed (only in windowed mode), yes (also in full screen).
+
+ The value ``auto`` will try to determine whether the compositor is active,
+ and calls ``DwmFlush`` only if it seems to be.
+
+ This may help to get more consistent frame intervals, especially with
+ high-fps clips - which might also reduce dropped frames. Typically, a value
+ of ``windowed`` should be enough, since full screen may bypass the DWM.
+
+``--angle-d3d11-feature-level=<11_0|10_1|10_0|9_3>``
+ Selects a specific feature level when using the ANGLE backend with D3D11.
+ By default, the highest available feature level is used. This option can be
+ used to select a lower feature level, which is mainly useful for debugging.
+ Note that OpenGL ES 3.0 is only supported at feature level 10_1 or higher.
+ Most extended OpenGL features will not work at lower feature levels
+ (similar to ``--gpu-dumb-mode``).
+
+ Windows with ANGLE only.
+
+``--angle-d3d11-warp=<yes|no|auto>``
+ Use WARP (Windows Advanced Rasterization Platform) when using the ANGLE
+ backend with D3D11 (default: auto). This is a high performance software
+ renderer. By default, it is used when the Direct3D hardware does not
+ support Direct3D 11 feature level 9_3. While the extended OpenGL features
+ will work with WARP, they can be very slow.
+
+ Windows with ANGLE only.
+
+``--angle-egl-windowing=<yes|no|auto>``
+ Use ANGLE's built in EGL windowing functions to create a swap chain
+ (default: auto). If this is set to ``no`` and the D3D11 renderer is in use,
+ ANGLE's built in swap chain will not be used and a custom swap chain that
+ is optimized for video rendering will be created instead. If set to
+ ``auto``, a custom swap chain will be used for D3D11 and the built in swap
+ chain will be used for D3D9. This option is mainly for debugging purposes,
+ in case the custom swap chain has poor performance or does not work.
+
+ If set to ``yes``, the ``--angle-flip`` option will have no effect.
+
+ Windows with ANGLE only.
+
+``--angle-flip=<yes|no>``
+ Enable flip-model presentation, which avoids unnecessarily copying the
+ backbuffer by sharing surfaces with the DWM (default: yes). This may cause
+ performance issues with older drivers. If flip-model presentation is not
+ supported (for example, on Windows 7 without the platform update), mpv will
+ automatically fall back to the older bitblt presentation model.
+
+ If set to ``no``, the ``--angle-swapchain-length`` option will have no
+ effect.
+
+ Windows with ANGLE only.
+
+``--angle-renderer=<d3d9|d3d11|auto>``
+ Forces a specific renderer when using the ANGLE backend (default: auto). In
+ auto mode this will pick D3D11 for systems that support Direct3D 11 feature
+ level 9_3 or higher, and D3D9 otherwise. This option is mainly for
+ debugging purposes. Normally there is no reason to force a specific
+ renderer, though ``--angle-renderer=d3d9`` may give slightly better
+ performance on old hardware. Note that the D3D9 renderer only supports
+ OpenGL ES 2.0, so most extended OpenGL features will not work if this
+ renderer is selected (similar to ``--gpu-dumb-mode``).
+
+ Windows with ANGLE only.
+
+``--macos-force-dedicated-gpu=<yes|no>``
+ Deactivates the automatic graphics switching and forces the dedicated GPU.
+ (default: no)
+
+ macOS only.
+
+``--cocoa-cb-sw-renderer=<yes|no|auto>``
+ Use the Apple Software Renderer when using cocoa-cb (default: auto). If set
+ to ``no`` the software renderer is never used and instead fails when a the
+ usual pixel format could not be created, ``yes`` will always only use the
+ software renderer, and ``auto`` only falls back to the software renderer
+ when the usual pixel format couldn't be created.
+
+ macOS only.
+
+``--cocoa-cb-10bit-context=<yes|no>``
+ Creates a 10bit capable pixel format for the context creation (default: yes).
+ Instead of 8bit integer framebuffer a 16bit half-float framebuffer is
+ requested.
+
+ macOS only.
+
+``--macos-title-bar-appearance=<appearance>``
+ Sets the appearance of the title bar (default: auto). Not all combinations
+ of appearances and ``--macos-title-bar-material`` materials make sense or
+ are unique. Appearances that are not supported by you current macOS version
+ fall back to the default value.
+ macOS and cocoa-cb only
+
+ ``<appearance>`` can be one of the following:
+
+ :auto: Detects the system settings and sets the title
+ bar appearance appropriately. On macOS 10.14 it
+ also detects run time changes.
+ :aqua: The standard macOS Light appearance.
+ :darkAqua: The standard macOS Dark appearance. (macOS 10.14+)
+ :vibrantLight: Light vibrancy appearance with.
+ :vibrantDark: Dark vibrancy appearance with.
+ :aquaHighContrast: Light Accessibility appearance. (macOS 10.14+)
+ :darkAquaHighContrast: Dark Accessibility appearance. (macOS 10.14+)
+ :vibrantLightHighContrast: Light vibrancy Accessibility appearance.
+ (macOS 10.14+)
+ :vibrantDarkHighContrast: Dark vibrancy Accessibility appearance.
+ (macOS 10.14+)
+
+``--macos-title-bar-material=<material>``
+ Sets the material of the title bar (default: titlebar). All deprecated
+ materials should not be used on macOS 10.14+ because their functionality
+ is not guaranteed. Not all combinations of materials and
+ ``--macos-title-bar-appearance`` appearances make sense or are unique.
+ Materials that are not supported by you current macOS version fall back to
+ the default value.
+ macOS and cocoa-cb only
+
+ ``<material>`` can be one of the following:
+
+ :titlebar: The standard macOS title bar material.
+ :selection: The standard macOS selection material.
+ :menu: The standard macOS menu material. (macOS 10.11+)
+ :popover: The standard macOS popover material. (macOS 10.11+)
+ :sidebar: The standard macOS sidebar material. (macOS 10.11+)
+ :headerView: The standard macOS header view material.
+ (macOS 10.14+)
+ :sheet: The standard macOS sheet material. (macOS 10.14+)
+ :windowBackground: The standard macOS window background material.
+ (macOS 10.14+)
+ :hudWindow: The standard macOS hudWindow material. (macOS 10.14+)
+ :fullScreen: The standard macOS full screen material.
+ (macOS 10.14+)
+ :toolTip: The standard macOS tool tip material. (macOS 10.14+)
+ :contentBackground: The standard macOS content background material.
+ (macOS 10.14+)
+ :underWindowBackground: The standard macOS under window background material.
+ (macOS 10.14+)
+ :underPageBackground: The standard macOS under page background material.
+ (deprecated in macOS 10.14+)
+ :dark: The standard macOS dark material.
+ (deprecated in macOS 10.14+)
+ :light: The standard macOS light material.
+ (macOS 10.14+)
+ :mediumLight: The standard macOS mediumLight material.
+ (macOS 10.11+, deprecated in macOS 10.14+)
+ :ultraDark: The standard macOS ultraDark material.
+ (macOS 10.11+ deprecated in macOS 10.14+)
+
+``--macos-title-bar-color=<color>``
+ Sets the color of the title bar (default: completely transparent). Is
+ influenced by ``--macos-title-bar-appearance`` and
+ ``--macos-title-bar-material``.
+ See ``--sub-color`` for color syntax.
+
+``--macos-fs-animation-duration=<default|0-1000>``
+ Sets the fullscreen resize animation duration in ms (default: default).
+ The default value is slightly less than the system's animation duration
+ (500ms) to prevent some problems when the end of an async animation happens
+ at the same time as the end of the system wide fullscreen animation. Setting
+ anything higher than 500ms will only prematurely cancel the resize animation
+ after the system wide animation ended. The upper limit is still set at
+ 1000ms since it's possible that Apple or the user changes the system
+ defaults. Anything higher than 1000ms though seems too long and shouldn't be
+ set anyway.
+ (macOS and cocoa-cb only)
+
+
+``--macos-app-activation-policy=<regular|accessory|prohibited>``
+ Changes the App activation policy. With accessory the mpv icon in the Dock
+ can be hidden. (default: regular)
+
+ macOS only.
+
+``--macos-geometry-calculation=<visible|whole>``
+ This changes the rectangle which is used to calculate the screen position
+ and size of the window (default: visible). ``visible`` takes the the menu
+ bar and Dock into account and the window is only positioned/sized within the
+ visible screen frame rectangle, ``whole`` takes the whole screen frame
+ rectangle and ignores the menu bar and Dock. Other previous restrictions
+ still apply, like the window can't be placed on top of the menu bar etc.
+
+ macOS only.
+
+``--macos-render-timer=<timer>``
+ Sets the mode (default: callback) for syncing the rendering of frames to the display's
+ vertical refresh rate.
+ macOS and Vulkan (macvk) only.
+
+ ``<timer>`` can be one of the following:
+
+ :callback: Syncs to the CVDisplayLink callback
+ :precise: Syncs to the time of the next vertical display refresh reported by the
+ CVDisplayLink callback provided information
+ :system: No manual syncing, depend on the layer mechanic and the next drawable
+
+``--android-surface-size=<WxH>``
+ Set dimensions of the rendering surface used by the Android gpu context.
+ Needs to be set by the embedding application if the dimensions change during
+ runtime (i.e. if the device is rotated), via the surfaceChanged callback.
+
+ Android with ``--gpu-context=android`` only.
+
+``--gpu-sw``
+ Continue even if a software renderer is detected.
+
+``--gpu-context=<sys>``
+ The value ``auto`` (the default) selects the GPU context. You can also pass
+ ``help`` to get a complete list of compiled in backends (sorted by
+ autoprobe order).
+
+ auto
+ auto-select (default)
+ cocoa
+ Cocoa/macOS (deprecated, use --vo=libmpv instead)
+ win
+ Win32/WGL
+ winvk
+ VK_KHR_win32_surface
+ angle
+ Direct3D11 through the OpenGL ES translation layer ANGLE. This supports
+ almost everything the ``win`` backend does (if the ANGLE build is new
+ enough).
+ dxinterop (experimental)
+ Win32, using WGL for rendering and Direct3D 9Ex for presentation. Works
+ on Nvidia and AMD. Newer Intel chips with the latest drivers may also
+ work.
+ d3d11
+ Win32, with native Direct3D 11 rendering.
+ x11
+ X11/GLX (deprecated/legacy, EGL is preferred these days)
+ x11vk
+ VK_KHR_xlib_surface
+ wayland
+ Wayland/EGL
+ waylandvk
+ VK_KHR_wayland_surface
+ drm
+ DRM/EGL
+ displayvk
+ VK_KHR_display. This backend is roughly the Vukan equivalent of
+ DRM/EGL, allowing for direct rendering via Vulkan without a display
+ manager.
+ x11egl
+ X11/EGL
+ android
+ Android/EGL. Requires ``--wid`` be set to an ``android.view.Surface``.
+ macvk
+ Vulkan on macOS with a metal surface through a translation layer (experimental)
+
+``--gpu-api=<type>``
+ Controls which type of graphics APIs will be accepted:
+
+ auto
+ Use any available API (default)
+ opengl
+ Allow only OpenGL (requires OpenGL 2.1+ or GLES 2.0+)
+ vulkan
+ Allow only Vulkan (requires a valid/working ``--spirv-compiler``)
+ d3d11
+ Allow only ``--gpu-context=d3d11``
+
+``--opengl-es=<mode>``
+ Controls which type of OpenGL context will be accepted:
+
+ auto
+ Allow all types of OpenGL (default)
+ yes
+ Only allow GLES
+ no
+ Only allow desktop/core GL
+
+``--fbo-format=<fmt>``
+ Selects the internal format of textures used for FBOs. The format can
+ influence performance and quality of the video output. ``fmt`` can be one
+ of: rgb8, rgb10, rgb10_a2, rgb16, rgb16f, rgb32f, rgba12, rgba16, rgba16f,
+ rgba16hf, rgba32f.
+
+ Default: ``auto``, which first attempts to utilize 16bit float
+ (rgba16f, rgba16hf), and falls back to rgba16 if those are not available.
+ Finally, attempts to utilize rgb10_a2 or rgba8 if all of the previous formats
+ are not available.
+
+``--gamma-factor=<0.1..2.0>``
+ Set an additional raw gamma factor (default: 1.0). If gamma is adjusted in
+ other ways (like with the ``--gamma`` option or key bindings and the
+ ``gamma`` property), the value is multiplied with the other gamma value.
+
+ This option is deprecated and may be removed in the future.
+
+``--gamma-auto``
+ Automatically corrects the gamma value depending on ambient lighting
+ conditions (adding a gamma boost for bright rooms).
+
+ This option is deprecated and may be removed in the future.
+
+ NOTE: Only implemented on macOS.
+
+``--image-lut=<file>``
+ Specifies a custom LUT file (in Adobe .cube format) to apply to the colors
+ during image decoding. The exact interpretation of the LUT depends on
+ the value of ``--image-lut-type``. (Only for ``--vo=gpu-next``)
+
+``--image-lut-type=<value>``
+ Controls the interpretation of color values fed to and from the LUT
+ specified as ``--image-lut``. Valid values are:
+
+ auto
+ Chooses the interpretation of the LUT automatically from tagged
+ metadata, and otherwise falls back to ``native``. (Default)
+ native
+ Applied to the raw image contents in its native colorspace, before
+ decoding to RGB. For example, for a HDR10 image, this would be fed
+ PQ-encoded YCbCr values in the range 0.0 - 1.0.
+ normalized
+ Applied to the normalized RGB image contents, after decoding from
+ its native color encoding, but before linearization.
+ conversion
+ Fully replaces the color decoding. A LUT of this type should ingest the
+ image's native colorspace and output normalized non-linear RGB.
+
+``--target-colorspace-hint``
+ Automatically configure the output colorspace of the display to pass
+ through the input values of the stream (e.g. for HDR passthrough), if
+ possible. Requires a supporting driver and ``--vo=gpu-next``.
+
+``--target-prim=<value>``
+ Specifies the primaries of the display. Video colors will be adapted to
+ this colorspace when ICC color management is not being used. Valid values
+ are:
+
+ auto
+ Disable any adaptation, except for atypical color spaces. Specifically,
+ wide/unusual gamuts get automatically adapted to BT.709, while standard
+ gamut (i.e. BT.601 and BT.709) content is not touched. (default)
+ bt.470m
+ ITU-R BT.470 M
+ bt.601-525
+ ITU-R BT.601 (525-line SD systems, eg. NTSC), SMPTE 170M/240M
+ bt.601-625
+ ITU-R BT.601 (625-line SD systems, eg. PAL/SECAM), ITU-R BT.470 B/G
+ bt.709
+ ITU-R BT.709 (HD), IEC 61966-2-4 (sRGB), SMPTE RP177 Annex B
+ bt.2020
+ ITU-R BT.2020 (UHD)
+ apple
+ Apple RGB
+ adobe
+ Adobe RGB (1998)
+ prophoto
+ ProPhoto RGB (ROMM)
+ cie1931
+ CIE 1931 RGB (not to be confused with CIE XYZ)
+ dci-p3
+ DCI-P3 (Digital Cinema Colorspace), SMPTE RP431-2
+ v-gamut
+ Panasonic V-Gamut (VARICAM) primaries
+ s-gamut
+ Sony S-Gamut (S-Log) primaries
+
+``--target-trc=<value>``
+ Specifies the transfer characteristics (gamma) of the display. Video colors
+ will be adjusted to this curve when ICC color management is not being used.
+ Valid values are:
+
+ auto
+ Disable any adaptation, except for atypical transfers. Specifically,
+ HDR or linear light source material gets automatically converted to
+ gamma 2.2, while SDR content is not touched. (default)
+ bt.1886
+ ITU-R BT.1886 curve (assuming infinite contrast)
+ srgb
+ IEC 61966-2-4 (sRGB)
+ linear
+ Linear light output
+ gamma1.8
+ Pure power curve (gamma 1.8), also used for Apple RGB
+ gamma2.0
+ Pure power curve (gamma 2.0)
+ gamma2.2
+ Pure power curve (gamma 2.2)
+ gamma2.4
+ Pure power curve (gamma 2.4)
+ gamma2.6
+ Pure power curve (gamma 2.6)
+ gamma2.8
+ Pure power curve (gamma 2.8), also used for BT.470-BG
+ prophoto
+ ProPhoto RGB (ROMM)
+ pq
+ ITU-R BT.2100 PQ (Perceptual quantizer) curve, aka SMPTE ST2084
+ hlg
+ ITU-R BT.2100 HLG (Hybrid Log-gamma) curve, aka ARIB STD-B67
+ v-log
+ Panasonic V-Log (VARICAM) curve
+ s-log1
+ Sony S-Log1 curve
+ s-log2
+ Sony S-Log2 curve
+
+ .. note::
+
+ When using HDR output formats, mpv will encode to the specified
+ curve but it will not set any HDMI flags or other signalling that might
+ be required for the target device to correctly display the HDR signal.
+ The user should independently guarantee this before using these signal
+ formats for display.
+
+``--target-peak=<auto|nits>``
+ Specifies the measured peak brightness of the output display, in cd/m^2
+ (AKA nits). The interpretation of this brightness depends on the configured
+ ``--target-trc``. In all cases, it imposes a limit on the signal values
+ that will be sent to the display. If the source exceeds this brightness
+ level, a tone mapping filter will be inserted. For HLG, it has the
+ additional effect of parametrizing the inverse OOTF, in order to get
+ colorimetrically consistent results with the mastering display. For SDR, or
+ when using an ICC (profile (``--icc-profile``), setting this to a value
+ above 203 essentially causes the display to be treated as if it were an HDR
+ display in disguise. (See the note below)
+
+ In ``auto`` mode (the default), the chosen peak is an appropriate value
+ based on the TRC in use. For SDR curves, it uses 203. For HDR curves, it
+ uses 203 * the transfer function's nominal peak.
+
+ .. note::
+
+ When using an SDR transfer function, this is normally not needed, and
+ setting it may lead to very unexpected results. The one time it *is*
+ useful is if you want to calibrate a HDR display using traditional
+ transfer functions and calibration equipment. In such cases, you can
+ set your HDR display to a high brightness such as 800 cd/m^2, and then
+ calibrate it to a standard curve like gamma2.8. Setting this value to
+ 800 would then instruct mpv to essentially treat it as an HDR display
+ with the given peak. This may be a good alternative in environments
+ where PQ or HLG input to the display is not possible, and makes it
+ possible to use HDR displays with mpv regardless of operating system
+ support for HDMI HDR metadata.
+
+ In such a configuration, we highly recommend setting ``--tone-mapping``
+ to ``mobius`` or even ``clip``.
+
+``--target-contrast=<auto|10-1000000|inf>``
+ Specifies the measured contrast of the output display. ``--target-contrast``
+ in conjunction with ``--target-peak`` value is used to calculate display
+ black point. Used in black point compensation during HDR tone-mapping.
+ ``auto`` is the default and assumes 1000:1 contrast as a typical SDR display
+ would have or an infinite contrast when HDR ``--target-trc`` is used.
+ ``inf`` contrast specifies display with perfect black level, in practice OLED.
+ (Only for ``--vo=gpu-next``)
+
+``--target-gamut=<value>``
+ Constrains the gamut of the display. You can use this option to output e.g.
+ DCIP3-in-BT.2020. Set ``--target-prim`` to the primaries of the containing
+ colorspace (into which values will be encoded), and ``--target-gamut`` to
+ the gamut you want to limit colors to. Takes the same values as
+ ``--target-prim``. (Only for ``--vo=gpu-next``)
+
+``--target-lut=<file>``
+ Specifies a custom LUT file (in Adobe .cube format) to apply to the colors
+ before display on-screen. This LUT is fed values in normalized RGB, after
+ encoding into the target colorspace, so after the application of
+ ``--target-trc``. (Only for ``--vo=gpu-next``)
+
+``--tone-mapping=<value>``
+ Specifies the algorithm used for tone-mapping images onto the target
+ display. This is relevant for both HDR->SDR conversion as well as gamut
+ reduction (e.g. playing back BT.2020 content on a standard gamut display).
+ Valid values are:
+
+ auto
+ Choose the best curve according to internal heuristics. (Default)
+ clip
+ Hard-clip any out-of-range values. Use this when you care about
+ perfect color accuracy for in-range values at the cost of completely
+ distorting out-of-range values. Not generally recommended.
+ mobius
+ Generalization of Reinhard to a Möbius transform with linear section.
+ Smoothly maps out-of-range values while retaining contrast and colors
+ for in-range material as much as possible. Use this when you care about
+ color accuracy more than detail preservation. This is somewhere in
+ between ``clip`` and ``reinhard``, depending on the value of
+ ``--tone-mapping-param``.
+ reinhard
+ Reinhard tone mapping algorithm. Very simple continuous curve.
+ Preserves overall image brightness but uses nonlinear contrast, which
+ results in flattening of details and degradation in color accuracy.
+ hable
+ Similar to ``reinhard`` but preserves both dark and bright details
+ better (slightly sigmoidal), at the cost of slightly darkening /
+ desaturating everything. Developed by John Hable for use in video
+ games. Use this when you care about detail preservation more than
+ color/brightness accuracy. This is roughly equivalent to
+ ``--tone-mapping=reinhard --tone-mapping-param=0.24``. If possible,
+ you should also enable ``--hdr-compute-peak`` for the best results.
+ bt.2390
+ Perceptual tone mapping curve (EETF) specified in ITU-R Report BT.2390.
+ gamma
+ Fits a logarithmic transfer between the tone curves.
+ linear
+ Linearly stretches the entire reference gamut to (a linear multiple of)
+ the display.
+ spline
+ Perceptually linear single-pivot polynomial. (``--vo=gpu-next`` only)
+ bt.2446a
+ HDR<->SDR mapping specified in ITU-R Report BT.2446, method A. This is
+ the recommended curve for well-mastered content. (``--vo=gpu-next``
+ only)
+ st2094-40
+ Dynamic HDR10+ tone-mapping method specified in SMPTE ST2094-40 Annex
+ B. In the absence of metadata, falls back to a fixed spline matched to
+ the input/output average brightness characteristics. (``--vo=gpu-next``
+ only)
+ st2094-10
+ Dynamic tone-mapping method specified in SMPTE ST2094-10 Annex B.2.
+ Conceptually simpler than ST2094-40, and generally produces worse
+ results.
+
+``--tone-mapping-param=<value>``
+ Set tone mapping parameters. By default, this is set to the special string
+ ``default``, which maps to an algorithm-specific default value. Ignored if
+ the tone mapping algorithm is not tunable. This affects the following tone
+ mapping algorithms:
+
+ clip
+ Specifies an extra linear coefficient to multiply into the signal
+ before clipping. Defaults to 1.0.
+ mobius
+ Specifies the transition point from linear to mobius transform. Every
+ value below this point is guaranteed to be mapped 1:1. The higher the
+ value, the more accurate the result will be, at the cost of losing
+ bright details. Defaults to 0.3, which due to the steep initial slope
+ still preserves in-range colors fairly accurately.
+ reinhard
+ Specifies the local contrast coefficient at the display peak. Defaults
+ to 0.5, which means that in-gamut values will be about half as bright
+ as when clipping.
+ bt.2390
+ Specifies the offset for the knee point. Defaults to 1.0, which is
+ higher than the value from the original ITU-R specification (0.5).
+ (``--vo=gpu-next`` only)
+ gamma
+ Specifies the exponent of the function. Defaults to 1.8.
+ linear
+ Specifies the scale factor to use while stretching. Defaults to 1.0.
+ spline
+ Specifies the knee point (in PQ space). Defaults to 0.30.
+ st2094-10
+ Specifies the contrast (slope) at the knee point. Defaults to 1.0.
+
+``--inverse-tone-mapping``
+ If set, allows inverse tone mapping (expanding SDR to HDR). Not supported
+ by all tone mapping curves. Use with caution. (``--vo=gpu-next`` only)
+
+``--tone-mapping-max-boost=<1.0..10.0>``
+ Upper limit for how much the tone mapping algorithm is allowed to boost
+ the average brightness by over-exposing the image. The default value of 1.0
+ allows no additional brightness boost. A value of 2.0 would allow
+ over-exposing by a factor of 2, and so on. Raising this setting can help
+ reveal details that would otherwise be hidden in dark scenes, but raising
+ it too high will make dark scenes appear unnaturally bright. (``--vo=gpu``
+ only)
+
+``--tone-mapping-visualize``
+ Display a (PQ-PQ) graph of the active tone-mapping LUT. Intended only for
+ debugging purposes. The X axis shows PQ input values, the Y axis shows PQ
+ output values. The tone-mapping curve is shown in green/yellow. Yellow
+ means the brightness has been boosted from the source, dark blue regions
+ show where the brightness has been reduced. The extra colored regions and
+ lines indicate various monitor limits, as well a reference diagonal
+ (neutral tone-mapping) and source scene average brightness information (if
+ available). (``--vo=gpu-next`` only)
+
+``--gamut-mapping-mode``
+ Specifies the algorithm used for reducing the gamut of images for the
+ target display, after any tone mapping is done.
+
+ auto
+ Choose the best mode automatically. (Default)
+ clip
+ Hard-clip to the gamut (per-channel). Very low quality, but free.
+ perceptual
+ Performs a perceptually balanced gamut mapping using a soft knee
+ function to roll-off clipped regions, and a hue shifting function to
+ preserve saturation. (``--vo=gpu-next`` only)
+ relative
+ Performs relative colorimetric clipping, while maintaining an
+ exponential relationship between brightness and chromaticity.
+ (``--vo=gpu-next`` only)
+ saturation
+ Performs simple RGB->RGB saturation mapping. The input R/G/B channels
+ are mapped directly onto the output R/G/B channels. Will never clip,
+ but will distort all hues and/or result in a faded look.
+ (``--vo=gpu-next`` only)
+ absolute
+ Performs absolute colorimetric clipping. Like ``relative``, but does
+ not adapt the white point. (``--vo=gpu-next`` only)
+ desaturate
+ Performs constant-luminance colorimetric clipping, desaturing colors
+ towards white until they're in-range.
+ darken
+ Uniformly darkens the input slightly to prevent clipping on blown-out
+ highlights, then clamps colorimetrically to the input gamut boundary,
+ biased slightly to preserve chromaticity over luminance.
+ (``--vo=gpu-next`` only)
+ warn
+ Performs no gamut mapping, but simply highlights out-of-gamut pixels.
+ linear
+ Linearly/uniformly desaturates the image in order to bring the entire
+ image into the target gamut. (``--vo=gpu-next`` only)
+
+``--hdr-compute-peak=<auto|yes|no>``
+ Compute the HDR peak and frame average brightness per-frame instead of
+ relying on tagged metadata. These values are averaged over local regions as
+ well as over several frames to prevent the value from jittering around too
+ much. This option basically gives you dynamic, per-scene tone mapping.
+ Requires compute shaders, which is a fairly recent OpenGL feature, and will
+ probably also perform horribly on some drivers, so enable at your own risk.
+ The special value ``auto`` (default) will enable HDR peak computation
+ automatically if compute shaders and SSBOs are supported.
+
+``--allow-delayed-peak-detect``
+ When using ``--hdr-compute-peak``, allow delaying the detected peak by a
+ frame when beneficial for performance. In particular, this is required to
+ avoid an unnecessary FBO indirection when no advanced rendering is required
+ otherwise. Has no effect if there already is an indirect pass, such as when
+ advanced scaling is enabled. Defaults to no. (Only affects
+ ``--vo=gpu-next``, note that ``--vo=gpu`` always delays the peak.)
+
+``--hdr-peak-percentile=<0.0..100.0>``
+ Which percentile of the input image brightness histogram to consider as the
+ true peak of the scene. If this is set to 100 (default), the
+ brightest pixel is measured. Otherwise, the top of the frequency
+ distribution is progressively cut off. Setting this too low will cause
+ clipping of very bright details, but can improve the dynamic brightness
+ range of scenes with very bright isolated highlights. Values other than 100
+ come with a small performance penalty. (Only for ``--vo=gpu-next``)
+
+``--hdr-peak-decay-rate=<0.0..1000.0>``
+ The decay rate used for the HDR peak detection algorithm (default: 20.0).
+ This is only relevant when ``--hdr-compute-peak`` is enabled. Higher values
+ make the peak decay more slowly, leading to more stable values at the cost
+ of more "eye adaptation"-like effects (although this is mitigated somewhat
+ by ``--hdr-scene-threshold``). A value of 0.0 (the lowest possible) disables
+ all averaging, meaning each frame's value is used directly as measured,
+ but doing this is not recommended for "noisy" sources since it may lead
+ to excessive flicker. (In signal theory terms, this controls the time
+ constant "tau" of an IIR low pass filter)
+
+``--hdr-scene-threshold-low=<0.0..100.0>``, ``--hdr-scene-threshold-high=<0.0..100.0>``
+ The lower and upper thresholds (in dB) for a brightness difference
+ to be considered a scene change (default: 1.0 low, 3.0 high). This is only
+ relevant when ``--hdr-compute-peak`` is enabled. Normally, small
+ fluctuations in the frame brightness are compensated for by the peak
+ averaging mechanism, but for large jumps in the brightness this can result
+ in the frame remaining too bright or too dark for up to several seconds,
+ depending on the value of ``--hdr-peak-decay-rate``. To counteract this,
+ when the brightness between the running average and the current frame
+ exceeds the low threshold, mpv will make the averaging filter more
+ aggressive, up to the limit of the high threshold (at which point the
+ filter becomes instant).
+
+``--hdr-contrast-recovery=<0.0..2.0>``, ``--hdr-contrast-smoothness=<1.0..100.0>``
+ Enables the HDR contrast recovery algorithm, which is to designed to
+ enhance contrast of HDR video after tone mapping. The strength (default:
+ 0.0) indicates the degree of contrast recovery, with 0.0 being completely
+ disabled and 1.0 being 100% strength. Values higher than 1.0 are allowed,
+ but may result in excessive sharpening. The smoothness (default: 3.5)
+ indicates the degree to which the HDR source is low-passed in order to
+ obtain contrast information - a value of 2.0 corresponds to 2x downscaling.
+ Users on low DPI displays (<= 100) may want to lower this value, while
+ users on very high DPI displays ("retina") may want to increase it. (Only
+ for ``vo=gpu-next``)
+
+``--use-embedded-icc-profile``
+ Load the embedded ICC profile contained in media files such as PNG images.
+ (Default: yes). Note that this option only works when also using a display
+ ICC profile (``--icc-profile`` or ``--icc-profile-auto``), and also
+ requires LittleCMS 2 support.
+
+``--icc-profile=<file>``
+ Load an ICC profile and use it to transform video RGB to screen output.
+ Needs LittleCMS 2 support compiled in. This option overrides the
+ ``--target-prim``, ``--target-trc`` and ``--icc-profile-auto`` options.
+
+``--icc-profile-auto``
+ Automatically select the ICC display profile currently specified by the
+ display settings of the operating system.
+
+ NOTE: On Windows, the default profile must be an ICC profile. WCS profiles
+ are not supported.
+
+ Applications using libmpv with the render API need to provide the ICC
+ profile via ``MPV_RENDER_PARAM_ICC_PROFILE``.
+
+``--icc-cache``
+ Store and load 3DLUTs created from the ICC profile on disk in the
+ cache directory (Default: ``yes``). This can be used to speed up loading,
+ since LittleCMS 2 can take a while to create a 3D LUT. Note that these
+ files contain uncompressed LUTs. Their size depends on the
+ ``--icc-3dlut-size``, and can be very big.
+
+ NOTE: On ``--vo=gpu``, this is not cleaned automatically, so old, unused
+ cache files may stick around indefinitely.
+
+``--icc-cache-dir``
+ The directory where icc cache is stored. Cache is stored in the system's
+ cache directory (usually ``~/.cache/mpv``) if this is unset.
+
+``--icc-intent=<value>``
+ Specifies the ICC intent used for the color transformation (when using
+ ``--icc-profile``).
+
+ 0
+ perceptual
+ 1
+ relative colorimetric (default)
+ 2
+ saturation
+ 3
+ absolute colorimetric
+
+``--icc-3dlut-size=<auto|RxGxB>``
+ Size of the 3D LUT generated from the ICC profile in each dimension. The
+ default of ``auto`` means to pick the size automatically based on the
+ profile characteristics. Sizes may range from 2 to 512.
+
+ NOTE: Setting this option to anything other than ``auto`` is **strongly**
+ discouraged, except for testing.
+
+``--icc-force-contrast=<no|0-1000000|inf>``
+ Override the target device's detected contrast ratio by a specific value.
+ This is detected automatically from the profile if possible, but for some
+ profiles it might be missing, causing the contrast to be assumed as
+ infinite. As a result, video may appear darker than intended. If this is
+ the case, setting this option might help. This only affects BT.1886
+ content. The default of ``no`` means to use the profile values. The special
+ value ``inf`` causes the BT.1886 curve to be treated as a pure power gamma
+ 2.4 function.
+
+``--icc-use-luma``
+ Use ICC profile luminance value. (Only for ``--vo=gpu-next``)
+
+``--lut=<file>``
+ Specifies a custom LUT (in Adobe .cube format) to apply to the colors
+ as part of color conversion. The exact interpretation depends on the value
+ of ``--lut-type``. (Only for ``--vo=gpu-next``)
+
+``--lut-type=<value>``
+ Controls the interpretation of color values fed to and from the LUT
+ specified as ``--lut``. Valid values are:
+
+ auto
+ Chooses the interpretation of the LUT automatically from tagged
+ metadata, and otherwise falls back to ``native``. (Default)
+ native
+ Applied to raw image contents in its native RGB colorspace (non-linear
+ light), before conversion to the output color space.
+ normalized
+ Applied to the normalized RGB image contents, in linear light, before
+ conversion to the output color space.
+ conversion
+ Fully replaces the conversion from the image color space to the output
+ color space. If such a LUT is present, it has the highest priority, and
+ overrides any ICC profiles, as well as options related to tone mapping
+ and output colorimetry (``--target-prim``, ``--target-trc`` etc.).
+
+``--blend-subtitles=<yes|video|no>``
+ Blend subtitles directly onto upscaled video frames, before interpolation
+ and/or color management (default: no). Enabling this causes subtitles to be
+ affected by ``--icc-profile``, ``--target-prim``, ``--target-trc``,
+ ``--interpolation``, ``--gamma-factor`` and ``--glsl-shaders``. It also
+ increases subtitle performance when using ``--interpolation``.
+
+ The downside of enabling this is that it restricts subtitles to the visible
+ portion of the video, so you can't have subtitles exist in the black
+ margins below a video (for example).
+
+ If ``video`` is selected, the behavior is similar to ``yes``, but subs are
+ drawn at the video's native resolution, and scaled along with the video.
+
+ .. warning:: This changes the way subtitle colors are handled. Normally,
+ subtitle colors are assumed to be in sRGB and color managed as
+ such. Enabling this makes them treated as being in the video's
+ color space instead. This is good if you want things like
+ softsubbed ASS signs to match the video colors, but may cause
+ SRT subtitles or similar to look slightly off.
+
+``--alpha=<blend-tiles|blend|yes|no>``
+ Decides what to do if the input has an alpha component.
+
+ blend-tiles
+ Blend the frame against a 16x16 gray/white tiles background (default).
+ blend
+ Blend the frame against the background color (``--background``, normally
+ black).
+ yes
+ Try to create a framebuffer with alpha component. This only makes sense
+ if the video contains alpha information (which is extremely rare) or if
+ you make the background color transparent. May not be supported on all
+ platforms. If alpha framebuffers are unavailable, it silently falls
+ back on a normal framebuffer. Note that if you set the ``--fbo-format``
+ option to a non-default value, a format with alpha must be specified,
+ or this won't work. Whether this really works depends on the windowing
+ system and desktop environment.
+ no
+ Ignore alpha component.
+
+``--opengl-rectangle-textures``
+ Force use of rectangle textures (default: no). Normally this shouldn't have
+ any advantages over normal textures. Note that hardware decoding overrides
+ this flag. Could be removed any time.
+
+``--background=<color>``
+ Color used to draw parts of the mpv window not covered by video. See the
+ ``--sub-color`` option for how colors are defined.
+
+``--gpu-tex-pad-x``, ``--gpu-tex-pad-y``
+ Enlarge the video source textures by this many pixels. For debugging only
+ (normally textures are sized exactly, but due to hardware decoding interop
+ we may have to deal with additional padding, which can be tested with these
+ options). Could be removed any time.
+
+``--opengl-early-flush=<yes|no|auto>``
+ Call ``glFlush()`` after rendering a frame and before attempting to display
+ it (default: auto). Can fix stuttering in some cases, in other cases
+ probably causes it. The ``auto`` mode will call ``glFlush()`` only if
+ the renderer is going to wait for a while after rendering, instead of
+ flipping GL front and backbuffers immediately (i.e. it doesn't call it
+ in display-sync mode).
+
+ On macOS this is always deactivated because it only causes performance
+ problems and other regressions.
+
+``--gpu-dumb-mode=<yes|no|auto>``
+ This mode is extremely restricted, and will disable most extended
+ features. That includes high quality scalers and custom shaders!
+
+ It is intended for hardware that does not support FBOs (including GLES,
+ which supports it insufficiently), or to get some more performance out of
+ bad or old hardware.
+
+ This mode is forced automatically if needed, and this option is mostly
+ useful for debugging. The default of ``auto`` will enable it automatically
+ if nothing uses features which require FBOs.
+
+ This option might be silently removed in the future.
+
+``--gpu-shader-cache``
+ Store and load compiled GLSL shaders in the cache directory (Default:
+ ``yes``). Normally, shader compilation is very fast, so this is not usually
+ needed. It mostly matters for anything based on D3D11 (including ANGLE), as
+ well as on some other proprietary drivers. Enabling this can improve startup
+ performance on these platforms.
+
+ NOTE: On ``--vo=gpu``, is not cleaned automatically, so old, unused cache
+ files may stick around indefinitely.
+
+``--gpu-shader-cache-dir``
+ The directory where gpu shader cache is stored. Cache is stored in the system's
+ cache directory (usually ``~/.cache/mpv``) if this is unset.
+
+``--libplacebo-opts=<key>=<value>[,<key>=<value>[,...]]``
+ Passes extra raw option to the libplacebo rendering backend (used by
+ ``--vo=gpu-next``). May override the effects of any other options set using
+ the normal options system. Requires libplacebo v6.309 or higher. Included
+ for debugging purposes only. For more information, see:
+
+ https://libplacebo.org/options/
+
+Miscellaneous
+-------------
+
+``--display-tags=tag1,tags2,...``
+ Set the list of tags that should be displayed on the terminal. Tags that
+ are in the list, but are not present in the played file, will not be shown.
+ If a value ends with ``*``, all tags are matched by prefix (though there
+ is no general globbing). Just passing ``*`` essentially filtering.
+
+ The default includes a common list of tags, call mpv with ``--list-options``
+ to see it.
+
+ This is a string list option. See `List Options`_ for details.
+
+``--mc=<seconds/frame>``
+ Maximum A-V sync correction per frame (in seconds)
+
+``--autosync=<factor>``
+ Gradually adjusts the A/V sync based on audio delay measurements.
+ Specifying ``--autosync=0``, the default, will cause frame timing to be
+ based entirely on audio delay measurements. Specifying ``--autosync=1``
+ will do the same, but will subtly change the A/V correction algorithm. An
+ uneven video framerate in a video which plays fine with ``--no-audio`` can
+ often be helped by setting this to an integer value greater than 1. The
+ higher the value, the closer the timing will be to ``--no-audio``. Try
+ ``--autosync=30`` to smooth out problems with sound drivers which do not
+ implement a perfect audio delay measurement. With this value, if large A/V
+ sync offsets occur, they will only take about 1 or 2 seconds to settle
+ out. This delay in reaction time to sudden A/V offsets should be the only
+ side effect of turning this option on, for all sound drivers.
+
+``--video-timing-offset=<seconds>``
+ Control how long before video display target time the frame should be
+ rendered (default: 0.050). If a video frame should be displayed at a
+ certain time, the VO will start rendering the frame earlier, and then will
+ perform a blocking wait until the display time, and only then "swap" the
+ frame to display. The rendering cannot start before the previous frame is
+ displayed, so this value is implicitly limited by the video framerate. With
+ normal video frame rates, the default value will ensure that rendering is
+ always immediately started after the previous frame was displayed. On the
+ other hand, setting a too high value can reduce responsiveness with low
+ FPS value.
+
+ This option is interesting for client API users using the render API
+ because you can stop it from limiting your FPS
+ (see ``mpv_render_context_render()`` documentation).
+
+ This applies only to audio timing modes (e.g. ``--video-sync=audio``). In
+ other modes (``--video-sync=display-...``), video timing relies on vsync
+ blocking, and this option is not used.
+
+``--video-sync=<audio|...>``
+ How the player synchronizes audio and video.
+
+ If you use this option, you usually want to set it to ``display-resample``
+ to enable a timing mode that tries to not skip or repeat frames when for
+ example playing 24fps video on a 24Hz screen.
+
+ The modes starting with ``display-`` try to output video frames completely
+ synchronously to the display, using the detected display vertical refresh
+ rate as a hint how fast frames will be displayed on average. These modes
+ change video speed slightly to match the display. See ``--video-sync-...``
+ options for fine tuning. The robustness of this mode is further reduced by
+ making a some idealized assumptions, which may not always apply in reality.
+ Behavior can depend on the VO and the system's video and audio drivers.
+ Media files must use constant framerate. Section-wise VFR might work as well
+ with some container formats (but not e.g. mkv).
+
+ Under some circumstances, the player automatically reverts to ``audio`` mode
+ for some time or permanently. This can happen on very low framerate video,
+ or if the framerate cannot be detected.
+
+ Also in display-sync modes it can happen that interruptions to video
+ playback (such as toggling fullscreen mode, or simply resizing the window)
+ will skip the video frames that should have been displayed, while ``audio``
+ mode will display them after the renderer has resumed (typically resulting
+ in a short A/V desync and the video "catching up").
+
+ Before mpv 0.30.0, there was a fallback to ``audio`` mode on severe A/V
+ desync. This was changed for the sake of not sporadically stopping. Now,
+ ``display-desync`` does what it promises and may desync with audio by an
+ arbitrary amount, until it is manually fixed with a seek.
+
+ These modes also require a vsync blocked presentation mode. For OpenGL, this
+ translates to ``--opengl-swapinterval=1``. For Vulkan, it translates to
+ ``--vulkan-swap-mode=fifo`` (or ``fifo-relaxed``).
+
+ The modes with ``desync`` in their names do not attempt to keep audio/video
+ in sync. They will slowly (or quickly) desync, until e.g. the next seek
+ happens. These modes are meant for testing, not serious use.
+
+ :audio: Time video frames to audio. This is the most robust
+ mode, because the player doesn't have to assume anything
+ about how the display behaves. The disadvantage is that
+ it can lead to occasional frame drops or repeats. If
+ audio is disabled, this uses the system clock. This is
+ the default mode.
+ :display-resample: Resample audio to match the video. This mode will also
+ try to adjust audio speed to compensate for other drift.
+ (This means it will play the audio at a different speed
+ every once in a while to reduce the A/V difference.)
+ :display-resample-vdrop: Resample audio to match the video. Drop video
+ frames to compensate for drift.
+ :display-resample-desync: Like the previous mode, but no A/V compensation.
+ :display-tempo: Same as ``display-resample``, but apply audio speed
+ changes to audio filters instead of resampling to avoid
+ the change in pitch. Beware that some audio filters
+ don't do well with a speed close to 1. It is recommend
+ to use a conditional profile to automatically switch to
+ ``display-resample`` when speed gets too close to 1 for
+ your filter setup. Use (speed * video_speed_correction)
+ to get the actual playback speed in the condition.
+ See `Conditional auto profiles`_ for details.
+ :display-vdrop: Drop or repeat video frames to compensate desyncing
+ video. (Although it should have the same effects as
+ ``audio``, the implementation is very different.)
+ :display-adrop: Drop or repeat audio data to compensate desyncing
+ video. This mode will cause severe audio artifacts if
+ the real monitor refresh rate is too different from
+ the reported or forced rate. Since mpv 0.33.0, this
+ acts on entire audio frames, instead of single samples.
+ :display-desync: Sync video to display, and let audio play on its own.
+ :desync: Sync video according to system clock, and let audio play
+ on its own.
+
+``--video-sync-max-factor=<value>``
+ Maximum multiple for which to try to fit the video's FPS to the display's
+ FPS (default: 5).
+
+ For example, if this is set to 1, the video FPS is forced to an integer
+ multiple of the display FPS, as long as the speed change does not exceed
+ the value set by ``--video-sync-max-video-change``.
+
+ See ``--interpolation-threshold`` for how this option affects
+ interpolation.
+
+``--video-sync-max-video-change=<value>``
+ Maximum speed difference in percent that is applied to video with
+ ``--video-sync=display-...`` (default: 1). Display sync mode will be
+ disabled if the monitor and video refresh way do not match within the
+ given range. It tries multiples as well: playing 30 fps video on a 60 Hz
+ screen will duplicate every second frame. Playing 24 fps video on a 60 Hz
+ screen will play video in a 2-3-2-3-... pattern.
+
+ The default settings are not loose enough to speed up 23.976 fps video to
+ 25 fps. We consider the pitch change too extreme to allow this behavior
+ by default. Set this option to a value of ``5`` to enable it.
+
+ Note that ``--video-sync=display-tempo`` avoids this pitch change.
+
+ Also note that in the ``--video-sync=display-resample`` or
+ ``--video-sync=display-tempo`` mode, audio speed will additionally be
+ changed by a small amount if necessary for A/V sync. See
+ ``--video-sync-max-audio-change``.
+
+``--video-sync-max-audio-change=<value>``
+ Maximum *additional* speed difference in percent that is applied to audio
+ with ``--video-sync=display-...`` (default: 0.125). Normally, the player
+ plays the audio at the speed of the video. But if the difference between
+ audio and video position is too high, e.g. due to drift or other timing
+ errors, it will attempt to speed up or slow down audio by this additional
+ factor. Too low values could lead to video frame dropping or repeating if
+ the A/V desync cannot be compensated, too high values could lead to chaotic
+ frame dropping due to the audio "overshooting" and skipping multiple video
+ frames before the sync logic can react.
+
+``--mf-fps=<value>``
+ Framerate used when decoding from multiple PNG or JPEG files with ``mf://``
+ (default: 1).
+
+``--mf-type=<value>``
+ Input file type for ``mf://`` (available: jpeg, png, tga, sgi). By default,
+ this is guessed from the file extension.
+
+``--stream-dump=<destination-filename>``
+ Instead of playing a file, read its byte stream and write it to the given
+ destination file. The destination is overwritten. Can be useful to test
+ network-related behavior.
+
+``--stream-lavf-o=opt1=value1,opt2=value2,...``
+ Set AVOptions on streams opened with libavformat. Unknown or misspelled
+ options are silently ignored. (They are mentioned in the terminal output
+ in verbose mode, i.e. ``--v``. In general we can't print errors, because
+ other options such as e.g. user agent are not available with all protocols,
+ and printing errors for unknown options would end up being too noisy.)
+
+ This is a key/value list option. See `List Options`_ for details.
+
+``--backdrop-type=<auto|none|mica|acrylic|mica-alt>``
+ (Windows only)
+ Controls the backdrop/border style.
+
+ :auto: Default Windows behavior
+ :none: The backdrop will be black or white depending on the system's theme settings.
+ :mica: Enables the Mica style, which is the default on Windows 11.
+ :acrylic: Enables the Acrylic style (frosted glass look).
+ :mica-alt: Same as Mica, except reversed.
+
+``--window-affinity=<default|excludefromcmcapture|monitor>``
+ (Windows only)
+ Controls the window affinity behavior of mpv.
+
+ :default: Default Windows behavior
+ :excludefromcapture: mpv's window will be completely excluded from capture by external applications or screen recording software.
+ :monitor: Blacks out the mpv window
+
+``--vo-mmcss-profile=<name>``
+ (Windows only)
+ Set the MMCSS profile for the video renderer thread (default: ``Playback``).
+
+``--priority=<prio>``
+ (Windows only)
+ Set process priority for mpv according to the predefined priorities
+ available under Windows.
+
+ Possible values of ``<prio>``:
+ idle|belownormal|normal|abovenormal|high|realtime
+
+ .. warning:: Using realtime priority can cause system lockup.
+
+``--force-media-title=<string>``
+ Force the contents of the ``media-title`` property to this value. Useful
+ for scripts which want to set a title, without overriding the user's
+ setting in ``--title``.
+
+``--external-files=<file-list>``
+ Load a file and add all of its tracks. This is useful to play different
+ files together (for example audio from one file, video from another), or
+ for advanced ``--lavfi-complex`` used (like playing two video files at
+ the same time).
+
+ Unlike ``--sub-files`` and ``--audio-files``, this includes all tracks, and
+ does not cause default stream selection over the "proper" file. This makes
+ it slightly less intrusive. (In mpv 0.28.0 and before, this was not quite
+ strictly enforced.)
+
+ This is a path list option. See `List Options`_ for details.
+
+``--external-file=<file>``
+ CLI/config file only alias for ``--external-files-append``. Each use of this
+ option will add a new external file.
+
+``--cover-art-files=<file-list>``
+ Use an external file as cover art while playing audio. This makes it appear
+ on the track list and subject to automatic track selection. Options like
+ ``--audio-display`` control whether such tracks are supposed to be selected.
+
+ (The difference to loading a file with ``--external-files`` is that video
+ tracks will be marked as being pictures, which affects the auto-selection
+ method. If the passed file is a video, only the first frame will be decoded
+ and displayed. Enabling the cover art track during playback may show a
+ random frame if the source file is a video. Normally you're not supposed to
+ pass videos to this option, so this paragraph describes the behavior
+ coincidentally resulting from implementation details.)
+
+ This is a path list option. See `List Options`_ for details.
+
+``--cover-art-file=<file>``
+ CLI/config file only alias for ``--cover-art-files-append``. Each use of this
+ option will add a new external file.
+
+``--cover-art-auto=<no|exact|fuzzy|all>``
+ Whether to load _external_ cover art automatically. Similar to
+ ``--sub-auto`` and ``--audio-file-auto``. If a video already has tracks
+ (which are not marked as cover art), external cover art will not be loaded.
+
+ :no: Don't automatically load cover art.
+ :exact: Load the media filename with an image file extension (default).
+ :fuzzy: Load all cover art containing the media filename.
+ :all: Load all images in the current directory.
+
+ See ``--cover-art-files`` for details about what constitutes cover art.
+
+ See ``--audio-display`` how to control display of cover art (this can be
+ used to disable cover art that is part of the file).
+
+``--cover-art-auto-exts=ext1,ext2,...``
+ Cover art extentions to try and match when using ``cover-art-auto``.
+
+ This is a string list option. See `List Options`_ for details.
+
+``--cover-art-whitelist=<no|yes>``
+ Whether to load files with a filename among "AlbumArt", "Album", "cover",
+ "front", "AlbumArtSmall", "Folder", ".folder", "thumb", and an extension in
+ ``--cover-art-auto-exts``, as cover art. This has no effect if
+ ``cover-art-auto`` is ``no``.
+
+ Default: ``yes``.
+
+``--autoload-files=<yes|no>``
+ Automatically load/select external files (default: yes).
+
+ If set to ``no``, then do not automatically load external files as specified
+ by ``--sub-auto``, ``--audio-file-auto`` and ``--cover-art-auto``. If
+ external files are forcibly added (like with ``--sub-files``), they will
+ not be auto-selected.
+
+ This does not affect playlist expansion, redirection, or other loading of
+ referenced files like with ordered chapters.
+
+``--stream-record=<file>``
+ Write received/read data from the demuxer to the given output file. The
+ output file will always be overwritten without asking. The output format
+ is determined by the extension of the output file.
+
+ Switching streams or seeking during recording might result in recording
+ being stopped and/or broken files. Use with care.
+
+ Seeking outside of the demuxer cache will result in "skips" in the output
+ file, but seeking within the demuxer cache should not affect recording. One
+ exception is when you seek back far enough to exceed the forward buffering
+ size, in which case the cache stops actively reading. This will return in
+ dropped data if it's a live stream.
+
+ If this is set at runtime, the old file is closed, and the new file is
+ opened. Note that this will write only data that is appended at the end of
+ the cache, and the already cached data cannot be written. You can try the
+ ``dump-cache`` command as an alternative.
+
+ External files (``--audio-file`` etc.) are ignored by this, it works on the
+ "main" file only. Using this with files using ordered chapters or EDL files
+ will also not work correctly in general.
+
+ There are some glitches with this because it uses FFmpeg's libavformat for
+ writing the output file. For example, it's typical that it will only work if
+ the output format is the same as the input format. This is the case even if
+ it works with the ``ffmpeg`` tool. One reason for this is that ``ffmpeg``
+ and its libraries contain certain hacks and workarounds for these issues,
+ that are unavailable to outside users.
+
+``--lavfi-complex=<string>``
+ Set a "complex" libavfilter filter, which means a single filter graph can
+ take input from multiple source audio and video tracks. The graph can result
+ in a single audio or video output (or both).
+
+ Currently, the filter graph labels are used to select the participating
+ input tracks and audio/video output. The following rules apply:
+
+ - A label of the form ``aidN`` selects audio track N as input (e.g.
+ ``aid1``).
+ - A label of the form ``vidN`` selects video track N as input.
+ - A label named ``ao`` will be connected to the audio output.
+ - A label named ``vo`` will be connected to the video output.
+
+ Each label can be used only once. If you want to use e.g. an audio stream
+ for multiple filters, you need to use the ``asplit`` filter. Multiple
+ video or audio outputs are not possible, but you can use filters to merge
+ them into one.
+
+ It's not possible to change the tracks connected to the filter at runtime,
+ unless you explicitly change the ``lavfi-complex`` property and set new
+ track assignments. When the graph is changed, the track selection is changed
+ according to the used labels as well.
+
+ Other tracks, as long as they're not connected to the filter, and the
+ corresponding output is not connected to the filter, can still be freely
+ changed with the normal methods.
+
+ Note that the normal filter chains (``--af``, ``--vf``) are applied between
+ the complex graphs (e.g. ``ao`` label) and the actual output.
+
+ .. admonition:: Examples
+
+ - ``--lavfi-complex='[aid1] [aid2] amix [ao]'``
+ Play audio track 1 and 2 at the same time.
+ - ``--lavfi-complex='[vid1] [vid2] vstack [vo]'``
+ Stack video track 1 and 2 and play them at the same time. Note that
+ both tracks need to have the same width, or filter initialization
+ will fail (you can add ``scale`` filters before the ``vstack`` filter
+ to fix the size).
+ To load a video track from another file, you can use
+ ``--external-file=other.mkv``.
+ - ``--lavfi-complex='[vid1] [vid2] [vid3] hstack=inputs=3 [vo]'``
+ Use the inputs option to stack more than 2 tracks.
+ - ``--lavfi-complex='[aid1] asplit [t1] [ao] ; [t1] showvolume [t2] ; [vid1] [t2] overlay [vo]'``
+ Play audio track 1, and overlay the measured volume for each speaker
+ over video track 1.
+
+ See the FFmpeg libavfilter documentation for details on the available
+ filters.
+
+``--metadata-codepage=<codepage>``
+ Codepage for various input metadata (default: ``auto``). This affects how
+ file tags, chapter titles, etc. are interpreted. In most cases, this merely
+ evaluates to UTF-8 as non-UTF-8 codepages are obscure.
+
+ See ``--sub-codepage`` option on how codepages are specified and further
+ details regarding autodetection and codepage conversion. (The underlying
+ code is the same.)
+
+ Conversion is not applied to metadata that is updated at runtime.
diff --git a/DOCS/man/osc.rst b/DOCS/man/osc.rst
new file mode 100644
index 0000000..c791d75
--- /dev/null
+++ b/DOCS/man/osc.rst
@@ -0,0 +1,456 @@
+ON SCREEN CONTROLLER
+====================
+
+The On Screen Controller (short: OSC) is a minimal GUI integrated with mpv to
+offer basic mouse-controllability. It is intended to make interaction easier
+for new users and to enable precise and direct seeking.
+
+The OSC is enabled by default if mpv was compiled with Lua support. It can be
+disabled entirely using the ``--osc=no`` option.
+
+Using the OSC
+-------------
+
+By default, the OSC will show up whenever the mouse is moved inside the
+player window and will hide if the mouse is not moved outside the OSC for
+0.5 seconds or if the mouse leaves the window.
+
+The Interface
+~~~~~~~~~~~~~
+
+::
+
+ +---------+----------+------------------------------------------+----------+
+ | pl prev | pl next | title | cache |
+ +------+--+---+------+---------+-----------+------+-------+-----+-----+----+
+ | play | skip | skip | time | seekbar | time | audio | sub | vol | fs |
+ | | back | frwd | elapsed | | left | | | | |
+ +------+------+------+---------+-----------+------+-------+-----+-----+----+
+
+
+pl prev
+ ============= ================================================
+ left-click play previous file in playlist
+ right-click show playlist
+ shift+L-click show playlist
+ ============= ================================================
+
+pl next
+ ============= ================================================
+ left-click play next file in playlist
+ right-click show playlist
+ shift+L-click show playlist
+ ============= ================================================
+
+title
+ | Displays current media-title, filename, custom title, or target chapter
+ name while hovering the seekbar.
+
+ ============= ================================================
+ left-click show playlist position and length and full title
+ right-click show filename
+ ============= ================================================
+
+cache
+ | Shows current cache fill status
+
+play
+ ============= ================================================
+ left-click toggle play/pause
+ ============= ================================================
+
+skip back
+ ============= ================================================
+ left-click go to beginning of chapter / previous chapter
+ right-click show chapters
+ shift+L-click show chapters
+ ============= ================================================
+
+skip frwd
+ ============= ================================================
+ left-click go to next chapter
+ right-click show chapters
+ shift+L-click show chapters
+ ============= ================================================
+
+time elapsed
+ | Shows current playback position timestamp
+
+ ============= ================================================
+ left-click toggle displaying timecodes with milliseconds
+ ============= ================================================
+
+seekbar
+ | Indicates current playback position and position of chapters
+
+ ============= ================================================
+ left-click seek to position
+ mouse wheel seek forward/backward
+ ============= ================================================
+
+time left
+ | Shows remaining playback time timestamp
+
+ ============= ================================================
+ left-click toggle between total and remaining time
+ ============= ================================================
+
+audio and sub
+ | Displays selected track and amount of available tracks
+
+ ============= ================================================
+ left-click cycle audio/sub tracks forward
+ right-click cycle audio/sub tracks backwards
+ shift+L-click show available audio/sub tracks
+ mouse wheel cycle audio/sub tracks forward/backwards
+ ============= ================================================
+
+vol
+ ============= ================================================
+ left-click toggle mute
+ mouse wheel volume up/down
+ ============= ================================================
+
+fs
+ ============= ================================================
+ left-click toggle fullscreen
+ ============= ================================================
+
+Key Bindings
+~~~~~~~~~~~~
+
+These key bindings are active by default if nothing else is already bound to
+these keys. In case of collision, the function needs to be bound to a
+different key. See the `Script Commands`_ section.
+
+============= ================================================
+del Cycles visibility between never / auto (mouse-move) / always
+============= ================================================
+
+Configuration
+-------------
+
+The OSC offers limited configuration through a config file
+``script-opts/osc.conf`` placed in mpv's user dir and through the
+``--script-opts`` command-line option. Options provided through the command-line
+will override those from the config file.
+
+Config Syntax
+~~~~~~~~~~~~~
+
+The config file must exactly follow the following syntax::
+
+ # this is a comment
+ optionA=value1
+ optionB=value2
+
+``#`` can only be used at the beginning of a line and there may be no
+spaces around the ``=`` or anywhere else.
+
+Command-line Syntax
+~~~~~~~~~~~~~~~~~~~
+
+To avoid collisions with other scripts, all options need to be prefixed with
+``osc-``.
+
+Example::
+
+ --script-opts=osc-optionA=value1,osc-optionB=value2
+
+
+Configurable Options
+~~~~~~~~~~~~~~~~~~~~
+
+``layout``
+ Default: bottombar
+
+ The layout for the OSC. Currently available are: box, slimbox,
+ bottombar and topbar. Default pre-0.21.0 was 'box'.
+
+``seekbarstyle``
+ Default: bar
+
+ Sets the style of the playback position marker and overall shape
+ of the seekbar: ``bar``, ``diamond`` or ``knob``.
+
+``seekbarhandlesize``
+ Default: 0.6
+
+ Size ratio of the seek handle if ``seekbarstyle`` is set to ``diamond``
+ or ``knob``. This is relative to the full height of the seekbar.
+
+``seekbarkeyframes``
+ Default: yes
+
+ Controls the mode used to seek when dragging the seekbar. If set to ``yes``,
+ default seeking mode is used (usually keyframes, but player defaults and
+ heuristics can change it to exact). If set to ``no``, exact seeking on
+ mouse drags will be used instead. Keyframes are preferred, but exact seeks
+ may be useful in cases where keyframes cannot be found. Note that using
+ exact seeks can potentially make mouse dragging much slower.
+
+``seekrangestyle``
+ Default: inverted
+
+ Display seekable ranges on the seekbar. ``bar`` shows them on the full
+ height of the bar, ``line`` as a thick line and ``inverted`` as a thin
+ line that is inverted over playback position markers. ``none`` will hide
+ them. Additionally, ``slider`` will show a permanent handle inside the seekbar
+ with cached ranges marked inside. Note that these will look differently
+ based on the seekbarstyle option. Also, ``slider`` does not work with
+ ``seekbarstyle`` set to ``bar``.
+
+``seekrangeseparate``
+ Default: yes
+
+ Controls whether to show line-style seekable ranges on top of the
+ seekbar or separately if ``seekbarstyle`` is set to ``bar``.
+
+``seekrangealpha``
+ Default: 200
+
+ Alpha of the seekable ranges, 0 (opaque) to 255 (fully transparent).
+
+``deadzonesize``
+ Default: 0.5
+
+ Size of the deadzone. The deadzone is an area that makes the mouse act
+ like leaving the window. Movement there won't make the OSC show up and
+ it will hide immediately if the mouse enters it. The deadzone starts
+ at the window border opposite to the OSC and the size controls how much
+ of the window it will span. Values between 0.0 and 1.0, where 0 means the
+ OSC will always popup with mouse movement in the window, and 1 means the
+ OSC will only show up when the mouse hovers it. Default pre-0.21.0 was 0.
+
+``minmousemove``
+ Default: 0
+
+ Minimum amount of pixels the mouse has to move between ticks to make
+ the OSC show up. Default pre-0.21.0 was 3.
+
+``showwindowed``
+ Default: yes
+
+ Enable the OSC when windowed
+
+``showfullscreen``
+ Default: yes
+
+ Enable the OSC when fullscreen
+
+``idlescreen``
+ Default: yes
+
+ Show the mpv logo and message when idle
+
+``scalewindowed``
+ Default: 1.0
+
+ Scale factor of the OSC when windowed.
+
+``scalefullscreen``
+ Default: 1.0
+
+ Scale factor of the OSC when fullscreen
+
+``scaleforcedwindow``
+ Default: 2.0
+
+ Scale factor of the OSC when rendered on a forced (dummy) window
+
+``vidscale``
+ Default: yes
+
+ Scale the OSC with the video
+ ``no`` tries to keep the OSC size constant as much as the window size allows
+
+``valign``
+ Default: 0.8
+
+ Vertical alignment, -1 (top) to 1 (bottom)
+
+``halign``
+ Default: 0.0
+
+ Horizontal alignment, -1 (left) to 1 (right)
+
+``barmargin``
+ Default: 0
+
+ Margin from bottom (bottombar) or top (topbar), in pixels
+
+``boxalpha``
+ Default: 80
+
+ Alpha of the background box, 0 (opaque) to 255 (fully transparent)
+
+``hidetimeout``
+ Default: 500
+
+ Duration in ms until the OSC hides if no mouse movement, must not be
+ negative
+
+``fadeduration``
+ Default: 200
+
+ Duration of fade out in ms, 0 = no fade
+
+``title``
+ Default: ${media-title}
+
+ String that supports property expansion that will be displayed as
+ OSC title.
+ ASS tags are escaped, and newlines and trailing slashes are stripped.
+
+``tooltipborder``
+ Default: 1
+
+ Size of the tooltip outline when using bottombar or topbar layouts
+
+``timetotal``
+ Default: no
+
+ Show total time instead of time remaining
+
+``remaining_playtime``
+ Default: yes
+
+ Whether the time-remaining display takes speed into account.
+ ``yes`` - how much playback time remains at the current speed.
+ ``no`` - how much video-time remains.
+
+``timems``
+ Default: no
+
+ Display timecodes with milliseconds
+
+``tcspace``
+ Default: 100 (allowed: 50-200)
+
+ Adjust space reserved for timecodes (current time and time remaining) in
+ the ``bottombar`` and ``topbar`` layouts. The timecode width depends on the
+ font, and with some fonts the spacing near the timecodes becomes too small.
+ Use values above 100 to increase that spacing, or below 100 to decrease it.
+
+``visibility``
+ Default: auto (auto hide/show on mouse move)
+
+ Also supports ``never`` and ``always``
+
+``boxmaxchars``
+ Default: 80
+
+ Max chars for the osc title at the box layout. mpv does not measure the
+ text width on screen and so it needs to limit it by number of chars. The
+ default is conservative to allow wide fonts to be used without overflow.
+ However, with many common fonts a bigger number can be used. YMMV.
+
+``boxvideo``
+ Default: no
+
+ Whether to overlay the osc over the video (``no``), or to box the video
+ within the areas not covered by the osc (``yes``). If this option is set,
+ the osc may overwrite the ``--video-margin-ratio-*`` options, even if the
+ user has set them. (It will not overwrite them if all of them are set to
+ default values.) Additionally, ``visibility`` must be set to ``always``.
+ Otherwise, this option does nothing.
+
+ Currently, this is supported for the ``bottombar`` and ``topbar`` layout
+ only. The other layouts do not change if this option is set. Separately,
+ if window controls are present (see below), they will be affected
+ regardless of which osc layout is in use.
+
+ The border is static and appears even if the OSC is configured to appear
+ only on mouse interaction. If the OSC is invisible, the border is simply
+ filled with the background color (black by default).
+
+ This currently still makes the OSC overlap with subtitles (if the
+ ``--sub-use-margins`` option is set to ``yes``, the default). This may be
+ fixed later.
+
+ This does not work correctly with video outputs like ``--vo=xv``, which
+ render OSD into the unscaled video.
+
+``windowcontrols``
+ Default: auto (Show window controls if there is no window border)
+
+ Whether to show window management controls over the video, and if so,
+ which side of the window to place them. This may be desirable when the
+ window has no decorations, either because they have been explicitly
+ disabled (``border=no``) or because the current platform doesn't support
+ them (eg: gnome-shell with wayland).
+
+ The set of window controls is fixed, offering ``minimize``, ``maximize``,
+ and ``quit``. Not all platforms implement ``minimize`` and ``maximize``,
+ but ``quit`` will always work.
+
+``windowcontrols_alignment``
+ Default: right
+
+ If window controls are shown, indicates which side should they be aligned
+ to.
+
+ Supports ``left`` and ``right`` which will place the controls on those
+ respective sides.
+
+``greenandgrumpy``
+ Default: no
+
+ Set to ``yes`` to reduce festivity (i.e. disable santa hat in December.)
+
+``livemarkers``
+ Default: yes
+
+ Update chapter markers positions on duration changes, e.g. live streams.
+ The updates are unoptimized - consider disabling it on very low-end systems.
+
+``chapters_osd``, ``playlist_osd``
+ Default: yes
+
+ Whether to display the chapters/playlist at the OSD when left-clicking the
+ next/previous OSC buttons, respectively.
+
+``chapter_fmt``
+ Default: ``Chapter: %s``
+
+ Template for the chapter-name display when hovering the seekbar.
+ Use ``no`` to disable chapter display on hover. Otherwise it's a lua
+ ``string.format`` template and ``%s`` is replaced with the actual name.
+
+``unicodeminus``
+ Default: no
+
+ Use a Unicode minus sign instead of an ASCII hyphen when displaying
+ the remaining playback time.
+
+
+Script Commands
+~~~~~~~~~~~~~~~
+
+The OSC script listens to certain script commands. These commands can bound
+in ``input.conf``, or sent by other scripts.
+
+``osc-message``
+ Show a message on screen using the OSC. First argument is the message,
+ second the duration in seconds.
+
+``osc-visibility``
+ Controls visibility mode ``never`` / ``auto`` (on mouse move) / ``always``
+ and also ``cycle`` to cycle between the modes
+
+Example
+
+You could put this into ``input.conf`` to hide the OSC with the ``a`` key and
+to set auto mode (the default) with ``b``::
+
+ a script-message osc-visibility never
+ b script-message osc-visibility auto
+
+``osc-idlescreen``
+ Controls the visibility of the mpv logo on idle. Valid arguments are ``yes``,
+ ``no``, and ``cycle`` to toggle between yes and no.
+
+``osc-playlist``, ``osc-chapterlist``, ``osc-tracklist``
+ Shows a limited view of the respective type of list using the OSC. First
+ argument is duration in seconds.
+
diff --git a/DOCS/man/stats.rst b/DOCS/man/stats.rst
new file mode 100644
index 0000000..88238bc
--- /dev/null
+++ b/DOCS/man/stats.rst
@@ -0,0 +1,233 @@
+STATS
+=====
+
+This builtin script displays information and statistics for the currently
+played file. It is enabled by default if mpv was compiled with Lua support.
+It can be disabled entirely using the ``--load-stats-overlay=no`` option.
+
+Usage
+-----
+
+The following key bindings are active by default unless something else is
+already bound to them:
+
+==== ==============================================
+i Show stats for a fixed duration
+I Toggle stats (shown until toggled again)
+==== ==============================================
+
+While the stats are visible on screen the following key bindings are active,
+regardless of existing bindings. They allow you to switch between *pages* of
+stats:
+
+==== ==================
+1 Show usual stats
+2 Show frame timings (scroll)
+3 Input cache stats
+4 Active key bindings (scroll)
+0 Internal stuff (scroll)
+==== ==================
+
+On pages which support scroll, these key bindings are also active:
+
+==== ==================
+UP Scroll one line up
+DOWN Scroll one line down
+==== ==================
+
+Configuration
+-------------
+
+This script can be customized through a config file ``script-opts/stats.conf``
+placed in mpv's user directory and through the ``--script-opts`` command-line
+option. The configuration syntax is described in `ON SCREEN CONTROLLER`_.
+
+Configurable Options
+~~~~~~~~~~~~~~~~~~~~
+
+``key_page_1``
+ Default: 1
+``key_page_2``
+ Default: 2
+``key_page_3``
+ Default: 3
+``key_page_4``
+ Default: 4
+``key_page_0``
+ Default: 0
+
+ Key bindings for page switching while stats are displayed.
+
+``key_scroll_up``
+ Default: UP
+``key_scroll_down``
+ Default: DOWN
+``scroll_lines``
+ Default: 1
+
+ Scroll key bindings and number of lines to scroll on pages which support it.
+
+``duration``
+ Default: 4
+
+ How long the stats are shown in seconds (oneshot).
+
+``redraw_delay``
+ Default: 1
+
+ How long it takes to refresh the displayed stats in seconds (toggling).
+
+``persistent_overlay``
+ Default: no
+
+ When `no`, other scripts printing text to the screen can overwrite the
+ displayed stats. When `yes`, displayed stats are persistently shown for the
+ respective duration. This can result in overlapping text when multiple
+ scripts decide to print text at the same time.
+
+``plot_perfdata``
+ Default: yes
+
+ Show graphs for performance data (page 2).
+
+``plot_vsync_ratio``
+ Default: yes
+``plot_vsync_jitter``
+ Default: yes
+
+ Show graphs for vsync and jitter values (page 1). Only when toggled.
+
+``plot_tonemapping_lut``
+ Default: no
+
+ Enable tone-mapping LUT visualization automatically. Only when toggled.
+
+``flush_graph_data``
+ Default: yes
+
+ Clear data buffers used for drawing graphs when toggling.
+
+``font``
+ Default: sans-serif
+
+ Font name. Should support as many font weights as possible for optimal
+ visual experience.
+
+``font_mono``
+ Default: monospace
+
+ Font name for parts where monospaced characters are necessary to align
+ text. Currently, monospaced digits are sufficient.
+
+``font_size``
+ Default: 8
+
+ Font size used to render text.
+
+``font_color``
+ Default: FFFFFF
+
+ Font color.
+
+``border_size``
+ Default: 0.8
+
+ Size of border drawn around the font.
+
+``border_color``
+ Default: 262626
+
+ Color of drawn border.
+
+``alpha``
+ Default: 11
+
+ Transparency for drawn text.
+
+``plot_bg_border_color``
+ Default: 0000FF
+
+ Border color used for drawing graphs.
+
+``plot_bg_color``
+ Default: 262626
+
+ Background color used for drawing graphs.
+
+``plot_color``
+ Default: FFFFFF
+
+ Color used for drawing graphs.
+
+Note: colors are given as hexadecimal values and use ASS tag order: BBGGRR
+(blue green red).
+
+Different key bindings
+~~~~~~~~~~~~~~~~~~~~~~
+
+Additional keys can be configured in ``input.conf`` to display the stats::
+
+ e script-binding stats/display-stats
+ E script-binding stats/display-stats-toggle
+
+And to display a certain page directly::
+
+ i script-binding stats/display-page-1
+ e script-binding stats/display-page-2
+
+Active key bindings page
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Lists the active key bindings and the commands they're bound to, excluding the
+interactive keys of the stats script itself. See also ``--input-test`` for more
+detailed view of each binding.
+
+The keys are grouped automatically using a simple analysis of the command
+string, and one should not expect documentation-level grouping accuracy,
+however, it should still be reasonably useful.
+
+Using ``--idle --script-opts=stats-bindlist=yes`` will print the list to the
+terminal and quit immediately. By default long lines are shortened to 79 chars,
+and terminal escape sequences are enabled. A different length limit can be
+set by changing ``yes`` to a number (at least 40), and escape sequences can be
+disabled by adding ``-`` before the value, e.g. ``...=-yes`` or ``...=-120``.
+
+Like with ``--input-test``, the list includes bindings from ``input.conf`` and
+from user scripts. Use ``--no-config`` to list only built-in bindings.
+
+Internal stuff page
+~~~~~~~~~~~~~~~~~~~
+
+Most entries shown on this page have rather vague meaning. Likely none of this
+is useful for you. Don't attempt to use it. Forget its existence.
+
+Selecting this for the first time will start collecting some internal
+performance data. That means performance will be slightly lower than normal for
+the rest of the time the player is running (even if the stats page is closed).
+Note that the stats page itself uses a lot of CPU and even GPU resources, and
+may have a heavy impact on performance.
+
+The displayed information is accumulated over the redraw delay (shown as
+``poll-time`` field).
+
+This adds entries for each Lua script. If there are too many scripts running,
+parts of the list will simply be out of the screen, but it can be scrolled.
+
+If the underlying platform does not support pthread per thread times, the
+displayed times will be 0 or something random (I suspect that at time of this
+writing, only Linux provides the correct via pthread APIs for per thread times).
+
+Most entries are added lazily and only during data collection, which is why
+entries may pop up randomly after some time. It's also why the memory usage
+entries for scripts that have been inactive since the start of data collection
+are missing.
+
+Memory usage is approximate and does not reflect internal fragmentation.
+
+JS scripts memory reporting is disabled by default because collecting the data
+at the JS side has an overhead and will increase memory usage. It can be
+enabled by setting the ``--js-memory-report`` option before starting mpv.
+
+If entries have ``/time`` and ``/cpu`` variants, the former gives the real time
+(monotonic clock), while the latter the thread CPU time (only if the
+corresponding pthread API works and is supported).
diff --git a/DOCS/man/vf.rst b/DOCS/man/vf.rst
new file mode 100644
index 0000000..1423e4c
--- /dev/null
+++ b/DOCS/man/vf.rst
@@ -0,0 +1,794 @@
+VIDEO FILTERS
+=============
+
+Video filters allow you to modify the video stream and its properties. All of
+the information described in this section applies to audio filters as well
+(generally using the prefix ``--af`` instead of ``--vf``).
+
+The exact syntax is:
+
+``--vf=<filter1[=parameter1:parameter2:...],filter2,...>``
+ Setup a chain of video filters. This consists on the filter name, and an
+ option list of parameters after ``=``. The parameters are separated by
+ ``:`` (not ``,``, as that starts a new filter entry).
+
+ Before the filter name, a label can be specified with ``@name:``, where
+ name is an arbitrary user-given name, which identifies the filter. This
+ is only needed if you want to toggle the filter at runtime.
+
+ A ``!`` before the filter name means the filter is disabled by default. It
+ will be skipped on filter creation. This is also useful for runtime filter
+ toggling.
+
+ See the ``vf`` command (and ``toggle`` sub-command) for further explanations
+ and examples.
+
+ The general filter entry syntax is:
+
+ ``["@"<label-name>":"] ["!"] <filter-name> [ "=" <filter-parameter-list> ]``
+
+ or for the special "toggle" syntax (see ``vf`` command):
+
+ ``"@"<label-name>``
+
+ and the ``filter-parameter-list``:
+
+ ``<filter-parameter> | <filter-parameter> "," <filter-parameter-list>``
+
+ and ``filter-parameter``:
+
+ ``( <param-name> "=" <param-value> ) | <param-value>``
+
+ ``param-value`` can further be quoted in ``[`` / ``]`` in case the value
+ contains characters like ``,`` or ``=``. This is used in particular with
+ the ``lavfi`` filter, which uses a very similar syntax as mpv (MPlayer
+ historically) to specify filters and their parameters.
+
+.. note::
+
+ ``--vf`` can only take a single track as input, even if the filter supports
+ dynamic input. Filters that require multiple inputs can't be used.
+ Use ``--lavfi-complex`` for such a use case. This also applies for ``--af``.
+
+Filters can be manipulated at run time. You can use ``@`` labels as described
+above in combination with the ``vf`` command (see `COMMAND INTERFACE`_) to get
+more control over this. Initially disabled filters with ``!`` are useful for
+this as well.
+
+.. note::
+
+ To get a full list of available video filters, see ``--vf=help`` and
+ https://ffmpeg.org/ffmpeg-filters.html .
+
+ Also, keep in mind that most actual filters are available via the ``lavfi``
+ wrapper, which gives you access to most of libavfilter's filters. This
+ includes all filters that have been ported from MPlayer to libavfilter.
+
+ Most builtin filters are deprecated in some ways, unless they're only available
+ in mpv (such as filters which deal with mpv specifics, or which are
+ implemented in mpv only).
+
+ If a filter is not builtin, the ``lavfi-bridge`` will be automatically
+ tried. This bridge does not support help output, and does not verify
+ parameters before the filter is actually used. Although the mpv syntax
+ is rather similar to libavfilter's, it's not the same. (Which means not
+ everything accepted by vf_lavfi's ``graph`` option will be accepted by
+ ``--vf``.)
+
+ You can also prefix the filter name with ``lavfi-`` to force the wrapper.
+ This is helpful if the filter name collides with a deprecated mpv builtin
+ filter. For example ``--vf=lavfi-scale=args`` would use libavfilter's
+ ``scale`` filter over mpv's deprecated builtin one.
+
+Video filters are managed in lists. There are a few commands to manage the
+filter list.
+
+``--vf-append=filter``
+ Appends the filter given as arguments to the filter list.
+
+``--vf-add=filter``
+ Appends the filter given as arguments to the filter list. (Passing multiple
+ filters is currently still possible, but deprecated.)
+
+``--vf-pre=filter``
+ Prepends the filters given as arguments to the filter list. (Passing
+ multiple filters is currently still possible, but deprecated.)
+
+``--vf-remove=filter``
+ Deletes the filter from the list. The filter can be either given the way it
+ was added (filter name and its full argument list), or by label (prefixed
+ with ``@``). Matching of filters works as follows: if either of the compared
+ filters has a label set, only the labels are compared. If none of the
+ filters have a label, the filter name, arguments, and argument order are
+ compared. (Passing multiple filters is currently still possible, but
+ deprecated.)
+
+``-vf-toggle=filter``
+ Add the given filter to the list if it was not present yet, or remove it
+ from the list if it was present. Matching of filters works as described in
+ ``--vf-remove``.
+
+``--vf-clr``
+ Completely empties the filter list.
+
+With filters that support it, you can access parameters by their name.
+
+``--vf=<filter>=help``
+ Prints the parameter names and parameter value ranges for a particular
+ filter.
+
+Available mpv-only filters are:
+
+``format=fmt=<value>:colormatrix=<value>:...``
+ Applies video parameter overrides, with optional conversion. By default,
+ this overrides the video's parameters without conversion (except for the
+ ``fmt`` parameter), but can be made to perform an appropriate conversion
+ with ``convert=yes`` for parameters for which conversion is supported.
+
+ ``<fmt>``
+ Image format name, e.g. rgb15, bgr24, 420p, etc. (default: don't change).
+
+ This filter always performs conversion to the given format.
+
+ .. note::
+
+ For a list of available formats, use ``--vf=format=fmt=help``.
+
+ .. note::
+
+ Conversion between hardware formats is supported in some cases.
+ eg: ``cuda`` to ``vulkan``, or ``vaapi`` to ``vulkan``.
+
+ ``<convert=yes|no>``
+ Force conversion of color parameters (default: no).
+
+ If this is disabled (the default), the only conversion that is possibly
+ performed is format conversion if ``<fmt>`` is set. All other parameters
+ (like ``<colormatrix>``) are forced without conversion. This mode is
+ typically useful when files have been incorrectly tagged.
+
+ If this is enabled, libswscale or zimg is used if any of the parameters
+ mismatch. zimg is used of the input/output image formats are supported
+ by mpv's zimg wrapper, and if ``--sws-allow-zimg=yes`` is used. Both
+ libraries may not support all kinds of conversions. This typically
+ results in silent incorrect conversion. zimg has in many cases a better
+ chance of performing the conversion correctly.
+
+ In both cases, the color parameters are set on the output stage of the
+ image format conversion (if ``fmt`` was set). The difference is that
+ with ``convert=no``, the color parameters are not passed on to the
+ converter.
+
+ If input and output video parameters are the same, conversion is always
+ skipped.
+
+ When converting between hardware formats, this parameter has no effect,
+ and the only conversion that is done is the format conversion.
+
+ .. admonition:: Examples
+
+ ``mpv test.mkv --vf=format:colormatrix=ycgco``
+ Results in incorrect colors (if test.mkv was tagged correctly).
+
+ ``mpv test.mkv --vf=format:colormatrix=ycgco:convert=yes --sws-allow-zimg``
+ Results in true conversion to ``ycgco``, assuming the renderer
+ supports it (``--vo=gpu`` normally does). You can add ``--vo=xv``
+ to force a VO which definitely does not support it, which should
+ show incorrect colors as confirmation.
+
+ Using ``--sws-allow-zimg=no`` (or disabling zimg at build time)
+ will use libswscale, which cannot perform this conversion as
+ of this writing.
+
+ ``<colormatrix>``
+ Controls the YUV to RGB color space conversion when playing video. There
+ are various standards. Normally, BT.601 should be used for SD video, and
+ BT.709 for HD video. (This is done by default.) Using incorrect color space
+ results in slightly under or over saturated and shifted colors.
+
+ These options are not always supported. Different video outputs provide
+ varying degrees of support. The ``gpu`` and ``vdpau`` video output
+ drivers usually offer full support. The ``xv`` output can set the color
+ space if the system video driver supports it, but not input and output
+ levels. The ``scale`` video filter can configure color space and input
+ levels, but only if the output format is RGB (if the video output driver
+ supports RGB output, you can force this with ``-vf scale,format=rgba``).
+
+ If this option is set to ``auto`` (which is the default), the video's
+ color space flag will be used. If that flag is unset, the color space
+ will be selected automatically. This is done using a simple heuristic that
+ attempts to distinguish SD and HD video. If the video is larger than
+ 1279x576 pixels, BT.709 (HD) will be used; otherwise BT.601 (SD) is
+ selected.
+
+ Available color spaces are:
+
+ :auto: automatic selection (default)
+ :bt.601: ITU-R BT.601 (SD)
+ :bt.709: ITU-R BT.709 (HD)
+ :bt.2020-ncl: ITU-R BT.2020 non-constant luminance system
+ :bt.2020-cl: ITU-R BT.2020 constant luminance system
+ :smpte-240m: SMPTE-240M
+
+ ``<colorlevels>``
+ YUV color levels used with YUV to RGB conversion. This option is only
+ necessary when playing broken files which do not follow standard color
+ levels or which are flagged wrong. If the video does not specify its
+ color range, it is assumed to be limited range.
+
+ The same limitations as with ``<colormatrix>`` apply.
+
+ Available color ranges are:
+
+ :auto: automatic selection (normally limited range) (default)
+ :limited: limited range (16-235 for luma, 16-240 for chroma)
+ :full: full range (0-255 for both luma and chroma)
+
+ ``<primaries>``
+ RGB primaries the source file was encoded with. Normally this should be set
+ in the file header, but when playing broken or mistagged files this can be
+ used to override the setting.
+
+ This option only affects video output drivers that perform color
+ management, for example ``gpu`` with the ``target-prim`` or
+ ``icc-profile`` suboptions set.
+
+ If this option is set to ``auto`` (which is the default), the video's
+ primaries flag will be used. If that flag is unset, the color space will
+ be selected automatically, using the following heuristics: If the
+ ``<colormatrix>`` is set or determined as BT.2020 or BT.709, the
+ corresponding primaries are used. Otherwise, if the video height is
+ exactly 576 (PAL), BT.601-625 is used. If it's exactly 480 or 486 (NTSC),
+ BT.601-525 is used. If the video resolution is anything else, BT.709 is
+ used.
+
+ Available primaries are:
+
+ :auto: automatic selection (default)
+ :bt.601-525: ITU-R BT.601 (SD) 525-line systems (NTSC, SMPTE-C)
+ :bt.601-625: ITU-R BT.601 (SD) 625-line systems (PAL, SECAM)
+ :bt.709: ITU-R BT.709 (HD) (same primaries as sRGB)
+ :bt.2020: ITU-R BT.2020 (UHD)
+ :apple: Apple RGB
+ :adobe: Adobe RGB (1998)
+ :prophoto: ProPhoto RGB (ROMM)
+ :cie1931: CIE 1931 RGB
+ :dci-p3: DCI-P3 (Digital Cinema)
+ :v-gamut: Panasonic V-Gamut primaries
+
+ ``<gamma>``
+ Gamma function the source file was encoded with. Normally this should be set
+ in the file header, but when playing broken or mistagged files this can be
+ used to override the setting.
+
+ This option only affects video output drivers that perform color management.
+
+ If this option is set to ``auto`` (which is the default), the gamma will
+ be set to BT.1886 for YCbCr content, sRGB for RGB content and Linear for
+ XYZ content.
+
+ Available gamma functions are:
+
+ :auto: automatic selection (default)
+ :bt.1886: ITU-R BT.1886 (EOTF corresponding to BT.601/BT.709/BT.2020)
+ :srgb: IEC 61966-2-4 (sRGB)
+ :linear: Linear light
+ :gamma1.8: Pure power curve (gamma 1.8)
+ :gamma2.0: Pure power curve (gamma 2.0)
+ :gamma2.2: Pure power curve (gamma 2.2)
+ :gamma2.4: Pure power curve (gamma 2.4)
+ :gamma2.6: Pure power curve (gamma 2.6)
+ :gamma2.8: Pure power curve (gamma 2.8)
+ :prophoto: ProPhoto RGB (ROMM) curve
+ :pq: ITU-R BT.2100 PQ (Perceptual quantizer) curve
+ :hlg: ITU-R BT.2100 HLG (Hybrid Log-gamma) curve
+ :v-log: Panasonic V-Log transfer curve
+ :s-log1: Sony S-Log1 transfer curve
+ :s-log2: Sony S-Log2 transfer curve
+
+ ``<sig-peak>``
+ Reference peak illumination for the video file, relative to the
+ signal's reference white level. This is mostly interesting for HDR, but
+ it can also be used tone map SDR content to simulate a different
+ exposure. Normally inferred from tags such as MaxCLL or mastering
+ metadata.
+
+ The default of 0.0 will default to the source's nominal peak luminance.
+
+ ``<light>``
+ Light type of the scene. This is mostly correctly inferred based on the
+ gamma function, but it can be useful to override this when viewing raw
+ camera footage (e.g. V-Log), which is normally scene-referred instead
+ of display-referred.
+
+ Available light types are:
+
+ :auto: Automatic selection (default)
+ :display: Display-referred light (most content)
+ :hlg: Scene-referred using the HLG OOTF (e.g. HLG content)
+ :709-1886: Scene-referred using the BT709+BT1886 interaction
+ :gamma1.2: Scene-referred using a pure power OOTF (gamma=1.2)
+
+ ``<dolbyvision=yes|no>``
+ Whether or not to include Dolby Vision metadata (default: yes). If
+ disabled, any Dolby Vision metadata will be stripped from frames.
+
+ ``<film-grain=yes|no>``
+ Whether or not to include film grain metadata (default: yes). If
+ disabled, any film grain metadata will be stripped from frames.
+
+ ``<stereo-in>``
+ Set the stereo mode the video is assumed to be encoded in. Use
+ ``--vf=format:stereo-in=help`` to list all available modes. Check with
+ the ``stereo3d`` filter documentation to see what the names mean.
+
+ ``<stereo-out>``
+ Set the stereo mode the video should be displayed as. Takes the
+ same values as the ``stereo-in`` option.
+
+ ``<rotate>``
+ Set the rotation the video is assumed to be encoded with in degrees.
+ The special value ``-1`` uses the input format.
+
+ ``<w>``, ``<h>``
+ If not 0, perform conversion to the given size. Ignored if
+ ``convert=yes`` is not set.
+
+ ``<dw>``, ``<dh>``
+ Set the display size. Note that setting the display size such that
+ the video is scaled in both directions instead of just changing the
+ aspect ratio is an implementation detail, and might change later.
+
+ ``<dar>``
+ Set the display aspect ratio of the video frame. This is a float,
+ but values such as ``[16:9]`` can be passed too (``[...]`` for quoting
+ to prevent the option parser from interpreting the ``:`` character).
+
+ ``<force-scaler=auto|zimg|sws>``
+ Force a specific scaler backend, if applicable. This is a debug option
+ and could go away any time.
+
+ ``<alpha=auto|straight|premul>``
+ Set the kind of alpha the video uses. Undefined effect if the image
+ format has no alpha channel (could be ignored or cause an error,
+ depending on how mpv internals evolve). Setting this may or may not
+ cause downstream image processing to treat alpha differently, depending
+ on support. With ``convert`` and zimg used, this will convert the alpha.
+ libswscale and other FFmpeg components completely ignore this.
+
+``lavfi=graph[:sws-flags[:o=opts]]``
+ Filter video using FFmpeg's libavfilter.
+
+ ``<graph>``
+ The libavfilter graph string. The filter must have a single video input
+ pad and a single video output pad.
+
+ See `<https://ffmpeg.org/ffmpeg-filters.html>`_ for syntax and available
+ filters.
+
+ .. warning::
+
+ If you want to use the full filter syntax with this option, you have
+ to quote the filter graph in order to prevent mpv's syntax and the
+ filter graph syntax from clashing. To prevent a quoting and escaping
+ mess, consider using ``--lavfi-complex`` if you know which video
+ track you want to use from the input file. (There is only one video
+ track for nearly all video files anyway.)
+
+ .. admonition:: Examples
+
+ ``--vf=lavfi=[gradfun=20:30,vflip]``
+ ``gradfun`` filter with nonsense parameters, followed by a
+ ``vflip`` filter. (This demonstrates how libavfilter takes a
+ graph and not just a single filter.) The filter graph string is
+ quoted with ``[`` and ``]``. This requires no additional quoting
+ or escaping with some shells (like bash), while others (like
+ zsh) require additional ``"`` quotes around the option string.
+
+ ``'--vf=lavfi="gradfun=20:30,vflip"'``
+ Same as before, but uses quoting that should be safe with all
+ shells. The outer ``'`` quotes make sure that the shell does not
+ remove the ``"`` quotes needed by mpv.
+
+ ``'--vf=lavfi=graph="gradfun=radius=30:strength=20,vflip"'``
+ Same as before, but uses named parameters for everything.
+
+ ``<sws-flags>``
+ If libavfilter inserts filters for pixel format conversion, this
+ option gives the flags which should be passed to libswscale. This
+ option is numeric and takes a bit-wise combination of ``SWS_`` flags.
+
+ See ``https://git.videolan.org/?p=ffmpeg.git;a=blob;f=libswscale/swscale.h``.
+
+ ``<o>``
+ Set AVFilterGraph options. These should be documented by FFmpeg.
+
+ .. admonition:: Example
+
+ ``'--vf=lavfi=yadif:o="threads=2,thread_type=slice"'``
+ forces a specific threading configuration.
+
+``sub=[=bottom-margin:top-margin]``
+ Moves subtitle rendering to an arbitrary point in the filter chain, or force
+ subtitle rendering in the video filter as opposed to using video output OSD
+ support.
+
+ ``<bottom-margin>``
+ Adds a black band at the bottom of the frame. The SSA/ASS renderer can
+ place subtitles there (with ``--sub-use-margins``).
+ ``<top-margin>``
+ Black band on the top for toptitles (with ``--sub-use-margins``).
+
+ .. admonition:: Examples
+
+ ``--vf=sub,eq``
+ Moves sub rendering before the eq filter. This will put both
+ subtitle colors and video under the influence of the video equalizer
+ settings.
+
+``vapoursynth=file:buffered-frames:concurrent-frames``
+ Loads a VapourSynth filter script. This is intended for streamed
+ processing: mpv actually provides a source filter, instead of using a
+ native VapourSynth video source. The mpv source will answer frame
+ requests only within a small window of frames (the size of this window
+ is controlled with the ``buffered-frames`` parameter), and requests outside
+ of that will return errors. As such, you can't use the full power of
+ VapourSynth, but you can use certain filters.
+
+ .. warning::
+
+ Do not use this filter, unless you have expert knowledge in VapourSynth,
+ and know how to fix bugs in the mpv VapourSynth wrapper code.
+
+ If you just want to play video generated by VapourSynth (i.e. using
+ a native VapourSynth video source), it's better to use ``vspipe`` and a
+ pipe or FIFO to feed the video to mpv. The same applies if the filter script
+ requires random frame access (see ``buffered-frames`` parameter).
+
+ ``file``
+ Filename of the script source. Currently, this is always a python
+ script (``.vpy`` in VapourSynth convention).
+
+ The variable ``video_in`` is set to the mpv video source, and it is
+ expected that the script reads video from it. (Otherwise, mpv will
+ decode no video, and the video packet queue will overflow, eventually
+ leading to only audio playing, or worse.)
+
+ The filter graph created by the script is also expected to pass through
+ timestamps using the ``_DurationNum`` and ``_DurationDen`` frame
+ properties.
+
+ See the end of the option list for a full list of script variables
+ defined by mpv.
+
+ .. admonition:: Example:
+
+ ::
+
+ import vapoursynth as vs
+ from vapoursynth import core
+ core.std.AddBorders(video_in, 10, 10, 20, 20).set_output()
+
+ .. warning::
+
+ The script will be reloaded on every seek. This is done to reset
+ the filter properly on discontinuities.
+
+ ``buffered-frames``
+ Maximum number of decoded video frames that should be buffered before
+ the filter (default: 4). This specifies the maximum number of frames
+ the script can request in backward direction.
+
+ E.g. if ``buffered-frames=5``, and the script just requested frame 15,
+ it can still request frame 10, but frame 9 is not available anymore.
+ If it requests frame 30, mpv will decode 15 more frames, and keep only
+ frames 25-30.
+
+ The only reason why this buffer exists is to serve the random access
+ requests the VapourSynth filter can make.
+
+ The VapourSynth API has a ``getFrameAsync`` function, which takes an
+ absolute frame number. Source filters must respond to all requests. For
+ example, a source filter can request frame 2432, and then frame 3.
+ Source filters typically implement this by pre-indexing the entire
+ file.
+
+ mpv on the other hand is stream oriented, and does not allow filters to
+ seek. (And it would not make sense to allow it, because it would ruin
+ performance.) Filters get frames sequentially in playback direction, and
+ cannot request them out of order.
+
+ To compensate for this mismatch, mpv allows the filter to access frames
+ within a certain window. ``buffered-frames`` controls the size of this
+ window. Most VapourSynth filters happen to work with this, because mpv
+ requests frames sequentially increasing from it, and most filters only
+ require frames "close" to the requested frame.
+
+ If the filter requests a frame that has a higher frame number than the
+ highest buffered frame, new frames will be decoded until the requested
+ frame number is reached. Excessive frames will be flushed out in a FIFO
+ manner (there are only at most ``buffered-frames`` in this buffer).
+
+ If the filter requests a frame that has a lower frame number than the
+ lowest buffered frame, the request cannot be satisfied, and an error
+ is returned to the filter. This kind of error is not supposed to happen
+ in a "proper" VapourSynth environment. What exactly happens depends on
+ the filters involved.
+
+ Increasing this buffer will not improve performance. Rather, it will
+ waste memory, and slow down seeks (when enough frames to fill the buffer
+ need to be decoded at once). It is only needed to prevent the error
+ described in the previous paragraph.
+
+ How many frames a filter requires depends on filter implementation
+ details, and mpv has no way of knowing. A scale filter might need only
+ 1 frame, an interpolation filter may require a small number of frames,
+ and the ``Reverse`` filter will require an infinite number of frames.
+
+ If you want reliable operation to the full extend VapourSynth is
+ capable, use ``vspipe``.
+
+ The actual number of buffered frames also depends on the value of the
+ ``concurrent-frames`` option. Currently, both option values are
+ multiplied to get the final buffer size.
+
+ ``concurrent-frames``
+ Number of frames that should be requested in parallel. The
+ level of concurrency depends on the filter and how quickly mpv can
+ decode video to feed the filter. This value should probably be
+ proportional to the number of cores on your machine. Most time,
+ making it higher than the number of cores can actually make it
+ slower.
+
+ Technically, mpv will call the VapourSynth ``getFrameAsync`` function
+ in a loop, until there are ``concurrent-frames`` frames that have not
+ been returned by the filter yet. This also assumes that the rest of the
+ mpv filter chain reads the output of the ``vapoursynth`` filter quickly
+ enough. (For example, if you pause the player, filtering will stop very
+ soon, because the filtered frames are waiting in a queue.)
+
+ Actual concurrency depends on many other factors.
+
+ By default, this uses the special value ``auto``, which sets the option
+ to the number of detected logical CPU cores.
+
+ The following ``.vpy`` script variables are defined by mpv:
+
+ ``video_in``
+ The mpv video source as vapoursynth clip. Note that this has an
+ incorrect (very high) length set, which confuses many filters. This is
+ necessary, because the true number of frames is unknown. You can use the
+ ``Trim`` filter on the clip to reduce the length.
+
+ ``video_in_dw``, ``video_in_dh``
+ Display size of the video. Can be different from video size if the
+ video does not use square pixels (e.g. DVD).
+
+ ``container_fps``
+ FPS value as reported by file headers. This value can be wrong or
+ completely broken (e.g. 0 or NaN). Even if the value is correct,
+ if another filter changes the real FPS (by dropping or inserting
+ frames), the value of this variable will not be useful. Note that
+ the ``--container-fps-override`` command line option overrides this value.
+
+ Useful for some filters which insist on having a FPS.
+
+ ``display_fps``
+ Refresh rate of the current display. Note that this value can be 0.
+
+ ``display_res``
+ Resolution of the current display. This is an integer array with the
+ first entry corresponding to the width and the second entry coresponding
+ to the height. These values can be 0. Note that this will not respond to
+ monitor changes and may not work on all platforms.
+
+``vavpp``
+ VA-API video post processing. Requires the system to support VA-API,
+ i.e. Linux/BSD only. Works with ``--vo=vaapi`` and ``--vo=gpu`` only.
+ Currently deinterlaces. This filter is automatically inserted if
+ deinterlacing is requested (either using the ``d`` key, by default mapped to
+ the command ``cycle deinterlace``, or the ``--deinterlace`` option).
+
+ ``deint=<method>``
+ Select the deinterlacing algorithm.
+
+ no
+ Don't perform deinterlacing.
+ auto
+ Select the best quality deinterlacing algorithm (default). This
+ goes by the order of the options as documented, with
+ ``motion-compensated`` being considered best quality.
+ first-field
+ Show only first field.
+ bob
+ bob deinterlacing.
+ weave, motion-adaptive, motion-compensated
+ Advanced deinterlacing algorithms. Whether these actually work
+ depends on the GPU hardware, the GPU drivers, driver bugs, and
+ mpv bugs.
+
+ ``<interlaced-only>``
+ :no: Deinterlace all frames (default).
+ :yes: Only deinterlace frames marked as interlaced.
+
+ ``reversal-bug=<yes|no>``
+ :no: Use the API as it was interpreted by older Mesa drivers. While
+ this interpretation was more obvious and intuitive, it was
+ apparently wrong, and not shared by Intel driver developers.
+ :yes: Use Intel interpretation of surface forward and backwards
+ references (default). This is what Intel drivers and newer Mesa
+ drivers expect. Matters only for the advanced deinterlacing
+ algorithms.
+
+``vdpaupp``
+ VDPAU video post processing. Works with ``--vo=vdpau`` and ``--vo=gpu``
+ only. This filter is automatically inserted if deinterlacing is requested
+ (either using the ``d`` key, by default mapped to the command
+ ``cycle deinterlace``, or the ``--deinterlace`` option). When enabling
+ deinterlacing, it is always preferred over software deinterlacer filters
+ if the ``vdpau`` VO is used, and also if ``gpu`` is used and hardware
+ decoding was activated at least once (i.e. vdpau was loaded).
+
+ ``sharpen=<-1-1>``
+ For positive values, apply a sharpening algorithm to the video, for
+ negative values a blurring algorithm (default: 0).
+ ``denoise=<0-1>``
+ Apply a noise reduction algorithm to the video (default: 0; no noise
+ reduction).
+ ``deint=<yes|no>``
+ Whether deinterlacing is enabled (default: no). If enabled, it will use
+ the mode selected with ``deint-mode``.
+ ``deint-mode=<first-field|bob|temporal|temporal-spatial>``
+ Select deinterlacing mode (default: temporal).
+
+ Note that there's currently a mechanism that allows the ``vdpau`` VO to
+ change the ``deint-mode`` of auto-inserted ``vdpaupp`` filters. To avoid
+ confusion, it's recommended not to use the ``--vo=vdpau`` suboptions
+ related to filtering.
+
+ first-field
+ Show only first field.
+ bob
+ Bob deinterlacing.
+ temporal
+ Motion-adaptive temporal deinterlacing. May lead to A/V desync
+ with slow video hardware and/or high resolution.
+ temporal-spatial
+ Motion-adaptive temporal deinterlacing with edge-guided spatial
+ interpolation. Needs fast video hardware.
+ ``chroma-deint``
+ Makes temporal deinterlacers operate both on luma and chroma (default).
+ Use no-chroma-deint to solely use luma and speed up advanced
+ deinterlacing. Useful with slow video memory.
+ ``pullup``
+ Try to apply inverse telecine, needs motion adaptive temporal
+ deinterlacing.
+ ``interlaced-only=<yes|no>``
+ If ``yes``, only deinterlace frames marked as interlaced (default: no).
+ ``hqscaling=<0-9>``
+ 0
+ Use default VDPAU scaling (default).
+ 1-9
+ Apply high quality VDPAU scaling (needs capable hardware).
+
+``d3d11vpp``
+ Direct3D 11 video post processing. Currently requires D3D11 hardware
+ decoding for use.
+
+ ``deint=<yes|no>``
+ Whether deinterlacing is enabled (default: no).
+ ``interlaced-only=<yes|no>``
+ If ``yes``, only deinterlace frames marked as interlaced (default: no).
+ ``mode=<blend|bob|adaptive|mocomp|ivctc|none>``
+ Tries to select a video processor with the given processing capability.
+ If a video processor supports multiple capabilities, it is not clear
+ which algorithm is actually selected. ``none`` always falls back. On
+ most if not all hardware, this option will probably do nothing, because
+ a video processor usually supports all modes or none.
+
+``fingerprint=...``
+ Compute video frame fingerprints and provide them as metadata. Actually, it
+ currently barely deserved to be called ``fingerprint``, because it does not
+ compute "proper" fingerprints, only tiny downscaled images (but which can be
+ used to compute image hashes or for similarity matching).
+
+ The main purpose of this filter is to support the ``skip-logo.lua`` script.
+ If this script is dropped, or mpv ever gains a way to load user-defined
+ filters (other than VapourSynth), this filter will be removed. Due to the
+ "special" nature of this filter, it will be removed without warning.
+
+ The intended way to read from the filter is using ``vf-metadata`` (also
+ see ``clear-on-query`` filter parameter). The property will return a list
+ of key/value pairs as follows:
+
+ ::
+
+ fp0.pts = 1.2345
+ fp0.hex = 1234abcdef...bcde
+ fp1.pts = 1.4567
+ fp1.hex = abcdef1234...6789
+ ...
+ fpN.pts = ...
+ fpN.hex = ...
+ type = gray-hex-16x16
+
+ Each ``fp<N>`` entry is for a frame. The ``pts`` entry specifies the
+ timestamp of the frame (within the filter chain; in simple cases this is
+ the same as the display timestamp). The ``hex`` field is the hex encoded
+ fingerprint, whose size and meaning depend on the ``type`` filter option.
+ The ``type`` field has the same value as the option the filter was created
+ with.
+
+ This returns the frames that were filtered since the last query of the
+ property. If ``clear-on-query=no`` was set, a query doesn't reset the list
+ of frames. In both cases, a maximum of 10 frames is returned. If there are
+ more frames, the oldest frames are discarded. Frames are returned in filter
+ order.
+
+ (This doesn't return a structured list for the per-frame details because the
+ internals of the ``vf-metadata`` mechanism suck. The returned format may
+ change in the future.)
+
+ This filter uses zimg for speed and profit. However, it will fallback to
+ libswscale in a number of situations: lesser pixel formats, unaligned data
+ pointers or strides, or if zimg fails to initialize for unknown reasons. In
+ these cases, the filter will use more CPU. Also, it will output different
+ fingerprints, because libswscale cannot perform the full range expansion we
+ normally request from zimg. As a consequence, the filter may be slower and
+ not work correctly in random situations.
+
+ ``type=...``
+ What fingerprint to compute. Available types are:
+
+ :gray-hex-8x8: grayscale, 8 bit, 8x8 size
+ :gray-hex-16x16: grayscale, 8 bit, 16x16 size (default)
+
+ Both types simply remove all colors, downscale the image, concatenate
+ all pixel values to a byte array, and convert the array to a hex string.
+
+ ``clear-on-query=yes|no``
+ Clear the list of frame fingerprints if the ``vf-metadata`` property for
+ this filter is queried (default: yes). This requires some care by the
+ user. Some types of accesses might query the filter multiple times,
+ which leads to lost frames.
+
+ ``print=yes|no``
+ Print computed fingerprints to the terminal (default: no). This is
+ mostly for testing and such. Scripts should use ``vf-metadata`` to
+ read information from this filter instead.
+
+``gpu=...``
+ Convert video to RGB using the OpenGL renderer normally used with
+ ``--vo=gpu``. This requires that the EGL implementation supports off-screen
+ rendering on the default display. (This is the case with Mesa.)
+
+ Sub-options:
+
+ ``w=<pixels>``, ``h=<pixels>``
+ Size of the output in pixels (default: 0). If not positive, this will
+ use the size of the first filtered input frame.
+
+ .. warning::
+
+ This is highly experimental. Performance is bad, and it will not work
+ everywhere in the first place. Some features are not supported.
+
+ .. warning::
+
+ This does not do OSD rendering. If you see OSD, then it has been
+ rendered by the VO backend. (Subtitles are rendered by the ``gpu``
+ filter, if possible.)
+
+ .. warning::
+
+ If you use this with encoding mode, keep in mind that encoding mode will
+ convert the RGB filter's output back to yuv420p in software, using the
+ configured software scaler. Using ``zimg`` might improve this, but in
+ any case it might go against your goals when using this filter.
+
+ .. warning::
+
+ Do not use this with ``--vo=gpu``. It will apply filtering twice, since
+ most ``--vo=gpu`` options are unconditionally applied to the ``gpu``
+ filter. There is no mechanism in mpv to prevent this.
+
diff --git a/DOCS/man/vo.rst b/DOCS/man/vo.rst
new file mode 100644
index 0000000..5ee4eaa
--- /dev/null
+++ b/DOCS/man/vo.rst
@@ -0,0 +1,710 @@
+VIDEO OUTPUT DRIVERS
+====================
+
+Video output drivers are interfaces to different video output facilities. The
+syntax is:
+
+``--vo=<driver1,driver2,...[,]>``
+ Specify a priority list of video output drivers to be used.
+
+If the list has a trailing ``,``, mpv will fall back on drivers not contained
+in the list.
+
+.. note::
+
+ See ``--vo=help`` for a list of compiled-in video output drivers.
+
+ The recommended output driver is ``--vo=gpu``, which is the default. All
+ other drivers are for compatibility or special purposes. If the default
+ does not work, it will fallback to other drivers (in the same order as
+ listed by ``--vo=help``).
+
+Available video output drivers are:
+
+``gpu``
+ General purpose, customizable, GPU-accelerated video output driver. It
+ supports extended scaling methods, dithering, color management, custom
+ shaders, HDR, and more.
+
+ See `GPU renderer options`_ for options specific to this VO.
+
+ By default, mpv utilizes settings that balance quality and performance.
+ Additionally, two predefined profiles are available: ``fast`` for maximum
+ performance and ``high-quality`` for superior rendering quality. You can
+ apply a specific profile using the ``--profile=<name>`` option and inspect
+ its contents using ``--show-profile=<name>``.
+
+ This VO abstracts over several possible graphics APIs and windowing
+ contexts, which can be influenced using the ``--gpu-api`` and
+ ``--gpu-context`` options.
+
+ Hardware decoding over OpenGL-interop is supported to some degree. Note
+ that in this mode, some corner case might not be gracefully handled, and
+ color space conversion and chroma upsampling is generally in the hand of
+ the hardware decoder APIs.
+
+ ``gpu`` makes use of FBOs by default. Sometimes you can achieve better
+ quality or performance by changing the ``--fbo-format`` option to
+ ``rgb16f``, ``rgb32f`` or ``rgb``. Known problems include Mesa/Intel not
+ accepting ``rgb16``, Mesa sometimes not being compiled with float texture
+ support, and some macOS setups being very slow with ``rgb16`` but fast
+ with ``rgb32f``. If you have problems, you can also try enabling the
+ ``--gpu-dumb-mode=yes`` option.
+
+``gpu-next``
+ Experimental video renderer based on ``libplacebo``. This supports almost
+ the same set of features as ``--vo=gpu``. See `GPU renderer options`_ for a
+ list.
+
+ Should generally be faster and higher quality, but some features may still
+ be missing or misbehave. Expect (and report!) bugs. See here for a list of
+ known differences and bugs:
+
+ https://github.com/mpv-player/mpv/wiki/GPU-Next-vs-GPU
+
+``xv`` (X11 only)
+ Uses the XVideo extension to enable hardware-accelerated display. This is
+ the most compatible VO on X, but may be low-quality, and has issues with
+ OSD and subtitle display.
+
+ .. note:: This driver is for compatibility with old systems.
+
+ The following global options are supported by this video output:
+
+ ``--xv-adaptor=<number>``
+ Select a specific XVideo adapter (check xvinfo results).
+ ``--xv-port=<number>``
+ Select a specific XVideo port.
+ ``--xv-ck=<cur|use|set>``
+ Select the source from which the color key is taken (default: cur).
+
+ cur
+ The default takes the color key currently set in Xv.
+ use
+ Use but do not set the color key from mpv (use the ``--colorkey``
+ option to change it).
+ set
+ Same as use but also sets the supplied color key.
+
+ ``--xv-ck-method=<none|man|bg|auto>``
+ Sets the color key drawing method (default: man).
+
+ none
+ Disables color-keying.
+ man
+ Draw the color key manually (reduces flicker in some cases).
+ bg
+ Set the color key as window background.
+ auto
+ Let Xv draw the color key.
+
+ ``--xv-colorkey=<number>``
+ Changes the color key to an RGB value of your choice. ``0x000000`` is
+ black and ``0xffffff`` is white.
+
+ ``--xv-buffers=<number>``
+ Number of image buffers to use for the internal ringbuffer (default: 2).
+ Increasing this will use more memory, but might help with the X server
+ not responding quickly enough if video FPS is close to or higher than
+ the display refresh rate.
+
+``x11`` (X11 only)
+ Shared memory video output driver without hardware acceleration that works
+ whenever X11 is present.
+
+ Since mpv 0.30.0, you may need to use ``--profile=sw-fast`` to get decent
+ performance.
+
+ .. note:: This is a fallback only, and should not be normally used.
+
+``vdpau`` (X11 only)
+ Uses the VDPAU interface to display and optionally also decode video.
+ Hardware decoding is used with ``--hwdec=vdpau``. Note that there is
+ absolutely no reason to use this, other than compatibility. We strongly
+ recommend that you use ``--vo=gpu`` with ``--hwdec=nvdec`` instead.
+
+ .. note::
+
+ Earlier versions of mpv (and MPlayer, mplayer2) provided sub-options
+ to tune vdpau post-processing, like ``deint``, ``sharpen``, ``denoise``,
+ ``chroma-deint``, ``pullup``, ``hqscaling``. These sub-options are
+ deprecated, and you should use the ``vdpaupp`` video filter instead.
+
+ The following global options are supported by this video output:
+
+ ``--vo-vdpau-sharpen=<-1-1>``
+ (Deprecated. See note about ``vdpaupp``.)
+
+ For positive values, apply a sharpening algorithm to the video, for
+ negative values a blurring algorithm (default: 0).
+ ``--vo-vdpau-denoise=<0-1>``
+ (Deprecated. See note about ``vdpaupp``.)
+
+ Apply a noise reduction algorithm to the video (default: 0; no noise
+ reduction).
+ ``--vo-vdpau-chroma-deint``
+ (Deprecated. See note about ``vdpaupp``.)
+
+ Makes temporal deinterlacers operate both on luma and chroma (default).
+ Use no-chroma-deint to solely use luma and speed up advanced
+ deinterlacing. Useful with slow video memory.
+ ``--vo-vdpau-pullup``
+ (Deprecated. See note about ``vdpaupp``.)
+
+ Try to apply inverse telecine, needs motion adaptive temporal
+ deinterlacing.
+ ``--vo-vdpau-hqscaling=<0-9>``
+ (Deprecated. See note about ``vdpaupp``.)
+
+ 0
+ Use default VDPAU scaling (default).
+ 1-9
+ Apply high quality VDPAU scaling (needs capable hardware).
+ ``--vo-vdpau-fps=<number>``
+ Override autodetected display refresh rate value (the value is needed
+ for framedrop to allow video playback rates higher than display
+ refresh rate, and for vsync-aware frame timing adjustments). Default 0
+ means use autodetected value. A positive value is interpreted as a
+ refresh rate in Hz and overrides the autodetected value. A negative
+ value disables all timing adjustment and framedrop logic.
+ ``--vo-vdpau-composite-detect``
+ NVIDIA's current VDPAU implementation behaves somewhat differently
+ under a compositing window manager and does not give accurate frame
+ timing information. With this option enabled, the player tries to
+ detect whether a compositing window manager is active. If one is
+ detected, the player disables timing adjustments as if the user had
+ specified ``fps=-1`` (as they would be based on incorrect input). This
+ means timing is somewhat less accurate than without compositing, but
+ with the composited mode behavior of the NVIDIA driver, there is no
+ hard playback speed limit even without the disabled logic. Enabled by
+ default, use ``--vo-vdpau-composite-detect=no`` to disable.
+ ``--vo-vdpau-queuetime-windowed=<number>`` and ``queuetime-fs=<number>``
+ Use VDPAU's presentation queue functionality to queue future video
+ frame changes at most this many milliseconds in advance (default: 50).
+ See below for additional information.
+ ``--vo-vdpau-output-surfaces=<2-15>``
+ Allocate this many output surfaces to display video frames (default:
+ 3). See below for additional information.
+ ``--vo-vdpau-colorkey=<#RRGGBB|#AARRGGBB>``
+ Set the VDPAU presentation queue background color, which in practice
+ is the colorkey used if VDPAU operates in overlay mode (default:
+ ``#020507``, some shade of black). If the alpha component of this value
+ is 0, the default VDPAU colorkey will be used instead (which is usually
+ green).
+ ``--vo-vdpau-force-yuv``
+ Never accept RGBA input. This means mpv will insert a filter to convert
+ to a YUV format before the VO. Sometimes useful to force availability
+ of certain YUV-only features, like video equalizer or deinterlacing.
+
+ Using the VDPAU frame queuing functionality controlled by the queuetime
+ options makes mpv's frame flip timing less sensitive to system CPU load and
+ allows mpv to start decoding the next frame(s) slightly earlier, which can
+ reduce jitter caused by individual slow-to-decode frames. However, the
+ NVIDIA graphics drivers can make other window behavior such as window moves
+ choppy if VDPAU is using the blit queue (mainly happens if you have the
+ composite extension enabled) and this feature is active. If this happens on
+ your system and it bothers you then you can set the queuetime value to 0 to
+ disable this feature. The settings to use in windowed and fullscreen mode
+ are separate because there should be no reason to disable this for
+ fullscreen mode (as the driver issue should not affect the video itself).
+
+ You can queue more frames ahead by increasing the queuetime values and the
+ ``output_surfaces`` count (to ensure enough surfaces to buffer video for a
+ certain time ahead you need at least as many surfaces as the video has
+ frames during that time, plus two). This could help make video smoother in
+ some cases. The main downsides are increased video RAM requirements for
+ the surfaces and laggier display response to user commands (display
+ changes only become visible some time after they're queued). The graphics
+ driver implementation may also have limits on the length of maximum
+ queuing time or number of queued surfaces that work well or at all.
+
+``direct3d`` (Windows only)
+ Video output driver that uses the Direct3D interface.
+
+ .. note:: This driver is for compatibility with systems that don't provide
+ proper OpenGL drivers, and where ANGLE does not perform well.
+
+ The following global options are supported by this video output:
+
+ ``--vo-direct3d-disable-texture-align``
+ Normally texture sizes are always aligned to 16. With this option
+ enabled, the video texture will always have exactly the same size as
+ the video itself.
+
+
+ Debug options. These might be incorrect, might be removed in the future,
+ might crash, might cause slow downs, etc. Contact the developers if you
+ actually need any of these for performance or proper operation.
+
+ ``--vo-direct3d-force-power-of-2``
+ Always force textures to power of 2, even if the device reports
+ non-power-of-2 texture sizes as supported.
+
+ ``--vo-direct3d-texture-memory=<mode>``
+ Only affects operation with shaders/texturing enabled, and (E)OSD.
+ Possible values:
+
+ ``default`` (default)
+ Use ``D3DPOOL_DEFAULT``, with a ``D3DPOOL_SYSTEMMEM`` texture for
+ locking. If the driver supports ``D3DDEVCAPS_TEXTURESYSTEMMEMORY``,
+ ``D3DPOOL_SYSTEMMEM`` is used directly.
+
+ ``default-pool``
+ Use ``D3DPOOL_DEFAULT``. (Like ``default``, but never use a
+ shadow-texture.)
+
+ ``default-pool-shadow``
+ Use ``D3DPOOL_DEFAULT``, with a ``D3DPOOL_SYSTEMMEM`` texture for
+ locking. (Like ``default``, but always force the shadow-texture.)
+
+ ``managed``
+ Use ``D3DPOOL_MANAGED``.
+
+ ``scratch``
+ Use ``D3DPOOL_SCRATCH``, with a ``D3DPOOL_SYSTEMMEM`` texture for
+ locking.
+
+ ``--vo-direct3d-swap-discard``
+ Use ``D3DSWAPEFFECT_DISCARD``, which might be faster.
+ Might be slower too, as it must(?) clear every frame.
+
+ ``--vo-direct3d-exact-backbuffer``
+ Always resize the backbuffer to window size.
+
+``sdl``
+ SDL 2.0+ Render video output driver, depending on system with or without
+ hardware acceleration. Should work on all platforms supported by SDL 2.0.
+ For tuning, refer to your copy of the file ``SDL_hints.h``.
+
+ .. note:: This driver is for compatibility with systems that don't provide
+ proper graphics drivers.
+
+ The following global options are supported by this video output:
+
+ ``--sdl-sw``
+ Continue even if a software renderer is detected.
+
+ ``--sdl-switch-mode``
+ Instruct SDL to switch the monitor video mode when going fullscreen.
+
+``dmabuf-wayland``
+ Experimental Wayland output driver designed for use with either drm stateless
+ or VA API hardware decoding. The driver is designed to avoid any GPU to CPU copies,
+ and to perform scaling and color space conversion using fixed-function hardware,
+ if available, rather than GPU shaders. This frees up GPU resources for other tasks.
+ It is highly recommended to use this VO with the appropriate ``--hwdec`` option such
+ as ``auto-safe``. It can still work in some circumstances without ``--hwdec`` due to
+ mpv's internal conversion filters, but this is not recommended as it's a needless
+ extra step. Correct output depends on support from your GPU, drivers, and compositor.
+ Weston and wlroots-based compositors like Sway and Intel GPUs are known to generally
+ work.
+
+``vaapi``
+ Intel VA API video output driver with support for hardware decoding. Note
+ that there is absolutely no reason to use this, other than compatibility.
+ This is low quality, and has issues with OSD. We strongly recommend that
+ you use ``--vo=gpu`` with ``--hwdec=vaapi`` instead.
+
+ The following global options are supported by this video output:
+
+ ``--vo-vaapi-scaling=<algorithm>``
+ default
+ Driver default (mpv default as well).
+ fast
+ Fast, but low quality.
+ hq
+ Unspecified driver dependent high-quality scaling, slow.
+ nla
+ ``non-linear anamorphic scaling``
+
+ ``--vo-vaapi-scaled-osd=<yes|no>``
+ If enabled, then the OSD is rendered at video resolution and scaled to
+ display resolution. By default, this is disabled, and the OSD is
+ rendered at display resolution if the driver supports it.
+
+``null``
+ Produces no video output. Useful for benchmarking.
+
+ Usually, it's better to disable video with ``--no-video`` instead.
+
+ The following global options are supported by this video output:
+
+ ``--vo-null-fps=<value>``
+ Simulate display FPS. This artificially limits how many frames the
+ VO accepts per second.
+
+``caca``
+ Color ASCII art video output driver that works on a text console.
+
+ .. note:: This driver is a joke.
+
+``tct``
+ Color Unicode art video output driver that works on a text console.
+ By default depends on support of true color by modern terminals to display
+ the images at full color range, but 256-colors output is also supported (see
+ below). On Windows it requires an ansi terminal such as mintty.
+
+ Since mpv 0.30.0, you may need to use ``--profile=sw-fast`` to get decent
+ performance.
+
+ Note: the TCT image output is not synchronized with other terminal output
+ from mpv, which can lead to broken images. The options ``--no-terminal`` or
+ ``--really-quiet`` can help with that.
+
+ ``--vo-tct-algo=<algo>``
+ Select how to write the pixels to the terminal.
+
+ half-blocks
+ Uses unicode LOWER HALF BLOCK character to achieve higher vertical
+ resolution. (Default.)
+ plain
+ Uses spaces. Causes vertical resolution to drop twofolds, but in
+ theory works in more places.
+
+ ``--vo-tct-width=<width>`` ``--vo-tct-height=<height>``
+ Assume the terminal has the specified character width and/or height.
+ These default to 80x25 if the terminal size cannot be determined.
+
+ ``--vo-tct-256=<yes|no>`` (default: no)
+ Use 256 colors - for terminals which don't support true color.
+
+``kitty``
+ Graphical output for the terminal, using the kitty graphics protocol.
+ Tested with kitty and Konsole.
+
+ You may need to use ``--profile=sw-fast`` to get decent performance.
+
+ Kitty size and alignment options:
+
+ ``--vo-kitty-cols=<columns>``, ``--vo-kitty-rows=<rows>`` (default: 0)
+ Specify the terminal size in character cells, otherwise (0) read it
+ from the terminal, or fall back to 80x25.
+
+ ``--vo-kitty-width=<width>``, ``--vo-kitty-height=<height>`` (default: 0)
+ Specify the available size in pixels, otherwise (0) read it from the
+ terminal, or fall back to 320x240.
+
+ ``--vo-kitty-left=<col>``, ``--vo-kitty-top=<row>`` (default: 0)
+ Specify the position in character cells where the image starts (1 is
+ the first column or row). If 0 (default) then try to automatically
+ determine it according to the other values and the image aspect ratio
+ and zoom.
+
+ ``--vo-kitty-config-clear=<yes|no>`` (default: yes)
+ Whether or not to clear the terminal whenever the output is
+ reconfigured (e.g. when video size changes).
+
+ ``--vo-kitty-alt-screen=<yes|no>`` (default: yes)
+ Whether or not to use the alternate screen buffer and return the
+ terminal to its previous state on exit. When set to no, the last
+ kitty image stays on screen after quit, with the cursor following it.
+
+ ``--vo-kitty-use-shm=<yes|no>`` (default: no)
+ Use shared memory objects to transfer image data to the terminal.
+ This is much faster than sending the data as escape codes, but is not
+ supported by as many terminals. It also only works on the local machine
+ and not via e.g. SSH connections.
+
+ This option is not implemented on Windows.
+
+``sixel``
+ Graphical output for the terminal, using sixels. Tested with ``mlterm`` and
+ ``xterm``.
+
+ Note: the Sixel image output is not synchronized with other terminal
+ output from mpv, which can lead to broken images.
+ The option ``--really-quiet`` can help with that, and is recommended.
+ On some platforms, using the ``--vo-sixel-buffered`` option may work as
+ well.
+
+ You may need to use ``--profile=sw-fast`` to get decent performance.
+
+ Note: at the time of writing, ``xterm`` does not enable sixel by default -
+ launching it as ``xterm -ti 340`` is one way to enable it. Also, ``xterm``
+ does not display images bigger than 1000x1000 pixels by default.
+
+ To render and align sixel images correctly, mpv needs to know the terminal
+ size both in cells and in pixels. By default it tries to use values which
+ the terminal reports, however, due to differences between terminals this is
+ an error-prone process which cannot be automated with certainty - some
+ terminals report the size in pixels including the padding - e.g. ``xterm``,
+ while others report the actual usable number of pixels - like ``mlterm``.
+ Additionally, they may behave differently when maximized or in fullscreen,
+ and mpv cannot detect this state using standard methods.
+
+ Sixel size and alignment options:
+
+ ``--vo-sixel-cols=<columns>``, ``--vo-sixel-rows=<rows>`` (default: 0)
+ Specify the terminal size in character cells, otherwise (0) read it
+ from the terminal, or fall back to 80x25. Note that mpv doesn't use the
+ the last row with sixel because this seems to result in scrolling.
+
+ ``--vo-sixel-width=<width>``, ``--vo-sixel-height=<height>`` (default: 0)
+ Specify the available size in pixels, otherwise (0) read it from the
+ terminal, or fall back to 320x240. Other than excluding the last line,
+ the height is also further rounded down to a multiple of 6 (sixel unit
+ height) to avoid overflowing below the designated size.
+
+ ``--vo-sixel-left=<col>``, ``--vo-sixel-top=<row>`` (default: 0)
+ Specify the position in character cells where the image starts (1 is
+ the first column or row). If 0 (default) then try to automatically
+ determine it according to the other values and the image aspect ratio
+ and zoom.
+
+ ``--vo-sixel-pad-x=<pad_x>``, ``--vo-sixel-pad-y=<pad_y>`` (default: -1)
+ Used only when mpv reads the size in pixels from the terminal.
+ Specify the number of padding pixels (on one side) which are included
+ at the size which the terminal reports. If -1 (default) then the number
+ of pixels is rounded down to a multiple of number of cells (per axis),
+ to take into account padding at the report - this only works correctly
+ when the overall padding per axis is smaller than the number of cells.
+
+ ``--vo-sixel-config-clear=<yes|no>`` (default: yes)
+ Whether or not to clear the terminal whenever the output is
+ reconfigured (e.g. when video size changes).
+
+ ``--vo-sixel-alt-screen=<yes|no>`` (default: yes)
+ Whether or not to use the alternate screen buffer and return the
+ terminal to its previous state on exit. When set to no, the last
+ sixel image stays on screen after quit, with the cursor following it.
+
+ ``--vo-sixel-exit-clear`` is a deprecated alias for this option and
+ may be removed in the future.
+
+ ``--vo-sixel-buffered=<yes|no>`` (default: no)
+ Buffers the full output sequence before writing it to the terminal.
+ On POSIX platforms, this can help prevent interruption (including from
+ other applications) and thus broken images, but may come at a
+ performance cost with some terminals and is subject to implementation
+ details.
+
+ Sixel image quality options:
+
+ ``--vo-sixel-dither=<algo>``
+ Selects the dither algorithm which libsixel should apply.
+ Can be one of the below list as per libsixel's documentation.
+
+ auto (Default)
+ Let libsixel choose the dithering method.
+ none
+ Don't diffuse
+ atkinson
+ Diffuse with Bill Atkinson's method.
+ fs
+ Diffuse with Floyd-Steinberg method
+ jajuni
+ Diffuse with Jarvis, Judice & Ninke method
+ stucki
+ Diffuse with Stucki's method
+ burkes
+ Diffuse with Burkes' method
+ arithmetic
+ Positionally stable arithmetic dither
+ xor
+ Positionally stable arithmetic xor based dither
+
+ ``--vo-sixel-fixedpalette=<yes|no>`` (default: yes)
+ Use libsixel's built-in static palette using the XTERM256 profile
+ for dither. Fixed palette uses 256 colors for dithering. Note that
+ using ``no`` (at the time of writing) will slow down ``xterm``.
+
+ ``--vo-sixel-reqcolors=<colors>`` (default: 256)
+ Has no effect with fixed palette. Set up libsixel to use required
+ number of colors for dynamic palette. This value depends on the
+ terminal emulator as well. Xterm supports 256 colors. Can set this to
+ a lower value for faster performance.
+
+ ``--vo-sixel-threshold=<threshold>`` (default: -1)
+ Has no effect with fixed palette. Defines the threshold to change the
+ palette - as percentage of the number of colors, e.g. 20 will change
+ the palette when the number of colors changed by 20%. It's a simple
+ measure to reduce the number of palette changes, because it can be slow
+ in some terminals (``xterm``). The default (-1) will choose a palette
+ on every frame and will have better quality.
+
+``image``
+ Output each frame into an image file in the current directory. Each file
+ takes the frame number padded with leading zeros as name.
+
+ The following global options are supported by this video output:
+
+ ``--vo-image-format=<format>``
+ Select the image file format.
+
+ jpg
+ JPEG files, extension .jpg. (Default.)
+ jpeg
+ JPEG files, extension .jpeg.
+ png
+ PNG files.
+ webp
+ WebP files.
+
+ ``--vo-image-png-compression=<0-9>``
+ PNG compression factor (speed vs. file size tradeoff) (default: 7)
+ ``--vo-image-png-filter=<0-5>``
+ Filter applied prior to PNG compression (0 = none; 1 = sub; 2 = up;
+ 3 = average; 4 = Paeth; 5 = mixed) (default: 5)
+ ``--vo-image-jpeg-quality=<0-100>``
+ JPEG quality factor (default: 90)
+ ``--vo-image-jpeg-optimize=<0-100>``
+ JPEG optimization factor (default: 100)
+ ``--vo-image-webp-lossless=<yes|no>``
+ Enable writing lossless WebP files (default: no)
+ ``--vo-image-webp-quality=<0-100>``
+ WebP quality (default: 75)
+ ``--vo-image-webp-compression=<0-6>``
+ WebP compression factor (default: 4)
+ ``--vo-image-outdir=<dirname>``
+ Specify the directory to save the image files to (default: ``./``).
+
+``libmpv``
+ For use with libmpv direct embedding. As a special case, on macOS it
+ is used like a normal VO within mpv (cocoa-cb). Otherwise useless in any
+ other contexts.
+ (See ``<mpv/render.h>``.)
+
+ This also supports many of the options the ``gpu`` VO has, depending on the
+ backend.
+
+``rpi`` (Raspberry Pi)
+ Native video output on the Raspberry Pi using the MMAL API.
+
+ The following global options are supported by this video output:
+
+ ``--rpi-display=<number>``
+ Select the display number on which the video overlay should be shown
+ (default: 0).
+
+ ``--rpi-layer=<number>``
+ Select the dispmanx layer on which the video overlay should be shown
+ (default: -10). Note that mpv will also use the 2 layers above the
+ selected layer, to handle the window background and OSD. Actual video
+ rendering will happen on the layer above the selected layer.
+
+ ``--rpi-background=<yes|no>``
+ Whether to render a black background behind the video (default: no).
+ Normally it's better to kill the console framebuffer instead, which
+ gives better performance.
+
+ ``--rpi-osd=<yes|no>``
+ Enabled by default. If disabled with ``no``, no OSD layer is created.
+ This also means there will be no subtitles rendered.
+
+``drm`` (Direct Rendering Manager)
+ Video output driver using Kernel Mode Setting / Direct Rendering Manager.
+ Should be used when one doesn't want to install full-blown graphical
+ environment (e.g. no X). Does not support hardware acceleration (if you
+ need this, check the ``drm`` backend for ``gpu`` VO).
+
+ Since mpv 0.30.0, you may need to use ``--profile=sw-fast`` to get decent
+ performance.
+
+ The following global options are supported by this video output:
+
+ ``--drm-connector=<name>``
+ Select the connector to use (usually this is a monitor.) If ``<name>``
+ is empty or ``auto``, mpv renders the output on the first available
+ connector. Use ``--drm-connector=help`` to get a list of available
+ connectors. (default: empty)
+
+ ``--drm-device=<path>``
+ Select the DRM device file to use. If specified this overrides automatic
+ card selection. (default: empty)
+
+ ``--drm-mode=<preferred|highest|N|WxH[@R]>``
+ Mode to use (resolution and frame rate).
+ Possible values:
+
+ :preferred: Use the preferred mode for the screen on the selected
+ connector. (default)
+ :highest: Use the mode with the highest resolution available on the
+ selected connector.
+ :N: Select mode by index.
+ :WxH[@R]: Specify mode by width, height, and optionally refresh rate.
+ In case several modes match, selects the mode that comes
+ first in the EDID list of modes.
+
+ Use ``--drm-mode=help`` to get a list of available modes for all active
+ connectors.
+
+ ``--drm-draw-plane=<primary|overlay|N>``
+ Select the DRM plane to which video and OSD is drawn to, under normal
+ circumstances. The plane can be specified as ``primary``, which will
+ pick the first applicable primary plane; ``overlay``, which will pick
+ the first applicable overlay plane; or by index. The index is zero
+ based, and related to the CRTC.
+ (default: primary)
+
+ When using this option with the drmprime-overlay hwdec interop, only the
+ OSD is rendered to this plane.
+
+ ``--drm-drmprime-video-plane=<primary|overlay|N>``
+ Select the DRM plane to use for video with the drmprime-overlay hwdec
+ interop (used by e.g. the rkmpp hwdec on RockChip SoCs, and v4l2 hwdec:s
+ on various other SoC:s). The plane is unused otherwise. This option
+ accepts the same values as ``--drm-draw-plane``. (default: overlay)
+
+ To be able to successfully play 4K video on various SoCs you might need
+ to set ``--drm-draw-plane=overlay --drm-drmprime-video-plane=primary``
+ and setting ``--drm-draw-surface-size=1920x1080``, to render the OSD at a
+ lower resolution (the video when handled by the hwdec will be on the
+ drmprime-video plane and at full 4K resolution)
+
+ ``--drm-format=<xrgb8888|xrgb2101010>``
+ Select the DRM format to use (default: xrgb8888). This allows you to
+ choose the bit depth of the DRM mode. xrgb8888 is your usual 24 bit per
+ pixel/8 bits per channel packed RGB format with 8 bits of padding.
+ xrgb2101010 is a packed 30 bits per pixel/10 bits per channel packed RGB
+ format with 2 bits of padding.
+
+ There are cases when xrgb2101010 will work with the ``drm`` VO, but not
+ with the ``drm`` backend for the ``gpu`` VO. This is because with the
+ ``gpu`` VO, in addition to requiring support in your DRM driver,
+ requires support for xrgb2101010 in your EGL driver
+
+ ``--drm-draw-surface-size=<[WxH]>``
+ Sets the size of the surface used on the draw plane. The surface will
+ then be upscaled to the current screen resolution. This option can be
+ useful when used together with the drmprime-overlay hwdec interop at
+ high resolutions, as it allows scaling the draw plane (which in this
+ case only handles the OSD) down to a size the GPU can handle.
+
+ When used without the drmprime-overlay hwdec interop this option will
+ just cause the video to get rendered at a different resolution and then
+ scaled to screen size.
+
+ (default: display resolution)
+
+ ``--drm-vrr-enabled=<no|yes|auto>``
+ Toggle use of Variable Refresh Rate (VRR), aka Freesync or Adaptive Sync
+ on compatible systems. VRR allows for the display to be refreshed at any
+ rate within a range (usually ~40Hz-60Hz for 60Hz displays). This can help
+ with playback of 24/25/50fps content. Support depends on the use of a
+ compatible monitor, GPU, and a sufficiently new kernel with drivers
+ that support the feature.
+
+ :no: Do not attempt to enable VRR. (default)
+ :yes: Attempt to enable VRR, whether the capability is reported or not.
+ :auto: Attempt to enable VRR if support is reported.
+
+``mediacodec_embed`` (Android)
+ Renders ``IMGFMT_MEDIACODEC`` frames directly to an ``android.view.Surface``.
+ Requires ``--hwdec=mediacodec`` for hardware decoding, along with
+ ``--vo=mediacodec_embed`` and ``--wid=(intptr_t)(*android.view.Surface)``.
+
+ Since this video output driver uses native decoding and rendering routines,
+ many of mpv's features (subtitle rendering, OSD/OSC, video filters, etc)
+ are not available with this driver.
+
+ To use hardware decoding with ``--vo=gpu`` instead, use ``--hwdec=mediacodec``
+ or ``mediacodec-copy`` along with ``--gpu-context=android``.
+
+``wlshm`` (Wayland only)
+ Shared memory video output driver without hardware acceleration that works
+ whenever Wayland is present.
+
+ Since mpv 0.30.0, you may need to use ``--profile=sw-fast`` to get decent
+ performance.
+
+ .. note:: This is a fallback only, and should not be normally used.
diff --git a/DOCS/mplayer-changes.rst b/DOCS/mplayer-changes.rst
new file mode 100644
index 0000000..6ecb1d0
--- /dev/null
+++ b/DOCS/mplayer-changes.rst
@@ -0,0 +1,455 @@
+CHANGES FROM OTHER VERSIONS OF MPLAYER
+======================================
+
+**mpv** is based on mplayer2, which in turn is based on the original MPlayer
+(also called mplayer, mplayer-svn, mplayer1). Many changes have been made, a
+large part of which is incompatible or completely changes how the player
+behaves. Although there are still many similarities to its ancestors, **mpv**
+should generally be treated as a completely different program.
+
+.. admonition:: Warning
+
+ This document is **not updated** anymore, and is **incomplete** and
+ **outdated**. If you look for old option replacements, always check with
+ the current mpv manpage, as the options could have changed meanwhile.
+
+General Changes from MPlayer to mpv
+-----------------------------------
+
+This listing is about changes introduced by mplayer2 and mpv relatively to
+MPlayer.
+
+Player
+~~~~~~
+
+* New name for the binary (``mpv``). New location for config files (either
+ ``~/.config/mpv/mpv.conf``, or if you want, ``~/.mpv/config``).
+* Encoding functionality (replacement for MEncoder, see the `ENCODING`_ section).
+* Support for Lua scripting (see the `LUA SCRIPTING`_ section).
+* Better pause handling (e.g. do not unpause on a command).
+* Precise seeking support.
+* Improvements in audio/video sync handling.
+* Do not lose settings when playing a new file in the same player instance.
+* Slave mode compatibility broken (see below).
+* Re-enable screensaver while the player is paused.
+* Allow resuming playback at a later point with ``Shift+q``, also see the
+ ``quit-watch-later`` input command.
+* ``--keep-open`` option to stop the player from closing the window and
+ exiting after playback ends.
+* A client API, that allows embedding **mpv** into applications
+ (see ``libmpv/client.h`` in the sources).
+
+Input
+~~~~~
+
+* Improved default keybindings. MPlayer bindings are also available (see
+ ``etc/mplayer-input.conf`` in the source tree).
+* Improved responsiveness on user input.
+* Support for modifier keys (alt, shift, ctrl) in input.conf.
+* Allow customizing whether a key binding for seeking shows the video time, the
+ OSD bar, or nothing (see the `Input Command Prefixes`_ section).
+* Support mapping multiple commands to one key.
+* Classic LIRC support was removed. Install remotes as input devices instead.
+ This way they will send X11 key events to the mpv window, which can be bound
+ using the normal ``input.conf``.
+ Also see: https://github.com/mpv-player/mpv/wiki/IR-remotes
+* Joystick support was removed. It was considered useless and was the cause
+ of some problems (e.g. a laptop's accelerator being recognized as joystick).
+* Support for relative seeking by percentage.
+
+Audio
+~~~~~
+
+* Support for gapless audio (see the ``--gapless-audio`` option).
+* Support for OSS4 volume control.
+* Improved support for PulseAudio.
+* Make ``--softvol`` default (**mpv** is not a mixer control panel).
+* By default, do pitch correction if playback speed is increased.
+* Improved downmixing and output of surround audio:
+
+ - Instead of using hardcoded pan filters to do remixing, libavresample is used
+ - Channel maps are used to identify the channel layout, so e.g. ``3.0`` and
+ ``2.1`` audio can be distinguished.
+
+Video
+~~~~~
+
+* Wayland support.
+* Native support for VAAPI and VDA. Improved VDPAU video output.
+* Improved GPU-accelerated video output (see the ``gpu-hq`` preset).
+* Make hardware decoding work with the ``gpu`` video output.
+* Support for libavfilter (for video->video and audio->audio). This allows
+ using most of FFmpeg's filters, which improve greatly on the old MPlayer
+ filters in features, performance, and correctness.
+* More correct color reproduction (color matrix generation), including support
+ for BT.2020 (Ultra HD). linear XYZ (Digital Cinema) and SMPTE ST2084 (HDR)
+ inputs.
+* Support for color managed displays, via ICC profiles.
+* High-quality image resamplers (see the ``--scale`` suboption).
+* Support for scaling in (sigmoidized) linear light.
+* Better subtitle rendering using libass by default.
+* Improvements when playing multiple files (``-fixed-vo`` is default, do not
+ reset settings by default when playing a new file).
+* Replace image video outputs (``--vo=jpeg`` etc.) with ``--vo=image``.
+* Removal of ``--vo=gif89a``, ``--vo=md5sum``, ``--vo=yuv4mpeg``, as encoding
+ can handle these use cases. For yuv4mpeg, for example, use::
+
+ mpv input.mkv -o output.y4m --no-audio --oautofps --oneverdrop
+
+* Image subtitles (DVDs etc.) are rendered in color and use more correct
+ positioning (color for image subs can be disabled with ``--sub-gray``).
+* Support for the X11 video output is removed, since it was considered
+ deprecated. SDL video output can still be used as a fallback.
+
+OSD and terminal
+~~~~~~~~~~~~~~~~
+
+* Cleaned up terminal output: nicer status line, less useless noise.
+* Improved OSD rendering using libass, with full Unicode support.
+* New OSD bar with chapter marks. Not positioned in the middle of the video
+ (this can be customized with the ``--osd-bar-align-y`` option).
+* Display list of chapters and audio/subtitle tracks on OSD (see the
+ `Properties`_ section).
+
+Screenshots
+~~~~~~~~~~~
+
+* Instant screenshots without 1-frame delay.
+* Support for taking screenshots even with hardware decoding.
+* Support for saving screenshots as JPEG or PNG.
+* Support for configurable file names.
+* Support for taking screenshots with or without subtitles.
+
+Note that the ``screenshot`` video filter is not needed anymore, and should not
+be put into the mpv config file.
+
+Miscellaneous
+~~~~~~~~~~~~~
+
+* Better MKV support (e.g. ordered chapters, 3D metadata).
+* Matroska edition switching at runtime.
+* Support for playing URLs of popular streaming sites directly.
+ (e.g. ``mpv https://www.youtube.com/watch?v=...``).
+ Requires a recent version of ``youtube-dl`` to be installed. Can be
+ disabled with ``ytdl=no`` in the mpv config file.
+* Support for precise scrolling which scales the parameter of commands. If the
+ input doesn't support precise scrolling the scale factor stays 1.
+* Allow changing/adjusting video filters at runtime. (This is also used to make
+ the ``D`` key insert vf_yadif if deinterlacing is not supported otherwise).
+* Improved support for .cue files.
+
+Mac OS X
+~~~~~~~~
+
+* Native OpenGL backend.
+* Cocoa event loop is independent from MPlayer's event loop, so user
+ actions like accessing menus and live resizing do not block the playback.
+* Media Keys support.
+* VDA support using libavcodec hwaccel API instead of FFmpeg's decoder with up
+ to 2-2.5x reduction in CPU usage.
+
+Windows
+~~~~~~~
+
+* Improved support for Unicode file names.
+* Improved window handling.
+* Do not block playback when moving the window.
+* Improved Direct3D video output.
+* Added WASAPI audio output.
+
+Internal changes
+~~~~~~~~~~~~~~~~
+
+* Switch to GPLv2+ (see ``Copyright`` file for details).
+* Removal of lots of cruft:
+
+ - Internal GUI (replaced by the OSC, see the `ON SCREEN CONTROLLER`_ section).
+ - MEncoder (replaced by native encoding, see the `ENCODING`_ section).
+ - OSD menu.
+ - Kernel video drivers for Linux 2.4 (including VIDIX).
+ - Teletext support.
+ - Support for dead platforms.
+ - Most built-in demuxers have been replaced by their libavformat counterparts.
+ - Built-in network support has been replaced by libavformat's (which also
+ supports https URLs).
+ - Embedded copies of libraries (such as FFmpeg).
+
+* General code cleanups (including refactoring or rewrites of many parts).
+* New build system.
+* Many bug fixes and removal of long-standing issues.
+* Generally preferring FFmpeg/Libav over internal demuxers, decoders, and
+ filters.
+
+Detailed Listing of User-visible Changes
+----------------------------------------
+
+This listing is about changed command line switches, slave commands, and similar
+things. Completely removed features are not listed.
+
+Command Line Switches
+~~~~~~~~~~~~~~~~~~~~~
+
+* There is a new command line syntax, which is generally preferred over the old
+ syntax. ``-optname optvalue`` becomes ``--optname=optvalue``.
+
+ The old syntax will not be removed. However, the new syntax is mentioned in
+ all documentation and so on, and unlike the old syntax is not ambiguous,
+ so it is a good thing to know about this change.
+* In general, negating switches like ``-noopt`` now have to be written as
+ ``-no-opt`` or ``--no-opt``.
+* Per-file options are not the default anymore. You can explicitly specify
+ file-local options. See ``Usage`` section.
+* Many options have been renamed, removed or changed semantics. Some options
+ that are required for a good playback experience with MPlayer are now
+ superfluous or even worse than the defaults, so make sure to read the manual
+ before trying to use your existing configuration with **mpv**.
+* Table of renamed/replaced switches:
+
+ =========================== ========================================
+ Old New
+ =========================== ========================================
+ ``-no<opt>`` ``--no-<opt>`` (add a dash)
+ ``-a52drc level`` ``--ad-lavc-ac3drc=level``
+ ``-ac spdifac3`` ``--ad=spdif:ac3`` (see ``--ad=help``)
+ ``-af volnorm`` (removed; use acompressor ffmpeg filter instead)
+ ``-afm hwac3`` ``--ad=spdif:ac3,spdif:dts``
+ ``-ao alsa:device=hw=0.3`` ``--ao=alsa:device=[hw:0,3]``
+ ``-aspect`` ``--video-aspect-override``
+ ``-ass-bottom-margin`` ``--vf=sub=bottom:top``
+ ``-ass`` ``--sub-ass``
+ ``-audiofile-cache`` (removed; the main cache settings are used)
+ ``-audiofile`` ``--audio-file``
+ ``-benchmark`` ``--untimed`` (no stats)
+ ``-capture`` ``--stream-capture=<filename>``
+ ``-channels`` ``--audio-channels`` (changed semantics)
+ ``-cursor-autohide-delay`` ``--cursor-autohide``
+ ``-delay`` ``--audio-delay``
+ ``-dumpstream`` ``--stream-dump=<filename>``
+ ``-dvdangle`` ``--dvd-angle``
+ ``-endpos`` ``--length``
+ ``-fixed-vo`` (removed; always the default)
+ ``-font`` ``--osd-font``
+ ``-forcedsubsonly`` ``--sub-forced-events-only``
+ ``-forceidx`` ``--index``
+ ``-format`` ``--audio-format``
+ ``-fsmode-dontuse`` (removed)
+ ``-fstype`` ``--x11-netwm`` (changed semantics)
+ ``-hardframedrop`` ``--framedrop=hard``
+ ``-identify`` (removed; use TOOLS/mpv_identify.sh)
+ ``-idx`` ``--index``
+ ``-lavdopts ...`` ``--vd-lavc-...``
+ ``-lavfdopts`` ``--demuxer-lavf-...``
+ ``-loop 0`` ``--loop=inf``
+ ``-mixer-channel`` AO suboptions (``alsa``, ``oss``)
+ ``-mixer`` AO suboptions (``alsa``, ``oss``)
+ ``-mouse-movements`` ``--input-cursor``
+ ``-msgcolor`` ``--msg-color``
+ ``-msglevel`` ``--msg-level`` (changed semantics)
+ ``-msgmodule`` ``--msg-module``
+ ``-name`` ``--x11-name``
+ ``-noar`` ``(removed; replaced by MediaPlayer framework)``
+ ``-noautosub`` ``--no-sub-auto``
+ ``-noconsolecontrols`` ``--no-input-terminal``
+ ``-nosound`` ``--no-audio``
+ ``-osdlevel`` ``--osd-level``
+ ``-panscanrange`` ``--video-zoom``, ``--video-pan-x/y``
+ ``-playing-msg`` ``--term-playing-msg``
+ ``-pp ...`` ``'--vf=lavfi=[pp=...]'``
+ ``-pphelp`` (See FFmpeg libavfilter documentation.)
+ ``-rawaudio ...`` ``--demuxer-rawaudio-...``
+ ``-rawvideo ...`` ``--demuxer-rawvideo-...``
+ ``-spugauss`` ``--sub-gauss``
+ ``-srate`` ``--audio-samplerate``
+ ``-ss`` ``--start``
+ ``-ssf <sub>`` ``--sws-...``
+ ``-stop-xscreensaver`` ``--stop-screensaver``
+ ``-sub-fuzziness`` ``--sub-auto``
+ ``-sub-text-*`` ``--sub-*``
+ ``-sub`` ``--sub-file``
+ ``-subcp`` ``--sub-codepage``
+ ``-subdelay`` ``--sub-delay``
+ ``-subfile`` ``--sub-file``
+ ``-subfont-*`` ``--sub-*``, ``--osd-*``
+ ``-subfont-text-scale`` ``--sub-scale``
+ ``-subfont`` ``--sub-font``
+ ``-subfps`` ``--sub-fps``
+ ``-subpos`` ``--sub-pos``
+ ``-sws`` ``--sws-scaler``
+ ``-tvscan`` ``--tv-scan``
+ ``-use-filename-title`` ``--title='${filename}'``
+ ``-vc ffh264vdpau`` (etc.) ``--hwdec=vdpau``
+ ``-vobsub`` ``--sub-file`` (pass the .idx file)
+ ``-x W``, ``-y H`` ``--geometry=WxH`` + ``--no-keepaspect``
+ ``-xineramascreen`` ``--screen`` (different values)
+ ``-xy W`` ``--autofit=W``
+ ``-zoom`` Inverse available as ``--video-unscaled``
+ ``dvdnav://`` Removed.
+ ``dvd://1`` ``dvd://0`` (0-based offset)
+ =========================== ========================================
+
+.. note::
+
+ ``-opt val`` becomes ``--opt=val``.
+
+.. note::
+
+ Quite some video filters, video outputs, audio filters, audio outputs, had
+ changes in their option parsing. These aren't mentioned in the table above.
+
+ Also, some video and audio filters have been removed, and you have to use
+ libavfilter (using ``--vf=lavfi=[...]`` or ``--af=lavfi=[...]``) to get
+ them back.
+
+input.conf and Slave Commands
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* Table of renamed input commands:
+
+ This lists only commands that are not always gracefully handled by the
+ internal legacy translation layer. If an input.conf contains any legacy
+ commands, a warning will be printed when starting the player. The warnings
+ also show the replacement commands.
+
+ Properties containing ``_`` to separate words use ``-`` instead.
+
+ +--------------------------------+----------------------------------------+
+ | Old | New |
+ +================================+========================================+
+ | ``pt_step 1 [0|1]`` | ``playlist-next [weak|force]`` |
+ | | (translation layer cannot deal with |
+ | | whitespace) |
+ +--------------------------------+----------------------------------------+
+ | ``pt_step -1 [0|1]`` | ``playlist-prev [weak|force] (same)`` |
+ +--------------------------------+----------------------------------------+
+ | ``switch_ratio [<ratio>]`` | ``set video-aspect-override <ratio>`` |
+ | | |
+ | | ``set video-aspect-override 0`` (reset)|
+ +--------------------------------+----------------------------------------+
+ | ``step_property_osd <prop>`` | ``cycle <prop> <step>`` (wraps), |
+ | ``<step> <dir>`` | ``add <prop> <step>`` (clamps). |
+ | | ``<dir>`` parameter unsupported. Use |
+ | | a negative ``<step>`` instead. |
+ +--------------------------------+----------------------------------------+
+ | ``step_property <prop>`` | Prefix ``cycle`` or ``add`` with |
+ | ``<step> <dir>`` | ``no-osd``: ``no-osd cycle <prop>`` |
+ | | ``<step>`` |
+ +--------------------------------+----------------------------------------+
+ | ``osd_show_property_text`` | ``show-text <text>`` |
+ | ``<text>`` | The property expansion format string |
+ | | syntax slightly changed. |
+ +--------------------------------+----------------------------------------+
+ | ``osd_show_text`` | Now does the same as |
+ | | ``osd_show_property_text``. Use the |
+ | | ``raw`` prefix to disable property |
+ | | expansion. |
+ +--------------------------------+----------------------------------------+
+ | ``show_tracks`` | ``show-text ${track-list}`` |
+ +--------------------------------+----------------------------------------+
+ | ``show_chapters`` | ``show-text ${chapter-list}`` |
+ +--------------------------------+----------------------------------------+
+ | ``af_switch``, ``af_add``, ... | ``af set|add|...`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_start_scan`` | ``set tv-scan yes`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_set_channel <val>`` | ``set tv-channel <val>`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_step_channel`` | ``cycle tv-channel`` |
+ +--------------------------------+----------------------------------------+
+ | ``dvb_set_channel <v1> <v2>`` | ``set dvb-channel <v1>-<v2>`` |
+ +--------------------------------+----------------------------------------+
+ | ``dvb_step_channel`` | ``cycle dvb-channel`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_set_freq <val>`` | ``set tv-freq <val>`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_step_freq`` | ``cycle tv-freq`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_set_norm <norm>`` | ``set tv-norm <norm>`` |
+ +--------------------------------+----------------------------------------+
+ | ``tv_step_norm`` | ``cycle tv-norm`` |
+ +--------------------------------+----------------------------------------+
+
+ .. note::
+
+ Due to lack of hardware and users using the TV/DVB/PVR features, and
+ due to the need to cleanup the related command code, it's possible
+ that the new commands are buggy or behave worse. This can be improved
+ if testers are available. Otherwise, some of the TV code will be
+ removed at some point.
+
+Slave mode
+~~~~~~~~~~
+
+* Slave mode was removed. A proper slave mode application needed tons of code
+ and hacks to get
+ it right. The main problem is that slave mode is a bad and incomplete
+ interface, and to get around that, applications parsed output messages
+ intended for users. It is hard to know which messages exactly are parsed by
+ slave mode applications. This makes it virtually impossible to improve
+ terminal output intended for users without possibly breaking something.
+
+ This is absolutely insane, and since initial improvements to **mpv** quickly
+ made slave mode incompatible to most applications, it was removed as useless
+ cruft. The client API (see below) is provided instead.
+
+ ``--identify`` was replaced by the ``TOOLS/mpv_identify.sh`` wrapper script.
+
+* For some time (until including release 0.4.x), mpv supported a
+ ``--slave-broken`` option. The following options are equivalent:
+
+ ::
+
+ --input-file=/dev/stdin --input-terminal=no
+
+
+ Assuming the system supports ``/dev/stdin``.
+
+ (The option was added back in 0.5.1 and sets exactly these options. It was
+ removed in 0.10.x again.)
+
+* A JSON RPC protocol giving access to the client API is also supported. See
+ `JSON IPC`_ for more information.
+
+* **mpv** also provides a client API, which can be used to embed the player
+ by loading it as shared library. (See ``libmpv/client.h`` in the sources.)
+ It might also be possible to implement a custom slave mode-like protocol
+ using Lua scripting.
+
+Policy for Removed Features
+---------------------------
+
+**mpv** is in active development. If something is in the way of more important
+development (such as fixing bugs or implementing new features), we sometimes
+remove features. Usually this happens only with old features that either seem
+to be useless, or are not used by anyone. Often these are obscure, or
+"inherited", or were marked experimental, but never received any particular
+praise by any users.
+
+Sometimes, features are replaced by something new. The new code will be either
+simpler or more powerful, but doesn't necessarily provide everything the old
+feature did.
+
+We can not exclude that we accidentally remove features that are actually
+popular. Generally, we do not know how much a specific functionality is used.
+If you miss a feature and think it should be re-added, please open an issue
+on the mpv bug tracker. Hopefully, a solution can be found. Often, it turns
+out that re-adding something is not much of a problem, or that there are
+better alternatives.
+
+Why this Fork?
+--------------
+
+mplayer2 is practically dead, and mpv started out as a branch containing
+new/experimental development. (Some of it was merged right *after* the fork
+was made public, seemingly as an acknowledgment that development, or at
+least merging, should have been more active.)
+
+MPlayer is focused on not breaking anything, but is stuck with a horrible
+codebase resistant to cleanup. (Unless you do what mpv did - merciless and
+consequent pruning of bad, old code.) Cleanup and keeping broken things
+conflict, so the kind of development mpv strives for can't be done within
+MPlayer due to clashing development policies.
+
+Additionally, mplayer2 already had lots of changes over MPlayer, which would
+have needed to be backported to the MPlayer codebase. This would not only
+have been hard (several years of diverging development), but also would have
+been impossible due to the aforementioned MPlayer development policy.
diff --git a/DOCS/release-policy.md b/DOCS/release-policy.md
new file mode 100644
index 0000000..2426ab4
--- /dev/null
+++ b/DOCS/release-policy.md
@@ -0,0 +1,138 @@
+Release Policy
+==============
+
+Once or twice a year, a new release is cut off of the master branch and is
+assigned a 0.X.Y version number, where X is incremented each time a release
+contains breaking changes, such as changed options or added/removed features,
+and Y is incremented if a release contains only bugfixes and other minor
+changes.
+
+Releases are tagged on the master branch and will not be maintained separately.
+Patch releases may be made if the amount or severity of bugs justify it, or in
+the event of security issues.
+
+The goal of releases is to provide Linux distributions with something to
+package. If you want the newest features, just use the master branch.
+We try our best to keep it deployable at all times.
+
+Releases other than the latest release are unsupported and unmaintained.
+
+Release procedure
+-----------------
+
+While on master:
+
+- Update the `RELEASE_NOTES` file, replacing the previous release notes.
+
+- Update the `VERSION` file.
+
+- Update `DOCS/client-api-changes.rst` and `DOCS/interface-changes.rst`
+ (in particular, update the last version numbers if necessary)
+
+- Create signed commit with changes.
+
+- Create signed tag v0.X.Y.
+
+- Push release branch (`release/0.X`) and tag to GitHub.
+
+- Create a new GitHub release using the content of `RELEASE_NOTES` related to
+ the new version.
+
+- Readd -UNKNOWN suffix to version in `VERSION` file.
+
+If necessary (to e.g. exclude commits already on master), the release can
+be done on a branch with different commit history. The release branch **must**
+then be merged to master so `git describe` will pick up the tag.
+
+This does not apply to patch releases, which are tagged directly on the
+`release/0.X` branch. The master branch always remains at v0.X.0.
+
+Release notes template
+----------------------
+
+Here is a template that can be used for writing the `RELEASE_NOTES` file:
+
+```markdown
+Release 0.X.Y
+=============
+
+This release requires FFmpeg <ver> or newer.
+
+Features
+--------
+
+New
+~~~
+
+- List of new features
+
+
+Changed
+~~~~~~~
+
+- List of changed features
+
+
+Removed
+~~~~~~~
+
+- List of removed features
+
+
+Options and Commands
+--------------------
+
+Added
+~~~~~
+
+- List of added options and commands
+
+
+Changed
+~~~~~~~
+
+- List of changed options and commands
+
+
+Deprecated
+~~~~~~~~~~
+
+- List of deprecated options and commands
+
+
+Removed
+~~~~~~~
+
+- List of removed options and commands
+
+
+Fixes and Minor Enhancements
+----------------------------
+
+- List of fixes and minor enhancements
+
+
+This listing is not complete. Check DOCS/client-api-changes.rst for a history
+of changes to the client API, and DOCS/interface-changes.rst for a history
+of changes to other user-visible interfaces.
+
+A complete changelog can be seen by running `git log <start>..<end>`
+in the git repository.
+```
+
+When creating a new point release its changes should be added on top of the
+`RELEASE_NOTES` file (with the appropriate title) so that all the changes in
+the current 0.X branch will be included. This way the `RELEASE_NOTES` file
+can be used by distributors as changelog for point releases too.
+
+The changelog of lists all changes since the last release, including those
+that have been backported to patch releases already.
+
+Some additional advice:
+- Especially for features, try to reword the messages so that the user-visible
+ change is clear to the reader. But don't simplify too much or be too verbose.
+- It often makes sense to merge multiple related changes into one line
+- Changes that have been made and reverted within the same release must not
+ appear in the changelog
+- Limit the "Options and Commands" section to relevant changes
+- When filling in the GitHub release, remove the "Release 0.X.Y" heading
diff --git a/DOCS/tech-overview.txt b/DOCS/tech-overview.txt
new file mode 100644
index 0000000..e723f78
--- /dev/null
+++ b/DOCS/tech-overview.txt
@@ -0,0 +1,656 @@
+This file intends to give a big picture overview of how mpv is structured.
+
+player/*.c:
+ Essentially makes up the player applications, including the main() function
+ and the playback loop.
+
+ Generally, it accesses all other subsystems, initializes them, and pushes
+ data between them during playback.
+
+ The structure is as follows (as of commit e13c05366557cb):
+ * main():
+ * basic initializations (e.g. init_libav() and more)
+ * pre-parse command line (verbosity level, config file locations)
+ * load config files (parse_cfgfiles())
+ * parse command line, add files from the command line to playlist
+ (m_config_parse_mp_command_line())
+ * check help options etc. (call handle_help_options()), possibly exit
+ * call mp_play_files() function that works down the playlist:
+ * run idle loop (idle_loop()), until there are files in the
+ playlist or an exit command was given (only if --idle it set)
+ * actually load and play a file in play_current_file():
+ * run all the dozens of functions to load the file and
+ initialize playback
+ * run a small loop that does normal playback, until the file is
+ done or a command terminates playback
+ (on each iteration, run_playloop() is called, which is rather
+ big and complicated - it decodes some audio and video on
+ each frame, waits for input, etc.)
+ * uninitialize playback
+ * determine next entry on the playlist to play
+ * loop, or exit if no next file or quit is requested
+ (see enum stop_play_reason)
+ * call mp_destroy()
+ * run_playloop():
+ * calls fill_audio_out_buffers()
+ This checks whether new audio needs to be decoded, and pushes it
+ to the AO.
+ * calls write_video()
+ Decode new video, and push it to the VO.
+ * determines whether playback of the current file has ended
+ * determines when to start playback after seeks
+ * and calls a whole lot of other stuff
+ (Really, this function does everything.)
+
+ Things worth saying about the playback core:
+ - most state is in MPContext (core.h), which is not available to the
+ subsystems (and should not be made available)
+ - the currently played tracks are in mpctx->current_tracks, and decoder
+ state in track.dec/d_sub
+ - the other subsystems rarely call back into the frontend, and the frontend
+ polls them instead (probably a good thing)
+ - one exceptions are wakeup callbacks, which notify a "higher" component
+ of a changed situation in a subsystem
+
+ I like to call the player/*.c files the "frontend".
+
+ta.h & ta.c:
+ Hierarchical memory manager inspired by talloc from Samba. It's like a
+ malloc() with more features. Most importantly, each talloc allocation can
+ have a parent, and if the parent is free'd, all children will be free'd as
+ well. The parent is an arbitrary talloc allocation. It's either set by the
+ allocation call by passing a talloc parent, usually as first argument to the
+ allocation function. It can also be set or reset later by other calls (at
+ least talloc_steal()). A talloc allocation that is used as parent is often
+ called a talloc context.
+
+ One very useful feature of talloc is fast tracking of memory leaks. ("Fast"
+ as in it doesn't require valgrind.) You can enable it by setting the
+ MPV_LEAK_REPORT environment variable to "1":
+ export MPV_LEAK_REPORT=1
+ Or permanently by building with --enable-ta-leak-report.
+ This will list all unfree'd allocations on exit.
+
+ Documentation can be found here:
+ http://git.samba.org/?p=samba.git;a=blob;f=lib/talloc/talloc.h;hb=HEAD
+
+ For some reason, we're still using API-compatible wrappers instead of TA
+ directly. The talloc wrapper has only a subset of the functionality, and
+ in particular the wrappers abort() on memory allocation failure.
+
+ Note: unlike tcmalloc, jemalloc, etc., talloc() is not actually a malloc
+ replacement. It works on top of system malloc and provides additional
+ features that are supposed to make memory management easier.
+
+player/command.c:
+ This contains the implementation for client API commands and properties.
+ Properties are essentially dynamic variables changed by certain commands.
+ This is basically responsible for all user commands, like initiating
+ seeking, switching tracks, etc. It calls into other player/*.c files,
+ where most of the work is done, but also calls other parts of mpv.
+
+player/core.h:
+ Data structures and function prototypes for most of player/*.c. They are
+ usually not accessed by other parts of mpv for the sake of modularization.
+
+player/client.c:
+ This implements the client API (libmpv/client.h). For the most part, this
+ just calls into other parts of the player. This also manages a ringbuffer
+ of events from player to clients.
+
+options/options.h, options/options.c
+ options.h contains the global option struct MPOpts. The option declarations
+ (option names, types, and MPOpts offsets for the option parser) are in
+ options.c. Most default values for options and MPOpts are in
+ mp_default_opts at the end of options.c.
+
+ MPOpts is unfortunately quite monolithic, but is being incrementally broken
+ up into sub-structs. Many components have their own sub-option structs
+ separate from MPOpts. New options should be bound to the component that uses
+ them. Add a new option table/struct if needed.
+
+ The global MPOpts still contains the sub-structs as fields, which serves to
+ link them to the option parser. For example, an entry like this may be
+ typical:
+
+ {"", OPT_SUBSTRUCT(demux_opts, demux_conf)},
+
+ This directs the option access code to include all options in demux_conf
+ into the global option list, with no prefix (""), and as part of the
+ MPOpts.demux_opts field. The MPOpts.demux_opts field is actually not
+ accessed anywhere, and instead demux.c does this:
+
+ struct m_config_cache *opts_cache =
+ m_config_cache_alloc(demuxer, global, &demux_conf);
+ struct demux_opts *opts = opts_cache->opts;
+
+ ... to get a copy of its options.
+
+ See m_config.h (below) how to access options.
+
+ The actual option parser is spread over m_option.c, m_config.c, and
+ parse_commandline.c, and uses the option table in options.c.
+
+options/m_config.h & m_config.c:
+ Code for querying and managing options. This (unfortunately) contains both
+ declarations for the "legacy-ish" global m_config struct, and ways to access
+ options in a threads-safe way anywhere, like m_config_cache_alloc().
+
+ m_config_cache_alloc() lets anyone read, observe, and write options in any
+ thread. The only state it needs is struct mpv_global, which is an opaque
+ type that can be passed "down" the component hierarchy. For safety reasons,
+ you should not pass down any pointers to option structs (like MPOpts), but
+ instead pass down mpv_global, and use m_config_cache_alloc() (or similar)
+ to get a synchronized copy of the options.
+
+input/input.c:
+ This translates keyboard input coming from VOs and other sources (such
+ as remote control devices like Apple IR or client API commands) to the
+ key bindings listed in the user's (or the builtin) input.conf and turns
+ them into items of type struct mp_cmd. These commands are queued, and read
+ by playloop.c. They get pushed with run_command() to command.c.
+
+ Note that keyboard input and commands used by the client API are the same.
+ The client API only uses the command parser though, and has its own queue
+ of input commands somewhere else.
+
+common/msg.h:
+ All terminal output must go through mp_msg().
+
+stream/*:
+ File input is implemented here. stream.h/.c provides a simple stream based
+ interface (like reading a number of bytes at a given offset). mpv can
+ also play from http streams and such, which is implemented here.
+
+ E.g. if mpv sees "http://something" on the command line, it will pick
+ stream_lavf.c based on the prefix, and pass the rest of the filename to it.
+
+ Some stream inputs are quite special: stream_dvd.c turns DVDs into mpeg
+ streams (DVDs are actually a bunch of vob files etc. on a filesystem),
+ stream_tv.c provides TV input including channel switching.
+
+ Some stream inputs are just there to invoke special demuxers, like
+ stream_mf.c. (Basically to make the prefix "mf://" do something special.)
+
+demux/:
+ Demuxers split data streams into audio/video/sub streams, which in turn
+ are split in packets. Packets (see demux_packet.h) are mostly byte chunks
+ tagged with a playback time (PTS). These packets are passed to the decoders.
+
+ Most demuxers have been removed from this fork, and the only important and
+ "actual" demuxers left are demux_mkv.c and demux_lavf.c (uses libavformat).
+ There are some pseudo demuxers like demux_cue.c.
+
+ The main interface is in demux.h. The stream headers are in stheader.h.
+ There is a stream header for each audio/video/sub stream, and each of them
+ holds codec information about the stream and other information.
+
+ demux.c is a bit big, the main reason being that it contains the demuxer
+ cache, which is implemented as a list of packets. The cache is complex
+ because it support seeking, multiple ranges, prefetching, and so on.
+
+video/:
+ This contains several things related to audio/video decoding, as well as
+ video filters.
+
+ mp_image.h and img_format.h define how mpv stores decoded video frames
+ internally.
+
+video/decode/:
+ vd_*.c are video decoders. (There's only vd_lavc.c left.) dec_video.c
+ handles most of connecting the frontend with the actual decoder.
+
+video/filter/:
+ vf_*.c and vf.c form the video filter chain. They are fed by the video
+ decoder, and output the filtered images to the VOs though vf_vo.c. By
+ default, no video filters (except vf_vo) are used. vf_scale is automatically
+ inserted if the video output can't handle the video format used by the
+ decoder.
+
+video/out/:
+ Video output. They also create GUI windows and handle user input. In most
+ cases, the windowing code is shared among VOs, like x11_common.c for X11 and
+ w32_common.c for Windows. The VOs stand between frontend and windowing code.
+ vo_gpu can pick a windowing system at runtime, e.g. the same binary can
+ provide both X11 and Cocoa support on OSX.
+
+ VOs can be reconfigured at runtime. A vo_reconfig() call can change the video
+ resolution and format, without destroying the window.
+
+ vo_gpu should be taken as reference.
+
+audio/:
+ format.h/format.c define the uncompressed audio formats. (As well as some
+ compressed formats used for spdif.)
+
+audio/decode/:
+ ad_*.c and dec_audio.c handle audio decoding. ad_lavc.c is the
+ decoder using ffmpeg. ad_spdif.c is not really a decoder, but is used for
+ compressed audio passthrough.
+
+audio/filter/:
+ Audio filter chain. af_lavrresample is inserted if any form of conversion
+ between audio formats is needed.
+
+audio/out/:
+ Audio outputs.
+
+ Unlike VOs, AOs can't be reconfigured on a format change. On audio format
+ changes, the AO will simply be closed and re-opened.
+
+ There are wrappers to support for two types of audio APIs: push.c and
+ pull.c. ao.c calls into one of these. They contain generic code to deal
+ with the data flow these APIs impose.
+
+ Note that mpv synchronizes the video to the audio. That's the reason
+ why buggy audio drivers can have a bad influence on playback quality.
+
+sub/:
+ Contains subtitle and OSD rendering.
+
+ osd.c/.h is actually the OSD code. It queries dec_sub.c to retrieve
+ decoded/rendered subtitles. osd_libass.c is the actual implementation of
+ the OSD text renderer (which uses libass, and takes care of all the tricky
+ fontconfig/freetype API usage and text layouting).
+
+ The VOs call osd.c to render OSD and subtitle (via e.g. osd_draw()). osd.c
+ in turn asks dec_sub.c for subtitle overlay bitmaps, which relays the
+ request to one of the sd_*.c subtitle decoders/renderers.
+
+ Subtitle loading is in demux/. The MPlayer subreader.c is mostly gone - parts
+ of it survive in demux_subreader.c. It's used as last fallback, or to handle
+ some text subtitle types on Libav. It should go away eventually. Normally,
+ subtitles are loaded via demux_lavf.c.
+
+ The subtitles are passed to dec_sub.c and the subtitle decoders in sd_*.c
+ as they are demuxed. All text subtitles are rendered by sd_ass.c. If text
+ subtitles are not in the ASS format, the libavcodec subtitle converters are
+ used (lavc_conv.c).
+
+ Text subtitles can be preloaded, in which case they are read fully as soon
+ as the subtitle is selected. In this case, they are effectively stored in
+ sd_ass.c's internal state.
+
+etc/:
+ The file input.conf is actually integrated into the mpv binary by the
+ build system. It contains the default keybindings.
+
+Best practices and Concepts within mpv
+======================================
+
+General contribution etc.
+-------------------------
+
+See: DOCS/contribute.md
+
+Error checking
+--------------
+
+If an error is relevant, it should be handled. If it's interesting, log the
+error. However, mpv often keeps errors silent and reports failures somewhat
+coarsely by propagating them upwards the caller chain. This is OK, as long as
+the errors are not very interesting, or would require a developer to debug it
+anyway (in which case using a debugger would be more convenient, and the
+developer would need to add temporary debug printfs to get extremely detailed
+information which would not be appropriate during normal operation).
+
+Basically, keep a balance on error reporting. But always check them, unless you
+have a good argument not to.
+
+Memory allocation errors (OOM) are a special class of errors. Normally such
+allocation failures are not handled "properly". Instead, abort() is called.
+(New code should use MP_HANDLE_OOM() for this.) This is done out of laziness and
+for convenience, and due to the fact that MPlayer/mplayer2 never handled it
+correctly. (MPlayer varied between handling it correctly, trying to do so but
+failing, and just not caring, while mplayer2 started using abort() for it.)
+
+This is justifiable in a number of ways. Error handling paths are notoriously
+untested and buggy, so merely having them won't make your program more reliable.
+Having these error handling paths also complicates non-error code, due to the
+need to roll back state at any point after a memory allocation.
+
+Take any larger body of code, that is supposed to handle OOM, and test whether
+the error paths actually work, for example by overriding malloc with a version
+that randomly fails. You will find bugs quickly, and often they will be very
+annoying to fix (if you can even reproduce them).
+
+In addition, a clear indication that something went wrong may be missing. On
+error your program may exhibit "degraded" behavior by design. Consider a video
+encoder dropping frames somewhere in the middle of a video due to temporary
+allocation failures, instead of just exiting with an errors. In other cases, it
+may open conceptual security holes. Failing fast may be better.
+
+mpv uses GPU APIs, which may be break on allocation errors (because driver
+authors will have the same issues as described here), or don't even have a real
+concept for dealing with OOM (OpenGL).
+
+libmpv is often used by GUIs, which I predict always break if OOM happens.
+
+Last but not least, OSes like Linux use "overcommit", which basically means that
+your program may crash any time OOM happens, even if it doesn't use malloc() at
+all!
+
+But still, don't just assume malloc() always succeeds. Use MP_HANDLE_OOM(). The
+ta* APIs do this for you. The reason for this is that dereferencing a NULL
+pointer can have security relevant consequences if large offsets are involved.
+Also, a clear error message is better than a random segfault.
+
+Some big memory allocations are checked anyway. For example, all code must
+assume that allocating video frames or packets can fail. (The above example
+of dropping video frames during encoding is entirely possible in mpv.)
+
+Undefined behavior
+------------------
+
+Undefined behavior (UB) is a concept in the C language. C is famous for being a
+language that makes it almost impossible to write working code, because
+undefined behavior is so easily triggered, compilers will happily abuse it to
+generate "faster" code, debugging tools will shout at you, and sometimes it
+even means your code doesn't work.
+
+There is a lot of literature on this topic. Read it.
+
+(In C's defense, UB exists in other languages too, but since they're not used
+for low level infrastructure, and/or these languages are at times not rigorously
+defined, simply nobody cares. However, the C standard committee is still guilty
+for not addressing this. I'll admit that I can't even tell from the standard's
+gibberish whether some specific behavior is UB or not. It's written like tax
+law.)
+
+In mpv, we generally try to avoid undefined behavior. For one, we want portable
+and reliable operation. But more importantly, we want clean output from
+debugging tools, in order to find real bugs more quickly and effectively.
+
+Avoid the "works in practice" argument. Once debugging tools come into play, or
+simply when "in practice" stops being true, this will all get back to you in a
+bad way.
+
+Global state, library safety
+----------------------------
+
+Mutable global state is when code uses global variables that are not read-only.
+This must be avoided in mpv. Always use context structs that the caller of
+your code needs to allocate, and whose pointers are passed to your functions.
+
+Library safety means that your code (or library) can be used by a library
+without causing conflicts with other library users in the same process. To any
+piece of code, a "safe" library's API can simply be used, without having to
+worry about other API users that may be around somewhere.
+
+Libraries are often not library safe, because they use global mutable state
+or other "global" resources. Typical examples include use of signals, simple
+global variables (like hsearch() in libc), or internal caches not protected by
+locks.
+
+A surprisingly high number of libraries are not library safe because they need
+global initialization. Typically they provide an API function, which
+"initializes" the library, and which must be called before calling any other
+API functions. Often, you are to provide global configuration parameters, which
+can change the behavior of the library. If two libraries A and B use library C,
+but A and B initialize C with different parameters, something "bad" may happen.
+In addition, these global initialization functions are often not thread-safe. So
+if A and B try to initialize C at the same time (from different threads and
+without knowing about each other), it may cause undefined behavior. (libcurl is
+a good example of both of these issues. FFmpeg and some TLS libraries used to be
+affected, but improved.)
+
+This is so bad because library A and B from the previous example most likely
+have no way to cooperate, because they're from different authors and have no
+business knowing each others. They'd need a library D, which wraps library C
+in a safe way. Unfortunately, typically something worse happens: libraries get
+"infected" by the unsafeness of its sub-libraries, and export a global init API
+just to initialize the sub-libraries. In the previous example, libraries A and B
+would export global init APIs just to init library C, even though the rest of
+A/B are clean and library safe. (Again, libcurl is an example of this, if you
+subtract other historic anti-features.)
+
+The main problem with library safety is that its lack propagates to all
+libraries using the library.
+
+We require libmpv to be library safe. This is not really possible, because some
+libraries are not library safe (FFmpeg, Xlib, partially ALSA). However, for
+ideological reasons, there is no global init API, and best effort is made to try
+to avoid problems.
+
+libmpv has some features that are not library safe, but which are disabled by
+default (such as terminal usage aka stdout, or JSON IPC blocking SIGPIPE for
+internal convenience).
+
+A notable, very disgustingly library unsafe behavior of libmpv is calling
+abort() on some memory allocation failure. See error checking section.
+
+Logging
+-------
+
+All logging and terminal output in mpv goes through the functions and macros
+provided in common/msg.h. This is in part for library safety, and in part to
+make sure users can silence all output, or to redirect the output elsewhere,
+like a log file or the internal console.lua script.
+
+Locking
+-------
+
+See generally available literature. In mpv, we use mp_thread for this.
+
+Always keep locking clean. Don't skip locking just because it will work "in
+practice". (See undefined behavior section.) If your use case is simple, you may
+use C11 atomics, but most likely you will only hurt yourself and others.
+
+Always make clear which fields in a struct are protected by which lock. If a
+field is immutable, or simply not thread-safe (e.g. state for a single worker
+thread), document it as well.
+
+Internal mpv APIs are assumed to be not thread-safe by default. If they have
+special guarantees (such as being usable by more than one thread at a time),
+these should be explicitly documented.
+
+All internal mpv APIs must be free of global state. Even if a component is not
+thread-safe, multiple threads can use _different_ instances of it without any
+locking.
+
+On a side note, recursive locks may seem convenient at first, but introduce
+additional problems with condition variables and locking hierarchies. They
+should be avoided.
+
+Locking hierarchy
+-----------------
+
+A simple way to avoid deadlocks with classic locking is to define a locking
+hierarchy or lock order. If all threads acquire locks in the same order, no
+deadlocks will happen.
+
+For example, a "leaf" lock is a lock that is below all other locks in the
+hierarchy. You can acquire it any time, as long as you don't acquire other
+locks while holding it.
+
+Unfortunately, C has no way to declare or check the lock order, so you should at
+least document it.
+
+In addition, try to avoid exposing locks to the outside. Making the declaration
+of a lock private to a specific .c file (and _not_ exporting accessors or
+lock/unlock functions that manipulate the lock) is a good idea. Your component's
+API may acquire internal locks, but should release them when returning. Keeping
+the entire locking in a single file makes it easy to check it.
+
+Avoiding callback hell
+----------------------
+
+mpv code is separated in components, like the "frontend" (i.e. MPContext mpctx),
+VOs, AOs, demuxers, and more. The frontend usually calls "down" the usage
+hierarchy: mpctx almost on top, then things like vo/ao, and utility code on the
+very bottom.
+
+"Callback hell" is when components call both up and down the hierarchy,
+which for example leads to accidentally recursion, reentrancy problems, or
+locking nightmares. This is avoided by (mostly) calling only down the hierarchy.
+Basically the call graph forms a DAG. The other direction is handled by event
+queues, wakeup callbacks, and similar mechanisms.
+
+Typically, a component provides an API, and does not know anything about its
+user. The API user (component higher in the hierarchy) polls the state of the
+lower component when needed.
+
+This also enforces some level of modularization, and with some luck the locking
+hierarchy. (Basically, locks of lower components automatically become leaf
+locks.) Another positive effect is simpler memory management.
+
+(Also see e.g.: http://250bpm.com/blog:24)
+
+Wakeup callbacks
+----------------
+
+This is a common concept in mpv. Even the public API uses it. It's used when an
+API has internal threads (or otherwise triggers asynchronous events), but the
+component call hierarchy needs to be kept. The wakeup callback is the only
+exception to the call hierarchy, and always calls up.
+
+For example, vo spawns a thread that the API user (the mpv frontend) does not
+need to know about. vo simply provides a single-threaded API (or that looks like
+one). This API needs a way to notify the API user of new events. But the vo
+event producer is on the vo thread - it can't simply invoke a callback back into
+the API user, because then the API user has to deal with locking, despite not
+using threads. In addition, this will probably cause problems like mentioned in
+the "callback hell" section, especially lock order issues.
+
+The solution is the wakeup callback. It merely unblocks the API user from
+waiting, and the API user then uses the normal vo API to examine whether or
+which state changed. As a concept, it documents what a wakeup callback is
+allowed to do and what not, to avoid the aforementioned problems.
+
+Generally, you are not allowed to call any API from the wakeup callback. You
+just do whatever is needed to unblock your thread. For example, if it's waiting
+on a mutex/condition variable, acquire the mutex, set a change flag, signal
+the condition variable, unlock, return. (This mutex must not be held when
+calling the API. It must be a leaf lock.)
+
+Restricting the wakeup callback like this sidesteps any reentrancy issues and
+other complexities. The API implementation can simply hold internal (and
+non-recursive) locks while invoking the wakeup callback.
+
+The API user still needs to deal with locking (probably), but there's only the
+need to implement a single "receiver", that can handle the entire API of the
+used component. (Or multiple APIs - MPContext for example has only 1 wakeup
+callback that handles all AOs, VOs, input, demuxers, and more. It simple re-runs
+the playloop.)
+
+You could get something more advanced by turning this into a message queue. The
+API would append a message to the queue, and the API user can read it. But then
+you still need a way to "wakeup" the API user (unless you force the API user
+to block on your API, which will make things inconvenient for the API user). You
+also need to worry about what happens if the message queue overruns (you either
+lose messages or have unbounded memory usage). In the mpv public API, the
+distinction between message queue and wakeup callback is sort of blurry, because
+it does provide a message queue, but an additional wakeup callback, so API
+users are not required to call mpv_wait_event() with a high timeout.
+
+mpv itself prefers using wakeup callbacks over a generic event queue, because
+most times an event queue is not needed (or complicates things), and it is
+better to do it manually.
+
+(You could still abstract the API user side of wakeup callback handling, and
+avoid reimplementing it all the time. Although mp_dispatch_queue already
+provides mechanisms for this.)
+
+Condition variables
+-------------------
+
+They're used whenever a thread needs to wait for something, without nonsense
+like sleep calls or busy waiting. mpv uses the mp_thread API for this.
+There's a lot of literature on condition variables, threading in general. Read it.
+
+For initial understanding, it may be helpful to know that condition variables
+are not variables that signal a condition. mp_cond does not have any
+state per-se. Maybe mp_cond would better be named mp_interrupt,
+because its sole purpose is to interrupt a thread waiting via mp_cond_wait()
+(or similar). The "something" in "waiting for something" can be called
+predicate (to avoid confusing it with "condition"). Consult literature for the
+proper terms.
+
+The very short version is...
+
+Shared declarations:
+
+ mp_mutex lock;
+ mp_cond cond_var;
+ struct something state_var; // protected by lock, changes signaled by cond_var
+
+Waiter thread:
+
+ mp_mutex_lock(&lock);
+
+ // Wait for a change in state_var. We want to wait until predicate_fulfilled()
+ // returns true.
+ // Must be a loop for 2 reasons:
+ // 1. cond_var may be associated with other conditions too
+ // 2. mp_cond_wait() can have sporadic wakeups
+ while (!predicate_fulfilled(&state_var)) {
+ // This unlocks, waits for cond_var to be signaled, and then locks again.
+ // The _whole_ point of cond_var is that unlocking and waiting for the
+ // signal happens atomically.
+ mp_cond_wait(&cond_var, &lock);
+ }
+
+ // Here you may react to the state change. The state cannot change
+ // asynchronously as long as you still hold the lock (and didn't release
+ // and reacquire it).
+ // ...
+
+ mp_mutex_unlock(&lock);
+
+Signaler thread:
+
+ mp_mutex_lock(&lock);
+
+ // Something changed. Update the shared variable with the new state.
+ update_state(&state_var);
+
+ // Notify that something changed. This will wake up the waiter thread if
+ // it's blocked in mp_cond_wait(). If not, nothing happens.
+ mp_cond_broadcast(&cond_var);
+
+ // Fun fact: good implementations wake up the waiter only when the lock is
+ // released, to reduce kernel scheduling overhead.
+ mp_mutex_unlock(&lock);
+
+Some basic rules:
+ 1. Always access your state under proper locking
+ 2. Always check your predicate before every call to mp_cond_wait()
+ (And don't call mp_cond_wait() if the predicate is fulfilled.)
+ 3. Always call mp_cond_wait() in a loop
+ (And only if your predicate failed without releasing the lock..)
+ 4. Always call mp_cond_broadcast()/_signal() inside of its associated
+ lock
+
+mpv sometimes violates rule 3, and leaves "retrying" (i.e. looping) to the
+caller.
+
+Common pitfalls:
+ - Thinking that mp_cond is some kind of semaphore, or holds any
+ application state or the user predicate (it _only_ wakes up threads
+ that are at the same time blocking on mp_cond_wait() and friends,
+ nothing else)
+ - Changing the predicate, but not updating all mp_cond_broadcast()/
+ _signal() calls correctly
+ - Forgetting that mp_cond_wait() unlocks the lock (other threads can
+ and must acquire the lock)
+ - Holding multiple nested locks while trying to wait (=> deadlock, violates
+ the lock order anyway)
+ - Waiting for a predicate correctly, but unlocking/relocking before acting
+ on it (unlocking allows arbitrary state changes)
+ - Confusing which lock/condition var. is used to manage a bit of state
+
+Generally available literature probably has better examples and explanations.
+
+Using condition variables the proper way is generally preferred over using more
+messy variants of them. (Just saying because on win32, "SetEvent" exists, and
+it's inferior to condition variables. Try to avoid the win32 primitives, even if
+you're dealing with Windows-only code.)
+
+Threads
+-------
+
+Threading should be conservatively used. Normally, mpv code pretends to be
+single-threaded, and provides thread-unsafe APIs. Threads are used coarsely,
+and if you can avoid messing with threads, you should. For example, VOs and AOs
+do not need to deal with threads normally, even though they run on separate
+threads. The glue code "isolates" them from any threading issues.
diff --git a/LICENSE.GPL b/LICENSE.GPL
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE.GPL
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/LICENSE.LGPL b/LICENSE.LGPL
new file mode 100644
index 0000000..4362b49
--- /dev/null
+++ b/LICENSE.LGPL
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4a454d4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,208 @@
+![mpv logo](https://raw.githubusercontent.com/mpv-player/mpv.io/master/source/images/mpv-logo-128.png)
+
+# mpv
+
+
+* [External links](#external-links)
+* [Overview](#overview)
+* [System requirements](#system-requirements)
+* [Downloads](#downloads)
+* [Changelog](#changelog)
+* [Compilation](#compilation)
+* [Release cycle](#release-cycle)
+* [Bug reports](#bug-reports)
+* [Contributing](#contributing)
+* [License](#license)
+* [Contact](#contact)
+
+
+## External links
+
+
+* [Wiki](https://github.com/mpv-player/mpv/wiki)
+* [FAQ][FAQ]
+* [Manual](https://mpv.io/manual/master/)
+
+
+## Overview
+
+
+**mpv** is a free (as in freedom) media player for the command line. It supports
+a wide variety of media file formats, audio and video codecs, and subtitle types.
+
+There is a [FAQ][FAQ].
+
+Releases can be found on the [release list][releases].
+
+## System requirements
+
+- A not too ancient Linux, Windows 10 or later, or macOS 10.15 or later.
+- A somewhat capable CPU. Hardware decoding might help if the CPU is too slow to
+ decode video in realtime, but must be explicitly enabled with the `--hwdec`
+ option.
+- A not too crappy GPU. mpv's focus is not on power-efficient playback on
+ embedded or integrated GPUs (for example, hardware decoding is not even
+ enabled by default). Low power GPUs may cause issues like tearing, stutter,
+ etc. The main video output uses shaders for video rendering and scaling,
+ rather than GPU fixed function hardware. On Windows, you might want to make
+ sure the graphics drivers are current. In some cases, ancient fallback video
+ output methods can help (such as `--vo=xv` on Linux), but this use is not
+ recommended or supported.
+
+mpv does not go out of its way to break on older hardware or old, unsupported
+operating systems, but development is not done with them in mind. Keeping
+compatibility with such setups is not guaranteed. If things work, consider it
+a happy accident.
+
+## Downloads
+
+
+For semi-official builds and third-party packages please see
+[mpv.io/installation](https://mpv.io/installation/).
+
+## Changelog
+
+
+There is no complete changelog; however, changes to the player core interface
+are listed in the [interface changelog][interface-changes].
+
+Changes to the C API are documented in the [client API changelog][api-changes].
+
+The [release list][releases] has a summary of most of the important changes
+on every release.
+
+Changes to the default key bindings are indicated in
+[restore-old-bindings.conf][restore-old-bindings].
+
+## Compilation
+
+
+Compiling with full features requires development files for several
+external libraries. Mpv requires [meson](https://mesonbuild.com/index.html)
+to build. Meson can be obtained from your distro or PyPI.
+
+After creating your build directory (e.g. `meson setup build`), you can view a list
+of all the build options via `meson configure build`. You could also just simply
+look at the `meson_options.txt` file. Logs are stored in `meson-logs` within
+your build directory.
+
+Example:
+
+ meson setup build
+ meson compile -C build
+ meson install -C build
+
+Essential dependencies (incomplete list):
+
+- gcc or clang
+- X development headers (xlib, xrandr, xext, xscrnsaver, xpresent, libvdpau,
+ libGL, GLX, EGL, xv, ...)
+- Audio output development headers (libasound/ALSA, pulseaudio)
+- FFmpeg libraries (libavutil libavcodec libavformat libswscale libavfilter
+ and either libswresample or libavresample)
+- libplacebo
+- zlib
+- iconv (normally provided by the system libc)
+- libass (OSD, OSC, text subtitles)
+- Lua (optional, required for the OSC pseudo-GUI and youtube-dl integration)
+- libjpeg (optional, used for screenshots only)
+- uchardet (optional, for subtitle charset detection)
+- nvdec and vaapi libraries for hardware decoding on Linux (optional)
+
+Libass dependencies (when building libass):
+
+- gcc or clang, yasm on x86 and x86_64
+- fribidi, freetype, fontconfig development headers (for libass)
+- harfbuzz (required for correct rendering of combining characters, particularly
+ for correct rendering of non-English text on OSX, and Arabic/Indic scripts on
+ any platform)
+
+FFmpeg dependencies (when building FFmpeg):
+
+- gcc or clang, yasm on x86 and x86_64
+- OpenSSL or GnuTLS (have to be explicitly enabled when compiling FFmpeg)
+- libx264/libmp3lame/libfdk-aac if you want to use encoding (have to be
+ explicitly enabled when compiling FFmpeg)
+- For native DASH playback, FFmpeg needs to be built with --enable-libxml2
+ (although there are security implications, and DASH support has lots of bugs).
+- AV1 decoding support requires dav1d.
+- For good nvidia support on Linux, make sure nv-codec-headers is installed
+ and can be found by configure.
+
+Most of the above libraries are available in suitable versions on normal
+Linux distributions. For ease of compiling the latest git master of everything,
+you may wish to use the separately available build wrapper ([mpv-build][mpv-build])
+which first compiles FFmpeg libraries and libass, and then compiles the player
+statically linked against those.
+
+If you want to build a Windows binary, you either have to use MSYS2 and MinGW,
+or cross-compile from Linux with MinGW. See
+[Windows compilation][windows_compilation].
+
+
+## Release cycle
+
+Once or twice a year, a release is cut off from the current development state
+and is assigned a 0.X.0 version number. No further maintenance is done, except
+in the event of security issues.
+
+The goal of releases is to make Linux distributions happy. Linux distributions
+are also expected to apply their own patches in case of bugs.
+
+Releases other than the latest release are unsupported and unmaintained.
+
+See the [release policy document][release-policy] for more information.
+
+## Bug reports
+
+
+Please use the [issue tracker][issue-tracker] provided by GitHub to send us bug
+reports or feature requests. Follow the template's instructions or the issue
+will likely be ignored or closed as invalid.
+
+Using the bug tracker as place for simple questions is fine but IRC is
+recommended (see [Contact](#Contact) below).
+
+## Contributing
+
+
+Please read [contribute.md][contribute.md].
+
+For small changes you can just send us pull requests through GitHub. For bigger
+changes come and talk to us on IRC before you start working on them. It will
+make code review easier for both parties later on.
+
+You can check [the wiki](https://github.com/mpv-player/mpv/wiki/Stuff-to-do)
+or the [issue tracker](https://github.com/mpv-player/mpv/issues?q=is%3Aopen+is%3Aissue+label%3Ameta%3Afeature-request)
+for ideas on what you could contribute with.
+
+## License
+
+GPLv2 "or later" by default, LGPLv2.1 "or later" with `-Dgpl=false`.
+See [details.](https://github.com/mpv-player/mpv/blob/master/Copyright)
+
+## History
+
+This software is based on the MPlayer project. Before mpv existed as a project,
+the code base was briefly developed under the mplayer2 project. For details,
+see the [FAQ][FAQ].
+
+## Contact
+
+
+Most activity happens on the IRC channel and the github issue tracker.
+
+- **GitHub issue tracker**: [issue tracker][issue-tracker] (report bugs here)
+- **User IRC Channel**: `#mpv` on `irc.libera.chat`
+- **Developer IRC Channel**: `#mpv-devel` on `irc.libera.chat`
+
+[FAQ]: https://github.com/mpv-player/mpv/wiki/FAQ
+[releases]: https://github.com/mpv-player/mpv/releases
+[mpv-build]: https://github.com/mpv-player/mpv-build
+[issue-tracker]: https://github.com/mpv-player/mpv/issues
+[release-policy]: https://github.com/mpv-player/mpv/blob/master/DOCS/release-policy.md
+[windows_compilation]: https://github.com/mpv-player/mpv/blob/master/DOCS/compile-windows.md
+[interface-changes]: https://github.com/mpv-player/mpv/blob/master/DOCS/interface-changes.rst
+[api-changes]: https://github.com/mpv-player/mpv/blob/master/DOCS/client-api-changes.rst
+[restore-old-bindings]: https://github.com/mpv-player/mpv/blob/master/etc/restore-old-bindings.conf
+[contribute.md]: https://github.com/mpv-player/mpv/blob/master/DOCS/contribute.md
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
new file mode 100644
index 0000000..fe2efb7
--- /dev/null
+++ b/RELEASE_NOTES
@@ -0,0 +1,214 @@
+Release 0.37.0
+==============
+
+This release requires FFmpeg 4.4 or newer and libplacebo 6.338.0 or newer.
+
+This is the first release to unconditionally require libplacebo, but note that
+the new improved renderer (vo_gpu_next) is not yet the default.
+
+
+Features
+--------
+
+New
+~~~
+
+- ao_oss: add SPDIF passthrough support
+- hwtransfer: implement support for HW->HW format conversions
+- stream/dvbin: add support for delivery system ISDB-T
+- audio/chmap: support up to 64 channels (including 22.2 layout)
+- libmpv: add mpv_time_ns()
+- vo_gpu, vo_gpu_next: add Vulkan support for macOS
+- meson: make libplacebo a required dependency
+- hwdec: support videotoolbox hwdec with libplacebo
+
+
+Changed
+~~~~~~~
+
+- msg: print warning and error messages to stderr
+- options: restore old default subtitle selection behavior
+- input.conf: swap wheel up/down with wheel left/right
+
+
+Removed
+~~~~~~~
+
+- waf: remove waf as a build system
+- osc.lua: remove toggle for forced only subpictures (appeared as [F])
+- mac: remove runtime checks and compatibility for macOS older than 10.15
+- cocoa: remove deprecated OpenGL cocoa backend
+
+
+Options and Commands
+--------------------
+
+Added
+~~~~~
+
+- vo_gpu_next: add --hdr-peak-percentile
+- player: add --term-remaining-playtime option
+- x11: add --x11-wid-title option
+- vo_gpu_next: add --libplacebo-opts
+- player: add --subs-match-os-language option (replaces 'auto' option)
+- vo: add --video-crop
+- win32: add --window-corners, --window-affinity, --title-bar, --backdrop-type
+- sub: add --sub-stretch-durations option
+
+
+Changed
+~~~~~~~
+
+- builtin.conf: add --hdr-peak-percentile=99.995 to gpu-hq profile
+- player: add 'always' option to --subs-fallback-forced
+- demux_playlist: default to --directory-mode=lazy
+- builtin.conf: add --allow-delayed-peak-detect=no to gpu-hq profile
+- vo_gpu, vo_gpu_next: support --icc-3dlut-size=auto
+- demux: prepend some cache options with --demuxer-
+- builtin.conf: modernize internal profiles for higher quality rendering by default,
+ rename 'gpu-hq' profile to 'high-quality', add 'fast' profile
+- vo_gpu, vo_gpu_next: default to dscale=hermite
+- builtin.conf: remove deprecated 'opengl-hq' profile
+- options: remove a bunch of old option fallbacks/deprecated ones
+- vo_gpu: allow --deband-iterations to be 0
+- stream_cdda: deprecate --cdda-toc-bias and always check for offsets
+- options: disable --allow-delayed-peak-detect by default
+- options: adjust default of --watch-later-options
+
+
+Deprecated
+~~~~~~~~~~
+
+- command: deprecate shared-script-properties
+- demux_cue: deprecate --demuxer-cue-codepage for --metadata-codepage
+
+
+Removed
+~~~~~~~
+
+- player: remove special 'auto' option from alang/slang/vlang (previous default)
+- vo_gpu: remove --tone-mapping-mode
+- vo_gpu: remove --scale-wblur, --scale-cutoff etc.
+- vo_gpu: remove --scaler-lut-size
+- m_option: drop support for -del for list options
+
+
+Fixes and Minor Enhancements
+----------------------------
+
+- build: remove unneeded libdl requirement for vaapi
+- zimg: fix abort on subsampled input with odd heights
+- video_writer: fix gamma for YUV screenshots
+- player/video: fix possible crash when changing lavfi-complex
+- ad_spdif: fix segfault due to early deallocation
+- ao_pipewire: fix race conditon with setting the media role
+- draw_bmp: fix overflowing coordinates in mark_rcs
+- ao_sndio: use sio_flush() to improve controls responsiveness
+- vo_vdpau: fix hwdec for this vo
+- vo_gpu, vo_gpu_next: fix setting an empty dscale
+- vd_lavc: repeatedly attempt to fallback if hwdec fails in reinit
+- options: fix relative time parsing on negative input
+- win32: signal DPI changes correctly
+- mp_image: properly infer color levels for some pixfmts
+- vo_gpu_next: add ability to use named hook params
+- vo_gpu_next: take into account PAR when taking screenshots
+- ao_audiotrack: support more channel layouts
+- osc.lua: support speed-independent time-remaining display
+- sub: fix switching tracks while paused
+- audio: fix clipping with gapless audio enabled
+- player/video: avoid spamming logs with EOF
+- player/command: detect aspect ratio name and show it in stats.lua
+- wayland: keyboard input fixes
+- demux_playlist: remove len restriction on headerless m3u
+- win32: fix display resolution calculation on mulitple monitors
+- vo_gpu_next: multiple adjustments and fixes to interpolation
+- loadfile: avoid infinite playlist loading loops
+- context_drm_egl: don't free EGL properties if they are null
+- x11: require xrandr 1.4, remove xinerama support
+- drm_common: skip cards that don't have connected outputs
+- win32_common: fixes minimized window being focused on launch
+- ao/jack: set device_buffer to JACK buffer size
+- meson: rename all features with underscores
+- input: add new keys: Back, Tools, ZoomIn, ZoomOut
+- win32: don't ignore --screen and --fs-screen
+- input: add missing keypad key defines and Windows multimedia keys
+- player: use audio pts corresponding to playing audio on EOF
+- command: add sub-ass-extradata property
+- vo_dmabuf_wayland: unmap osd surface when not needed
+- player: always write redirect entries for resuming playback
+- stats.lua: reorganize how video params are displayed
+- stats.lua: display HDR metadata
+- osc.lua: add scrolling to the seekbar
+- demux_lavf: prefer track durations over container durations to determine total
+- vo: vulkan: allow picking devices by UUID
+- video: allow overriding container crop if it is present
+- vo_gpu, vo_gpu_next, screenshot: support applying crop for screenshots
+- sd_lavc: account for floating point inaccuracy, fix sub PTS
+- stream: accept dav:// and davs:// urls
+- filter_kernels: refine some constants
+- filter_kernels: add ewa_lanczos4sharpest
+- osc.lua: add scrolling to audio/sub buttons
+- demux_mkv: support cropping and rotation
+- vo_dmabuf_wayland: support 90 degree rotations
+- filter_kernels: add hermite filter
+- vo: avoid unnecessary redraws when the OSD shows
+- scripting: support DLL cplugins
+- af_scaletempo2: various bug fixes
+- sdl_gamepad: fix button detection on modern controllers
+- vo_dmabuf_wayland: support osd rendering when there's no video
+- demux_playlist: add --directory-mode=auto
+- vo_gpu_next: use proper color for subtitles
+- win32: add an option to change window affinity and control title bar state
+- win32: reduce top border thickness to imitate DWM invisible borders
+- wayland: remove gnome-specific idle-inhibit warning
+- win32: pass window handle to the window-id property
+- osc.lua: fix calculation for slider's min-max average
+- recorder: fix a couple of memory leaks
+- af_scaletempo2: raise max playback rate to 8.0
+- osc.lua: move the idle logo behind other overlays
+- hwdec_drmprime: add nv16 support
+- various: change internal timing code to work in nanoseconds instead of microseconds
+- vo: increase display refresh rate estimation limit from 99 Hz to 400 Hz
+- external_files: base cover-art-whitelist on cover-art-auto-exts
+- path: don't override cache and state paths with --config-dir
+- codec_tags: map some more image mimetypes
+- af/vf-command: add ability to target a specific lavfi filter
+- win32: prevent white window flash on startup
+- demux_playlist: use --metacode-codepage when parsing playlist files
+- video: revert racey change that led to stutter and deadlocking
+- console.lua: various improvements
+- command: add playlist-next-playlist and playlist-prev-playlist
+- ytdl_hook.lua: set metadata with single tracks
+- defaults.lua: add a disabled parameter to timer constructors
+- terminal-unix: race condition fixes
+- af_scaletempo2: better defaults
+- hwtransfer: handle hwcontexts that don't implement frame constraints
+- stream_cdda: remove fallback for ancient libcdio versions
+- osdep: drop support for C11 without atomics
+- dvbin: do a big cleanup on code style and functions
+- ytdl_hook.lua: parse the separate cookies field
+- sub: update subtitles if current track is an image
+- javascript: use --js-memory-report option instead of MPV_LEAK_REPORT
+- ao_coreaudio: signal buffer underruns
+- ytdl_hook.lua: support thumbnails
+- demux: make hysteresis-secs respect cache-secs
+- mp_image: pass rotation correctly to/from AVFrame correctly
+- various: add new internal mp_thread abstraction and use it
+- drm: use present_sync mechanism for presentation feedback
+- vo_gpu: apply ICC profile and dithering only to window screenshots
+- audio: introduce ao_read_data_nonblocking() and use it in relevant backends
+- wayland: obey initial size hints set by the compositor
+- command: export storage aspect ratio (sar) properties
+- vo: delay vsync samples by at least 10 refreshes to improve convergence time
+- vo_sdl: fix broken mouse wheel multiplier
+- vo_gpu_next: simplify cache code and don't re-save when unmodified
+
+
+This listing is not complete. Check DOCS/client-api-changes.rst for a history
+of changes to the client API, and DOCS/interface-changes.rst for a history
+of changes to other user-visible interfaces.
+
+A complete changelog can be seen by running `git log v0.36.0..v0.37.0`
+in the git repository or by visiting either
+https://github.com/mpv-player/mpv/compare/v0.36.0...v0.37.0 or
+https://git.srsfckn.biz/mpv/log/?qt=range&q=v0.36.0..v0.37.0
diff --git a/TOOLS/docutils-wrapper.py b/TOOLS/docutils-wrapper.py
new file mode 100755
index 0000000..31ba976
--- /dev/null
+++ b/TOOLS/docutils-wrapper.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+"""
+Wrapper around docutils rst2x commands,
+converting their dependency files to a format understood by meson/ninja.
+"""
+
+#
+# This file is part of mpv.
+#
+# mpv is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# mpv is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import subprocess
+import sys
+
+
+def convert_depfile(output, depfile):
+ with open(depfile, 'r') as f:
+ deps = f.readlines()
+
+ with open(depfile, 'w') as f:
+ f.write(os.path.abspath(output))
+ f.write(': \\\n')
+ for dep in deps:
+ dep = dep[:-1]
+ f.write('\t')
+ f.write(os.path.abspath(dep))
+ f.write(' \\\n')
+
+def remove(path):
+ try:
+ os.remove(path)
+ except FileNotFoundError:
+ pass
+
+argv = sys.argv[1:]
+
+depfile = None
+output = argv[-1]
+
+for opt, optarg in zip(argv, argv[1:]):
+ if opt == '--record-dependencies':
+ depfile = optarg
+
+try:
+ proc = subprocess.run(argv, check=True)
+ if depfile is not None:
+ convert_depfile(output, depfile)
+except:
+ remove(output)
+ if depfile is not None:
+ remove(depfile)
+ sys.exit(1)
+
+sys.exit(proc.returncode)
diff --git a/TOOLS/dylib-unhell.py b/TOOLS/dylib-unhell.py
new file mode 100755
index 0000000..c41d200
--- /dev/null
+++ b/TOOLS/dylib-unhell.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+
+import re
+import os
+import sys
+import shutil
+import subprocess
+from functools import partial
+
+sys_re = re.compile("^/System")
+usr_re = re.compile("^/usr/lib/")
+exe_re = re.compile("@executable_path")
+
+def is_user_lib(objfile, libname):
+ return not sys_re.match(libname) and \
+ not usr_re.match(libname) and \
+ not exe_re.match(libname) and \
+ not "libobjc." in libname and \
+ not "libSystem." in libname and \
+ not "libc." in libname and \
+ not "libgcc." in libname and \
+ not os.path.basename(libname) == 'Python' and \
+ not os.path.basename(objfile) in libname and \
+ not "libswift" in libname
+
+def otool(objfile, rapths):
+ command = "otool -L '%s' | grep -e '\t' | awk '{ print $1 }'" % objfile
+ output = subprocess.check_output(command, shell = True, universal_newlines=True)
+ libs = set(filter(partial(is_user_lib, objfile), output.split()))
+
+ libs_resolved = set()
+ libs_relative = set()
+ for lib in libs:
+ lib_path = resolve_lib_path(objfile, lib, rapths)
+ libs_resolved.add(lib_path)
+ if lib_path != lib:
+ libs_relative.add(lib)
+
+ return libs_resolved, libs_relative
+
+def get_rapths(objfile):
+ rpaths = []
+ command = "otool -l '%s' | grep -A2 LC_RPATH | grep path" % objfile
+ pathRe = re.compile("^\s*path (.*) \(offset \d*\)$")
+
+ try:
+ result = subprocess.check_output(command, shell = True, universal_newlines=True)
+ except:
+ return rpaths
+
+ for line in result.splitlines():
+ rpaths.append(pathRe.search(line).group(1).strip())
+
+ return rpaths
+
+def get_rpaths_dev_tools(binary):
+ command = "otool -l '%s' | grep -A2 LC_RPATH | grep path | grep \"Xcode\|CommandLineTools\"" % binary
+ result = subprocess.check_output(command, shell = True, universal_newlines=True)
+ pathRe = re.compile("^\s*path (.*) \(offset \d*\)$")
+ output = []
+
+ for line in result.splitlines():
+ output.append(pathRe.search(line).group(1).strip())
+
+ return output
+
+def resolve_lib_path(objfile, lib, rapths):
+ if os.path.exists(lib):
+ return lib
+
+ if lib.startswith('@rpath/'):
+ lib = lib[len('@rpath/'):]
+ for rpath in rapths:
+ lib_path = os.path.join(rpath, lib)
+ if os.path.exists(lib_path):
+ return lib_path
+ elif lib.startswith('@loader_path/'):
+ lib = lib[len('@loader_path/'):]
+ lib_path = os.path.normpath(os.path.join(objfile, lib))
+ if os.path.exists(lib_path):
+ return lib_path
+
+ raise Exception('Could not resolve library: ' + lib)
+
+def install_name_tool_change(old, new, objfile):
+ subprocess.call(["install_name_tool", "-change", old, new, objfile], stderr=subprocess.DEVNULL)
+
+def install_name_tool_id(name, objfile):
+ subprocess.call(["install_name_tool", "-id", name, objfile], stderr=subprocess.DEVNULL)
+
+def install_name_tool_add_rpath(rpath, binary):
+ subprocess.call(["install_name_tool", "-add_rpath", rpath, binary])
+
+def install_name_tool_delete_rpath(rpath, binary):
+ subprocess.call(["install_name_tool", "-delete_rpath", rpath, binary])
+
+def libraries(objfile, result = dict(), result_relative = set(), rapths = []):
+ rapths = get_rapths(objfile) + rapths
+ libs_list, libs_relative = otool(objfile, rapths)
+ result[objfile] = libs_list
+ result_relative |= libs_relative
+
+ for lib in libs_list:
+ if lib not in result:
+ libraries(lib, result, result_relative, rapths)
+
+ return result, result_relative
+
+def lib_path(binary):
+ return os.path.join(os.path.dirname(binary), 'lib')
+
+def lib_name(lib):
+ return os.path.join("@executable_path", "lib", os.path.basename(lib))
+
+def process_libraries(libs_dict, libs_dyn, binary):
+ libs_set = set(libs_dict)
+ # Remove binary from libs_set to prevent a duplicate of the binary being
+ # added to the libs directory.
+ libs_set.remove(binary)
+
+ for src in libs_set:
+ name = lib_name(src)
+ dst = os.path.join(lib_path(binary), os.path.basename(src))
+
+ shutil.copy(src, dst)
+ os.chmod(dst, 0o755)
+ install_name_tool_id(name, dst)
+
+ if src in libs_dict[binary]:
+ install_name_tool_change(src, name, binary)
+
+ for p in libs_set:
+ if p in libs_dict[src]:
+ install_name_tool_change(p, lib_name(p), dst)
+
+ for lib in libs_dyn:
+ install_name_tool_change(lib, lib_name(lib), dst)
+
+ for lib in libs_dyn:
+ install_name_tool_change(lib, lib_name(lib), binary)
+
+def process_swift_libraries(binary):
+ command = ['xcrun', '--find', 'swift-stdlib-tool']
+ swiftStdlibTool = subprocess.check_output(command, universal_newlines=True).strip()
+ # from xcode11 on the dynamic swift libs reside in a separate directory from
+ # the std one, might need versioned paths for future swift versions
+ swiftLibPath = os.path.join(swiftStdlibTool, '../../lib/swift-5.0/macosx')
+ swiftLibPath = os.path.abspath(swiftLibPath)
+
+ command = [swiftStdlibTool, '--copy', '--platform', 'macosx', '--scan-executable', binary, '--destination', lib_path(binary)]
+
+ if os.path.exists(swiftLibPath):
+ command.extend(['--source-libraries', swiftLibPath])
+
+ subprocess.check_output(command, universal_newlines=True)
+
+ print(">> setting additional rpath for swift libraries")
+ install_name_tool_add_rpath("@executable_path/lib", binary)
+
+def remove_dev_tools_rapths(binary):
+ for path in get_rpaths_dev_tools(binary):
+ install_name_tool_delete_rpath(path, binary)
+
+def main():
+ binary = os.path.abspath(sys.argv[1])
+ if not os.path.exists(lib_path(binary)):
+ os.makedirs(lib_path(binary))
+ print(">> gathering all linked libraries")
+ libs, libs_rel = libraries(binary)
+
+ print(">> copying and processing all linked libraries")
+ process_libraries(libs, libs_rel, binary)
+
+ print(">> removing rpath definitions towards dev tools")
+ remove_dev_tools_rapths(binary)
+
+ print(">> copying and processing swift libraries")
+ process_swift_libraries(binary)
+
+if __name__ == "__main__":
+ main()
diff --git a/TOOLS/file2string.py b/TOOLS/file2string.py
new file mode 100755
index 0000000..5b1c4a9
--- /dev/null
+++ b/TOOLS/file2string.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+
+# Convert the contents of a file into a C string constant.
+# Note that the compiler will implicitly add an extra 0 byte at the end
+# of every string, so code using the string may need to remove that to get
+# the exact contents of the original file.
+
+#
+# This file is part of mpv.
+#
+# mpv is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# mpv is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys
+
+def file2string(infilename, infile, outfile):
+ outfile.write("// Generated from %s\n\n" % infilename)
+
+ conv = ["\\%03o" % c for c in range(256)]
+ safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" \
+ "0123456789!#%&'()*+,-./:;<=>[]^_{|}~ "
+
+ for c in safe_chars:
+ conv[ord(c)] = c
+ for c, esc in ("\nn", "\tt", r"\\", '""'):
+ conv[ord(c)] = '\\' + esc
+ for line in infile:
+ outfile.write('"' + ''.join(conv[c] for c in line) + '"\n')
+
+if __name__ == "__main__":
+ outfile = open(sys.argv[2], "w")
+ with open(sys.argv[1], 'rb') as infile:
+ file2string(sys.argv[1], infile, outfile)
diff --git a/TOOLS/gen-osd-font.sh b/TOOLS/gen-osd-font.sh
new file mode 100755
index 0000000..6838692
--- /dev/null
+++ b/TOOLS/gen-osd-font.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+# This script is expected to be called as TOOLS/gen-osd-font.sh (it will access
+# TOOLS/mpv-osd-symbols.sfdir), and it will write sub/osd_font.otf.
+
+# Needs fontforge with python scripting
+
+fontforge -lang=py -c 'f=open(argv[1]); f.generate(argv[2])' \
+ TOOLS/mpv-osd-symbols.sfdir sub/osd_font.otf
diff --git a/TOOLS/idet.sh b/TOOLS/idet.sh
new file mode 100755
index 0000000..7fb51c1
--- /dev/null
+++ b/TOOLS/idet.sh
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+: "${MPV:=mpv}"
+: "${ILDETECT_MPV:=$MPV}"
+: "${ILDETECT_MPVFLAGS:=--start=35% --length=35}"
+: "${ILDETECT_DRY_RUN:=}"
+: "${ILDETECT_QUIET:=}"
+: "${ILDETECT_RUN_INTERLACED_ONLY:=}"
+: "${ILDETECT_FORCE_RUN:=}"
+
+# This script uses ffmpeg's "idet" filter for interlace detection. In the
+# long run this should replace ildetect.sh+ildetect.so.
+
+# exit status:
+# 0 progressive
+# 1 telecine
+# 2 interlaced
+# 8 unknown
+# 16 detect fail
+# 17+ mpv's status | 16
+
+testfun()
+{
+ $ILDETECT_MPV "$@" \
+ --vf-add=lavfi="[idet]" --msg-level=ffmpeg=v \
+ --o= --vo=null --no-audio --untimed \
+ $ILDETECT_MPVFLAGS \
+ | { if [ -n "$ILDETECT_QUIET" ]; then cat; else tee /dev/stderr; fi } \
+ | grep "Parsed_idet_0: Multi frame detection: "
+}
+
+judge()
+{
+ tff=0
+ bff=0
+ progressive=0
+ undetermined=0
+ while read -r _ _ _ _ _ _ tff1 _ bff1 _ progressive1 _ undetermined1 _; do
+ case "$tff1$bff1$progressive1$undetermined1" in
+ *[!0-9]*)
+ printf >&2 'ERROR: Unrecognized idet output: %s\n' "$out"
+ exit 16
+ ;;
+ esac
+ tff=$((tff + tff1))
+ bff=$((bff + bff1))
+ progressive=$((progressive + progressive1))
+ undetermined=$((undetermined + undetermined1))
+ done <<EOF
+$(testfun "$@" | sed 's/:/: /g')
+EOF
+
+ interlaced=$((bff + tff))
+ determined=$((interlaced + progressive))
+
+ if [ "$undetermined" -gt "$determined" ] || [ "$determined" -lt 250 ]; then
+ echo >&2 "ERROR: Less than 50% or 250 frames are determined."
+ [ -n "$ILDETECT_FORCE_RUN" ] || exit 8
+ echo >&2 "Assuming interlacing."
+ if [ "$tff" -gt $((bff * 10)) ]; then
+ verdict="interlaced-tff"
+ elif [ "$bff" -gt $((tff * 10)) ]; then
+ verdict="interlaced-bff"
+ else
+ verdict="interlaced"
+ fi
+ elif [ $((interlaced * 20)) -gt "$progressive" ]; then
+ # At least 5% of the frames are interlaced!
+ if [ "$tff" -gt $((bff * 10)) ]; then
+ verdict="interlaced-tff"
+ elif [ "$bff" -gt $((tff * 10)) ]; then
+ verdict="interlaced-bff"
+ else
+ echo >&2 "ERROR: Content is interlaced, but can't determine field order."
+ [ -n "$ILDETECT_FORCE_RUN" ] || exit 8
+ echo >&2 "Assuming interlacing with default field order."
+ verdict="interlaced"
+ fi
+ else
+ # Likely progressive
+ verdict="progressive"
+ fi
+
+ printf '%s\n' "$verdict"
+}
+
+judge "$@" --vf-clr
+case "$verdict" in
+ progressive)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ [ -n "$ILDETECT_RUN_INTERLACED_ONLY" ] || \
+ $ILDETECT_MPV "$@"
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 0
+ ;;
+ interlaced-tff)
+ judge "$@" --vf-clr --vf-pre=lavfi=\[setfield=tff\],pullup
+ case "$verdict" in
+ progressive)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ $ILDETECT_MPV "$@" --vf-pre=lavfi=\[setfield=tff\],pullup
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 1
+ ;;
+ *)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ $ILDETECT_MPV "$@" --vf-pre=lavfi=\[setfield=tff\],yadif
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 2
+ ;;
+ esac
+ ;;
+ interlaced-bff)
+ judge "$@" --vf-clr --vf-pre=lavfi=\[setfield=bff\],pullup
+ case "$verdict" in
+ progressive)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ $ILDETECT_MPV "$@" --vf-pre=lavfi=\[setfield=bff\],pullup
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 1
+ ;;
+ *)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ $ILDETECT_MPV "$@" --vf-pre=lavfi=\[setfield=bff\],yadif
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 2
+ ;;
+ esac
+ ;;
+ interlaced)
+ judge "$@" --vf-clr --vf-pre=pullup
+ case "$verdict" in
+ progressive)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ $ILDETECT_MPV "$@" --vf-pre=pullup
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 1
+ ;;
+ *)
+ [ -n "$ILDETECT_DRY_RUN" ] || \
+ $ILDETECT_MPV "$@" --vf-pre=yadif
+ r=$?
+ [ $r -eq 0 ] || exit $((r | 16))
+ exit 2
+ ;;
+ esac
+ ;;
+ *)
+ echo >&2 "ERROR: Internal error."
+ exit 16
+ ;;
+esac
diff --git a/TOOLS/lua/README.md b/TOOLS/lua/README.md
new file mode 100644
index 0000000..79b67d6
--- /dev/null
+++ b/TOOLS/lua/README.md
@@ -0,0 +1,20 @@
+mpv lua scripts
+===============
+
+The lua scripts in this folder can be loaded on a one-time basis by
+adding the option
+
+ --script=/path/to/script.lua
+
+to mpv's command line.
+
+Where appropriate, they may also be placed in ~/.config/mpv/scripts/ from
+where they will be automatically loaded when mpv starts.
+
+This is only a small selection of internally maintained scripts. Some of them
+are just for testing mpv internals, or serve as examples. An extensive
+user-edited list of 3rd party scripts is available here:
+
+ https://github.com/mpv-player/mpv/wiki/User-Scripts
+
+(Anyone can add their own scripts to that list.)
diff --git a/TOOLS/lua/acompressor.lua b/TOOLS/lua/acompressor.lua
new file mode 100644
index 0000000..6a69140
--- /dev/null
+++ b/TOOLS/lua/acompressor.lua
@@ -0,0 +1,155 @@
+-- This script adds control to the dynamic range compression ffmpeg
+-- filter including key bindings for adjusting parameters.
+--
+-- See https://ffmpeg.org/ffmpeg-filters.html#acompressor for explanation
+-- of the parameters.
+
+local mp = require 'mp'
+local options = require 'mp.options'
+
+local o = {
+ default_enable = false,
+ show_osd = true,
+ osd_timeout = 4000,
+ filter_label = mp.get_script_name(),
+
+ key_toggle = 'n',
+ key_increase_threshold = 'F1',
+ key_decrease_threshold = 'Shift+F1',
+ key_increase_ratio = 'F2',
+ key_decrease_ratio = 'Shift+F2',
+ key_increase_knee = 'F3',
+ key_decrease_knee = 'Shift+F3',
+ key_increase_makeup = 'F4',
+ key_decrease_makeup = 'Shift+F4',
+ key_increase_attack = 'F5',
+ key_decrease_attack = 'Shift+F5',
+ key_increase_release = 'F6',
+ key_decrease_release = 'Shift+F6',
+
+ default_threshold = -25.0,
+ default_ratio = 3.0,
+ default_knee = 2.0,
+ default_makeup = 8.0,
+ default_attack = 20.0,
+ default_release = 250.0,
+
+ step_threshold = -2.5,
+ step_ratio = 1.0,
+ step_knee = 1.0,
+ step_makeup = 1.0,
+ step_attack = 10.0,
+ step_release = 10.0,
+}
+options.read_options(o)
+
+local params = {
+ { name = 'attack', min=0.01, max=2000, hide_default=true, dB='' },
+ { name = 'release', min=0.01, max=9000, hide_default=true, dB='' },
+ { name = 'threshold', min= -30, max= 0, hide_default=false, dB='dB' },
+ { name = 'ratio', min= 1, max= 20, hide_default=false, dB='' },
+ { name = 'knee', min= 1, max= 10, hide_default=true, dB='dB' },
+ { name = 'makeup', min= 0, max= 24, hide_default=false, dB='dB' },
+}
+
+local function parse_value(value)
+ -- Using nil here because tonumber differs between lua 5.1 and 5.2 when parsing fractions in combination with explicit base argument set to 10.
+ -- And we can't omit it because gsub returns 2 values which would get unpacked and cause more problems. Gotta love scripting languages.
+ return tonumber(value:gsub('dB$', ''), nil)
+end
+
+local function format_value(value, dB)
+ return string.format('%g%s', value, dB)
+end
+
+local function show_osd(filter)
+ if not o.show_osd then
+ return
+ end
+
+ if not filter.enabled then
+ mp.commandv('show-text', 'Dynamic range compressor: disabled', o.osd_timeout)
+ return
+ end
+
+ local pretty = {}
+ for _,param in ipairs(params) do
+ local value = parse_value(filter.params[param.name])
+ if not (param.hide_default and value == o['default_' .. param.name]) then
+ pretty[#pretty+1] = string.format('%s: %g%s', param.name:gsub("^%l", string.upper), value, param.dB)
+ end
+ end
+
+ if #pretty == 0 then
+ pretty = ''
+ else
+ pretty = '\n(' .. table.concat(pretty, ', ') .. ')'
+ end
+
+ mp.commandv('show-text', 'Dynamic range compressor: enabled' .. pretty, o.osd_timeout)
+end
+
+local function get_filter()
+ local af = mp.get_property_native('af', {})
+
+ for i = 1, #af do
+ if af[i].label == o.filter_label then
+ return af, i
+ end
+ end
+
+ af[#af+1] = {
+ name = 'acompressor',
+ label = o.filter_label,
+ enabled = false,
+ params = {},
+ }
+
+ for _,param in pairs(params) do
+ af[#af].params[param.name] = format_value(o['default_' .. param.name], param.dB)
+ end
+
+ return af, #af
+end
+
+local function toggle_acompressor()
+ local af, i = get_filter()
+ af[i].enabled = not af[i].enabled
+ mp.set_property_native('af', af)
+ show_osd(af[i])
+end
+
+local function update_param(name, increment)
+ for _,param in pairs(params) do
+ if param.name == string.lower(name) then
+ local af, i = get_filter()
+ local value = parse_value(af[i].params[param.name])
+ value = math.max(param.min, math.min(value + increment, param.max))
+ af[i].params[param.name] = format_value(value, param.dB)
+ af[i].enabled = true
+ mp.set_property_native('af', af)
+ show_osd(af[i])
+ return
+ end
+ end
+
+ mp.msg.error('Unknown parameter "' .. name .. '"')
+end
+
+mp.add_key_binding(o.key_toggle, "toggle-acompressor", toggle_acompressor)
+mp.register_script_message('update-param', update_param)
+
+for _,param in pairs(params) do
+ for direction,step in pairs({increase=1, decrease=-1}) do
+ mp.add_key_binding(o['key_' .. direction .. '_' .. param.name],
+ 'acompressor-' .. direction .. '-' .. param.name,
+ function() update_param(param.name, step*o['step_' .. param.name]); end,
+ { repeatable = true })
+ end
+end
+
+if o.default_enable then
+ local af, i = get_filter()
+ af[i].enabled = true
+ mp.set_property_native('af', af)
+end
diff --git a/TOOLS/lua/ao-null-reload.lua b/TOOLS/lua/ao-null-reload.lua
new file mode 100644
index 0000000..5b2330b
--- /dev/null
+++ b/TOOLS/lua/ao-null-reload.lua
@@ -0,0 +1,20 @@
+-- Handles the edge case where previous attempts to init audio have failed, but
+-- might start working due to a newly added device. This is required in
+-- particular for ao=wasapi, since the internal IMMNotificationClient code that
+-- normally triggers ao-reload will not be running in this case.
+
+function do_reload()
+ mp.command("ao-reload")
+ reloading = nil
+end
+
+function on_audio_device_list_change()
+ if mp.get_property("current-ao") == "null" and not reloading then
+ mp.msg.verbose("audio-device-list changed: reloading audio")
+ -- avoid calling ao-reload too often
+ reloading = mp.add_timeout(0.5, do_reload)
+ end
+end
+
+mp.set_property("options/audio-fallback-to-null", "yes")
+mp.observe_property("audio-device-list", "native", on_audio_device_list_change)
diff --git a/TOOLS/lua/audio-hotplug-test.lua b/TOOLS/lua/audio-hotplug-test.lua
new file mode 100644
index 0000000..8dedc68
--- /dev/null
+++ b/TOOLS/lua/audio-hotplug-test.lua
@@ -0,0 +1,8 @@
+local utils = require("mp.utils")
+
+mp.observe_property("audio-device-list", "native", function(name, val)
+ print("Audio device list changed:")
+ for index, e in ipairs(val) do
+ print(" - '" .. e.name .. "' (" .. e.description .. ")")
+ end
+end)
diff --git a/TOOLS/lua/autocrop.lua b/TOOLS/lua/autocrop.lua
new file mode 100644
index 0000000..b9e1120
--- /dev/null
+++ b/TOOLS/lua/autocrop.lua
@@ -0,0 +1,298 @@
+--[[
+This script uses the lavfi cropdetect filter and the video-crop property to
+automatically crop the currently playing video with appropriate parameters.
+
+It automatically crops the video when playback starts.
+
+You can also manually crop the video by pressing the "C" (shift+c) key.
+Pressing it again undoes the crop.
+
+The workflow is as follows: First, it inserts the cropdetect filter. After
+<detect_seconds> (default is 1) seconds, it then sets video-crop based on the
+vf-metadata values gathered by cropdetect. The cropdetect filter is removed
+after video-crop is set as it is no longer needed.
+
+Since the crop parameters are determined from the 1 second of video between
+inserting the cropdetect filter and setting video-crop, the "C" key should be
+pressed at a position in the video where the crop region is unambiguous (i.e.,
+not a black frame, black background title card, or dark scene).
+
+If non-copy-back hardware decoding is in use, hwdec is temporarily disabled for
+the duration of cropdetect as the filter would fail otherwise.
+
+These are the default options. They can be overridden by adding
+script-opts-append=autocrop-<parameter>=<value> to mpv.conf.
+--]]
+local options = {
+ -- Whether to automatically apply crop at the start of playback. If you
+ -- don't want to crop automatically, add
+ -- script-opts-append=autocrop-auto=no to mpv.conf.
+ auto = true,
+ -- Delay before starting crop in auto mode. You can try to increase this
+ -- value to avoid dark scenes or fade ins at beginning. Automatic cropping
+ -- will not occur if the value is larger than the remaining playback time.
+ auto_delay = 4,
+ -- Black threshold for cropdetect. Smaller values will generally result in
+ -- less cropping. See limit of
+ -- https://ffmpeg.org/ffmpeg-filters.html#cropdetect
+ detect_limit = "24/255",
+ -- The value which the width/height should be divisible by. Smaller
+ -- values have better detection accuracy. If you have problems with
+ -- other filters, you can try to set it to 4 or 16. See round of
+ -- https://ffmpeg.org/ffmpeg-filters.html#cropdetect
+ detect_round = 2,
+ -- The ratio of the minimum clip size to the original. A number from 0 to
+ -- 1. If the picture is over cropped, try adjusting this value.
+ detect_min_ratio = 0.5,
+ -- How long to gather cropdetect data. Increasing this may be desirable to
+ -- allow cropdetect more time to collect data.
+ detect_seconds = 1,
+ -- Whether the OSD shouldn't be used when cropdetect and video-crop are
+ -- applied and removed.
+ suppress_osd = false,
+}
+
+require "mp.options".read_options(options)
+
+local cropdetect_label = mp.get_script_name() .. "-cropdetect"
+
+timers = {
+ auto_delay = nil,
+ detect_crop = nil
+}
+
+local hwdec_backup
+
+local command_prefix = options.suppress_osd and 'no-osd' or ''
+
+function is_enough_time(seconds)
+
+ -- Plus 1 second for deviation.
+ local time_needed = seconds + 1
+ local playtime_remaining = mp.get_property_native("playtime-remaining")
+
+ return playtime_remaining and time_needed < playtime_remaining
+end
+
+function is_cropable(time_needed)
+ if mp.get_property_native('current-tracks/video/image') ~= false then
+ mp.msg.warn("autocrop only works for videos.")
+ return false
+ end
+
+ if not is_enough_time(time_needed) then
+ mp.msg.warn("Not enough time to detect crop.")
+ return false
+ end
+
+ return true
+end
+
+function remove_cropdetect()
+ for _, filter in pairs(mp.get_property_native("vf")) do
+ if filter.label == cropdetect_label then
+ mp.command(
+ string.format("%s vf remove @%s", command_prefix, filter.label))
+
+ return
+ end
+ end
+end
+
+function restore_hwdec()
+ if hwdec_backup then
+ mp.set_property("hwdec", hwdec_backup)
+ hwdec_backup = nil
+ end
+end
+
+function cleanup()
+ remove_cropdetect()
+
+ -- Kill all timers.
+ for index, timer in pairs(timers) do
+ if timer then
+ timer:kill()
+ timers[index] = nil
+ end
+ end
+
+ restore_hwdec()
+end
+
+function detect_crop()
+ local time_needed = options.detect_seconds
+
+ if not is_cropable(time_needed) then
+ return
+ end
+
+ local hwdec_current = mp.get_property("hwdec-current")
+ if hwdec_current:find("-copy$") == nil and hwdec_current ~= "no" and
+ hwdec_current ~= "crystalhd" and hwdec_current ~= "rkmpp" then
+ hwdec_backup = mp.get_property("hwdec")
+ mp.set_property("hwdec", "no")
+ end
+
+ -- Insert the cropdetect filter.
+ local limit = options.detect_limit
+ local round = options.detect_round
+
+ mp.command(
+ string.format(
+ '%s vf pre @%s:cropdetect=limit=%s:round=%d:reset=0',
+ command_prefix, cropdetect_label, limit, round
+ )
+ )
+
+ -- Wait to gather data.
+ timers.detect_crop = mp.add_timeout(time_needed, detect_end)
+end
+
+function detect_end()
+
+ -- Get the metadata and remove the cropdetect filter.
+ local cropdetect_metadata = mp.get_property_native(
+ "vf-metadata/" .. cropdetect_label)
+ remove_cropdetect()
+
+ -- Remove the timer of detect crop.
+ if timers.detect_crop then
+ timers.detect_crop:kill()
+ timers.detect_crop = nil
+ end
+
+ restore_hwdec()
+
+ local meta = {}
+
+ -- Verify the existence of metadata.
+ if cropdetect_metadata then
+ meta = {
+ w = cropdetect_metadata["lavfi.cropdetect.w"],
+ h = cropdetect_metadata["lavfi.cropdetect.h"],
+ x = cropdetect_metadata["lavfi.cropdetect.x"],
+ y = cropdetect_metadata["lavfi.cropdetect.y"],
+ }
+ else
+ mp.msg.error("No crop data.")
+ mp.msg.info("Was the cropdetect filter successfully inserted?")
+ mp.msg.info("Does your version of ffmpeg/libav support AVFrame metadata?")
+ return
+ end
+
+ -- Verify that the metadata meets the requirements and convert it.
+ if meta.w and meta.h and meta.x and meta.y then
+ local width = mp.get_property_native("width")
+ local height = mp.get_property_native("height")
+
+ meta = {
+ w = tonumber(meta.w),
+ h = tonumber(meta.h),
+ x = tonumber(meta.x),
+ y = tonumber(meta.y),
+ min_w = width * options.detect_min_ratio,
+ min_h = height * options.detect_min_ratio,
+ max_w = width,
+ max_h = height
+ }
+ else
+ mp.msg.error("Got empty crop data.")
+ mp.msg.info("You might need to increase detect_seconds.")
+ end
+
+ apply_crop(meta)
+end
+
+function apply_crop(meta)
+
+ -- Verify if it is necessary to crop.
+ local is_effective = meta.w and meta.h and meta.x and meta.y and
+ (meta.x > 0 or meta.y > 0
+ or meta.w < meta.max_w or meta.h < meta.max_h)
+
+ -- Verify it is not over cropped.
+ local is_excessive = false
+ if is_effective and (meta.w < meta.min_w or meta.h < meta.min_h) then
+ mp.msg.info("The area to be cropped is too large.")
+ mp.msg.info("You might need to decrease detect_min_ratio.")
+ is_excessive = true
+ end
+
+ if not is_effective or is_excessive then
+ -- Clear any existing crop.
+ mp.command(string.format("%s set file-local-options/video-crop ''", command_prefix))
+ return
+ end
+
+ -- Apply crop.
+ mp.command(string.format("%s set file-local-options/video-crop %sx%s+%s+%s",
+ command_prefix, meta.w, meta.h, meta.x, meta.y))
+end
+
+function on_start()
+
+ -- Clean up at the beginning.
+ cleanup()
+
+ -- If auto is not true, exit.
+ if not options.auto then
+ return
+ end
+
+ -- If it is the beginning, wait for detect_crop
+ -- after auto_delay seconds, otherwise immediately.
+ local playback_time = mp.get_property_native("playback-time")
+ local is_delay_needed = playback_time
+ and options.auto_delay > playback_time
+
+ if is_delay_needed then
+
+ -- Verify if there is enough time for autocrop.
+ local time_needed = options.auto_delay + options.detect_seconds
+
+ if not is_cropable(time_needed) then
+ return
+ end
+
+ timers.auto_delay = mp.add_timeout(time_needed,
+ function()
+ detect_crop()
+
+ -- Remove the timer of auto delay.
+ timers.auto_delay:kill()
+ timers.auto_delay = nil
+ end
+ )
+ else
+ detect_crop()
+ end
+end
+
+function on_toggle()
+
+ -- If it is during auto_delay, kill the timer.
+ if timers.auto_delay then
+ timers.auto_delay:kill()
+ timers.auto_delay = nil
+ end
+
+ -- Cropped => Remove it.
+ if mp.get_property("video-crop") ~= "" then
+ mp.command(string.format("%s set file-local-options/video-crop ''", command_prefix))
+ return
+ end
+
+ -- Detecting => Leave it.
+ if timers.detect_crop then
+ mp.msg.warn("Already cropdetecting!")
+ return
+ end
+
+ -- Neither => Detect crop.
+ detect_crop()
+end
+
+mp.add_key_binding("C", "toggle_crop", on_toggle)
+mp.register_event("end-file", cleanup)
+mp.register_event("file-loaded", on_start)
diff --git a/TOOLS/lua/autodeint.lua b/TOOLS/lua/autodeint.lua
new file mode 100644
index 0000000..b891c9a
--- /dev/null
+++ b/TOOLS/lua/autodeint.lua
@@ -0,0 +1,156 @@
+-- This script uses the lavfi idet filter to automatically insert the
+-- appropriate deinterlacing filter based on a short section of the
+-- currently playing video.
+--
+-- It registers the key-binding ctrl+d, which when pressed, inserts the filters
+-- ``vf=idet,lavfi-pullup,idet``. After 4 seconds, it removes these
+-- filters and decides whether the content is progressive, interlaced, or
+-- telecined and the interlacing field dominance.
+--
+-- Based on this information, it may set mpv's ``deinterlace`` property (which
+-- usually inserts the yadif filter), or insert the ``pullup`` filter if the
+-- content is telecined. It also sets field dominance with lavfi setfield.
+--
+-- OPTIONS:
+-- The default detection time may be overridden by adding
+--
+-- --script-opts=autodeint.detect_seconds=<number of seconds>
+--
+-- to mpv's arguments. This may be desirable to allow idet more
+-- time to collect data.
+--
+-- To see counts of the various types of frames for each detection phase,
+-- the verbosity can be increased with
+--
+-- --msg-level=autodeint=v
+
+require "mp.msg"
+
+script_name = mp.get_script_name()
+detect_label = string.format("%s-detect", script_name)
+pullup_label = string.format("%s", script_name)
+dominance_label = string.format("%s-dominance", script_name)
+ivtc_detect_label = string.format("%s-ivtc-detect", script_name)
+
+-- number of seconds to gather cropdetect data
+detect_seconds = tonumber(mp.get_opt(string.format("%s.detect_seconds", script_name)))
+if not detect_seconds then
+ detect_seconds = 4
+end
+
+function del_filter_if_present(label)
+ -- necessary because mp.command('vf del @label:filter') raises an
+ -- error if the filter doesn't exist
+ local vfs = mp.get_property_native("vf")
+
+ for i,vf in pairs(vfs) do
+ if vf["label"] == label then
+ table.remove(vfs, i)
+ mp.set_property_native("vf", vfs)
+ return true
+ end
+ end
+ return false
+end
+
+local function add_vf(label, filter)
+ return mp.command(('vf add @%s:%s'):format(label, filter))
+end
+
+function start_detect()
+ -- exit if detection is already in progress
+ if timer then
+ mp.msg.warn("already detecting!")
+ return
+ end
+
+ mp.set_property("deinterlace","no")
+ del_filter_if_present(pullup_label)
+ del_filter_if_present(dominance_label)
+
+ -- insert the detection filters
+ if not (add_vf(detect_label, 'idet') and
+ add_vf(dominance_label, 'setfield=mode=auto') and
+ add_vf(pullup_label, 'lavfi-pullup') and
+ add_vf(ivtc_detect_label, 'idet')) then
+ mp.msg.error("failed to insert detection filters")
+ return
+ end
+
+ -- wait to gather data
+ timer = mp.add_timeout(detect_seconds, select_filter)
+end
+
+function stop_detect()
+ del_filter_if_present(detect_label)
+ del_filter_if_present(ivtc_detect_label)
+ timer = nil
+end
+
+progressive, interlaced_tff, interlaced_bff, interlaced = 0, 1, 2, 3, 4
+
+function judge(label)
+ -- get the metadata
+ local result = mp.get_property_native(string.format("vf-metadata/%s", label))
+ local num_tff = tonumber(result["lavfi.idet.multiple.tff"])
+ local num_bff = tonumber(result["lavfi.idet.multiple.bff"])
+ local num_progressive = tonumber(result["lavfi.idet.multiple.progressive"])
+ local num_undetermined = tonumber(result["lavfi.idet.multiple.undetermined"])
+ local num_interlaced = num_tff + num_bff
+ local num_determined = num_interlaced + num_progressive
+
+ mp.msg.verbose(label.." progressive = "..num_progressive)
+ mp.msg.verbose(label.." interlaced-tff = "..num_tff)
+ mp.msg.verbose(label.." interlaced-bff = "..num_bff)
+ mp.msg.verbose(label.." undetermined = "..num_undetermined)
+
+ if num_determined < num_undetermined then
+ mp.msg.warn("majority undetermined frames")
+ end
+ if num_progressive > 20*num_interlaced then
+ return progressive
+ elseif num_tff > 10*num_bff then
+ return interlaced_tff
+ elseif num_bff > 10*num_tff then
+ return interlaced_bff
+ else
+ return interlaced
+ end
+end
+
+function select_filter()
+ -- handle the first detection filter results
+ local verdict = judge(detect_label)
+ local ivtc_verdict = judge(ivtc_detect_label)
+ local dominance = "auto"
+ if verdict == progressive then
+ mp.msg.info("progressive: doing nothing")
+ stop_detect()
+ del_filter_if_present(dominance_label)
+ del_filter_if_present(pullup_label)
+ return
+ else
+ if verdict == interlaced_tff then
+ dominance = "tff"
+ add_vf(dominance_label, 'setfield=mode='..dominance)
+ elseif verdict == interlaced_bff then
+ dominance = "bff"
+ add_vf(dominance_label, 'setfield=mode='..dominance)
+ else
+ del_filter_if_present(dominance_label)
+ end
+ end
+
+ -- handle the ivtc detection filter results
+ if ivtc_verdict == progressive then
+ mp.msg.info(string.format("telecined with %s field dominance: using pullup", dominance))
+ stop_detect()
+ else
+ mp.msg.info(string.format("interlaced with %s field dominance: setting deinterlace property", dominance))
+ del_filter_if_present(pullup_label)
+ mp.set_property("deinterlace","yes")
+ stop_detect()
+ end
+end
+
+mp.add_key_binding("ctrl+d", script_name, start_detect)
diff --git a/TOOLS/lua/autoload.lua b/TOOLS/lua/autoload.lua
new file mode 100644
index 0000000..4003cbc
--- /dev/null
+++ b/TOOLS/lua/autoload.lua
@@ -0,0 +1,328 @@
+-- This script automatically loads playlist entries before and after the
+-- the currently played file. It does so by scanning the directory a file is
+-- located in when starting playback. It sorts the directory entries
+-- alphabetically, and adds entries before and after the current file to
+-- the internal playlist. (It stops if it would add an already existing
+-- playlist entry at the same position - this makes it "stable".)
+-- Add at most 5000 * 2 files when starting a file (before + after).
+
+--[[
+To configure this script use file autoload.conf in directory script-opts (the "script-opts"
+directory must be in the mpv configuration directory, typically ~/.config/mpv/).
+
+Example configuration would be:
+
+disabled=no
+images=no
+videos=yes
+audio=yes
+additional_image_exts=list,of,ext
+additional_video_exts=list,of,ext
+additional_audio_exts=list,of,ext
+ignore_hidden=yes
+same_type=yes
+directory_mode=recursive
+
+--]]
+
+MAXENTRIES = 5000
+MAXDIRSTACK = 20
+
+local msg = require 'mp.msg'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+o = {
+ disabled = false,
+ images = true,
+ videos = true,
+ audio = true,
+ additional_image_exts = "",
+ additional_video_exts = "",
+ additional_audio_exts = "",
+ ignore_hidden = true,
+ same_type = false,
+ directory_mode = "auto"
+}
+options.read_options(o, nil, function(list)
+ split_option_exts(list.additional_video_exts, list.additional_audio_exts, list.additional_image_exts)
+ if list.videos or list.additional_video_exts or
+ list.audio or list.additional_audio_exts or
+ list.images or list.additional_image_exts then
+ create_extensions()
+ end
+ if list.directory_mode then
+ validate_directory_mode()
+ end
+end)
+
+function Set (t)
+ local set = {}
+ for _, v in pairs(t) do set[v] = true end
+ return set
+end
+
+function SetUnion (a,b)
+ for k in pairs(b) do a[k] = true end
+ return a
+end
+
+function Split (s)
+ local set = {}
+ for v in string.gmatch(s, '([^,]+)') do set[v] = true end
+ return set
+end
+
+EXTENSIONS_VIDEO = Set {
+ '3g2', '3gp', 'avi', 'flv', 'm2ts', 'm4v', 'mj2', 'mkv', 'mov',
+ 'mp4', 'mpeg', 'mpg', 'ogv', 'rmvb', 'webm', 'wmv', 'y4m'
+}
+
+EXTENSIONS_AUDIO = Set {
+ 'aiff', 'ape', 'au', 'flac', 'm4a', 'mka', 'mp3', 'oga', 'ogg',
+ 'ogm', 'opus', 'wav', 'wma'
+}
+
+EXTENSIONS_IMAGES = Set {
+ 'avif', 'bmp', 'gif', 'j2k', 'jp2', 'jpeg', 'jpg', 'jxl', 'png',
+ 'svg', 'tga', 'tif', 'tiff', 'webp'
+}
+
+function split_option_exts(video, audio, image)
+ if video then o.additional_video_exts = Split(o.additional_video_exts) end
+ if audio then o.additional_audio_exts = Split(o.additional_audio_exts) end
+ if image then o.additional_image_exts = Split(o.additional_image_exts) end
+end
+split_option_exts(true, true, true)
+
+function create_extensions()
+ EXTENSIONS = {}
+ if o.videos then SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_VIDEO), o.additional_video_exts) end
+ if o.audio then SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_AUDIO), o.additional_audio_exts) end
+ if o.images then SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_IMAGES), o.additional_image_exts) end
+end
+create_extensions()
+
+function validate_directory_mode()
+ if o.directory_mode ~= "recursive" and o.directory_mode ~= "lazy" and o.directory_mode ~= "ignore" then
+ o.directory_mode = nil
+ end
+end
+validate_directory_mode()
+
+function add_files(files)
+ local oldcount = mp.get_property_number("playlist-count", 1)
+ for i = 1, #files do
+ mp.commandv("loadfile", files[i][1], "append")
+ mp.commandv("playlist-move", oldcount + i - 1, files[i][2])
+ end
+end
+
+function get_extension(path)
+ match = string.match(path, "%.([^%.]+)$" )
+ if match == nil then
+ return "nomatch"
+ else
+ return match
+ end
+end
+
+table.filter = function(t, iter)
+ for i = #t, 1, -1 do
+ if not iter(t[i]) then
+ table.remove(t, i)
+ end
+ end
+end
+
+table.append = function(t1, t2)
+ local t1_size = #t1
+ for i = 1, #t2 do
+ t1[t1_size + i] = t2[i]
+ end
+end
+
+-- alphanum sorting for humans in Lua
+-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
+
+function alphanumsort(filenames)
+ local function padnum(n, d)
+ return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
+ or ("%03d%s"):format(#n, n)
+ end
+
+ local tuples = {}
+ for i, f in ipairs(filenames) do
+ tuples[i] = {f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f}
+ end
+ table.sort(tuples, function(a, b)
+ return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
+ end)
+ for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end
+ return filenames
+end
+
+local autoloaded = nil
+local added_entries = {}
+local autoloaded_dir = nil
+
+function scan_dir(path, current_file, dir_mode, separator, dir_depth, total_files, extensions)
+ if dir_depth == MAXDIRSTACK then
+ return
+ end
+ msg.trace("scanning: " .. path)
+ local files = utils.readdir(path, "files") or {}
+ local dirs = dir_mode ~= "ignore" and utils.readdir(path, "dirs") or {}
+ local prefix = path == "." and "" or path
+ table.filter(files, function (v)
+ -- The current file could be a hidden file, ignoring it doesn't load other
+ -- files from the current directory.
+ if (o.ignore_hidden and not (prefix .. v == current_file) and string.match(v, "^%.")) then
+ return false
+ end
+ local ext = get_extension(v)
+ if ext == nil then
+ return false
+ end
+ return extensions[string.lower(ext)]
+ end)
+ table.filter(dirs, function(d)
+ return not ((o.ignore_hidden and string.match(d, "^%.")))
+ end)
+ alphanumsort(files)
+ alphanumsort(dirs)
+
+ for i, file in ipairs(files) do
+ files[i] = prefix .. file
+ end
+
+ table.append(total_files, files)
+ if dir_mode == "recursive" then
+ for _, dir in ipairs(dirs) do
+ scan_dir(prefix .. dir .. separator, current_file, dir_mode,
+ separator, dir_depth + 1, total_files, extensions)
+ end
+ else
+ for i, dir in ipairs(dirs) do
+ dirs[i] = prefix .. dir
+ end
+ table.append(total_files, dirs)
+ end
+end
+
+function find_and_add_entries()
+ local path = mp.get_property("path", "")
+ local dir, filename = utils.split_path(path)
+ msg.trace(("dir: %s, filename: %s"):format(dir, filename))
+ if o.disabled then
+ msg.debug("stopping: autoload disabled")
+ return
+ elseif #dir == 0 then
+ msg.debug("stopping: not a local path")
+ return
+ end
+
+ local pl_count = mp.get_property_number("playlist-count", 1)
+ this_ext = get_extension(filename)
+ -- check if this is a manually made playlist
+ if (pl_count > 1 and autoloaded == nil) or
+ (pl_count == 1 and EXTENSIONS[string.lower(this_ext)] == nil) then
+ msg.debug("stopping: manually made playlist")
+ return
+ else
+ if pl_count == 1 then
+ autoloaded = true
+ autoloaded_dir = dir
+ added_entries = {}
+ end
+ end
+
+ local extensions = {}
+ if o.same_type then
+ if EXTENSIONS_VIDEO[string.lower(this_ext)] ~= nil then
+ extensions = EXTENSIONS_VIDEO
+ elseif EXTENSIONS_AUDIO[string.lower(this_ext)] ~= nil then
+ extensions = EXTENSIONS_AUDIO
+ else
+ extensions = EXTENSIONS_IMAGES
+ end
+ else
+ extensions = EXTENSIONS
+ end
+
+ local pl = mp.get_property_native("playlist", {})
+ local pl_current = mp.get_property_number("playlist-pos-1", 1)
+ msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current,
+ utils.to_string(pl)))
+
+ local files = {}
+ do
+ local dir_mode = o.directory_mode or mp.get_property("directory-mode", "lazy")
+ local separator = mp.get_property_native("platform") == "windows" and "\\" or "/"
+ scan_dir(autoloaded_dir, path, dir_mode, separator, 0, files, extensions)
+ end
+
+ if next(files) == nil then
+ msg.debug("no other files or directories in directory")
+ return
+ end
+
+ -- Find the current pl entry (dir+"/"+filename) in the sorted dir list
+ local current
+ for i = 1, #files do
+ if files[i] == path then
+ current = i
+ break
+ end
+ end
+ if current == nil then
+ return
+ end
+ msg.trace("current file position in files: "..current)
+
+ -- treat already existing playlist entries, independent of how they got added
+ -- as if they got added by autoload
+ for _, entry in ipairs(pl) do
+ added_entries[entry.filename] = true
+ end
+
+ local append = {[-1] = {}, [1] = {}}
+ for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1
+ for i = 1, MAXENTRIES do
+ local pos = current + i * direction
+ local file = files[pos]
+ if file == nil or file[1] == "." then
+ break
+ end
+
+ -- skip files that are/were already in the playlist
+ if not added_entries[file] then
+ if direction == -1 then
+ msg.verbose("Prepending " .. file)
+ table.insert(append[-1], 1, {file, pl_current + i * direction + 1})
+ else
+ msg.verbose("Adding " .. file)
+ if pl_count > 1 then
+ table.insert(append[1], {file, pl_current + i * direction - 1})
+ else
+ mp.commandv("loadfile", file, "append")
+ end
+ end
+ end
+ added_entries[file] = true
+ end
+ if pl_count == 1 and direction == -1 and #append[-1] > 0 then
+ for i = 1, #append[-1] do
+ mp.commandv("loadfile", append[-1][i][1], "append")
+ end
+ mp.commandv("playlist-move", 0, current)
+ end
+ end
+
+ if pl_count > 1 then
+ add_files(append[1])
+ add_files(append[-1])
+ end
+end
+
+mp.register_event("start-file", find_and_add_entries)
diff --git a/TOOLS/lua/command-test.lua b/TOOLS/lua/command-test.lua
new file mode 100644
index 0000000..877cacd
--- /dev/null
+++ b/TOOLS/lua/command-test.lua
@@ -0,0 +1,124 @@
+-- Test script for some command API details.
+
+local utils = require("mp.utils")
+
+function join(sep, arr, count)
+ local r = ""
+ if count == nil then
+ count = #arr
+ end
+ for i = 1, count do
+ if i > 1 then
+ r = r .. sep
+ end
+ r = r .. utils.to_string(arr[i])
+ end
+ return r
+end
+
+mp.observe_property("vo-configured", "bool", function(_, v)
+ if v ~= true then
+ return
+ end
+
+ print("async expand-text")
+ mp.command_native_async({"expand-text", "hello ${path}!"},
+ function(res, val, err)
+ print("done async expand-text: " .. join(" ", {res, val, err}))
+ end)
+
+ -- make screenshot writing very slow
+ mp.set_property("screenshot-format", "png")
+ mp.set_property("screenshot-png-compression", "9")
+
+ timer = mp.add_periodic_timer(0.1, function() print("I'm alive") end)
+ timer:resume()
+
+ print("Slow screenshot command...")
+ res, err = mp.command_native({"screenshot"})
+ print("done, res: " .. utils.to_string(res))
+
+ print("Slow screenshot async command...")
+ res, err = mp.command_native_async({"screenshot"}, function(res)
+ print("done (async), res: " .. utils.to_string(res))
+ timer:kill()
+ end)
+ print("done (sending), res: " .. utils.to_string(res))
+
+ print("Broken screenshot async command...")
+ mp.command_native_async({"screenshot-to-file", "/nonexistent/bogus.png"},
+ function(res, val, err)
+ print("done err scr.: " .. join(" ", {res, val, err}))
+ end)
+
+ mp.command_native_async({name = "subprocess", args = {"sh", "-c", "echo hi && sleep 10s"}, capture_stdout = true},
+ function(res, val, err)
+ print("done subprocess: " .. join(" ", {res, val, err}))
+ end)
+
+ local x = mp.command_native_async({name = "subprocess", args = {"sleep", "inf"}},
+ function(res, val, err)
+ print("done sleep inf subprocess: " .. join(" ", {res, val, err}))
+ end)
+ mp.add_timeout(15, function()
+ print("aborting sleep inf subprocess after timeout")
+ mp.abort_async_command(x)
+ end)
+
+ -- (assuming this "freezes")
+ local y = mp.command_native_async({name = "sub-add", url = "-"},
+ function(res, val, err)
+ print("done sub-add stdin: " .. join(" ", {res, val, err}))
+ end)
+ mp.add_timeout(20, function()
+ print("aborting sub-add stdin after timeout")
+ mp.abort_async_command(y)
+ end)
+
+
+ mp.command_native_async({name = "subprocess", args = {"wc", "-c"},
+ stdin_data = "hello", capture_stdout = true},
+ function(res, val, err)
+ print("Should be '5': " .. val.stdout)
+ end)
+ -- blocking stdin by default
+ mp.command_native_async({name = "subprocess", args = {"cat"},
+ capture_stdout = true},
+ function(res, val, err)
+ print("Should be 0: " .. #val.stdout)
+ end)
+ -- stdin + detached
+ mp.command_native_async({name = "subprocess",
+ args = {"bash", "-c", "(sleep 5s ; cat)"},
+ stdin_data = "this should appear after 5s.\n",
+ detach = true},
+ function(res, val, err)
+ print("5s test: " .. val.status)
+ end)
+
+ -- This should get killed on script exit.
+ mp.command_native_async({name = "subprocess", playback_only = false,
+ args = {"sleep", "inf"}}, function()end)
+
+ -- Runs detached; should be killed on player exit (forces timeout)
+ mp.command_native({_flags={"async"}, name = "subprocess",
+ playback_only = false, args = {"sleep", "inf"}})
+end)
+
+function freeze_test(playback_only)
+ -- This "freezes" the script, should be killed via timeout.
+ counter = counter and counter + 1 or 0
+ print("freeze! " .. counter)
+ local x = mp.command_native({name = "subprocess",
+ playback_only = playback_only,
+ args = {"sleep", "inf"}})
+ print("done, killed=" .. utils.to_string(x.killed_by_us))
+end
+
+mp.register_event("shutdown", function()
+ freeze_test(false)
+end)
+
+mp.register_event("idle", function()
+ freeze_test(true)
+end)
diff --git a/TOOLS/lua/cycle-deinterlace-pullup.lua b/TOOLS/lua/cycle-deinterlace-pullup.lua
new file mode 100644
index 0000000..2902e40
--- /dev/null
+++ b/TOOLS/lua/cycle-deinterlace-pullup.lua
@@ -0,0 +1,56 @@
+-- This script cycles between deinterlacing, pullup (inverse
+-- telecine), and both filters off. It uses the "deinterlace" property
+-- so that a hardware deinterlacer will be used if available.
+--
+-- It overrides the default deinterlace toggle keybinding "D"
+-- (shift+d), so that rather than merely cycling the "deinterlace" property
+-- between on and off, it adds a "pullup" step to the cycle.
+--
+-- It provides OSD feedback as to the actual state of the two filters
+-- after each cycle step/keypress.
+--
+-- Note: if hardware decoding is enabled, pullup filter will likely
+-- fail to insert.
+--
+-- TODO: It might make sense to use hardware assisted vdpaupp=pullup,
+-- if available, but I don't have hardware to test it. Patch welcome.
+
+script_name = mp.get_script_name()
+pullup_label = string.format("%s-pullup", script_name)
+
+function pullup_on()
+ for i,vf in pairs(mp.get_property_native('vf')) do
+ if vf['label'] == pullup_label then
+ return "yes"
+ end
+ end
+ return "no"
+end
+
+function do_cycle()
+ if pullup_on() == "yes" then
+ -- if pullup is on remove it
+ mp.command(string.format("vf del @%s:pullup", pullup_label))
+ return
+ elseif mp.get_property("deinterlace") == "yes" then
+ -- if deinterlace is on, turn it off and insert pullup filter
+ mp.set_property("deinterlace", "no")
+ mp.command(string.format("vf add @%s:pullup", pullup_label))
+ return
+ else
+ -- if neither is on, turn on deinterlace
+ mp.set_property("deinterlace", "yes")
+ return
+ end
+end
+
+function cycle_deinterlace_pullup_handler()
+ do_cycle()
+ -- independently determine current state and give user feedback
+ mp.osd_message(string.format("deinterlace: %s\n"..
+ "pullup: %s",
+ mp.get_property("deinterlace"),
+ pullup_on()))
+end
+
+mp.add_key_binding("D", "cycle-deinterlace-pullup", cycle_deinterlace_pullup_handler)
diff --git a/TOOLS/lua/nan-test.lua b/TOOLS/lua/nan-test.lua
new file mode 100644
index 0000000..d3f1c8c
--- /dev/null
+++ b/TOOLS/lua/nan-test.lua
@@ -0,0 +1,37 @@
+-- Test a float property which internally uses NaN.
+-- Run with --no-config (or just scale-param1 not set).
+
+local utils = require 'mp.utils'
+
+prop_name = "scale-param1"
+
+-- internal NaN, return string "default" instead of NaN
+v = mp.get_property_native(prop_name, "fail")
+print("Exp:", "string", "\"default\"")
+print("Got:", type(v), utils.to_string(v))
+
+v = mp.get_property(prop_name)
+print("Exp:", "default")
+print("Got:", v)
+
+-- not representable -> return provided fallback value
+v = mp.get_property_number(prop_name, -100)
+print("Exp:", -100)
+print("Got:", v)
+
+mp.set_property_native(prop_name, 123)
+v = mp.get_property_number(prop_name, -100)
+print("Exp:", "number", 123)
+print("Got:", type(v), utils.to_string(v))
+
+-- try to set an actual NaN
+st, msg = mp.set_property_number(prop_name, 0.0/0)
+print("Exp:", nil, "<message>")
+print("Got:", st, msg)
+
+-- set default
+mp.set_property(prop_name, "default")
+
+v = mp.get_property(prop_name)
+print("Exp:", "default")
+print("Got:", v)
diff --git a/TOOLS/lua/observe-all.lua b/TOOLS/lua/observe-all.lua
new file mode 100644
index 0000000..0037439
--- /dev/null
+++ b/TOOLS/lua/observe-all.lua
@@ -0,0 +1,22 @@
+-- Test script for property change notification mechanism.
+-- Note that watching/reading some properties can be very expensive, or
+-- require the player to synchronously wait on network (when playing
+-- remote files), so you should in general only watch properties you
+-- are interested in.
+
+local utils = require("mp.utils")
+
+function observe(name)
+ mp.observe_property(name, "native", function(name, val)
+ print("property '" .. name .. "' changed to '" ..
+ utils.to_string(val) .. "'")
+ end)
+end
+
+for i,name in ipairs(mp.get_property_native("property-list")) do
+ observe(name)
+end
+
+for i,name in ipairs(mp.get_property_native("options")) do
+ observe("options/" .. name)
+end
diff --git a/TOOLS/lua/ontop-playback.lua b/TOOLS/lua/ontop-playback.lua
new file mode 100644
index 0000000..b02716c
--- /dev/null
+++ b/TOOLS/lua/ontop-playback.lua
@@ -0,0 +1,19 @@
+--makes mpv disable ontop when pausing and re-enable it again when resuming playback
+--please note that this won't do anything if ontop was not enabled before pausing
+
+local was_ontop = false
+
+mp.observe_property("pause", "bool", function(name, value)
+ local ontop = mp.get_property_native("ontop")
+ if value then
+ if ontop then
+ mp.set_property_native("ontop", false)
+ was_ontop = true
+ end
+ else
+ if was_ontop and not ontop then
+ mp.set_property_native("ontop", true)
+ end
+ was_ontop = false
+ end
+end)
diff --git a/TOOLS/lua/osd-test.lua b/TOOLS/lua/osd-test.lua
new file mode 100644
index 0000000..1b17819
--- /dev/null
+++ b/TOOLS/lua/osd-test.lua
@@ -0,0 +1,35 @@
+local assdraw = require 'mp.assdraw'
+local utils = require 'mp.utils'
+
+things = {}
+for i = 1, 2 do
+ things[i] = {
+ osd1 = mp.create_osd_overlay("ass-events"),
+ osd2 = mp.create_osd_overlay("ass-events")
+ }
+end
+things[1].text = "{\\an5}hello\\Nworld"
+things[2].text = "{\\pos(400, 200)}something something"
+
+mp.add_periodic_timer(2, function()
+ for i, thing in ipairs(things) do
+ thing.osd1.data = thing.text
+ thing.osd1.compute_bounds = true
+ --thing.osd1.hidden = true
+ local res = thing.osd1:update()
+ print("res " .. i .. ": " .. utils.to_string(res))
+
+ thing.osd2.hidden = true
+ if res ~= nil and res.x0 ~= nil then
+ local draw = assdraw.ass_new()
+ draw:append("{\\alpha&H80}")
+ draw:draw_start()
+ draw:pos(0, 0)
+ draw:rect_cw(res.x0, res.y0, res.x1, res.y1)
+ draw:draw_stop()
+ thing.osd2.hidden = false
+ thing.osd2.data = draw.text
+ end
+ thing.osd2:update()
+ end
+end)
diff --git a/TOOLS/lua/pause-when-minimize.lua b/TOOLS/lua/pause-when-minimize.lua
new file mode 100644
index 0000000..99add70
--- /dev/null
+++ b/TOOLS/lua/pause-when-minimize.lua
@@ -0,0 +1,20 @@
+-- This script pauses playback when minimizing the window, and resumes playback
+-- if it's brought back again. If the player was already paused when minimizing,
+-- then try not to mess with the pause state.
+
+local did_minimize = false
+
+mp.observe_property("window-minimized", "bool", function(name, value)
+ local pause = mp.get_property_native("pause")
+ if value == true then
+ if pause == false then
+ mp.set_property_native("pause", true)
+ did_minimize = true
+ end
+ elseif value == false then
+ if did_minimize and (pause == true) then
+ mp.set_property_native("pause", false)
+ end
+ did_minimize = false
+ end
+end)
diff --git a/TOOLS/lua/skip-logo.lua b/TOOLS/lua/skip-logo.lua
new file mode 100644
index 0000000..8e1f9da
--- /dev/null
+++ b/TOOLS/lua/skip-logo.lua
@@ -0,0 +1,265 @@
+--[[
+
+Automatically skip in files if video frames with pre-supplied fingerprints are
+detected. This will skip ahead by a pre-configured amount of time if a matching
+video frame is detected.
+
+This requires the vf_fingerprint video filter to be compiled in. Read the
+documentation of this filter for caveats (which will automatically apply to
+this script as well), such as no support for zero-copy hardware decoding.
+
+You need to manually gather and provide fingerprints for video frames and add
+them to a configuration file in script-opts/skip-logo.conf (the "script-opts"
+directory must be in the mpv configuration directory, typically ~/.config/mpv/).
+
+Example script-opts/skip-logo.conf:
+
+
+ cases = {
+ {
+ -- Skip ahead 10 seconds if a black frame was detected
+ -- Note: this is dangerous non-sense. It's just for demonstration.
+ name = "black frame", -- print if matched
+ skip = 10, -- number of seconds to skip forward
+ score = 0.3, -- required score
+ fingerprint = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ },
+ {
+ -- Skip ahead 20 seconds if a white frame was detected
+ -- Note: this is dangerous non-sense. It's just for demonstration.
+ name = "fun2",
+ skip = 20,
+ fingerprint = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ },
+ }
+
+This is actually a lua file. Lua was chosen because it seemed less of a pain to
+parse. Future versions of this script may change the format.
+
+The fingerprint is a video frame, converted to "gray" (8 bit per pixels), full
+range, each pixel concatenated into an array, converted to a hex string. You
+can produce these fingerprints by running this manually:
+
+ mpv --vf=fingerprint:print yourfile.mkv
+
+This will log the fingerprint of each video frame to the console, along with its
+timestamp. You find the fingerprint of a unique-enough looking frame, and add
+it as entry to skip-logo.conf.
+
+You can provide a score for "fuzziness". If no score is provided, a default
+value of 0.3 is used. The score is inverse: 0 means exactly the same, while a
+higher score means a higher difference. Currently, the score is computed as
+euclidean distance between the video frame and the pre-provided fingerprint,
+thus the highest score is 16. You probably want a score lower than 1 at least.
+(This algorithm is very primitive, but also simple and fast to compute.)
+
+There's always the danger of false positives, which might be quite annoying.
+It's up to you what you hate more, the logo, or random skips if false positives
+are detected. Also, it's always active, and might eat too much CPU with files
+that have a high resolution or framerate. To temporarily disable the script,
+having a keybind like this in your input.conf will be helpful:
+
+ ctrl+k vf toggle @skip-logo
+
+This will disable/enable the fingerprint filter, which the script automatically
+adds at start.
+
+Another important caveat is that the script currently disables matching during
+seeking or playback initialization, which means it cannot match the first few
+frames of a video. This could be fixed, but the author was too lazy to do so.
+
+--]]
+
+local utils = require "mp.utils"
+local msg = require "mp.msg"
+
+local label = "skip-logo"
+local meta_property = string.format("vf-metadata/%s", label)
+
+local config = {}
+local cases = {}
+local cur_bmp
+local seeking = false
+local playback_start_pts = nil
+
+-- Convert a hex string to an array. Convert each byte to a [0,1] float by
+-- interpreting it as normalized uint8_t.
+-- The data parameter, if not nil, may be used as storage (avoiding garbage).
+local function hex_to_norm8(hex, data)
+ local size = math.floor(#hex / 2)
+ if #hex ~= size * 2 then
+ return nil
+ end
+ local res
+ if (data ~= nil) and (#data == size) then
+ res = data
+ else
+ res = {}
+ end
+ for i = 1, size do
+ local num = tonumber(hex:sub(i * 2, i * 2 + 1), 16)
+ if num == nil then
+ return nil
+ end
+ res[i] = num / 255.0
+ end
+ return res
+end
+
+local function compare_bmp(a, b)
+ if #a ~= #b then
+ return nil -- can't compare
+ end
+ local sum = 0
+ for i = 1, #a do
+ local diff = a[i] - b[i]
+ sum = sum + diff * diff
+ end
+ return math.sqrt(sum)
+end
+
+local function load_config()
+ local conf_file = mp.find_config_file("script-opts/skip-logo.conf")
+ local conf_fn
+ local err = nil
+ if conf_file then
+ if setfenv then
+ conf_fn, err = loadfile(conf_file)
+ if conf_fn then
+ setfenv(conf_fn, config)
+ end
+ else
+ conf_fn, err = loadfile(conf_file, "t", config)
+ end
+ else
+ err = "config file not found"
+ end
+
+ if conf_fn and (not err) then
+ local ok, err2 = pcall(conf_fn)
+ err = err2
+ end
+
+ if err then
+ msg.error("Failed to load config file:", err)
+ end
+
+ if config.cases then
+ for n, case in ipairs(config.cases) do
+ local err = nil
+ case.bitmap = hex_to_norm8(case.fingerprint)
+ if case.bitmap == nil then
+ err = "invalid or missing fingerprint field"
+ end
+ if case.score == nil then
+ case.score = 0.3
+ end
+ if type(case.score) ~= "number" then
+ err = "score field is not a number"
+ end
+ if type(case.skip) ~= "number" then
+ err = "skip field is not a number or missing"
+ end
+ if case.name == nil then
+ case.name = ("Entry %d"):format(n)
+ end
+ if err == nil then
+ cases[#cases + 1] = case
+ else
+ msg.error(("Entry %s: %s, ignoring."):format(case.name, err))
+ end
+ end
+ end
+end
+
+load_config()
+
+-- Returns true on match and if something was done.
+local function check_fingerprint(hex, pts)
+ local bmp = hex_to_norm8(hex, cur_bmp)
+ cur_bmp = bmp
+
+ -- If parsing the filter's result failed (well, it shouldn't).
+ assert(bmp ~= nil, "filter returned nonsense")
+
+ for _, case in ipairs(cases) do
+ local score = compare_bmp(case.bitmap, bmp)
+ if (score ~= nil) and (score <= case.score) then
+ msg.warn(("Matching %s: score=%f (required: %f) at %s, skipping %f seconds"):
+ format(case.name, score, case.score, mp.format_time(pts), case.skip))
+ mp.commandv("seek", pts + case.skip, "absolute+exact")
+ return true
+ end
+ end
+
+ return false
+end
+
+local function read_frames()
+ local result = mp.get_property_native(meta_property)
+ if result == nil then
+ return
+ end
+
+ -- Try to get all entries. Out of laziness, assume that there are at most
+ -- 100 entries. (In fact, vf_fingerprint limits it to 10.)
+ for i = 0, 99 do
+ local prefix = string.format("fp%d.", i)
+ local hex = result[prefix .. "hex"]
+
+ local pts = tonumber(result[prefix .. "pts"])
+ if (hex == nil) or (pts == nil) then
+ break
+ end
+
+ local skip = false -- blame Lua for not having "continue" or "goto", not me
+
+ -- If seeking just stopped, there will be frames before the seek target,
+ -- ignore them by checking the timestamps.
+ if playback_start_pts ~= nil then
+ if pts >= playback_start_pts then
+ playback_start_pts = nil -- just for robustness
+ else
+ skip = true
+ end
+ end
+
+ if not skip then
+ if check_fingerprint(hex, pts) then
+ break
+ end
+ end
+ end
+end
+
+mp.observe_property(meta_property, "none", function()
+ -- Ignore frames that are decoded/filtered during seeking.
+ if seeking then
+ return
+ end
+
+ read_frames()
+end)
+
+mp.observe_property("seeking", "bool", function(name, val)
+ seeking = val
+ if seeking == false then
+ playback_start_pts = mp.get_property_number("playback-time")
+ read_frames()
+ end
+end)
+
+local filters = mp.get_property_native("option-info/vf/choices", {})
+local found = false
+for _, f in ipairs(filters) do
+ if f == "fingerprint" then
+ found = true
+ break
+ end
+end
+
+if found then
+ mp.command(("no-osd vf add @%s:fingerprint"):format(label, filter))
+else
+ msg.warn("vf_fingerprint not found")
+end
diff --git a/TOOLS/lua/status-line.lua b/TOOLS/lua/status-line.lua
new file mode 100644
index 0000000..e40dce2
--- /dev/null
+++ b/TOOLS/lua/status-line.lua
@@ -0,0 +1,92 @@
+-- Rebuild the terminal status line as a lua script
+-- Be aware that this will require more cpu power!
+-- Also, this is based on a rather old version of the
+-- builtin mpv status line.
+
+-- Add a string to the status line
+function atsl(s)
+ newStatus = newStatus .. s
+end
+
+function update_status_line()
+ -- Reset the status line
+ newStatus = ""
+
+ if mp.get_property_bool("pause") then
+ atsl("(Paused) ")
+ elseif mp.get_property_bool("paused-for-cache") then
+ atsl("(Buffering) ")
+ end
+
+ if mp.get_property("aid") ~= "no" then
+ atsl("A")
+ end
+ if mp.get_property("vid") ~= "no" then
+ atsl("V")
+ end
+
+ atsl(": ")
+
+ atsl(mp.get_property_osd("time-pos"))
+
+ atsl(" / ");
+ atsl(mp.get_property_osd("duration"));
+
+ atsl(" (")
+ atsl(mp.get_property_osd("percent-pos", -1))
+ atsl("%)")
+
+ local r = mp.get_property_number("speed", -1)
+ if r ~= 1 then
+ atsl(string.format(" x%4.2f", r))
+ end
+
+ r = mp.get_property_number("avsync", nil)
+ if r ~= nil then
+ atsl(string.format(" A-V: %f", r))
+ end
+
+ r = mp.get_property("total-avsync-change", 0)
+ if math.abs(r) > 0.05 then
+ atsl(string.format(" ct:%7.3f", r))
+ end
+
+ r = mp.get_property_number("decoder-drop-frame-count", -1)
+ if r > 0 then
+ atsl(" Late: ")
+ atsl(r)
+ end
+
+ r = mp.get_property_osd("video-bitrate")
+ if r ~= nil and r ~= "" then
+ atsl(" Vb: ")
+ atsl(r)
+ end
+
+ r = mp.get_property_osd("audio-bitrate")
+ if r ~= nil and r ~= "" then
+ atsl(" Ab: ")
+ atsl(r)
+ end
+
+ r = mp.get_property_number("cache", 0)
+ if r > 0 then
+ atsl(string.format(" Cache: %d%% ", r))
+ end
+
+ -- Set the new status line
+ mp.set_property("options/term-status-msg", newStatus)
+end
+
+timer = mp.add_periodic_timer(1, update_status_line)
+
+function on_pause_change(name, value)
+ if value == false then
+ timer:resume()
+ else
+ timer:stop()
+ end
+ mp.add_timeout(0.1, update_status_line)
+end
+mp.observe_property("pause", "bool", on_pause_change)
+mp.register_event("seek", update_status_line)
diff --git a/TOOLS/lua/test-hooks.lua b/TOOLS/lua/test-hooks.lua
new file mode 100644
index 0000000..4e84d9e
--- /dev/null
+++ b/TOOLS/lua/test-hooks.lua
@@ -0,0 +1,32 @@
+local utils = require("mp.utils")
+
+function hardsleep()
+ os.execute("sleep 1s")
+end
+
+local hooks = {"on_before_start_file", "on_load", "on_load_fail",
+ "on_preloaded", "on_unload", "on_after_end_file"}
+
+for _, name in ipairs(hooks) do
+ mp.add_hook(name, 0, function()
+ print("--- hook: " .. name)
+ hardsleep()
+ print(" ... continue")
+ end)
+end
+
+local events = {"start-file", "end-file", "file-loaded", "seek",
+ "playback-restart", "idle", "shutdown"}
+for _, name in ipairs(events) do
+ mp.register_event(name, function()
+ print("--- event: " .. name)
+ end)
+end
+
+local props = {"path", "metadata"}
+for _, name in ipairs(props) do
+ mp.observe_property(name, "native", function(name, val)
+ print("property '" .. name .. "' changed to '" ..
+ utils.to_string(val) .. "'")
+ end)
+end
diff --git a/TOOLS/macos-sdk-version.py b/TOOLS/macos-sdk-version.py
new file mode 100755
index 0000000..12e1071
--- /dev/null
+++ b/TOOLS/macos-sdk-version.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+
+# This checks for the sdk path, the sdk version, and
+# the sdk build version.
+
+import re
+import os
+import string
+import subprocess
+import sys
+from shutil import which
+from subprocess import check_output
+
+def find_macos_sdk():
+ sdk = os.environ.get('MACOS_SDK', '')
+ sdk_version = os.environ.get('MACOS_SDK_VERSION', '0.0')
+ xcrun = which('xcrun')
+ xcodebuild = which('xcodebuild')
+
+ if not xcrun:
+ return sdk,sdk_version
+
+ if not sdk:
+ sdk = check_output([xcrun, '--sdk', 'macosx', '--show-sdk-path'],
+ encoding="UTF-8")
+
+ # find macOS SDK paths and version
+ if sdk_version == '0.0':
+ sdk_version = check_output([xcrun, '--sdk', 'macosx', '--show-sdk-version'],
+ encoding="UTF-8")
+
+ # use xcode tools when installed, still necessary for xcode versions <12.0
+ try:
+ sdk_version = check_output([xcodebuild, '-sdk', 'macosx', '-version', 'ProductVersion'],
+ encoding="UTF-8", stderr=subprocess.DEVNULL)
+ except:
+ pass
+
+ if not isinstance(sdk_version, str):
+ sdk_version = '10.10.0'
+
+ return sdk.strip(),sdk_version.strip()
+
+if __name__ == "__main__":
+ sdk_info = find_macos_sdk()
+ sys.stdout.write(','.join(sdk_info))
diff --git a/TOOLS/macos-swift-lib-directory.py b/TOOLS/macos-swift-lib-directory.py
new file mode 100755
index 0000000..51e8cbe
--- /dev/null
+++ b/TOOLS/macos-swift-lib-directory.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+
+# Finds the macos swift library directory and prints the full path to stdout.
+# First argument is the path to the swift executable.
+
+import os
+import sys
+from shutil import which
+from subprocess import check_output
+
+def find_swift_lib():
+ swift_lib_dir = os.environ.get('SWIFT_LIB_DYNAMIC', '')
+ if swift_lib_dir:
+ return swift_lib_dir
+
+ # first check for lib dir relative to swift executable
+ xcode_dir = os.path.dirname(os.path.dirname(sys.argv[1]))
+ swift_lib_dir = os.path.join(xcode_dir, "lib", "swift", "macosx")
+
+ if os.path.isdir(swift_lib_dir):
+ return swift_lib_dir
+
+ # fallback to xcode-select path
+ xcode_select = which("xcode-select")
+ if not xcode_select:
+ sys.exit(1)
+
+ xcode_path = check_output([xcode_select, "-p"], encoding="UTF-8")
+
+ swift_lib_dir = os.path.join(xcode_path, "Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx")
+ if os.path.isdir(swift_lib_dir):
+ return swift_lib_dir
+
+ # last resort if we still haven't found a path
+ swift_lib_dir = os.path.join(xcode_path, "usr/lib/swift/macosx")
+ if not os.path.isdir(swift_lib_dir):
+ sys.exit(1)
+ return swift_lib_dir
+
+if __name__ == "__main__":
+ swift_lib_dir = find_swift_lib()
+ sys.stdout.write(swift_lib_dir)
diff --git a/TOOLS/matroska.py b/TOOLS/matroska.py
new file mode 100755
index 0000000..52bac48
--- /dev/null
+++ b/TOOLS/matroska.py
@@ -0,0 +1,479 @@
+#!/usr/bin/env python3
+"""
+Generate C definitions for parsing Matroska files.
+Can also be used to directly parse Matroska files and display their contents.
+"""
+
+#
+# This file is part of mpv.
+#
+# mpv is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# mpv is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+elements_ebml = (
+ 'EBML, 1a45dfa3, sub', (
+ 'EBMLVersion, 4286, uint',
+ 'EBMLReadVersion, 42f7, uint',
+ 'EBMLMaxIDLength, 42f2, uint',
+ 'EBMLMaxSizeLength, 42f3, uint',
+ 'DocType, 4282, str',
+ 'DocTypeVersion, 4287, uint',
+ 'DocTypeReadVersion, 4285, uint',
+ ),
+
+ 'CRC32, bf, binary',
+ 'Void, ec, binary',
+)
+
+elements_matroska = (
+ 'Segment, 18538067, sub', (
+
+ 'SeekHead*, 114d9b74, sub', (
+ 'Seek*, 4dbb, sub', (
+ 'SeekID, 53ab, ebml_id',
+ 'SeekPosition, 53ac, uint',
+ ),
+ ),
+
+ 'Info*, 1549a966, sub', (
+ 'SegmentUID, 73a4, binary',
+ 'PrevUID, 3cb923, binary',
+ 'NextUID, 3eb923, binary',
+ 'TimecodeScale, 2ad7b1, uint',
+ 'DateUTC, 4461, sint',
+ 'Title, 7ba9, str',
+ 'MuxingApp, 4d80, str',
+ 'WritingApp, 5741, str',
+ 'Duration, 4489, float',
+ ),
+
+ 'Cluster*, 1f43b675, sub', (
+ 'Timecode, e7, uint',
+ 'BlockGroup*, a0, sub', (
+ 'Block, a1, binary',
+ 'BlockDuration, 9b, uint',
+ 'ReferenceBlock*, fb, sint',
+ 'DiscardPadding, 75A2, sint',
+ 'BlockAdditions, 75A1, sub', (
+ 'BlockMore*, A6, sub', (
+ 'BlockAddID, EE, uint',
+ 'BlockAdditional, A5, binary',
+ ),
+ ),
+ ),
+ 'SimpleBlock*, a3, binary',
+ ),
+
+ 'Tracks*, 1654ae6b, sub', (
+ 'TrackEntry*, ae, sub', (
+ 'TrackNumber, d7, uint',
+ 'TrackUID, 73c5, uint',
+ 'TrackType, 83, uint',
+ 'FlagEnabled, b9, uint',
+ 'FlagDefault, 88, uint',
+ 'FlagForced, 55aa, uint',
+ 'FlagLacing, 9c, uint',
+ 'MinCache, 6de7, uint',
+ 'MaxCache, 6df8, uint',
+ 'DefaultDuration, 23e383, uint',
+ 'TrackTimecodeScale, 23314f, float',
+ 'MaxBlockAdditionID, 55ee, uint',
+ 'Name, 536e, str',
+ 'Language, 22b59c, str',
+ 'CodecID, 86, str',
+ 'CodecPrivate, 63a2, binary',
+ 'CodecName, 258688, str',
+ 'CodecDecodeAll, aa, uint',
+ 'CodecDelay, 56aa, uint',
+ 'SeekPreRoll, 56bb, uint',
+ 'Video, e0, sub', (
+ 'FlagInterlaced, 9a, uint',
+ 'PixelWidth, b0, uint',
+ 'PixelHeight, ba, uint',
+ 'DisplayWidth, 54b0, uint',
+ 'DisplayHeight, 54ba, uint',
+ 'DisplayUnit, 54b2, uint',
+ 'PixelCropTop, 54bb, uint',
+ 'PixelCropLeft, 54cc, uint',
+ 'PixelCropRight, 54dd, uint',
+ 'PixelCropBottom, 54aa, uint',
+ 'FrameRate, 2383e3, float',
+ 'ColourSpace, 2eb524, binary',
+ 'StereoMode, 53b8, uint',
+ 'Colour, 55b0, sub', (
+ 'MatrixCoefficients, 55B1, uint',
+ 'BitsPerChannel, 55B2, uint',
+ 'ChromaSubsamplingHorz, 55B3, uint',
+ 'ChromaSubsamplingVert, 55B4, uint',
+ 'CbSubsamplingHorz, 55B5, uint',
+ 'CbSubsamplingVert, 55B6, uint',
+ 'ChromaSitingHorz, 55B7, uint',
+ 'ChromaSitingVert, 55B8, uint',
+ 'Range, 55B9, uint',
+ 'TransferCharacteristics, 55BA, uint',
+ 'Primaries, 55BB, uint',
+ 'MaxCLL, 55BC, uint',
+ 'MaxFALL, 55BD, uint',
+ 'MasteringMetadata, 55D0, sub', (
+ 'PrimaryRChromaticityX, 55D1, float',
+ 'PrimaryRChromaticityY, 55D2, float',
+ 'PrimaryGChromaticityX, 55D3, float',
+ 'PrimaryGChromaticityY, 55D4, float',
+ 'PrimaryBChromaticityX, 55D5, float',
+ 'PrimaryBChromaticityY, 55D6, float',
+ 'WhitePointChromaticityX, 55D7, float',
+ 'WhitePointChromaticityY, 55D8, float',
+ 'LuminanceMax, 55D9, float',
+ 'LuminanceMin, 55DA, float',
+ ),
+ ),
+ 'Projection, 7670, sub', (
+ 'ProjectionType, 7671, uint',
+ 'ProjectionPrivate, 7672, binary',
+ 'ProjectionPoseYaw, 7673, float',
+ 'ProjectionPosePitch, 7674, float',
+ 'ProjectionPoseRoll, 7675, float',
+ ),
+ ),
+ 'Audio, e1, sub', (
+ 'SamplingFrequency, b5, float',
+ 'OutputSamplingFrequency, 78b5, float',
+ 'Channels, 9f, uint',
+ 'BitDepth, 6264, uint',
+ ),
+ 'ContentEncodings, 6d80, sub', (
+ 'ContentEncoding*, 6240, sub', (
+ 'ContentEncodingOrder, 5031, uint',
+ 'ContentEncodingScope, 5032, uint',
+ 'ContentEncodingType, 5033, uint',
+ 'ContentCompression, 5034, sub', (
+ 'ContentCompAlgo, 4254, uint',
+ 'ContentCompSettings, 4255, binary',
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ 'Cues, 1c53bb6b, sub', (
+ 'CuePoint*, bb, sub', (
+ 'CueTime, b3, uint',
+ 'CueTrackPositions*, b7, sub', (
+ 'CueTrack, f7, uint',
+ 'CueClusterPosition, f1, uint',
+ 'CueRelativePosition, f0, uint',
+ 'CueDuration, b2, uint',
+ ),
+ ),
+ ),
+
+ 'Attachments, 1941a469, sub', (
+ 'AttachedFile*, 61a7, sub', (
+ 'FileDescription, 467e, str',
+ 'FileName, 466e, str',
+ 'FileMimeType, 4660, str',
+ 'FileData, 465c, binary',
+ 'FileUID, 46ae, uint',
+ ),
+ ),
+
+ 'Chapters, 1043a770, sub', (
+ 'EditionEntry*, 45b9, sub', (
+ 'EditionUID, 45bc, uint',
+ 'EditionFlagHidden, 45bd, uint',
+ 'EditionFlagDefault, 45db, uint',
+ 'EditionFlagOrdered, 45dd, uint',
+ 'ChapterAtom*, b6, sub', (
+ 'ChapterUID, 73c4, uint',
+ 'ChapterTimeStart, 91, uint',
+ 'ChapterTimeEnd, 92, uint',
+ 'ChapterFlagHidden, 98, uint',
+ 'ChapterFlagEnabled, 4598, uint',
+ 'ChapterSegmentUID, 6e67, binary',
+ 'ChapterSegmentEditionUID, 6ebc, uint',
+ 'ChapterDisplay*, 80, sub', (
+ 'ChapString, 85, str',
+ 'ChapLanguage*, 437c, str',
+ 'ChapCountry*, 437e, str',
+ ),
+ ),
+ ),
+ ),
+ 'Tags*, 1254c367, sub', (
+ 'Tag*, 7373, sub', (
+ 'Targets, 63c0, sub', (
+ 'TargetTypeValue, 68ca, uint',
+ 'TargetType, 63ca, str',
+ 'TargetTrackUID, 63c5, uint',
+ 'TargetEditionUID, 63c9, uint',
+ 'TargetChapterUID, 63c4, uint',
+ 'TargetAttachmentUID, 63c6, uint',
+ ),
+ 'SimpleTag*, 67c8, sub', (
+ 'TagName, 45a3, str',
+ 'TagLanguage, 447a, str',
+ 'TagString, 4487, str',
+ 'TagDefault, 4484, uint',
+ ),
+ ),
+ ),
+ ),
+)
+
+
+import sys
+from math import ldexp
+from binascii import hexlify
+
+def byte2num(s):
+ return int(hexlify(s), 16)
+
+class EOF(Exception): pass
+
+def camelcase_to_words(name):
+ parts = []
+ start = 0
+ for i in range(1, len(name)):
+ if name[i].isupper() and (name[i-1].islower() or
+ name[i+1:i+2].islower()):
+ parts.append(name[start:i])
+ start = i
+ parts.append(name[start:])
+ return '_'.join(parts).lower()
+
+class MatroskaElement(object):
+
+ def __init__(self, name, elid, valtype, namespace):
+ self.name = name
+ self.definename = '{0}_ID_{1}'.format(namespace, name.upper())
+ self.fieldname = camelcase_to_words(name)
+ self.structname = 'ebml_' + self.fieldname
+ self.elid = elid
+ self.valtype = valtype
+ if valtype == 'sub':
+ self.ebmltype = 'EBML_TYPE_SUBELEMENTS'
+ self.valname = 'struct ' + self.structname
+ else:
+ self.ebmltype = 'EBML_TYPE_' + valtype.upper()
+ try:
+ self.valname = {'uint': 'uint64_t', 'str': 'char *',
+ 'binary': 'bstr', 'ebml_id': 'uint32_t',
+ 'float': 'double', 'sint': 'int64_t',
+ }[valtype]
+ except KeyError:
+ raise SyntaxError('Unrecognized value type ' + valtype)
+ self.subelements = ()
+
+ def add_subelements(self, subelements):
+ self.subelements = subelements
+ self.subids = {x[0].elid for x in subelements}
+
+elementd = {}
+elementlist = []
+def parse_elems(l, namespace):
+ subelements = []
+ for el in l:
+ if isinstance(el, str):
+ name, hexid, eltype = [x.strip() for x in el.split(',')]
+ hexid = hexid.lower()
+ multiple = name.endswith('*')
+ name = name.strip('*')
+ new = MatroskaElement(name, hexid, eltype, namespace)
+ elementd[hexid] = new
+ elementlist.append(new)
+ subelements.append((new, multiple))
+ else:
+ new.add_subelements(parse_elems(el, namespace))
+ return subelements
+
+parse_elems(elements_ebml, 'EBML')
+parse_elems(elements_matroska, 'MATROSKA')
+
+def printf(out, *args):
+ out.write(' '.join(str(x) for x in args))
+ out.write('\n')
+
+def generate_C_header(out):
+ printf(out, '// Generated by TOOLS/matroska.py, do not edit manually')
+ printf(out)
+
+ for el in elementlist:
+ printf(out, '#define {0.definename:40} 0x{0.elid}'.format(el))
+
+ printf(out)
+
+ for el in reversed(elementlist):
+ if not el.subelements:
+ continue
+ printf(out)
+ printf(out, 'struct {0.structname} {{'.format(el))
+ l = max(len(subel.valname) for subel, multiple in el.subelements)+1
+ for subel, multiple in el.subelements:
+ printf(out, ' {e.valname:{l}} {star}{e.fieldname};'.format(
+ e=subel, l=l, star=' *'[multiple]))
+ printf(out)
+ for subel, multiple in el.subelements:
+ printf(out, ' int n_{0.fieldname};'.format(subel))
+ printf(out, '};')
+
+ for el in elementlist:
+ if not el.subelements:
+ continue
+ printf(out, 'extern const struct ebml_elem_desc {0.structname}_desc;'.format(el))
+
+ printf(out)
+ printf(out, '#define MAX_EBML_SUBELEMENTS', max(len(el.subelements)
+ for el in elementlist))
+
+
+def generate_C_definitions(out):
+ printf(out, '// Generated by TOOLS/matroska.py, do not edit manually')
+ printf(out)
+ for el in reversed(elementlist):
+ printf(out)
+ if el.subelements:
+ printf(out, '#define N', el.fieldname)
+ printf(out, 'E_S("{0}", {1})'.format(el.name, len(el.subelements)))
+ for subel, multiple in el.subelements:
+ printf(out, 'F({0.definename}, {0.fieldname}, {1})'.format(
+ subel, int(multiple)))
+ printf(out, '}};')
+ printf(out, '#undef N')
+ else:
+ printf(out, 'E("{0.name}", {0.fieldname}, {0.ebmltype})'.format(el))
+
+def read(s, length):
+ t = s.read(length)
+ if len(t) != length:
+ raise EOF
+ return t
+
+def read_id(s):
+ t = read(s, 1)
+ i = 0
+ mask = 128
+ if ord(t) == 0:
+ raise SyntaxError
+ while not ord(t) & mask:
+ i += 1
+ mask >>= 1
+ t += read(s, i)
+ return t
+
+def read_vint(s):
+ t = read(s, 1)
+ i = 0
+ mask = 128
+ if ord(t) == 0:
+ raise SyntaxError
+ while not ord(t) & mask:
+ i += 1
+ mask >>= 1
+ t = bytes((ord(t) & (mask - 1),))
+ t += read(s, i)
+ return i+1, byte2num(t)
+
+def read_str(s, length):
+ return read(s, length)
+
+def read_uint(s, length):
+ t = read(s, length)
+ return byte2num(t)
+
+def read_sint(s, length):
+ i = read_uint(s, length)
+ mask = 1 << (length * 8 - 1)
+ if i & mask:
+ i -= 2 * mask
+ return i
+
+def read_float(s, length):
+ t = read(s, length)
+ i = byte2num(t)
+ if length == 4:
+ f = ldexp((i & 0x7fffff) + (1 << 23), (i >> 23 & 0xff) - 150)
+ if i & (1 << 31):
+ f = -f
+ elif length == 8:
+ f = ldexp((i & ((1 << 52) - 1)) + (1 << 52), (i >> 52 & 0x7ff) - 1075)
+ if i & (1 << 63):
+ f = -f
+ else:
+ raise SyntaxError
+ return f
+
+def parse_one(s, depth, parent, maxlen):
+ elid = hexlify(read_id(s)).decode('ascii')
+ elem = elementd.get(elid)
+ size, length = read_vint(s)
+ this_length = len(elid) / 2 + size + length
+ if elem is not None:
+ if elem.valtype != 'skip':
+ print(" " * depth, '[' + elid + ']', elem.name, 'size:', length, 'value:', end=' ')
+ if elem.valtype == 'sub':
+ print('subelements:')
+ while length > 0:
+ length -= parse_one(s, depth + 1, elem, length)
+ if length < 0:
+ raise SyntaxError
+ elif elem.valtype == 'str':
+ print('string', repr(read_str(s, length).decode('utf8', 'replace')))
+ elif elem.valtype in ('binary', 'ebml_id'):
+ t = read_str(s, length)
+ dec = ''
+ if elem.valtype == 'ebml_id':
+ idelem = elementd.get(hexlify(t).decode('ascii'))
+ if idelem is None:
+ dec = '(UNKNOWN)'
+ else:
+ dec = '({0.name})'.format(idelem)
+ if len(t) < 20:
+ t = hexlify(t).decode('ascii')
+ else:
+ t = '<{0} bytes>'.format(len(t))
+ print('binary', t, dec)
+ elif elem.valtype == 'uint':
+ print('uint', read_uint(s, length))
+ elif elem.valtype == 'sint':
+ print('sint', read_sint(s, length))
+ elif elem.valtype == 'float':
+ print('float', read_float(s, length))
+ elif elem.valtype == 'skip':
+ read(s, length)
+ else:
+ raise NotImplementedError
+ else:
+ print(" " * depth, '[' + elid + '] Unknown element! size:', length)
+ read(s, length)
+ return this_length
+
+if __name__ == "__main__":
+ def parse_toplevel(s):
+ parse_one(s, 0, None, 1 << 63)
+
+ if sys.argv[1] == '--generate-header':
+ generate_C_header(open(sys.argv[2], "w"))
+ elif sys.argv[1] == '--generate-definitions':
+ generate_C_definitions(open(sys.argv[2], "w"))
+ else:
+ s = open(sys.argv[1], "rb")
+ while 1:
+ start = s.tell()
+ try:
+ parse_toplevel(s)
+ except EOF:
+ if s.tell() != start:
+ raise Exception("Unexpected end of file")
+ break
diff --git a/TOOLS/mpv-osd-symbols.sfdir/.notdef.glyph b/TOOLS/mpv-osd-symbols.sfdir/.notdef.glyph
new file mode 100644
index 0000000..99cdece
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/.notdef.glyph
@@ -0,0 +1,6 @@
+StartChar: .notdef
+Encoding: 65536 -1 0
+Width: 400
+Flags: W
+LayerCount: 2
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/font.props b/TOOLS/mpv-osd-symbols.sfdir/font.props
new file mode 100644
index 0000000..8198d27
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/font.props
@@ -0,0 +1,77 @@
+SplineFontDB: 3.0
+FontName: mpv-osd-symbols-Regular
+FullName: mpv-osd-symbols Regular
+FamilyName: mpv-osd-symbols
+Weight: Normal
+Copyright: This is generated file.
+Version: 001.000
+ItalicAngle: 0
+UnderlinePosition: -133
+UnderlineWidth: 50
+Ascent: 800
+Descent: 200
+InvalidEm: 0
+sfntRevision: 0x00010000
+LayerCount: 2
+Layer: 0 0 "Back" 1
+Layer: 1 0 "Fore" 0
+XUID: [1021 879 -1597228462 15927]
+StyleMap: 0x0040
+FSType: 8
+OS2Version: 3
+OS2_WeightWidthSlopeOnly: 0
+OS2_UseTypoMetrics: 0
+CreationTime: 1408646554
+ModificationTime: 1576096543
+PfmFamily: 81
+TTFWeight: 400
+TTFWidth: 5
+LineGap: 0
+VLineGap: 0
+Panose: 0 0 5 0 0 0 0 0 0 0
+OS2TypoAscent: 800
+OS2TypoAOffset: 0
+OS2TypoDescent: -200
+OS2TypoDOffset: 0
+OS2TypoLinegap: 90
+OS2WinAscent: 1000
+OS2WinAOffset: 0
+OS2WinDescent: 200
+OS2WinDOffset: 0
+HheadAscent: 1000
+HheadAOffset: 0
+HheadDescent: -200
+HheadDOffset: 0
+OS2SubXSize: 650
+OS2SubYSize: 600
+OS2SubXOff: 0
+OS2SubYOff: 75
+OS2SupXSize: 650
+OS2SupYSize: 600
+OS2SupXOff: 0
+OS2SupYOff: 350
+OS2StrikeYSize: 50
+OS2StrikeYPos: 220
+OS2Vendor: 'PfEd'
+OS2CodePages: 00000001.00000000
+OS2UnicodeRanges: 00000000.00000000.00000000.00000000
+DEI: 91125
+LangName: 1033 "" "" "Regular" "1.000;PfEd;mpv-osd-symbols-Regular" "mpv-osd-symbols" "Version 1.000;PS 001.000;hotconv 1.0.70;makeotf.lib2.5.58329"
+Encoding: UnicodeBmp
+UnicodeInterp: none
+NameList: AGL For New Fonts
+DisplaySize: -72
+AntiAlias: 1
+FitToEm: 0
+WinInfo: 57600 8 2
+BeginPrivate: 8
+BlueValues 31 [-10 0 640 650 720 730 800 810]
+BlueScale 5 0.037
+BlueShift 1 0
+BlueFuzz 1 0
+StdHW 4 [65]
+StdVW 4 [65]
+StemSnapH 8 [65 800]
+StemSnapV 8 [65 150]
+EndPrivate
+EndSplineFont
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE001.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE001.glyph
new file mode 100644
index 0000000..ebfb715
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE001.glyph
@@ -0,0 +1,16 @@
+StartChar: uniE001
+Encoding: 57345 57345 1
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 780 20G<200 200>
+VStem: 200 375<400 400 400 800>
+LayerCount: 2
+Fore
+SplineSet
+575 400 m 1
+ 200 0 l 1
+ 200 800 l 1
+ 575 400 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE002.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE002.glyph
new file mode 100644
index 0000000..f47c153
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE002.glyph
@@ -0,0 +1,22 @@
+StartChar: uniE002
+Encoding: 57346 57346 2
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 0 800<200 350 200 350 200 500 500 650>
+VStem: 200 150<0 800 0 800> 500 150<0 800 0 800>
+LayerCount: 2
+Fore
+SplineSet
+350 800 m 1
+ 350 0 l 1
+ 200 0 l 1
+ 200 800 l 1
+ 350 800 l 1
+650 800 m 1
+ 650 0 l 1
+ 500 0 l 1
+ 500 800 l 1
+ 650 800 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE003.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE003.glyph
new file mode 100644
index 0000000..ba45630
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE003.glyph
@@ -0,0 +1,17 @@
+StartChar: uniE003
+Encoding: 57347 57347 3
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 0 720<110 775 110 775>
+VStem: 110 665<0 720 0 720>
+LayerCount: 2
+Fore
+SplineSet
+775 720 m 1
+ 775 0 l 1
+ 110 0 l 1
+ 110 720 l 1
+ 775 720 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE004.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE004.glyph
new file mode 100644
index 0000000..62ce5f7
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE004.glyph
@@ -0,0 +1,20 @@
+StartChar: uniE004
+Encoding: 57348 57348 4
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 780 20G<423 423 813 813>
+VStem: 48 765<400 400>
+LayerCount: 2
+Fore
+SplineSet
+423 800 m 1
+ 423 0 l 1
+ 48 400 l 1
+ 423 800 l 1
+813 800 m 1
+ 813 0 l 1
+ 438 400 l 1
+ 813 800 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE005.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE005.glyph
new file mode 100644
index 0000000..26db474
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE005.glyph
@@ -0,0 +1,20 @@
+StartChar: uniE005
+Encoding: 57349 57349 5
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 780 20G<95 95 485 485>
+VStem: 95 765<400 400 400 800 400 800>
+LayerCount: 2
+Fore
+SplineSet
+470 400 m 1
+ 95 0 l 1
+ 95 800 l 1
+ 470 400 l 1
+860 400 m 1
+ 485 0 l 1
+ 485 800 l 1
+ 860 400 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE006.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE006.glyph
new file mode 100644
index 0000000..474d3c1
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE006.glyph
@@ -0,0 +1,47 @@
+StartChar: uniE006
+Encoding: 57350 57350 6
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: -10 11<396 461 396 543> 196 171<397 461 396 462> 401 31 801 9<397 462 397 397>
+VStem: 20 9<369 434 369 513> 224 173<369 408 367 434 367 434> 363 34 397 65<367 404 367 408 367 404 606 801> 462 173<368 404 404 404> 830 10<368 433 433 433>
+LayerCount: 2
+Fore
+SplineSet
+430 810 m 0x9940
+ 656 810 840 626 840 400 c 0
+ 840 174 656 -10 430 -10 c 0
+ 204 -10 20 174 20 400 c 0
+ 20 626 204 810 430 810 c 0x9940
+397 606 m 1x5280
+ 462 606 l 1
+ 462 801 l 1
+ 397 801 l 1
+ 397 606 l 1x5280
+462 367 m 1
+ 462 404 l 1
+ 710 627 l 1
+ 683 657 l 1
+ 439 437 l 1
+ 363 592 l 1
+ 318 570 l 1x4280
+ 397 408 l 1
+ 397 367 l 1x44
+ 462 367 l 1
+224 434 m 1
+ 29 434 l 1
+ 29 369 l 1
+ 224 369 l 1
+ 224 434 l 1
+830 368 m 1
+ 830 433 l 1
+ 635 433 l 1
+ 635 368 l 1
+ 830 368 l 1
+461 196 m 1
+ 396 196 l 1
+ 396 1 l 1
+ 461 1 l 1
+ 461 196 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE007.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE007.glyph
new file mode 100644
index 0000000..719d9fa
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE007.glyph
@@ -0,0 +1,21 @@
+StartChar: uniE007
+Encoding: 57351 57351 7
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: -10 80<339 430 339 543> 730 80<339 430>
+VStem: 20 80<309 491 309 513>
+LayerCount: 2
+Fore
+SplineSet
+430 -10 m 0
+ 204 -10 20 174 20 400 c 0
+ 20 626 204 810 430 810 c 0
+ 656 810 840 626 840 400 c 0
+ 840 174 656 -10 430 -10 c 0
+430 70 m 1
+ 430 730 l 1
+ 248 730 100 582 100 400 c 0
+ 100 218 248 70 430 70 c 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE008.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE008.glyph
new file mode 100644
index 0000000..b36acfb
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE008.glyph
@@ -0,0 +1,37 @@
+StartChar: uniE008
+Encoding: 57352 57352 8
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: -10 80<339 521 339 543> 240 160<269 313 546 590> 480 160<408 452> 730 80<339 521>
+VStem: 20 80<309 491 309 513> 211 160<298 342> 350 160<538 582> 488 160<298 342> 760 80<309 491>
+LayerCount: 2
+Fore
+SplineSet
+430 -10 m 0xfa80
+ 204 -10 20 174 20 400 c 0
+ 20 626 204 810 430 810 c 0
+ 656 810 840 626 840 400 c 0
+ 840 174 656 -10 430 -10 c 0xfa80
+430 70 m 0
+ 612 70 760 218 760 400 c 0
+ 760 582 612 730 430 730 c 0
+ 248 730 100 582 100 400 c 0
+ 100 218 248 70 430 70 c 0
+430 480 m 0
+ 386 480 350 516 350 560 c 0
+ 350 604 386 640 430 640 c 0
+ 474 640 510 604 510 560 c 0
+ 510 516 474 480 430 480 c 0
+291 240 m 0xfd80
+ 247 240 211 276 211 320 c 0
+ 211 364 247 400 291 400 c 0
+ 335 400 371 364 371 320 c 0
+ 371 276 335 240 291 240 c 0xfd80
+568 240 m 0
+ 524 240 488 276 488 320 c 0
+ 488 364 524 400 568 400 c 0
+ 612 400 648 364 648 320 c 0
+ 648 276 612 240 568 240 c 0
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE009.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE009.glyph
new file mode 100644
index 0000000..26a2f37
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE009.glyph
@@ -0,0 +1,17 @@
+StartChar: uniE009
+Encoding: 57353 57353 9
+Width: 880
+Flags: HMW
+LayerCount: 2
+Fore
+SplineSet
+716 10 m 1
+ 378 244 l 1
+ 165 244 l 1
+ 165 559 l 1
+ 379 559 l 1
+ 716 793 l 1
+ 716 10 l 1
+EndSplineSet
+Validated: 1
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE00A.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE00A.glyph
new file mode 100644
index 0000000..634249f
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE00A.glyph
@@ -0,0 +1,50 @@
+StartChar: uniE00A
+Encoding: 57354 57354 10
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 252 48<406 406 456 456> 375 50<57 282 57 282 578 805> 500 48<406 406> 627 20G<149 149 713 713>
+VStem: 282 48<375 375 425 425> 406 50<26 252 26 252 548 774> 530 48<375 375>
+LayerCount: 2
+Fore
+SplineSet
+805 375 m 1
+ 578 375 l 1
+ 574 352 565 331 553 313 c 1
+ 713 153 l 1
+ 678 118 l 1
+ 518 278 l 1
+ 500 265 479 256 456 252 c 1
+ 456 26 l 1
+ 406 26 l 1
+ 406 252 l 1
+ 383 256 362 265 343 277 c 1
+ 184 118 l 1
+ 149 153 l 1
+ 308 312 l 1
+ 295 330 286 352 282 375 c 1
+ 57 375 l 1
+ 57 425 l 1
+ 282 425 l 1
+ 286 448 295 470 308 488 c 1
+ 149 647 l 1
+ 184 682 l 1
+ 343 523 l 1
+ 362 535 383 544 406 548 c 1
+ 406 774 l 1
+ 456 774 l 1
+ 456 548 l 1
+ 479 544 500 535 518 522 c 1
+ 678 682 l 1
+ 713 647 l 1
+ 553 487 l 1
+ 565 469 574 448 578 425 c 1
+ 805 425 l 1
+ 805 375 l 1
+430 300 m 0
+ 485 300 530 345 530 400 c 0
+ 530 455 485 500 430 500 c 0
+ 375 500 330 455 330 400 c 0
+ 330 345 375 300 430 300 c 0
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE00B.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE00B.glyph
new file mode 100644
index 0000000..21e0966
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE00B.glyph
@@ -0,0 +1,27 @@
+StartChar: uniE00B
+Encoding: 57355 57355 11
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: -10 244<341 519 341 543> 566 54<413.5 446.5 413.5 519> 740 70<413.5 446.5>
+VStem: 20 88<354 446 354 513> 752 88<354 446>
+LayerCount: 2
+Fore
+SplineSet
+430 -10 m 0
+ 204 -10 20 174 20 400 c 0
+ 20 626 204 810 430 810 c 0
+ 656 810 840 626 840 400 c 0
+ 840 174 656 -10 430 -10 c 0
+430 620 m 0
+ 463 620 490 647 490 680 c 0
+ 490 713 463 740 430 740 c 0
+ 397 740 370 713 370 680 c 0
+ 370 647 397 620 430 620 c 0
+430 234 m 0
+ 608 234 752 308 752 400 c 0
+ 752 492 608 566 430 566 c 0
+ 252 566 108 492 108 400 c 0
+ 108 308 252 234 430 234 c 0
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE010.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE010.glyph
new file mode 100644
index 0000000..519e34a
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE010.glyph
@@ -0,0 +1,21 @@
+StartChar: uniE010
+Encoding: 57360 57360 12
+Width: 334
+GlyphClass: 2
+Flags: MW
+HStem: 0 90<221 258 221 258> 550 90<221 258 221 221>
+VStem: 76 145<90 550 90 640 90 640>
+LayerCount: 2
+Fore
+SplineSet
+258 640 m 1
+ 258 550 l 1
+ 221 550 l 1
+ 221 90 l 1
+ 258 90 l 1
+ 258 0 l 1
+ 76 0 l 1
+ 76 640 l 1
+ 258 640 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE011.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE011.glyph
new file mode 100644
index 0000000..b3c7b56
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE011.glyph
@@ -0,0 +1,17 @@
+StartChar: uniE011
+Encoding: 57361 57361 13
+Width: 334
+GlyphClass: 2
+Flags: MW
+HStem: 0 640<84 250 84 250>
+VStem: 84 166<0 640 0 640>
+LayerCount: 2
+Fore
+SplineSet
+250 640 m 1
+ 250 0 l 1
+ 84 0 l 1
+ 84 640 l 1
+ 250 640 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE012.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE012.glyph
new file mode 100644
index 0000000..86c07cd
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE012.glyph
@@ -0,0 +1,21 @@
+StartChar: uniE012
+Encoding: 57362 57362 14
+Width: 334
+GlyphClass: 2
+Flags: MW
+HStem: 0 90<76 113 76 258 76 113> 550 90<76 113 76 258>
+VStem: 113 145<90 550 550 550>
+LayerCount: 2
+Fore
+SplineSet
+113 90 m 1
+ 113 550 l 1
+ 76 550 l 1
+ 76 640 l 1
+ 258 640 l 1
+ 258 0 l 1
+ 76 0 l 1
+ 76 90 l 1
+ 113 90 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE013.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE013.glyph
new file mode 100644
index 0000000..b456777
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE013.glyph
@@ -0,0 +1,17 @@
+StartChar: uniE013
+Encoding: 57363 57363 15
+Width: 334
+GlyphClass: 2
+Flags: MW
+HStem: 255 130<102 232 102 232>
+VStem: 102 130<255 385 255 385>
+LayerCount: 2
+Fore
+SplineSet
+232 385 m 1
+ 232 255 l 1
+ 102 255 l 1
+ 102 385 l 1
+ 232 385 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE101.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE101.glyph
new file mode 100644
index 0000000..8e0d4b6
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE101.glyph
@@ -0,0 +1,16 @@
+StartChar: uniE101
+Encoding: 57601 57601 16
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 780 20G<222 222>
+VStem: 222 600<400 400 400 800 400 800>
+LayerCount: 2
+Fore
+SplineSet
+822 400 m 1
+ 222 0 l 1
+ 222 800 l 1
+ 822 400 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE104.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE104.glyph
new file mode 100644
index 0000000..7c7ba13
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE104.glyph
@@ -0,0 +1,25 @@
+StartChar: uniE104
+Encoding: 57604 57604 17
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 0 800<3 126 3 126 3 502 3 878> 780 20G<3 126 126 126 502 502 878 878>
+VStem: 3 123<0 800 0 800>
+LayerCount: 2
+Fore
+SplineSet
+126 800 m 1x60
+ 126 0 l 1
+ 3 0 l 1xa0
+ 3 800 l 1
+ 126 800 l 1x60
+502 800 m 1
+ 502 0 l 1
+ 127 400 l 1
+ 502 800 l 1
+878 800 m 1
+ 878 0 l 1
+ 503 400 l 1
+ 878 800 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE105.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE105.glyph
new file mode 100644
index 0000000..bb0f87f
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE105.glyph
@@ -0,0 +1,25 @@
+StartChar: uniE105
+Encoding: 57605 57605 18
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 0 800<3 3 3 379 3 755 755 878> 780 20G<3 3 379 379 755 878 878 878>
+VStem: 755 123<0 800>
+LayerCount: 2
+Fore
+SplineSet
+3 0 m 1x60
+ 3 800 l 1
+ 378 400 l 1
+ 3 0 l 1x60
+379 0 m 1xa0
+ 379 800 l 1
+ 754 400 l 1
+ 379 0 l 1xa0
+878 0 m 1
+ 755 0 l 1
+ 755 800 l 1
+ 878 800 l 1
+ 878 0 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE106.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE106.glyph
new file mode 100644
index 0000000..b7d6ca2
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE106.glyph
@@ -0,0 +1,22 @@
+StartChar: uniE106
+Encoding: 57606 57606 19
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 0 649<68.5 556.5>
+VStem: 0 880
+LayerCount: 2
+Fore
+SplineSet
+880 380 m 0
+ 880 230 685 101 449 101 c 0
+ 390 101 336 107 283 118 c 1
+ 278 49 189 0 85 0 c 0
+ 52 0 24 14 0 40 c 1
+ 82 48 137 93 137 158 c 0
+ 137 163 137 168 135 172 c 0
+ 58 217 19 273 19 380 c 0
+ 19 528 258 649 449 649 c 0
+ 664 649 880 508 880 380 c 0
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE107.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE107.glyph
new file mode 100644
index 0000000..01065b6
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE107.glyph
@@ -0,0 +1,56 @@
+StartChar: uniE107
+Encoding: 57607 57607 20
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 0 60<0 60 60 820> 106 50<146 343 146 343 146 390 390 641 680 735> 207 50<99 317 99 317 99 363 363 488 529 781> 580 60<60 820 60 820>
+VStem: 0 60<60 580> 99 218<207 257 207 257> 146 197<106 156 106 156> 363 125<207 257 207 257> 529 112<106 257 106 257> 680 55<106 156 106 156> 820 60<0 60 60 580>
+LayerCount: 2
+Fore
+SplineSet
+0 60 m 2xfde0
+ 0 580 l 2
+ 0 610 30 640 60 640 c 2
+ 820 640 l 2
+ 850 640 880 610 880 580 c 2
+ 880 60 l 2
+ 880 30 850 0 820 0 c 2
+ 60 0 l 2
+ 30 0 0 30 0 60 c 2xfde0
+60 60 m 1
+ 820 60 l 1
+ 820 580 l 1
+ 60 580 l 1
+ 60 60 l 1
+317 257 m 1
+ 317 207 l 1
+ 99 207 l 1
+ 99 257 l 1
+ 317 257 l 1
+488 257 m 1
+ 488 207 l 1
+ 363 207 l 1
+ 363 257 l 1
+ 488 257 l 1
+781 257 m 1
+ 781 207 l 1
+ 529 207 l 1
+ 529 257 l 1
+ 781 257 l 1
+343 156 m 1x4280
+ 343 106 l 1
+ 146 106 l 1
+ 146 156 l 1
+ 343 156 l 1x4280
+641 156 m 1
+ 641 106 l 1
+ 390 106 l 1
+ 390 156 l 1x41c0
+ 641 156 l 1
+735 156 m 1
+ 735 106 l 1
+ 680 106 l 1
+ 680 156 l 1
+ 735 156 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE108.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE108.glyph
new file mode 100644
index 0000000..83659e3
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE108.glyph
@@ -0,0 +1,39 @@
+StartChar: uniE108
+Encoding: 57608 57608 21
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 80 60<0 60 60 820> 321 159<274 606 274 606> 660 60<60 820 60 820>
+VStem: 0 60<140 660> 274 332<321 480 321 480> 820 60<80 140 140 660>
+LayerCount: 2
+Fore
+SplineSet
+0 140 m 2
+ 0 660 l 2
+ 0 690 30 720 60 720 c 2
+ 820 720 l 2
+ 850 720 880 690 880 660 c 2
+ 880 140 l 2
+ 880 110 850 80 820 80 c 2
+ 60 80 l 2
+ 30 80 0 110 0 140 c 2
+60 140 m 1
+ 820 140 l 1
+ 820 660 l 1
+ 60 660 l 1
+ 60 140 l 1
+580 600 m 1
+ 760 600 l 1
+ 760 420 l 1
+ 580 600 l 1
+606 480 m 1
+ 606 321 l 1
+ 274 321 l 1
+ 274 480 l 1
+ 606 480 l 1
+300 200 m 1
+ 120 200 l 1
+ 120 380 l 1
+ 300 200 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE109.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE109.glyph
new file mode 100644
index 0000000..4038f3d
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE109.glyph
@@ -0,0 +1,39 @@
+StartChar: uniE109
+Encoding: 57609 57609 22
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 80 60<0 60 60 820> 296 209<285 595 285 595> 660 60<60 820 60 820>
+VStem: 0 60<140 660> 120 107<400 400 400 510> 285 310<296 505 296 505> 653 107<400 400> 820 60<80 140 140 660>
+LayerCount: 2
+Fore
+SplineSet
+0 140 m 2
+ 0 660 l 2
+ 0 690 30 720 60 720 c 2
+ 820 720 l 2
+ 850 720 880 690 880 660 c 2
+ 880 140 l 2
+ 880 110 850 80 820 80 c 2
+ 60 80 l 2
+ 30 80 0 110 0 140 c 2
+60 140 m 1
+ 820 140 l 1
+ 820 660 l 1
+ 60 660 l 1
+ 60 140 l 1
+227 400 m 1
+ 120 290 l 1
+ 120 510 l 1
+ 227 400 l 1
+760 290 m 1
+ 653 400 l 1
+ 760 510 l 1
+ 760 290 l 1
+595 505 m 1
+ 595 296 l 1
+ 285 296 l 1
+ 285 505 l 1
+ 595 505 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE10A.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE10A.glyph
new file mode 100644
index 0000000..029bf74
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE10A.glyph
@@ -0,0 +1,36 @@
+StartChar: uniE10A
+Encoding: 57610 57610 23
+Width: 1977
+GlyphClass: 2
+Flags: HMWO
+HStem: 242 248<2 155 2 156>
+LayerCount: 2
+Fore
+SplineSet
+398 58 m 1
+ 155 242 l 1
+ 2 242 l 1
+ 2 490 l 1
+ 156 490 l 1
+ 398 674 l 1
+ 398 58 l 1
+809 524 m 0
+ 814 524 819 523 823 519 c 0
+ 831 511 831 499 823 491 c 2
+ 697 365 l 1
+ 823 239 l 2
+ 830 232 830 221 823 213 c 0
+ 816 206 804 206 797 213 c 2
+ 671 339 l 1
+ 545 213 l 2
+ 538 206 525 206 518 213 c 0
+ 510 221 510 233 518 241 c 2
+ 643 367 l 1
+ 517 493 l 2
+ 510 500 510 511 517 519 c 0
+ 525 526 536 526 544 519 c 2
+ 670 393 l 1
+ 796 519 l 2
+ 799 523 804 524 809 524 c 0
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE10B.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE10B.glyph
new file mode 100644
index 0000000..780b4e4
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE10B.glyph
@@ -0,0 +1,26 @@
+StartChar: uniE10B
+Encoding: 57611 57611 24
+Width: 1977
+GlyphClass: 2
+Flags: MW
+HStem: 242 248<2 155 2 156>
+VStem: 603 47<326.5 405>
+LayerCount: 2
+Fore
+SplineSet
+650 366 m 0
+ 650 281 615 198 545 119 c 1
+ 507 142 l 1
+ 571 213 603 287 603 366 c 0
+ 603 444 571 518 507 590 c 1
+ 545 613 l 1
+ 615 534 650 452 650 366 c 0
+398 58 m 1
+ 155 242 l 1
+ 2 242 l 1
+ 2 490 l 1
+ 156 490 l 1
+ 398 674 l 1
+ 398 58 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE10C.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE10C.glyph
new file mode 100644
index 0000000..b17f57c
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE10C.glyph
@@ -0,0 +1,33 @@
+StartChar: uniE10C
+Encoding: 57612 57612 25
+Width: 1977
+GlyphClass: 2
+Flags: MW
+HStem: 242 248<2 155 2 156>
+VStem: 603 47<326.5 405> 717 47<316.5 416>
+LayerCount: 2
+Fore
+SplineSet
+764 366 m 0
+ 764 258 720 156 631 61 c 1
+ 593 84 l 1
+ 675 173 717 267 717 366 c 0
+ 717 466 675 559 593 647 c 1
+ 631 671 l 1
+ 720 578 764 476 764 366 c 0
+650 366 m 0
+ 650 281 615 198 545 119 c 1
+ 507 142 l 1
+ 571 213 603 287 603 366 c 0
+ 603 444 571 518 507 590 c 1
+ 545 613 l 1
+ 615 534 650 452 650 366 c 0
+398 58 m 1
+ 155 242 l 1
+ 2 242 l 1
+ 2 490 l 1
+ 156 490 l 1
+ 398 674 l 1
+ 398 58 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE10D.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE10D.glyph
new file mode 100644
index 0000000..24e0acf
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE10D.glyph
@@ -0,0 +1,40 @@
+StartChar: uniE10D
+Encoding: 57613 57613 26
+Width: 1977
+GlyphClass: 2
+Flags: MW
+HStem: 242 248<2 155 2 156> 709 20G<717 717>
+VStem: 603 47<326.5 405> 717 47<316.5 416> 830 47<304 426.5>
+LayerCount: 2
+Fore
+SplineSet
+877 366 m 0
+ 877 236 824 115 717 3 c 1
+ 679 27 l 1
+ 780 130 830 242 830 366 c 0
+ 830 487 780 600 679 705 c 1
+ 717 729 l 1
+ 824 617 877 496 877 366 c 0
+764 366 m 0
+ 764 258 720 156 631 61 c 1
+ 593 84 l 1
+ 675 173 717 267 717 366 c 0
+ 717 466 675 559 593 647 c 1
+ 631 671 l 1
+ 720 578 764 476 764 366 c 0
+650 366 m 0
+ 650 281 615 198 545 119 c 1
+ 507 142 l 1
+ 571 213 603 287 603 366 c 0
+ 603 444 571 518 507 590 c 1
+ 545 613 l 1
+ 615 534 650 452 650 366 c 0
+398 58 m 1
+ 155 242 l 1
+ 2 242 l 1
+ 2 490 l 1
+ 156 490 l 1
+ 398 674 l 1
+ 398 58 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE10E.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE10E.glyph
new file mode 100644
index 0000000..17b4af6
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE10E.glyph
@@ -0,0 +1,53 @@
+StartChar: uniE10E
+Encoding: 57614 57614 27
+Width: 1977
+GlyphClass: 2
+Flags: MW
+HStem: 49 95<806 818.5> 242 248<2 155 2 156>
+VStem: 559 47<326.5 405> 673 47<316.5 416> 767 91<90 103.5 648 652>
+LayerCount: 2
+Fore
+SplineSet
+720 366 m 0
+ 720 258 676 156 587 61 c 1
+ 549 84 l 1
+ 631 173 673 267 673 366 c 0
+ 673 466 631 559 549 647 c 1
+ 587 671 l 1
+ 676 578 720 476 720 366 c 0
+606 366 m 0
+ 606 281 571 198 501 119 c 1
+ 463 142 l 1
+ 527 213 559 287 559 366 c 0
+ 559 444 527 518 463 590 c 1
+ 501 613 l 1
+ 571 534 606 452 606 366 c 0
+398 58 m 1
+ 155 242 l 1
+ 2 242 l 1
+ 2 490 l 1
+ 156 490 l 1
+ 398 674 l 1
+ 398 58 l 1
+858 648 m 1
+ 824 238 l 2
+ 823 224 819 217 812 217 c 0
+ 805 217 801 224 800 238 c 2
+ 767 648 l 1
+ 767 652 l 2
+ 767 665 771 675 780 682 c 0
+ 789 690 800 694 812 694 c 0
+ 825 694 835 690 844 682 c 0
+ 853 675 858 665 858 652 c 2
+ 858 648 l 1
+858 97 m 0
+ 858 83 853 72 845 63 c 0
+ 836 53 825 49 812 49 c 0
+ 800 49 789 53 780 63 c 0
+ 771 72 767 83 767 97 c 0
+ 767 110 771 121 780 130 c 0
+ 789 140 800 144 812 144 c 0
+ 825 144 836 140 845 130 c 0
+ 853 121 858 110 858 97 c 0
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE110.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE110.glyph
new file mode 100644
index 0000000..f4d22f5
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE110.glyph
@@ -0,0 +1,16 @@
+StartChar: uniE110
+Encoding: 57616 57616 28
+Width: 880
+GlyphClass: 2
+Flags: MW
+HStem: 780 20G<656 656>
+VStem: 56 600<400 400>
+LayerCount: 2
+Fore
+SplineSet
+656 0 m 1
+ 56 400 l 1
+ 656 800 l 1
+ 656 0 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE111.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE111.glyph
new file mode 100644
index 0000000..370f803
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE111.glyph
@@ -0,0 +1,37 @@
+StartChar: uniE111
+Encoding: 57617 57617 29
+Width: 880
+GlyphClass: 2
+Flags: W
+LayerCount: 2
+Fore
+SplineSet
+0 140 m 2
+ 0 660 l 2
+ 0 690 30 720 60 720 c 2
+ 820 720 l 2
+ 850 720 880 690 880 660 c 2
+ 880 140 l 2
+ 880 110 850 80 820 80 c 2
+ 60 80 l 2
+ 30 80 0 110 0 140 c 2
+60 140 m 1
+ 820 140 l 1
+ 820 660 l 1
+ 60 660 l 1
+ 60 140 l 1
+227 400 m 1
+ 120 290 l 1
+ 120 510 l 1
+ 227 400 l 1
+760 290 m 1
+ 653 400 l 1
+ 760 510 l 1
+ 760 290 l 1
+595 505 m 1
+ 595 296 l 1
+ 285 296 l 1
+ 285 505 l 1
+ 595 505 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE112.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE112.glyph
new file mode 100644
index 0000000..90c29c2
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE112.glyph
@@ -0,0 +1,15 @@
+StartChar: uniE112
+Encoding: 57618 57618 30
+Width: 768
+VWidth: 1176
+Flags: HW
+LayerCount: 2
+Fore
+SplineSet
+512 40 m 1
+ 0 40 l 1
+ 0 168 l 1
+ 512 168 l 1
+ 512 40 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE113.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE113.glyph
new file mode 100644
index 0000000..6319b8f
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE113.glyph
@@ -0,0 +1,20 @@
+StartChar: uniE113
+Encoding: 57619 57619 31
+Width: 622
+VWidth: 1178
+Flags: HW
+LayerCount: 2
+Fore
+SplineSet
+768 42 m 5
+ 0 42 l 5
+ 0 746 l 5
+ 768 746 l 5
+ 768 42 l 5
+704 106 m 5
+ 704 618 l 5
+ 64 618 l 5
+ 64 106 l 5
+ 704 106 l 5
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE114.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE114.glyph
new file mode 100644
index 0000000..36e6577
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE114.glyph
@@ -0,0 +1,31 @@
+StartChar: uniE114
+Encoding: 57620 57620 32
+Width: 896
+VWidth: 1178
+Flags: HW
+LayerCount: 2
+Fore
+SplineSet
+768 298 m 1
+ 576 298 l 1
+ 576 42 l 1
+ 0 42 l 1
+ 0 490 l 1
+ 192 490 l 1
+ 192 746 l 1
+ 768 746 l 1
+ 768 298 l 1
+704 362 m 1
+ 704 618 l 1
+ 256 618 l 1
+ 256 490 l 1
+ 576 490 l 1
+ 576 362 l 1
+ 704 362 l 1
+512 106 m 1
+ 512 362 l 1
+ 64 362 l 1
+ 64 106 l 1
+ 512 106 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv-osd-symbols.sfdir/uniE115.glyph b/TOOLS/mpv-osd-symbols.sfdir/uniE115.glyph
new file mode 100644
index 0000000..10c1195
--- /dev/null
+++ b/TOOLS/mpv-osd-symbols.sfdir/uniE115.glyph
@@ -0,0 +1,27 @@
+StartChar: uniE115
+Encoding: 57621 57621 33
+Width: 844
+VWidth: 1112
+Flags: HW
+LayerCount: 2
+Fore
+SplineSet
+671 40 m 1
+ 575 40 l 1
+ 335 277 l 1
+ 98 40 l 1
+ -1 40 l 1
+ -1 136 l 1
+ 236 376 l 1
+ -1 613 l 1
+ -1 712 l 1
+ 98 712 l 1
+ 335 475 l 1
+ 575 712 l 1
+ 671 712 l 1
+ 671 613 l 1
+ 434 376 l 1
+ 671 139 l 1
+ 671 40 l 1
+EndSplineSet
+EndChar
diff --git a/TOOLS/mpv_identify.sh b/TOOLS/mpv_identify.sh
new file mode 100755
index 0000000..fa0e134
--- /dev/null
+++ b/TOOLS/mpv_identify.sh
@@ -0,0 +1,149 @@
+#!/bin/sh
+
+# file identification script
+#
+# manual usage:
+# mpv_identify.sh foo.mkv
+#
+# sh/dash/ksh/bash usage:
+# . mpv_identify.sh FOO_ foo.mkv
+# will fill properties into variables like FOO_length
+#
+# zsh usage:
+# mpv_identify() { emulate -L sh; . mpv_identify.sh "$@"; }
+# mpv_identify FOO_ foo.mkv
+# will fill properties into variables like FOO_length
+#
+# When multiple files were specified, their info will be put into FOO_* for the
+# first file, FOO_1_* for the second file, FOO_2_* for the third file, etc.
+
+__midentify__main() {
+
+ case "$0" in
+ mpv_identify.sh|*/mpv_identify.sh)
+ # we are NOT being sourced
+ [ -n "$1" ] && set -- '' "$@"
+ ;;
+ esac
+
+ if [ "$#" -lt 2 ]; then
+ cat >&2 <<EOF
+Usage 1 (for humans only): $0 filename.mkv
+will print all property values.
+Note that this output really shouldn't be parsed, as the
+format is subject to change.
+
+Usage 2 (for use by scripts): see top of this file
+
+NOTE: for mkv with ordered chapters, this may
+not always identify the specified file, but the
+file providing the first chapter. Specify
+--no-ordered-chapters to prevent this.
+EOF
+ return 2
+ fi
+
+ local LF="
+"
+
+ local nextprefix="$1"
+ shift
+
+ if [ -n "$nextprefix" ]; then
+ # in case of error, we always want this unset
+ unset "${nextprefix}path"
+ fi
+
+ local allprops="
+ filename
+ path
+ stream-start
+ stream-end
+ stream-length
+
+ demuxer
+
+ length
+ chapters
+ editions
+ titles
+ duration
+
+ audio
+ audio-bitrate
+ audio-codec
+ audio-codec-name
+
+ video
+ angle
+ video-bitrate
+ video-codec
+ video-format
+ video-params/aspect
+ container-fps
+ width
+ height
+ dwidth
+ dheight
+
+ sub
+ "
+ # TODO add metadata support once mpv can do it
+
+ local propstr="X-MIDENTIFY-START:$LF"
+ local key
+ for key in $allprops; do
+ propstr="${propstr}X-MIDENTIFY: $key \${=$key}$LF"
+ key="$(printf '%s\n' "$key" | tr - _)"
+ unset "$nextprefix$key"
+ done
+
+ local fileindex=0
+ local prefix=
+ local line
+ while IFS= read -r line; do
+ case "$line" in
+ X-MIDENTIFY-START:)
+ if [ -n "$nextprefix" ]; then
+ prefix="$nextprefix"
+ if [ "$fileindex" -gt 0 ]; then
+ nextprefix="${prefix%${fileindex}_}"
+ fi
+ fileindex="$((fileindex+1))"
+ nextprefix="${nextprefix}${fileindex}_"
+ for key in $allprops; do
+ key="$(printf '%s\n' "$key" | tr - _)"
+ unset "$nextprefix$key"
+ done
+ else
+ if [ "$fileindex" -gt 0 ]; then
+ printf '\n'
+ fi
+ fileindex="$((fileindex+1))"
+ fi
+ ;;
+ X-MIDENTIFY:\ *)
+ local key="${line#X-MIDENTIFY: }"
+ local value="${key#* }"
+ key="${key%% *}"
+ key="$(printf '%s\n' "$key" | tr - _)"
+ if [ -n "$nextprefix" ]; then
+ if [ -z "$prefix" ]; then
+ echo >&2 "Got X-MIDENTIFY: without X-MIDENTIFY-START:"
+ elif [ -n "$value" ]; then
+ eval "$prefix$key"='"$value"'
+ fi
+ else
+ if [ -n "$value" ]; then
+ printf '%s=%s\n' "$key" "$value"
+ fi
+ fi
+ ;;
+ esac
+ done <<EOF
+$(${MPV:-mpv} --term-playing-msg="$propstr" --vo=null --ao=null \
+ --frames=1 --quiet --no-cache --no-config -- "$@")
+EOF
+}
+
+__midentify__main "$@"
diff --git a/TOOLS/osxbundle.py b/TOOLS/osxbundle.py
new file mode 100755
index 0000000..98699e4
--- /dev/null
+++ b/TOOLS/osxbundle.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+import os
+import shutil
+import sys
+import fileinput
+from optparse import OptionParser
+
+def sh(command):
+ return os.popen(command).read().strip()
+
+def bundle_path(binary_name):
+ return "%s.app" % binary_name
+
+def bundle_name(binary_name):
+ return os.path.basename(bundle_path(binary_name))
+
+def target_plist(binary_name):
+ return os.path.join(bundle_path(binary_name), 'Contents', 'Info.plist')
+
+def target_directory(binary_name):
+ return os.path.join(bundle_path(binary_name), 'Contents', 'MacOS')
+
+def target_binary(binary_name):
+ return os.path.join(target_directory(binary_name),
+ os.path.basename(binary_name))
+
+def copy_bundle(binary_name):
+ if os.path.isdir(bundle_path(binary_name)):
+ shutil.rmtree(bundle_path(binary_name))
+ shutil.copytree(
+ os.path.join('TOOLS', 'osxbundle', bundle_name(binary_name)),
+ bundle_path(binary_name))
+
+def copy_binary(binary_name):
+ shutil.copy(binary_name, target_binary(binary_name))
+
+def apply_plist_template(plist_file, version):
+ for line in fileinput.input(plist_file, inplace=1):
+ print(line.rstrip().replace('${VERSION}', version))
+
+def sign_bundle(binary_name):
+ sh('codesign --force --deep -s - ' + bundle_path(binary_name))
+
+def bundle_version():
+ if os.path.exists('VERSION'):
+ x = open('VERSION')
+ version = x.read()
+ x.close()
+ else:
+ version = sh("./version.sh").strip()
+ return version
+
+def main():
+ version = bundle_version().rstrip()
+
+ usage = "usage: %prog [options] arg"
+ parser = OptionParser(usage)
+ parser.add_option("-s", "--skip-deps", action="store_false", dest="deps",
+ default=True,
+ help="don't bundle the dependencies")
+
+ (options, args) = parser.parse_args()
+
+ if len(args) != 1:
+ parser.error("incorrect number of arguments")
+ else:
+ binary_name = args[0]
+
+ print("Creating Mac OS X application bundle (version: %s)..." % version)
+ print("> copying bundle skeleton")
+ copy_bundle(binary_name)
+ print("> copying binary")
+ copy_binary(binary_name)
+ print("> generating Info.plist")
+ apply_plist_template(target_plist(binary_name), version)
+
+ if options.deps:
+ print("> bundling dependencies")
+ print(sh(" ".join(["TOOLS/dylib-unhell.py", target_binary(binary_name)])))
+
+ print("> signing bundle with ad-hoc pseudo identity")
+ sign_bundle(binary_name)
+
+ print("done.")
+
+if __name__ == "__main__":
+ main()
diff --git a/TOOLS/osxbundle/meson.build b/TOOLS/osxbundle/meson.build
new file mode 100644
index 0000000..a271b41
--- /dev/null
+++ b/TOOLS/osxbundle/meson.build
@@ -0,0 +1,8 @@
+input = join_paths(source_root, 'TOOLS', 'osxbundle',
+ 'mpv.app', 'Contents', 'Resources', 'icon.icns')
+osxbundle = custom_target('osxbundle',
+ input: input,
+ output: 'icon.icns.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+)
+sources += osxbundle
diff --git a/TOOLS/osxbundle/mpv.app/Contents/Info.plist b/TOOLS/osxbundle/mpv.app/Contents/Info.plist
new file mode 100644
index 0000000..e239dc7
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/Info.plist
@@ -0,0 +1,874 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleDocumentTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>AAC</string>
+ <string>AC3</string>
+ <string>AIFF</string>
+ <string>M4A</string>
+ <string>MKA</string>
+ <string>MP3</string>
+ <string>OGG</string>
+ <string>PCM</string>
+ <string>VAW</string>
+ <string>WAV</string>
+ <string>WAW</string>
+ <string>WMA</string>
+ <string>aac</string>
+ <string>ac3</string>
+ <string>aiff</string>
+ <string>m4a</string>
+ <string>mka</string>
+ <string>mp3</string>
+ <string>ogg</string>
+ <string>pcm</string>
+ <string>vaw</string>
+ <string>wav</string>
+ <string>waw</string>
+ <string>wma</string>
+ </array>
+ <key>CFBundleTypeIconFile</key>
+ <string>document.icns</string>
+ <key>CFBundleTypeName</key>
+ <string>Audio file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>LSTypeIsPackage</key>
+ <false/>
+ <key>NSPersistentStoreTypeKey</key>
+ <string>XML</string>
+ </dict>
+ <dict>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>*</string>
+ <string>*</string>
+ <string>3GP</string>
+ <string>3IV</string>
+ <string>3gp</string>
+ <string>3iv</string>
+ <string>ASF</string>
+ <string>AVI</string>
+ <string>CPK</string>
+ <string>DAT</string>
+ <string>DIVX</string>
+ <string>DV</string>
+ <string>FLAC</string>
+ <string>FLI</string>
+ <string>FLV</string>
+ <string>H264</string>
+ <string>I263</string>
+ <string>M2TS</string>
+ <string>M4V</string>
+ <string>MKV</string>
+ <string>MOV</string>
+ <string>MP2</string>
+ <string>MP4</string>
+ <string>MPEG</string>
+ <string>MPG</string>
+ <string>MPG2</string>
+ <string>MPG4</string>
+ <string>NSV</string>
+ <string>NUT</string>
+ <string>NUV</string>
+ <string>OGG</string>
+ <string>OGM</string>
+ <string>QT</string>
+ <string>RM</string>
+ <string>RMVB</string>
+ <string>VCD</string>
+ <string>VFW</string>
+ <string>VOB</string>
+ <string>WEBM</string>
+ <string>WMV</string>
+ <string>MK3D</string>
+ <string>asf</string>
+ <string>avi</string>
+ <string>cpk</string>
+ <string>dat</string>
+ <string>divx</string>
+ <string>dv</string>
+ <string>flac</string>
+ <string>fli</string>
+ <string>flv</string>
+ <string>h264</string>
+ <string>i263</string>
+ <string>m2ts</string>
+ <string>m4v</string>
+ <string>mkv</string>
+ <string>mov</string>
+ <string>mp2</string>
+ <string>mp4</string>
+ <string>mpeg</string>
+ <string>mpg</string>
+ <string>mpg2</string>
+ <string>mpg4</string>
+ <string>nsv</string>
+ <string>nut</string>
+ <string>nuv</string>
+ <string>ogg</string>
+ <string>ogm</string>
+ <string>qt</string>
+ <string>rm</string>
+ <string>rmvb</string>
+ <string>vcd</string>
+ <string>vfw</string>
+ <string>vob</string>
+ <string>webm</string>
+ <string>wmv</string>
+ <string>mk3d</string>
+ </array>
+ <key>CFBundleTypeIconFile</key>
+ <string>document.icns</string>
+ <key>CFBundleTypeName</key>
+ <string>Movie file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>LSTypeIsPackage</key>
+ <false/>
+ <key>NSPersistentStoreTypeKey</key>
+ <string>XML</string>
+ </dict>
+ <dict>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>AQT</string>
+ <string>ASS</string>
+ <string>JSS</string>
+ <string>RT</string>
+ <string>SMI</string>
+ <string>SRT</string>
+ <string>SSA</string>
+ <string>SUB</string>
+ <string>TXT</string>
+ <string>UTF</string>
+ <string>aqt</string>
+ <string>ass</string>
+ <string>jss</string>
+ <string>rt</string>
+ <string>smi</string>
+ <string>srt</string>
+ <string>ssa</string>
+ <string>sub</string>
+ <string>txt</string>
+ <string>utf</string>
+ </array>
+ <key>CFBundleTypeIconFile</key>
+ <string>document.icns</string>
+ <key>CFBundleTypeName</key>
+ <string>Subtitles file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>LSTypeIsPackage</key>
+ <false/>
+ <key>NSPersistentStoreTypeKey</key>
+ <string>XML</string>
+ </dict>
+ </array>
+ <key>CFBundleExecutable</key>
+ <string>mpv</string>
+ <key>CFBundleIconFile</key>
+ <string>icon</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.mpv</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>mpv</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>${VERSION}</string>
+ <key>NSHighResolutionCapable</key>
+ <true/>
+ <key>LSEnvironment</key>
+ <dict>
+ <key>MallocNanoZone</key>
+ <string>0</string>
+ <key>MPVBUNDLE</key>
+ <string>true</string>
+ </dict>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>CFBundleURLName</key>
+ <string>mpv Custom Protocol</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>mpv</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>CFBundleURLName</key>
+ <string>Streaming Protocol</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>mms</string>
+ <string>mmst</string>
+ <string>http</string>
+ <string>https</string>
+ <string>httpproxy</string>
+ <string>rtp</string>
+ <string>rtsp</string>
+ <string>ftp</string>
+ <string>udp</string>
+ <string>smb</string>
+ <string>srt</string>
+ <string>rist</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>CFBundleURLName</key>
+ <string>CD/DVD/Bluray Media</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>cdda</string>
+ <string>dvd</string>
+ <string>vcd</string>
+ <string>bd</string>
+ </array>
+ </dict>
+ </array>
+ <key>UTImportedTypeDeclarations</key>
+ <array>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>AC3 Audio</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.ac3</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://wiki.multimedia.cx/index.php?title=AC3</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>ac3</string>
+ <string>a52</string>
+ <string>eac3</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>DTS Audio</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.dts</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://wiki.multimedia.cx/index.php?title=DTS</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>dts</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Free Lossless Audio Codec</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.flac</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://flac.sourceforge.net/format.html</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>flac</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Matroska Audio</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.mka</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.matroska.org</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>mka</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Ogg Audio</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.ogg-audio</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://xiph.org/ogg</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>ogg</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>PCM Audio</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.pcm</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/Pulse-code_modulation</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>pcm</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.audio</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Windows Media Audio</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.wma</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/Windows_Media_Audio</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>wma</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Audio Video Interleave</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.avi</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.the-labs.com/Video/odmlff2-avidef.pdf</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>avi</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>DIVX Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.divx</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.divx.com</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>divx</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>DV Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.dv</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/DV</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>dv</string>
+ <string>hdv</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Flash Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.flv</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/Flash_Video</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>flv</string>
+ <string>f4v</string>
+ <string>f4p</string>
+ <string>swf</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>MPEG-2 Transport Stream</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.mts</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/.m2ts</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>trp</string>
+ <string>m2t</string>
+ <string>m2ts</string>
+ <string>mts</string>
+ <string>mtv</string>
+ <string>ts</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Matroska Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.mkv</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.matroska.org</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>mkv</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Matroska stereoscopic/3D video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.mk3d</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.matroska.org</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>mk3d</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>WebM Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.webm</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.webmproject.org</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>webm</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Ogg Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.ogv</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://xiph.org/ogg</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>ogm</string>
+ <string>ogv</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Real Media</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.rmvb</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.real.com</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>rmvb</string>
+ <string>rm</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Video Object</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.vob</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/VOB</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>vob</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>Windows Media Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.wmv</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/Windows_Media_Video</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>wmv</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>XVID Video</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.xvid</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.xvid.org</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>xvid</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>AVC raw stream</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.h264</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://www.itu.int/rec/T-REC-H.264</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>264</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>HEVC raw stream</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.hevc</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://hevc.info</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>hevc</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>YUV stream</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.yuv</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/YUV</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>yuv</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.movie</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>YUV4MPEG2 stream</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.y4m</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://wiki.multimedia.cx/index.php?title=YUV4MPEG2</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>y4m</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.plain-text</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>SubRip Subtitle</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.subrip</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/SubRip</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>srt</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.plain-text</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>MicroDVD Subtitle</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.sub</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/MicroDVD</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>sub</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.plain-text</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>SubStation Alpha Subtitle</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.ass</string>
+ <key>UTTypeReferenceURL</key>
+ <string>https://github.com/libass/libass</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>ass</string>
+ <string>ssa</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.data</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>VobSub Subtitle</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.vobsub</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/DirectVobSub</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>idx</string>
+ <string>sub</string>
+ </array>
+ </dict>
+ </dict>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.plain-text</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>SAMI Subtitle</string>
+ <key>UTTypeIconFile</key>
+ <string>document.icns</string>
+ <key>UTTypeIdentifier</key>
+ <string>io.mpv.smi</string>
+ <key>UTTypeReferenceURL</key>
+ <string>http://en.wikipedia.org/wiki/SAMI</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <array>
+ <string>smi</string>
+ <string>smil</string>
+ </array>
+ </dict>
+ </dict>
+ </array>
+ </dict>
+</plist>
diff --git a/TOOLS/osxbundle/mpv.app/Contents/MacOS/.gitkeep b/TOOLS/osxbundle/mpv.app/Contents/MacOS/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/MacOS/.gitkeep
diff --git a/TOOLS/osxbundle/mpv.app/Contents/MacOS/lib/.gitkeep b/TOOLS/osxbundle/mpv.app/Contents/MacOS/lib/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/MacOS/lib/.gitkeep
diff --git a/TOOLS/osxbundle/mpv.app/Contents/PkgInfo b/TOOLS/osxbundle/mpv.app/Contents/PkgInfo
new file mode 100644
index 0000000..bd04210
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/PkgInfo
@@ -0,0 +1 @@
+APPL???? \ No newline at end of file
diff --git a/TOOLS/osxbundle/mpv.app/Contents/Resources/document.icns b/TOOLS/osxbundle/mpv.app/Contents/Resources/document.icns
new file mode 100644
index 0000000..d616296
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/Resources/document.icns
Binary files differ
diff --git a/TOOLS/osxbundle/mpv.app/Contents/Resources/icon.icns b/TOOLS/osxbundle/mpv.app/Contents/Resources/icon.icns
new file mode 100644
index 0000000..035a934
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/Resources/icon.icns
Binary files differ
diff --git a/TOOLS/osxbundle/mpv.app/Contents/Resources/mpv.conf b/TOOLS/osxbundle/mpv.app/Contents/Resources/mpv.conf
new file mode 100644
index 0000000..618f87e
--- /dev/null
+++ b/TOOLS/osxbundle/mpv.app/Contents/Resources/mpv.conf
@@ -0,0 +1,2 @@
+player-operation-mode=pseudo-gui
+log-file=~/Library/Logs/mpv.log
diff --git a/TOOLS/stats-conv.py b/TOOLS/stats-conv.py
new file mode 100755
index 0000000..16d787a
--- /dev/null
+++ b/TOOLS/stats-conv.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+from pyqtgraph.Qt import QtGui, QtCore
+import pyqtgraph as pg
+import sys
+import re
+
+filename = sys.argv[1]
+
+events = ".*"
+if len(sys.argv) > 2:
+ events = sys.argv[2]
+event_regex = re.compile(events)
+
+"""
+This script is meant to display stats written by mpv --dump-stats=filename.
+In general, each line in that file is an event of the form:
+
+ <timestamp in microseconds> <text> '#' <comment>
+
+e.g.:
+
+ 10474959 start flip #cplayer
+
+<text> is what MP_STATS(log, "...") writes. The rest is added by msg.c.
+
+Currently, the following event types are supported:
+
+ 'signal' <name> singular event
+ 'start' <name> start of the named event
+ 'end' <name> end of the named event
+ 'value' <float> <name> a normal value (as opposed to event)
+ 'event-timed' <ts> <name> singular event at the given timestamp
+ 'value-timed' <ts> <float> <name> a value for an event at the given timestamp
+ 'range-timed' <ts1> <ts2> <name> like start/end, but explicit times
+ <name> singular event (same as 'signal')
+
+"""
+
+class G:
+ events = {}
+ start = None
+ markers = ["o", "s", "t", "d"]
+ curveno = {}
+
+def find_marker():
+ if len(G.markers) == 0:
+ return "o"
+ m = G.markers[0]
+ G.markers = G.markers[1:]
+ return m
+
+class Event:
+ pass
+
+def get_event(event, evtype):
+ if event not in G.events:
+ e = Event()
+ e.name = event
+ e.vals = []
+ e.type = evtype
+ e.marker = "o"
+ if e.type == "event-signal":
+ e.marker = find_marker()
+ if not event_regex.match(e.name):
+ return e
+ G.events[event] = e
+ return G.events[event]
+
+colors = [(0.0, 0.5, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.75, 0.75, 0), (0.0, 0.75, 0.75), (0.75, 0, 0.75)]
+def mkColor(t):
+ return pg.mkColor(int(t[0] * 255), int(t[1] * 255), int(t[2] * 255))
+
+SCALE = 1e6 # microseconds to seconds
+
+for line in [line.split("#")[0].strip() for line in open(filename, "r")]:
+ line = line.strip()
+ if not line:
+ continue
+ ts, event = line.split(" ", 1)
+ ts = int(ts) / SCALE
+ if G.start is None:
+ G.start = ts
+ ts = ts - G.start
+ if event.startswith("start "):
+ e = get_event(event[6:], "event")
+ e.vals.append((ts, 0))
+ e.vals.append((ts, 1))
+ elif event.startswith("end "):
+ e = get_event(event[4:], "event")
+ e.vals.append((ts, 1))
+ e.vals.append((ts, 0))
+ elif event.startswith("value "):
+ _, val, name = event.split(" ", 2)
+ val = float(val)
+ e = get_event(name, "value")
+ e.vals.append((ts, val))
+ elif event.startswith("event-timed "):
+ _, val, name = event.split(" ", 2)
+ val = int(val) / SCALE - G.start
+ e = get_event(name, "event-signal")
+ e.vals.append((val, 1))
+ elif event.startswith("range-timed "):
+ _, ts1, ts2, name = event.split(" ", 3)
+ ts1 = int(ts1) / SCALE - G.start
+ ts2 = int(ts2) / SCALE - G.start
+ e = get_event(name, "event")
+ e.vals.append((ts1, 0))
+ e.vals.append((ts1, 1))
+ e.vals.append((ts2, 1))
+ e.vals.append((ts2, 0))
+ elif event.startswith("value-timed "):
+ _, tsval, val, name = event.split(" ", 3)
+ tsval = int(tsval) / SCALE - G.start
+ val = float(val)
+ e = get_event(name, "value")
+ e.vals.append((tsval, val))
+ elif event.startswith("signal "):
+ name = event.split(" ", 2)[1]
+ e = get_event(name, "event-signal")
+ e.vals.append((ts, 1))
+ else:
+ e = get_event(event, "event-signal")
+ e.vals.append((ts, 1))
+
+# deterministically sort them; make sure the legend is sorted too
+G.sevents = list(G.events.values())
+G.sevents.sort(key=lambda x: x.name)
+hasval = False
+for e, index in zip(G.sevents, range(len(G.sevents))):
+ m = len(G.sevents)
+ if e.type == "value":
+ hasval = True
+ else:
+ e.vals = [(x, y * (m - index) / m) for (x, y) in e.vals]
+
+pg.setConfigOption('background', 'w')
+pg.setConfigOption('foreground', 'k')
+app = QtGui.QApplication([])
+win = pg.GraphicsWindow()
+#win.resize(1500, 900)
+
+ax = [None, None]
+plots = 2 if hasval else 1
+ax[0] = win.addPlot()
+if hasval:
+ win.nextRow()
+ ax[1] = win.addPlot()
+ ax[1].setXLink(ax[0])
+for cur in ax:
+ if cur is not None:
+ cur.addLegend(offset = (-1, 1))
+for e in G.sevents:
+ cur = ax[1 if e.type == "value" else 0]
+ if not cur in G.curveno:
+ G.curveno[cur] = 0
+ args = {'name': e.name,'antialias':True}
+ color = mkColor(colors[G.curveno[cur] % len(colors)])
+ if e.type == "event-signal":
+ args['symbol'] = e.marker
+ args['symbolBrush'] = pg.mkBrush(color, width=0)
+ else:
+ args['pen'] = pg.mkPen(color, width=0)
+ G.curveno[cur] += 1
+ n = cur.plot([x for x,y in e.vals], [y for x,y in e.vals], **args)
+
+QtGui.QApplication.instance().exec_()
diff --git a/TOOLS/umpv b/TOOLS/umpv
new file mode 100755
index 0000000..1eece5d
--- /dev/null
+++ b/TOOLS/umpv
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+
+"""
+This script emulates "unique application" functionality on Linux. When starting
+playback with this script, it will try to reuse an already running instance of
+mpv (but only if that was started with umpv). Other mpv instances (not started
+by umpv) are ignored, and the script doesn't know about them.
+
+This only takes filenames as arguments. Custom options can't be used; the script
+interprets them as filenames. If mpv is already running, the files passed to
+umpv are appended to mpv's internal playlist. If a file does not exist or is
+otherwise not playable, mpv will skip the playlist entry when attempting to
+play it (from the GUI perspective, it's silently ignored).
+
+If mpv isn't running yet, this script will start mpv and let it control the
+current terminal. It will not write output to stdout/stderr, because this
+will typically just fill ~/.xsession-errors with garbage.
+
+mpv will terminate if there are no more files to play, and running the umpv
+script after that will start a new mpv instance.
+
+Note: you can supply custom mpv path and options with the MPV environment
+ variable. The environment variable will be split on whitespace, and the
+ first item is used as path to mpv binary and the rest is passed as options
+ _if_ the script starts mpv. If mpv is not started by the script (i.e. mpv
+ is already running), this will be ignored.
+"""
+
+import sys
+import os
+import socket
+import errno
+import subprocess
+import string
+import shlex
+
+files = sys.argv[1:]
+
+# this is the same method mpv uses to decide this
+def is_url(filename):
+ parts = filename.split("://", 1)
+ if len(parts) < 2:
+ return False
+ # protocol prefix has no special characters => it's an URL
+ allowed_symbols = string.ascii_letters + string.digits + '_'
+ prefix = parts[0]
+ return all(map(lambda c: c in allowed_symbols, prefix))
+
+# make them absolute; also makes them safe against interpretation as options
+def make_abs(filename):
+ if not is_url(filename):
+ return os.path.abspath(filename)
+ return filename
+files = (make_abs(f) for f in files)
+
+SOCK = os.path.join(os.getenv("XDG_RUNTIME_DIR", os.getenv("HOME")), ".umpv_socket")
+
+sock = None
+try:
+ sock = socket.socket(socket.AF_UNIX)
+ sock.connect(SOCK)
+except socket.error as e:
+ if e.errno == errno.ECONNREFUSED:
+ sock = None
+ pass # abandoned socket
+ elif e.errno == errno.ENOENT:
+ sock = None
+ pass # doesn't exist
+ else:
+ raise e
+
+if sock:
+ # Unhandled race condition: what if mpv is terminating right now?
+ for f in files:
+ # escape: \ \n "
+ f = f.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
+ f = "\"" + f + "\""
+ sock.send(("raw loadfile " + f + " append\n").encode("utf-8"))
+else:
+ # Let mpv recreate socket if it doesn't already exist.
+
+ opts = shlex.split(os.getenv("MPV") or "mpv")
+ opts.extend(["--no-terminal", "--force-window", "--input-ipc-server=" + SOCK,
+ "--"])
+ opts.extend(files)
+
+ subprocess.check_call(opts)
diff --git a/TOOLS/uncrustify.cfg b/TOOLS/uncrustify.cfg
new file mode 100644
index 0000000..837d9a1
--- /dev/null
+++ b/TOOLS/uncrustify.cfg
@@ -0,0 +1,165 @@
+# Usage:
+# uncrustify -l C -c TOOLS/uncrustify.cfg --no-backup --replace file.c
+#
+# Keep in mind that this uncrustify configuration still produces some
+# bad/broken formatting.
+#
+
+code_width=80
+indent_align_string=false
+indent_braces=false
+indent_braces_no_func=false
+indent_brace_parent=false
+indent_namespace=false
+indent_extern=false
+indent_class=false
+indent_class_colon=false
+indent_else_if=false
+indent_func_call_param=false
+indent_func_def_param=false
+indent_func_proto_param=false
+indent_func_class_param=false
+indent_func_ctor_var_param=false
+indent_template_param=false
+indent_func_param_double=false
+indent_relative_single_line_comments=false
+indent_col1_comment=false
+indent_access_spec_body=false
+indent_paren_nl=false
+indent_comma_paren=false
+indent_bool_paren=false
+indent_square_nl=false
+indent_preserve_sql=false
+indent_align_assign=true
+sp_balance_nested_parens=false
+align_keep_tabs=false
+align_with_tabs=false
+align_on_tabstop=false
+align_number_left=false
+align_func_params=false
+align_same_func_call_params=false
+align_var_def_colon=false
+align_var_def_attribute=false
+align_var_def_inline=false
+align_right_cmt_mix=false
+align_on_operator=false
+align_mix_var_proto=false
+align_single_line_func=false
+align_single_line_brace=false
+align_nl_cont=false
+align_left_shift=true
+nl_collapse_empty_body=false
+nl_assign_leave_one_liners=false
+nl_class_leave_one_liners=false
+nl_enum_leave_one_liners=false
+nl_getset_leave_one_liners=false
+nl_func_leave_one_liners=false
+nl_if_leave_one_liners=false
+nl_multi_line_cond=false
+nl_multi_line_define=false
+nl_before_case=false
+nl_after_case=false
+nl_after_return=false
+nl_after_semicolon=true
+nl_after_brace_open=true
+nl_after_brace_open_cmt=false
+nl_after_vbrace_open=true
+nl_after_brace_close=false
+nl_define_macro=false
+nl_squeeze_ifdef=false
+nl_ds_struct_enum_cmt=false
+nl_ds_struct_enum_close_brace=false
+nl_create_if_one_liner=false
+nl_create_for_one_liner=false
+nl_create_while_one_liner=false
+ls_for_split_full=false
+ls_func_split_full=false
+nl_after_multiline_comment=false
+eat_blanks_after_open_brace=false
+eat_blanks_before_close_brace=false
+mod_pawn_semicolon=false
+mod_full_paren_if_bool=false
+mod_remove_extra_semicolon=false
+mod_sort_import=false
+mod_sort_using=false
+mod_sort_include=false
+mod_move_case_break=false
+mod_remove_empty_return=false
+cmt_indent_multi=true
+cmt_c_group=false
+cmt_c_nl_start=false
+cmt_c_nl_end=false
+cmt_cpp_group=false
+cmt_cpp_nl_start=false
+cmt_cpp_nl_end=false
+cmt_cpp_to_c=false
+cmt_star_cont=false
+cmt_multi_check_last=true
+cmt_insert_before_preproc=false
+pp_indent_at_level=false
+pp_region_indent_code=false
+pp_if_indent_code=false
+pp_define_at_level=false
+indent_columns=4
+nl_end_of_file_min=1
+mod_full_brace_nl=2
+indent_with_tabs=0
+sp_arith=add
+sp_assign=add
+sp_enum_assign=force
+sp_bool=force
+sp_compare=force
+sp_inside_paren=remove
+sp_paren_paren=remove
+sp_before_ptr_star=force
+sp_between_ptr_star=remove
+sp_after_ptr_star=remove
+sp_after_ptr_star_func=remove
+sp_before_sparen=add
+sp_inside_sparen=remove
+sp_sparen_brace=force
+sp_before_semi=remove
+sp_after_semi_for_empty=remove
+sp_before_square=remove
+sp_inside_square=remove
+sp_after_comma=force
+sp_before_comma=remove
+sp_inside_paren_cast=remove
+sp_func_proto_paren=remove
+sp_func_def_paren=remove
+sp_inside_fparens=remove
+sp_inside_fparen=remove
+sp_square_fparen=remove
+sp_func_call_paren=remove
+sp_return_paren=force
+sp_else_brace=force
+sp_brace_else=force
+sp_brace_typedef=add
+sp_not=remove
+sp_inv=remove
+sp_addr=remove
+sp_member=remove
+sp_deref=remove
+sp_sign=remove
+sp_incdec=remove
+sp_before_nl_cont=add
+nl_end_of_file=force
+nl_if_brace=remove
+nl_brace_else=remove
+nl_elseif_brace=remove
+nl_else_brace=remove
+nl_else_if=remove
+nl_for_brace=remove
+nl_while_brace=remove
+nl_do_brace=remove
+nl_brace_while=remove
+nl_switch_brace=remove
+nl_func_type_name=remove
+nl_func_proto_type_name=remove
+nl_func_paren=remove
+nl_func_decl_start=remove
+nl_fdef_brace=force
+mod_full_brace_for=remove
+mod_full_brace_if=remove
+mod_full_brace_while=remove
+mod_paren_on_return=remove
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..0f1a7df
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.37.0
diff --git a/audio/aframe.c b/audio/aframe.c
new file mode 100644
index 0000000..cb6ea17
--- /dev/null
+++ b/audio/aframe.c
@@ -0,0 +1,720 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/frame.h>
+#include <libavutil/mem.h>
+
+#include "config.h"
+
+#include "common/common.h"
+
+#include "chmap.h"
+#include "chmap_avchannel.h"
+#include "fmt-conversion.h"
+#include "format.h"
+#include "aframe.h"
+
+struct mp_aframe {
+ AVFrame *av_frame;
+ // We support channel layouts different from AVFrame channel masks
+ struct mp_chmap chmap;
+ // We support spdif formats, which are allocated as AV_SAMPLE_FMT_S16.
+ int format;
+ double pts;
+ double speed;
+};
+
+struct avframe_opaque {
+ double speed;
+};
+
+static void free_frame(void *ptr)
+{
+ struct mp_aframe *frame = ptr;
+ av_frame_free(&frame->av_frame);
+}
+
+struct mp_aframe *mp_aframe_create(void)
+{
+ struct mp_aframe *frame = talloc_zero(NULL, struct mp_aframe);
+ frame->av_frame = av_frame_alloc();
+ MP_HANDLE_OOM(frame->av_frame);
+ talloc_set_destructor(frame, free_frame);
+ mp_aframe_reset(frame);
+ return frame;
+}
+
+struct mp_aframe *mp_aframe_new_ref(struct mp_aframe *frame)
+{
+ if (!frame)
+ return NULL;
+
+ struct mp_aframe *dst = mp_aframe_create();
+
+ dst->chmap = frame->chmap;
+ dst->format = frame->format;
+ dst->pts = frame->pts;
+ dst->speed = frame->speed;
+
+ if (mp_aframe_is_allocated(frame)) {
+ if (av_frame_ref(dst->av_frame, frame->av_frame) < 0)
+ abort();
+ } else {
+ // av_frame_ref() would fail.
+ mp_aframe_config_copy(dst, frame);
+ }
+
+ return dst;
+}
+
+// Revert to state after mp_aframe_create().
+void mp_aframe_reset(struct mp_aframe *frame)
+{
+ av_frame_unref(frame->av_frame);
+ frame->chmap.num = 0;
+ frame->format = 0;
+ frame->pts = MP_NOPTS_VALUE;
+ frame->speed = 1.0;
+}
+
+// Remove all actual audio data and leave only the metadata.
+void mp_aframe_unref_data(struct mp_aframe *frame)
+{
+ // In a fucked up way, this is less complex than just unreffing the data.
+ struct mp_aframe *tmp = mp_aframe_create();
+ MPSWAP(struct mp_aframe, *tmp, *frame);
+ mp_aframe_reset(frame);
+ mp_aframe_config_copy(frame, tmp);
+ talloc_free(tmp);
+}
+
+// Allocate this much data. Returns false for failure (data already allocated,
+// invalid sample count or format, allocation failures).
+// Normally you're supposed to use a frame pool and mp_aframe_pool_allocate().
+bool mp_aframe_alloc_data(struct mp_aframe *frame, int samples)
+{
+ if (mp_aframe_is_allocated(frame))
+ return false;
+ struct mp_aframe_pool *p = mp_aframe_pool_create(NULL);
+ int r = mp_aframe_pool_allocate(p, frame, samples);
+ talloc_free(p);
+ return r >= 0;
+}
+
+// Return a new reference to the data in av_frame. av_frame itself is not
+// touched. Returns NULL if not representable, or if input is NULL.
+// Does not copy the timestamps.
+struct mp_aframe *mp_aframe_from_avframe(struct AVFrame *av_frame)
+{
+ if (!av_frame || av_frame->width > 0 || av_frame->height > 0)
+ return NULL;
+
+#if HAVE_AV_CHANNEL_LAYOUT
+ if (!av_channel_layout_check(&av_frame->ch_layout))
+ return NULL;
+
+ struct mp_chmap converted_map = { 0 };
+ if (!mp_chmap_from_av_layout(&converted_map, &av_frame->ch_layout)) {
+ return NULL;
+ }
+#endif
+
+ int format = af_from_avformat(av_frame->format);
+ if (!format && av_frame->format != AV_SAMPLE_FMT_NONE)
+ return NULL;
+
+ struct mp_aframe *frame = mp_aframe_create();
+
+ // This also takes care of forcing refcounting.
+ if (av_frame_ref(frame->av_frame, av_frame) < 0)
+ abort();
+
+ frame->format = format;
+#if !HAVE_AV_CHANNEL_LAYOUT
+ mp_chmap_from_lavc(&frame->chmap, frame->av_frame->channel_layout);
+
+ // FFmpeg being a stupid POS again
+ if (frame->chmap.num != frame->av_frame->channels)
+ mp_chmap_from_channels(&frame->chmap, av_frame->channels);
+#else
+ frame->chmap = converted_map;
+#endif
+
+ if (av_frame->opaque_ref) {
+ struct avframe_opaque *op = (void *)av_frame->opaque_ref->data;
+ frame->speed = op->speed;
+ }
+
+ return frame;
+}
+
+// Return a new reference to the data in frame. Returns NULL is not
+// representable (), or if input is NULL.
+// Does not copy the timestamps.
+struct AVFrame *mp_aframe_to_avframe(struct mp_aframe *frame)
+{
+ if (!frame)
+ return NULL;
+
+ if (af_to_avformat(frame->format) != frame->av_frame->format)
+ return NULL;
+
+ if (!mp_chmap_is_lavc(&frame->chmap))
+ return NULL;
+
+ if (!frame->av_frame->opaque_ref && frame->speed != 1.0) {
+ frame->av_frame->opaque_ref =
+ av_buffer_alloc(sizeof(struct avframe_opaque));
+ if (!frame->av_frame->opaque_ref)
+ return NULL;
+
+ struct avframe_opaque *op = (void *)frame->av_frame->opaque_ref->data;
+ op->speed = frame->speed;
+ }
+
+ return av_frame_clone(frame->av_frame);
+}
+
+struct AVFrame *mp_aframe_to_avframe_and_unref(struct mp_aframe *frame)
+{
+ AVFrame *av = mp_aframe_to_avframe(frame);
+ talloc_free(frame);
+ return av;
+}
+
+// You must not use this.
+struct AVFrame *mp_aframe_get_raw_avframe(struct mp_aframe *frame)
+{
+ return frame->av_frame;
+}
+
+// Return whether it has associated audio data. (If not, metadata only.)
+bool mp_aframe_is_allocated(struct mp_aframe *frame)
+{
+ return frame->av_frame->buf[0] || frame->av_frame->extended_data[0];
+}
+
+// Clear dst, and then copy the configuration to it.
+void mp_aframe_config_copy(struct mp_aframe *dst, struct mp_aframe *src)
+{
+ mp_aframe_reset(dst);
+
+ dst->chmap = src->chmap;
+ dst->format = src->format;
+
+ mp_aframe_copy_attributes(dst, src);
+
+ dst->av_frame->sample_rate = src->av_frame->sample_rate;
+ dst->av_frame->format = src->av_frame->format;
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ dst->av_frame->channel_layout = src->av_frame->channel_layout;
+ // FFmpeg being a stupid POS again
+ dst->av_frame->channels = src->av_frame->channels;
+#else
+ if (av_channel_layout_copy(&dst->av_frame->ch_layout,
+ &src->av_frame->ch_layout) < 0)
+ abort();
+#endif
+}
+
+// Copy "soft" attributes from src to dst, excluding things which affect
+// frame allocation and organization.
+void mp_aframe_copy_attributes(struct mp_aframe *dst, struct mp_aframe *src)
+{
+ dst->pts = src->pts;
+ dst->speed = src->speed;
+
+ int rate = dst->av_frame->sample_rate;
+
+ if (av_frame_copy_props(dst->av_frame, src->av_frame) < 0)
+ abort();
+
+ dst->av_frame->sample_rate = rate;
+}
+
+// Return whether a and b use the same physical audio format. Extra metadata
+// such as PTS, per-frame signalling, and AVFrame side data is not compared.
+bool mp_aframe_config_equals(struct mp_aframe *a, struct mp_aframe *b)
+{
+ struct mp_chmap ca = {0}, cb = {0};
+ mp_aframe_get_chmap(a, &ca);
+ mp_aframe_get_chmap(b, &cb);
+ return mp_chmap_equals(&ca, &cb) &&
+ mp_aframe_get_rate(a) == mp_aframe_get_rate(b) &&
+ mp_aframe_get_format(a) == mp_aframe_get_format(b);
+}
+
+// Return whether all required format fields have been set.
+bool mp_aframe_config_is_valid(struct mp_aframe *frame)
+{
+ return frame->format && frame->chmap.num && frame->av_frame->sample_rate;
+}
+
+// Return the pointer to the first sample for each plane. The pointers stay
+// valid until the next call that mutates frame somehow. You must not write to
+// the audio data. Returns NULL if no frame allocated.
+uint8_t **mp_aframe_get_data_ro(struct mp_aframe *frame)
+{
+ return mp_aframe_is_allocated(frame) ? frame->av_frame->extended_data : NULL;
+}
+
+// Like mp_aframe_get_data_ro(), but you can write to the audio data.
+// Additionally, it will return NULL if copy-on-write fails.
+uint8_t **mp_aframe_get_data_rw(struct mp_aframe *frame)
+{
+ if (!mp_aframe_is_allocated(frame))
+ return NULL;
+ if (av_frame_make_writable(frame->av_frame) < 0)
+ return NULL;
+ return frame->av_frame->extended_data;
+}
+
+int mp_aframe_get_format(struct mp_aframe *frame)
+{
+ return frame->format;
+}
+
+bool mp_aframe_get_chmap(struct mp_aframe *frame, struct mp_chmap *out)
+{
+ if (!mp_chmap_is_valid(&frame->chmap))
+ return false;
+ *out = frame->chmap;
+ return true;
+}
+
+int mp_aframe_get_channels(struct mp_aframe *frame)
+{
+ return frame->chmap.num;
+}
+
+int mp_aframe_get_rate(struct mp_aframe *frame)
+{
+ return frame->av_frame->sample_rate;
+}
+
+int mp_aframe_get_size(struct mp_aframe *frame)
+{
+ return frame->av_frame->nb_samples;
+}
+
+double mp_aframe_get_pts(struct mp_aframe *frame)
+{
+ return frame->pts;
+}
+
+bool mp_aframe_set_format(struct mp_aframe *frame, int format)
+{
+ if (mp_aframe_is_allocated(frame))
+ return false;
+ enum AVSampleFormat av_format = af_to_avformat(format);
+ if (av_format == AV_SAMPLE_FMT_NONE && format) {
+ if (!af_fmt_is_spdif(format))
+ return false;
+ av_format = AV_SAMPLE_FMT_S16;
+ }
+ frame->format = format;
+ frame->av_frame->format = av_format;
+ return true;
+}
+
+bool mp_aframe_set_chmap(struct mp_aframe *frame, struct mp_chmap *in)
+{
+ if (!mp_chmap_is_valid(in) && !mp_chmap_is_empty(in))
+ return false;
+ if (mp_aframe_is_allocated(frame) && in->num != frame->chmap.num)
+ return false;
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ uint64_t lavc_layout = mp_chmap_to_lavc_unchecked(in);
+ if (!lavc_layout && in->num)
+ return false;
+#endif
+ frame->chmap = *in;
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ frame->av_frame->channel_layout = lavc_layout;
+ // FFmpeg being a stupid POS again
+ frame->av_frame->channels = frame->chmap.num;
+#else
+ mp_chmap_to_av_layout(&frame->av_frame->ch_layout, in);
+#endif
+ return true;
+}
+
+bool mp_aframe_set_rate(struct mp_aframe *frame, int rate)
+{
+ if (rate < 1 || rate > 10000000)
+ return false;
+ frame->av_frame->sample_rate = rate;
+ return true;
+}
+
+bool mp_aframe_set_size(struct mp_aframe *frame, int samples)
+{
+ if (!mp_aframe_is_allocated(frame) || mp_aframe_get_size(frame) < samples)
+ return false;
+ frame->av_frame->nb_samples = MPMAX(samples, 0);
+ return true;
+}
+
+void mp_aframe_set_pts(struct mp_aframe *frame, double pts)
+{
+ frame->pts = pts;
+}
+
+// Set a speed factor. This is multiplied with the sample rate to get the
+// "effective" samplerate (mp_aframe_get_effective_rate()), which will be used
+// to do PTS calculations. If speed!=1.0, the PTS values always refer to the
+// original PTS (before changing speed), and if you want reasonably continuous
+// PTS between frames, you need to use the effective samplerate.
+void mp_aframe_set_speed(struct mp_aframe *frame, double factor)
+{
+ frame->speed = factor;
+}
+
+// Adjust current speed factor.
+void mp_aframe_mul_speed(struct mp_aframe *frame, double factor)
+{
+ frame->speed *= factor;
+}
+
+double mp_aframe_get_speed(struct mp_aframe *frame)
+{
+ return frame->speed;
+}
+
+// Matters for speed changed frames (such as a frame which has been resampled
+// to play at a different speed).
+// Return the sample rate at which the frame would have to be played to result
+// in the same duration as the original frame before the speed change.
+// This is used for A/V sync.
+double mp_aframe_get_effective_rate(struct mp_aframe *frame)
+{
+ return mp_aframe_get_rate(frame) / frame->speed;
+}
+
+// Return number of data pointers.
+int mp_aframe_get_planes(struct mp_aframe *frame)
+{
+ return af_fmt_is_planar(mp_aframe_get_format(frame))
+ ? mp_aframe_get_channels(frame) : 1;
+}
+
+// Return number of bytes between 2 consecutive samples on the same plane.
+size_t mp_aframe_get_sstride(struct mp_aframe *frame)
+{
+ int format = mp_aframe_get_format(frame);
+ return af_fmt_to_bytes(format) *
+ (af_fmt_is_planar(format) ? 1 : mp_aframe_get_channels(frame));
+}
+
+// Return total number of samples on each plane.
+int mp_aframe_get_total_plane_samples(struct mp_aframe *frame)
+{
+ return frame->av_frame->nb_samples *
+ (af_fmt_is_planar(mp_aframe_get_format(frame))
+ ? 1 : mp_aframe_get_channels(frame));
+}
+
+char *mp_aframe_format_str_buf(char *buf, size_t buf_size, struct mp_aframe *fmt)
+{
+ char ch[128];
+ mp_chmap_to_str_buf(ch, sizeof(ch), &fmt->chmap);
+ char *hr_ch = mp_chmap_to_str_hr(&fmt->chmap);
+ if (strcmp(hr_ch, ch) != 0)
+ mp_snprintf_cat(ch, sizeof(ch), " (%s)", hr_ch);
+ snprintf(buf, buf_size, "%dHz %s %dch %s", fmt->av_frame->sample_rate,
+ ch, fmt->chmap.num, af_fmt_to_str(fmt->format));
+ return buf;
+}
+
+// Set data to the audio after the given number of samples (i.e. slice it).
+void mp_aframe_skip_samples(struct mp_aframe *f, int samples)
+{
+ assert(samples >= 0 && samples <= mp_aframe_get_size(f));
+
+ if (av_frame_make_writable(f->av_frame) < 0)
+ return; // go complain to ffmpeg
+
+ int num_planes = mp_aframe_get_planes(f);
+ size_t sstride = mp_aframe_get_sstride(f);
+ for (int n = 0; n < num_planes; n++) {
+ memmove(f->av_frame->extended_data[n],
+ f->av_frame->extended_data[n] + samples * sstride,
+ (f->av_frame->nb_samples - samples) * sstride);
+ }
+
+ f->av_frame->nb_samples -= samples;
+
+ if (f->pts != MP_NOPTS_VALUE)
+ f->pts += samples / mp_aframe_get_effective_rate(f);
+}
+
+// sanitize a floating point sample value
+#define sanitizef(f) do { \
+ if (!isnormal(f)) \
+ (f) = 0; \
+} while (0)
+
+void mp_aframe_sanitize_float(struct mp_aframe *mpa)
+{
+ int format = af_fmt_from_planar(mp_aframe_get_format(mpa));
+ if (format != AF_FORMAT_FLOAT && format != AF_FORMAT_DOUBLE)
+ return;
+ int num_planes = mp_aframe_get_planes(mpa);
+ uint8_t **planes = mp_aframe_get_data_rw(mpa);
+ if (!planes)
+ return;
+ for (int p = 0; p < num_planes; p++) {
+ void *ptr = planes[p];
+ int total = mp_aframe_get_total_plane_samples(mpa);
+ switch (format) {
+ case AF_FORMAT_FLOAT:
+ for (int s = 0; s < total; s++)
+ sanitizef(((float *)ptr)[s]);
+ break;
+ case AF_FORMAT_DOUBLE:
+ for (int s = 0; s < total; s++)
+ sanitizef(((double *)ptr)[s]);
+ break;
+ }
+ }
+}
+
+// Return the timestamp of the sample just after the end of this frame.
+double mp_aframe_end_pts(struct mp_aframe *f)
+{
+ double rate = mp_aframe_get_effective_rate(f);
+ if (f->pts == MP_NOPTS_VALUE || rate <= 0)
+ return MP_NOPTS_VALUE;
+ return f->pts + f->av_frame->nb_samples / rate;
+}
+
+// Return the duration in seconds of the frame (0 if invalid).
+double mp_aframe_duration(struct mp_aframe *f)
+{
+ double rate = mp_aframe_get_effective_rate(f);
+ if (rate <= 0)
+ return 0;
+ return f->av_frame->nb_samples / rate;
+}
+
+// Clip the given frame to the given timestamp range. Adjusts the frame size
+// and timestamp.
+// Refuses to change spdif frames.
+void mp_aframe_clip_timestamps(struct mp_aframe *f, double start, double end)
+{
+ double f_end = mp_aframe_end_pts(f);
+ double rate = mp_aframe_get_effective_rate(f);
+ if (f_end == MP_NOPTS_VALUE)
+ return;
+ if (end != MP_NOPTS_VALUE) {
+ if (f_end >= end) {
+ if (f->pts >= end) {
+ f->av_frame->nb_samples = 0;
+ } else {
+ if (af_fmt_is_spdif(mp_aframe_get_format(f)))
+ return;
+ int new = (end - f->pts) * rate;
+ f->av_frame->nb_samples = MPCLAMP(new, 0, f->av_frame->nb_samples);
+ }
+ }
+ }
+ if (start != MP_NOPTS_VALUE) {
+ if (f->pts < start) {
+ if (f_end <= start) {
+ f->av_frame->nb_samples = 0;
+ f->pts = f_end;
+ } else {
+ if (af_fmt_is_spdif(mp_aframe_get_format(f)))
+ return;
+ int skip = (start - f->pts) * rate;
+ skip = MPCLAMP(skip, 0, f->av_frame->nb_samples);
+ mp_aframe_skip_samples(f, skip);
+ }
+ }
+ }
+}
+
+bool mp_aframe_copy_samples(struct mp_aframe *dst, int dst_offset,
+ struct mp_aframe *src, int src_offset,
+ int samples)
+{
+ if (!mp_aframe_config_equals(dst, src))
+ return false;
+
+ if (mp_aframe_get_size(dst) < dst_offset + samples ||
+ mp_aframe_get_size(src) < src_offset + samples)
+ return false;
+
+ uint8_t **s = mp_aframe_get_data_ro(src);
+ uint8_t **d = mp_aframe_get_data_rw(dst);
+ if (!s || !d)
+ return false;
+
+ int planes = mp_aframe_get_planes(dst);
+ size_t sstride = mp_aframe_get_sstride(dst);
+
+ for (int n = 0; n < planes; n++) {
+ memcpy(d[n] + dst_offset * sstride, s[n] + src_offset * sstride,
+ samples * sstride);
+ }
+
+ return true;
+}
+
+bool mp_aframe_set_silence(struct mp_aframe *f, int offset, int samples)
+{
+ if (mp_aframe_get_size(f) < offset + samples)
+ return false;
+
+ int format = mp_aframe_get_format(f);
+ uint8_t **d = mp_aframe_get_data_rw(f);
+ if (!d)
+ return false;
+
+ int planes = mp_aframe_get_planes(f);
+ size_t sstride = mp_aframe_get_sstride(f);
+
+ for (int n = 0; n < planes; n++)
+ af_fill_silence(d[n] + offset * sstride, samples * sstride, format);
+
+ return true;
+}
+
+bool mp_aframe_reverse(struct mp_aframe *f)
+{
+ int format = mp_aframe_get_format(f);
+ size_t bps = af_fmt_to_bytes(format);
+ if (!af_fmt_is_pcm(format) || bps > 16)
+ return false;
+
+ uint8_t **d = mp_aframe_get_data_rw(f);
+ if (!d)
+ return false;
+
+ int planes = mp_aframe_get_planes(f);
+ int samples = mp_aframe_get_size(f);
+ int channels = mp_aframe_get_channels(f);
+ size_t sstride = mp_aframe_get_sstride(f);
+
+ int plane_samples = channels;
+ if (af_fmt_is_planar(format))
+ plane_samples = 1;
+
+ for (int p = 0; p < planes; p++) {
+ for (int n = 0; n < samples / 2; n++) {
+ int s1_offset = n * sstride;
+ int s2_offset = (samples - 1 - n) * sstride;
+ for (int c = 0; c < plane_samples; c++) {
+ // Nobody said it'd be fast.
+ char tmp[16];
+ uint8_t *s1 = d[p] + s1_offset + c * bps;
+ uint8_t *s2 = d[p] + s2_offset + c * bps;
+ memcpy(tmp, s2, bps);
+ memcpy(s2, s1, bps);
+ memcpy(s1, tmp, bps);
+ }
+ }
+ }
+
+ return true;
+}
+
+int mp_aframe_approx_byte_size(struct mp_aframe *frame)
+{
+ // God damn, AVFrame is too fucking annoying. Just go with the size that
+ // allocating a new frame would use.
+ int planes = mp_aframe_get_planes(frame);
+ size_t sstride = mp_aframe_get_sstride(frame);
+ int samples = frame->av_frame->nb_samples;
+ int plane_size = MP_ALIGN_UP(sstride * MPMAX(samples, 1), 32);
+ return plane_size * planes + sizeof(*frame);
+}
+
+struct mp_aframe_pool {
+ AVBufferPool *avpool;
+ int element_size;
+};
+
+struct mp_aframe_pool *mp_aframe_pool_create(void *ta_parent)
+{
+ return talloc_zero(ta_parent, struct mp_aframe_pool);
+}
+
+static void mp_aframe_pool_destructor(void *p)
+{
+ struct mp_aframe_pool *pool = p;
+ av_buffer_pool_uninit(&pool->avpool);
+}
+
+// Like mp_aframe_allocate(), but use the pool to allocate data.
+int mp_aframe_pool_allocate(struct mp_aframe_pool *pool, struct mp_aframe *frame,
+ int samples)
+{
+ int planes = mp_aframe_get_planes(frame);
+ size_t sstride = mp_aframe_get_sstride(frame);
+ // FFmpeg hardcodes similar hidden possibly-requirements in a number of
+ // places: av_frame_get_buffer(), libavcodec's get_buffer(), mem.c,
+ // probably more.
+ int align_samples = MP_ALIGN_UP(MPMAX(samples, 1), 32);
+ int plane_size = MP_ALIGN_UP(sstride * align_samples, 64);
+ int size = plane_size * planes;
+
+ if (size <= 0 || mp_aframe_is_allocated(frame))
+ return -1;
+
+ if (!pool->avpool || size > pool->element_size) {
+ size_t alloc = ta_calc_prealloc_elems(size);
+ if (alloc >= INT_MAX)
+ return -1;
+ av_buffer_pool_uninit(&pool->avpool);
+ pool->element_size = alloc;
+ pool->avpool = av_buffer_pool_init(pool->element_size, NULL);
+ if (!pool->avpool)
+ return -1;
+ talloc_set_destructor(pool, mp_aframe_pool_destructor);
+ }
+
+ // Yes, you have to do all this shit manually.
+ // At least it's less stupid than av_frame_get_buffer(), which just wipes
+ // the entire frame struct on error for no reason.
+ AVFrame *av_frame = frame->av_frame;
+ if (av_frame->extended_data != av_frame->data)
+ av_freep(&av_frame->extended_data); // sigh
+ if (planes > AV_NUM_DATA_POINTERS) {
+ av_frame->extended_data =
+ av_calloc(planes, sizeof(av_frame->extended_data[0]));
+ MP_HANDLE_OOM(av_frame->extended_data);
+ } else {
+ av_frame->extended_data = av_frame->data;
+ }
+ av_frame->buf[0] = av_buffer_pool_get(pool->avpool);
+ if (!av_frame->buf[0])
+ return -1;
+ av_frame->linesize[0] = samples * sstride;
+ for (int n = 0; n < planes; n++)
+ av_frame->extended_data[n] = av_frame->buf[0]->data + n * plane_size;
+ if (planes > AV_NUM_DATA_POINTERS) {
+ for (int n = 0; n < AV_NUM_DATA_POINTERS; n++)
+ av_frame->data[n] = av_frame->extended_data[n];
+ }
+ av_frame->nb_samples = samples;
+
+ return 0;
+}
diff --git a/audio/aframe.h b/audio/aframe.h
new file mode 100644
index 0000000..d19c7e8
--- /dev/null
+++ b/audio/aframe.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+struct mp_aframe;
+struct AVFrame;
+struct mp_chmap;
+
+struct mp_aframe *mp_aframe_from_avframe(struct AVFrame *av_frame);
+struct mp_aframe *mp_aframe_create(void);
+struct mp_aframe *mp_aframe_new_ref(struct mp_aframe *frame);
+
+void mp_aframe_reset(struct mp_aframe *frame);
+void mp_aframe_unref_data(struct mp_aframe *frame);
+
+struct AVFrame *mp_aframe_to_avframe(struct mp_aframe *frame);
+struct AVFrame *mp_aframe_to_avframe_and_unref(struct mp_aframe *frame);
+struct AVFrame *mp_aframe_get_raw_avframe(struct mp_aframe *frame);
+
+bool mp_aframe_is_allocated(struct mp_aframe *frame);
+bool mp_aframe_alloc_data(struct mp_aframe *frame, int samples);
+
+void mp_aframe_config_copy(struct mp_aframe *dst, struct mp_aframe *src);
+bool mp_aframe_config_equals(struct mp_aframe *a, struct mp_aframe *b);
+bool mp_aframe_config_is_valid(struct mp_aframe *frame);
+
+void mp_aframe_copy_attributes(struct mp_aframe *dst, struct mp_aframe *src);
+
+uint8_t **mp_aframe_get_data_ro(struct mp_aframe *frame);
+uint8_t **mp_aframe_get_data_rw(struct mp_aframe *frame);
+
+int mp_aframe_get_format(struct mp_aframe *frame);
+bool mp_aframe_get_chmap(struct mp_aframe *frame, struct mp_chmap *out);
+int mp_aframe_get_channels(struct mp_aframe *frame);
+int mp_aframe_get_rate(struct mp_aframe *frame);
+int mp_aframe_get_size(struct mp_aframe *frame);
+double mp_aframe_get_pts(struct mp_aframe *frame);
+double mp_aframe_get_speed(struct mp_aframe *frame);
+double mp_aframe_get_effective_rate(struct mp_aframe *frame);
+
+bool mp_aframe_set_format(struct mp_aframe *frame, int format);
+bool mp_aframe_set_chmap(struct mp_aframe *frame, struct mp_chmap *in);
+bool mp_aframe_set_rate(struct mp_aframe *frame, int rate);
+bool mp_aframe_set_size(struct mp_aframe *frame, int samples);
+void mp_aframe_set_pts(struct mp_aframe *frame, double pts);
+void mp_aframe_set_speed(struct mp_aframe *frame, double factor);
+void mp_aframe_mul_speed(struct mp_aframe *frame, double factor);
+
+int mp_aframe_get_planes(struct mp_aframe *frame);
+int mp_aframe_get_total_plane_samples(struct mp_aframe *frame);
+size_t mp_aframe_get_sstride(struct mp_aframe *frame);
+
+bool mp_aframe_reverse(struct mp_aframe *frame);
+
+int mp_aframe_approx_byte_size(struct mp_aframe *frame);
+
+char *mp_aframe_format_str_buf(char *buf, size_t buf_size, struct mp_aframe *fmt);
+#define mp_aframe_format_str(fmt) mp_aframe_format_str_buf((char[32]){0}, 32, (fmt))
+
+void mp_aframe_skip_samples(struct mp_aframe *f, int samples);
+void mp_aframe_sanitize_float(struct mp_aframe *f);
+double mp_aframe_end_pts(struct mp_aframe *f);
+double mp_aframe_duration(struct mp_aframe *f);
+void mp_aframe_clip_timestamps(struct mp_aframe *f, double start, double end);
+bool mp_aframe_copy_samples(struct mp_aframe *dst, int dst_offset,
+ struct mp_aframe *src, int src_offset,
+ int samples);
+bool mp_aframe_set_silence(struct mp_aframe *f, int offset, int samples);
+
+struct mp_aframe_pool;
+struct mp_aframe_pool *mp_aframe_pool_create(void *ta_parent);
+int mp_aframe_pool_allocate(struct mp_aframe_pool *pool, struct mp_aframe *frame,
+ int samples);
diff --git a/audio/chmap.c b/audio/chmap.c
new file mode 100644
index 0000000..e2b95f4
--- /dev/null
+++ b/audio/chmap.c
@@ -0,0 +1,515 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "chmap.h"
+
+// Names taken from libavutil/channel_layout.c (Not accessible by API.)
+// Use of these names is hard-coded in some places (e.g. ao_alsa.c)
+static const char *const speaker_names[MP_SPEAKER_ID_COUNT][2] = {
+ [MP_SPEAKER_ID_FL] = {"fl", "front left"},
+ [MP_SPEAKER_ID_FR] = {"fr", "front right"},
+ [MP_SPEAKER_ID_FC] = {"fc", "front center"},
+ [MP_SPEAKER_ID_LFE] = {"lfe", "low frequency"},
+ [MP_SPEAKER_ID_BL] = {"bl", "back left"},
+ [MP_SPEAKER_ID_BR] = {"br", "back right"},
+ [MP_SPEAKER_ID_FLC] = {"flc", "front left-of-center"},
+ [MP_SPEAKER_ID_FRC] = {"frc", "front right-of-center"},
+ [MP_SPEAKER_ID_BC] = {"bc", "back center"},
+ [MP_SPEAKER_ID_SL] = {"sl", "side left"},
+ [MP_SPEAKER_ID_SR] = {"sr", "side right"},
+ [MP_SPEAKER_ID_TC] = {"tc", "top center"},
+ [MP_SPEAKER_ID_TFL] = {"tfl", "top front left"},
+ [MP_SPEAKER_ID_TFC] = {"tfc", "top front center"},
+ [MP_SPEAKER_ID_TFR] = {"tfr", "top front right"},
+ [MP_SPEAKER_ID_TBL] = {"tbl", "top back left"},
+ [MP_SPEAKER_ID_TBC] = {"tbc", "top back center"},
+ [MP_SPEAKER_ID_TBR] = {"tbr", "top back right"},
+ [MP_SPEAKER_ID_DL] = {"dl", "downmix left"},
+ [MP_SPEAKER_ID_DR] = {"dr", "downmix right"},
+ [MP_SPEAKER_ID_WL] = {"wl", "wide left"},
+ [MP_SPEAKER_ID_WR] = {"wr", "wide right"},
+ [MP_SPEAKER_ID_SDL] = {"sdl", "surround direct left"},
+ [MP_SPEAKER_ID_SDR] = {"sdr", "surround direct right"},
+ [MP_SPEAKER_ID_LFE2] = {"lfe2", "low frequency 2"},
+ [MP_SPEAKER_ID_TSL] = {"tsl", "top side left"},
+ [MP_SPEAKER_ID_TSR] = {"tsr", "top side right"},
+ [MP_SPEAKER_ID_BFC] = {"bfc", "bottom front center"},
+ [MP_SPEAKER_ID_BFL] = {"bfl", "bottom front left"},
+ [MP_SPEAKER_ID_BFR] = {"bfr", "bottom front right"},
+ [MP_SPEAKER_ID_NA] = {"na", "not available"},
+};
+
+// Names taken from libavutil/channel_layout.c (Not accessible by API.)
+// Channel order corresponds to lavc/waveex, except for the alsa entries.
+static const char *const std_layout_names[][2] = {
+ {"empty", ""}, // not in lavc
+ {"mono", "fc"},
+ {"1.0", "fc"}, // not in lavc
+ {"stereo", "fl-fr"},
+ {"2.0", "fl-fr"}, // not in lavc
+ {"2.1", "fl-fr-lfe"},
+ {"3.0", "fl-fr-fc"},
+ {"3.0(back)", "fl-fr-bc"},
+ {"4.0", "fl-fr-fc-bc"},
+ {"quad", "fl-fr-bl-br"},
+ {"quad(side)", "fl-fr-sl-sr"},
+ {"3.1", "fl-fr-fc-lfe"},
+ {"3.1(back)", "fl-fr-lfe-bc"}, // not in lavc
+ {"5.0", "fl-fr-fc-bl-br"},
+ {"5.0(alsa)", "fl-fr-bl-br-fc"}, // not in lavc
+ {"5.0(side)", "fl-fr-fc-sl-sr"},
+ {"4.1", "fl-fr-fc-lfe-bc"},
+ {"4.1(alsa)", "fl-fr-bl-br-lfe"}, // not in lavc
+ {"5.1", "fl-fr-fc-lfe-bl-br"},
+ {"5.1(alsa)", "fl-fr-bl-br-fc-lfe"}, // not in lavc
+ {"5.1(side)", "fl-fr-fc-lfe-sl-sr"},
+ {"6.0", "fl-fr-fc-bc-sl-sr"},
+ {"6.0(front)", "fl-fr-flc-frc-sl-sr"},
+ {"hexagonal", "fl-fr-fc-bl-br-bc"},
+ {"6.1", "fl-fr-fc-lfe-bc-sl-sr"},
+ {"6.1(back)", "fl-fr-fc-lfe-bl-br-bc"},
+ {"6.1(top)", "fl-fr-fc-lfe-bl-br-tc"}, // not in lavc
+ {"6.1(front)", "fl-fr-lfe-flc-frc-sl-sr"},
+ {"7.0", "fl-fr-fc-bl-br-sl-sr"},
+ {"7.0(front)", "fl-fr-fc-flc-frc-sl-sr"},
+ {"7.0(rear)", "fl-fr-fc-bl-br-sdl-sdr"}, // not in lavc
+ {"7.1", "fl-fr-fc-lfe-bl-br-sl-sr"},
+ {"7.1(alsa)", "fl-fr-bl-br-fc-lfe-sl-sr"}, // not in lavc
+ {"7.1(wide)", "fl-fr-fc-lfe-bl-br-flc-frc"},
+ {"7.1(wide-side)", "fl-fr-fc-lfe-flc-frc-sl-sr"},
+ {"7.1(top)", "fl-fr-fc-lfe-bl-br-tfl-tfr"},
+ {"7.1(rear)", "fl-fr-fc-lfe-bl-br-sdl-sdr"}, // not in lavc
+ {"octagonal", "fl-fr-fc-bl-br-bc-sl-sr"},
+ {"cube", "fl-fr-bl-br-tfl-tfr-tbl-tbr"},
+ {"hexadecagonal", "fl-fr-fc-bl-br-bc-sl-sr-tfc-tfl-tfr-tbl-tbc-tbr-wl-wr"},
+ {"downmix", "fl-fr"},
+ {"22.2", "fl-fr-fc-lfe-bl-br-flc-frc-bc-sl-sr-tc-tfl-tfc-tfr-tbl-tbc-tbr-lfe2-tsl-tsr-bfc-bfl-bfr"},
+ {"auto", ""}, // not in lavc
+ {0}
+};
+
+static const struct mp_chmap default_layouts[] = {
+ {0}, // empty
+ MP_CHMAP_INIT_MONO, // mono
+ MP_CHMAP2(FL, FR), // stereo
+ MP_CHMAP3(FL, FR, LFE), // 2.1
+ MP_CHMAP4(FL, FR, FC, BC), // 4.0
+ MP_CHMAP5(FL, FR, FC, BL, BR), // 5.0
+ MP_CHMAP6(FL, FR, FC, LFE, BL, BR), // 5.1
+ MP_CHMAP7(FL, FR, FC, LFE, BC, SL, SR), // 6.1
+ MP_CHMAP8(FL, FR, FC, LFE, BL, BR, SL, SR), // 7.1
+};
+
+// Returns true if speakers are mapped uniquely, and there's at least 1 channel.
+bool mp_chmap_is_valid(const struct mp_chmap *src)
+{
+ bool mapped[MP_SPEAKER_ID_COUNT] = {0};
+ for (int n = 0; n < src->num; n++) {
+ int sp = src->speaker[n];
+ if (sp >= MP_SPEAKER_ID_COUNT || mapped[sp])
+ return false;
+ if (sp != MP_SPEAKER_ID_NA)
+ mapped[sp] = true;
+ }
+ return src->num > 0;
+}
+
+bool mp_chmap_is_empty(const struct mp_chmap *src)
+{
+ return src->num == 0;
+}
+
+// Return true if the channel map defines the number of the channels only, and
+// the channels have to meaning associated with them.
+bool mp_chmap_is_unknown(const struct mp_chmap *src)
+{
+ for (int n = 0; n < src->num; n++) {
+ if (src->speaker[n] != MP_SPEAKER_ID_NA)
+ return false;
+ }
+ return mp_chmap_is_valid(src);
+}
+
+// Note: empty channel maps compare as equal. Invalid ones can equal too.
+bool mp_chmap_equals(const struct mp_chmap *a, const struct mp_chmap *b)
+{
+ if (a->num != b->num)
+ return false;
+ for (int n = 0; n < a->num; n++) {
+ if (a->speaker[n] != b->speaker[n])
+ return false;
+ }
+ return true;
+}
+
+// Whether they use the same speakers (even if in different order).
+bool mp_chmap_equals_reordered(const struct mp_chmap *a, const struct mp_chmap *b)
+{
+ struct mp_chmap t1 = *a, t2 = *b;
+ mp_chmap_reorder_norm(&t1);
+ mp_chmap_reorder_norm(&t2);
+ return mp_chmap_equals(&t1, &t2);
+}
+
+bool mp_chmap_is_stereo(const struct mp_chmap *src)
+{
+ static const struct mp_chmap stereo = MP_CHMAP_INIT_STEREO;
+ return mp_chmap_equals(src, &stereo);
+}
+
+static int comp_uint8(const void *a, const void *b)
+{
+ return *(const uint8_t *)a - *(const uint8_t *)b;
+}
+
+// Reorder channels to normal order, with monotonically increasing speaker IDs.
+// We define this order as the same order used with waveex.
+void mp_chmap_reorder_norm(struct mp_chmap *map)
+{
+ uint8_t *arr = &map->speaker[0];
+ qsort(arr, map->num, 1, comp_uint8);
+}
+
+// Remove silent (NA) channels, if any.
+void mp_chmap_remove_na(struct mp_chmap *map)
+{
+ struct mp_chmap new = {0};
+ for (int n = 0; n < map->num; n++) {
+ int sp = map->speaker[n];
+ if (sp != MP_SPEAKER_ID_NA)
+ new.speaker[new.num++] = map->speaker[n];
+ }
+ *map = new;
+}
+
+// Add silent (NA) channels to map until map->num >= num.
+void mp_chmap_fill_na(struct mp_chmap *map, int num)
+{
+ assert(num <= MP_NUM_CHANNELS);
+ while (map->num < num)
+ map->speaker[map->num++] = MP_SPEAKER_ID_NA;
+}
+
+// Set *dst to a standard layout with the given number of channels.
+// If the number of channels is invalid, an invalid map is set, and
+// mp_chmap_is_valid(dst) will return false.
+void mp_chmap_from_channels(struct mp_chmap *dst, int num_channels)
+{
+ *dst = (struct mp_chmap) {0};
+ if (num_channels >= 0 && num_channels < MP_ARRAY_SIZE(default_layouts))
+ *dst = default_layouts[num_channels];
+ if (!dst->num)
+ mp_chmap_set_unknown(dst, num_channels);
+}
+
+// Set *dst to an unknown layout for the given numbers of channels.
+// If the number of channels is invalid, an invalid map is set, and
+// mp_chmap_is_valid(dst) will return false.
+// A mp_chmap with all entries set to NA is treated specially in some
+// contexts (watch out for mp_chmap_is_unknown()).
+void mp_chmap_set_unknown(struct mp_chmap *dst, int num_channels)
+{
+ if (num_channels < 0 || num_channels > MP_NUM_CHANNELS) {
+ *dst = (struct mp_chmap) {0};
+ } else {
+ dst->num = num_channels;
+ for (int n = 0; n < dst->num; n++)
+ dst->speaker[n] = MP_SPEAKER_ID_NA;
+ }
+}
+
+// Return the ffmpeg/libav channel layout as in <libavutil/channel_layout.h>.
+// Speakers not representable by ffmpeg/libav are dropped.
+// Warning: this ignores the order of the channels, and will return a channel
+// mask even if the order is different from libavcodec's.
+// Also, "unknown" channel maps are translated to non-sense channel
+// maps with the same number of channels.
+uint64_t mp_chmap_to_lavc_unchecked(const struct mp_chmap *src)
+{
+ struct mp_chmap t = *src;
+ if (t.num > 64)
+ return 0;
+ // lavc has no concept for unknown layouts yet, so pick something that does
+ // the job of signaling the number of channels, even if it makes no sense
+ // as a proper layout.
+ if (mp_chmap_is_unknown(&t))
+ return t.num == 64 ? (uint64_t)-1 : (1ULL << t.num) - 1;
+ uint64_t mask = 0;
+ for (int n = 0; n < t.num; n++) {
+ if (t.speaker[n] < 64) // ignore MP_SPEAKER_ID_NA etc.
+ mask |= 1ULL << t.speaker[n];
+ }
+ return mask;
+}
+
+// Return the ffmpeg/libav channel layout as in <libavutil/channel_layout.h>.
+// Returns 0 if the channel order doesn't match lavc's or if it's invalid.
+uint64_t mp_chmap_to_lavc(const struct mp_chmap *src)
+{
+ if (!mp_chmap_is_lavc(src))
+ return 0;
+ return mp_chmap_to_lavc_unchecked(src);
+}
+
+// Set channel map from the ffmpeg/libav channel layout as in
+// <libavutil/channel_layout.h>.
+// If the number of channels exceed MP_NUM_CHANNELS, set dst to empty.
+void mp_chmap_from_lavc(struct mp_chmap *dst, uint64_t src)
+{
+ dst->num = 0;
+ for (int n = 0; n < 64; n++) {
+ if (src & (1ULL << n)) {
+ if (dst->num >= MP_NUM_CHANNELS) {
+ dst->num = 0;
+ return;
+ }
+ dst->speaker[dst->num] = n;
+ dst->num++;
+ }
+ }
+}
+
+bool mp_chmap_is_lavc(const struct mp_chmap *src)
+{
+ if (!mp_chmap_is_valid(src))
+ return false;
+ if (mp_chmap_is_unknown(src))
+ return true;
+ // lavc's channel layout is a bit mask, and channels are always ordered
+ // from LSB to MSB speaker bits, so speaker IDs have to increase.
+ assert(src->num > 0);
+ for (int n = 1; n < src->num; n++) {
+ if (src->speaker[n - 1] >= src->speaker[n])
+ return false;
+ }
+ for (int n = 0; n < src->num; n++) {
+ if (src->speaker[n] >= 64)
+ return false;
+ }
+ return true;
+}
+
+// Warning: for "unknown" channel maps, this returns something that may not
+// make sense. Invalid channel maps are not changed.
+void mp_chmap_reorder_to_lavc(struct mp_chmap *map)
+{
+ if (!mp_chmap_is_valid(map))
+ return;
+ uint64_t mask = mp_chmap_to_lavc_unchecked(map);
+ mp_chmap_from_lavc(map, mask);
+}
+
+// Get reordering array for from->to reordering. from->to must have the same set
+// of speakers (i.e. same number and speaker IDs, just different order). Then,
+// for each speaker n, src[n] will be set such that:
+// to->speaker[n] = from->speaker[src[n]]
+// (src[n] gives the source channel for destination channel n)
+// If *from and *to don't contain the same set of speakers, then the above
+// invariant is not guaranteed. Instead, src[n] can be set to -1 if the channel
+// at to->speaker[n] is unmapped.
+void mp_chmap_get_reorder(int src[MP_NUM_CHANNELS], const struct mp_chmap *from,
+ const struct mp_chmap *to)
+{
+ for (int n = 0; n < MP_NUM_CHANNELS; n++)
+ src[n] = -1;
+
+ if (mp_chmap_is_unknown(from) || mp_chmap_is_unknown(to)) {
+ for (int n = 0; n < to->num; n++)
+ src[n] = n < from->num ? n : -1;
+ return;
+ }
+
+ for (int n = 0; n < to->num; n++) {
+ for (int i = 0; i < from->num; i++) {
+ if (to->speaker[n] == from->speaker[i]) {
+ src[n] = i;
+ break;
+ }
+ }
+ }
+
+ for (int n = 0; n < to->num; n++)
+ assert(src[n] < 0 || (to->speaker[n] == from->speaker[src[n]]));
+}
+
+// Return the number of channels only in a.
+int mp_chmap_diffn(const struct mp_chmap *a, const struct mp_chmap *b)
+{
+ uint64_t a_mask = mp_chmap_to_lavc_unchecked(a);
+ uint64_t b_mask = mp_chmap_to_lavc_unchecked(b);
+ return av_popcount64((a_mask ^ b_mask) & a_mask);
+}
+
+// Returns something like "fl-fr-fc". If there's a standard layout in lavc
+// order, return that, e.g. "3.0" instead of "fl-fr-fc".
+// Unassigned but valid speakers get names like "sp28".
+char *mp_chmap_to_str_buf(char *buf, size_t buf_size, const struct mp_chmap *src)
+{
+ buf[0] = '\0';
+
+ if (mp_chmap_is_unknown(src)) {
+ snprintf(buf, buf_size, "unknown%d", src->num);
+ return buf;
+ }
+
+ for (int n = 0; n < src->num; n++) {
+ int sp = src->speaker[n];
+ const char *s = sp < MP_SPEAKER_ID_COUNT ? speaker_names[sp][0] : NULL;
+ char sp_buf[10];
+ if (!s) {
+ snprintf(sp_buf, sizeof(sp_buf), "sp%d", sp);
+ s = sp_buf;
+ }
+ mp_snprintf_cat(buf, buf_size, "%s%s", n > 0 ? "-" : "", s);
+ }
+
+ // To standard layout name
+ for (int n = 0; std_layout_names[n][0]; n++) {
+ if (strcmp(buf, std_layout_names[n][1]) == 0) {
+ snprintf(buf, buf_size, "%s", std_layout_names[n][0]);
+ break;
+ }
+ }
+
+ return buf;
+}
+
+// If src can be parsed as channel map (as produced by mp_chmap_to_str()),
+// return true and set *dst. Otherwise, return false and don't change *dst.
+// Note: call mp_chmap_is_valid() to test whether the returned map is valid
+// the map could be empty, or contain multiply mapped channels
+bool mp_chmap_from_str(struct mp_chmap *dst, bstr src)
+{
+ // Single number corresponds to mp_chmap_from_channels()
+ if (src.len > 0) {
+ bstr t = src;
+ bool unknown = bstr_eatstart0(&t, "unknown");
+ bstr rest;
+ long long count = bstrtoll(t, &rest, 10);
+ if (rest.len == 0) {
+ struct mp_chmap res;
+ if (unknown) {
+ mp_chmap_set_unknown(&res, count);
+ } else {
+ mp_chmap_from_channels(&res, count);
+ }
+ if (mp_chmap_is_valid(&res)) {
+ *dst = res;
+ return true;
+ }
+ }
+ }
+
+ // From standard layout name
+ for (int n = 0; std_layout_names[n][0]; n++) {
+ if (bstr_equals0(src, std_layout_names[n][0])) {
+ src = bstr0(std_layout_names[n][1]);
+ break;
+ }
+ }
+
+ // Explicit speaker list (separated by "-")
+ struct mp_chmap res = {0};
+ while (src.len) {
+ bstr s;
+ bstr_split_tok(src, "-", &s, &src);
+ int speaker = -1;
+ for (int n = 0; n < MP_SPEAKER_ID_COUNT; n++) {
+ const char *name = speaker_names[n][0];
+ if (name && bstr_equals0(s, name)) {
+ speaker = n;
+ break;
+ }
+ }
+ if (speaker < 0) {
+ if (bstr_eatstart0(&s, "sp")) {
+ long long sp = bstrtoll(s, &s, 0);
+ if (s.len == 0 && sp >= 0 && sp < MP_SPEAKER_ID_COUNT)
+ speaker = sp;
+ }
+ if (speaker < 0)
+ return false;
+ }
+ if (res.num >= MP_NUM_CHANNELS)
+ return false;
+ res.speaker[res.num] = speaker;
+ res.num++;
+ }
+
+ *dst = res;
+ return true;
+}
+
+// Output a human readable "canonical" channel map string. Converting this from
+// a string back to a channel map can yield a different map, but the string
+// looks nicer. E.g. "fc-fl-fr-na" becomes "3.0".
+char *mp_chmap_to_str_hr_buf(char *buf, size_t buf_size, const struct mp_chmap *src)
+{
+ struct mp_chmap map = *src;
+ mp_chmap_remove_na(&map);
+ for (int n = 0; std_layout_names[n][0]; n++) {
+ struct mp_chmap s;
+ if (mp_chmap_from_str(&s, bstr0(std_layout_names[n][0])) &&
+ mp_chmap_equals_reordered(&s, &map))
+ {
+ map = s;
+ break;
+ }
+ }
+ return mp_chmap_to_str_buf(buf, buf_size, &map);
+}
+
+mp_ch_layout_tuple *mp_iterate_builtin_layouts(void **opaque)
+{
+ uintptr_t i = (uintptr_t)*opaque;
+
+ if (i >= MP_ARRAY_SIZE(std_layout_names) ||
+ !std_layout_names[i][0])
+ return NULL;
+
+ *opaque = (void *)(i + 1);
+
+ if (std_layout_names[i][1][0] == '\0') {
+ return mp_iterate_builtin_layouts(opaque);
+ }
+
+ return &std_layout_names[i];
+}
+
+void mp_chmap_print_help(struct mp_log *log)
+{
+ mp_info(log, "Speakers:\n");
+ for (int n = 0; n < MP_SPEAKER_ID_COUNT; n++) {
+ if (speaker_names[n][0])
+ mp_info(log, " %-16s (%s)\n",
+ speaker_names[n][0], speaker_names[n][1]);
+ }
+ mp_info(log, "Standard layouts:\n");
+ for (int n = 0; std_layout_names[n][0]; n++) {
+ mp_info(log, " %-16s (%s)\n",
+ std_layout_names[n][0], std_layout_names[n][1]);
+ }
+ for (int n = 0; n < MP_NUM_CHANNELS; n++)
+ mp_info(log, " unknown%d\n", n + 1);
+}
diff --git a/audio/chmap.h b/audio/chmap.h
new file mode 100644
index 0000000..58a3f71
--- /dev/null
+++ b/audio/chmap.h
@@ -0,0 +1,163 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_CHMAP_H
+#define MP_CHMAP_H
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include "misc/bstr.h"
+
+#define MP_NUM_CHANNELS 64
+
+// Speaker a channel can be assigned to.
+// This corresponds to WAVEFORMATEXTENSIBLE channel mask bit indexes.
+// E.g. channel_mask = (1 << MP_SPEAKER_ID_FL) | ...
+enum mp_speaker_id {
+ // Official WAVEFORMATEXTENSIBLE (shortened names)
+ MP_SPEAKER_ID_FL = 0, // FRONT_LEFT
+ MP_SPEAKER_ID_FR, // FRONT_RIGHT
+ MP_SPEAKER_ID_FC, // FRONT_CENTER
+ MP_SPEAKER_ID_LFE, // LOW_FREQUENCY
+ MP_SPEAKER_ID_BL, // BACK_LEFT
+ MP_SPEAKER_ID_BR, // BACK_RIGHT
+ MP_SPEAKER_ID_FLC, // FRONT_LEFT_OF_CENTER
+ MP_SPEAKER_ID_FRC, // FRONT_RIGHT_OF_CENTER
+ MP_SPEAKER_ID_BC, // BACK_CENTER
+ MP_SPEAKER_ID_SL, // SIDE_LEFT
+ MP_SPEAKER_ID_SR, // SIDE_RIGHT
+ MP_SPEAKER_ID_TC, // TOP_CENTER
+ MP_SPEAKER_ID_TFL, // TOP_FRONT_LEFT
+ MP_SPEAKER_ID_TFC, // TOP_FRONT_CENTER
+ MP_SPEAKER_ID_TFR, // TOP_FRONT_RIGHT
+ MP_SPEAKER_ID_TBL, // TOP_BACK_LEFT
+ MP_SPEAKER_ID_TBC, // TOP_BACK_CENTER
+ MP_SPEAKER_ID_TBR, // TOP_BACK_RIGHT
+ // Unofficial/libav* extensions
+ MP_SPEAKER_ID_DL = 29, // STEREO_LEFT (stereo downmix special speakers)
+ MP_SPEAKER_ID_DR, // STEREO_RIGHT
+ MP_SPEAKER_ID_WL, // WIDE_LEFT
+ MP_SPEAKER_ID_WR, // WIDE_RIGHT
+ MP_SPEAKER_ID_SDL, // SURROUND_DIRECT_LEFT
+ MP_SPEAKER_ID_SDR, // SURROUND_DIRECT_RIGHT
+ MP_SPEAKER_ID_LFE2, // LOW_FREQUENCY_2
+ MP_SPEAKER_ID_TSL, // TOP_SIDE_LEFT
+ MP_SPEAKER_ID_TSR, // TOP_SIDE_RIGHT,
+ MP_SPEAKER_ID_BFC, // BOTTOM_FRONT_CENTER
+ MP_SPEAKER_ID_BFL, // BOTTOM_FRONT_LEFT
+ MP_SPEAKER_ID_BFR, // BOTTOM_FRONT_RIGHT
+
+ // Speaker IDs >= 64 are not representable in WAVEFORMATEXTENSIBLE or libav*.
+
+ // "Silent" channels. These are sometimes used to insert padding for
+ // unused channels. Unlike other speaker types, multiple of these can
+ // occur in a single mp_chmap.
+ MP_SPEAKER_ID_NA = 64,
+
+ // Including the unassigned IDs in between. This is not a valid ID anymore,
+ // but is still within uint8_t.
+ MP_SPEAKER_ID_COUNT,
+};
+
+struct mp_chmap {
+ uint8_t num; // number of channels
+ // Given a channel n, speaker[n] is the speaker ID driven by that channel.
+ // Entries after speaker[num - 1] are undefined.
+ uint8_t speaker[MP_NUM_CHANNELS];
+};
+
+typedef const char * const (mp_ch_layout_tuple)[2];
+
+#define MP_SP(speaker) MP_SPEAKER_ID_ ## speaker
+
+#define MP_CHMAP2(a, b) \
+ {2, {MP_SP(a), MP_SP(b)}}
+#define MP_CHMAP3(a, b, c) \
+ {3, {MP_SP(a), MP_SP(b), MP_SP(c)}}
+#define MP_CHMAP4(a, b, c, d) \
+ {4, {MP_SP(a), MP_SP(b), MP_SP(c), MP_SP(d)}}
+#define MP_CHMAP5(a, b, c, d, e) \
+ {5, {MP_SP(a), MP_SP(b), MP_SP(c), MP_SP(d), MP_SP(e)}}
+#define MP_CHMAP6(a, b, c, d, e, f) \
+ {6, {MP_SP(a), MP_SP(b), MP_SP(c), MP_SP(d), MP_SP(e), MP_SP(f)}}
+#define MP_CHMAP7(a, b, c, d, e, f, g) \
+ {7, {MP_SP(a), MP_SP(b), MP_SP(c), MP_SP(d), MP_SP(e), MP_SP(f), MP_SP(g)}}
+#define MP_CHMAP8(a, b, c, d, e, f, g, h) \
+ {8, {MP_SP(a), MP_SP(b), MP_SP(c), MP_SP(d), MP_SP(e), MP_SP(f), MP_SP(g), MP_SP(h)}}
+
+#define MP_CHMAP_INIT_MONO {1, {MP_SPEAKER_ID_FC}}
+#define MP_CHMAP_INIT_STEREO MP_CHMAP2(FL, FR)
+
+bool mp_chmap_is_valid(const struct mp_chmap *src);
+bool mp_chmap_is_empty(const struct mp_chmap *src);
+bool mp_chmap_is_unknown(const struct mp_chmap *src);
+bool mp_chmap_equals(const struct mp_chmap *a, const struct mp_chmap *b);
+bool mp_chmap_equals_reordered(const struct mp_chmap *a, const struct mp_chmap *b);
+bool mp_chmap_is_stereo(const struct mp_chmap *src);
+
+void mp_chmap_reorder_norm(struct mp_chmap *map);
+void mp_chmap_remove_na(struct mp_chmap *map);
+void mp_chmap_fill_na(struct mp_chmap *map, int num);
+
+void mp_chmap_from_channels(struct mp_chmap *dst, int num_channels);
+void mp_chmap_set_unknown(struct mp_chmap *dst, int num_channels);
+
+uint64_t mp_chmap_to_lavc(const struct mp_chmap *src);
+uint64_t mp_chmap_to_lavc_unchecked(const struct mp_chmap *src);
+void mp_chmap_from_lavc(struct mp_chmap *dst, uint64_t src);
+
+bool mp_chmap_is_lavc(const struct mp_chmap *src);
+void mp_chmap_reorder_to_lavc(struct mp_chmap *map);
+
+void mp_chmap_get_reorder(int src[MP_NUM_CHANNELS], const struct mp_chmap *from,
+ const struct mp_chmap *to);
+
+int mp_chmap_diffn(const struct mp_chmap *a, const struct mp_chmap *b);
+
+char *mp_chmap_to_str_buf(char *buf, size_t buf_size, const struct mp_chmap *src);
+#define mp_chmap_to_str_(m, sz) mp_chmap_to_str_buf((char[sz]){0}, sz, (m))
+#define mp_chmap_to_str(m) mp_chmap_to_str_(m, MP_NUM_CHANNELS * 4)
+
+char *mp_chmap_to_str_hr_buf(char *buf, size_t buf_size, const struct mp_chmap *src);
+#define mp_chmap_to_str_hr_(m, sz) mp_chmap_to_str_hr_buf((char[sz]){0}, sz, (m))
+#define mp_chmap_to_str_hr(m) mp_chmap_to_str_hr_(m, MP_NUM_CHANNELS * 4)
+
+bool mp_chmap_from_str(struct mp_chmap *dst, bstr src);
+
+/**
+ * Iterate over all built-in channel layouts which have mapped channels.
+ *
+ * @param opaque a pointer where the iteration state is stored. Must point
+ * to nullptr to start the iteration.
+ *
+ * @return nullptr when the iteration is finished.
+ * Otherwise a pointer to an array of two char pointers.
+ * - [0] being the human-readable layout name.
+ * - [1] being the string representation of the layout.
+ */
+mp_ch_layout_tuple *mp_iterate_builtin_layouts(void **opaque);
+
+struct mp_log;
+void mp_chmap_print_help(struct mp_log *log);
+
+// Use these to avoid chaos in case lavc's definition should diverge from MS.
+#define mp_chmap_to_waveext mp_chmap_to_lavc
+#define mp_chmap_from_waveext mp_chmap_from_lavc
+#define mp_chmap_is_waveext mp_chmap_is_lavc
+#define mp_chmap_reorder_to_waveext mp_chmap_reorder_to_lavc
+
+#endif
diff --git a/audio/chmap_avchannel.c b/audio/chmap_avchannel.c
new file mode 100644
index 0000000..ec961de
--- /dev/null
+++ b/audio/chmap_avchannel.c
@@ -0,0 +1,51 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/channel_layout.h>
+
+#include "chmap.h"
+#include "chmap_avchannel.h"
+
+bool mp_chmap_from_av_layout(struct mp_chmap *dst, const AVChannelLayout *src)
+{
+ *dst = (struct mp_chmap) {0};
+
+ switch (src->order) {
+ case AV_CHANNEL_ORDER_UNSPEC:
+ mp_chmap_from_channels(dst, src->nb_channels);
+ return dst->num == src->nb_channels;
+ case AV_CHANNEL_ORDER_NATIVE:
+ mp_chmap_from_lavc(dst, src->u.mask);
+ return dst->num == src->nb_channels;
+ default:
+ // TODO: handle custom layouts
+ return false;
+ }
+}
+
+void mp_chmap_to_av_layout(AVChannelLayout *dst, const struct mp_chmap *src)
+{
+ *dst = (AVChannelLayout){
+ .order = AV_CHANNEL_ORDER_UNSPEC,
+ .nb_channels = src->num,
+ };
+
+ // TODO: handle custom layouts
+ if (!mp_chmap_is_unknown(src)) {
+ av_channel_layout_from_mask(dst, mp_chmap_to_lavc(src));
+ }
+}
diff --git a/audio/chmap_avchannel.h b/audio/chmap_avchannel.h
new file mode 100644
index 0000000..e136ccc
--- /dev/null
+++ b/audio/chmap_avchannel.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libavutil/channel_layout.h>
+
+#include "config.h"
+
+#include "chmap.h"
+
+#if HAVE_AV_CHANNEL_LAYOUT
+
+bool mp_chmap_from_av_layout(struct mp_chmap *dst, const AVChannelLayout *src);
+
+void mp_chmap_to_av_layout(AVChannelLayout *dst, const struct mp_chmap *src);
+
+#endif
diff --git a/audio/chmap_sel.c b/audio/chmap_sel.c
new file mode 100644
index 0000000..4fb7544
--- /dev/null
+++ b/audio/chmap_sel.c
@@ -0,0 +1,389 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+#include <limits.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "chmap_sel.h"
+
+static const struct mp_chmap speaker_replacements[][2] = {
+ // 5.1 <-> 5.1 (side)
+ { MP_CHMAP2(SL, SR), MP_CHMAP2(BL, BR) },
+ // 7.1 <-> 7.1 (rear ext)
+ { MP_CHMAP2(SL, SR), MP_CHMAP2(SDL, SDR) },
+};
+
+// Try to replace speakers from the left of the list with the ones on the
+// right, or the other way around.
+static bool replace_speakers(struct mp_chmap *map, struct mp_chmap list[2])
+{
+ assert(list[0].num == list[1].num);
+ if (!mp_chmap_is_valid(map))
+ return false;
+ for (int dir = 0; dir < 2; dir++) {
+ int from = dir ? 0 : 1;
+ int to = dir ? 1 : 0;
+ bool replaced = false;
+ struct mp_chmap t = *map;
+ for (int n = 0; n < t.num; n++) {
+ for (int i = 0; i < list[0].num; i++) {
+ if (t.speaker[n] == list[from].speaker[i]) {
+ t.speaker[n] = list[to].speaker[i];
+ replaced = true;
+ break;
+ }
+ }
+ }
+ if (replaced && mp_chmap_is_valid(&t)) {
+ *map = t;
+ return true;
+ }
+ }
+ return false;
+}
+
+// These go strictly from the first to the second entry and always use the
+// full layout (possibly reordered and/or padding channels added).
+static const struct mp_chmap preferred_remix[][2] = {
+ // mono can be perfectly played as stereo
+ { MP_CHMAP_INIT_MONO, MP_CHMAP_INIT_STEREO },
+};
+
+// Conversion from src to dst is explicitly encouraged and should be preferred
+// over "mathematical" upmixes or downmixes (which minimize lost channels).
+static bool test_preferred_remix(const struct mp_chmap *src,
+ const struct mp_chmap *dst)
+{
+ struct mp_chmap src_p = *src, dst_p = *dst;
+ mp_chmap_remove_na(&src_p);
+ mp_chmap_remove_na(&dst_p);
+ for (int n = 0; n < MP_ARRAY_SIZE(preferred_remix); n++) {
+ if (mp_chmap_equals_reordered(&src_p, &preferred_remix[n][0]) &&
+ mp_chmap_equals_reordered(&dst_p, &preferred_remix[n][1]))
+ return true;
+ }
+ return false;
+}
+
+// Allow all channel layouts that can be expressed with mp_chmap.
+// (By default, all layouts are rejected.)
+void mp_chmap_sel_add_any(struct mp_chmap_sel *s)
+{
+ s->allow_any = true;
+}
+
+// Allow all waveext formats, and force waveext channel order.
+void mp_chmap_sel_add_waveext(struct mp_chmap_sel *s)
+{
+ s->allow_waveext = true;
+}
+
+// Add a channel map that should be allowed.
+void mp_chmap_sel_add_map(struct mp_chmap_sel *s, const struct mp_chmap *map)
+{
+ if (!mp_chmap_is_valid(map))
+ return;
+ if (!s->chmaps)
+ s->chmaps = s->chmaps_storage;
+ if (s->num_chmaps == MP_ARRAY_SIZE(s->chmaps_storage)) {
+ if (!s->tmp)
+ return;
+ s->chmaps = talloc_memdup(s->tmp, s->chmaps, sizeof(s->chmaps_storage));
+ }
+ if (s->chmaps != s->chmaps_storage)
+ MP_TARRAY_GROW(s->tmp, s->chmaps, s->num_chmaps);
+ s->chmaps[s->num_chmaps++] = *map;
+}
+
+// Allow all waveext formats in default order.
+void mp_chmap_sel_add_waveext_def(struct mp_chmap_sel *s)
+{
+ for (int n = 1; n <= MP_NUM_CHANNELS; n++) {
+ struct mp_chmap map;
+ mp_chmap_from_channels(&map, n);
+ mp_chmap_sel_add_map(s, &map);
+ }
+}
+
+// Whitelist a speaker (MP_SPEAKER_ID_...). All layouts that contain whitelisted
+// speakers are allowed.
+void mp_chmap_sel_add_speaker(struct mp_chmap_sel *s, int id)
+{
+ assert(id >= 0 && id < MP_SPEAKER_ID_COUNT);
+ s->speakers[id] = true;
+}
+
+static bool test_speakers(const struct mp_chmap_sel *s, struct mp_chmap *map)
+{
+ for (int n = 0; n < map->num; n++) {
+ if (!s->speakers[map->speaker[n]])
+ return false;
+ }
+ return true;
+}
+
+static bool test_maps(const struct mp_chmap_sel *s, struct mp_chmap *map)
+{
+ for (int n = 0; n < s->num_chmaps; n++) {
+ if (mp_chmap_equals_reordered(&s->chmaps[n], map)) {
+ *map = s->chmaps[n];
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool test_waveext(const struct mp_chmap_sel *s, struct mp_chmap *map)
+{
+ if (s->allow_waveext) {
+ struct mp_chmap t = *map;
+ mp_chmap_reorder_to_waveext(&t);
+ if (mp_chmap_is_waveext(&t)) {
+ *map = t;
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool test_layout(const struct mp_chmap_sel *s, struct mp_chmap *map)
+{
+ if (!mp_chmap_is_valid(map))
+ return false;
+
+ return s->allow_any || test_waveext(s, map) || test_speakers(s, map) ||
+ test_maps(s, map);
+}
+
+// Determine which channel map to use given a source channel map, and various
+// parameters restricting possible choices. If the map doesn't match, select
+// a fallback and set it.
+// If no matching layout is found, a reordered layout may be returned.
+// If that is not possible, a fallback for up/downmixing may be returned.
+// If no choice is possible, set *map to empty.
+bool mp_chmap_sel_adjust(const struct mp_chmap_sel *s, struct mp_chmap *map)
+{
+ if (test_layout(s, map))
+ return true;
+ if (mp_chmap_is_unknown(map)) {
+ struct mp_chmap t = {0};
+ if (mp_chmap_sel_get_def(s, &t, map->num) && test_layout(s, &t)) {
+ *map = t;
+ return true;
+ }
+ }
+
+ if (mp_chmap_sel_fallback(s, map))
+ return true;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(speaker_replacements); i++) {
+ struct mp_chmap t = *map;
+ struct mp_chmap *r = (struct mp_chmap *)speaker_replacements[i];
+ if (replace_speakers(&t, r) && test_layout(s, &t)) {
+ *map = t;
+ return true;
+ }
+ }
+
+ // Fallback to mono/stereo as last resort
+ *map = (struct mp_chmap) MP_CHMAP_INIT_STEREO;
+ if (test_layout(s, map))
+ return true;
+ *map = (struct mp_chmap) MP_CHMAP_INIT_MONO;
+ if (test_layout(s, map))
+ return true;
+ *map = (struct mp_chmap) {0};
+ return false;
+}
+
+// Like mp_chmap_diffn(), but find the minimum difference with all possible
+// speaker replacements considered.
+static int mp_chmap_diffn_r(const struct mp_chmap *a, const struct mp_chmap *b)
+{
+ int mindiff = INT_MAX;
+
+ for (int i = -1; i < (int)MP_ARRAY_SIZE(speaker_replacements); i++) {
+ struct mp_chmap ar = *a;
+ if (i >= 0) {
+ struct mp_chmap *r = (struct mp_chmap *)speaker_replacements[i];
+ if (!replace_speakers(&ar, r))
+ continue;
+ }
+ int d = mp_chmap_diffn(&ar, b);
+ if (d < mindiff)
+ mindiff = d;
+ }
+
+ // Special-case: we consider stereo a replacement for mono. (This is not
+ // true in the other direction. Also, fl-fr is generally not a replacement
+ // for fc. Thus it's not part of the speaker replacement list.)
+ struct mp_chmap mono = MP_CHMAP_INIT_MONO;
+ struct mp_chmap stereo = MP_CHMAP_INIT_STEREO;
+ if (mp_chmap_equals(&mono, b) && mp_chmap_equals(&stereo, a))
+ mindiff = 0;
+
+ return mindiff;
+}
+
+// Decide whether we should prefer old or new for the requested layout.
+// Return true if new should be used, false if old should be used.
+// If old is empty, always return new (initial case).
+static bool mp_chmap_is_better(struct mp_chmap *req, struct mp_chmap *old,
+ struct mp_chmap *new)
+{
+ // Initial case
+ if (!old->num)
+ return true;
+
+ // Exact pick - this also ensures that the best layout is chosen if the
+ // layouts are the same, but with different order of channels.
+ if (mp_chmap_equals(req, old))
+ return false;
+ if (mp_chmap_equals(req, new))
+ return true;
+
+ // If there's no exact match, strictly do a preferred conversion.
+ bool old_pref = test_preferred_remix(req, old);
+ bool new_pref = test_preferred_remix(req, new);
+ if (old_pref && !new_pref)
+ return false;
+ if (!old_pref && new_pref)
+ return true;
+
+ int old_lost_r = mp_chmap_diffn_r(req, old); // num. channels only in req
+ int new_lost_r = mp_chmap_diffn_r(req, new);
+
+ // Imperfect upmix (no real superset) - minimize lost channels
+ if (new_lost_r != old_lost_r)
+ return new_lost_r < old_lost_r;
+
+ struct mp_chmap old_p = *old, new_p = *new;
+ mp_chmap_remove_na(&old_p);
+ mp_chmap_remove_na(&new_p);
+
+ // If the situation is equal with replaced speakers, but the replacement is
+ // perfect for only one of them, let the better one win. This prefers
+ // inexact equivalents over exact supersets.
+ bool perfect_r_new = !new_lost_r && new_p.num <= old_p.num;
+ bool perfect_r_old = !old_lost_r && old_p.num <= new_p.num;
+ if (perfect_r_new != perfect_r_old)
+ return perfect_r_new;
+
+ int old_lost = mp_chmap_diffn(req, old);
+ int new_lost = mp_chmap_diffn(req, new);
+ // If the situation is equal with replaced speakers, pick the better one,
+ // even if it means an upmix.
+ if (new_lost != old_lost)
+ return new_lost < old_lost;
+
+ // Some kind of upmix. If it's perfect, prefer the smaller one. Even if not,
+ // both have equal loss, so also prefer the smaller one.
+ // Drop padding channels (NA) for the sake of this check, as the number of
+ // padding channels isn't really meaningful.
+ if (new_p.num != old_p.num)
+ return new_p.num < old_p.num;
+
+ // Again, with physical channels (minimizes number of NA channels).
+ return new->num < old->num;
+}
+
+// Determine which channel map to fallback to given a source channel map.
+bool mp_chmap_sel_fallback(const struct mp_chmap_sel *s, struct mp_chmap *map)
+{
+ struct mp_chmap best = {0};
+
+ for (int n = 0; n < s->num_chmaps; n++) {
+ struct mp_chmap e = s->chmaps[n];
+
+ if (mp_chmap_is_unknown(&e))
+ continue;
+
+ if (mp_chmap_is_better(map, &best, &e))
+ best = e;
+ }
+
+ if (best.num) {
+ *map = best;
+ return true;
+ }
+
+ return false;
+}
+
+// Set map to a default layout with num channels. Used for audio APIs that
+// return a channel count as part of format negotiation, but give no
+// information about the channel layout.
+// If the channel count is correct, do nothing and leave *map untouched.
+bool mp_chmap_sel_get_def(const struct mp_chmap_sel *s, struct mp_chmap *map,
+ int num)
+{
+ if (map->num != num) {
+ *map = (struct mp_chmap) {0};
+ // Set of speakers or waveext might allow it.
+ struct mp_chmap t;
+ mp_chmap_from_channels(&t, num);
+ mp_chmap_reorder_to_waveext(&t);
+ if (test_layout(s, &t)) {
+ *map = t;
+ } else {
+ for (int n = 0; n < s->num_chmaps; n++) {
+ if (s->chmaps[n].num == num) {
+ *map = s->chmaps[n];
+ break;
+ }
+ }
+ }
+ }
+ return map->num > 0;
+}
+
+// Print the set of allowed channel layouts.
+void mp_chmal_sel_log(const struct mp_chmap_sel *s, struct mp_log *log, int lev)
+{
+ if (!mp_msg_test(log, lev))
+ return;
+
+ for (int i = 0; i < s->num_chmaps; i++)
+ mp_msg(log, lev, " - %s\n", mp_chmap_to_str(&s->chmaps[i]));
+ for (int i = 0; i < MP_SPEAKER_ID_COUNT; i++) {
+ if (!s->speakers[i])
+ continue;
+ struct mp_chmap l = {.num = 1, .speaker = { i }};
+ mp_msg(log, lev, " - #%s\n",
+ i == MP_SPEAKER_ID_FC ? "fc" : mp_chmap_to_str_hr(&l));
+ }
+ if (s->allow_waveext)
+ mp_msg(log, lev, " - waveext\n");
+ if (s->allow_any)
+ mp_msg(log, lev, " - anything\n");
+}
+
+// Select a channel map from the given list that fits best to c. Don't change
+// *c if there's no match, or the list is empty.
+void mp_chmap_sel_list(struct mp_chmap *c, struct mp_chmap *maps, int num_maps)
+{
+ // This is a separate function to keep messing with mp_chmap_sel internals
+ // within this source file.
+ struct mp_chmap_sel sel = {
+ .chmaps = maps,
+ .num_chmaps = num_maps,
+ };
+ mp_chmap_sel_fallback(&sel, c);
+}
diff --git a/audio/chmap_sel.h b/audio/chmap_sel.h
new file mode 100644
index 0000000..4b11557
--- /dev/null
+++ b/audio/chmap_sel.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_CHMAP_SEL_H
+#define MP_CHMAP_SEL_H
+
+#include <stdbool.h>
+
+#include "chmap.h"
+
+struct mp_chmap_sel {
+ // should be considered opaque
+ bool allow_any, allow_waveext;
+ bool speakers[MP_SPEAKER_ID_COUNT];
+ struct mp_chmap *chmaps;
+ int num_chmaps;
+
+ struct mp_chmap chmaps_storage[20];
+
+ void *tmp; // set to any talloc context to allow more chmaps entries
+};
+
+void mp_chmap_sel_add_any(struct mp_chmap_sel *s);
+void mp_chmap_sel_add_waveext(struct mp_chmap_sel *s);
+void mp_chmap_sel_add_waveext_def(struct mp_chmap_sel *s);
+void mp_chmap_sel_add_map(struct mp_chmap_sel *s, const struct mp_chmap *map);
+void mp_chmap_sel_add_speaker(struct mp_chmap_sel *s, int id);
+bool mp_chmap_sel_adjust(const struct mp_chmap_sel *s, struct mp_chmap *map);
+bool mp_chmap_sel_fallback(const struct mp_chmap_sel *s, struct mp_chmap *map);
+bool mp_chmap_sel_get_def(const struct mp_chmap_sel *s, struct mp_chmap *map,
+ int num);
+
+struct mp_log;
+void mp_chmal_sel_log(const struct mp_chmap_sel *s, struct mp_log *log, int lev);
+
+void mp_chmap_sel_list(struct mp_chmap *c, struct mp_chmap *maps, int num_maps);
+
+#endif
diff --git a/audio/decode/ad_lavc.c b/audio/decode/ad_lavc.c
new file mode 100644
index 0000000..08b789a
--- /dev/null
+++ b/audio/decode/ad_lavc.c
@@ -0,0 +1,325 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/opt.h>
+#include <libavutil/common.h>
+#include <libavutil/intreadwrite.h>
+
+#include "config.h"
+
+#include "mpv_talloc.h"
+#include "audio/aframe.h"
+#include "audio/chmap_avchannel.h"
+#include "audio/fmt-conversion.h"
+#include "common/av_common.h"
+#include "common/codecs.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "demux/packet.h"
+#include "demux/stheader.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+struct priv {
+ AVCodecContext *avctx;
+ AVFrame *avframe;
+ AVPacket *avpkt;
+ struct mp_chmap force_channel_map;
+ uint32_t skip_samples, trim_samples;
+ bool preroll_done;
+ double next_pts;
+ AVRational codec_timebase;
+ struct lavc_state state;
+
+ struct mp_decoder public;
+};
+
+#define OPT_BASE_STRUCT struct ad_lavc_params
+struct ad_lavc_params {
+ float ac3drc;
+ bool downmix;
+ int threads;
+ char **avopts;
+};
+
+const struct m_sub_options ad_lavc_conf = {
+ .opts = (const m_option_t[]) {
+ {"ac3drc", OPT_FLOAT(ac3drc), M_RANGE(0, 6)},
+ {"downmix", OPT_BOOL(downmix)},
+ {"threads", OPT_INT(threads), M_RANGE(0, 16)},
+ {"o", OPT_KEYVALUELIST(avopts)},
+ {0}
+ },
+ .size = sizeof(struct ad_lavc_params),
+ .defaults = &(const struct ad_lavc_params){
+ .ac3drc = 0,
+ .threads = 1,
+ },
+};
+
+static bool init(struct mp_filter *da, struct mp_codec_params *codec,
+ const char *decoder)
+{
+ struct priv *ctx = da->priv;
+ struct MPOpts *mpopts = mp_get_config_group(ctx, da->global, &mp_opt_root);
+ struct ad_lavc_params *opts =
+ mp_get_config_group(ctx, da->global, &ad_lavc_conf);
+ AVCodecContext *lavc_context;
+ const AVCodec *lavc_codec;
+
+ ctx->codec_timebase = mp_get_codec_timebase(codec);
+
+ if (codec->force_channels)
+ ctx->force_channel_map = codec->channels;
+
+ lavc_codec = avcodec_find_decoder_by_name(decoder);
+ if (!lavc_codec) {
+ MP_ERR(da, "Cannot find codec '%s' in libavcodec...\n", decoder);
+ return false;
+ }
+
+ lavc_context = avcodec_alloc_context3(lavc_codec);
+ ctx->avctx = lavc_context;
+ ctx->avframe = av_frame_alloc();
+ ctx->avpkt = av_packet_alloc();
+ MP_HANDLE_OOM(ctx->avctx && ctx->avframe && ctx->avpkt);
+ lavc_context->codec_type = AVMEDIA_TYPE_AUDIO;
+ lavc_context->codec_id = lavc_codec->id;
+ lavc_context->pkt_timebase = ctx->codec_timebase;
+
+ if (opts->downmix && mpopts->audio_output_channels.num_chmaps == 1) {
+ const struct mp_chmap *requested_layout =
+ &mpopts->audio_output_channels.chmaps[0];
+#if !HAVE_AV_CHANNEL_LAYOUT
+ lavc_context->request_channel_layout =
+ mp_chmap_to_lavc(requested_layout);
+#else
+ AVChannelLayout av_layout = { 0 };
+ mp_chmap_to_av_layout(&av_layout, requested_layout);
+
+ // Always try to set requested output layout - currently only something
+ // supported by AC3, MLP/TrueHD, DTS and the fdk-aac wrapper.
+ av_opt_set_chlayout(lavc_context, "downmix", &av_layout,
+ AV_OPT_SEARCH_CHILDREN);
+
+ av_channel_layout_uninit(&av_layout);
+#endif
+ }
+
+ // Always try to set - option only exists for AC3 at the moment
+ av_opt_set_double(lavc_context, "drc_scale", opts->ac3drc,
+ AV_OPT_SEARCH_CHILDREN);
+
+ // Let decoder add AV_FRAME_DATA_SKIP_SAMPLES.
+ av_opt_set(lavc_context, "flags2", "+skip_manual", AV_OPT_SEARCH_CHILDREN);
+
+ mp_set_avopts(da->log, lavc_context, opts->avopts);
+
+ if (mp_set_avctx_codec_headers(lavc_context, codec) < 0) {
+ MP_ERR(da, "Could not set decoder parameters.\n");
+ return false;
+ }
+
+ mp_set_avcodec_threads(da->log, lavc_context, opts->threads);
+
+ /* open it */
+ if (avcodec_open2(lavc_context, lavc_codec, NULL) < 0) {
+ MP_ERR(da, "Could not open codec.\n");
+ return false;
+ }
+
+ ctx->next_pts = MP_NOPTS_VALUE;
+
+ return true;
+}
+
+static void destroy(struct mp_filter *da)
+{
+ struct priv *ctx = da->priv;
+
+ avcodec_free_context(&ctx->avctx);
+ av_frame_free(&ctx->avframe);
+ mp_free_av_packet(&ctx->avpkt);
+}
+
+static void reset(struct mp_filter *da)
+{
+ struct priv *ctx = da->priv;
+
+ avcodec_flush_buffers(ctx->avctx);
+ ctx->skip_samples = 0;
+ ctx->trim_samples = 0;
+ ctx->preroll_done = false;
+ ctx->next_pts = MP_NOPTS_VALUE;
+ ctx->state = (struct lavc_state){0};
+}
+
+static int send_packet(struct mp_filter *da, struct demux_packet *mpkt)
+{
+ struct priv *priv = da->priv;
+ AVCodecContext *avctx = priv->avctx;
+
+ // If the decoder discards the timestamp for some reason, we use the
+ // interpolated PTS. Initialize it so that it works for the initial
+ // packet as well.
+ if (mpkt && priv->next_pts == MP_NOPTS_VALUE)
+ priv->next_pts = mpkt->pts;
+
+ mp_set_av_packet(priv->avpkt, mpkt, &priv->codec_timebase);
+
+ int ret = avcodec_send_packet(avctx, mpkt ? priv->avpkt : NULL);
+ if (ret < 0)
+ MP_ERR(da, "Error decoding audio.\n");
+ return ret;
+}
+
+static int receive_frame(struct mp_filter *da, struct mp_frame *out)
+{
+ struct priv *priv = da->priv;
+ AVCodecContext *avctx = priv->avctx;
+
+ int ret = avcodec_receive_frame(avctx, priv->avframe);
+
+ if (ret == AVERROR_EOF) {
+ // If flushing was initialized earlier and has ended now, make it start
+ // over in case we get new packets at some point in the future.
+ // (Dont' reset the filter itself, we want to keep other state.)
+ avcodec_flush_buffers(priv->avctx);
+ return ret;
+ } else if (ret < 0 && ret != AVERROR(EAGAIN)) {
+ MP_ERR(da, "Error decoding audio.\n");
+ }
+
+ if (priv->avframe->flags & AV_FRAME_FLAG_DISCARD)
+ av_frame_unref(priv->avframe);
+
+ if (!priv->avframe->buf[0])
+ return ret;
+
+ double out_pts = mp_pts_from_av(priv->avframe->pts, &priv->codec_timebase);
+
+ struct mp_aframe *mpframe = mp_aframe_from_avframe(priv->avframe);
+ if (!mpframe) {
+ MP_ERR(da, "Converting libavcodec frame to mpv frame failed.\n");
+ return ret;
+ }
+
+ if (priv->force_channel_map.num)
+ mp_aframe_set_chmap(mpframe, &priv->force_channel_map);
+
+ if (out_pts == MP_NOPTS_VALUE)
+ out_pts = priv->next_pts;
+ mp_aframe_set_pts(mpframe, out_pts);
+
+ priv->next_pts = mp_aframe_end_pts(mpframe);
+
+ AVFrameSideData *sd =
+ av_frame_get_side_data(priv->avframe, AV_FRAME_DATA_SKIP_SAMPLES);
+ if (sd && sd->size >= 10) {
+ char *d = sd->data;
+ priv->skip_samples += AV_RL32(d + 0);
+ priv->trim_samples += AV_RL32(d + 4);
+ }
+
+ if (!priv->preroll_done) {
+ // Skip only if this isn't already handled by AV_FRAME_DATA_SKIP_SAMPLES.
+ if (!priv->skip_samples)
+ priv->skip_samples = avctx->delay;
+ priv->preroll_done = true;
+ }
+
+ uint32_t skip = MPMIN(priv->skip_samples, mp_aframe_get_size(mpframe));
+ if (skip) {
+ mp_aframe_skip_samples(mpframe, skip);
+ priv->skip_samples -= skip;
+ }
+ uint32_t trim = MPMIN(priv->trim_samples, mp_aframe_get_size(mpframe));
+ if (trim) {
+ mp_aframe_set_size(mpframe, mp_aframe_get_size(mpframe) - trim);
+ priv->trim_samples -= trim;
+ }
+
+ // Strip possibly bogus float values like Infinity, NaN, denormalized
+ mp_aframe_sanitize_float(mpframe);
+
+ if (mp_aframe_get_size(mpframe) > 0) {
+ *out = MAKE_FRAME(MP_FRAME_AUDIO, mpframe);
+ } else {
+ talloc_free(mpframe);
+ }
+
+ av_frame_unref(priv->avframe);
+
+ return ret;
+}
+
+static void process(struct mp_filter *ad)
+{
+ struct priv *priv = ad->priv;
+
+ lavc_process(ad, &priv->state, send_packet, receive_frame);
+}
+
+static const struct mp_filter_info ad_lavc_filter = {
+ .name = "ad_lavc",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static struct mp_decoder *create(struct mp_filter *parent,
+ struct mp_codec_params *codec,
+ const char *decoder)
+{
+ struct mp_filter *da = mp_filter_create(parent, &ad_lavc_filter);
+ if (!da)
+ return NULL;
+
+ mp_filter_add_pin(da, MP_PIN_IN, "in");
+ mp_filter_add_pin(da, MP_PIN_OUT, "out");
+
+ da->log = mp_log_new(da, parent->log, NULL);
+
+ struct priv *priv = da->priv;
+ priv->public.f = da;
+
+ if (!init(da, codec, decoder)) {
+ talloc_free(da);
+ return NULL;
+ }
+ return &priv->public;
+}
+
+static void add_decoders(struct mp_decoder_list *list)
+{
+ mp_add_lavc_decoders(list, AVMEDIA_TYPE_AUDIO);
+}
+
+const struct mp_decoder_fns ad_lavc = {
+ .create = create,
+ .add_decoders = add_decoders,
+};
diff --git a/audio/decode/ad_spdif.c b/audio/decode/ad_spdif.c
new file mode 100644
index 0000000..393af8a
--- /dev/null
+++ b/audio/decode/ad_spdif.c
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2012 Naoya OYAMA
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <assert.h>
+
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/opt.h>
+
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "common/av_common.h"
+#include "common/codecs.h"
+#include "common/msg.h"
+#include "demux/packet.h"
+#include "demux/stheader.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+#include "options/options.h"
+
+#define OUTBUF_SIZE 65536
+
+struct spdifContext {
+ struct mp_log *log;
+ enum AVCodecID codec_id;
+ AVFormatContext *lavf_ctx;
+ AVPacket *avpkt;
+ int out_buffer_len;
+ uint8_t out_buffer[OUTBUF_SIZE];
+ bool need_close;
+ bool use_dts_hd;
+ struct mp_aframe *fmt;
+ int sstride;
+ struct mp_aframe_pool *pool;
+
+ struct mp_decoder public;
+};
+
+static int write_packet(void *p, uint8_t *buf, int buf_size)
+{
+ struct spdifContext *ctx = p;
+
+ int buffer_left = OUTBUF_SIZE - ctx->out_buffer_len;
+ if (buf_size > buffer_left) {
+ MP_ERR(ctx, "spdif packet too large.\n");
+ buf_size = buffer_left;
+ }
+
+ memcpy(&ctx->out_buffer[ctx->out_buffer_len], buf, buf_size);
+ ctx->out_buffer_len += buf_size;
+ return buf_size;
+}
+
+// (called on both filter destruction _and_ if lavf fails to init)
+static void destroy(struct mp_filter *da)
+{
+ struct spdifContext *spdif_ctx = da->priv;
+ AVFormatContext *lavf_ctx = spdif_ctx->lavf_ctx;
+
+ if (lavf_ctx) {
+ if (spdif_ctx->need_close)
+ av_write_trailer(lavf_ctx);
+ if (lavf_ctx->pb)
+ av_freep(&lavf_ctx->pb->buffer);
+ av_freep(&lavf_ctx->pb);
+ avformat_free_context(lavf_ctx);
+ spdif_ctx->lavf_ctx = NULL;
+ }
+ mp_free_av_packet(&spdif_ctx->avpkt);
+}
+
+static void determine_codec_params(struct mp_filter *da, AVPacket *pkt,
+ int *out_profile, int *out_rate)
+{
+ struct spdifContext *spdif_ctx = da->priv;
+ int profile = FF_PROFILE_UNKNOWN;
+ AVCodecContext *ctx = NULL;
+ AVFrame *frame = NULL;
+
+ AVCodecParserContext *parser = av_parser_init(spdif_ctx->codec_id);
+ if (parser) {
+ // Don't make it wait for the next frame.
+ parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
+
+ ctx = avcodec_alloc_context3(NULL);
+ if (!ctx) {
+ av_parser_close(parser);
+ goto done;
+ }
+
+ uint8_t *d = NULL;
+ int s = 0;
+ av_parser_parse2(parser, ctx, &d, &s, pkt->data, pkt->size, 0, 0, 0);
+ *out_profile = profile = ctx->profile;
+ *out_rate = ctx->sample_rate;
+
+ avcodec_free_context(&ctx);
+ av_parser_close(parser);
+ }
+
+ if (profile != FF_PROFILE_UNKNOWN || spdif_ctx->codec_id != AV_CODEC_ID_DTS)
+ return;
+
+ const AVCodec *codec = avcodec_find_decoder(spdif_ctx->codec_id);
+ if (!codec)
+ goto done;
+
+ frame = av_frame_alloc();
+ if (!frame)
+ goto done;
+
+ ctx = avcodec_alloc_context3(codec);
+ if (!ctx)
+ goto done;
+
+ if (avcodec_open2(ctx, codec, NULL) < 0)
+ goto done;
+
+ if (avcodec_send_packet(ctx, pkt) < 0)
+ goto done;
+ if (avcodec_receive_frame(ctx, frame) < 0)
+ goto done;
+
+ *out_profile = profile = ctx->profile;
+ *out_rate = ctx->sample_rate;
+
+done:
+ av_frame_free(&frame);
+ avcodec_free_context(&ctx);
+
+ if (profile == FF_PROFILE_UNKNOWN)
+ MP_WARN(da, "Failed to parse codec profile.\n");
+}
+
+static int init_filter(struct mp_filter *da)
+{
+ struct spdifContext *spdif_ctx = da->priv;
+
+ AVPacket *pkt = spdif_ctx->avpkt;
+
+ int profile = FF_PROFILE_UNKNOWN;
+ int c_rate = 0;
+ determine_codec_params(da, pkt, &profile, &c_rate);
+ MP_VERBOSE(da, "In: profile=%d samplerate=%d\n", profile, c_rate);
+
+ AVFormatContext *lavf_ctx = avformat_alloc_context();
+ if (!lavf_ctx)
+ goto fail;
+
+ spdif_ctx->lavf_ctx = lavf_ctx;
+
+ lavf_ctx->oformat = av_guess_format("spdif", NULL, NULL);
+ if (!lavf_ctx->oformat)
+ goto fail;
+
+ void *buffer = av_mallocz(OUTBUF_SIZE);
+ MP_HANDLE_OOM(buffer);
+ lavf_ctx->pb = avio_alloc_context(buffer, OUTBUF_SIZE, 1, spdif_ctx, NULL,
+ write_packet, NULL);
+ if (!lavf_ctx->pb) {
+ av_free(buffer);
+ goto fail;
+ }
+
+ // Request minimal buffering
+ lavf_ctx->pb->direct = 1;
+
+ AVStream *stream = avformat_new_stream(lavf_ctx, 0);
+ if (!stream)
+ goto fail;
+
+ stream->codecpar->codec_id = spdif_ctx->codec_id;
+
+ AVDictionary *format_opts = NULL;
+
+ spdif_ctx->fmt = mp_aframe_create();
+ talloc_steal(spdif_ctx, spdif_ctx->fmt);
+
+ int num_channels = 0;
+ int sample_format = 0;
+ int samplerate = 0;
+ switch (spdif_ctx->codec_id) {
+ case AV_CODEC_ID_AAC:
+ sample_format = AF_FORMAT_S_AAC;
+ samplerate = 48000;
+ num_channels = 2;
+ break;
+ case AV_CODEC_ID_AC3:
+ sample_format = AF_FORMAT_S_AC3;
+ samplerate = c_rate > 0 ? c_rate : 48000;
+ num_channels = 2;
+ break;
+ case AV_CODEC_ID_DTS: {
+ bool is_hd = profile == FF_PROFILE_DTS_HD_HRA ||
+ profile == FF_PROFILE_DTS_HD_MA ||
+ profile == FF_PROFILE_UNKNOWN;
+
+ // Apparently, DTS-HD over SPDIF is specified to be 7.1 (8 channels)
+ // for DTS-HD MA, and stereo (2 channels) for DTS-HD HRA. The bit
+ // streaming rate as well as the signaled channel count are defined
+ // based on this value.
+ int dts_hd_spdif_channel_count = profile == FF_PROFILE_DTS_HD_HRA ?
+ 2 : 8;
+ if (spdif_ctx->use_dts_hd && is_hd) {
+ av_dict_set_int(&format_opts, "dtshd_rate",
+ dts_hd_spdif_channel_count * 96000, 0);
+ sample_format = AF_FORMAT_S_DTSHD;
+ samplerate = 192000;
+ num_channels = dts_hd_spdif_channel_count;
+ } else {
+ sample_format = AF_FORMAT_S_DTS;
+ samplerate = 48000;
+ num_channels = 2;
+ }
+ break;
+ }
+ case AV_CODEC_ID_EAC3:
+ sample_format = AF_FORMAT_S_EAC3;
+ samplerate = 192000;
+ num_channels = 2;
+ break;
+ case AV_CODEC_ID_MP3:
+ sample_format = AF_FORMAT_S_MP3;
+ samplerate = 48000;
+ num_channels = 2;
+ break;
+ case AV_CODEC_ID_TRUEHD:
+ sample_format = AF_FORMAT_S_TRUEHD;
+ samplerate = 192000;
+ num_channels = 8;
+ break;
+ default:
+ abort();
+ }
+
+ struct mp_chmap chmap;
+ mp_chmap_from_channels(&chmap, num_channels);
+ mp_aframe_set_chmap(spdif_ctx->fmt, &chmap);
+ mp_aframe_set_format(spdif_ctx->fmt, sample_format);
+ mp_aframe_set_rate(spdif_ctx->fmt, samplerate);
+
+ spdif_ctx->sstride = mp_aframe_get_sstride(spdif_ctx->fmt);
+
+ if (avformat_write_header(lavf_ctx, &format_opts) < 0) {
+ MP_FATAL(da, "libavformat spdif initialization failed.\n");
+ av_dict_free(&format_opts);
+ goto fail;
+ }
+ av_dict_free(&format_opts);
+
+ spdif_ctx->need_close = true;
+
+ return 0;
+
+fail:
+ destroy(da);
+ mp_filter_internal_mark_failed(da);
+ return -1;
+}
+
+static void process(struct mp_filter *da)
+{
+ struct spdifContext *spdif_ctx = da->priv;
+
+ if (!mp_pin_can_transfer_data(da->ppins[1], da->ppins[0]))
+ return;
+
+ struct mp_frame inframe = mp_pin_out_read(da->ppins[0]);
+ if (inframe.type == MP_FRAME_EOF) {
+ mp_pin_in_write(da->ppins[1], inframe);
+ return;
+ } else if (inframe.type != MP_FRAME_PACKET) {
+ if (inframe.type) {
+ MP_ERR(da, "unknown frame type\n");
+ mp_filter_internal_mark_failed(da);
+ }
+ return;
+ }
+
+ struct demux_packet *mpkt = inframe.data;
+ struct mp_aframe *out = NULL;
+ double pts = mpkt->pts;
+
+ if (!spdif_ctx->avpkt) {
+ spdif_ctx->avpkt = av_packet_alloc();
+ MP_HANDLE_OOM(spdif_ctx->avpkt);
+ }
+ mp_set_av_packet(spdif_ctx->avpkt, mpkt, NULL);
+ spdif_ctx->avpkt->pts = spdif_ctx->avpkt->dts = 0;
+ if (!spdif_ctx->lavf_ctx) {
+ if (init_filter(da) < 0)
+ goto done;
+ assert(spdif_ctx->avpkt);
+ }
+
+ spdif_ctx->out_buffer_len = 0;
+ int ret = av_write_frame(spdif_ctx->lavf_ctx, spdif_ctx->avpkt);
+ avio_flush(spdif_ctx->lavf_ctx->pb);
+ if (ret < 0) {
+ MP_ERR(da, "spdif mux error: '%s'\n", mp_strerror(AVUNERROR(ret)));
+ goto done;
+ }
+
+ out = mp_aframe_new_ref(spdif_ctx->fmt);
+ int samples = spdif_ctx->out_buffer_len / spdif_ctx->sstride;
+ if (mp_aframe_pool_allocate(spdif_ctx->pool, out, samples) < 0) {
+ TA_FREEP(&out);
+ goto done;
+ }
+
+ uint8_t **data = mp_aframe_get_data_rw(out);
+ if (!data) {
+ TA_FREEP(&out);
+ goto done;
+ }
+
+ memcpy(data[0], spdif_ctx->out_buffer, spdif_ctx->out_buffer_len);
+ mp_aframe_set_pts(out, pts);
+
+done:
+ talloc_free(mpkt);
+ if (out) {
+ mp_pin_in_write(da->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out));
+ } else {
+ mp_filter_internal_mark_failed(da);
+ }
+}
+
+static const int codecs[] = {
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_MP3,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_NONE
+};
+
+static bool find_codec(const char *name)
+{
+ for (int n = 0; codecs[n] != AV_CODEC_ID_NONE; n++) {
+ const char *format = mp_codec_from_av_codec_id(codecs[n]);
+ if (format && name && strcmp(format, name) == 0)
+ return true;
+ }
+ return false;
+}
+
+// codec is the libavcodec name of the source audio codec.
+// pref is a ","-separated list of names, some of them which do not match with
+// libavcodec names (like dts-hd).
+struct mp_decoder_list *select_spdif_codec(const char *codec, const char *pref)
+{
+ struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list);
+
+ if (!find_codec(codec))
+ return list;
+
+ bool spdif_allowed = false, dts_hd_allowed = false;
+ bstr sel = bstr0(pref);
+ while (sel.len) {
+ bstr decoder;
+ bstr_split_tok(sel, ",", &decoder, &sel);
+ if (decoder.len) {
+ if (bstr_equals0(decoder, codec))
+ spdif_allowed = true;
+ if (bstr_equals0(decoder, "dts-hd") && strcmp(codec, "dts") == 0)
+ spdif_allowed = dts_hd_allowed = true;
+ }
+ }
+
+ if (!spdif_allowed)
+ return list;
+
+ const char *suffix_name = dts_hd_allowed ? "dts_hd" : codec;
+ char name[80];
+ snprintf(name, sizeof(name), "spdif_%s", suffix_name);
+ mp_add_decoder(list, codec, name,
+ "libavformat/spdifenc audio pass-through decoder");
+ return list;
+}
+
+static const struct mp_filter_info ad_spdif_filter = {
+ .name = "ad_spdif",
+ .priv_size = sizeof(struct spdifContext),
+ .process = process,
+ .destroy = destroy,
+};
+
+static struct mp_decoder *create(struct mp_filter *parent,
+ struct mp_codec_params *codec,
+ const char *decoder)
+{
+ struct mp_filter *da = mp_filter_create(parent, &ad_spdif_filter);
+ if (!da)
+ return NULL;
+
+ mp_filter_add_pin(da, MP_PIN_IN, "in");
+ mp_filter_add_pin(da, MP_PIN_OUT, "out");
+
+ da->log = mp_log_new(da, parent->log, NULL);
+
+ struct spdifContext *spdif_ctx = da->priv;
+ spdif_ctx->log = da->log;
+ spdif_ctx->pool = mp_aframe_pool_create(spdif_ctx);
+ spdif_ctx->public.f = da;
+
+ if (strcmp(decoder, "spdif_dts_hd") == 0)
+ spdif_ctx->use_dts_hd = true;
+
+ spdif_ctx->codec_id = mp_codec_to_av_codec_id(codec->codec);
+
+
+ if (spdif_ctx->codec_id == AV_CODEC_ID_NONE) {
+ talloc_free(da);
+ return NULL;
+ }
+
+ return &spdif_ctx->public;
+}
+
+const struct mp_decoder_fns ad_spdif = {
+ .create = create,
+};
diff --git a/audio/filter/af_drop.c b/audio/filter/af_drop.c
new file mode 100644
index 0000000..724c482
--- /dev/null
+++ b/audio/filter/af_drop.c
@@ -0,0 +1,114 @@
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+
+struct priv {
+ double speed;
+ double diff; // amount of too many additional samples in normal speed
+ struct mp_aframe *last; // for repeating
+};
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ struct mp_frame frame = {0};
+
+ double last_dur = p->last ? mp_aframe_duration(p->last) : 0;
+ if (p->last && p->diff < 0 && -p->diff > last_dur / 2) {
+ MP_VERBOSE(f, "repeat\n");
+ frame = MAKE_FRAME(MP_FRAME_AUDIO, p->last);
+ p->last = NULL;
+ } else {
+ frame = mp_pin_out_read(f->ppins[0]);
+
+ if (frame.type == MP_FRAME_AUDIO) {
+ last_dur = mp_aframe_duration(frame.data);
+ p->diff -= last_dur;
+ if (p->diff > last_dur / 2) {
+ MP_VERBOSE(f, "drop\n");
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_progress(f);
+ }
+ }
+ }
+
+ if (frame.type == MP_FRAME_AUDIO) {
+ struct mp_aframe *fr = frame.data;
+ talloc_free(p->last);
+ p->last = mp_aframe_new_ref(fr);
+ mp_aframe_mul_speed(fr, p->speed);
+ p->diff += mp_aframe_duration(fr);
+ mp_aframe_set_pts(p->last, mp_aframe_end_pts(fr));
+ } else if (frame.type == MP_FRAME_EOF) {
+ TA_FREEP(&p->last);
+ }
+ mp_pin_in_write(f->ppins[1], frame);
+}
+
+static bool command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *p = f->priv;
+
+ switch (cmd->type) {
+ case MP_FILTER_COMMAND_SET_SPEED:
+ p->speed = cmd->speed;
+ return true;
+ }
+
+ return false;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ TA_FREEP(&p->last);
+ p->diff = 0;
+}
+
+static void destroy(struct mp_filter *f)
+{
+ reset(f);
+}
+
+static const struct mp_filter_info af_drop_filter = {
+ .name = "drop",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .command = command,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static struct mp_filter *af_drop_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &af_drop_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->speed = 1.0;
+
+ return f;
+}
+
+const struct mp_user_filter_entry af_drop = {
+ .desc = {
+ .description = "Change audio speed by dropping/repeating frames",
+ .name = "drop",
+ .priv_size = sizeof(struct priv),
+ },
+ .create = af_drop_create,
+};
diff --git a/audio/filter/af_format.c b/audio/filter/af_format.c
new file mode 100644
index 0000000..2d1c1cc
--- /dev/null
+++ b/audio/filter/af_format.c
@@ -0,0 +1,143 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+
+struct f_opts {
+ int in_format;
+ int in_srate;
+ struct m_channels in_channels;
+ int out_format;
+ int out_srate;
+ struct m_channels out_channels;
+
+ bool fail;
+};
+
+struct priv {
+ struct f_opts *opts;
+ struct mp_pin *in_pin;
+};
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], p->in_pin))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(p->in_pin);
+
+ if (p->opts->fail) {
+ MP_ERR(f, "Failing on purpose.\n");
+ goto error;
+ }
+
+ if (frame.type == MP_FRAME_EOF) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+
+ if (frame.type != MP_FRAME_AUDIO) {
+ MP_ERR(f, "audio frame expected\n");
+ goto error;
+ }
+
+ struct mp_aframe *in = frame.data;
+
+ if (p->opts->out_channels.num_chmaps > 0) {
+ if (!mp_aframe_set_chmap(in, &p->opts->out_channels.chmaps[0])) {
+ MP_ERR(f, "could not force output channels\n");
+ goto error;
+ }
+ }
+
+ if (p->opts->out_srate)
+ mp_aframe_set_rate(in, p->opts->out_srate);
+
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+
+error:
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+}
+
+static const struct mp_filter_info af_format_filter = {
+ .name = "format",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+};
+
+static struct mp_filter *af_format_create(struct mp_filter *parent,
+ void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &af_format_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct mp_autoconvert *conv = mp_autoconvert_create(f);
+ if (!conv)
+ abort();
+
+ if (p->opts->in_format)
+ mp_autoconvert_add_afmt(conv, p->opts->in_format);
+ if (p->opts->in_srate)
+ mp_autoconvert_add_srate(conv, p->opts->in_srate);
+ if (p->opts->in_channels.num_chmaps > 0)
+ mp_autoconvert_add_chmap(conv, &p->opts->in_channels.chmaps[0]);
+
+ mp_pin_connect(conv->f->pins[0], f->ppins[0]);
+ p->in_pin = conv->f->pins[1];
+
+ return f;
+}
+
+#define OPT_BASE_STRUCT struct f_opts
+
+const struct mp_user_filter_entry af_format = {
+ .desc = {
+ .name = "format",
+ .description = "Force audio format",
+ .priv_size = sizeof(struct f_opts),
+ .options = (const struct m_option[]) {
+ {"format", OPT_AUDIOFORMAT(in_format)},
+ {"srate", OPT_INT(in_srate), M_RANGE(1000, 8*48000)},
+ {"channels", OPT_CHANNELS(in_channels),
+ .flags = M_OPT_CHANNELS_LIMITED},
+ {"out-srate", OPT_INT(out_srate), M_RANGE(1000, 8*48000)},
+ {"out-channels", OPT_CHANNELS(out_channels),
+ .flags = M_OPT_CHANNELS_LIMITED},
+ {"fail", OPT_BOOL(fail)},
+ {0}
+ },
+ },
+ .create = af_format_create,
+};
diff --git a/audio/filter/af_lavcac3enc.c b/audio/filter/af_lavcac3enc.c
new file mode 100644
index 0000000..b4a1d59
--- /dev/null
+++ b/audio/filter/af_lavcac3enc.c
@@ -0,0 +1,437 @@
+/*
+ * audio filter for runtime AC-3 encoding with libavcodec.
+ *
+ * Copyright (C) 2007 Ulion <ulion A gmail P com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/common.h>
+#include <libavutil/bswap.h>
+#include <libavutil/mem.h>
+
+#include "config.h"
+
+#include "audio/aframe.h"
+#include "audio/chmap_avchannel.h"
+#include "audio/chmap_sel.h"
+#include "audio/fmt-conversion.h"
+#include "audio/format.h"
+#include "common/av_common.h"
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/f_utils.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+
+
+#define AC3_MAX_CHANNELS 6
+#define AC3_MAX_CODED_FRAME_SIZE 3840
+#define AC3_FRAME_SIZE (6 * 256)
+const static uint16_t ac3_bitrate_tab[19] = {
+ 32, 40, 48, 56, 64, 80, 96, 112, 128,
+ 160, 192, 224, 256, 320, 384, 448, 512, 576, 640
+};
+
+struct f_opts {
+ bool add_iec61937_header;
+ int bit_rate;
+ int min_channel_num;
+ char *encoder;
+ char **avopts;
+};
+
+struct priv {
+ struct f_opts *opts;
+
+ struct mp_pin *in_pin;
+ struct mp_aframe *cur_format;
+ struct mp_aframe *in_frame;
+ struct mp_aframe_pool *out_pool;
+
+ const struct AVCodec *lavc_acodec;
+ struct AVCodecContext *lavc_actx;
+ AVPacket *lavc_pkt;
+ int bit_rate;
+ int out_samples; // upper bound on encoded output per AC3 frame
+};
+
+static bool reinit(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ mp_aframe_reset(s->cur_format);
+
+ static const int default_bit_rate[AC3_MAX_CHANNELS+1] = \
+ {0, 96000, 192000, 256000, 384000, 448000, 448000};
+
+ if (s->opts->add_iec61937_header) {
+ s->out_samples = AC3_FRAME_SIZE;
+ } else {
+ s->out_samples = AC3_MAX_CODED_FRAME_SIZE /
+ mp_aframe_get_sstride(s->in_frame);
+ }
+
+ int format = mp_aframe_get_format(s->in_frame);
+ int rate = mp_aframe_get_rate(s->in_frame);
+ struct mp_chmap chmap = {0};
+ mp_aframe_get_chmap(s->in_frame, &chmap);
+
+ int bit_rate = s->bit_rate;
+ if (!bit_rate && chmap.num < AC3_MAX_CHANNELS + 1)
+ bit_rate = default_bit_rate[chmap.num];
+
+ avcodec_close(s->lavc_actx);
+
+ // Put sample parameters
+ s->lavc_actx->sample_fmt = af_to_avformat(format);
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ s->lavc_actx->channels = chmap.num;
+ s->lavc_actx->channel_layout = mp_chmap_to_lavc(&chmap);
+#else
+ mp_chmap_to_av_layout(&s->lavc_actx->ch_layout, &chmap);
+#endif
+ s->lavc_actx->sample_rate = rate;
+ s->lavc_actx->bit_rate = bit_rate;
+
+ if (avcodec_open2(s->lavc_actx, s->lavc_acodec, NULL) < 0) {
+ MP_ERR(f, "Couldn't open codec %s, br=%d.\n", "ac3", bit_rate);
+ return false;
+ }
+
+ if (s->lavc_actx->frame_size < 1) {
+ MP_ERR(f, "encoder didn't specify input frame size\n");
+ return false;
+ }
+
+ mp_aframe_config_copy(s->cur_format, s->in_frame);
+ return true;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ TA_FREEP(&s->in_frame);
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ reset(f);
+ av_packet_free(&s->lavc_pkt);
+ avcodec_free_context(&s->lavc_actx);
+}
+
+static void swap_16(uint16_t *ptr, size_t size)
+{
+ for (size_t n = 0; n < size; n++)
+ ptr[n] = av_bswap16(ptr[n]);
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ bool err = true;
+ struct mp_aframe *out = NULL;
+ AVPacket *pkt = s->lavc_pkt;
+
+ // Send input as long as it wants.
+ while (1) {
+ if (avcodec_is_open(s->lavc_actx)) {
+ int lavc_ret = avcodec_receive_packet(s->lavc_actx, pkt);
+ if (lavc_ret >= 0)
+ break;
+ if (lavc_ret < 0 && lavc_ret != AVERROR(EAGAIN)) {
+ MP_FATAL(f, "Encode failed (receive).\n");
+ goto error;
+ }
+ }
+ AVFrame *frame = NULL;
+ struct mp_frame input = mp_pin_out_read(s->in_pin);
+ // The following code assumes no sample data buffering in the encoder.
+ switch (input.type) {
+ case MP_FRAME_NONE:
+ goto done; // no data yet
+ case MP_FRAME_EOF:
+ mp_pin_in_write(f->ppins[1], input);
+ goto done;
+ case MP_FRAME_AUDIO:
+ TA_FREEP(&s->in_frame);
+ s->in_frame = input.data;
+ frame = mp_frame_to_av(input, NULL);
+ if (!frame)
+ goto error;
+ if (mp_aframe_get_channels(s->in_frame) < s->opts->min_channel_num) {
+ // Just pass it through.
+ s->in_frame = NULL;
+ mp_pin_in_write(f->ppins[1], input);
+ goto done;
+ }
+ if (!mp_aframe_config_equals(s->in_frame, s->cur_format)) {
+ if (!reinit(f))
+ goto error;
+ }
+ break;
+ default: goto error; // unexpected packet type
+ }
+ int lavc_ret = avcodec_send_frame(s->lavc_actx, frame);
+ av_frame_free(&frame);
+ if (lavc_ret < 0 && lavc_ret != AVERROR(EAGAIN)) {
+ MP_FATAL(f, "Encode failed (send).\n");
+ goto error;
+ }
+ }
+
+ if (!s->in_frame)
+ goto error;
+
+ out = mp_aframe_create();
+ mp_aframe_set_format(out, AF_FORMAT_S_AC3);
+ mp_aframe_set_chmap(out, &(struct mp_chmap)MP_CHMAP_INIT_STEREO);
+ mp_aframe_set_rate(out, 48000);
+
+ if (mp_aframe_pool_allocate(s->out_pool, out, s->out_samples) < 0)
+ goto error;
+
+ int sstride = mp_aframe_get_sstride(out);
+
+ mp_aframe_copy_attributes(out, s->in_frame);
+
+ int frame_size = pkt->size;
+ int header_len = 0;
+ char hdr[8];
+
+ if (s->opts->add_iec61937_header && pkt->size > 5) {
+ int bsmod = pkt->data[5] & 0x7;
+ int len = frame_size;
+
+ frame_size = AC3_FRAME_SIZE * 2 * 2;
+ header_len = 8;
+
+ AV_WL16(hdr, 0xF872); // iec 61937 syncword 1
+ AV_WL16(hdr + 2, 0x4E1F); // iec 61937 syncword 2
+ hdr[5] = bsmod; // bsmod
+ hdr[4] = 0x01; // data-type ac3
+ AV_WL16(hdr + 6, len << 3); // number of bits in payload
+ }
+
+ if (frame_size > s->out_samples * sstride)
+ abort();
+
+ uint8_t **planes = mp_aframe_get_data_rw(out);
+ if (!planes)
+ goto error;
+ char *buf = planes[0];
+ memcpy(buf, hdr, header_len);
+ memcpy(buf + header_len, pkt->data, pkt->size);
+ memset(buf + header_len + pkt->size, 0,
+ frame_size - (header_len + pkt->size));
+ swap_16((uint16_t *)(buf + header_len), pkt->size / 2);
+ mp_aframe_set_size(out, frame_size / sstride);
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out));
+ out = NULL;
+
+done:
+ err = false;
+ // fall through
+error:
+ av_packet_unref(pkt);
+ talloc_free(out);
+ if (err)
+ mp_filter_internal_mark_failed(f);
+}
+
+static const struct mp_filter_info af_lavcac3enc_filter = {
+ .name = "lavcac3enc",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static void add_chmaps_to_autoconv(struct mp_filter *f,
+ struct mp_autoconvert *conv,
+ const struct AVCodec *codec)
+{
+#if !HAVE_AV_CHANNEL_LAYOUT
+ const uint64_t *lch = codec->channel_layouts;
+ for (int n = 0; lch && lch[n]; n++) {
+ struct mp_chmap chmap = {0};
+ mp_chmap_from_lavc(&chmap, lch[n]);
+ if (mp_chmap_is_valid(&chmap))
+ mp_autoconvert_add_chmap(conv, &chmap);
+ }
+#else
+ const AVChannelLayout *lch = codec->ch_layouts;
+ for (int n = 0; lch && lch[n].nb_channels; n++) {
+ struct mp_chmap chmap = {0};
+
+ if (!mp_chmap_from_av_layout(&chmap, &lch[n])) {
+ char layout[128] = {0};
+ MP_VERBOSE(f, "Skipping unsupported channel layout: %s\n",
+ av_channel_layout_describe(&lch[n],
+ layout, 128) < 0 ?
+ "undefined" : layout);
+ continue;
+ }
+
+ if (mp_chmap_is_valid(&chmap))
+ mp_autoconvert_add_chmap(conv, &chmap);
+ }
+#endif
+}
+
+static struct mp_filter *af_lavcac3enc_create(struct mp_filter *parent,
+ void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &af_lavcac3enc_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *s = f->priv;
+ s->opts = talloc_steal(s, options);
+ s->cur_format = talloc_steal(s, mp_aframe_create());
+ s->out_pool = mp_aframe_pool_create(s);
+
+ s->lavc_acodec = avcodec_find_encoder_by_name(s->opts->encoder);
+ if (!s->lavc_acodec) {
+ MP_ERR(f, "Couldn't find encoder %s.\n", s->opts->encoder);
+ goto error;
+ }
+
+ s->lavc_actx = avcodec_alloc_context3(s->lavc_acodec);
+ if (!s->lavc_actx) {
+ MP_ERR(f, "Audio LAVC, couldn't allocate context!\n");
+ goto error;
+ }
+
+ s->lavc_pkt = av_packet_alloc();
+ if (!s->lavc_pkt)
+ goto error;
+
+ if (mp_set_avopts(f->log, s->lavc_actx, s->opts->avopts) < 0)
+ goto error;
+
+ // For this one, we require the decoder to export lists of all supported
+ // parameters. (Not all decoders do that, but the ones we're interested
+ // in do.)
+ if (!s->lavc_acodec->sample_fmts ||
+#if !HAVE_AV_CHANNEL_LAYOUT
+ !s->lavc_acodec->channel_layouts
+#else
+ !s->lavc_acodec->ch_layouts
+#endif
+ )
+ {
+ MP_ERR(f, "Audio encoder doesn't list supported parameters.\n");
+ goto error;
+ }
+
+ if (s->opts->bit_rate) {
+ int i;
+ for (i = 0; i < 19; i++) {
+ if (ac3_bitrate_tab[i] == s->opts->bit_rate) {
+ s->bit_rate = ac3_bitrate_tab[i] * 1000;
+ break;
+ }
+ }
+ if (i >= 19) {
+ MP_WARN(f, "unable set unsupported bitrate %d, using default "
+ "bitrate (check manpage to see supported bitrates).\n",
+ s->opts->bit_rate);
+ }
+ }
+
+ struct mp_autoconvert *conv = mp_autoconvert_create(f);
+ if (!conv)
+ abort();
+
+ const enum AVSampleFormat *lf = s->lavc_acodec->sample_fmts;
+ for (int i = 0; lf && lf[i] != AV_SAMPLE_FMT_NONE; i++) {
+ int mpfmt = af_from_avformat(lf[i]);
+ if (mpfmt)
+ mp_autoconvert_add_afmt(conv, mpfmt);
+ }
+
+ add_chmaps_to_autoconv(f, conv, s->lavc_acodec);
+
+ // At least currently, the AC3 encoder doesn't export sample rates.
+ mp_autoconvert_add_srate(conv, 48000);
+
+ mp_pin_connect(conv->f->pins[0], f->ppins[0]);
+
+ struct mp_filter *fs = mp_fixed_aframe_size_create(f, AC3_FRAME_SIZE, true);
+ if (!fs)
+ abort();
+
+ mp_pin_connect(fs->pins[0], conv->f->pins[1]);
+ s->in_pin = fs->pins[1];
+
+ return f;
+
+error:
+ av_packet_free(&s->lavc_pkt);
+ avcodec_free_context(&s->lavc_actx);
+ talloc_free(f);
+ return NULL;
+}
+
+#define OPT_BASE_STRUCT struct f_opts
+
+const struct mp_user_filter_entry af_lavcac3enc = {
+ .desc = {
+ .description = "runtime encode to ac3 using libavcodec",
+ .name = "lavcac3enc",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT) {
+ .add_iec61937_header = true,
+ .bit_rate = 640,
+ .min_channel_num = 3,
+ .encoder = "ac3",
+ },
+ .options = (const struct m_option[]) {
+ {"tospdif", OPT_BOOL(add_iec61937_header)},
+ {"bitrate", OPT_CHOICE(bit_rate,
+ {"auto", 0}, {"default", 0}), M_RANGE(32, 640)},
+ {"minch", OPT_INT(min_channel_num), M_RANGE(2, 6)},
+ {"encoder", OPT_STRING(encoder)},
+ {"o", OPT_KEYVALUELIST(avopts)},
+ {0}
+ },
+ },
+ .create = af_lavcac3enc_create,
+};
diff --git a/audio/filter/af_rubberband.c b/audio/filter/af_rubberband.c
new file mode 100644
index 0000000..48e5cc1
--- /dev/null
+++ b/audio/filter/af_rubberband.c
@@ -0,0 +1,382 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+
+#include <rubberband/rubberband-c.h>
+
+#include "config.h"
+
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+
+// command line options
+struct f_opts {
+ int transients, detector, phase, window,
+ smoothing, formant, pitch, channels, engine;
+ double scale;
+};
+
+struct priv {
+ struct f_opts *opts;
+
+ struct mp_pin *in_pin;
+ struct mp_aframe *cur_format;
+ struct mp_aframe_pool *out_pool;
+ bool sent_final;
+ RubberBandState rubber;
+ double speed;
+ double pitch;
+ struct mp_aframe *pending;
+ // Estimate how much librubberband has buffered internally.
+ // I could not find a way to do this with the librubberband API.
+ double rubber_delay;
+};
+
+static void update_speed(struct priv *p, double new_speed)
+{
+ p->speed = new_speed;
+ if (p->rubber)
+ rubberband_set_time_ratio(p->rubber, 1.0 / p->speed);
+}
+
+static bool update_pitch(struct priv *p, double new_pitch)
+{
+ if (new_pitch < 0.01 || new_pitch > 100.0)
+ return false;
+
+ p->pitch = new_pitch;
+ if (p->rubber)
+ rubberband_set_pitch_scale(p->rubber, p->pitch);
+ return true;
+}
+
+static bool init_rubberband(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ assert(!p->rubber);
+ assert(p->pending);
+
+ int opts = p->opts->transients | p->opts->detector | p->opts->phase |
+ p->opts->window | p->opts->smoothing | p->opts->formant |
+ p->opts->pitch | p->opts->channels |
+#if HAVE_RUBBERBAND_3
+ p->opts->engine |
+#endif
+ RubberBandOptionProcessRealTime;
+
+ int rate = mp_aframe_get_rate(p->pending);
+ int channels = mp_aframe_get_channels(p->pending);
+ if (mp_aframe_get_format(p->pending) != AF_FORMAT_FLOATP)
+ return false;
+
+ p->rubber = rubberband_new(rate, channels, opts, 1.0, 1.0);
+ if (!p->rubber) {
+ MP_FATAL(f, "librubberband initialization failed.\n");
+ return false;
+ }
+
+ mp_aframe_config_copy(p->cur_format, p->pending);
+
+ update_speed(p, p->speed);
+ update_pitch(p, p->pitch);
+
+ return true;
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ while (!p->rubber || !p->pending || rubberband_available(p->rubber) <= 0) {
+ const float *dummy[MP_NUM_CHANNELS] = {0};
+ const float **in_data = dummy;
+ size_t in_samples = 0;
+
+ bool eof = false;
+ if (!p->pending || !mp_aframe_get_size(p->pending)) {
+ struct mp_frame frame = mp_pin_out_read(p->in_pin);
+ if (frame.type == MP_FRAME_AUDIO) {
+ TA_FREEP(&p->pending);
+ p->pending = frame.data;
+ } else if (frame.type == MP_FRAME_EOF) {
+ eof = true;
+ } else if (frame.type) {
+ MP_ERR(f, "unexpected frame type\n");
+ goto error;
+ } else {
+ return; // no new data yet
+ }
+ }
+ assert(p->pending || eof);
+
+ if (!p->rubber) {
+ if (!p->pending) {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ return;
+ }
+ if (!init_rubberband(f))
+ goto error;
+ }
+
+ bool format_change =
+ p->pending && !mp_aframe_config_equals(p->pending, p->cur_format);
+
+ if (p->pending && !format_change) {
+ size_t needs = rubberband_get_samples_required(p->rubber);
+ uint8_t **planes = mp_aframe_get_data_ro(p->pending);
+ int num_planes = mp_aframe_get_planes(p->pending);
+ for (int n = 0; n < num_planes; n++)
+ in_data[n] = (void *)planes[n];
+ in_samples = MPMIN(mp_aframe_get_size(p->pending), needs);
+ }
+
+ bool final = format_change || eof;
+ if (!p->sent_final)
+ rubberband_process(p->rubber, in_data, in_samples, final);
+ p->sent_final |= final;
+
+ p->rubber_delay += in_samples;
+
+ if (p->pending && !format_change)
+ mp_aframe_skip_samples(p->pending, in_samples);
+
+ if (rubberband_available(p->rubber) > 0) {
+ if (eof)
+ mp_pin_out_repeat_eof(p->in_pin); // drain more next time
+ } else {
+ if (eof) {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ rubberband_reset(p->rubber);
+ p->rubber_delay = 0;
+ TA_FREEP(&p->pending);
+ p->sent_final = false;
+ return;
+ } else if (format_change) {
+ // go on with proper reinit on the next iteration
+ rubberband_delete(p->rubber);
+ p->sent_final = false;
+ p->rubber = NULL;
+ }
+ }
+ }
+
+ assert(p->pending);
+
+ int out_samples = rubberband_available(p->rubber);
+ if (out_samples > 0) {
+ struct mp_aframe *out = mp_aframe_new_ref(p->cur_format);
+ if (mp_aframe_pool_allocate(p->out_pool, out, out_samples) < 0) {
+ talloc_free(out);
+ goto error;
+ }
+
+ mp_aframe_copy_attributes(out, p->pending);
+
+ float *out_data[MP_NUM_CHANNELS] = {0};
+ uint8_t **planes = mp_aframe_get_data_rw(out);
+ assert(planes);
+ int num_planes = mp_aframe_get_planes(out);
+ for (int n = 0; n < num_planes; n++)
+ out_data[n] = (void *)planes[n];
+
+ out_samples = rubberband_retrieve(p->rubber, out_data, out_samples);
+
+ if (!out_samples) {
+ mp_filter_internal_mark_progress(f); // unexpected, just try again
+ talloc_free(out);
+ return;
+ }
+
+ mp_aframe_set_size(out, out_samples);
+
+ p->rubber_delay -= out_samples * p->speed;
+
+ double pts = mp_aframe_get_pts(p->pending);
+ if (pts != MP_NOPTS_VALUE) {
+ // Note: rubberband_get_latency() does not do what you'd expect.
+ double delay = p->rubber_delay / mp_aframe_get_effective_rate(out);
+ mp_aframe_set_pts(out, pts - delay);
+ }
+
+ mp_aframe_mul_speed(out, p->speed);
+
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out));
+ }
+
+ return;
+error:
+ mp_filter_internal_mark_failed(f);
+}
+
+static bool command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *p = f->priv;
+
+ switch (cmd->type) {
+ case MP_FILTER_COMMAND_TEXT: {
+ char *endptr = NULL;
+ double pitch = p->pitch;
+ if (!strcmp(cmd->cmd, "set-pitch")) {
+ pitch = strtod(cmd->arg, &endptr);
+ if (*endptr)
+ return false;
+ return update_pitch(p, pitch);
+ } else if (!strcmp(cmd->cmd, "multiply-pitch")) {
+ double mult = strtod(cmd->arg, &endptr);
+ if (*endptr || mult <= 0)
+ return false;
+ pitch *= mult;
+ return update_pitch(p, pitch);
+ }
+ return false;
+ }
+ case MP_FILTER_COMMAND_SET_SPEED:
+ update_speed(p, cmd->speed);
+ return true;
+ }
+
+ return false;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (p->rubber)
+ rubberband_reset(p->rubber);
+ p->rubber_delay = 0;
+ p->sent_final = false;
+ TA_FREEP(&p->pending);
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (p->rubber)
+ rubberband_delete(p->rubber);
+ talloc_free(p->pending);
+}
+
+static const struct mp_filter_info af_rubberband_filter = {
+ .name = "rubberband",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .command = command,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static struct mp_filter *af_rubberband_create(struct mp_filter *parent,
+ void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &af_rubberband_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+ p->speed = 1.0;
+ p->pitch = p->opts->scale;
+ p->cur_format = talloc_steal(p, mp_aframe_create());
+ p->out_pool = mp_aframe_pool_create(p);
+
+ struct mp_autoconvert *conv = mp_autoconvert_create(f);
+ if (!conv)
+ abort();
+
+ mp_autoconvert_add_afmt(conv, AF_FORMAT_FLOATP);
+
+ mp_pin_connect(conv->f->pins[0], f->ppins[0]);
+ p->in_pin = conv->f->pins[1];
+
+ return f;
+}
+
+#define OPT_BASE_STRUCT struct f_opts
+
+const struct mp_user_filter_entry af_rubberband = {
+ .desc = {
+ .description = "Pitch conversion with librubberband",
+ .name = "rubberband",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT) {
+ .scale = 1.0,
+ .pitch = RubberBandOptionPitchHighConsistency,
+ .transients = RubberBandOptionTransientsMixed,
+ .formant = RubberBandOptionFormantPreserved,
+ .channels = RubberBandOptionChannelsTogether,
+#if HAVE_RUBBERBAND_3
+ .engine = RubberBandOptionEngineFiner,
+#endif
+ },
+ .options = (const struct m_option[]) {
+ {"transients", OPT_CHOICE(transients,
+ {"crisp", RubberBandOptionTransientsCrisp},
+ {"mixed", RubberBandOptionTransientsMixed},
+ {"smooth", RubberBandOptionTransientsSmooth})},
+ {"detector", OPT_CHOICE(detector,
+ {"compound", RubberBandOptionDetectorCompound},
+ {"percussive", RubberBandOptionDetectorPercussive},
+ {"soft", RubberBandOptionDetectorSoft})},
+ {"phase", OPT_CHOICE(phase,
+ {"laminar", RubberBandOptionPhaseLaminar},
+ {"independent", RubberBandOptionPhaseIndependent})},
+ {"window", OPT_CHOICE(window,
+ {"standard", RubberBandOptionWindowStandard},
+ {"short", RubberBandOptionWindowShort},
+ {"long", RubberBandOptionWindowLong})},
+ {"smoothing", OPT_CHOICE(smoothing,
+ {"off", RubberBandOptionSmoothingOff},
+ {"on", RubberBandOptionSmoothingOn})},
+ {"formant", OPT_CHOICE(formant,
+ {"shifted", RubberBandOptionFormantShifted},
+ {"preserved", RubberBandOptionFormantPreserved})},
+ {"pitch", OPT_CHOICE(pitch,
+ {"quality", RubberBandOptionPitchHighQuality},
+ {"speed", RubberBandOptionPitchHighSpeed},
+ {"consistency", RubberBandOptionPitchHighConsistency})},
+ {"channels", OPT_CHOICE(channels,
+ {"apart", RubberBandOptionChannelsApart},
+ {"together", RubberBandOptionChannelsTogether})},
+#if HAVE_RUBBERBAND_3
+ {"engine", OPT_CHOICE(engine,
+ {"finer", RubberBandOptionEngineFiner},
+ {"faster", RubberBandOptionEngineFaster})},
+#endif
+ {"pitch-scale", OPT_DOUBLE(scale), M_RANGE(0.01, 100)},
+ {0}
+ },
+ },
+ .create = af_rubberband_create,
+};
diff --git a/audio/filter/af_scaletempo.c b/audio/filter/af_scaletempo.c
new file mode 100644
index 0000000..f06478f
--- /dev/null
+++ b/audio/filter/af_scaletempo.c
@@ -0,0 +1,626 @@
+/*
+ * scaletempo audio filter
+ *
+ * scale tempo while maintaining pitch
+ * (WSOLA technique with cross correlation)
+ * inspired by SoundTouch library by Olli Parviainen
+ *
+ * basic algorithm
+ * - produce 'stride' output samples per loop
+ * - consume stride*scale input samples per loop
+ *
+ * to produce smoother transitions between strides, blend next overlap
+ * samples from last stride with correlated samples of current input
+ *
+ * Copyright (c) 2007 Robert Juliano
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+
+struct f_opts {
+ float scale_nominal;
+ float ms_stride;
+ float ms_search;
+ float factor_overlap;
+#define SCALE_TEMPO 1
+#define SCALE_PITCH 2
+ int speed_opt;
+};
+
+struct priv {
+ struct f_opts *opts;
+
+ struct mp_pin *in_pin;
+ struct mp_aframe *cur_format;
+ struct mp_aframe_pool *out_pool;
+ double current_pts;
+ struct mp_aframe *in;
+
+ // stride
+ float scale;
+ float speed;
+ int frames_stride;
+ float frames_stride_scaled;
+ float frames_stride_error;
+ int bytes_per_frame;
+ int bytes_stride;
+ int bytes_queue;
+ int bytes_queued;
+ int bytes_to_slide;
+ int8_t *buf_queue;
+ // overlap
+ int samples_overlap;
+ int samples_standing;
+ int bytes_overlap;
+ int bytes_standing;
+ void *buf_overlap;
+ void *table_blend;
+ void (*output_overlap)(struct priv *s, void *out_buf,
+ int bytes_off);
+ // best overlap
+ int frames_search;
+ int num_channels;
+ void *buf_pre_corr;
+ void *table_window;
+ int (*best_overlap_offset)(struct priv *s);
+};
+
+static bool reinit(struct mp_filter *f);
+
+// Return whether it got enough data for filtering.
+static bool fill_queue(struct priv *s)
+{
+ int bytes_in = s->in ? mp_aframe_get_size(s->in) * s->bytes_per_frame : 0;
+ int offset = 0;
+
+ if (s->bytes_to_slide > 0) {
+ if (s->bytes_to_slide < s->bytes_queued) {
+ int bytes_move = s->bytes_queued - s->bytes_to_slide;
+ memmove(s->buf_queue, s->buf_queue + s->bytes_to_slide, bytes_move);
+ s->bytes_to_slide = 0;
+ s->bytes_queued = bytes_move;
+ } else {
+ int bytes_skip;
+ s->bytes_to_slide -= s->bytes_queued;
+ bytes_skip = MPMIN(s->bytes_to_slide, bytes_in);
+ s->bytes_queued = 0;
+ s->bytes_to_slide -= bytes_skip;
+ offset += bytes_skip;
+ bytes_in -= bytes_skip;
+ }
+ }
+
+ int bytes_needed = s->bytes_queue - s->bytes_queued;
+ assert(bytes_needed >= 0);
+
+ int bytes_copy = MPMIN(bytes_needed, bytes_in);
+ if (bytes_copy > 0) {
+ uint8_t **planes = mp_aframe_get_data_ro(s->in);
+ memcpy(s->buf_queue + s->bytes_queued, planes[0] + offset, bytes_copy);
+ s->bytes_queued += bytes_copy;
+ offset += bytes_copy;
+ bytes_needed -= bytes_copy;
+ }
+
+ if (s->in)
+ mp_aframe_skip_samples(s->in, offset / s->bytes_per_frame);
+
+ return bytes_needed == 0;
+}
+
+#define UNROLL_PADDING (4 * 4)
+
+static int best_overlap_offset_float(struct priv *s)
+{
+ float best_corr = INT_MIN;
+ int best_off = 0;
+
+ float *pw = s->table_window;
+ float *po = s->buf_overlap;
+ po += s->num_channels;
+ float *ppc = s->buf_pre_corr;
+ for (int i = s->num_channels; i < s->samples_overlap; i++)
+ *ppc++ = *pw++ **po++;
+
+ float *search_start = (float *)s->buf_queue + s->num_channels;
+ for (int off = 0; off < s->frames_search; off++) {
+ float corr = 0;
+ float *ps = search_start;
+ ppc = s->buf_pre_corr;
+ for (int i = s->num_channels; i < s->samples_overlap; i++)
+ corr += *ppc++ **ps++;
+ if (corr > best_corr) {
+ best_corr = corr;
+ best_off = off;
+ }
+ search_start += s->num_channels;
+ }
+
+ return best_off * 4 * s->num_channels;
+}
+
+static int best_overlap_offset_s16(struct priv *s)
+{
+ int64_t best_corr = INT64_MIN;
+ int best_off = 0;
+
+ int32_t *pw = s->table_window;
+ int16_t *po = s->buf_overlap;
+ po += s->num_channels;
+ int32_t *ppc = s->buf_pre_corr;
+ for (long i = s->num_channels; i < s->samples_overlap; i++)
+ *ppc++ = (*pw++ **po++) >> 15;
+
+ int16_t *search_start = (int16_t *)s->buf_queue + s->num_channels;
+ for (int off = 0; off < s->frames_search; off++) {
+ int64_t corr = 0;
+ int16_t *ps = search_start;
+ ppc = s->buf_pre_corr;
+ ppc += s->samples_overlap - s->num_channels;
+ ps += s->samples_overlap - s->num_channels;
+ long i = -(s->samples_overlap - s->num_channels);
+ do {
+ corr += ppc[i + 0] * (int64_t)ps[i + 0];
+ corr += ppc[i + 1] * (int64_t)ps[i + 1];
+ corr += ppc[i + 2] * (int64_t)ps[i + 2];
+ corr += ppc[i + 3] * (int64_t)ps[i + 3];
+ i += 4;
+ } while (i < 0);
+ if (corr > best_corr) {
+ best_corr = corr;
+ best_off = off;
+ }
+ search_start += s->num_channels;
+ }
+
+ return best_off * 2 * s->num_channels;
+}
+
+static void output_overlap_float(struct priv *s, void *buf_out,
+ int bytes_off)
+{
+ float *pout = buf_out;
+ float *pb = s->table_blend;
+ float *po = s->buf_overlap;
+ float *pin = (float *)(s->buf_queue + bytes_off);
+ for (int i = 0; i < s->samples_overlap; i++) {
+ *pout++ = *po - *pb++ *(*po - *pin++);
+ po++;
+ }
+}
+
+static void output_overlap_s16(struct priv *s, void *buf_out,
+ int bytes_off)
+{
+ int16_t *pout = buf_out;
+ int32_t *pb = s->table_blend;
+ int16_t *po = s->buf_overlap;
+ int16_t *pin = (int16_t *)(s->buf_queue + bytes_off);
+ for (int i = 0; i < s->samples_overlap; i++) {
+ *pout++ = *po - ((*pb++ *(*po - *pin++)) >> 16);
+ po++;
+ }
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ struct mp_aframe *out = NULL;
+
+ bool drain = false;
+ bool is_eof = false;
+ if (!s->in) {
+ struct mp_frame frame = mp_pin_out_read(s->in_pin);
+ if (!frame.type)
+ return; // no input yet
+ if (frame.type != MP_FRAME_AUDIO && frame.type != MP_FRAME_EOF) {
+ MP_ERR(f, "unexpected frame type\n");
+ goto error;
+ }
+
+ s->in = frame.type == MP_FRAME_AUDIO ? frame.data : NULL;
+ is_eof = drain = !s->in;
+
+ // EOF before it was even initialized once.
+ if (is_eof && !mp_aframe_config_is_valid(s->cur_format)) {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ return;
+ }
+
+ if (s->in && !mp_aframe_config_equals(s->in, s->cur_format)) {
+ if (s->bytes_queued) {
+ // Drain remaining data before executing the format change.
+ MP_VERBOSE(f, "draining\n");
+ mp_pin_out_unread(s->in_pin, frame);
+ s->in = NULL;
+ drain = true;
+ } else {
+ if (!reinit(f)) {
+ MP_ERR(f, "initialization failed\n");
+ goto error;
+ }
+ }
+ }
+
+ if (s->in)
+ s->current_pts = mp_aframe_end_pts(s->in);
+ }
+
+ if (!fill_queue(s) && !drain) {
+ TA_FREEP(&s->in);
+ mp_pin_out_request_data_next(s->in_pin);
+ return;
+ }
+
+ int max_out_samples = s->bytes_stride / s->bytes_per_frame;
+ if (drain)
+ max_out_samples += s->bytes_queued;
+
+ out = mp_aframe_new_ref(s->cur_format);
+ if (mp_aframe_pool_allocate(s->out_pool, out, max_out_samples) < 0)
+ goto error;
+
+ if (s->in)
+ mp_aframe_copy_attributes(out, s->in);
+
+ uint8_t **out_planes = mp_aframe_get_data_rw(out);
+ if (!out_planes)
+ goto error;
+ int8_t *pout = out_planes[0];
+ int out_offset = 0;
+ if (s->bytes_queued >= s->bytes_queue) {
+ int ti;
+ float tf;
+ int bytes_off = 0;
+
+ // output stride
+ if (s->output_overlap) {
+ if (s->best_overlap_offset)
+ bytes_off = s->best_overlap_offset(s);
+ s->output_overlap(s, pout + out_offset, bytes_off);
+ }
+ memcpy(pout + out_offset + s->bytes_overlap,
+ s->buf_queue + bytes_off + s->bytes_overlap,
+ s->bytes_standing);
+ out_offset += s->bytes_stride;
+
+ // input stride
+ memcpy(s->buf_overlap,
+ s->buf_queue + bytes_off + s->bytes_stride,
+ s->bytes_overlap);
+ tf = s->frames_stride_scaled + s->frames_stride_error;
+ ti = (int)tf;
+ s->frames_stride_error = tf - ti;
+ s->bytes_to_slide = ti * s->bytes_per_frame;
+ }
+ // Drain remaining buffered data.
+ if (drain && s->bytes_queued) {
+ memcpy(pout + out_offset, s->buf_queue, s->bytes_queued);
+ out_offset += s->bytes_queued;
+ s->bytes_queued = 0;
+ }
+ mp_aframe_set_size(out, out_offset / s->bytes_per_frame);
+
+ // This filter can have a negative delay when scale > 1:
+ // output corresponding to some length of input can be decided and written
+ // after receiving only a part of that input.
+ float delay = (out_offset * s->speed + s->bytes_queued - s->bytes_to_slide) /
+ s->bytes_per_frame / mp_aframe_get_effective_rate(out)
+ + (s->in ? mp_aframe_duration(s->in) : 0);
+
+ if (s->current_pts != MP_NOPTS_VALUE)
+ mp_aframe_set_pts(out, s->current_pts - delay);
+
+ mp_aframe_mul_speed(out, s->speed);
+
+ if (!mp_aframe_get_size(out))
+ TA_FREEP(&out);
+
+ if (is_eof && out) {
+ mp_pin_out_repeat_eof(s->in_pin);
+ } else if (is_eof && !out) {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ } else if (!is_eof && !out) {
+ mp_pin_out_request_data_next(s->in_pin);
+ }
+
+ if (out)
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out));
+
+ return;
+
+error:
+ TA_FREEP(&s->in);
+ talloc_free(out);
+ mp_filter_internal_mark_failed(f);
+}
+
+static void update_speed(struct priv *s, float speed)
+{
+ s->speed = speed;
+
+ double factor = (s->opts->speed_opt & SCALE_PITCH) ? 1.0 / s->speed : s->speed;
+ s->scale = factor * s->opts->scale_nominal;
+
+ s->frames_stride_scaled = s->scale * s->frames_stride;
+ s->frames_stride_error = MPMIN(s->frames_stride_error, s->frames_stride_scaled);
+}
+
+static bool reinit(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ mp_aframe_reset(s->cur_format);
+
+ float srate = mp_aframe_get_rate(s->in) / 1000.0;
+ int nch = mp_aframe_get_channels(s->in);
+ int format = mp_aframe_get_format(s->in);
+
+ int use_int = 0;
+ if (format == AF_FORMAT_S16) {
+ use_int = 1;
+ } else if (format != AF_FORMAT_FLOAT) {
+ return false;
+ }
+ int bps = use_int ? 2 : 4;
+
+ s->frames_stride = srate * s->opts->ms_stride;
+ s->bytes_stride = s->frames_stride * bps * nch;
+
+ update_speed(s, s->speed);
+
+ int frames_overlap = s->frames_stride * s->opts->factor_overlap;
+ if (frames_overlap <= 0) {
+ s->bytes_standing = s->bytes_stride;
+ s->samples_standing = s->bytes_standing / bps;
+ s->output_overlap = NULL;
+ s->bytes_overlap = 0;
+ } else {
+ s->samples_overlap = frames_overlap * nch;
+ s->bytes_overlap = frames_overlap * nch * bps;
+ s->bytes_standing = s->bytes_stride - s->bytes_overlap;
+ s->samples_standing = s->bytes_standing / bps;
+ s->buf_overlap = realloc(s->buf_overlap, s->bytes_overlap);
+ s->table_blend = realloc(s->table_blend, s->bytes_overlap * 4);
+ if (!s->buf_overlap || !s->table_blend) {
+ MP_FATAL(f, "Out of memory\n");
+ return false;
+ }
+ memset(s->buf_overlap, 0, s->bytes_overlap);
+ if (use_int) {
+ int32_t *pb = s->table_blend;
+ int64_t blend = 0;
+ for (int i = 0; i < frames_overlap; i++) {
+ int32_t v = blend / frames_overlap;
+ for (int j = 0; j < nch; j++)
+ *pb++ = v;
+ blend += 65536; // 2^16
+ }
+ s->output_overlap = output_overlap_s16;
+ } else {
+ float *pb = s->table_blend;
+ for (int i = 0; i < frames_overlap; i++) {
+ float v = i / (float)frames_overlap;
+ for (int j = 0; j < nch; j++)
+ *pb++ = v;
+ }
+ s->output_overlap = output_overlap_float;
+ }
+ }
+
+ s->frames_search = (frames_overlap > 1) ? srate * s->opts->ms_search : 0;
+ if (s->frames_search <= 0)
+ s->best_overlap_offset = NULL;
+ else {
+ if (use_int) {
+ int64_t t = frames_overlap;
+ int32_t n = 8589934588LL / (t * t); // 4 * (2^31 - 1) / t^2
+ s->buf_pre_corr = realloc(s->buf_pre_corr,
+ s->bytes_overlap * 2 + UNROLL_PADDING);
+ s->table_window = realloc(s->table_window,
+ s->bytes_overlap * 2 - nch * bps * 2);
+ if (!s->buf_pre_corr || !s->table_window) {
+ MP_FATAL(f, "Out of memory\n");
+ return false;
+ }
+ memset((char *)s->buf_pre_corr + s->bytes_overlap * 2, 0,
+ UNROLL_PADDING);
+ int32_t *pw = s->table_window;
+ for (int i = 1; i < frames_overlap; i++) {
+ int32_t v = (i * (t - i) * n) >> 15;
+ for (int j = 0; j < nch; j++)
+ *pw++ = v;
+ }
+ s->best_overlap_offset = best_overlap_offset_s16;
+ } else {
+ s->buf_pre_corr = realloc(s->buf_pre_corr, s->bytes_overlap);
+ s->table_window = realloc(s->table_window,
+ s->bytes_overlap - nch * bps);
+ if (!s->buf_pre_corr || !s->table_window) {
+ MP_FATAL(f, "Out of memory\n");
+ return false;
+ }
+ float *pw = s->table_window;
+ for (int i = 1; i < frames_overlap; i++) {
+ float v = i * (frames_overlap - i);
+ for (int j = 0; j < nch; j++)
+ *pw++ = v;
+ }
+ s->best_overlap_offset = best_overlap_offset_float;
+ }
+ }
+
+ s->bytes_per_frame = bps * nch;
+ s->num_channels = nch;
+
+ s->bytes_queue = (s->frames_search + s->frames_stride + frames_overlap)
+ * bps * nch;
+ s->buf_queue = realloc(s->buf_queue, s->bytes_queue + UNROLL_PADDING);
+ if (!s->buf_queue) {
+ MP_FATAL(f, "Out of memory\n");
+ return false;
+ }
+
+ s->bytes_queued = 0;
+ s->bytes_to_slide = 0;
+
+ MP_DBG(f, ""
+ "%.2f stride_in, %i stride_out, %i standing, "
+ "%i overlap, %i search, %i queue, %s mode\n",
+ s->frames_stride_scaled,
+ (int)(s->bytes_stride / nch / bps),
+ (int)(s->bytes_standing / nch / bps),
+ (int)(s->bytes_overlap / nch / bps),
+ s->frames_search,
+ (int)(s->bytes_queue / nch / bps),
+ (use_int ? "s16" : "float"));
+
+ mp_aframe_config_copy(s->cur_format, s->in);
+
+ return true;
+}
+
+static bool command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *s = f->priv;
+
+ if (cmd->type == MP_FILTER_COMMAND_SET_SPEED) {
+ if (s->opts->speed_opt & SCALE_TEMPO) {
+ if (s->opts->speed_opt & SCALE_PITCH)
+ return false;
+ update_speed(s, cmd->speed);
+ return true;
+ } else if (s->opts->speed_opt & SCALE_PITCH) {
+ update_speed(s, cmd->speed);
+ return false; // do not signal OK
+ }
+ }
+
+ return false;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+
+ s->current_pts = MP_NOPTS_VALUE;
+ s->bytes_queued = 0;
+ s->bytes_to_slide = 0;
+ s->frames_stride_error = 0;
+ if (s->buf_overlap && s->bytes_overlap)
+ memset(s->buf_overlap, 0, s->bytes_overlap);
+ TA_FREEP(&s->in);
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *s = f->priv;
+ free(s->buf_queue);
+ free(s->buf_overlap);
+ free(s->buf_pre_corr);
+ free(s->table_blend);
+ free(s->table_window);
+ TA_FREEP(&s->in);
+ mp_filter_free_children(f);
+}
+
+static const struct mp_filter_info af_scaletempo_filter = {
+ .name = "scaletempo",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .command = command,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static struct mp_filter *af_scaletempo_create(struct mp_filter *parent,
+ void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &af_scaletempo_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *s = f->priv;
+ s->opts = talloc_steal(s, options);
+ s->speed = 1.0;
+ s->cur_format = talloc_steal(s, mp_aframe_create());
+ s->out_pool = mp_aframe_pool_create(s);
+
+ struct mp_autoconvert *conv = mp_autoconvert_create(f);
+ if (!conv)
+ abort();
+
+ mp_autoconvert_add_afmt(conv, AF_FORMAT_S16);
+ mp_autoconvert_add_afmt(conv, AF_FORMAT_FLOAT);
+
+ mp_pin_connect(conv->f->pins[0], f->ppins[0]);
+ s->in_pin = conv->f->pins[1];
+
+ return f;
+}
+
+#define OPT_BASE_STRUCT struct f_opts
+
+const struct mp_user_filter_entry af_scaletempo = {
+ .desc = {
+ .description = "Scale audio tempo while maintaining pitch",
+ .name = "scaletempo",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT) {
+ .ms_stride = 60,
+ .factor_overlap = .20,
+ .ms_search = 14,
+ .speed_opt = SCALE_TEMPO,
+ .scale_nominal = 1.0,
+ },
+ .options = (const struct m_option[]) {
+ {"scale", OPT_FLOAT(scale_nominal), M_RANGE(0.01, DBL_MAX)},
+ {"stride", OPT_FLOAT(ms_stride), M_RANGE(0.01, DBL_MAX)},
+ {"overlap", OPT_FLOAT(factor_overlap), M_RANGE(0, 1)},
+ {"search", OPT_FLOAT(ms_search), M_RANGE(0, DBL_MAX)},
+ {"speed", OPT_CHOICE(speed_opt,
+ {"pitch", SCALE_PITCH},
+ {"tempo", SCALE_TEMPO},
+ {"none", 0},
+ {"both", SCALE_TEMPO | SCALE_PITCH})},
+ {0}
+ },
+ },
+ .create = af_scaletempo_create,
+};
diff --git a/audio/filter/af_scaletempo2.c b/audio/filter/af_scaletempo2.c
new file mode 100644
index 0000000..7ad8e35
--- /dev/null
+++ b/audio/filter/af_scaletempo2.c
@@ -0,0 +1,254 @@
+#include "audio/aframe.h"
+#include "audio/filter/af_scaletempo2_internals.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+
+struct priv {
+ struct mp_scaletempo2 data;
+ struct mp_pin *in_pin;
+ struct mp_aframe *cur_format;
+ struct mp_aframe_pool *out_pool;
+ bool sent_final;
+ struct mp_aframe *pending;
+ bool initialized;
+ float speed;
+};
+
+static bool init_scaletempo2(struct mp_filter *f);
+static void reset(struct mp_filter *f);
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ while (!p->initialized || !p->pending ||
+ !mp_scaletempo2_frames_available(&p->data, p->speed))
+ {
+ bool eof = false;
+ if (!p->pending || !mp_aframe_get_size(p->pending)) {
+ struct mp_frame frame = mp_pin_out_read(p->in_pin);
+ if (frame.type == MP_FRAME_AUDIO) {
+ TA_FREEP(&p->pending);
+ p->pending = frame.data;
+ } else if (frame.type == MP_FRAME_EOF) {
+ eof = true;
+ } else if (frame.type) {
+ MP_ERR(f, "unexpected frame type\n");
+ goto error;
+ } else {
+ return; // no new data yet
+ }
+ }
+ assert(p->pending || eof);
+
+ if (!p->initialized) {
+ if (!p->pending) {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ return;
+ }
+ if (!init_scaletempo2(f))
+ goto error;
+ }
+
+ bool format_change =
+ p->pending && !mp_aframe_config_equals(p->pending, p->cur_format);
+
+ bool final = format_change || eof;
+ if (p->pending && !format_change && !p->sent_final) {
+ int frame_size = mp_aframe_get_size(p->pending);
+ uint8_t **planes = mp_aframe_get_data_ro(p->pending);
+ int read = mp_scaletempo2_fill_input_buffer(&p->data,
+ planes, frame_size, p->speed);
+ mp_aframe_skip_samples(p->pending, read);
+ }
+ if (final && p->pending && !p->sent_final) {
+ mp_scaletempo2_set_final(&p->data);
+ p->sent_final = true;
+ }
+
+ if (mp_scaletempo2_frames_available(&p->data, p->speed)) {
+ if (eof) {
+ mp_pin_out_repeat_eof(p->in_pin); // drain more next time
+ }
+ } else if (final) {
+ p->initialized = false;
+ p->sent_final = false;
+ if (eof) {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ return;
+ }
+ // for format change go on with proper reinit on the next iteration
+ }
+ }
+
+ assert(p->pending);
+ if (mp_scaletempo2_frames_available(&p->data, p->speed)) {
+ struct mp_aframe *out = mp_aframe_new_ref(p->cur_format);
+ int out_samples = p->data.ola_hop_size;
+ if (mp_aframe_pool_allocate(p->out_pool, out, out_samples) < 0) {
+ talloc_free(out);
+ goto error;
+ }
+
+ mp_aframe_copy_attributes(out, p->pending);
+
+ uint8_t **planes = mp_aframe_get_data_rw(out);
+ assert(planes);
+ assert(mp_aframe_get_planes(out) == p->data.channels);
+
+ out_samples = mp_scaletempo2_fill_buffer(&p->data,
+ (float**)planes, out_samples, p->speed);
+
+ double pts = mp_aframe_get_pts(p->pending);
+ if (pts != MP_NOPTS_VALUE) {
+ double frame_delay = mp_scaletempo2_get_latency(&p->data, p->speed)
+ + out_samples * p->speed;
+ mp_aframe_set_pts(out, pts - frame_delay / mp_aframe_get_effective_rate(out));
+
+ if (p->sent_final) {
+ double remain_pts = pts - mp_aframe_get_pts(out);
+ double rate = mp_aframe_get_effective_rate(out) / p->speed;
+ int max_samples = MPMAX(0, (int) (remain_pts * rate));
+ // truncate final packet to expected length
+ if (out_samples >= max_samples) {
+ out_samples = max_samples;
+
+ // reset the filter to ensure it stops generating audio
+ // and mp_scaletempo2_frames_available returns false
+ mp_scaletempo2_reset(&p->data);
+ }
+ }
+ }
+
+ mp_aframe_set_size(out, out_samples);
+ mp_aframe_mul_speed(out, p->speed);
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out));
+ }
+
+ return;
+error:
+ mp_filter_internal_mark_failed(f);
+}
+
+static bool init_scaletempo2(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ assert(p->pending);
+
+ if (mp_aframe_get_format(p->pending) != AF_FORMAT_FLOATP)
+ return false;
+
+ mp_aframe_reset(p->cur_format);
+ p->initialized = true;
+ p->sent_final = false;
+ mp_aframe_config_copy(p->cur_format, p->pending);
+
+ mp_scaletempo2_init(&p->data, mp_aframe_get_channels(p->pending),
+ mp_aframe_get_rate(p->pending));
+
+ return true;
+}
+
+static bool command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *p = f->priv;
+
+ switch (cmd->type) {
+ case MP_FILTER_COMMAND_SET_SPEED:
+ p->speed = cmd->speed;
+ return true;
+ }
+
+ return false;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ mp_scaletempo2_reset(&p->data);
+ p->initialized = false;
+ TA_FREEP(&p->pending);
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ mp_scaletempo2_destroy(&p->data);
+ talloc_free(p->pending);
+}
+
+static const struct mp_filter_info af_scaletempo2_filter = {
+ .name = "scaletempo2",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .command = command,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static struct mp_filter *af_scaletempo2_create(
+ struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &af_scaletempo2_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->data.opts = talloc_steal(p, options);
+ p->speed = 1.0;
+ p->cur_format = talloc_steal(p, mp_aframe_create());
+ p->out_pool = mp_aframe_pool_create(p);
+ p->pending = NULL;
+ p->initialized = false;
+
+ struct mp_autoconvert *conv = mp_autoconvert_create(f);
+ if (!conv)
+ abort();
+
+ mp_autoconvert_add_afmt(conv, AF_FORMAT_FLOATP);
+
+ mp_pin_connect(conv->f->pins[0], f->ppins[0]);
+ p->in_pin = conv->f->pins[1];
+
+ return f;
+}
+
+#define OPT_BASE_STRUCT struct mp_scaletempo2_opts
+const struct mp_user_filter_entry af_scaletempo2 = {
+ .desc = {
+ .description = "Scale audio tempo while maintaining pitch"
+ " (filter ported from chromium)",
+ .name = "scaletempo2",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT) {
+ .min_playback_rate = 0.25,
+ .max_playback_rate = 8.0,
+ .ola_window_size_ms = 12,
+ .wsola_search_interval_ms = 40,
+ },
+ .options = (const struct m_option[]) {
+ {"search-interval",
+ OPT_FLOAT(wsola_search_interval_ms), M_RANGE(1, 1000)},
+ {"window-size",
+ OPT_FLOAT(ola_window_size_ms), M_RANGE(1, 1000)},
+ {"min-speed",
+ OPT_FLOAT(min_playback_rate), M_RANGE(0, FLT_MAX)},
+ {"max-speed",
+ OPT_FLOAT(max_playback_rate), M_RANGE(0, FLT_MAX)},
+ {0}
+ }
+ },
+ .create = af_scaletempo2_create,
+};
diff --git a/audio/filter/af_scaletempo2_internals.c b/audio/filter/af_scaletempo2_internals.c
new file mode 100644
index 0000000..534f4f6
--- /dev/null
+++ b/audio/filter/af_scaletempo2_internals.c
@@ -0,0 +1,873 @@
+#include <float.h>
+#include <math.h>
+
+#include "audio/chmap.h"
+#include "audio/filter/af_scaletempo2_internals.h"
+
+#include "config.h"
+
+// Algorithm overview (from chromium):
+// Waveform Similarity Overlap-and-add (WSOLA).
+//
+// One WSOLA iteration
+//
+// 1) Extract |target_block| as input frames at indices
+// [|target_block_index|, |target_block_index| + |ola_window_size|).
+// Note that |target_block| is the "natural" continuation of the output.
+//
+// 2) Extract |search_block| as input frames at indices
+// [|search_block_index|,
+// |search_block_index| + |num_candidate_blocks| + |ola_window_size|).
+//
+// 3) Find a block within the |search_block| that is most similar
+// to |target_block|. Let |optimal_index| be the index of such block and
+// write it to |optimal_block|.
+//
+// 4) Update:
+// |optimal_block| = |transition_window| * |target_block| +
+// (1 - |transition_window|) * |optimal_block|.
+//
+// 5) Overlap-and-add |optimal_block| to the |wsola_output|.
+//
+// 6) Update:write
+
+struct interval {
+ int lo;
+ int hi;
+};
+
+static bool in_interval(int n, struct interval q)
+{
+ return n >= q.lo && n <= q.hi;
+}
+
+static float **realloc_2d(float **p, int x, int y)
+{
+ float **array = realloc(p, sizeof(float*) * x + sizeof(float) * x * y);
+ float* data = (float*) (array + x);
+ for (int i = 0; i < x; ++i) {
+ array[i] = data + i * y;
+ }
+ return array;
+}
+
+static void zero_2d(float **a, int x, int y)
+{
+ memset(a + x, 0, sizeof(float) * x * y);
+}
+
+static void zero_2d_partial(float **a, int x, int y)
+{
+ for (int i = 0; i < x; ++i) {
+ memset(a[i], 0, sizeof(float) * y);
+ }
+}
+
+// Energies of sliding windows of channels are interleaved.
+// The number windows is |input_frames| - (|frames_per_window| - 1), hence,
+// the method assumes |energy| must be, at least, of size
+// (|input_frames| - (|frames_per_window| - 1)) * |channels|.
+static void multi_channel_moving_block_energies(
+ float **input, int input_frames, int channels,
+ int frames_per_block, float *energy)
+{
+ int num_blocks = input_frames - (frames_per_block - 1);
+
+ for (int k = 0; k < channels; ++k) {
+ const float* input_channel = input[k];
+
+ energy[k] = 0;
+
+ // First block of channel |k|.
+ for (int m = 0; m < frames_per_block; ++m) {
+ energy[k] += input_channel[m] * input_channel[m];
+ }
+
+ const float* slide_out = input_channel;
+ const float* slide_in = input_channel + frames_per_block;
+ for (int n = 1; n < num_blocks; ++n, ++slide_in, ++slide_out) {
+ energy[k + n * channels] = energy[k + (n - 1) * channels]
+ - *slide_out * *slide_out + *slide_in * *slide_in;
+ }
+ }
+}
+
+static float multi_channel_similarity_measure(
+ const float* dot_prod_a_b,
+ const float* energy_a, const float* energy_b,
+ int channels)
+{
+ const float epsilon = 1e-12f;
+ float similarity_measure = 0.0f;
+ for (int n = 0; n < channels; ++n) {
+ similarity_measure += dot_prod_a_b[n]
+ / sqrtf(energy_a[n] * energy_b[n] + epsilon);
+ }
+ return similarity_measure;
+}
+
+#if HAVE_VECTOR
+
+typedef float v8sf __attribute__ ((vector_size (32), aligned (1)));
+
+// Dot-product of channels of two AudioBus. For each AudioBus an offset is
+// given. |dot_product[k]| is the dot-product of channel |k|. The caller should
+// allocate sufficient space for |dot_product|.
+static void multi_channel_dot_product(
+ float **a, int frame_offset_a,
+ float **b, int frame_offset_b,
+ int channels,
+ int num_frames, float *dot_product)
+{
+ assert(frame_offset_a >= 0);
+ assert(frame_offset_b >= 0);
+
+ for (int k = 0; k < channels; ++k) {
+ const float* ch_a = a[k] + frame_offset_a;
+ const float* ch_b = b[k] + frame_offset_b;
+ float sum = 0.0;
+ if (num_frames < 32)
+ goto rest;
+
+ const v8sf *va = (const v8sf *) ch_a;
+ const v8sf *vb = (const v8sf *) ch_b;
+ v8sf vsum[4] = {
+ // Initialize to product of first 32 floats
+ va[0] * vb[0],
+ va[1] * vb[1],
+ va[2] * vb[2],
+ va[3] * vb[3],
+ };
+ va += 4;
+ vb += 4;
+
+ // Process `va` and `vb` across four vertical stripes
+ for (int n = 1; n < num_frames / 32; n++) {
+ vsum[0] += va[0] * vb[0];
+ vsum[1] += va[1] * vb[1];
+ vsum[2] += va[2] * vb[2];
+ vsum[3] += va[3] * vb[3];
+ va += 4;
+ vb += 4;
+ }
+
+ // Vertical sum across `vsum` entries
+ vsum[0] += vsum[1];
+ vsum[2] += vsum[3];
+ vsum[0] += vsum[2];
+
+ // Horizontal sum across `vsum[0]`, could probably be done better but
+ // this section is not super performance critical
+ float *vf = (float *) &vsum[0];
+ sum = vf[0] + vf[1] + vf[2] + vf[3] + vf[4] + vf[5] + vf[6] + vf[7];
+ ch_a = (const float *) va;
+ ch_b = (const float *) vb;
+
+rest:
+ // Process the remainder
+ for (int n = 0; n < num_frames % 32; n++)
+ sum += *ch_a++ * *ch_b++;
+
+ dot_product[k] = sum;
+ }
+}
+
+#else // !HAVE_VECTOR
+
+static void multi_channel_dot_product(
+ float **a, int frame_offset_a,
+ float **b, int frame_offset_b,
+ int channels,
+ int num_frames, float *dot_product)
+{
+ assert(frame_offset_a >= 0);
+ assert(frame_offset_b >= 0);
+
+ for (int k = 0; k < channels; ++k) {
+ const float* ch_a = a[k] + frame_offset_a;
+ const float* ch_b = b[k] + frame_offset_b;
+ float sum = 0.0;
+ for (int n = 0; n < num_frames; n++)
+ sum += *ch_a++ * *ch_b++;
+ dot_product[k] = sum;
+ }
+}
+
+#endif // HAVE_VECTOR
+
+// Fit the curve f(x) = a * x^2 + b * x + c such that
+// f(-1) = y[0]
+// f(0) = y[1]
+// f(1) = y[2]
+// and return the maximum, assuming that y[0] <= y[1] >= y[2].
+static void quadratic_interpolation(
+ const float* y_values, float* extremum, float* extremum_value)
+{
+ float a = 0.5f * (y_values[2] + y_values[0]) - y_values[1];
+ float b = 0.5f * (y_values[2] - y_values[0]);
+ float c = y_values[1];
+
+ if (a == 0.f) {
+ // The coordinates are colinear (within floating-point error).
+ *extremum = 0;
+ *extremum_value = y_values[1];
+ } else {
+ *extremum = -b / (2.f * a);
+ *extremum_value = a * (*extremum) * (*extremum) + b * (*extremum) + c;
+ }
+}
+
+// Search a subset of all candid blocks. The search is performed every
+// |decimation| frames. This reduces complexity by a factor of about
+// 1 / |decimation|. A cubic interpolation is used to have a better estimate of
+// the best match.
+static int decimated_search(
+ int decimation, struct interval exclude_interval,
+ float **target_block, int target_block_frames,
+ float **search_segment, int search_segment_frames,
+ int channels,
+ const float *energy_target_block, const float *energy_candidate_blocks)
+{
+ int num_candidate_blocks = search_segment_frames - (target_block_frames - 1);
+ float dot_prod [MP_NUM_CHANNELS];
+ float similarity[3]; // Three elements for cubic interpolation.
+
+ int n = 0;
+ multi_channel_dot_product(
+ target_block, 0,
+ search_segment, n,
+ channels,
+ target_block_frames, dot_prod);
+ similarity[0] = multi_channel_similarity_measure(
+ dot_prod, energy_target_block,
+ &energy_candidate_blocks[n * channels], channels);
+
+ // Set the starting point as optimal point.
+ float best_similarity = similarity[0];
+ int optimal_index = 0;
+
+ n += decimation;
+ if (n >= num_candidate_blocks) {
+ return 0;
+ }
+
+ multi_channel_dot_product(
+ target_block, 0,
+ search_segment, n,
+ channels,
+ target_block_frames, dot_prod);
+ similarity[1] = multi_channel_similarity_measure(
+ dot_prod, energy_target_block,
+ &energy_candidate_blocks[n * channels], channels);
+
+ n += decimation;
+ if (n >= num_candidate_blocks) {
+ // We cannot do any more sampling. Compare these two values and return the
+ // optimal index.
+ return similarity[1] > similarity[0] ? decimation : 0;
+ }
+
+ for (; n < num_candidate_blocks; n += decimation) {
+ multi_channel_dot_product(
+ target_block, 0,
+ search_segment, n,
+ channels,
+ target_block_frames, dot_prod);
+
+ similarity[2] = multi_channel_similarity_measure(
+ dot_prod, energy_target_block,
+ &energy_candidate_blocks[n * channels], channels);
+
+ if ((similarity[1] > similarity[0] && similarity[1] >= similarity[2]) ||
+ (similarity[1] >= similarity[0] && similarity[1] > similarity[2]))
+ {
+ // A local maximum is found. Do a cubic interpolation for a better
+ // estimate of candidate maximum.
+ float normalized_candidate_index;
+ float candidate_similarity;
+ quadratic_interpolation(similarity, &normalized_candidate_index,
+ &candidate_similarity);
+
+ int candidate_index = n - decimation
+ + (int)(normalized_candidate_index * decimation + 0.5f);
+ if (candidate_similarity > best_similarity
+ && !in_interval(candidate_index, exclude_interval)) {
+ optimal_index = candidate_index;
+ best_similarity = candidate_similarity;
+ }
+ } else if (n + decimation >= num_candidate_blocks &&
+ similarity[2] > best_similarity &&
+ !in_interval(n, exclude_interval))
+ {
+ // If this is the end-point and has a better similarity-measure than
+ // optimal, then we accept it as optimal point.
+ optimal_index = n;
+ best_similarity = similarity[2];
+ }
+ memmove(similarity, &similarity[1], 2 * sizeof(*similarity));
+ }
+ return optimal_index;
+}
+
+// Search [|low_limit|, |high_limit|] of |search_segment| to find a block that
+// is most similar to |target_block|. |energy_target_block| is the energy of the
+// |target_block|. |energy_candidate_blocks| is the energy of all blocks within
+// |search_block|.
+static int full_search(
+ int low_limit, int high_limit,
+ struct interval exclude_interval,
+ float **target_block, int target_block_frames,
+ float **search_block, int search_block_frames,
+ int channels,
+ const float* energy_target_block,
+ const float* energy_candidate_blocks)
+{
+ // int block_size = target_block->frames;
+ float dot_prod [sizeof(float) * MP_NUM_CHANNELS];
+
+ float best_similarity = -FLT_MAX;//FLT_MIN;
+ int optimal_index = 0;
+
+ for (int n = low_limit; n <= high_limit; ++n) {
+ if (in_interval(n, exclude_interval)) {
+ continue;
+ }
+ multi_channel_dot_product(target_block, 0, search_block, n, channels,
+ target_block_frames, dot_prod);
+
+ float similarity = multi_channel_similarity_measure(
+ dot_prod, energy_target_block,
+ &energy_candidate_blocks[n * channels], channels);
+
+ if (similarity > best_similarity) {
+ best_similarity = similarity;
+ optimal_index = n;
+ }
+ }
+
+ return optimal_index;
+}
+
+// Find the index of the block, within |search_block|, that is most similar
+// to |target_block|. Obviously, the returned index is w.r.t. |search_block|.
+// |exclude_interval| is an interval that is excluded from the search.
+static int compute_optimal_index(
+ float **search_block, int search_block_frames,
+ float **target_block, int target_block_frames,
+ float *energy_candidate_blocks,
+ int channels,
+ struct interval exclude_interval)
+{
+ int num_candidate_blocks = search_block_frames - (target_block_frames - 1);
+
+ // This is a compromise between complexity reduction and search accuracy. I
+ // don't have a proof that down sample of order 5 is optimal.
+ // One can compute a decimation factor that minimizes complexity given
+ // the size of |search_block| and |target_block|. However, my experiments
+ // show the rate of missing the optimal index is significant.
+ // This value is chosen heuristically based on experiments.
+ const int search_decimation = 5;
+
+ float energy_target_block [MP_NUM_CHANNELS];
+ // energy_candidate_blocks must have at least size
+ // sizeof(float) * channels * num_candidate_blocks
+
+ // Energy of all candid frames.
+ multi_channel_moving_block_energies(
+ search_block,
+ search_block_frames,
+ channels,
+ target_block_frames,
+ energy_candidate_blocks);
+
+ // Energy of target frame.
+ multi_channel_dot_product(
+ target_block, 0,
+ target_block, 0,
+ channels,
+ target_block_frames, energy_target_block);
+
+ int optimal_index = decimated_search(
+ search_decimation, exclude_interval,
+ target_block, target_block_frames,
+ search_block, search_block_frames,
+ channels,
+ energy_target_block,
+ energy_candidate_blocks);
+
+ int lim_low = MPMAX(0, optimal_index - search_decimation);
+ int lim_high = MPMIN(num_candidate_blocks - 1,
+ optimal_index + search_decimation);
+ return full_search(
+ lim_low, lim_high, exclude_interval,
+ target_block, target_block_frames,
+ search_block, search_block_frames,
+ channels,
+ energy_target_block, energy_candidate_blocks);
+}
+
+static void peek_buffer(struct mp_scaletempo2 *p,
+ int frames, int read_offset, int write_offset, float **dest)
+{
+ assert(p->input_buffer_frames >= frames);
+ for (int i = 0; i < p->channels; ++i) {
+ memcpy(dest[i] + write_offset,
+ p->input_buffer[i] + read_offset,
+ frames * sizeof(float));
+ }
+}
+
+static void seek_buffer(struct mp_scaletempo2 *p, int frames)
+{
+ assert(p->input_buffer_frames >= frames);
+ p->input_buffer_frames -= frames;
+ if (p->input_buffer_final_frames > 0) {
+ p->input_buffer_final_frames = MPMAX(0, p->input_buffer_final_frames - frames);
+ }
+ for (int i = 0; i < p->channels; ++i) {
+ memmove(p->input_buffer[i], p->input_buffer[i] + frames,
+ p->input_buffer_frames * sizeof(float));
+ }
+}
+
+static int write_completed_frames_to(struct mp_scaletempo2 *p,
+ int requested_frames, int dest_offset, float **dest)
+{
+ int rendered_frames = MPMIN(p->num_complete_frames, requested_frames);
+
+ if (rendered_frames == 0)
+ return 0; // There is nothing to read from |wsola_output|, return.
+
+ for (int i = 0; i < p->channels; ++i) {
+ memcpy(dest[i] + dest_offset, p->wsola_output[i],
+ rendered_frames * sizeof(float));
+ }
+
+ // Remove the frames which are read.
+ int frames_to_move = p->wsola_output_size - rendered_frames;
+ for (int k = 0; k < p->channels; ++k) {
+ float *ch = p->wsola_output[k];
+ memmove(ch, &ch[rendered_frames], sizeof(*ch) * frames_to_move);
+ }
+ p->num_complete_frames -= rendered_frames;
+ return rendered_frames;
+}
+
+// next output_time for the given playback_rate
+static double get_updated_time(struct mp_scaletempo2 *p, double playback_rate)
+{
+ return p->output_time + p->ola_hop_size * playback_rate;
+}
+
+// search_block_index for the given output_time
+static int get_search_block_index(struct mp_scaletempo2 *p, double output_time)
+{
+ return (int)(output_time - p->search_block_center_offset + 0.5);
+}
+
+// number of frames needed until a wsola iteration can be performed
+static int frames_needed(struct mp_scaletempo2 *p, double playback_rate)
+{
+ int search_block_index =
+ get_search_block_index(p, get_updated_time(p, playback_rate));
+ return MPMAX(0, MPMAX(
+ p->target_block_index + p->ola_window_size - p->input_buffer_frames,
+ search_block_index + p->search_block_size - p->input_buffer_frames));
+}
+
+static bool can_perform_wsola(struct mp_scaletempo2 *p, double playback_rate)
+{
+ return frames_needed(p, playback_rate) <= 0;
+}
+
+static void resize_input_buffer(struct mp_scaletempo2 *p, int size)
+{
+ p->input_buffer_size = size;
+ p->input_buffer = realloc_2d(p->input_buffer, p->channels, size);
+}
+
+// pad end with silence until a wsola iteration can be performed
+static void add_input_buffer_final_silence(struct mp_scaletempo2 *p, double playback_rate)
+{
+ int needed = frames_needed(p, playback_rate);
+ if (needed <= 0)
+ return; // no silence needed for iteration
+
+ int required_size = needed + p->input_buffer_frames;
+ if (required_size > p->input_buffer_size)
+ resize_input_buffer(p, required_size);
+
+ for (int i = 0; i < p->channels; ++i) {
+ float *ch_input = p->input_buffer[i];
+ for (int j = 0; j < needed; ++j) {
+ ch_input[p->input_buffer_frames + j] = 0.0f;
+ }
+ }
+
+ p->input_buffer_added_silence += needed;
+ p->input_buffer_frames += needed;
+}
+
+void mp_scaletempo2_set_final(struct mp_scaletempo2 *p)
+{
+ if (p->input_buffer_final_frames <= 0) {
+ p->input_buffer_final_frames = p->input_buffer_frames;
+ }
+}
+
+int mp_scaletempo2_fill_input_buffer(struct mp_scaletempo2 *p,
+ uint8_t **planes, int frame_size, double playback_rate)
+{
+ int needed = frames_needed(p, playback_rate);
+ int read = MPMIN(needed, frame_size);
+ if (read == 0)
+ return 0;
+
+ int required_size = read + p->input_buffer_frames;
+ if (required_size > p->input_buffer_size)
+ resize_input_buffer(p, required_size);
+
+ for (int i = 0; i < p->channels; ++i) {
+ memcpy(p->input_buffer[i] + p->input_buffer_frames,
+ planes[i], read * sizeof(float));
+ }
+
+ p->input_buffer_frames += read;
+ return read;
+}
+
+static bool target_is_within_search_region(struct mp_scaletempo2 *p)
+{
+ return p->target_block_index >= p->search_block_index
+ && p->target_block_index + p->ola_window_size
+ <= p->search_block_index + p->search_block_size;
+}
+
+
+static void peek_audio_with_zero_prepend(struct mp_scaletempo2 *p,
+ int read_offset_frames, float **dest, int dest_frames)
+{
+ assert(read_offset_frames + dest_frames <= p->input_buffer_frames);
+
+ int write_offset = 0;
+ int num_frames_to_read = dest_frames;
+ if (read_offset_frames < 0) {
+ int num_zero_frames_appended = MPMIN(
+ -read_offset_frames, num_frames_to_read);
+ read_offset_frames = 0;
+ num_frames_to_read -= num_zero_frames_appended;
+ write_offset = num_zero_frames_appended;
+ zero_2d_partial(dest, p->channels, num_zero_frames_appended);
+ }
+ peek_buffer(p, num_frames_to_read, read_offset_frames, write_offset, dest);
+}
+
+static void get_optimal_block(struct mp_scaletempo2 *p)
+{
+ int optimal_index = 0;
+
+ // An interval around last optimal block which is excluded from the search.
+ // This is to reduce the buzzy sound. The number 160 is rather arbitrary and
+ // derived heuristically.
+ const int exclude_interval_length_frames = 160;
+ if (target_is_within_search_region(p)) {
+ optimal_index = p->target_block_index;
+ peek_audio_with_zero_prepend(p,
+ optimal_index, p->optimal_block, p->ola_window_size);
+ } else {
+ peek_audio_with_zero_prepend(p,
+ p->target_block_index, p->target_block, p->ola_window_size);
+ peek_audio_with_zero_prepend(p,
+ p->search_block_index, p->search_block, p->search_block_size);
+ int last_optimal = p->target_block_index
+ - p->ola_hop_size - p->search_block_index;
+ struct interval exclude_iterval = {
+ .lo = last_optimal - exclude_interval_length_frames / 2,
+ .hi = last_optimal + exclude_interval_length_frames / 2
+ };
+
+ // |optimal_index| is in frames and it is relative to the beginning of the
+ // |search_block|.
+ optimal_index = compute_optimal_index(
+ p->search_block, p->search_block_size,
+ p->target_block, p->ola_window_size,
+ p->energy_candidate_blocks,
+ p->channels,
+ exclude_iterval);
+
+ // Translate |index| w.r.t. the beginning of |audio_buffer| and extract the
+ // optimal block.
+ optimal_index += p->search_block_index;
+ peek_audio_with_zero_prepend(p,
+ optimal_index, p->optimal_block, p->ola_window_size);
+
+ // Make a transition from target block to the optimal block if different.
+ // Target block has the best continuation to the current output.
+ // Optimal block is the most similar block to the target, however, it might
+ // introduce some discontinuity when over-lap-added. Therefore, we combine
+ // them for a smoother transition. The length of transition window is twice
+ // as that of the optimal-block which makes it like a weighting function
+ // where target-block has higher weight close to zero (weight of 1 at index
+ // 0) and lower weight close the end.
+ for (int k = 0; k < p->channels; ++k) {
+ float* ch_opt = p->optimal_block[k];
+ float* ch_target = p->target_block[k];
+ for (int n = 0; n < p->ola_window_size; ++n) {
+ ch_opt[n] = ch_opt[n] * p->transition_window[n]
+ + ch_target[n] * p->transition_window[p->ola_window_size + n];
+ }
+ }
+ }
+
+ // Next target is one hop ahead of the current optimal.
+ p->target_block_index = optimal_index + p->ola_hop_size;
+}
+
+static void set_output_time(struct mp_scaletempo2 *p, double output_time)
+{
+ p->output_time = output_time;
+ p->search_block_index = get_search_block_index(p, output_time);
+}
+
+static void remove_old_input_frames(struct mp_scaletempo2 *p)
+{
+ const int earliest_used_index = MPMIN(
+ p->target_block_index, p->search_block_index);
+ if (earliest_used_index <= 0)
+ return; // Nothing to remove.
+
+ // Remove frames from input and adjust indices accordingly.
+ seek_buffer(p, earliest_used_index);
+ p->target_block_index -= earliest_used_index;
+ p->output_time -= earliest_used_index;
+ p->search_block_index -= earliest_used_index;
+}
+
+static bool run_one_wsola_iteration(struct mp_scaletempo2 *p, double playback_rate)
+{
+ if (!can_perform_wsola(p, playback_rate)) {
+ return false;
+ }
+
+ set_output_time(p, get_updated_time(p, playback_rate));
+ remove_old_input_frames(p);
+
+ assert(p->search_block_index + p->search_block_size <= p->input_buffer_frames);
+
+ get_optimal_block(p);
+
+ // Overlap-and-add.
+ for (int k = 0; k < p->channels; ++k) {
+ float* ch_opt_frame = p->optimal_block[k];
+ float* ch_output = p->wsola_output[k] + p->num_complete_frames;
+ if (p->wsola_output_started) {
+ for (int n = 0; n < p->ola_hop_size; ++n) {
+ ch_output[n] = ch_output[n] * p->ola_window[p->ola_hop_size + n] +
+ ch_opt_frame[n] * p->ola_window[n];
+ }
+
+ // Copy the second half to the output.
+ memcpy(&ch_output[p->ola_hop_size], &ch_opt_frame[p->ola_hop_size],
+ sizeof(*ch_opt_frame) * p->ola_hop_size);
+ } else {
+ // No overlap for the first iteration.
+ memcpy(ch_output, ch_opt_frame,
+ sizeof(*ch_opt_frame) * p->ola_window_size);
+ }
+ }
+
+ p->num_complete_frames += p->ola_hop_size;
+ p->wsola_output_started = true;
+ return true;
+}
+
+static int read_input_buffer(struct mp_scaletempo2 *p, int dest_size, float **dest)
+{
+ int frames_to_copy = MPMIN(dest_size, p->input_buffer_frames - p->target_block_index);
+
+ if (frames_to_copy <= 0)
+ return 0; // There is nothing to read from input buffer; return.
+
+ peek_buffer(p, frames_to_copy, p->target_block_index, 0, dest);
+ seek_buffer(p, frames_to_copy);
+ return frames_to_copy;
+}
+
+int mp_scaletempo2_fill_buffer(struct mp_scaletempo2 *p,
+ float **dest, int dest_size, double playback_rate)
+{
+ if (playback_rate == 0) return 0;
+
+ if (p->input_buffer_final_frames > 0) {
+ add_input_buffer_final_silence(p, playback_rate);
+ }
+
+ // Optimize the muted case to issue a single clear instead of performing
+ // the full crossfade and clearing each crossfaded frame.
+ if (playback_rate < p->opts->min_playback_rate
+ || (playback_rate > p->opts->max_playback_rate
+ && p->opts->max_playback_rate > 0))
+ {
+ int frames_to_render = MPMIN(dest_size,
+ (int) (p->input_buffer_frames / playback_rate));
+
+ // Compute accurate number of frames to actually skip in the source data.
+ // Includes the leftover partial frame from last request. However, we can
+ // only skip over complete frames, so a partial frame may remain for next
+ // time.
+ p->muted_partial_frame += frames_to_render * playback_rate;
+ int seek_frames = (int) (p->muted_partial_frame);
+ zero_2d_partial(dest, p->channels, frames_to_render);
+ seek_buffer(p, seek_frames);
+
+ // Determine the partial frame that remains to be skipped for next call. If
+ // the user switches back to playing, it may be off time by this partial
+ // frame, which would be undetectable. If they subsequently switch to
+ // another playback rate that mutes, the code will attempt to line up the
+ // frames again.
+ p->muted_partial_frame -= seek_frames;
+ return frames_to_render;
+ }
+
+ int slower_step = (int) ceilf(p->ola_window_size * playback_rate);
+ int faster_step = (int) ceilf(p->ola_window_size / playback_rate);
+
+ // Optimize the most common |playback_rate| ~= 1 case to use a single copy
+ // instead of copying frame by frame.
+ if (p->ola_window_size <= faster_step && slower_step >= p->ola_window_size) {
+
+ if (p->wsola_output_started) {
+ p->wsola_output_started = false;
+
+ // sync audio precisely again
+ set_output_time(p, p->target_block_index);
+ remove_old_input_frames(p);
+ }
+
+ return read_input_buffer(p, dest_size, dest);
+ }
+
+ int rendered_frames = 0;
+ do {
+ rendered_frames += write_completed_frames_to(p,
+ dest_size - rendered_frames, rendered_frames, dest);
+ } while (rendered_frames < dest_size
+ && run_one_wsola_iteration(p, playback_rate));
+ return rendered_frames;
+}
+
+double mp_scaletempo2_get_latency(struct mp_scaletempo2 *p, double playback_rate)
+{
+ return p->input_buffer_frames - p->output_time
+ - p->input_buffer_added_silence
+ + p->num_complete_frames * playback_rate;
+}
+
+bool mp_scaletempo2_frames_available(struct mp_scaletempo2 *p, double playback_rate)
+{
+ return p->input_buffer_final_frames > p->target_block_index
+ || can_perform_wsola(p, playback_rate)
+ || p->num_complete_frames > 0;
+}
+
+void mp_scaletempo2_destroy(struct mp_scaletempo2 *p)
+{
+ free(p->ola_window);
+ free(p->transition_window);
+ free(p->wsola_output);
+ free(p->optimal_block);
+ free(p->search_block);
+ free(p->target_block);
+ free(p->input_buffer);
+ free(p->energy_candidate_blocks);
+}
+
+void mp_scaletempo2_reset(struct mp_scaletempo2 *p)
+{
+ p->input_buffer_frames = 0;
+ p->input_buffer_final_frames = 0;
+ p->input_buffer_added_silence = 0;
+ p->output_time = 0.0;
+ p->search_block_index = 0;
+ p->target_block_index = 0;
+ // Clear the queue of decoded packets.
+ zero_2d(p->wsola_output, p->channels, p->wsola_output_size);
+ p->num_complete_frames = 0;
+ p->wsola_output_started = false;
+}
+
+// Return a "periodic" Hann window. This is the first L samples of an L+1
+// Hann window. It is perfect reconstruction for overlap-and-add.
+static void get_symmetric_hanning_window(int window_length, float* window)
+{
+ const float scale = 2.0f * M_PI / window_length;
+ for (int n = 0; n < window_length; ++n)
+ window[n] = 0.5f * (1.0f - cosf(n * scale));
+}
+
+
+void mp_scaletempo2_init(struct mp_scaletempo2 *p, int channels, int rate)
+{
+ p->muted_partial_frame = 0;
+ p->output_time = 0;
+ p->search_block_index = 0;
+ p->target_block_index = 0;
+ p->num_complete_frames = 0;
+ p->wsola_output_started = false;
+ p->channels = channels;
+
+ p->samples_per_second = rate;
+ p->num_candidate_blocks = (int)(p->opts->wsola_search_interval_ms
+ * p->samples_per_second / 1000);
+ p->ola_window_size = (int)(p->opts->ola_window_size_ms
+ * p->samples_per_second / 1000);
+ // Make sure window size in an even number.
+ p->ola_window_size += p->ola_window_size & 1;
+ p->ola_hop_size = p->ola_window_size / 2;
+ // |num_candidate_blocks| / 2 is the offset of the center of the search
+ // block to the center of the first (left most) candidate block. The offset
+ // of the center of a candidate block to its left most point is
+ // |ola_window_size| / 2 - 1. Note that |ola_window_size| is even and in
+ // our convention the center belongs to the left half, so we need to subtract
+ // one frame to get the correct offset.
+ //
+ // Search Block
+ // <------------------------------------------->
+ //
+ // |ola_window_size| / 2 - 1
+ // <----
+ //
+ // |num_candidate_blocks| / 2
+ // <----------------
+ // center
+ // X----X----------------X---------------X-----X
+ // <----------> <---------->
+ // Candidate ... Candidate
+ // 1, ... |num_candidate_blocks|
+ p->search_block_center_offset = p->num_candidate_blocks / 2
+ + (p->ola_window_size / 2 - 1);
+ p->ola_window = realloc(p->ola_window, sizeof(float) * p->ola_window_size);
+ get_symmetric_hanning_window(p->ola_window_size, p->ola_window);
+ p->transition_window = realloc(p->transition_window,
+ sizeof(float) * p->ola_window_size * 2);
+ get_symmetric_hanning_window(2 * p->ola_window_size, p->transition_window);
+
+ p->wsola_output_size = p->ola_window_size + p->ola_hop_size;
+ p->wsola_output = realloc_2d(p->wsola_output, p->channels, p->wsola_output_size);
+ // Initialize for overlap-and-add of the first block.
+ zero_2d(p->wsola_output, p->channels, p->wsola_output_size);
+
+ // Auxiliary containers.
+ p->optimal_block = realloc_2d(p->optimal_block, p->channels, p->ola_window_size);
+ p->search_block_size = p->num_candidate_blocks + (p->ola_window_size - 1);
+ p->search_block = realloc_2d(p->search_block, p->channels, p->search_block_size);
+ p->target_block = realloc_2d(p->target_block, p->channels, p->ola_window_size);
+
+ resize_input_buffer(p, 4 * MPMAX(p->ola_window_size, p->search_block_size));
+ p->input_buffer_frames = 0;
+ p->input_buffer_final_frames = 0;
+ p->input_buffer_added_silence = 0;
+
+ p->energy_candidate_blocks = realloc(p->energy_candidate_blocks,
+ sizeof(float) * p->channels * p->num_candidate_blocks);
+}
diff --git a/audio/filter/af_scaletempo2_internals.h b/audio/filter/af_scaletempo2_internals.h
new file mode 100644
index 0000000..6c3c94c
--- /dev/null
+++ b/audio/filter/af_scaletempo2_internals.h
@@ -0,0 +1,134 @@
+// This filter was ported from Chromium
+// (https://chromium.googlesource.com/chromium/chromium/+/51ed77e3f37a9a9b80d6d0a8259e84a8ca635259/media/filters/audio_renderer_algorithm.cc)
+//
+// Copyright 2015 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "common/common.h"
+
+struct mp_scaletempo2_opts {
+ // Max/min supported playback rates for fast/slow audio. Audio outside of these
+ // ranges are muted.
+ // Audio at these speeds would sound better under a frequency domain algorithm.
+ float min_playback_rate;
+ float max_playback_rate;
+ // Overlap-and-add window size in milliseconds.
+ float ola_window_size_ms;
+ // Size of search interval in milliseconds. The search interval is
+ // [-delta delta] around |output_index| * |playback_rate|. So the search
+ // interval is 2 * delta.
+ float wsola_search_interval_ms;
+};
+
+struct mp_scaletempo2 {
+ struct mp_scaletempo2_opts *opts;
+ // Number of channels in audio stream.
+ int channels;
+ // Sample rate of audio stream.
+ int samples_per_second;
+ // If muted, keep track of partial frames that should have been skipped over.
+ double muted_partial_frame;
+ // Book keeping of the current time of generated audio, in frames.
+ // Corresponds to the center of |search_block|. This is increased in
+ // intervals of |ola_hop_size| multiplied by the current playback_rate,
+ // for every WSOLA iteration. This tracks the number of advanced frames as
+ // a double to achieve accurate playback rates beyond the integer precision
+ // of |search_block_index|.
+ // Needs to be adjusted like any other index when frames are evicted from
+ // |input_buffer|.
+ double output_time;
+ // The offset of the center frame of |search_block| w.r.t. its first frame.
+ int search_block_center_offset;
+ // Index of the beginning of the |search_block|, in frames. This may be
+ // negative, which is handled by |peek_audio_with_zero_prepend|.
+ int search_block_index;
+ // Number of Blocks to search to find the most similar one to the target
+ // frame.
+ int num_candidate_blocks;
+ // Index of the beginning of the target block, counted in frames.
+ int target_block_index;
+ // Overlap-and-add window size in frames.
+ int ola_window_size;
+ // The hop size of overlap-and-add in frames. This implementation assumes 50%
+ // overlap-and-add.
+ int ola_hop_size;
+ // Number of frames in |wsola_output| that overlap-and-add is completed for
+ // them and can be copied to output if fill_buffer() is called. It also
+ // specifies the index where the next WSOLA window has to overlap-and-add.
+ int num_complete_frames;
+ // Whether |wsola_output| contains an additional |ola_hop_size| of overlap
+ // frames for the next iteration.
+ bool wsola_output_started;
+ // Overlap-and-add window.
+ float *ola_window;
+ // Transition window, used to update |optimal_block| by a weighted sum of
+ // |optimal_block| and |target_block|.
+ float *transition_window;
+ // This stores a part of the output that is created but couldn't be rendered.
+ // Output is generated frame-by-frame which at some point might exceed the
+ // number of requested samples. Furthermore, due to overlap-and-add,
+ // the last half-window of the output is incomplete, which is stored in this
+ // buffer.
+ float **wsola_output;
+ int wsola_output_size;
+ // Auxiliary variables to avoid allocation in every iteration.
+ // Stores the optimal block in every iteration. This is the most
+ // similar block to |target_block| within |search_block| and it is
+ // overlap-and-added to |wsola_output|.
+ float **optimal_block;
+ // A block of data that search is performed over to find the |optimal_block|.
+ float **search_block;
+ int search_block_size;
+ // Stores the target block, denoted as |target| above. |search_block| is
+ // searched for a block (|optimal_block|) that is most similar to
+ // |target_block|.
+ float **target_block;
+ // Buffered audio data.
+ float **input_buffer;
+ int input_buffer_size;
+ int input_buffer_frames;
+ // How many frames in |input_buffer| need to be flushed by padding with
+ // silence to process the final packet. While this is nonzero, the filter
+ // appends silence to |input_buffer| until these frames are processed.
+ int input_buffer_final_frames;
+ // How many additional frames of silence have been added to |input_buffer|
+ // for padding after the final packet.
+ int input_buffer_added_silence;
+ float *energy_candidate_blocks;
+};
+
+void mp_scaletempo2_destroy(struct mp_scaletempo2 *p);
+void mp_scaletempo2_reset(struct mp_scaletempo2 *p);
+void mp_scaletempo2_init(struct mp_scaletempo2 *p, int channels, int rate);
+double mp_scaletempo2_get_latency(struct mp_scaletempo2 *p, double playback_rate);
+int mp_scaletempo2_fill_input_buffer(struct mp_scaletempo2 *p,
+ uint8_t **planes, int frame_size, double playback_rate);
+void mp_scaletempo2_set_final(struct mp_scaletempo2 *p);
+int mp_scaletempo2_fill_buffer(struct mp_scaletempo2 *p,
+ float **dest, int dest_size, double playback_rate);
+bool mp_scaletempo2_frames_available(struct mp_scaletempo2 *p, double playback_rate);
diff --git a/audio/fmt-conversion.c b/audio/fmt-conversion.c
new file mode 100644
index 0000000..d72a50d
--- /dev/null
+++ b/audio/fmt-conversion.c
@@ -0,0 +1,60 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/avutil.h>
+#include <libavutil/samplefmt.h>
+#include "format.h"
+#include "fmt-conversion.h"
+
+static const struct {
+ enum AVSampleFormat sample_fmt;
+ int fmt;
+} audio_conversion_map[] = {
+ {AV_SAMPLE_FMT_U8, AF_FORMAT_U8},
+ {AV_SAMPLE_FMT_S16, AF_FORMAT_S16},
+ {AV_SAMPLE_FMT_S32, AF_FORMAT_S32},
+ {AV_SAMPLE_FMT_S64, AF_FORMAT_S64},
+ {AV_SAMPLE_FMT_FLT, AF_FORMAT_FLOAT},
+ {AV_SAMPLE_FMT_DBL, AF_FORMAT_DOUBLE},
+
+ {AV_SAMPLE_FMT_U8P, AF_FORMAT_U8P},
+ {AV_SAMPLE_FMT_S16P, AF_FORMAT_S16P},
+ {AV_SAMPLE_FMT_S32P, AF_FORMAT_S32P},
+ {AV_SAMPLE_FMT_S64P, AF_FORMAT_S64P},
+ {AV_SAMPLE_FMT_FLTP, AF_FORMAT_FLOATP},
+ {AV_SAMPLE_FMT_DBLP, AF_FORMAT_DOUBLEP},
+
+ {AV_SAMPLE_FMT_NONE, 0},
+};
+
+enum AVSampleFormat af_to_avformat(int fmt)
+{
+ for (int i = 0; audio_conversion_map[i].fmt; i++) {
+ if (audio_conversion_map[i].fmt == fmt)
+ return audio_conversion_map[i].sample_fmt;
+ }
+ return AV_SAMPLE_FMT_NONE;
+}
+
+int af_from_avformat(enum AVSampleFormat sample_fmt)
+{
+ for (int i = 0; audio_conversion_map[i].fmt; i++) {
+ if (audio_conversion_map[i].sample_fmt == sample_fmt)
+ return audio_conversion_map[i].fmt;
+ }
+ return 0;
+}
diff --git a/audio/fmt-conversion.h b/audio/fmt-conversion.h
new file mode 100644
index 0000000..63c315b
--- /dev/null
+++ b/audio/fmt-conversion.h
@@ -0,0 +1,24 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_SAMPLE_FMT_CONVERSION_H
+#define MPLAYER_SAMPLE_FMT_CONVERSION_H
+
+enum AVSampleFormat af_to_avformat(int fmt);
+int af_from_avformat(enum AVSampleFormat sample_fmt);
+
+#endif /* MPLAYER_SAMPLE_FMT_CONVERSION_H */
diff --git a/audio/format.c b/audio/format.c
new file mode 100644
index 0000000..4441456
--- /dev/null
+++ b/audio/format.c
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2005 Alex Beregszaszi
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <limits.h>
+
+#include "common/common.h"
+#include "format.h"
+
+// number of bytes per sample, 0 if invalid/unknown
+int af_fmt_to_bytes(int format)
+{
+ switch (af_fmt_from_planar(format)) {
+ case AF_FORMAT_U8: return 1;
+ case AF_FORMAT_S16: return 2;
+ case AF_FORMAT_S32: return 4;
+ case AF_FORMAT_S64: return 8;
+ case AF_FORMAT_FLOAT: return 4;
+ case AF_FORMAT_DOUBLE: return 8;
+ }
+ if (af_fmt_is_spdif(format))
+ return 2;
+ return 0;
+}
+
+// All formats are considered signed, except explicitly unsigned int formats.
+bool af_fmt_is_unsigned(int format)
+{
+ return format == AF_FORMAT_U8 || format == AF_FORMAT_U8P;
+}
+
+bool af_fmt_is_float(int format)
+{
+ format = af_fmt_from_planar(format);
+ return format == AF_FORMAT_FLOAT || format == AF_FORMAT_DOUBLE;
+}
+
+// true for both unsigned and signed ints
+bool af_fmt_is_int(int format)
+{
+ return format && !af_fmt_is_spdif(format) && !af_fmt_is_float(format);
+}
+
+bool af_fmt_is_spdif(int format)
+{
+ return af_format_sample_alignment(format) > 1;
+}
+
+bool af_fmt_is_pcm(int format)
+{
+ return af_fmt_is_valid(format) && !af_fmt_is_spdif(format);
+}
+
+static const int planar_formats[][2] = {
+ {AF_FORMAT_U8P, AF_FORMAT_U8},
+ {AF_FORMAT_S16P, AF_FORMAT_S16},
+ {AF_FORMAT_S32P, AF_FORMAT_S32},
+ {AF_FORMAT_S64P, AF_FORMAT_S64},
+ {AF_FORMAT_FLOATP, AF_FORMAT_FLOAT},
+ {AF_FORMAT_DOUBLEP, AF_FORMAT_DOUBLE},
+};
+
+bool af_fmt_is_planar(int format)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(planar_formats); n++) {
+ if (planar_formats[n][0] == format)
+ return true;
+ }
+ return false;
+}
+
+// Return the planar format corresponding to the given format.
+// If the format is already planar or if there's no equivalent,
+// return it.
+int af_fmt_to_planar(int format)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(planar_formats); n++) {
+ if (planar_formats[n][1] == format)
+ return planar_formats[n][0];
+ }
+ return format;
+}
+
+// Return the interleaved format corresponding to the given format.
+// If the format is already interleaved or if there's no equivalent,
+// return it.
+int af_fmt_from_planar(int format)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(planar_formats); n++) {
+ if (planar_formats[n][0] == format)
+ return planar_formats[n][1];
+ }
+ return format;
+}
+
+bool af_fmt_is_valid(int format)
+{
+ return format > 0 && format < AF_FORMAT_COUNT;
+}
+
+const char *af_fmt_to_str(int format)
+{
+ switch (format) {
+ case AF_FORMAT_U8: return "u8";
+ case AF_FORMAT_S16: return "s16";
+ case AF_FORMAT_S32: return "s32";
+ case AF_FORMAT_S64: return "s64";
+ case AF_FORMAT_FLOAT: return "float";
+ case AF_FORMAT_DOUBLE: return "double";
+ case AF_FORMAT_U8P: return "u8p";
+ case AF_FORMAT_S16P: return "s16p";
+ case AF_FORMAT_S32P: return "s32p";
+ case AF_FORMAT_S64P: return "s64p";
+ case AF_FORMAT_FLOATP: return "floatp";
+ case AF_FORMAT_DOUBLEP: return "doublep";
+ case AF_FORMAT_S_AAC: return "spdif-aac";
+ case AF_FORMAT_S_AC3: return "spdif-ac3";
+ case AF_FORMAT_S_DTS: return "spdif-dts";
+ case AF_FORMAT_S_DTSHD: return "spdif-dtshd";
+ case AF_FORMAT_S_EAC3: return "spdif-eac3";
+ case AF_FORMAT_S_MP3: return "spdif-mp3";
+ case AF_FORMAT_S_TRUEHD: return "spdif-truehd";
+ }
+ return "??";
+}
+
+void af_fill_silence(void *dst, size_t bytes, int format)
+{
+ memset(dst, af_fmt_is_unsigned(format) ? 0x80 : 0, bytes);
+}
+
+// Returns a "score" that serves as heuristic how lossy or hard a conversion is.
+// If the formats are equal, 1024 is returned. If they are gravely incompatible
+// (like s16<->ac3), INT_MIN is returned. If there is implied loss of precision
+// (like s16->s8), a value <0 is returned.
+int af_format_conversion_score(int dst_format, int src_format)
+{
+ if (dst_format == AF_FORMAT_UNKNOWN || src_format == AF_FORMAT_UNKNOWN)
+ return INT_MIN;
+ if (dst_format == src_format)
+ return 1024;
+ // Can't be normally converted
+ if (!af_fmt_is_pcm(dst_format) || !af_fmt_is_pcm(src_format))
+ return INT_MIN;
+ int score = 1024;
+ if (af_fmt_is_planar(dst_format) != af_fmt_is_planar(src_format))
+ score -= 1; // has to (de-)planarize
+ if (af_fmt_is_float(dst_format) != af_fmt_is_float(src_format)) {
+ int dst_bytes = af_fmt_to_bytes(dst_format);
+ if (af_fmt_is_float(dst_format)) {
+ // For int->float, consider a lower bound on the precision difference.
+ int bytes = (dst_bytes == 4 ? 3 : 6) - af_fmt_to_bytes(src_format);
+ if (bytes >= 0) {
+ score -= 8 * bytes; // excess precision
+ } else {
+ score += 1024 * (bytes - 1); // precision is lost (i.e. s32 -> float)
+ }
+ } else {
+ // float->int is the worst case. Penalize heavily and
+ // prefer highest bit depth int.
+ score -= 1048576 * (8 - dst_bytes);
+ }
+ score -= 512; // penalty for any float <-> int conversion
+ } else {
+ int bytes = af_fmt_to_bytes(dst_format) - af_fmt_to_bytes(src_format);
+ if (bytes > 0) {
+ score -= 8 * bytes; // has to add padding
+ } else if (bytes < 0) {
+ score += 1024 * (bytes - 1); // has to reduce bit depth
+ }
+ }
+ return score;
+}
+
+struct mp_entry {
+ int fmt;
+ int score;
+};
+
+static int cmp_entry(const void *a, const void *b)
+{
+#define CMP_INT(a, b) (a > b ? 1 : (a < b ? -1 : 0))
+ return -CMP_INT(((struct mp_entry *)a)->score, ((struct mp_entry *)b)->score);
+}
+
+// Return a list of sample format compatible to src_format, sorted by order
+// of preference. out_formats[0] will be src_format (as long as it's valid),
+// and the list is terminated with 0 (AF_FORMAT_UNKNOWN).
+// Keep in mind that this also returns formats with flipped interleaving
+// (e.g. for s16, it returns [s16, s16p, ...]).
+// out_formats must be an int[AF_FORMAT_COUNT + 1] array.
+void af_get_best_sample_formats(int src_format, int *out_formats)
+{
+ int num = 0;
+ struct mp_entry e[AF_FORMAT_COUNT + 1];
+ for (int fmt = 1; fmt < AF_FORMAT_COUNT; fmt++) {
+ int score = af_format_conversion_score(fmt, src_format);
+ if (score > INT_MIN)
+ e[num++] = (struct mp_entry){fmt, score};
+ }
+ qsort(e, num, sizeof(e[0]), cmp_entry);
+ for (int n = 0; n < num; n++)
+ out_formats[n] = e[n].fmt;
+ out_formats[num] = 0;
+}
+
+// Return the best match to src_samplerate from the list provided in the array
+// *available, which must be terminated by 0, or itself NULL. If *available is
+// empty or NULL, return a negative value. Exact match to src_samplerate is
+// most preferred, followed by the lowest integer multiple, followed by the
+// maximum of *available.
+int af_select_best_samplerate(int src_samplerate, const int *available)
+{
+ if (!available)
+ return -1;
+
+ int min_mult_rate = INT_MAX;
+ int max_rate = INT_MIN;
+ for (int i = 0; available[i]; i++) {
+ if (available[i] == src_samplerate)
+ return available[i];
+
+ if (!(available[i] % src_samplerate))
+ min_mult_rate = MPMIN(min_mult_rate, available[i]);
+
+ max_rate = MPMAX(max_rate, available[i]);
+ }
+
+ if (min_mult_rate < INT_MAX)
+ return min_mult_rate;
+
+ if (max_rate > INT_MIN)
+ return max_rate;
+
+ return -1;
+}
+
+// Return the number of samples that make up one frame in this format.
+// You get the byte size by multiplying them with sample size and channel count.
+int af_format_sample_alignment(int format)
+{
+ switch (format) {
+ case AF_FORMAT_S_AAC: return 16384 / 4;
+ case AF_FORMAT_S_AC3: return 6144 / 4;
+ case AF_FORMAT_S_DTSHD: return 32768 / 16;
+ case AF_FORMAT_S_DTS: return 2048 / 4;
+ case AF_FORMAT_S_EAC3: return 24576 / 4;
+ case AF_FORMAT_S_MP3: return 4608 / 4;
+ case AF_FORMAT_S_TRUEHD: return 61440 / 16;
+ default: return 1;
+ }
+}
diff --git a/audio/format.h b/audio/format.h
new file mode 100644
index 0000000..bdd4744
--- /dev/null
+++ b/audio/format.h
@@ -0,0 +1,77 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_AF_FORMAT_H
+#define MPLAYER_AF_FORMAT_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+enum af_format {
+ AF_FORMAT_UNKNOWN = 0,
+
+ AF_FORMAT_U8,
+ AF_FORMAT_S16,
+ AF_FORMAT_S32,
+ AF_FORMAT_S64,
+ AF_FORMAT_FLOAT,
+ AF_FORMAT_DOUBLE,
+
+ // Planar variants
+ AF_FORMAT_U8P,
+ AF_FORMAT_S16P,
+ AF_FORMAT_S32P,
+ AF_FORMAT_S64P,
+ AF_FORMAT_FLOATP,
+ AF_FORMAT_DOUBLEP,
+
+ // All of these use IEC61937 framing, and otherwise pretend to be like PCM.
+ AF_FORMAT_S_AAC,
+ AF_FORMAT_S_AC3,
+ AF_FORMAT_S_DTS,
+ AF_FORMAT_S_DTSHD,
+ AF_FORMAT_S_EAC3,
+ AF_FORMAT_S_MP3,
+ AF_FORMAT_S_TRUEHD,
+
+ AF_FORMAT_COUNT
+};
+
+const char *af_fmt_to_str(int format);
+
+int af_fmt_to_bytes(int format);
+
+bool af_fmt_is_valid(int format);
+bool af_fmt_is_unsigned(int format);
+bool af_fmt_is_float(int format);
+bool af_fmt_is_int(int format);
+bool af_fmt_is_planar(int format);
+bool af_fmt_is_spdif(int format);
+bool af_fmt_is_pcm(int format);
+
+int af_fmt_to_planar(int format);
+int af_fmt_from_planar(int format);
+
+void af_fill_silence(void *dst, size_t bytes, int format);
+
+void af_get_best_sample_formats(int src_format, int *out_formats);
+int af_format_conversion_score(int dst_format, int src_format);
+int af_select_best_samplerate(int src_sampelrate, const int *available);
+
+int af_format_sample_alignment(int format);
+
+#endif /* MPLAYER_AF_FORMAT_H */
diff --git a/audio/out/ao.c b/audio/out/ao.c
new file mode 100644
index 0000000..a5aa3a9
--- /dev/null
+++ b/audio/out/ao.c
@@ -0,0 +1,719 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+
+#include "options/options.h"
+#include "options/m_config_frontend.h"
+#include "osdep/endian.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "common/global.h"
+
+extern const struct ao_driver audio_out_oss;
+extern const struct ao_driver audio_out_audiotrack;
+extern const struct ao_driver audio_out_audiounit;
+extern const struct ao_driver audio_out_coreaudio;
+extern const struct ao_driver audio_out_coreaudio_exclusive;
+extern const struct ao_driver audio_out_rsound;
+extern const struct ao_driver audio_out_pipewire;
+extern const struct ao_driver audio_out_sndio;
+extern const struct ao_driver audio_out_pulse;
+extern const struct ao_driver audio_out_jack;
+extern const struct ao_driver audio_out_openal;
+extern const struct ao_driver audio_out_opensles;
+extern const struct ao_driver audio_out_null;
+extern const struct ao_driver audio_out_alsa;
+extern const struct ao_driver audio_out_wasapi;
+extern const struct ao_driver audio_out_pcm;
+extern const struct ao_driver audio_out_lavc;
+extern const struct ao_driver audio_out_sdl;
+
+static const struct ao_driver * const audio_out_drivers[] = {
+// native:
+#if HAVE_ANDROID
+ &audio_out_audiotrack,
+#endif
+#if HAVE_AUDIOUNIT
+ &audio_out_audiounit,
+#endif
+#if HAVE_COREAUDIO
+ &audio_out_coreaudio,
+#endif
+#if HAVE_PIPEWIRE
+ &audio_out_pipewire,
+#endif
+#if HAVE_PULSE
+ &audio_out_pulse,
+#endif
+#if HAVE_ALSA
+ &audio_out_alsa,
+#endif
+#if HAVE_WASAPI
+ &audio_out_wasapi,
+#endif
+#if HAVE_OSS_AUDIO
+ &audio_out_oss,
+#endif
+ // wrappers:
+#if HAVE_JACK
+ &audio_out_jack,
+#endif
+#if HAVE_OPENAL
+ &audio_out_openal,
+#endif
+#if HAVE_OPENSLES
+ &audio_out_opensles,
+#endif
+#if HAVE_SDL2_AUDIO
+ &audio_out_sdl,
+#endif
+#if HAVE_SNDIO
+ &audio_out_sndio,
+#endif
+ &audio_out_null,
+#if HAVE_COREAUDIO
+ &audio_out_coreaudio_exclusive,
+#endif
+ &audio_out_pcm,
+ &audio_out_lavc,
+};
+
+static bool get_desc(struct m_obj_desc *dst, int index)
+{
+ if (index >= MP_ARRAY_SIZE(audio_out_drivers))
+ return false;
+ const struct ao_driver *ao = audio_out_drivers[index];
+ *dst = (struct m_obj_desc) {
+ .name = ao->name,
+ .description = ao->description,
+ .priv_size = ao->priv_size,
+ .priv_defaults = ao->priv_defaults,
+ .options = ao->options,
+ .options_prefix = ao->options_prefix,
+ .global_opts = ao->global_opts,
+ .hidden = ao->encode,
+ .p = ao,
+ };
+ return true;
+}
+
+// For the ao option
+static const struct m_obj_list ao_obj_list = {
+ .get_desc = get_desc,
+ .description = "audio outputs",
+ .allow_trailer = true,
+ .disallow_positional_parameters = true,
+ .use_global_options = true,
+};
+
+#define OPT_BASE_STRUCT struct ao_opts
+const struct m_sub_options ao_conf = {
+ .opts = (const struct m_option[]) {
+ {"ao", OPT_SETTINGSLIST(audio_driver_list, &ao_obj_list),
+ .flags = UPDATE_AUDIO},
+ {"audio-device", OPT_STRING(audio_device), .flags = UPDATE_AUDIO},
+ {"audio-client-name", OPT_STRING(audio_client_name), .flags = UPDATE_AUDIO},
+ {"audio-buffer", OPT_DOUBLE(audio_buffer),
+ .flags = UPDATE_AUDIO, M_RANGE(0, 10)},
+ {0}
+ },
+ .size = sizeof(OPT_BASE_STRUCT),
+ .defaults = &(const OPT_BASE_STRUCT){
+ .audio_buffer = 0.2,
+ .audio_device = "auto",
+ .audio_client_name = "mpv",
+ },
+};
+
+static struct ao *ao_alloc(bool probing, struct mpv_global *global,
+ void (*wakeup_cb)(void *ctx), void *wakeup_ctx,
+ char *name)
+{
+ assert(wakeup_cb);
+
+ struct mp_log *log = mp_log_new(NULL, global->log, "ao");
+ struct m_obj_desc desc;
+ if (!m_obj_list_find(&desc, &ao_obj_list, bstr0(name))) {
+ mp_msg(log, MSGL_ERR, "Audio output %s not found!\n", name);
+ talloc_free(log);
+ return NULL;
+ };
+ struct ao_opts *opts = mp_get_config_group(NULL, global, &ao_conf);
+ struct ao *ao = talloc_ptrtype(NULL, ao);
+ talloc_steal(ao, log);
+ *ao = (struct ao) {
+ .driver = desc.p,
+ .probing = probing,
+ .global = global,
+ .wakeup_cb = wakeup_cb,
+ .wakeup_ctx = wakeup_ctx,
+ .log = mp_log_new(ao, log, name),
+ .def_buffer = opts->audio_buffer,
+ .client_name = talloc_strdup(ao, opts->audio_client_name),
+ };
+ talloc_free(opts);
+ ao->priv = m_config_group_from_desc(ao, ao->log, global, &desc, name);
+ if (!ao->priv)
+ goto error;
+ ao_set_gain(ao, 1.0f);
+ return ao;
+error:
+ talloc_free(ao);
+ return NULL;
+}
+
+static struct ao *ao_init(bool probing, struct mpv_global *global,
+ void (*wakeup_cb)(void *ctx), void *wakeup_ctx,
+ struct encode_lavc_context *encode_lavc_ctx, int flags,
+ int samplerate, int format, struct mp_chmap channels,
+ char *dev, char *name)
+{
+ struct ao *ao = ao_alloc(probing, global, wakeup_cb, wakeup_ctx, name);
+ if (!ao)
+ return NULL;
+ ao->samplerate = samplerate;
+ ao->channels = channels;
+ ao->format = format;
+ ao->encode_lavc_ctx = encode_lavc_ctx;
+ ao->init_flags = flags;
+ if (ao->driver->encode != !!ao->encode_lavc_ctx)
+ goto fail;
+
+ MP_VERBOSE(ao, "requested format: %d Hz, %s channels, %s\n",
+ ao->samplerate, mp_chmap_to_str(&ao->channels),
+ af_fmt_to_str(ao->format));
+
+ ao->device = talloc_strdup(ao, dev);
+ ao->stream_silence = flags & AO_INIT_STREAM_SILENCE;
+
+ init_buffer_pre(ao);
+
+ int r = ao->driver->init(ao);
+ if (r < 0) {
+ // Silly exception for coreaudio spdif redirection
+ if (ao->redirect) {
+ char redirect[80], rdevice[80];
+ snprintf(redirect, sizeof(redirect), "%s", ao->redirect);
+ snprintf(rdevice, sizeof(rdevice), "%s", ao->device ? ao->device : "");
+ ao_uninit(ao);
+ return ao_init(probing, global, wakeup_cb, wakeup_ctx,
+ encode_lavc_ctx, flags, samplerate, format, channels,
+ rdevice, redirect);
+ }
+ goto fail;
+ }
+ ao->driver_initialized = true;
+
+ ao->sstride = af_fmt_to_bytes(ao->format);
+ ao->num_planes = 1;
+ if (af_fmt_is_planar(ao->format)) {
+ ao->num_planes = ao->channels.num;
+ } else {
+ ao->sstride *= ao->channels.num;
+ }
+ ao->bps = ao->samplerate * ao->sstride;
+
+ if (ao->device_buffer <= 0 && ao->driver->write) {
+ MP_ERR(ao, "Device buffer size not set.\n");
+ goto fail;
+ }
+ if (ao->device_buffer)
+ MP_VERBOSE(ao, "device buffer: %d samples.\n", ao->device_buffer);
+ ao->buffer = MPMAX(ao->device_buffer, ao->def_buffer * ao->samplerate);
+ ao->buffer = MPMAX(ao->buffer, 1);
+
+ int align = af_format_sample_alignment(ao->format);
+ ao->buffer = (ao->buffer + align - 1) / align * align;
+ MP_VERBOSE(ao, "using soft-buffer of %d samples.\n", ao->buffer);
+
+ if (!init_buffer_post(ao))
+ goto fail;
+ return ao;
+
+fail:
+ ao_uninit(ao);
+ return NULL;
+}
+
+static void split_ao_device(void *tmp, char *opt, char **out_ao, char **out_dev)
+{
+ *out_ao = NULL;
+ *out_dev = NULL;
+ if (!opt)
+ return;
+ if (!opt[0] || strcmp(opt, "auto") == 0)
+ return;
+ // Split on "/". If "/" is the final character, or absent, out_dev is NULL.
+ bstr b_dev, b_ao;
+ bstr_split_tok(bstr0(opt), "/", &b_ao, &b_dev);
+ if (b_dev.len > 0)
+ *out_dev = bstrto0(tmp, b_dev);
+ *out_ao = bstrto0(tmp, b_ao);
+}
+
+struct ao *ao_init_best(struct mpv_global *global,
+ int init_flags,
+ void (*wakeup_cb)(void *ctx), void *wakeup_ctx,
+ struct encode_lavc_context *encode_lavc_ctx,
+ int samplerate, int format, struct mp_chmap channels)
+{
+ void *tmp = talloc_new(NULL);
+ struct ao_opts *opts = mp_get_config_group(tmp, global, &ao_conf);
+ struct mp_log *log = mp_log_new(tmp, global->log, "ao");
+ struct ao *ao = NULL;
+ struct m_obj_settings *ao_list = NULL;
+ int ao_num = 0;
+
+ for (int n = 0; opts->audio_driver_list && opts->audio_driver_list[n].name; n++)
+ MP_TARRAY_APPEND(tmp, ao_list, ao_num, opts->audio_driver_list[n]);
+
+ bool forced_dev = false;
+ char *pref_ao, *pref_dev;
+ split_ao_device(tmp, opts->audio_device, &pref_ao, &pref_dev);
+ if (!ao_num && pref_ao) {
+ // Reuse the autoselection code
+ MP_TARRAY_APPEND(tmp, ao_list, ao_num,
+ (struct m_obj_settings){.name = pref_ao});
+ forced_dev = true;
+ }
+
+ bool autoprobe = ao_num == 0;
+
+ // Something like "--ao=a,b," means do autoprobing after a and b fail.
+ if (ao_num && strlen(ao_list[ao_num - 1].name) == 0) {
+ ao_num -= 1;
+ autoprobe = true;
+ }
+
+ if (autoprobe) {
+ for (int n = 0; n < MP_ARRAY_SIZE(audio_out_drivers); n++) {
+ const struct ao_driver *driver = audio_out_drivers[n];
+ if (driver == &audio_out_null)
+ break;
+ MP_TARRAY_APPEND(tmp, ao_list, ao_num,
+ (struct m_obj_settings){.name = (char *)driver->name});
+ }
+ }
+
+ if (init_flags & AO_INIT_NULL_FALLBACK) {
+ MP_TARRAY_APPEND(tmp, ao_list, ao_num,
+ (struct m_obj_settings){.name = "null"});
+ }
+
+ for (int n = 0; n < ao_num; n++) {
+ struct m_obj_settings *entry = &ao_list[n];
+ bool probing = n + 1 != ao_num;
+ mp_verbose(log, "Trying audio driver '%s'\n", entry->name);
+ char *dev = NULL;
+ if (pref_ao && pref_dev && strcmp(entry->name, pref_ao) == 0) {
+ dev = pref_dev;
+ mp_verbose(log, "Using preferred device '%s'\n", dev);
+ }
+ ao = ao_init(probing, global, wakeup_cb, wakeup_ctx, encode_lavc_ctx,
+ init_flags, samplerate, format, channels, dev, entry->name);
+ if (ao)
+ break;
+ if (!probing)
+ mp_err(log, "Failed to initialize audio driver '%s'\n", entry->name);
+ if (dev && forced_dev) {
+ mp_err(log, "This audio driver/device was forced with the "
+ "--audio-device option.\nTry unsetting it.\n");
+ }
+ }
+
+ talloc_free(tmp);
+ return ao;
+}
+
+// Query the AO_EVENT_*s as requested by the events parameter, and return them.
+int ao_query_and_reset_events(struct ao *ao, int events)
+{
+ return atomic_fetch_and(&ao->events_, ~(unsigned)events) & events;
+}
+
+// Returns events that were set by this calls.
+int ao_add_events(struct ao *ao, int events)
+{
+ unsigned prev_events = atomic_fetch_or(&ao->events_, events);
+ unsigned new = events & ~prev_events;
+ if (new)
+ ao->wakeup_cb(ao->wakeup_ctx);
+ return new;
+}
+
+// Request that the player core destroys and recreates the AO. Fully thread-safe.
+void ao_request_reload(struct ao *ao)
+{
+ ao_add_events(ao, AO_EVENT_RELOAD);
+}
+
+// Notify the player that the device list changed. Fully thread-safe.
+void ao_hotplug_event(struct ao *ao)
+{
+ ao_add_events(ao, AO_EVENT_HOTPLUG);
+}
+
+bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s,
+ struct mp_chmap *map)
+{
+ MP_VERBOSE(ao, "Channel layouts:\n");
+ mp_chmal_sel_log(s, ao->log, MSGL_V);
+ bool r = mp_chmap_sel_adjust(s, map);
+ if (r)
+ MP_VERBOSE(ao, "result: %s\n", mp_chmap_to_str(map));
+ return r;
+}
+
+// safe_multichannel=true behaves like ao_chmap_sel_adjust.
+// safe_multichannel=false is a helper for callers which do not support safe
+// handling of arbitrary channel layouts. If the multichannel layouts are not
+// considered "always safe" (e.g. HDMI), then allow only stereo or mono, if
+// they are part of the list in *s.
+bool ao_chmap_sel_adjust2(struct ao *ao, const struct mp_chmap_sel *s,
+ struct mp_chmap *map, bool safe_multichannel)
+{
+ if (!safe_multichannel && (ao->init_flags & AO_INIT_SAFE_MULTICHANNEL_ONLY)) {
+ struct mp_chmap res = *map;
+ if (mp_chmap_sel_adjust(s, &res)) {
+ if (!mp_chmap_equals(&res, &(struct mp_chmap)MP_CHMAP_INIT_MONO) &&
+ !mp_chmap_equals(&res, &(struct mp_chmap)MP_CHMAP_INIT_STEREO))
+ {
+ MP_VERBOSE(ao, "Disabling multichannel output.\n");
+ *map = (struct mp_chmap)MP_CHMAP_INIT_STEREO;
+ }
+ }
+ }
+
+ return ao_chmap_sel_adjust(ao, s, map);
+}
+
+bool ao_chmap_sel_get_def(struct ao *ao, const struct mp_chmap_sel *s,
+ struct mp_chmap *map, int num)
+{
+ return mp_chmap_sel_get_def(s, map, num);
+}
+
+// --- The following functions just return immutable information.
+
+void ao_get_format(struct ao *ao,
+ int *samplerate, int *format, struct mp_chmap *channels)
+{
+ *samplerate = ao->samplerate;
+ *format = ao->format;
+ *channels = ao->channels;
+}
+
+const char *ao_get_name(struct ao *ao)
+{
+ return ao->driver->name;
+}
+
+const char *ao_get_description(struct ao *ao)
+{
+ return ao->driver->description;
+}
+
+bool ao_untimed(struct ao *ao)
+{
+ return ao->untimed;
+}
+
+// ---
+
+struct ao_hotplug {
+ struct mpv_global *global;
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_ctx;
+ // A single AO instance is used to listen to hotplug events. It wouldn't
+ // make much sense to allow multiple AO drivers; all sane platforms have
+ // a single audio API providing all events.
+ // This is _not_ necessarily the same AO instance as used for playing
+ // audio.
+ struct ao *ao;
+ // cached
+ struct ao_device_list *list;
+ bool needs_update;
+};
+
+struct ao_hotplug *ao_hotplug_create(struct mpv_global *global,
+ void (*wakeup_cb)(void *ctx),
+ void *wakeup_ctx)
+{
+ struct ao_hotplug *hp = talloc_ptrtype(NULL, hp);
+ *hp = (struct ao_hotplug){
+ .global = global,
+ .wakeup_cb = wakeup_cb,
+ .wakeup_ctx = wakeup_ctx,
+ .needs_update = true,
+ };
+ return hp;
+}
+
+static void get_devices(struct ao *ao, struct ao_device_list *list)
+{
+ if (ao->driver->list_devs) {
+ ao->driver->list_devs(ao, list);
+ } else {
+ ao_device_list_add(list, ao, &(struct ao_device_desc){"", ""});
+ }
+}
+
+bool ao_hotplug_check_update(struct ao_hotplug *hp)
+{
+ if (hp->ao && ao_query_and_reset_events(hp->ao, AO_EVENT_HOTPLUG)) {
+ hp->needs_update = true;
+ return true;
+ }
+ return false;
+}
+
+// The return value is valid until the next call to this API.
+struct ao_device_list *ao_hotplug_get_device_list(struct ao_hotplug *hp,
+ struct ao *playback_ao)
+{
+ if (hp->list && !hp->needs_update)
+ return hp->list;
+
+ talloc_free(hp->list);
+ struct ao_device_list *list = talloc_zero(hp, struct ao_device_list);
+ hp->list = list;
+
+ MP_TARRAY_APPEND(list, list->devices, list->num_devices,
+ (struct ao_device_desc){"auto", "Autoselect device"});
+
+ // Try to use the same AO for hotplug handling as for playback.
+ // Different AOs may not agree and the playback one is the only one the
+ // user knows about and may even have configured explicitly.
+ if (!hp->ao && playback_ao && playback_ao->driver->hotplug_init) {
+ struct ao *ao = ao_alloc(true, hp->global, hp->wakeup_cb, hp->wakeup_ctx,
+ (char *)playback_ao->driver->name);
+ if (playback_ao->driver->hotplug_init(ao) >= 0) {
+ hp->ao = ao;
+ } else {
+ talloc_free(ao);
+ }
+ }
+
+ for (int n = 0; n < MP_ARRAY_SIZE(audio_out_drivers); n++) {
+ const struct ao_driver *d = audio_out_drivers[n];
+ if (d == &audio_out_null)
+ break; // don't add unsafe/special entries
+
+ struct ao *ao = ao_alloc(true, hp->global, hp->wakeup_cb, hp->wakeup_ctx,
+ (char *)d->name);
+ if (!ao)
+ continue;
+
+ if (ao->driver->hotplug_init) {
+ if (ao->driver->hotplug_init(ao) >= 0) {
+ get_devices(ao, list);
+ if (hp->ao)
+ ao->driver->hotplug_uninit(ao);
+ else
+ hp->ao = ao; // keep this one
+ }
+ } else {
+ get_devices(ao, list);
+ }
+ if (ao != hp->ao)
+ talloc_free(ao);
+ }
+ hp->needs_update = false;
+ return list;
+}
+
+void ao_device_list_add(struct ao_device_list *list, struct ao *ao,
+ struct ao_device_desc *e)
+{
+ struct ao_device_desc c = *e;
+ const char *dname = ao->driver->name;
+ char buf[80];
+ if (!c.desc || !c.desc[0]) {
+ if (c.name && c.name[0]) {
+ c.desc = c.name;
+ } else if (list->num_devices) {
+ // Assume this is the default device.
+ snprintf(buf, sizeof(buf), "Default (%s)", dname);
+ c.desc = buf;
+ } else {
+ // First default device (and maybe the only one).
+ c.desc = "Default";
+ }
+ }
+ c.name = (c.name && c.name[0]) ? talloc_asprintf(list, "%s/%s", dname, c.name)
+ : talloc_strdup(list, dname);
+ c.desc = talloc_strdup(list, c.desc);
+ MP_TARRAY_APPEND(list, list->devices, list->num_devices, c);
+}
+
+void ao_hotplug_destroy(struct ao_hotplug *hp)
+{
+ if (!hp)
+ return;
+ if (hp->ao && hp->ao->driver->hotplug_uninit)
+ hp->ao->driver->hotplug_uninit(hp->ao);
+ talloc_free(hp->ao);
+ talloc_free(hp);
+}
+
+static void dummy_wakeup(void *ctx)
+{
+}
+
+void ao_print_devices(struct mpv_global *global, struct mp_log *log,
+ struct ao *playback_ao)
+{
+ struct ao_hotplug *hp = ao_hotplug_create(global, dummy_wakeup, NULL);
+ struct ao_device_list *list = ao_hotplug_get_device_list(hp, playback_ao);
+ mp_info(log, "List of detected audio devices:\n");
+ for (int n = 0; n < list->num_devices; n++) {
+ struct ao_device_desc *desc = &list->devices[n];
+ mp_info(log, " '%s' (%s)\n", desc->name, desc->desc);
+ }
+ ao_hotplug_destroy(hp);
+}
+
+void ao_set_gain(struct ao *ao, float gain)
+{
+ atomic_store(&ao->gain, gain);
+}
+
+#define MUL_GAIN_i(d, num_samples, gain, low, center, high) \
+ for (int n = 0; n < (num_samples); n++) \
+ (d)[n] = MPCLAMP( \
+ ((((int64_t)((d)[n]) - (center)) * (gain) + 128) >> 8) + (center), \
+ (low), (high))
+
+#define MUL_GAIN_f(d, num_samples, gain) \
+ for (int n = 0; n < (num_samples); n++) \
+ (d)[n] = MPCLAMP(((d)[n]) * (gain), -1.0, 1.0)
+
+static void process_plane(struct ao *ao, void *data, int num_samples)
+{
+ float gain = atomic_load_explicit(&ao->gain, memory_order_relaxed);
+ int gi = lrint(256.0 * gain);
+ if (gi == 256)
+ return;
+ switch (af_fmt_from_planar(ao->format)) {
+ case AF_FORMAT_U8:
+ MUL_GAIN_i((uint8_t *)data, num_samples, gi, 0, 128, 255);
+ break;
+ case AF_FORMAT_S16:
+ MUL_GAIN_i((int16_t *)data, num_samples, gi, INT16_MIN, 0, INT16_MAX);
+ break;
+ case AF_FORMAT_S32:
+ MUL_GAIN_i((int32_t *)data, num_samples, gi, INT32_MIN, 0, INT32_MAX);
+ break;
+ case AF_FORMAT_FLOAT:
+ MUL_GAIN_f((float *)data, num_samples, gain);
+ break;
+ case AF_FORMAT_DOUBLE:
+ MUL_GAIN_f((double *)data, num_samples, gain);
+ break;
+ default:;
+ // all other sample formats are simply not supported
+ }
+}
+
+void ao_post_process_data(struct ao *ao, void **data, int num_samples)
+{
+ bool planar = af_fmt_is_planar(ao->format);
+ int planes = planar ? ao->channels.num : 1;
+ int plane_samples = num_samples * (planar ? 1: ao->channels.num);
+ for (int n = 0; n < planes; n++)
+ process_plane(ao, data[n], plane_samples);
+}
+
+static int get_conv_type(struct ao_convert_fmt *fmt)
+{
+ if (af_fmt_to_bytes(fmt->src_fmt) * 8 == fmt->dst_bits && !fmt->pad_msb)
+ return 0; // passthrough
+ if (fmt->src_fmt == AF_FORMAT_S32 && fmt->dst_bits == 24 && !fmt->pad_msb)
+ return 1; // simple 32->24 bit conversion
+ if (fmt->src_fmt == AF_FORMAT_S32 && fmt->dst_bits == 32 && fmt->pad_msb == 8)
+ return 2; // simple 32->24 bit conversion, with MSB padding
+ return -1; // unsupported
+}
+
+// Check whether ao_convert_inplace() can be called. As an exception, the
+// planar-ness of the sample format and the number of channels is ignored.
+// All other parameters must be as passed to ao_convert_inplace().
+bool ao_can_convert_inplace(struct ao_convert_fmt *fmt)
+{
+ return get_conv_type(fmt) >= 0;
+}
+
+bool ao_need_conversion(struct ao_convert_fmt *fmt)
+{
+ return get_conv_type(fmt) != 0;
+}
+
+// The LSB is always ignored.
+#if BYTE_ORDER == BIG_ENDIAN
+#define SHIFT24(x) ((3-(x))*8)
+#else
+#define SHIFT24(x) (((x)+1)*8)
+#endif
+
+static void convert_plane(int type, void *data, int num_samples)
+{
+ switch (type) {
+ case 0:
+ break;
+ case 1: /* fall through */
+ case 2: {
+ int bytes = type == 1 ? 3 : 4;
+ for (int s = 0; s < num_samples; s++) {
+ uint32_t val = *((uint32_t *)data + s);
+ uint8_t *ptr = (uint8_t *)data + s * bytes;
+ ptr[0] = val >> SHIFT24(0);
+ ptr[1] = val >> SHIFT24(1);
+ ptr[2] = val >> SHIFT24(2);
+ if (type == 2)
+ ptr[3] = 0;
+ }
+ break;
+ }
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+}
+
+// data[n] contains the pointer to the first sample of the n-th plane, in the
+// format implied by fmt->src_fmt. src_fmt also controls whether the data is
+// all in one plane, or if there is a plane per channel.
+void ao_convert_inplace(struct ao_convert_fmt *fmt, void **data, int num_samples)
+{
+ int type = get_conv_type(fmt);
+ bool planar = af_fmt_is_planar(fmt->src_fmt);
+ int planes = planar ? fmt->channels : 1;
+ int plane_samples = num_samples * (planar ? 1: fmt->channels);
+ for (int n = 0; n < planes; n++)
+ convert_plane(type, data[n], plane_samples);
+}
diff --git a/audio/out/ao.h b/audio/out/ao.h
new file mode 100644
index 0000000..18c7cdc
--- /dev/null
+++ b/audio/out/ao.h
@@ -0,0 +1,122 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_AUDIO_OUT_H
+#define MPLAYER_AUDIO_OUT_H
+
+#include <stdbool.h>
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "audio/chmap.h"
+#include "audio/chmap_sel.h"
+
+enum aocontrol {
+ // _VOLUME commands take a pointer to float for input/output.
+ AOCONTROL_GET_VOLUME,
+ AOCONTROL_SET_VOLUME,
+ // _MUTE commands take a pointer to bool
+ AOCONTROL_GET_MUTE,
+ AOCONTROL_SET_MUTE,
+ // Has char* as argument, which contains the desired stream title.
+ AOCONTROL_UPDATE_STREAM_TITLE,
+};
+
+// If set, then the queued audio data is the last. Note that after a while, new
+// data might be written again, instead of closing the AO.
+#define PLAYER_FINAL_CHUNK 1
+
+enum {
+ AO_EVENT_RELOAD = 1,
+ AO_EVENT_HOTPLUG = 2,
+ AO_EVENT_INITIAL_UNBLOCK = 4,
+};
+
+enum {
+ // Allow falling back to ao_null if nothing else works.
+ AO_INIT_NULL_FALLBACK = 1 << 0,
+ // Only accept multichannel configurations that are guaranteed to work
+ // (i.e. not sending arbitrary layouts over HDMI).
+ AO_INIT_SAFE_MULTICHANNEL_ONLY = 1 << 1,
+ // Stream silence as long as no audio is playing.
+ AO_INIT_STREAM_SILENCE = 1 << 2,
+ // Force exclusive mode, i.e. lock out the system mixer.
+ AO_INIT_EXCLUSIVE = 1 << 3,
+ // Initialize with music role.
+ AO_INIT_MEDIA_ROLE_MUSIC = 1 << 4,
+};
+
+struct ao_device_desc {
+ const char *name; // symbolic name; will be set on ao->device
+ const char *desc; // verbose human readable name
+};
+
+struct ao_device_list {
+ struct ao_device_desc *devices;
+ int num_devices;
+};
+
+struct ao;
+struct mpv_global;
+struct input_ctx;
+struct encode_lavc_context;
+
+struct ao_opts {
+ struct m_obj_settings *audio_driver_list;
+ char *audio_device;
+ char *audio_client_name;
+ double audio_buffer;
+};
+
+struct ao *ao_init_best(struct mpv_global *global,
+ int init_flags,
+ void (*wakeup_cb)(void *ctx), void *wakeup_ctx,
+ struct encode_lavc_context *encode_lavc_ctx,
+ int samplerate, int format, struct mp_chmap channels);
+void ao_uninit(struct ao *ao);
+void ao_get_format(struct ao *ao,
+ int *samplerate, int *format, struct mp_chmap *channels);
+const char *ao_get_name(struct ao *ao);
+const char *ao_get_description(struct ao *ao);
+bool ao_untimed(struct ao *ao);
+int ao_control(struct ao *ao, enum aocontrol cmd, void *arg);
+void ao_set_gain(struct ao *ao, float gain);
+double ao_get_delay(struct ao *ao);
+void ao_reset(struct ao *ao);
+void ao_start(struct ao *ao);
+void ao_set_paused(struct ao *ao, bool paused, bool eof);
+void ao_drain(struct ao *ao);
+bool ao_is_playing(struct ao *ao);
+struct mp_async_queue;
+struct mp_async_queue *ao_get_queue(struct ao *ao);
+int ao_query_and_reset_events(struct ao *ao, int events);
+int ao_add_events(struct ao *ao, int events);
+void ao_unblock(struct ao *ao);
+void ao_request_reload(struct ao *ao);
+void ao_hotplug_event(struct ao *ao);
+
+struct ao_hotplug;
+struct ao_hotplug *ao_hotplug_create(struct mpv_global *global,
+ void (*wakeup_cb)(void *ctx),
+ void *wakeup_ctx);
+void ao_hotplug_destroy(struct ao_hotplug *hp);
+bool ao_hotplug_check_update(struct ao_hotplug *hp);
+struct ao_device_list *ao_hotplug_get_device_list(struct ao_hotplug *hp, struct ao *playback_ao);
+
+void ao_print_devices(struct mpv_global *global, struct mp_log *log, struct ao *playback_ao);
+
+#endif /* MPLAYER_AUDIO_OUT_H */
diff --git a/audio/out/ao_alsa.c b/audio/out/ao_alsa.c
new file mode 100644
index 0000000..75eda3b
--- /dev/null
+++ b/audio/out/ao_alsa.c
@@ -0,0 +1,1161 @@
+/*
+ * ALSA 0.9.x-1.x audio output driver
+ *
+ * Copyright (C) 2004 Alex Beregszaszi
+ * Zsolt Barat <joy@streamminister.de>
+ *
+ * modified for real ALSA 0.9.0 support by Zsolt Barat <joy@streamminister.de>
+ * additional AC-3 passthrough support by Andy Lo A Foe <andy@alsaplayer.org>
+ * 08/22/2002 iec958-init rewritten and merged with common init, zsolt
+ * 04/13/2004 merged with ao_alsa1.x, fixes provided by Jindrich Makovicka
+ * 04/25/2004 printfs converted to mp_msg, Zsolt.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <sys/time.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <math.h>
+#include <string.h>
+
+#include "options/options.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "common/msg.h"
+#include "osdep/endian.h"
+
+#include <alsa/asoundlib.h>
+
+#if defined(SND_CHMAP_API_VERSION) && SND_CHMAP_API_VERSION >= (1 << 16)
+#define HAVE_CHMAP_API 1
+#else
+#define HAVE_CHMAP_API 0
+#endif
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+
+struct ao_alsa_opts {
+ char *mixer_device;
+ char *mixer_name;
+ int mixer_index;
+ bool resample;
+ bool ni;
+ bool ignore_chmap;
+ int buffer_time;
+ int frags;
+};
+
+#define OPT_BASE_STRUCT struct ao_alsa_opts
+static const struct m_sub_options ao_alsa_conf = {
+ .opts = (const struct m_option[]) {
+ {"alsa-resample", OPT_BOOL(resample)},
+ {"alsa-mixer-device", OPT_STRING(mixer_device)},
+ {"alsa-mixer-name", OPT_STRING(mixer_name)},
+ {"alsa-mixer-index", OPT_INT(mixer_index), M_RANGE(0, 99)},
+ {"alsa-non-interleaved", OPT_BOOL(ni)},
+ {"alsa-ignore-chmap", OPT_BOOL(ignore_chmap)},
+ {"alsa-buffer-time", OPT_INT(buffer_time), M_RANGE(0, INT_MAX)},
+ {"alsa-periods", OPT_INT(frags), M_RANGE(0, INT_MAX)},
+ {0}
+ },
+ .defaults = &(const struct ao_alsa_opts) {
+ .mixer_device = "default",
+ .mixer_name = "Master",
+ .buffer_time = 100000,
+ .frags = 4,
+ },
+ .size = sizeof(struct ao_alsa_opts),
+};
+
+struct priv {
+ snd_pcm_t *alsa;
+ bool device_lost;
+ snd_pcm_format_t alsa_fmt;
+ bool can_pause;
+ snd_pcm_uframes_t buffersize;
+ snd_pcm_uframes_t outburst;
+
+ snd_output_t *output;
+
+ struct ao_convert_fmt convert;
+
+ struct ao_alsa_opts *opts;
+};
+
+#define CHECK_ALSA_ERROR(message) \
+ do { \
+ if (err < 0) { \
+ MP_ERR(ao, "%s: %s\n", (message), snd_strerror(err)); \
+ goto alsa_error; \
+ } \
+ } while (0)
+
+#define CHECK_ALSA_WARN(message) \
+ do { \
+ if (err < 0) \
+ MP_WARN(ao, "%s: %s\n", (message), snd_strerror(err)); \
+ } while (0)
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct priv *p = ao->priv;
+ snd_mixer_t *handle = NULL;
+ switch (cmd) {
+ case AOCONTROL_GET_MUTE:
+ case AOCONTROL_SET_MUTE:
+ case AOCONTROL_GET_VOLUME:
+ case AOCONTROL_SET_VOLUME:
+ {
+ int err;
+ snd_mixer_elem_t *elem;
+ snd_mixer_selem_id_t *sid;
+
+ long pmin, pmax;
+ long get_vol, set_vol;
+ float f_multi;
+
+ if (!af_fmt_is_pcm(ao->format))
+ return CONTROL_FALSE;
+
+ snd_mixer_selem_id_alloca(&sid);
+
+ snd_mixer_selem_id_set_index(sid, p->opts->mixer_index);
+ snd_mixer_selem_id_set_name(sid, p->opts->mixer_name);
+
+ err = snd_mixer_open(&handle, 0);
+ CHECK_ALSA_ERROR("Mixer open error");
+
+ err = snd_mixer_attach(handle, p->opts->mixer_device);
+ CHECK_ALSA_ERROR("Mixer attach error");
+
+ err = snd_mixer_selem_register(handle, NULL, NULL);
+ CHECK_ALSA_ERROR("Mixer register error");
+
+ err = snd_mixer_load(handle);
+ CHECK_ALSA_ERROR("Mixer load error");
+
+ elem = snd_mixer_find_selem(handle, sid);
+ if (!elem) {
+ MP_VERBOSE(ao, "Unable to find simple control '%s',%i.\n",
+ snd_mixer_selem_id_get_name(sid),
+ snd_mixer_selem_id_get_index(sid));
+ goto alsa_error;
+ }
+
+ snd_mixer_selem_get_playback_volume_range(elem, &pmin, &pmax);
+ f_multi = (100 / (float)(pmax - pmin));
+
+ switch (cmd) {
+ case AOCONTROL_SET_VOLUME: {
+ float *vol = arg;
+ set_vol = *vol / f_multi + pmin + 0.5;
+
+ err = snd_mixer_selem_set_playback_volume(elem, 0, set_vol);
+ CHECK_ALSA_ERROR("Error setting left channel");
+ MP_DBG(ao, "left=%li, ", set_vol);
+
+ err = snd_mixer_selem_set_playback_volume(elem, 1, set_vol);
+ CHECK_ALSA_ERROR("Error setting right channel");
+ MP_DBG(ao, "right=%li, pmin=%li, pmax=%li, mult=%f\n",
+ set_vol, pmin, pmax, f_multi);
+ break;
+ }
+ case AOCONTROL_GET_VOLUME: {
+ float *vol = arg;
+ float left, right;
+ snd_mixer_selem_get_playback_volume(elem, 0, &get_vol);
+ left = (get_vol - pmin) * f_multi;
+ snd_mixer_selem_get_playback_volume(elem, 1, &get_vol);
+ right = (get_vol - pmin) * f_multi;
+ *vol = (left + right) / 2.0;
+ MP_DBG(ao, "vol=%f\n", *vol);
+ break;
+ }
+ case AOCONTROL_SET_MUTE: {
+ bool *mute = arg;
+ if (!snd_mixer_selem_has_playback_switch(elem))
+ goto alsa_error;
+ if (!snd_mixer_selem_has_playback_switch_joined(elem)) {
+ snd_mixer_selem_set_playback_switch(elem, 1, !*mute);
+ }
+ snd_mixer_selem_set_playback_switch(elem, 0, !*mute);
+ break;
+ }
+ case AOCONTROL_GET_MUTE: {
+ bool *mute = arg;
+ if (!snd_mixer_selem_has_playback_switch(elem))
+ goto alsa_error;
+ int tmp = 1;
+ snd_mixer_selem_get_playback_switch(elem, 0, &tmp);
+ *mute = !tmp;
+ if (!snd_mixer_selem_has_playback_switch_joined(elem)) {
+ snd_mixer_selem_get_playback_switch(elem, 1, &tmp);
+ *mute &= !tmp;
+ }
+ break;
+ }
+ }
+ snd_mixer_close(handle);
+ return CONTROL_OK;
+ }
+
+ } //end switch
+ return CONTROL_UNKNOWN;
+
+alsa_error:
+ if (handle)
+ snd_mixer_close(handle);
+ return CONTROL_ERROR;
+}
+
+struct alsa_fmt {
+ int mp_format;
+ int alsa_format;
+ int bits; // alsa format full sample size (optional)
+ int pad_msb; // how many MSB bits are 0 (optional)
+};
+
+// Entries that have the same mp_format must be:
+// 1. consecutive
+// 2. sorted by preferred format (worst comes last)
+static const struct alsa_fmt mp_alsa_formats[] = {
+ {AF_FORMAT_U8, SND_PCM_FORMAT_U8},
+ {AF_FORMAT_S16, SND_PCM_FORMAT_S16},
+ {AF_FORMAT_S32, SND_PCM_FORMAT_S32},
+ {AF_FORMAT_S32, SND_PCM_FORMAT_S24, .bits = 32, .pad_msb = 8},
+ {AF_FORMAT_S32,
+ MP_SELECT_LE_BE(SND_PCM_FORMAT_S24_3LE, SND_PCM_FORMAT_S24_3BE),
+ .bits = 24, .pad_msb = 0},
+ {AF_FORMAT_FLOAT, SND_PCM_FORMAT_FLOAT},
+ {AF_FORMAT_DOUBLE, SND_PCM_FORMAT_FLOAT64},
+ {0},
+};
+
+static const struct alsa_fmt *find_alsa_format(int mp_format)
+{
+ for (int n = 0; mp_alsa_formats[n].mp_format; n++) {
+ if (mp_alsa_formats[n].mp_format == mp_format)
+ return &mp_alsa_formats[n];
+ }
+ return NULL;
+}
+
+#if HAVE_CHMAP_API
+
+static const int alsa_to_mp_channels[][2] = {
+ {SND_CHMAP_FL, MP_SP(FL)},
+ {SND_CHMAP_FR, MP_SP(FR)},
+ {SND_CHMAP_RL, MP_SP(BL)},
+ {SND_CHMAP_RR, MP_SP(BR)},
+ {SND_CHMAP_FC, MP_SP(FC)},
+ {SND_CHMAP_LFE, MP_SP(LFE)},
+ {SND_CHMAP_SL, MP_SP(SL)},
+ {SND_CHMAP_SR, MP_SP(SR)},
+ {SND_CHMAP_RC, MP_SP(BC)},
+ {SND_CHMAP_FLC, MP_SP(FLC)},
+ {SND_CHMAP_FRC, MP_SP(FRC)},
+ {SND_CHMAP_FLW, MP_SP(WL)},
+ {SND_CHMAP_FRW, MP_SP(WR)},
+ {SND_CHMAP_TC, MP_SP(TC)},
+ {SND_CHMAP_TFL, MP_SP(TFL)},
+ {SND_CHMAP_TFR, MP_SP(TFR)},
+ {SND_CHMAP_TFC, MP_SP(TFC)},
+ {SND_CHMAP_TRL, MP_SP(TBL)},
+ {SND_CHMAP_TRR, MP_SP(TBR)},
+ {SND_CHMAP_TRC, MP_SP(TBC)},
+ {SND_CHMAP_RRC, MP_SP(SDR)},
+ {SND_CHMAP_RLC, MP_SP(SDL)},
+ {SND_CHMAP_MONO, MP_SP(FC)},
+ {SND_CHMAP_NA, MP_SPEAKER_ID_NA},
+ {SND_CHMAP_UNKNOWN, MP_SPEAKER_ID_NA},
+ {SND_CHMAP_LAST, MP_SPEAKER_ID_COUNT}
+};
+
+static int find_mp_channel(int alsa_channel)
+{
+ for (int i = 0; alsa_to_mp_channels[i][1] != MP_SPEAKER_ID_COUNT; i++) {
+ if (alsa_to_mp_channels[i][0] == alsa_channel)
+ return alsa_to_mp_channels[i][1];
+ }
+
+ return MP_SPEAKER_ID_COUNT;
+}
+
+#define CHMAP(n, ...) &(struct mp_chmap) MP_CONCAT(MP_CHMAP, n) (__VA_ARGS__)
+
+// Replace each channel in a with b (a->num == b->num)
+static void replace_submap(struct mp_chmap *dst, struct mp_chmap *a,
+ struct mp_chmap *b)
+{
+ struct mp_chmap t = *dst;
+ if (!mp_chmap_is_valid(&t) || mp_chmap_diffn(a, &t) != 0)
+ return;
+ assert(a->num == b->num);
+ for (int n = 0; n < t.num; n++) {
+ for (int i = 0; i < a->num; i++) {
+ if (t.speaker[n] == a->speaker[i]) {
+ t.speaker[n] = b->speaker[i];
+ break;
+ }
+ }
+ }
+ if (mp_chmap_is_valid(&t))
+ *dst = t;
+}
+
+static bool mp_chmap_from_alsa(struct mp_chmap *dst, snd_pcm_chmap_t *src)
+{
+ *dst = (struct mp_chmap) {0};
+
+ if (src->channels > MP_NUM_CHANNELS)
+ return false;
+
+ dst->num = src->channels;
+ for (int c = 0; c < dst->num; c++)
+ dst->speaker[c] = find_mp_channel(src->pos[c]);
+
+ // Assume anything with 1 channel is mono.
+ if (dst->num == 1)
+ dst->speaker[0] = MP_SP(FC);
+
+ // Remap weird Intel HDA HDMI 7.1 layouts correctly.
+ replace_submap(dst, CHMAP(6, FL, FR, BL, BR, SDL, SDR),
+ CHMAP(6, FL, FR, SL, SR, BL, BR));
+
+ return mp_chmap_is_valid(dst);
+}
+
+static bool query_chmaps(struct ao *ao, struct mp_chmap *chmap)
+{
+ struct priv *p = ao->priv;
+ struct mp_chmap_sel chmap_sel = {.tmp = p};
+
+ snd_pcm_chmap_query_t **maps = snd_pcm_query_chmaps(p->alsa);
+ if (!maps) {
+ MP_VERBOSE(ao, "snd_pcm_query_chmaps() returned NULL\n");
+ return false;
+ }
+
+ for (int i = 0; maps[i] != NULL; i++) {
+ char aname[128];
+ if (snd_pcm_chmap_print(&maps[i]->map, sizeof(aname), aname) <= 0)
+ aname[0] = '\0';
+
+ struct mp_chmap entry;
+ if (mp_chmap_from_alsa(&entry, &maps[i]->map)) {
+ struct mp_chmap reorder = entry;
+ mp_chmap_reorder_norm(&reorder);
+
+ MP_DBG(ao, "got ALSA chmap: %s (%s) -> %s", aname,
+ snd_pcm_chmap_type_name(maps[i]->type),
+ mp_chmap_to_str(&entry));
+ if (!mp_chmap_equals(&entry, &reorder))
+ MP_DBG(ao, " -> %s", mp_chmap_to_str(&reorder));
+ MP_DBG(ao, "\n");
+
+ struct mp_chmap final =
+ maps[i]->type == SND_CHMAP_TYPE_VAR ? reorder : entry;
+ mp_chmap_sel_add_map(&chmap_sel, &final);
+ } else {
+ MP_VERBOSE(ao, "skipping unknown ALSA channel map: %s\n", aname);
+ }
+ }
+
+ snd_pcm_free_chmaps(maps);
+
+ return ao_chmap_sel_adjust2(ao, &chmap_sel, chmap, false);
+}
+
+// Map back our selected channel layout to an ALSA one. This is done this way so
+// that our ALSA->mp_chmap mapping function only has to go one way.
+// The return value is to be freed with free().
+static snd_pcm_chmap_t *map_back_chmap(struct ao *ao, struct mp_chmap *chmap)
+{
+ struct priv *p = ao->priv;
+ if (!mp_chmap_is_valid(chmap))
+ return NULL;
+
+ snd_pcm_chmap_query_t **maps = snd_pcm_query_chmaps(p->alsa);
+ if (!maps)
+ return NULL;
+
+ snd_pcm_chmap_t *alsa_chmap = NULL;
+
+ for (int i = 0; maps[i] != NULL; i++) {
+ struct mp_chmap entry;
+ if (!mp_chmap_from_alsa(&entry, &maps[i]->map))
+ continue;
+
+ if (mp_chmap_equals(chmap, &entry) ||
+ (mp_chmap_equals_reordered(chmap, &entry) &&
+ maps[i]->type == SND_CHMAP_TYPE_VAR))
+ {
+ alsa_chmap = calloc(1, sizeof(*alsa_chmap) +
+ sizeof(alsa_chmap->pos[0]) * entry.num);
+ if (!alsa_chmap)
+ break;
+ alsa_chmap->channels = entry.num;
+
+ // Undo if mp_chmap_reorder() was called on the result.
+ int reorder[MP_NUM_CHANNELS];
+ mp_chmap_get_reorder(reorder, chmap, &entry);
+ for (int n = 0; n < entry.num; n++)
+ alsa_chmap->pos[n] = maps[i]->map.pos[reorder[n]];
+ break;
+ }
+ }
+
+ snd_pcm_free_chmaps(maps);
+ return alsa_chmap;
+}
+
+
+static int set_chmap(struct ao *ao, struct mp_chmap *dev_chmap, int num_channels)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ snd_pcm_chmap_t *alsa_chmap = map_back_chmap(ao, dev_chmap);
+ if (alsa_chmap) {
+ char tmp[128];
+ if (snd_pcm_chmap_print(alsa_chmap, sizeof(tmp), tmp) > 0)
+ MP_VERBOSE(ao, "trying to set ALSA channel map: %s\n", tmp);
+
+ err = snd_pcm_set_chmap(p->alsa, alsa_chmap);
+ if (err == -ENXIO) {
+ // A device my not be able to set any channel map, even channel maps
+ // that were reported as supported. This is either because the ALSA
+ // device is broken (dmix), or because the driver has only 1
+ // channel map per channel count, and setting the map is not needed.
+ MP_VERBOSE(ao, "device returned ENXIO when setting channel map %s\n",
+ mp_chmap_to_str(dev_chmap));
+ } else {
+ CHECK_ALSA_WARN("Channel map setup failed");
+ }
+
+ free(alsa_chmap);
+ }
+
+ alsa_chmap = snd_pcm_get_chmap(p->alsa);
+ if (alsa_chmap) {
+ char tmp[128];
+ if (snd_pcm_chmap_print(alsa_chmap, sizeof(tmp), tmp) > 0)
+ MP_VERBOSE(ao, "channel map reported by ALSA: %s\n", tmp);
+
+ struct mp_chmap chmap;
+ mp_chmap_from_alsa(&chmap, alsa_chmap);
+
+ MP_VERBOSE(ao, "which we understand as: %s\n", mp_chmap_to_str(&chmap));
+
+ if (p->opts->ignore_chmap) {
+ MP_VERBOSE(ao, "user set ignore-chmap; ignoring the channel map.\n");
+ } else if (af_fmt_is_spdif(ao->format)) {
+ MP_VERBOSE(ao, "using spdif passthrough; ignoring the channel map.\n");
+ } else if (!mp_chmap_is_valid(&chmap)) {
+ MP_WARN(ao, "Got unknown channel map from ALSA.\n");
+ } else if (chmap.num != num_channels) {
+ MP_WARN(ao, "ALSA channel map conflicts with channel count!\n");
+ } else {
+ if (mp_chmap_equals(&chmap, &ao->channels)) {
+ MP_VERBOSE(ao, "which is what we requested.\n");
+ } else if (!mp_chmap_is_valid(dev_chmap)) {
+ MP_VERBOSE(ao, "ignoring the ALSA channel map.\n");
+ } else {
+ MP_VERBOSE(ao, "using the ALSA channel map.\n");
+ ao->channels = chmap;
+ }
+ }
+
+ free(alsa_chmap);
+ }
+
+ return 0;
+}
+
+#else /* HAVE_CHMAP_API */
+
+static bool query_chmaps(struct ao *ao, struct mp_chmap *chmap)
+{
+ return false;
+}
+
+static int set_chmap(struct ao *ao, struct mp_chmap *dev_chmap, int num_channels)
+{
+ return 0;
+}
+
+#endif /* else HAVE_CHMAP_API */
+
+static void dump_hw_params(struct ao *ao, const char *msg,
+ snd_pcm_hw_params_t *hw_params)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ err = snd_pcm_hw_params_dump(hw_params, p->output);
+ CHECK_ALSA_WARN("Dump hwparams error");
+
+ char *tmp = NULL;
+ size_t tmp_s = snd_output_buffer_string(p->output, &tmp);
+ if (tmp)
+ mp_msg(ao->log, MSGL_DEBUG, "%s---\n%.*s---\n", msg, (int)tmp_s, tmp);
+ snd_output_flush(p->output);
+}
+
+static int map_iec958_srate(int srate)
+{
+ switch (srate) {
+ case 44100: return IEC958_AES3_CON_FS_44100;
+ case 48000: return IEC958_AES3_CON_FS_48000;
+ case 32000: return IEC958_AES3_CON_FS_32000;
+ case 22050: return IEC958_AES3_CON_FS_22050;
+ case 24000: return IEC958_AES3_CON_FS_24000;
+ case 88200: return IEC958_AES3_CON_FS_88200;
+ case 768000: return IEC958_AES3_CON_FS_768000;
+ case 96000: return IEC958_AES3_CON_FS_96000;
+ case 176400: return IEC958_AES3_CON_FS_176400;
+ case 192000: return IEC958_AES3_CON_FS_192000;
+ default: return IEC958_AES3_CON_FS_NOTID;
+ }
+}
+
+// ALSA device strings can have parameters. They are usually appended to the
+// device name. There can be various forms, and we (sometimes) want to append
+// them to unknown device strings, which possibly already include params.
+static char *append_params(void *ta_parent, const char *device, const char *p)
+{
+ if (!p || !p[0])
+ return talloc_strdup(ta_parent, device);
+
+ int len = strlen(device);
+ char *end = strchr(device, ':');
+ if (!end) {
+ /* no existing parameters: add it behind device name */
+ return talloc_asprintf(ta_parent, "%s:%s", device, p);
+ } else if (end[1] == '\0') {
+ /* ":" but no parameters */
+ return talloc_asprintf(ta_parent, "%s%s", device, p);
+ } else if (end[1] == '{' && device[len - 1] == '}') {
+ /* parameters in config syntax: add it inside the { } block */
+ return talloc_asprintf(ta_parent, "%.*s %s}", len - 1, device, p);
+ } else {
+ /* a simple list of parameters: add it at the end of the list */
+ return talloc_asprintf(ta_parent, "%s,%s", device, p);
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+static int try_open_device(struct ao *ao, const char *device, int mode)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ if (af_fmt_is_spdif(ao->format)) {
+ void *tmp = talloc_new(NULL);
+ char *params = talloc_asprintf(tmp,
+ "AES0=%d,AES1=%d,AES2=0,AES3=%d",
+ IEC958_AES0_NONAUDIO | IEC958_AES0_PRO_EMPHASIS_NONE,
+ IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER,
+ map_iec958_srate(ao->samplerate));
+ const char *ac3_device = append_params(tmp, device, params);
+ MP_VERBOSE(ao, "opening device '%s' => '%s'\n", device, ac3_device);
+ err = snd_pcm_open(&p->alsa, ac3_device, SND_PCM_STREAM_PLAYBACK, mode);
+ if (err < 0) {
+ // Some spdif-capable devices do not accept the AES0 parameter,
+ // and instead require the iec958 pseudo-device (they will play
+ // noise otherwise). Unfortunately, ALSA gives us no way to map
+ // these devices, so try it for the default device only.
+ bstr dev;
+ bstr_split_tok(bstr0(device), ":", &dev, &(bstr){0});
+ if (bstr_equals0(dev, "default")) {
+ const char *const fallbacks[] = {"hdmi", "iec958", NULL};
+ for (int n = 0; fallbacks[n]; n++) {
+ char *ndev = append_params(tmp, fallbacks[n], params);
+ MP_VERBOSE(ao, "got error '%s'; opening iec fallback "
+ "device '%s'\n", snd_strerror(err), ndev);
+ err = snd_pcm_open
+ (&p->alsa, ndev, SND_PCM_STREAM_PLAYBACK, mode);
+ if (err >= 0)
+ break;
+ }
+ }
+ }
+ talloc_free(tmp);
+ } else {
+ MP_VERBOSE(ao, "opening device '%s'\n", device);
+ err = snd_pcm_open(&p->alsa, device, SND_PCM_STREAM_PLAYBACK, mode);
+ }
+
+ return err;
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ if (p->output)
+ snd_output_close(p->output);
+ p->output = NULL;
+
+ if (p->alsa) {
+ int err;
+
+ err = snd_pcm_close(p->alsa);
+ p->alsa = NULL;
+ CHECK_ALSA_ERROR("pcm close error");
+ }
+
+alsa_error: ;
+}
+
+#define INIT_DEVICE_ERR_GENERIC -1
+#define INIT_DEVICE_ERR_HWPARAMS -2
+static int init_device(struct ao *ao, int mode)
+{
+ struct priv *p = ao->priv;
+ struct ao_alsa_opts *opts = p->opts;
+ int ret = INIT_DEVICE_ERR_GENERIC;
+ char *tmp;
+ size_t tmp_s;
+ int err;
+
+ p->alsa_fmt = SND_PCM_FORMAT_UNKNOWN;
+
+ err = snd_output_buffer_open(&p->output);
+ CHECK_ALSA_ERROR("Unable to create output buffer");
+
+ const char *device = "default";
+ if (ao->device)
+ device = ao->device;
+
+ err = try_open_device(ao, device, mode);
+ CHECK_ALSA_ERROR("Playback open error");
+
+ err = snd_pcm_dump(p->alsa, p->output);
+ CHECK_ALSA_WARN("Dump PCM error");
+ tmp_s = snd_output_buffer_string(p->output, &tmp);
+ if (tmp)
+ MP_DBG(ao, "PCM setup:\n---\n%.*s---\n", (int)tmp_s, tmp);
+ snd_output_flush(p->output);
+
+ err = snd_pcm_nonblock(p->alsa, 0);
+ CHECK_ALSA_WARN("Unable to set blocking mode");
+
+ snd_pcm_hw_params_t *alsa_hwparams;
+ snd_pcm_hw_params_alloca(&alsa_hwparams);
+
+ err = snd_pcm_hw_params_any(p->alsa, alsa_hwparams);
+ CHECK_ALSA_ERROR("Unable to get initial parameters");
+
+ dump_hw_params(ao, "Start HW params:\n", alsa_hwparams);
+
+ // Some ALSA drivers have broken delay reporting, so disable the ALSA
+ // resampling plugin by default.
+ if (!p->opts->resample) {
+ err = snd_pcm_hw_params_set_rate_resample(p->alsa, alsa_hwparams, 0);
+ CHECK_ALSA_ERROR("Unable to disable resampling");
+ }
+ dump_hw_params(ao, "HW params after rate:\n", alsa_hwparams);
+
+ snd_pcm_access_t access = af_fmt_is_planar(ao->format)
+ ? SND_PCM_ACCESS_RW_NONINTERLEAVED
+ : SND_PCM_ACCESS_RW_INTERLEAVED;
+ err = snd_pcm_hw_params_set_access(p->alsa, alsa_hwparams, access);
+ if (err < 0 && af_fmt_is_planar(ao->format)) {
+ ao->format = af_fmt_from_planar(ao->format);
+ access = SND_PCM_ACCESS_RW_INTERLEAVED;
+ err = snd_pcm_hw_params_set_access(p->alsa, alsa_hwparams, access);
+ }
+ CHECK_ALSA_ERROR("Unable to set access type");
+ dump_hw_params(ao, "HW params after access:\n", alsa_hwparams);
+
+ bool found_format = false;
+ int try_formats[AF_FORMAT_COUNT + 1];
+ af_get_best_sample_formats(ao->format, try_formats);
+ for (int n = 0; try_formats[n] && !found_format; n++) {
+ int mp_format = try_formats[n];
+ if (af_fmt_is_planar(ao->format) != af_fmt_is_planar(mp_format))
+ continue; // implied SND_PCM_ACCESS mismatches
+ int mp_pformat = af_fmt_from_planar(mp_format);
+ if (af_fmt_is_spdif(mp_pformat))
+ mp_pformat = AF_FORMAT_S16;
+ const struct alsa_fmt *fmt = find_alsa_format(mp_pformat);
+ if (!fmt)
+ continue;
+ for (; fmt->mp_format == mp_pformat; fmt++) {
+ p->alsa_fmt = fmt->alsa_format;
+ p->convert = (struct ao_convert_fmt){
+ .src_fmt = mp_format,
+ .dst_bits = fmt->bits ? fmt->bits : af_fmt_to_bytes(mp_format) * 8,
+ .pad_msb = fmt->pad_msb,
+ };
+ if (!ao_can_convert_inplace(&p->convert))
+ continue;
+ MP_VERBOSE(ao, "trying format %s/%d\n", af_fmt_to_str(mp_pformat),
+ p->alsa_fmt);
+ if (snd_pcm_hw_params_test_format(p->alsa, alsa_hwparams,
+ p->alsa_fmt) >= 0)
+ {
+ ao->format = mp_format;
+ found_format = true;
+ break;
+ }
+ }
+ }
+
+ if (!found_format) {
+ MP_ERR(ao, "Can't find appropriate sample format.\n");
+ goto alsa_error;
+ }
+
+ err = snd_pcm_hw_params_set_format(p->alsa, alsa_hwparams, p->alsa_fmt);
+ CHECK_ALSA_ERROR("Unable to set format");
+ dump_hw_params(ao, "HW params after format:\n", alsa_hwparams);
+
+ // Stereo, or mono if input is 1 channel.
+ struct mp_chmap reduced;
+ mp_chmap_from_channels(&reduced, MPMIN(2, ao->channels.num));
+
+ struct mp_chmap dev_chmap = {0};
+ if (!af_fmt_is_spdif(ao->format) && !p->opts->ignore_chmap &&
+ !mp_chmap_equals(&ao->channels, &reduced))
+ {
+ struct mp_chmap res = ao->channels;
+ if (query_chmaps(ao, &res))
+ dev_chmap = res;
+
+ // Whatever it is, we dumb it down to mono or stereo. Some drivers may
+ // return things like bl-br, but the user (probably) still wants stereo.
+ // This also handles the failure case (dev_chmap.num==0).
+ if (dev_chmap.num <= 2) {
+ dev_chmap.num = 0;
+ ao->channels = reduced;
+ } else if (dev_chmap.num) {
+ ao->channels = dev_chmap;
+ }
+ }
+
+ int num_channels = ao->channels.num;
+ err = snd_pcm_hw_params_set_channels_near
+ (p->alsa, alsa_hwparams, &num_channels);
+ CHECK_ALSA_ERROR("Unable to set channels");
+ dump_hw_params(ao, "HW params after channels:\n", alsa_hwparams);
+
+ if (num_channels > MP_NUM_CHANNELS) {
+ MP_FATAL(ao, "Too many audio channels (%d).\n", num_channels);
+ goto alsa_error;
+ }
+
+ err = snd_pcm_hw_params_set_rate_near
+ (p->alsa, alsa_hwparams, &ao->samplerate, NULL);
+ CHECK_ALSA_ERROR("Unable to set samplerate-2");
+ dump_hw_params(ao, "HW params after rate-2:\n", alsa_hwparams);
+
+ snd_pcm_hw_params_t *hwparams_backup;
+ snd_pcm_hw_params_alloca(&hwparams_backup);
+ snd_pcm_hw_params_copy(hwparams_backup, alsa_hwparams);
+
+ // Cargo-culted buffer settings; might still be useful for PulseAudio.
+ err = 0;
+ if (opts->buffer_time) {
+ err = snd_pcm_hw_params_set_buffer_time_near
+ (p->alsa, alsa_hwparams, &(unsigned int){opts->buffer_time}, NULL);
+ CHECK_ALSA_WARN("Unable to set buffer time near");
+ }
+ if (err >= 0 && opts->frags) {
+ err = snd_pcm_hw_params_set_periods_near
+ (p->alsa, alsa_hwparams, &(unsigned int){opts->frags}, NULL);
+ CHECK_ALSA_WARN("Unable to set periods");
+ }
+ if (err < 0)
+ snd_pcm_hw_params_copy(alsa_hwparams, hwparams_backup);
+
+ dump_hw_params(ao, "Going to set final HW params:\n", alsa_hwparams);
+
+ /* finally install hardware parameters */
+ err = snd_pcm_hw_params(p->alsa, alsa_hwparams);
+ ret = INIT_DEVICE_ERR_HWPARAMS;
+ CHECK_ALSA_ERROR("Unable to set hw-parameters");
+ ret = INIT_DEVICE_ERR_GENERIC;
+ dump_hw_params(ao, "Final HW params:\n", alsa_hwparams);
+
+ if (set_chmap(ao, &dev_chmap, num_channels) < 0)
+ goto alsa_error;
+
+ if (num_channels != ao->channels.num) {
+ int req = ao->channels.num;
+ mp_chmap_from_channels(&ao->channels, MPMIN(2, num_channels));
+ mp_chmap_fill_na(&ao->channels, num_channels);
+ MP_ERR(ao, "Asked for %d channels, got %d - fallback to %s.\n", req,
+ num_channels, mp_chmap_to_str(&ao->channels));
+ if (num_channels != ao->channels.num) {
+ MP_FATAL(ao, "mismatching channel counts.\n");
+ goto alsa_error;
+ }
+ }
+
+ err = snd_pcm_hw_params_get_buffer_size(alsa_hwparams, &p->buffersize);
+ CHECK_ALSA_ERROR("Unable to get buffersize");
+
+ err = snd_pcm_hw_params_get_period_size(alsa_hwparams, &p->outburst, NULL);
+ CHECK_ALSA_ERROR("Unable to get period size");
+
+ p->can_pause = snd_pcm_hw_params_can_pause(alsa_hwparams);
+
+ snd_pcm_sw_params_t *alsa_swparams;
+ snd_pcm_sw_params_alloca(&alsa_swparams);
+
+ err = snd_pcm_sw_params_current(p->alsa, alsa_swparams);
+ CHECK_ALSA_ERROR("Unable to get sw-parameters");
+
+ snd_pcm_uframes_t boundary;
+ err = snd_pcm_sw_params_get_boundary(alsa_swparams, &boundary);
+ CHECK_ALSA_ERROR("Unable to get boundary");
+
+ // Manual trigger; INT_MAX as suggested by ALSA doxygen (they call it MAXINT).
+ err = snd_pcm_sw_params_set_start_threshold(p->alsa, alsa_swparams, INT_MAX);
+ CHECK_ALSA_ERROR("Unable to set start threshold");
+
+ /* play silence when there is an underrun */
+ err = snd_pcm_sw_params_set_silence_size
+ (p->alsa, alsa_swparams, boundary);
+ CHECK_ALSA_ERROR("Unable to set silence size");
+
+ err = snd_pcm_sw_params(p->alsa, alsa_swparams);
+ CHECK_ALSA_ERROR("Unable to set sw-parameters");
+
+ MP_VERBOSE(ao, "hw pausing supported: %s\n", p->can_pause ? "yes" : "no");
+ MP_VERBOSE(ao, "buffersize: %d samples\n", (int)p->buffersize);
+ MP_VERBOSE(ao, "period size: %d samples\n", (int)p->outburst);
+
+ ao->device_buffer = p->buffersize;
+
+ p->convert.channels = ao->channels.num;
+
+ err = snd_pcm_prepare(p->alsa);
+ CHECK_ALSA_ERROR("pcm prepare error");
+
+ return 0;
+
+alsa_error:
+ uninit(ao);
+ return ret;
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ p->opts = mp_get_config_group(ao, ao->global, &ao_alsa_conf);
+
+ if (!p->opts->ni)
+ ao->format = af_fmt_from_planar(ao->format);
+
+ MP_VERBOSE(ao, "using ALSA version: %s\n", snd_asoundlib_version());
+
+ int mode = 0;
+ int r = init_device(ao, mode);
+ if (r == INIT_DEVICE_ERR_HWPARAMS) {
+ // With some drivers, ALSA appears to be unable to set valid hwparams,
+ // but they work if at least SND_PCM_NO_AUTO_FORMAT is set. Also, it
+ // appears you can set this flag only on opening a device, thus there
+ // is the need to retry opening the device.
+ MP_WARN(ao, "Attempting to work around even more ALSA bugs...\n");
+ mode |= SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_FORMAT |
+ SND_PCM_NO_AUTO_RESAMPLE;
+ r = init_device(ao, mode);
+ }
+
+ // Sometimes, ALSA will advertise certain chmaps, but it's not possible to
+ // set them. This can happen with dmix: as of alsa 1.0.29, dmix can do
+ // stereo only, but advertises the surround chmaps of the underlying device.
+ // In this case, e.g. setting 6 channels will succeed, but requesting 5.1
+ // afterwards will fail. Then it will return something like "FL FR NA NA NA NA"
+ // as channel map. This means we would have to pad stereo output to 6
+ // channels with silence, which would require lots of extra processing. You
+ // can't change the number of channels to 2 either, because the hw params
+ // are already set! So just fuck it and reopen the device with the chmap
+ // "cleaned out" of NA entries.
+ if (r >= 0) {
+ struct mp_chmap without_na = ao->channels;
+ mp_chmap_remove_na(&without_na);
+
+ if (mp_chmap_is_valid(&without_na) && without_na.num <= 2 &&
+ ao->channels.num > 2)
+ {
+ MP_VERBOSE(ao, "Working around braindead dmix multichannel behavior.\n");
+ uninit(ao);
+ ao->channels = without_na;
+ r = init_device(ao, mode);
+ }
+ }
+
+ return r;
+}
+
+// Function for dealing with playback state. This attempts to recover the ALSA
+// state (bring it into SND_PCM_STATE_{PREPARED,RUNNING,PAUSED,UNDERRUN}). If
+// state!=NULL, fill it after recovery is attempted.
+// Returns true if PCM is in one the expected states.
+static bool recover_and_get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ snd_pcm_status_t *st;
+ snd_pcm_status_alloca(&st);
+
+ bool state_ok = false;
+ snd_pcm_state_t pcmst = SND_PCM_STATE_DISCONNECTED;
+
+ // Give it a number of chances to recover. This tries to deal with the fact
+ // that the API is asynchronous, and to account for some past cargo-cult
+ // (where things were retried in a loop).
+ for (int n = 0; n < 10; n++) {
+ err = snd_pcm_status(p->alsa, st);
+ if (err == -EPIPE) {
+ // ALSA APIs can return -EPIPE when an XRUN happens,
+ // we skip right to handling it by setting pcmst
+ // manually.
+ pcmst = SND_PCM_STATE_XRUN;
+ } else {
+ // Otherwise do error checking and query the PCM state properly.
+ CHECK_ALSA_ERROR("snd_pcm_status");
+
+ pcmst = snd_pcm_status_get_state(st);
+ }
+
+ if (pcmst == SND_PCM_STATE_PREPARED ||
+ pcmst == SND_PCM_STATE_RUNNING ||
+ pcmst == SND_PCM_STATE_PAUSED)
+ {
+ state_ok = true;
+ break;
+ }
+
+ MP_VERBOSE(ao, "attempt %d to recover from state '%s'...\n",
+ n + 1, snd_pcm_state_name(pcmst));
+
+ switch (pcmst) {
+ // Underrun; recover. (We never use draining.)
+ case SND_PCM_STATE_XRUN:
+ case SND_PCM_STATE_DRAINING:
+ err = snd_pcm_prepare(p->alsa);
+ CHECK_ALSA_ERROR("pcm prepare error");
+ continue;
+ // Hardware suspend.
+ case SND_PCM_STATE_SUSPENDED:
+ MP_INFO(ao, "PCM in suspend mode, trying to resume.\n");
+ err = snd_pcm_resume(p->alsa);
+ if (err == -EAGAIN) {
+ // Cargo-cult from decades ago, with a cargo cult timeout.
+ MP_INFO(ao, "PCM resume EAGAIN - retrying.\n");
+ sleep(1);
+ continue;
+ }
+ if (err == -ENOSYS) {
+ // As suggested by ALSA doxygen.
+ MP_VERBOSE(ao, "ENOSYS, retrying with snd_pcm_prepare().\n");
+ err = snd_pcm_prepare(p->alsa);
+ }
+ if (err < 0)
+ MP_ERR(ao, "resuming from SUSPENDED: %s\n", snd_strerror(err));
+ continue;
+ // Device lost. OPEN/SETUP are states we never enter after init, so
+ // treat them like DISCONNECTED.
+ case SND_PCM_STATE_DISCONNECTED:
+ case SND_PCM_STATE_OPEN:
+ case SND_PCM_STATE_SETUP:
+ default:
+ if (!p->device_lost) {
+ MP_WARN(ao, "Device lost, trying to recover...\n");
+ ao_request_reload(ao);
+ p->device_lost = true;
+ }
+ goto alsa_error;
+ }
+ }
+
+ if (!state_ok) {
+ MP_ERR(ao, "could not recover\n");
+ }
+
+alsa_error:
+
+ if (state) {
+ snd_pcm_sframes_t del = state_ok ? snd_pcm_status_get_delay(st) : 0;
+ state->delay = MPMAX(del, 0) / (double)ao->samplerate;
+ state->free_samples = state_ok ? snd_pcm_status_get_avail(st) : 0;
+ state->free_samples = MPCLAMP(state->free_samples, 0, ao->device_buffer);
+ // Align to period size.
+ state->free_samples = state->free_samples / p->outburst * p->outburst;
+ state->queued_samples = ao->device_buffer - state->free_samples;
+ state->playing = pcmst == SND_PCM_STATE_RUNNING ||
+ pcmst == SND_PCM_STATE_PAUSED;
+ }
+
+ return state_ok;
+}
+
+static void audio_get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ recover_and_get_state(ao, state);
+}
+
+static void audio_start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ recover_and_get_state(ao, NULL);
+
+ err = snd_pcm_start(p->alsa);
+ CHECK_ALSA_ERROR("pcm start error");
+
+alsa_error: ;
+}
+
+static void audio_reset(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ err = snd_pcm_drop(p->alsa);
+ CHECK_ALSA_ERROR("pcm drop error");
+ err = snd_pcm_prepare(p->alsa);
+ CHECK_ALSA_ERROR("pcm prepare error");
+
+ recover_and_get_state(ao, NULL);
+
+alsa_error: ;
+}
+
+static bool audio_set_paused(struct ao *ao, bool paused)
+{
+ struct priv *p = ao->priv;
+ int err;
+
+ recover_and_get_state(ao, NULL);
+
+ if (!p->can_pause)
+ return false;
+
+ snd_pcm_state_t pcmst = snd_pcm_state(p->alsa);
+ if (pcmst == SND_PCM_STATE_RUNNING && paused) {
+ err = snd_pcm_pause(p->alsa, 1);
+ CHECK_ALSA_ERROR("pcm pause error");
+ } else if (pcmst == SND_PCM_STATE_PAUSED && !paused) {
+ err = snd_pcm_pause(p->alsa, 0);
+ CHECK_ALSA_ERROR("pcm resume error");
+ }
+
+ return true;
+
+alsa_error:
+ return false;
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *p = ao->priv;
+
+ ao_convert_inplace(&p->convert, data, samples);
+
+ if (!recover_and_get_state(ao, NULL))
+ return false;
+
+ snd_pcm_sframes_t err = 0;
+ if (af_fmt_is_planar(ao->format)) {
+ err = snd_pcm_writen(p->alsa, data, samples);
+ } else {
+ err = snd_pcm_writei(p->alsa, data[0], samples);
+ }
+
+ CHECK_ALSA_ERROR("pcm write error");
+ if (err >= 0 && err != samples) {
+ MP_ERR(ao, "unexpected partial write (%d of %d frames), dropping audio\n",
+ (int)err, samples);
+ }
+
+ return true;
+
+alsa_error:
+ return false;
+}
+
+static bool is_useless_device(char *name)
+{
+ char *crap[] = {"rear", "center_lfe", "side", "pulse", "null", "dsnoop", "hw"};
+ for (int i = 0; i < MP_ARRAY_SIZE(crap); i++) {
+ int l = strlen(crap[i]);
+ if (name && strncmp(name, crap[i], l) == 0 &&
+ (!name[l] || name[l] == ':'))
+ return true;
+ }
+ // The standard default entry will achieve exactly the same.
+ if (name && strcmp(name, "default") == 0)
+ return true;
+ return false;
+}
+
+static void list_devs(struct ao *ao, struct ao_device_list *list)
+{
+ void **hints;
+ if (snd_device_name_hint(-1, "pcm", &hints) < 0)
+ return;
+
+ ao_device_list_add(list, ao, &(struct ao_device_desc){"", ""});
+
+ for (int n = 0; hints[n]; n++) {
+ char *name = snd_device_name_get_hint(hints[n], "NAME");
+ char *desc = snd_device_name_get_hint(hints[n], "DESC");
+ char *io = snd_device_name_get_hint(hints[n], "IOID");
+ if (!is_useless_device(name) && (!io || strcmp(io, "Output") == 0)) {
+ char desc2[1024];
+ snprintf(desc2, sizeof(desc2), "%s", desc ? desc : "");
+ for (int i = 0; desc2[i]; i++) {
+ if (desc2[i] == '\n')
+ desc2[i] = '/';
+ }
+ ao_device_list_add(list, ao, &(struct ao_device_desc){name, desc2});
+ }
+ free(name);
+ free(desc);
+ free(io);
+ }
+
+ snd_device_name_free_hint(hints);
+}
+
+const struct ao_driver audio_out_alsa = {
+ .description = "ALSA audio output",
+ .name = "alsa",
+ .init = init,
+ .uninit = uninit,
+ .control = control,
+ .get_state = audio_get_state,
+ .write = audio_write,
+ .start = audio_start,
+ .set_pause = audio_set_paused,
+ .reset = audio_reset,
+ .list_devs = list_devs,
+ .priv_size = sizeof(struct priv),
+ .global_opts = &ao_alsa_conf,
+};
diff --git a/audio/out/ao_audiotrack.c b/audio/out/ao_audiotrack.c
new file mode 100644
index 0000000..1392699
--- /dev/null
+++ b/audio/out/ao_audiotrack.c
@@ -0,0 +1,852 @@
+/*
+ * Android AudioTrack audio output driver.
+ * Copyright (C) 2018 Aman Gupta <aman@tmm1.net>
+ * Copyright (C) 2012-2015 VLC authors and VideoLAN, VideoLabs
+ * Authors: Thomas Guillem <thomas@gllm.fr>
+ * Ming Hu <tewilove@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ao.h"
+#include "internal.h"
+#include "common/msg.h"
+#include "audio/format.h"
+#include "options/m_option.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "misc/jni.h"
+
+struct priv {
+ jobject audiotrack;
+ jint samplerate;
+ jint channel_config;
+ jint format;
+ jint size;
+
+ jobject timestamp;
+ int64_t timestamp_fetched;
+ bool timestamp_set;
+ int timestamp_stable;
+
+ uint32_t written_frames; /* requires uint32_t rollover semantics */
+ uint32_t playhead_pos;
+ uint32_t playhead_offset;
+ bool reset_pending;
+
+ void *chunk;
+ int chunksize;
+ jbyteArray bytearray;
+ jshortArray shortarray;
+ jfloatArray floatarray;
+ jobject bbuf;
+
+ bool cfg_pcm_float;
+ int cfg_session_id;
+
+ bool needs_timestamp_offset;
+ int64_t timestamp_offset;
+
+ bool thread_terminate;
+ bool thread_created;
+ mp_thread thread;
+ mp_mutex lock;
+ mp_cond wakeup;
+};
+
+struct JNIByteBuffer {
+ jclass clazz;
+ jmethodID clear;
+ struct MPJniField mapping[];
+} ByteBuffer = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIByteBuffer, member)
+ {"java/nio/ByteBuffer", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
+ {"java/nio/ByteBuffer", "clear", "()Ljava/nio/Buffer;", MP_JNI_METHOD, OFFSET(clear), 1},
+ {0},
+ #undef OFFSET
+}};
+
+struct JNIAudioTrack {
+ jclass clazz;
+ jmethodID ctor;
+ jmethodID ctorV21;
+ jmethodID release;
+ jmethodID getState;
+ jmethodID getPlayState;
+ jmethodID play;
+ jmethodID stop;
+ jmethodID flush;
+ jmethodID pause;
+ jmethodID write;
+ jmethodID writeFloat;
+ jmethodID writeShortV23;
+ jmethodID writeBufferV21;
+ jmethodID getBufferSizeInFramesV23;
+ jmethodID getPlaybackHeadPosition;
+ jmethodID getTimestamp;
+ jmethodID getLatency;
+ jmethodID getMinBufferSize;
+ jmethodID getNativeOutputSampleRate;
+ jint STATE_INITIALIZED;
+ jint PLAYSTATE_STOPPED;
+ jint PLAYSTATE_PAUSED;
+ jint PLAYSTATE_PLAYING;
+ jint MODE_STREAM;
+ jint ERROR;
+ jint ERROR_BAD_VALUE;
+ jint ERROR_INVALID_OPERATION;
+ jint WRITE_BLOCKING;
+ jint WRITE_NON_BLOCKING;
+ struct MPJniField mapping[];
+} AudioTrack = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioTrack, member)
+ {"android/media/AudioTrack", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
+ {"android/media/AudioTrack", "<init>", "(IIIIIII)V", MP_JNI_METHOD, OFFSET(ctor), 1},
+ {"android/media/AudioTrack", "<init>", "(Landroid/media/AudioAttributes;Landroid/media/AudioFormat;III)V", MP_JNI_METHOD, OFFSET(ctorV21), 0},
+ {"android/media/AudioTrack", "release", "()V", MP_JNI_METHOD, OFFSET(release), 1},
+ {"android/media/AudioTrack", "getState", "()I", MP_JNI_METHOD, OFFSET(getState), 1},
+ {"android/media/AudioTrack", "getPlayState", "()I", MP_JNI_METHOD, OFFSET(getPlayState), 1},
+ {"android/media/AudioTrack", "play", "()V", MP_JNI_METHOD, OFFSET(play), 1},
+ {"android/media/AudioTrack", "stop", "()V", MP_JNI_METHOD, OFFSET(stop), 1},
+ {"android/media/AudioTrack", "flush", "()V", MP_JNI_METHOD, OFFSET(flush), 1},
+ {"android/media/AudioTrack", "pause", "()V", MP_JNI_METHOD, OFFSET(pause), 1},
+ {"android/media/AudioTrack", "write", "([BII)I", MP_JNI_METHOD, OFFSET(write), 1},
+ {"android/media/AudioTrack", "write", "([FIII)I", MP_JNI_METHOD, OFFSET(writeFloat), 1},
+ {"android/media/AudioTrack", "write", "([SIII)I", MP_JNI_METHOD, OFFSET(writeShortV23), 0},
+ {"android/media/AudioTrack", "write", "(Ljava/nio/ByteBuffer;II)I", MP_JNI_METHOD, OFFSET(writeBufferV21), 1},
+ {"android/media/AudioTrack", "getBufferSizeInFrames", "()I", MP_JNI_METHOD, OFFSET(getBufferSizeInFramesV23), 0},
+ {"android/media/AudioTrack", "getTimestamp", "(Landroid/media/AudioTimestamp;)Z", MP_JNI_METHOD, OFFSET(getTimestamp), 1},
+ {"android/media/AudioTrack", "getPlaybackHeadPosition", "()I", MP_JNI_METHOD, OFFSET(getPlaybackHeadPosition), 1},
+ {"android/media/AudioTrack", "getLatency", "()I", MP_JNI_METHOD, OFFSET(getLatency), 1},
+ {"android/media/AudioTrack", "getMinBufferSize", "(III)I", MP_JNI_STATIC_METHOD, OFFSET(getMinBufferSize), 1},
+ {"android/media/AudioTrack", "getNativeOutputSampleRate", "(I)I", MP_JNI_STATIC_METHOD, OFFSET(getNativeOutputSampleRate), 1},
+ {"android/media/AudioTrack", "WRITE_BLOCKING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(WRITE_BLOCKING), 0},
+ {"android/media/AudioTrack", "WRITE_NON_BLOCKING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(WRITE_NON_BLOCKING), 0},
+ {"android/media/AudioTrack", "STATE_INITIALIZED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(STATE_INITIALIZED), 1},
+ {"android/media/AudioTrack", "PLAYSTATE_STOPPED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_STOPPED), 1},
+ {"android/media/AudioTrack", "PLAYSTATE_PAUSED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_PAUSED), 1},
+ {"android/media/AudioTrack", "PLAYSTATE_PLAYING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_PLAYING), 1},
+ {"android/media/AudioTrack", "MODE_STREAM", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(MODE_STREAM), 1},
+ {"android/media/AudioTrack", "ERROR", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR), 1},
+ {"android/media/AudioTrack", "ERROR_BAD_VALUE", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_BAD_VALUE), 1},
+ {"android/media/AudioTrack", "ERROR_INVALID_OPERATION", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_INVALID_OPERATION), 1},
+ {0}
+ #undef OFFSET
+}};
+
+struct JNIAudioAttributes {
+ jclass clazz;
+ jint CONTENT_TYPE_MOVIE;
+ jint CONTENT_TYPE_MUSIC;
+ jint USAGE_MEDIA;
+ struct MPJniField mapping[];
+} AudioAttributes = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioAttributes, member)
+ {"android/media/AudioAttributes", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0},
+ {"android/media/AudioAttributes", "CONTENT_TYPE_MOVIE", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CONTENT_TYPE_MOVIE), 0},
+ {"android/media/AudioAttributes", "CONTENT_TYPE_MUSIC", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CONTENT_TYPE_MUSIC), 0},
+ {"android/media/AudioAttributes", "USAGE_MEDIA", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(USAGE_MEDIA), 0},
+ {0}
+ #undef OFFSET
+}};
+
+struct JNIAudioAttributesBuilder {
+ jclass clazz;
+ jmethodID ctor;
+ jmethodID setUsage;
+ jmethodID setContentType;
+ jmethodID build;
+ struct MPJniField mapping[];
+} AudioAttributesBuilder = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioAttributesBuilder, member)
+ {"android/media/AudioAttributes$Builder", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0},
+ {"android/media/AudioAttributes$Builder", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 0},
+ {"android/media/AudioAttributes$Builder", "setUsage", "(I)Landroid/media/AudioAttributes$Builder;", MP_JNI_METHOD, OFFSET(setUsage), 0},
+ {"android/media/AudioAttributes$Builder", "setContentType", "(I)Landroid/media/AudioAttributes$Builder;", MP_JNI_METHOD, OFFSET(setContentType), 0},
+ {"android/media/AudioAttributes$Builder", "build", "()Landroid/media/AudioAttributes;", MP_JNI_METHOD, OFFSET(build), 0},
+ {0}
+ #undef OFFSET
+}};
+
+struct JNIAudioFormat {
+ jclass clazz;
+ jint ENCODING_PCM_8BIT;
+ jint ENCODING_PCM_16BIT;
+ jint ENCODING_PCM_FLOAT;
+ jint ENCODING_IEC61937;
+ jint CHANNEL_OUT_MONO;
+ jint CHANNEL_OUT_STEREO;
+ jint CHANNEL_OUT_FRONT_CENTER;
+ jint CHANNEL_OUT_QUAD;
+ jint CHANNEL_OUT_5POINT1;
+ jint CHANNEL_OUT_BACK_CENTER;
+ jint CHANNEL_OUT_7POINT1_SURROUND;
+ struct MPJniField mapping[];
+} AudioFormat = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioFormat, member)
+ {"android/media/AudioFormat", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
+ {"android/media/AudioFormat", "ENCODING_PCM_8BIT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_8BIT), 1},
+ {"android/media/AudioFormat", "ENCODING_PCM_16BIT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_16BIT), 1},
+ {"android/media/AudioFormat", "ENCODING_PCM_FLOAT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_FLOAT), 1},
+ {"android/media/AudioFormat", "ENCODING_IEC61937", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_IEC61937), 0},
+ {"android/media/AudioFormat", "CHANNEL_OUT_MONO", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_MONO), 1},
+ {"android/media/AudioFormat", "CHANNEL_OUT_STEREO", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_STEREO), 1},
+ {"android/media/AudioFormat", "CHANNEL_OUT_FRONT_CENTER", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_FRONT_CENTER), 1},
+ {"android/media/AudioFormat", "CHANNEL_OUT_QUAD", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_QUAD), 1},
+ {"android/media/AudioFormat", "CHANNEL_OUT_5POINT1", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_5POINT1), 1},
+ {"android/media/AudioFormat", "CHANNEL_OUT_BACK_CENTER", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_BACK_CENTER), 1},
+ {"android/media/AudioFormat", "CHANNEL_OUT_7POINT1_SURROUND", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_7POINT1_SURROUND), 0},
+ {0}
+ #undef OFFSET
+}};
+
+struct JNIAudioFormatBuilder {
+ jclass clazz;
+ jmethodID ctor;
+ jmethodID setEncoding;
+ jmethodID setSampleRate;
+ jmethodID setChannelMask;
+ jmethodID build;
+ struct MPJniField mapping[];
+} AudioFormatBuilder = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioFormatBuilder, member)
+ {"android/media/AudioFormat$Builder", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0},
+ {"android/media/AudioFormat$Builder", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 0},
+ {"android/media/AudioFormat$Builder", "setEncoding", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setEncoding), 0},
+ {"android/media/AudioFormat$Builder", "setSampleRate", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setSampleRate), 0},
+ {"android/media/AudioFormat$Builder", "setChannelMask", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setChannelMask), 0},
+ {"android/media/AudioFormat$Builder", "build", "()Landroid/media/AudioFormat;", MP_JNI_METHOD, OFFSET(build), 0},
+ {0}
+ #undef OFFSET
+}};
+
+
+struct JNIAudioManager {
+ jclass clazz;
+ jint ERROR_DEAD_OBJECT;
+ jint STREAM_MUSIC;
+ struct MPJniField mapping[];
+} AudioManager = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioManager, member)
+ {"android/media/AudioManager", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
+ {"android/media/AudioManager", "STREAM_MUSIC", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(STREAM_MUSIC), 1},
+ {"android/media/AudioManager", "ERROR_DEAD_OBJECT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_DEAD_OBJECT), 0},
+ {0}
+ #undef OFFSET
+}};
+
+struct JNIAudioTimestamp {
+ jclass clazz;
+ jmethodID ctor;
+ jfieldID framePosition;
+ jfieldID nanoTime;
+ struct MPJniField mapping[];
+} AudioTimestamp = {.mapping = {
+ #define OFFSET(member) offsetof(struct JNIAudioTimestamp, member)
+ {"android/media/AudioTimestamp", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
+ {"android/media/AudioTimestamp", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 1},
+ {"android/media/AudioTimestamp", "framePosition", "J", MP_JNI_FIELD, OFFSET(framePosition), 1},
+ {"android/media/AudioTimestamp", "nanoTime", "J", MP_JNI_FIELD, OFFSET(nanoTime), 1},
+ {0}
+ #undef OFFSET
+}};
+
+#define MP_JNI_DELETELOCAL(o) (*env)->DeleteLocalRef(env, o)
+
+static int AudioTrack_New(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ jobject audiotrack = NULL;
+
+ if (AudioTrack.ctorV21) {
+ MP_VERBOSE(ao, "Using API21 initializer\n");
+ jobject tmp = NULL;
+
+ jobject format_builder = MP_JNI_NEW(AudioFormatBuilder.clazz, AudioFormatBuilder.ctor);
+ MP_JNI_EXCEPTION_LOG(ao);
+ tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setEncoding, p->format);
+ MP_JNI_DELETELOCAL(tmp);
+ tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setSampleRate, p->samplerate);
+ MP_JNI_DELETELOCAL(tmp);
+ tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setChannelMask, p->channel_config);
+ MP_JNI_DELETELOCAL(tmp);
+ jobject format = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.build);
+ MP_JNI_DELETELOCAL(format_builder);
+
+ jobject attr_builder = MP_JNI_NEW(AudioAttributesBuilder.clazz, AudioAttributesBuilder.ctor);
+ MP_JNI_EXCEPTION_LOG(ao);
+ tmp = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.setUsage, AudioAttributes.USAGE_MEDIA);
+ MP_JNI_DELETELOCAL(tmp);
+ jint content_type = (ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC) ?
+ AudioAttributes.CONTENT_TYPE_MUSIC : AudioAttributes.CONTENT_TYPE_MOVIE;
+ tmp = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.setContentType, content_type);
+ MP_JNI_DELETELOCAL(tmp);
+ jobject attr = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.build);
+ MP_JNI_DELETELOCAL(attr_builder);
+
+ audiotrack = MP_JNI_NEW(
+ AudioTrack.clazz,
+ AudioTrack.ctorV21,
+ attr,
+ format,
+ p->size,
+ AudioTrack.MODE_STREAM,
+ p->cfg_session_id
+ );
+
+ MP_JNI_DELETELOCAL(format);
+ MP_JNI_DELETELOCAL(attr);
+ } else {
+ MP_VERBOSE(ao, "Using legacy initializer\n");
+ audiotrack = MP_JNI_NEW(
+ AudioTrack.clazz,
+ AudioTrack.ctor,
+ AudioManager.STREAM_MUSIC,
+ p->samplerate,
+ p->channel_config,
+ p->format,
+ p->size,
+ AudioTrack.MODE_STREAM,
+ p->cfg_session_id
+ );
+ }
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0 || !audiotrack) {
+ MP_FATAL(ao, "AudioTrack Init failed\n");
+ return -1;
+ }
+
+ if (MP_JNI_CALL_INT(audiotrack, AudioTrack.getState) != AudioTrack.STATE_INITIALIZED) {
+ MP_JNI_CALL_VOID(audiotrack, AudioTrack.release);
+ MP_JNI_EXCEPTION_LOG(ao);
+ (*env)->DeleteLocalRef(env, audiotrack);
+ MP_ERR(ao, "AudioTrack.getState failed\n");
+ return -1;
+ }
+
+ if (AudioTrack.getBufferSizeInFramesV23) {
+ int bufferSize = MP_JNI_CALL_INT(audiotrack, AudioTrack.getBufferSizeInFramesV23);
+ if (bufferSize > 0) {
+ MP_VERBOSE(ao, "AudioTrack.getBufferSizeInFrames = %d\n", bufferSize);
+ ao->device_buffer = bufferSize;
+ }
+ }
+
+ p->audiotrack = (*env)->NewGlobalRef(env, audiotrack);
+ (*env)->DeleteLocalRef(env, audiotrack);
+ if (!p->audiotrack)
+ return -1;
+
+ return 0;
+}
+
+static int AudioTrack_Recreate(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.release);
+ MP_JNI_EXCEPTION_LOG(ao);
+ (*env)->DeleteGlobalRef(env, p->audiotrack);
+ p->audiotrack = NULL;
+ return AudioTrack_New(ao);
+}
+
+static uint32_t AudioTrack_getPlaybackHeadPosition(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ if (!p->audiotrack)
+ return 0;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ uint32_t pos = 0;
+ int64_t now = mp_raw_time_ns();
+ int state = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlayState);
+
+ int stable_count = 20;
+ int64_t wait = p->timestamp_stable < stable_count ? 50000000 : 3000000000;
+
+ if (state == AudioTrack.PLAYSTATE_PLAYING && p->format != AudioFormat.ENCODING_IEC61937 &&
+ (p->timestamp_fetched == 0 || now - p->timestamp_fetched >= wait)) {
+ if (!p->timestamp_fetched)
+ p->timestamp_stable = 0;
+
+ int64_t time1 = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime);
+ if (MP_JNI_CALL_BOOL(p->audiotrack, AudioTrack.getTimestamp, p->timestamp)) {
+ p->timestamp_set = true;
+ p->timestamp_fetched = now;
+ if (p->timestamp_stable < stable_count) {
+ uint32_t fpos = 0xFFFFFFFFL & MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.framePosition);
+ int64_t time2 = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime);
+ //MP_VERBOSE(ao, "getTimestamp: fpos= %u / time= %"PRId64" / now= %"PRId64" / stable= %d\n", fpos, time2, now, p->timestamp_stable);
+ if (time1 != time2 && time2 != 0 && fpos != 0) {
+ p->timestamp_stable++;
+ }
+ }
+ }
+ }
+
+ /* AudioTrack's framePosition and playbackHeadPosition return a signed integer,
+ * but documentation states it should be interpreted as a 32-bit unsigned integer.
+ */
+ if (p->timestamp_set) {
+ pos = 0xFFFFFFFFL & MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.framePosition);
+ uint32_t fpos = pos;
+ int64_t time = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime);
+ if (time == 0)
+ fpos = pos = 0;
+ if (p->needs_timestamp_offset) {
+ if (time != 0 && !p->timestamp_offset)
+ p->timestamp_offset = now - time;
+ time += p->timestamp_offset;
+ }
+ if (fpos != 0 && time != 0 && state == AudioTrack.PLAYSTATE_PLAYING) {
+ double diff = (double)(now - time) / 1e9;
+ pos += diff * ao->samplerate;
+ }
+ //MP_VERBOSE(ao, "position = %u via getTimestamp (state = %d / fpos= %u / time= %"PRId64")\n", pos, state, fpos, time);
+ } else {
+ pos = 0xFFFFFFFFL & MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlaybackHeadPosition);
+ //MP_VERBOSE(ao, "playbackHeadPosition = %u (reset_pending=%d)\n", pos, p->reset_pending);
+ }
+
+
+ if (p->format == AudioFormat.ENCODING_IEC61937) {
+ if (p->reset_pending) {
+ // after a flush(), playbackHeadPosition will not reset to 0 right away.
+ // sometimes, it will never reset at all.
+ // save the initial offset after the reset, to subtract it going forward.
+ if (p->playhead_offset == 0)
+ p->playhead_offset = pos;
+ p->reset_pending = false;
+ MP_VERBOSE(ao, "IEC/playbackHead offset = %d\n", pos);
+ }
+
+ // usually shortly after a flush(), playbackHeadPosition will reset to 0.
+ // clear out the position and offset to avoid regular "rollover" below
+ if (pos == 0 && p->playhead_offset != 0) {
+ MP_VERBOSE(ao, "IEC/playbackHeadPosition %d -> %d (flush)\n", p->playhead_pos, pos);
+ p->playhead_offset = 0;
+ p->playhead_pos = 0;
+ }
+
+ // sometimes on a new AudioTrack instance, playbackHeadPosition will reset
+ // to 0 shortly after playback starts for no reason.
+ if (pos == 0 && p->playhead_pos != 0) {
+ MP_VERBOSE(ao, "IEC/playbackHeadPosition %d -> %d (reset)\n", p->playhead_pos, pos);
+ p->playhead_offset = 0;
+ p->playhead_pos = 0;
+ p->written_frames = 0;
+ }
+ }
+
+ p->playhead_pos = pos;
+ return p->playhead_pos - p->playhead_offset;
+}
+
+static double AudioTrack_getLatency(struct ao *ao)
+{
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ struct priv *p = ao->priv;
+ if (!p->audiotrack)
+ return 0;
+
+ uint32_t playhead = AudioTrack_getPlaybackHeadPosition(ao);
+ uint32_t diff = p->written_frames - playhead;
+ double delay = diff / (double)(ao->samplerate);
+ if (!p->timestamp_set &&
+ p->format != AudioFormat.ENCODING_IEC61937)
+ delay += (double)MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getLatency)/1000.0;
+ if (delay > 2.0) {
+ //MP_WARN(ao, "getLatency: written=%u playhead=%u diff=%u delay=%f\n", p->written_frames, playhead, diff, delay);
+ p->timestamp_fetched = 0;
+ return 0;
+ }
+ return MPCLAMP(delay, 0.0, 2.0);
+}
+
+static int AudioTrack_write(struct ao *ao, int len)
+{
+ struct priv *p = ao->priv;
+ if (!p->audiotrack)
+ return -1;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ void *buf = p->chunk;
+
+ jint ret;
+ if (p->format == AudioFormat.ENCODING_IEC61937) {
+ (*env)->SetShortArrayRegion(env, p->shortarray, 0, len / 2, buf);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeShortV23, p->shortarray, 0, len / 2, AudioTrack.WRITE_BLOCKING);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ if (ret > 0) ret *= 2;
+
+ } else if (AudioTrack.writeBufferV21) {
+ // reset positions for reading
+ jobject bbuf = MP_JNI_CALL_OBJECT(p->bbuf, ByteBuffer.clear);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ (*env)->DeleteLocalRef(env, bbuf);
+ ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeBufferV21, p->bbuf, len, AudioTrack.WRITE_BLOCKING);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+
+ } else if (p->format == AudioFormat.ENCODING_PCM_FLOAT) {
+ (*env)->SetFloatArrayRegion(env, p->floatarray, 0, len / sizeof(float), buf);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeFloat, p->floatarray, 0, len / sizeof(float), AudioTrack.WRITE_BLOCKING);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ if (ret > 0) ret *= sizeof(float);
+
+ } else {
+ (*env)->SetByteArrayRegion(env, p->bytearray, 0, len, buf);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.write, p->bytearray, 0, len);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
+ }
+
+ return ret;
+}
+
+static void uninit_jni(struct ao *ao)
+{
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ mp_jni_reset_jfields(env, &AudioTrack, AudioTrack.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &AudioTimestamp, AudioTimestamp.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &AudioManager, AudioManager.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &AudioFormat, AudioFormat.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &AudioFormatBuilder, AudioFormatBuilder.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &AudioAttributes, AudioAttributes.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &AudioAttributesBuilder, AudioAttributesBuilder.mapping, 1, ao->log);
+ mp_jni_reset_jfields(env, &ByteBuffer, ByteBuffer.mapping, 1, ao->log);
+}
+
+static int init_jni(struct ao *ao)
+{
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ if (mp_jni_init_jfields(env, &AudioTrack, AudioTrack.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &ByteBuffer, ByteBuffer.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &AudioTimestamp, AudioTimestamp.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &AudioManager, AudioManager.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &AudioAttributes, AudioAttributes.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &AudioAttributesBuilder, AudioAttributesBuilder.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &AudioFormatBuilder, AudioFormatBuilder.mapping, 1, ao->log) < 0 ||
+ mp_jni_init_jfields(env, &AudioFormat, AudioFormat.mapping, 1, ao->log) < 0) {
+ uninit_jni(ao);
+ return -1;
+ }
+
+ return 0;
+}
+
+static MP_THREAD_VOID playthread(void *arg)
+{
+ struct ao *ao = arg;
+ struct priv *p = ao->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ mp_thread_set_name("ao/audiotrack");
+ mp_mutex_lock(&p->lock);
+ while (!p->thread_terminate) {
+ int state = AudioTrack.PLAYSTATE_PAUSED;
+ if (p->audiotrack) {
+ state = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlayState);
+ }
+ if (state == AudioTrack.PLAYSTATE_PLAYING) {
+ int read_samples = p->chunksize / ao->sstride;
+ int64_t ts = mp_time_ns();
+ ts += MP_TIME_S_TO_NS(read_samples / (double)(ao->samplerate));
+ ts += MP_TIME_S_TO_NS(AudioTrack_getLatency(ao));
+ int samples = ao_read_data_nonblocking(ao, &p->chunk, read_samples, ts);
+ int ret = AudioTrack_write(ao, samples * ao->sstride);
+ if (ret >= 0) {
+ p->written_frames += ret / ao->sstride;
+ } else if (ret == AudioManager.ERROR_DEAD_OBJECT) {
+ MP_WARN(ao, "AudioTrack.write failed with ERROR_DEAD_OBJECT. Recreating AudioTrack...\n");
+ if (AudioTrack_Recreate(ao) < 0) {
+ MP_ERR(ao, "AudioTrack_Recreate failed\n");
+ }
+ } else {
+ MP_ERR(ao, "AudioTrack.write failed with %d\n", ret);
+ }
+ } else {
+ mp_cond_timedwait(&p->wakeup, &p->lock, MP_TIME_MS_TO_NS(300));
+ }
+ }
+ mp_mutex_unlock(&p->lock);
+ MP_THREAD_RETURN();
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ if (p->audiotrack) {
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.stop);
+ MP_JNI_EXCEPTION_LOG(ao);
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.flush);
+ MP_JNI_EXCEPTION_LOG(ao);
+ }
+
+ mp_mutex_lock(&p->lock);
+ p->thread_terminate = true;
+ mp_cond_signal(&p->wakeup);
+ mp_mutex_unlock(&p->lock);
+
+ if (p->thread_created)
+ mp_thread_join(p->thread);
+
+ if (p->audiotrack) {
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.release);
+ MP_JNI_EXCEPTION_LOG(ao);
+ (*env)->DeleteGlobalRef(env, p->audiotrack);
+ p->audiotrack = NULL;
+ }
+
+ if (p->bytearray) {
+ (*env)->DeleteGlobalRef(env, p->bytearray);
+ p->bytearray = NULL;
+ }
+
+ if (p->shortarray) {
+ (*env)->DeleteGlobalRef(env, p->shortarray);
+ p->shortarray = NULL;
+ }
+
+ if (p->floatarray) {
+ (*env)->DeleteGlobalRef(env, p->floatarray);
+ p->floatarray = NULL;
+ }
+
+ if (p->bbuf) {
+ (*env)->DeleteGlobalRef(env, p->bbuf);
+ p->bbuf = NULL;
+ }
+
+ if (p->timestamp) {
+ (*env)->DeleteGlobalRef(env, p->timestamp);
+ p->timestamp = NULL;
+ }
+
+ mp_cond_destroy(&p->wakeup);
+ mp_mutex_destroy(&p->lock);
+
+ uninit_jni(ao);
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ if (!env)
+ return -1;
+
+ mp_mutex_init(&p->lock);
+ mp_cond_init(&p->wakeup);
+
+ if (init_jni(ao) < 0)
+ return -1;
+
+ if (af_fmt_is_spdif(ao->format)) {
+ p->format = AudioFormat.ENCODING_IEC61937;
+ } else if (ao->format == AF_FORMAT_U8) {
+ p->format = AudioFormat.ENCODING_PCM_8BIT;
+ } else if (p->cfg_pcm_float && af_fmt_is_float(ao->format)) {
+ ao->format = AF_FORMAT_FLOAT;
+ p->format = AudioFormat.ENCODING_PCM_FLOAT;
+ } else {
+ ao->format = AF_FORMAT_S16;
+ p->format = AudioFormat.ENCODING_PCM_16BIT;
+ }
+
+ if (AudioTrack.getNativeOutputSampleRate) {
+ jint samplerate = MP_JNI_CALL_STATIC_INT(
+ AudioTrack.clazz,
+ AudioTrack.getNativeOutputSampleRate,
+ AudioManager.STREAM_MUSIC
+ );
+ if (MP_JNI_EXCEPTION_LOG(ao) == 0) {
+ MP_VERBOSE(ao, "AudioTrack.nativeOutputSampleRate = %d\n", samplerate);
+ ao->samplerate = MPMIN(samplerate, ao->samplerate);
+ }
+ }
+ p->samplerate = ao->samplerate;
+
+ /* https://developer.android.com/reference/android/media/AudioFormat#channelPositionMask */
+ static const struct mp_chmap layouts[] = {
+ {0}, // empty
+ MP_CHMAP_INIT_MONO, // mono
+ MP_CHMAP_INIT_STEREO, // stereo
+ MP_CHMAP3(FL, FR, FC), // 3.0
+ MP_CHMAP4(FL, FR, BL, BR), // quad
+ MP_CHMAP5(FL, FR, FC, BL, BR), // 5.0
+ MP_CHMAP6(FL, FR, FC, LFE, BL, BR), // 5.1
+ MP_CHMAP7(FL, FR, FC, LFE, BL, BR, BC), // 6.1
+ MP_CHMAP8(FL, FR, FC, LFE, BL, BR, SL, SR), // 7.1
+ };
+ const jint layout_map[] = {
+ 0,
+ AudioFormat.CHANNEL_OUT_MONO,
+ AudioFormat.CHANNEL_OUT_STEREO,
+ AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER,
+ AudioFormat.CHANNEL_OUT_QUAD,
+ AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER,
+ AudioFormat.CHANNEL_OUT_5POINT1,
+ AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER,
+ AudioFormat.CHANNEL_OUT_7POINT1_SURROUND,
+ };
+ static_assert(MP_ARRAY_SIZE(layout_map) == MP_ARRAY_SIZE(layouts), "");
+ if (p->format == AudioFormat.ENCODING_IEC61937) {
+ p->channel_config = AudioFormat.CHANNEL_OUT_STEREO;
+ } else {
+ struct mp_chmap_sel sel = {0};
+ for (int i = 0; i < MP_ARRAY_SIZE(layouts); i++) {
+ if (layout_map[i])
+ mp_chmap_sel_add_map(&sel, &layouts[i]);
+ }
+ if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
+ goto error;
+ p->channel_config = layout_map[ao->channels.num];
+ assert(p->channel_config);
+ }
+
+ jint buffer_size = MP_JNI_CALL_STATIC_INT(
+ AudioTrack.clazz,
+ AudioTrack.getMinBufferSize,
+ p->samplerate,
+ p->channel_config,
+ p->format
+ );
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0 || buffer_size <= 0) {
+ MP_FATAL(ao, "AudioTrack.getMinBufferSize returned an invalid size: %d", buffer_size);
+ return -1;
+ }
+
+ // Choose double of the minimum buffer size suggested by the driver, but not
+ // less than 75ms or more than 150ms.
+ const int bps = af_fmt_to_bytes(ao->format);
+ int min = 0.075 * p->samplerate * bps * ao->channels.num;
+ int max = min * 2;
+ min = MP_ALIGN_UP(min, bps);
+ max = MP_ALIGN_UP(max, bps);
+ p->size = MPCLAMP(buffer_size * 2, min, max);
+ MP_VERBOSE(ao, "Setting bufferSize = %d (driver=%d, min=%d, max=%d)\n", p->size, buffer_size, min, max);
+ assert(p->size % bps == 0);
+ ao->device_buffer = p->size / bps;
+
+ p->chunksize = p->size;
+ p->chunk = talloc_size(ao, p->size);
+
+ jobject timestamp = MP_JNI_NEW(AudioTimestamp.clazz, AudioTimestamp.ctor);
+ if (MP_JNI_EXCEPTION_LOG(ao) < 0 || !timestamp) {
+ MP_FATAL(ao, "AudioTimestamp could not be created\n");
+ return -1;
+ }
+ p->timestamp = (*env)->NewGlobalRef(env, timestamp);
+ (*env)->DeleteLocalRef(env, timestamp);
+
+ // decide and create buffer of right type
+ if (p->format == AudioFormat.ENCODING_IEC61937) {
+ jshortArray shortarray = (*env)->NewShortArray(env, p->chunksize / 2);
+ p->shortarray = (*env)->NewGlobalRef(env, shortarray);
+ (*env)->DeleteLocalRef(env, shortarray);
+ } else if (AudioTrack.writeBufferV21) {
+ MP_VERBOSE(ao, "Using NIO ByteBuffer\n");
+ jobject bbuf = (*env)->NewDirectByteBuffer(env, p->chunk, p->chunksize);
+ p->bbuf = (*env)->NewGlobalRef(env, bbuf);
+ (*env)->DeleteLocalRef(env, bbuf);
+ } else if (p->format == AudioFormat.ENCODING_PCM_FLOAT) {
+ jfloatArray floatarray = (*env)->NewFloatArray(env, p->chunksize / sizeof(float));
+ p->floatarray = (*env)->NewGlobalRef(env, floatarray);
+ (*env)->DeleteLocalRef(env, floatarray);
+ } else {
+ jbyteArray bytearray = (*env)->NewByteArray(env, p->chunksize);
+ p->bytearray = (*env)->NewGlobalRef(env, bytearray);
+ (*env)->DeleteLocalRef(env, bytearray);
+ }
+
+ /* create AudioTrack object */
+ if (AudioTrack_New(ao) != 0) {
+ MP_FATAL(ao, "Failed to create AudioTrack\n");
+ goto error;
+ }
+
+ if (mp_thread_create(&p->thread, playthread, ao)) {
+ MP_ERR(ao, "pthread creation failed\n");
+ goto error;
+ }
+ p->thread_created = true;
+
+ return 1;
+
+error:
+ uninit(ao);
+ return -1;
+}
+
+static void stop(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ if (!p->audiotrack) {
+ MP_ERR(ao, "AudioTrack does not exist to stop!\n");
+ return;
+ }
+
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.pause);
+ MP_JNI_EXCEPTION_LOG(ao);
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.flush);
+ MP_JNI_EXCEPTION_LOG(ao);
+
+ p->playhead_offset = 0;
+ p->reset_pending = true;
+ p->written_frames = 0;
+ p->timestamp_fetched = 0;
+ p->timestamp_set = false;
+ p->timestamp_offset = 0;
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ if (!p->audiotrack) {
+ MP_ERR(ao, "AudioTrack does not exist to start!\n");
+ return;
+ }
+
+ JNIEnv *env = MP_JNI_GET_ENV(ao);
+ MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.play);
+ MP_JNI_EXCEPTION_LOG(ao);
+
+ mp_cond_signal(&p->wakeup);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_audiotrack = {
+ .description = "Android AudioTrack audio output",
+ .name = "audiotrack",
+ .init = init,
+ .uninit = uninit,
+ .reset = stop,
+ .start = start,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const OPT_BASE_STRUCT) {
+ .cfg_pcm_float = 1,
+ },
+ .options = (const struct m_option[]) {
+ {"pcm-float", OPT_BOOL(cfg_pcm_float)},
+ {"session-id", OPT_INT(cfg_session_id)},
+ {0}
+ },
+ .options_prefix = "audiotrack",
+};
diff --git a/audio/out/ao_audiounit.m b/audio/out/ao_audiounit.m
new file mode 100644
index 0000000..85b1226
--- /dev/null
+++ b/audio/out/ao_audiounit.m
@@ -0,0 +1,260 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+#include "osdep/timer.h"
+#include "options/m_option.h"
+#include "common/msg.h"
+#include "ao_coreaudio_utils.h"
+#include "ao_coreaudio_chmap.h"
+
+#import <AudioUnit/AudioUnit.h>
+#import <CoreAudio/CoreAudioTypes.h>
+#import <AudioToolbox/AudioToolbox.h>
+#import <AVFoundation/AVFoundation.h>
+#import <mach/mach_time.h>
+
+struct priv {
+ AudioUnit audio_unit;
+ double device_latency;
+};
+
+static OSStatus au_get_ary(AudioUnit unit, AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void **data, UInt32 *outDataSize)
+{
+ OSStatus err;
+
+ err = AudioUnitGetPropertyInfo(unit, inID, inScope, inElement, outDataSize, NULL);
+ CHECK_CA_ERROR_SILENT_L(coreaudio_error);
+
+ *data = talloc_zero_size(NULL, *outDataSize);
+
+ err = AudioUnitGetProperty(unit, inID, inScope, inElement, *data, outDataSize);
+ CHECK_CA_ERROR_SILENT_L(coreaudio_error_free);
+
+ return err;
+coreaudio_error_free:
+ talloc_free(*data);
+coreaudio_error:
+ return err;
+}
+
+static AudioChannelLayout *convert_layout(AudioChannelLayout *layout, UInt32* size)
+{
+ AudioChannelLayoutTag tag = layout->mChannelLayoutTag;
+ AudioChannelLayout *new_layout;
+ if (tag == kAudioChannelLayoutTag_UseChannelDescriptions)
+ return layout;
+ else if (tag == kAudioChannelLayoutTag_UseChannelBitmap)
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(UInt32), &layout->mChannelBitmap, size);
+ else
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(AudioChannelLayoutTag), &tag, size);
+ new_layout = talloc_zero_size(NULL, *size);
+ if (!new_layout) {
+ talloc_free(layout);
+ return NULL;
+ }
+ if (tag == kAudioChannelLayoutTag_UseChannelBitmap)
+ AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(UInt32), &layout->mChannelBitmap, size, new_layout);
+ else
+ AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(AudioChannelLayoutTag), &tag, size, new_layout);
+ new_layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
+ talloc_free(layout);
+ return new_layout;
+}
+
+
+static OSStatus render_cb_lpcm(void *ctx, AudioUnitRenderActionFlags *aflags,
+ const AudioTimeStamp *ts, UInt32 bus,
+ UInt32 frames, AudioBufferList *buffer_list)
+{
+ struct ao *ao = ctx;
+ struct priv *p = ao->priv;
+ void *planes[MP_NUM_CHANNELS] = {0};
+
+ for (int n = 0; n < ao->num_planes; n++)
+ planes[n] = buffer_list->mBuffers[n].mData;
+
+ int64_t end = mp_time_ns();
+ end += MP_TIME_S_TO_NS(p->device_latency);
+ end += ca_get_latency(ts) + ca_frames_to_ns(ao, frames);
+ ao_read_data(ao, planes, frames, end);
+ return noErr;
+}
+
+static bool init_audiounit(struct ao *ao)
+{
+ AudioStreamBasicDescription asbd;
+ OSStatus err;
+ uint32_t size;
+ AudioChannelLayout *layout = NULL;
+ struct priv *p = ao->priv;
+ AVAudioSession *instance = AVAudioSession.sharedInstance;
+ AVAudioSessionPortDescription *port = nil;
+ NSInteger maxChannels = instance.maximumOutputNumberOfChannels;
+ NSInteger prefChannels = MIN(maxChannels, ao->channels.num);
+
+ MP_VERBOSE(ao, "max channels: %ld, requested: %d\n", maxChannels, (int)ao->channels.num);
+
+ [instance setCategory:AVAudioSessionCategoryPlayback error:nil];
+ [instance setMode:AVAudioSessionModeMoviePlayback error:nil];
+ [instance setActive:YES error:nil];
+ [instance setPreferredOutputNumberOfChannels:prefChannels error:nil];
+
+ AudioComponentDescription desc = (AudioComponentDescription) {
+ .componentType = kAudioUnitType_Output,
+ .componentSubType = kAudioUnitSubType_RemoteIO,
+ .componentManufacturer = kAudioUnitManufacturer_Apple,
+ .componentFlags = 0,
+ .componentFlagsMask = 0,
+ };
+
+ AudioComponent comp = AudioComponentFindNext(NULL, &desc);
+ if (comp == NULL) {
+ MP_ERR(ao, "unable to find audio component\n");
+ goto coreaudio_error;
+ }
+
+ err = AudioComponentInstanceNew(comp, &(p->audio_unit));
+ CHECK_CA_ERROR("unable to open audio component");
+
+ err = AudioUnitInitialize(p->audio_unit);
+ CHECK_CA_ERROR_L(coreaudio_error_component,
+ "unable to initialize audio unit");
+
+ err = au_get_ary(p->audio_unit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output,
+ 0, (void**)&layout, &size);
+ CHECK_CA_ERROR_L(coreaudio_error_audiounit,
+ "unable to retrieve audio unit channel layout");
+
+ MP_VERBOSE(ao, "AU channel layout tag: %x (%x)\n", layout->mChannelLayoutTag, layout->mChannelBitmap);
+
+ layout = convert_layout(layout, &size);
+ if (!layout) {
+ MP_ERR(ao, "unable to convert channel layout to list format\n");
+ goto coreaudio_error_audiounit;
+ }
+
+ for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; i++) {
+ MP_VERBOSE(ao, "channel map: %i: %u\n", i, layout->mChannelDescriptions[i].mChannelLabel);
+ }
+
+ if (af_fmt_is_spdif(ao->format) || instance.outputNumberOfChannels <= 2) {
+ ao->channels = (struct mp_chmap)MP_CHMAP_INIT_STEREO;
+ MP_VERBOSE(ao, "using stereo output\n");
+ } else {
+ ao->channels.num = (uint8_t)layout->mNumberChannelDescriptions;
+ for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; i++) {
+ ao->channels.speaker[i] =
+ ca_label_to_mp_speaker_id(layout->mChannelDescriptions[i].mChannelLabel);
+ }
+ MP_VERBOSE(ao, "using standard channel mapping\n");
+ }
+
+ ca_fill_asbd(ao, &asbd);
+ size = sizeof(AudioStreamBasicDescription);
+ err = AudioUnitSetProperty(p->audio_unit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input, 0, &asbd, size);
+
+ CHECK_CA_ERROR_L(coreaudio_error_audiounit,
+ "unable to set the input format on the audio unit");
+
+ AURenderCallbackStruct render_cb = (AURenderCallbackStruct) {
+ .inputProc = render_cb_lpcm,
+ .inputProcRefCon = ao,
+ };
+
+ err = AudioUnitSetProperty(p->audio_unit,
+ kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0, &render_cb,
+ sizeof(AURenderCallbackStruct));
+
+ CHECK_CA_ERROR_L(coreaudio_error_audiounit,
+ "unable to set render callback on audio unit");
+
+ talloc_free(layout);
+
+ return true;
+
+coreaudio_error_audiounit:
+ AudioUnitUninitialize(p->audio_unit);
+coreaudio_error_component:
+ AudioComponentInstanceDispose(p->audio_unit);
+coreaudio_error:
+ talloc_free(layout);
+ return false;
+}
+
+static void stop(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ OSStatus err = AudioOutputUnitStop(p->audio_unit);
+ CHECK_CA_WARN("can't stop audio unit");
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ AVAudioSession *instance = AVAudioSession.sharedInstance;
+
+ p->device_latency = [instance outputLatency];
+
+ OSStatus err = AudioOutputUnitStart(p->audio_unit);
+ CHECK_CA_WARN("can't start audio unit");
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ AudioOutputUnitStop(p->audio_unit);
+ AudioUnitUninitialize(p->audio_unit);
+ AudioComponentInstanceDispose(p->audio_unit);
+
+ [AVAudioSession.sharedInstance
+ setActive:NO
+ withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
+ error:nil];
+}
+
+static int init(struct ao *ao)
+{
+ if (!init_audiounit(ao))
+ goto coreaudio_error;
+
+ return CONTROL_OK;
+
+coreaudio_error:
+ return CONTROL_ERROR;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_audiounit = {
+ .description = "AudioUnit (iOS)",
+ .name = "audiounit",
+ .uninit = uninit,
+ .init = init,
+ .reset = stop,
+ .start = start,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/audio/out/ao_coreaudio.c b/audio/out/ao_coreaudio.c
new file mode 100644
index 0000000..37f1313
--- /dev/null
+++ b/audio/out/ao_coreaudio.c
@@ -0,0 +1,435 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <CoreAudio/HostTime.h>
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+#include "osdep/timer.h"
+#include "options/m_option.h"
+#include "common/msg.h"
+#include "ao_coreaudio_chmap.h"
+#include "ao_coreaudio_properties.h"
+#include "ao_coreaudio_utils.h"
+
+struct priv {
+ AudioDeviceID device;
+ AudioUnit audio_unit;
+
+ uint64_t hw_latency_ns;
+
+ AudioStreamBasicDescription original_asbd;
+ AudioStreamID original_asbd_stream;
+
+ bool change_physical_format;
+};
+
+static int64_t ca_get_hardware_latency(struct ao *ao) {
+ struct priv *p = ao->priv;
+
+ double audiounit_latency_sec = 0.0;
+ uint32_t size = sizeof(audiounit_latency_sec);
+ OSStatus err = AudioUnitGetProperty(
+ p->audio_unit,
+ kAudioUnitProperty_Latency,
+ kAudioUnitScope_Global,
+ 0,
+ &audiounit_latency_sec,
+ &size);
+ CHECK_CA_ERROR("cannot get audio unit latency");
+
+ uint64_t audiounit_latency_ns = MP_TIME_S_TO_NS(audiounit_latency_sec);
+ uint64_t device_latency_ns = ca_get_device_latency_ns(ao, p->device);
+
+ MP_VERBOSE(ao, "audiounit latency [ns]: %lld\n", audiounit_latency_ns);
+ MP_VERBOSE(ao, "device latency [ns]: %lld\n", device_latency_ns);
+
+ return audiounit_latency_ns + device_latency_ns;
+
+coreaudio_error:
+ return 0;
+}
+
+static OSStatus render_cb_lpcm(void *ctx, AudioUnitRenderActionFlags *aflags,
+ const AudioTimeStamp *ts, UInt32 bus,
+ UInt32 frames, AudioBufferList *buffer_list)
+{
+ struct ao *ao = ctx;
+ struct priv *p = ao->priv;
+ void *planes[MP_NUM_CHANNELS] = {0};
+
+ for (int n = 0; n < ao->num_planes; n++)
+ planes[n] = buffer_list->mBuffers[n].mData;
+
+ int64_t end = mp_time_ns();
+ end += p->hw_latency_ns + ca_get_latency(ts) + ca_frames_to_ns(ao, frames);
+ int samples = ao_read_data_nonblocking(ao, planes, frames, end);
+
+ if (samples == 0)
+ *aflags |= kAudioUnitRenderAction_OutputIsSilence;
+
+ for (int n = 0; n < buffer_list->mNumberBuffers; n++)
+ buffer_list->mBuffers[n].mDataByteSize = samples * ao->sstride;
+
+ return noErr;
+}
+
+static int get_volume(struct ao *ao, float *vol) {
+ struct priv *p = ao->priv;
+ float auvol;
+ OSStatus err =
+ AudioUnitGetParameter(p->audio_unit, kHALOutputParam_Volume,
+ kAudioUnitScope_Global, 0, &auvol);
+
+ CHECK_CA_ERROR("could not get HAL output volume");
+ *vol = auvol * 100.0;
+ return CONTROL_TRUE;
+coreaudio_error:
+ return CONTROL_ERROR;
+}
+
+static int set_volume(struct ao *ao, float *vol) {
+ struct priv *p = ao->priv;
+ float auvol = *vol / 100.0;
+ OSStatus err =
+ AudioUnitSetParameter(p->audio_unit, kHALOutputParam_Volume,
+ kAudioUnitScope_Global, 0, auvol, 0);
+ CHECK_CA_ERROR("could not set HAL output volume");
+ return CONTROL_TRUE;
+coreaudio_error:
+ return CONTROL_ERROR;
+}
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME:
+ return get_volume(ao, arg);
+ case AOCONTROL_SET_VOLUME:
+ return set_volume(ao, arg);
+ }
+ return CONTROL_UNKNOWN;
+}
+
+static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd);
+static void init_physical_format(struct ao *ao);
+
+static bool reinit_device(struct ao *ao) {
+ struct priv *p = ao->priv;
+
+ OSStatus err = ca_select_device(ao, ao->device, &p->device);
+ CHECK_CA_ERROR("failed to select device");
+
+ return true;
+
+coreaudio_error:
+ return false;
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ if (!af_fmt_is_pcm(ao->format) || (ao->init_flags & AO_INIT_EXCLUSIVE)) {
+ MP_VERBOSE(ao, "redirecting to coreaudio_exclusive\n");
+ ao->redirect = "coreaudio_exclusive";
+ return CONTROL_ERROR;
+ }
+
+ if (!reinit_device(ao))
+ goto coreaudio_error;
+
+ if (p->change_physical_format)
+ init_physical_format(ao);
+
+ if (!ca_init_chmap(ao, p->device))
+ goto coreaudio_error;
+
+ AudioStreamBasicDescription asbd;
+ ca_fill_asbd(ao, &asbd);
+
+ if (!init_audiounit(ao, asbd))
+ goto coreaudio_error;
+
+ return CONTROL_OK;
+
+coreaudio_error:
+ return CONTROL_ERROR;
+}
+
+static void init_physical_format(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ OSErr err;
+
+ void *tmp = talloc_new(NULL);
+
+ AudioStreamBasicDescription asbd;
+ ca_fill_asbd(ao, &asbd);
+
+ AudioStreamID *streams;
+ size_t n_streams;
+
+ err = CA_GET_ARY_O(p->device, kAudioDevicePropertyStreams,
+ &streams, &n_streams);
+ CHECK_CA_ERROR("could not get number of streams");
+
+ talloc_steal(tmp, streams);
+
+ MP_VERBOSE(ao, "Found %zd substream(s).\n", n_streams);
+
+ for (int i = 0; i < n_streams; i++) {
+ AudioStreamRangedDescription *formats;
+ size_t n_formats;
+
+ MP_VERBOSE(ao, "Looking at formats in substream %d...\n", i);
+
+ err = CA_GET_ARY(streams[i], kAudioStreamPropertyAvailablePhysicalFormats,
+ &formats, &n_formats);
+
+ if (!CHECK_CA_WARN("could not get number of stream formats"))
+ continue; // try next one
+
+ talloc_steal(tmp, formats);
+
+ uint32_t direction;
+ err = CA_GET(streams[i], kAudioStreamPropertyDirection, &direction);
+ CHECK_CA_ERROR("could not get stream direction");
+ if (direction != 0) {
+ MP_VERBOSE(ao, "Not an output stream.\n");
+ continue;
+ }
+
+ AudioStreamBasicDescription best_asbd = {0};
+
+ for (int j = 0; j < n_formats; j++) {
+ AudioStreamBasicDescription *stream_asbd = &formats[j].mFormat;
+
+ ca_print_asbd(ao, "- ", stream_asbd);
+
+ if (!best_asbd.mFormatID || ca_asbd_is_better(&asbd, &best_asbd,
+ stream_asbd))
+ best_asbd = *stream_asbd;
+ }
+
+ if (best_asbd.mFormatID) {
+ p->original_asbd_stream = streams[i];
+ err = CA_GET(p->original_asbd_stream,
+ kAudioStreamPropertyPhysicalFormat,
+ &p->original_asbd);
+ CHECK_CA_WARN("could not get current physical stream format");
+
+ if (ca_asbd_equals(&p->original_asbd, &best_asbd)) {
+ MP_VERBOSE(ao, "Requested format already set, not changing.\n");
+ p->original_asbd.mFormatID = 0;
+ break;
+ }
+
+ if (!ca_change_physical_format_sync(ao, streams[i], best_asbd))
+ p->original_asbd = (AudioStreamBasicDescription){0};
+ break;
+ }
+ }
+
+coreaudio_error:
+ talloc_free(tmp);
+ return;
+}
+
+static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd)
+{
+ OSStatus err;
+ uint32_t size;
+ struct priv *p = ao->priv;
+
+ AudioComponentDescription desc = (AudioComponentDescription) {
+ .componentType = kAudioUnitType_Output,
+ .componentSubType = (ao->device) ?
+ kAudioUnitSubType_HALOutput :
+ kAudioUnitSubType_DefaultOutput,
+ .componentManufacturer = kAudioUnitManufacturer_Apple,
+ .componentFlags = 0,
+ .componentFlagsMask = 0,
+ };
+
+ AudioComponent comp = AudioComponentFindNext(NULL, &desc);
+ if (comp == NULL) {
+ MP_ERR(ao, "unable to find audio component\n");
+ goto coreaudio_error;
+ }
+
+ err = AudioComponentInstanceNew(comp, &(p->audio_unit));
+ CHECK_CA_ERROR("unable to open audio component");
+
+ err = AudioUnitInitialize(p->audio_unit);
+ CHECK_CA_ERROR_L(coreaudio_error_component,
+ "unable to initialize audio unit");
+
+ size = sizeof(AudioStreamBasicDescription);
+ err = AudioUnitSetProperty(p->audio_unit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input, 0, &asbd, size);
+
+ CHECK_CA_ERROR_L(coreaudio_error_audiounit,
+ "unable to set the input format on the audio unit");
+
+ err = AudioUnitSetProperty(p->audio_unit,
+ kAudioOutputUnitProperty_CurrentDevice,
+ kAudioUnitScope_Global, 0, &p->device,
+ sizeof(p->device));
+ CHECK_CA_ERROR_L(coreaudio_error_audiounit,
+ "can't link audio unit to selected device");
+
+ p->hw_latency_ns = ca_get_hardware_latency(ao);
+
+ AURenderCallbackStruct render_cb = (AURenderCallbackStruct) {
+ .inputProc = render_cb_lpcm,
+ .inputProcRefCon = ao,
+ };
+
+ err = AudioUnitSetProperty(p->audio_unit,
+ kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0, &render_cb,
+ sizeof(AURenderCallbackStruct));
+
+ CHECK_CA_ERROR_L(coreaudio_error_audiounit,
+ "unable to set render callback on audio unit");
+
+ return true;
+
+coreaudio_error_audiounit:
+ AudioUnitUninitialize(p->audio_unit);
+coreaudio_error_component:
+ AudioComponentInstanceDispose(p->audio_unit);
+coreaudio_error:
+ return false;
+}
+
+static void reset(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ OSStatus err = AudioUnitReset(p->audio_unit, kAudioUnitScope_Global, 0);
+ CHECK_CA_WARN("can't reset audio unit");
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ OSStatus err = AudioOutputUnitStart(p->audio_unit);
+ CHECK_CA_WARN("can't start audio unit");
+}
+
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ AudioOutputUnitStop(p->audio_unit);
+ AudioUnitUninitialize(p->audio_unit);
+ AudioComponentInstanceDispose(p->audio_unit);
+
+ if (p->original_asbd.mFormatID) {
+ OSStatus err = CA_SET(p->original_asbd_stream,
+ kAudioStreamPropertyPhysicalFormat,
+ &p->original_asbd);
+ CHECK_CA_WARN("could not restore physical stream format");
+ }
+}
+
+static OSStatus hotplug_cb(AudioObjectID id, UInt32 naddr,
+ const AudioObjectPropertyAddress addr[],
+ void *ctx)
+{
+ struct ao *ao = ctx;
+ MP_VERBOSE(ao, "Handling potential hotplug event...\n");
+ reinit_device(ao);
+ ao_hotplug_event(ao);
+ return noErr;
+}
+
+static uint32_t hotplug_properties[] = {
+ kAudioHardwarePropertyDevices,
+ kAudioHardwarePropertyDefaultOutputDevice
+};
+
+static int hotplug_init(struct ao *ao)
+{
+ if (!reinit_device(ao))
+ goto coreaudio_error;
+
+ OSStatus err = noErr;
+ for (int i = 0; i < MP_ARRAY_SIZE(hotplug_properties); i++) {
+ AudioObjectPropertyAddress addr = {
+ hotplug_properties[i],
+ kAudioObjectPropertyScopeGlobal,
+ kAudioObjectPropertyElementMaster
+ };
+ err = AudioObjectAddPropertyListener(
+ kAudioObjectSystemObject, &addr, hotplug_cb, (void *)ao);
+ if (err != noErr) {
+ char *c1 = mp_tag_str(hotplug_properties[i]);
+ char *c2 = mp_tag_str(err);
+ MP_ERR(ao, "failed to set device listener %s (%s)", c1, c2);
+ goto coreaudio_error;
+ }
+ }
+
+ return 0;
+
+coreaudio_error:
+ return -1;
+}
+
+static void hotplug_uninit(struct ao *ao)
+{
+ OSStatus err = noErr;
+ for (int i = 0; i < MP_ARRAY_SIZE(hotplug_properties); i++) {
+ AudioObjectPropertyAddress addr = {
+ hotplug_properties[i],
+ kAudioObjectPropertyScopeGlobal,
+ kAudioObjectPropertyElementMaster
+ };
+ err = AudioObjectRemovePropertyListener(
+ kAudioObjectSystemObject, &addr, hotplug_cb, (void *)ao);
+ if (err != noErr) {
+ char *c1 = mp_tag_str(hotplug_properties[i]);
+ char *c2 = mp_tag_str(err);
+ MP_ERR(ao, "failed to set device listener %s (%s)", c1, c2);
+ }
+ }
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_coreaudio = {
+ .description = "CoreAudio AudioUnit",
+ .name = "coreaudio",
+ .uninit = uninit,
+ .init = init,
+ .control = control,
+ .reset = reset,
+ .start = start,
+ .hotplug_init = hotplug_init,
+ .hotplug_uninit = hotplug_uninit,
+ .list_devs = ca_get_device_list,
+ .priv_size = sizeof(struct priv),
+ .options = (const struct m_option[]){
+ {"change-physical-format", OPT_BOOL(change_physical_format)},
+ {0}
+ },
+ .options_prefix = "coreaudio",
+};
diff --git a/audio/out/ao_coreaudio_chmap.c b/audio/out/ao_coreaudio_chmap.c
new file mode 100644
index 0000000..3fd9550
--- /dev/null
+++ b/audio/out/ao_coreaudio_chmap.c
@@ -0,0 +1,340 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <Availability.h>
+
+#include "common/common.h"
+
+#include "ao_coreaudio_utils.h"
+
+#include "ao_coreaudio_chmap.h"
+
+static const int speaker_map[][2] = {
+ { kAudioChannelLabel_Left, MP_SPEAKER_ID_FL },
+ { kAudioChannelLabel_Right, MP_SPEAKER_ID_FR },
+ { kAudioChannelLabel_Center, MP_SPEAKER_ID_FC },
+ { kAudioChannelLabel_LFEScreen, MP_SPEAKER_ID_LFE },
+ { kAudioChannelLabel_LeftSurround, MP_SPEAKER_ID_BL },
+ { kAudioChannelLabel_RightSurround, MP_SPEAKER_ID_BR },
+ { kAudioChannelLabel_LeftCenter, MP_SPEAKER_ID_FLC },
+ { kAudioChannelLabel_RightCenter, MP_SPEAKER_ID_FRC },
+ { kAudioChannelLabel_CenterSurround, MP_SPEAKER_ID_BC },
+ { kAudioChannelLabel_LeftSurroundDirect, MP_SPEAKER_ID_SL },
+ { kAudioChannelLabel_RightSurroundDirect, MP_SPEAKER_ID_SR },
+ { kAudioChannelLabel_TopCenterSurround, MP_SPEAKER_ID_TC },
+ { kAudioChannelLabel_VerticalHeightLeft, MP_SPEAKER_ID_TFL },
+ { kAudioChannelLabel_VerticalHeightCenter, MP_SPEAKER_ID_TFC },
+ { kAudioChannelLabel_VerticalHeightRight, MP_SPEAKER_ID_TFR },
+ { kAudioChannelLabel_TopBackLeft, MP_SPEAKER_ID_TBL },
+ { kAudioChannelLabel_TopBackCenter, MP_SPEAKER_ID_TBC },
+ { kAudioChannelLabel_TopBackRight, MP_SPEAKER_ID_TBR },
+
+ // unofficial extensions
+ { kAudioChannelLabel_RearSurroundLeft, MP_SPEAKER_ID_SDL },
+ { kAudioChannelLabel_RearSurroundRight, MP_SPEAKER_ID_SDR },
+ { kAudioChannelLabel_LeftWide, MP_SPEAKER_ID_WL },
+ { kAudioChannelLabel_RightWide, MP_SPEAKER_ID_WR },
+ { kAudioChannelLabel_LFE2, MP_SPEAKER_ID_LFE2 },
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000
+ { kAudioChannelLabel_LeftTopSurround, MP_SPEAKER_ID_TSL },
+ { kAudioChannelLabel_RightTopSurround, MP_SPEAKER_ID_TSR },
+ { kAudioChannelLabel_CenterBottom, MP_SPEAKER_ID_BFC },
+ { kAudioChannelLabel_LeftBottom, MP_SPEAKER_ID_BFL },
+ { kAudioChannelLabel_RightBottom, MP_SPEAKER_ID_BFR },
+#endif
+
+ { kAudioChannelLabel_HeadphonesLeft, MP_SPEAKER_ID_DL },
+ { kAudioChannelLabel_HeadphonesRight, MP_SPEAKER_ID_DR },
+
+ { kAudioChannelLabel_Unknown, MP_SPEAKER_ID_NA },
+
+ { 0, -1 },
+};
+
+int ca_label_to_mp_speaker_id(AudioChannelLabel label)
+{
+ for (int i = 0; speaker_map[i][1] >= 0; i++)
+ if (speaker_map[i][0] == label)
+ return speaker_map[i][1];
+ return -1;
+}
+
+#if HAVE_COREAUDIO
+static void ca_log_layout(struct ao *ao, int l, AudioChannelLayout *layout)
+{
+ if (!mp_msg_test(ao->log, l))
+ return;
+
+ AudioChannelDescription *descs = layout->mChannelDescriptions;
+
+ mp_msg(ao->log, l, "layout: tag: <%u>, bitmap: <%u>, "
+ "descriptions <%u>\n",
+ (unsigned) layout->mChannelLayoutTag,
+ (unsigned) layout->mChannelBitmap,
+ (unsigned) layout->mNumberChannelDescriptions);
+
+ for (int i = 0; i < layout->mNumberChannelDescriptions; i++) {
+ AudioChannelDescription d = descs[i];
+ mp_msg(ao->log, l, " - description %d: label <%u, %u>, "
+ " flags: <%u>, coords: <%f, %f, %f>\n", i,
+ (unsigned) d.mChannelLabel,
+ (unsigned) ca_label_to_mp_speaker_id(d.mChannelLabel),
+ (unsigned) d.mChannelFlags,
+ d.mCoordinates[0],
+ d.mCoordinates[1],
+ d.mCoordinates[2]);
+ }
+}
+
+static AudioChannelLayout *ca_layout_to_custom_layout(struct ao *ao,
+ void *talloc_ctx,
+ AudioChannelLayout *l)
+{
+ AudioChannelLayoutTag tag = l->mChannelLayoutTag;
+ AudioChannelLayout *r;
+ OSStatus err;
+
+ if (tag == kAudioChannelLayoutTag_UseChannelDescriptions)
+ return l;
+
+ if (tag == kAudioChannelLayoutTag_UseChannelBitmap) {
+ uint32_t psize;
+ err = AudioFormatGetPropertyInfo(
+ kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(uint32_t), &l->mChannelBitmap, &psize);
+ CHECK_CA_ERROR("failed to convert channel bitmap to descriptions (info)");
+ r = talloc_size(NULL, psize);
+ err = AudioFormatGetProperty(
+ kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(uint32_t), &l->mChannelBitmap, &psize, r);
+ CHECK_CA_ERROR("failed to convert channel bitmap to descriptions (get)");
+ } else {
+ uint32_t psize;
+ err = AudioFormatGetPropertyInfo(
+ kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(AudioChannelLayoutTag), &l->mChannelLayoutTag, &psize);
+ r = talloc_size(NULL, psize);
+ CHECK_CA_ERROR("failed to convert channel tag to descriptions (info)");
+ err = AudioFormatGetProperty(
+ kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(AudioChannelLayoutTag), &l->mChannelLayoutTag, &psize, r);
+ CHECK_CA_ERROR("failed to convert channel tag to descriptions (get)");
+ }
+
+ MP_VERBOSE(ao, "converted input channel layout:\n");
+ ca_log_layout(ao, MSGL_V, l);
+
+ return r;
+coreaudio_error:
+ return NULL;
+}
+
+
+#define CHMAP(n, ...) &(struct mp_chmap) MP_CONCAT(MP_CHMAP, n) (__VA_ARGS__)
+
+// Replace each channel in a with b (a->num == b->num)
+static void replace_submap(struct mp_chmap *dst, struct mp_chmap *a,
+ struct mp_chmap *b)
+{
+ struct mp_chmap t = *dst;
+ if (!mp_chmap_is_valid(&t) || mp_chmap_diffn(a, &t) != 0)
+ return;
+ assert(a->num == b->num);
+ for (int n = 0; n < t.num; n++) {
+ for (int i = 0; i < a->num; i++) {
+ if (t.speaker[n] == a->speaker[i]) {
+ t.speaker[n] = b->speaker[i];
+ break;
+ }
+ }
+ }
+ if (mp_chmap_is_valid(&t))
+ *dst = t;
+}
+
+static bool ca_layout_to_mp_chmap(struct ao *ao, AudioChannelLayout *layout,
+ struct mp_chmap *chmap)
+{
+ void *talloc_ctx = talloc_new(NULL);
+
+ MP_VERBOSE(ao, "input channel layout:\n");
+ ca_log_layout(ao, MSGL_V, layout);
+
+ AudioChannelLayout *l = ca_layout_to_custom_layout(ao, talloc_ctx, layout);
+ if (!l)
+ goto coreaudio_error;
+
+ if (l->mNumberChannelDescriptions > MP_NUM_CHANNELS) {
+ MP_VERBOSE(ao, "layout has too many descriptions (%u, max: %d)\n",
+ (unsigned) l->mNumberChannelDescriptions, MP_NUM_CHANNELS);
+ return false;
+ }
+
+ chmap->num = l->mNumberChannelDescriptions;
+ for (int n = 0; n < l->mNumberChannelDescriptions; n++) {
+ AudioChannelLabel label = l->mChannelDescriptions[n].mChannelLabel;
+ int speaker = ca_label_to_mp_speaker_id(label);
+ if (speaker < 0) {
+ MP_VERBOSE(ao, "channel label=%u unusable to build channel "
+ "bitmap, skipping layout\n", (unsigned) label);
+ goto coreaudio_error;
+ }
+ chmap->speaker[n] = speaker;
+ }
+
+ // Remap weird 7.1(rear) layouts correctly.
+ replace_submap(chmap, CHMAP(6, FL, FR, BL, BR, SDL, SDR),
+ CHMAP(6, FL, FR, SL, SR, BL, BR));
+
+ talloc_free(talloc_ctx);
+ MP_VERBOSE(ao, "mp chmap: %s\n", mp_chmap_to_str(chmap));
+ return mp_chmap_is_valid(chmap) && !mp_chmap_is_unknown(chmap);
+coreaudio_error:
+ MP_VERBOSE(ao, "converted input channel layout (failed):\n");
+ ca_log_layout(ao, MSGL_V, layout);
+ talloc_free(talloc_ctx);
+ return false;
+}
+
+static AudioChannelLayout* ca_query_layout(struct ao *ao,
+ AudioDeviceID device,
+ void *talloc_ctx)
+{
+ OSStatus err;
+ uint32_t psize;
+ AudioChannelLayout *r = NULL;
+
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = kAudioDevicePropertyPreferredChannelLayout,
+ .mScope = kAudioDevicePropertyScopeOutput,
+ .mElement = kAudioObjectPropertyElementWildcard,
+ };
+
+ err = AudioObjectGetPropertyDataSize(device, &p_addr, 0, NULL, &psize);
+ CHECK_CA_ERROR("could not get device preferred layout (size)");
+
+ r = talloc_size(talloc_ctx, psize);
+
+ err = AudioObjectGetPropertyData(device, &p_addr, 0, NULL, &psize, r);
+ CHECK_CA_ERROR("could not get device preferred layout (get)");
+
+coreaudio_error:
+ return r;
+}
+
+static AudioChannelLayout* ca_query_stereo_layout(struct ao *ao,
+ AudioDeviceID device,
+ void *talloc_ctx)
+{
+ OSStatus err;
+ const int nch = 2;
+ uint32_t channels[nch];
+ AudioChannelLayout *r = NULL;
+
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = kAudioDevicePropertyPreferredChannelsForStereo,
+ .mScope = kAudioDevicePropertyScopeOutput,
+ .mElement = kAudioObjectPropertyElementWildcard,
+ };
+
+ uint32_t psize = sizeof(channels);
+ err = AudioObjectGetPropertyData(device, &p_addr, 0, NULL, &psize, channels);
+ CHECK_CA_ERROR("could not get device preferred stereo layout");
+
+ psize = sizeof(AudioChannelLayout) + nch * sizeof(AudioChannelDescription);
+ r = talloc_zero_size(talloc_ctx, psize);
+ r->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
+ r->mNumberChannelDescriptions = nch;
+
+ AudioChannelDescription desc = {0};
+ desc.mChannelFlags = kAudioChannelFlags_AllOff;
+
+ for(int i = 0; i < nch; i++) {
+ desc.mChannelLabel = channels[i];
+ r->mChannelDescriptions[i] = desc;
+ }
+
+coreaudio_error:
+ return r;
+}
+
+static void ca_retrieve_layouts(struct ao *ao, struct mp_chmap_sel *s,
+ AudioDeviceID device)
+{
+ void *ta_ctx = talloc_new(NULL);
+ struct mp_chmap chmap;
+
+ AudioChannelLayout *ml = ca_query_layout(ao, device, ta_ctx);
+ if (ml && ca_layout_to_mp_chmap(ao, ml, &chmap))
+ mp_chmap_sel_add_map(s, &chmap);
+
+ AudioChannelLayout *sl = ca_query_stereo_layout(ao, device, ta_ctx);
+ if (sl && ca_layout_to_mp_chmap(ao, sl, &chmap))
+ mp_chmap_sel_add_map(s, &chmap);
+
+ talloc_free(ta_ctx);
+}
+
+bool ca_init_chmap(struct ao *ao, AudioDeviceID device)
+{
+ struct mp_chmap_sel chmap_sel = {0};
+ ca_retrieve_layouts(ao, &chmap_sel, device);
+
+ if (!chmap_sel.num_chmaps)
+ mp_chmap_sel_add_map(&chmap_sel, &(struct mp_chmap)MP_CHMAP_INIT_STEREO);
+
+ mp_chmap_sel_add_map(&chmap_sel, &(struct mp_chmap)MP_CHMAP_INIT_MONO);
+
+ if (!ao_chmap_sel_adjust(ao, &chmap_sel, &ao->channels)) {
+ MP_ERR(ao, "could not select a suitable channel map among the "
+ "hardware supported ones. Make sure to configure your "
+ "output device correctly in 'Audio MIDI Setup.app'\n");
+ return false;
+ }
+ return true;
+}
+
+void ca_get_active_chmap(struct ao *ao, AudioDeviceID device, int channel_count,
+ struct mp_chmap *out_map)
+{
+ // Apparently, we have to guess by looking back at the supported layouts,
+ // and I haven't found a property that retrieves the actual currently
+ // active channel layout.
+
+ struct mp_chmap_sel chmap_sel = {0};
+ ca_retrieve_layouts(ao, &chmap_sel, device);
+
+ // Use any exact match.
+ for (int n = 0; n < chmap_sel.num_chmaps; n++) {
+ if (chmap_sel.chmaps[n].num == channel_count) {
+ MP_VERBOSE(ao, "mismatching channels - fallback #%d\n", n);
+ *out_map = chmap_sel.chmaps[n];
+ return;
+ }
+ }
+
+ // Fall back to stereo or mono, and fill the rest with silence. (We don't
+ // know what the device expects. We could use a larger default layout here,
+ // but let's not.)
+ mp_chmap_from_channels(out_map, MPMIN(2, channel_count));
+ out_map->num = channel_count;
+ for (int n = 2; n < out_map->num; n++)
+ out_map->speaker[n] = MP_SPEAKER_ID_NA;
+ MP_WARN(ao, "mismatching channels - falling back to %s\n",
+ mp_chmap_to_str(out_map));
+}
+#endif
diff --git a/audio/out/ao_coreaudio_chmap.h b/audio/out/ao_coreaudio_chmap.h
new file mode 100644
index 0000000..b6d160c
--- /dev/null
+++ b/audio/out/ao_coreaudio_chmap.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_COREAUDIO_CHMAP_H
+#define MPV_COREAUDIO_CHMAP_H
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#include "config.h"
+
+struct mp_chmap;
+
+int ca_label_to_mp_speaker_id(AudioChannelLabel label);
+
+#if HAVE_COREAUDIO
+bool ca_init_chmap(struct ao *ao, AudioDeviceID device);
+void ca_get_active_chmap(struct ao *ao, AudioDeviceID device, int channel_count,
+ struct mp_chmap *out_map);
+#endif
+
+#endif
diff --git a/audio/out/ao_coreaudio_exclusive.c b/audio/out/ao_coreaudio_exclusive.c
new file mode 100644
index 0000000..e24f791
--- /dev/null
+++ b/audio/out/ao_coreaudio_exclusive.c
@@ -0,0 +1,472 @@
+/*
+ * CoreAudio audio output driver for Mac OS X
+ *
+ * original copyright (C) Timothy J. Wood - Aug 2000
+ * ported to MPlayer libao2 by Dan Christiansen
+ *
+ * Chris Roccati
+ * Stefano Pigozzi
+ *
+ * The S/PDIF part of the code is based on the auhal audio output
+ * module from VideoLAN:
+ * Copyright (c) 2006 Derk-Jan Hartman <hartman at videolan dot org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * The MacOS X CoreAudio framework doesn't mesh as simply as some
+ * simpler frameworks do. This is due to the fact that CoreAudio pulls
+ * audio samples rather than having them pushed at it (which is nice
+ * when you are wanting to do good buffering of audio).
+ */
+
+#include <stdatomic.h>
+
+#include <CoreAudio/HostTime.h>
+
+#include <libavutil/intreadwrite.h>
+#include <libavutil/intfloat.h>
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+#include "osdep/timer.h"
+#include "options/m_option.h"
+#include "common/msg.h"
+#include "audio/out/ao_coreaudio_chmap.h"
+#include "audio/out/ao_coreaudio_properties.h"
+#include "audio/out/ao_coreaudio_utils.h"
+
+struct priv {
+ AudioDeviceID device; // selected device
+
+ bool paused;
+
+ // audio render callback
+ AudioDeviceIOProcID render_cb;
+
+ // pid set for hog mode, (-1) means that hog mode on the device was
+ // released. hog mode is exclusive access to a device
+ pid_t hog_pid;
+
+ AudioStreamID stream;
+
+ // stream index in an AudioBufferList
+ int stream_idx;
+
+ // format we changed the stream to, and the original format to restore
+ AudioStreamBasicDescription stream_asbd;
+ AudioStreamBasicDescription original_asbd;
+
+ // Output s16 physical format, float32 virtual format, ac3/dts mpv format
+ bool spdif_hack;
+
+ bool changed_mixing;
+
+ atomic_bool reload_requested;
+
+ uint64_t hw_latency_ns;
+};
+
+static OSStatus property_listener_cb(
+ AudioObjectID object, uint32_t n_addresses,
+ const AudioObjectPropertyAddress addresses[],
+ void *data)
+{
+ struct ao *ao = data;
+ struct priv *p = ao->priv;
+
+ // Check whether we need to reset the compressed output stream.
+ AudioStreamBasicDescription f;
+ OSErr err = CA_GET(p->stream, kAudioStreamPropertyVirtualFormat, &f);
+ CHECK_CA_WARN("could not get stream format");
+ if (err != noErr || !ca_asbd_equals(&p->stream_asbd, &f)) {
+ if (atomic_compare_exchange_strong(&p->reload_requested,
+ &(bool){false}, true))
+ {
+ ao_request_reload(ao);
+ MP_INFO(ao, "Stream format changed! Reloading.\n");
+ }
+ }
+
+ return noErr;
+}
+
+static OSStatus enable_property_listener(struct ao *ao, bool enabled)
+{
+ struct priv *p = ao->priv;
+
+ uint32_t selectors[] = {kAudioDevicePropertyDeviceHasChanged,
+ kAudioHardwarePropertyDevices};
+ AudioDeviceID devs[] = {p->device,
+ kAudioObjectSystemObject};
+ assert(MP_ARRAY_SIZE(selectors) == MP_ARRAY_SIZE(devs));
+
+ OSStatus status = noErr;
+ for (int n = 0; n < MP_ARRAY_SIZE(devs); n++) {
+ AudioObjectPropertyAddress addr = {
+ .mScope = kAudioObjectPropertyScopeGlobal,
+ .mElement = kAudioObjectPropertyElementMaster,
+ .mSelector = selectors[n],
+ };
+ AudioDeviceID device = devs[n];
+
+ OSStatus status2;
+ if (enabled) {
+ status2 = AudioObjectAddPropertyListener(
+ device, &addr, property_listener_cb, ao);
+ } else {
+ status2 = AudioObjectRemovePropertyListener(
+ device, &addr, property_listener_cb, ao);
+ }
+ if (status == noErr)
+ status = status2;
+ }
+
+ return status;
+}
+
+// This is a hack for passing through AC3/DTS on drivers which don't support it.
+// The goal is to have the driver output the AC3 data bitexact, so basically we
+// feed it float data by converting the AC3 data to float in the reverse way we
+// assume the driver outputs it.
+// Input: data_as_int16[0..samples]
+// Output: data_as_float[0..samples]
+// The conversion is done in-place.
+static void bad_hack_mygodwhy(char *data, int samples)
+{
+ // In reverse, so we can do it in-place.
+ for (int n = samples - 1; n >= 0; n--) {
+ int16_t val = AV_RN16(data + n * 2);
+ float fval = val / (float)(1 << 15);
+ uint32_t ival = av_float2int(fval);
+ AV_WN32(data + n * 4, ival);
+ }
+}
+
+static OSStatus render_cb_compressed(
+ AudioDeviceID device, const AudioTimeStamp *ts,
+ const void *in_data, const AudioTimeStamp *in_ts,
+ AudioBufferList *out_data, const AudioTimeStamp *out_ts, void *ctx)
+{
+ struct ao *ao = ctx;
+ struct priv *p = ao->priv;
+ AudioBuffer buf = out_data->mBuffers[p->stream_idx];
+ int requested = buf.mDataByteSize;
+ int sstride = p->spdif_hack ? 4 * ao->channels.num : ao->sstride;
+
+ int pseudo_frames = requested / sstride;
+
+ // we expect the callback to read full frames, which are aligned accordingly
+ if (pseudo_frames * sstride != requested) {
+ MP_ERR(ao, "Unsupported unaligned read of %d bytes.\n", requested);
+ return kAudioHardwareUnspecifiedError;
+ }
+
+ int64_t end = mp_time_ns();
+ end += p->hw_latency_ns + ca_get_latency(ts)
+ + ca_frames_to_ns(ao, pseudo_frames);
+
+ ao_read_data(ao, &buf.mData, pseudo_frames, end);
+
+ if (p->spdif_hack)
+ bad_hack_mygodwhy(buf.mData, pseudo_frames * ao->channels.num);
+
+ return noErr;
+}
+
+// Apparently, audio devices can have multiple sub-streams. It's not clear to
+// me what devices with multiple streams actually do. So only select the first
+// one that fulfills some minimum requirements.
+// If this is not sufficient, we could duplicate the device list entries for
+// each sub-stream, and make it explicit.
+static int select_stream(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ AudioStreamID *streams;
+ size_t n_streams;
+ OSStatus err;
+
+ /* Get a list of all the streams on this device. */
+ err = CA_GET_ARY_O(p->device, kAudioDevicePropertyStreams,
+ &streams, &n_streams);
+ CHECK_CA_ERROR("could not get number of streams");
+ for (int i = 0; i < n_streams; i++) {
+ uint32_t direction;
+ err = CA_GET(streams[i], kAudioStreamPropertyDirection, &direction);
+ CHECK_CA_WARN("could not get stream direction");
+ if (err == noErr && direction != 0) {
+ MP_VERBOSE(ao, "Substream %d is not an output stream.\n", i);
+ continue;
+ }
+
+ if (af_fmt_is_pcm(ao->format) || p->spdif_hack ||
+ ca_stream_supports_compressed(ao, streams[i]))
+ {
+ MP_VERBOSE(ao, "Using substream %d/%zd.\n", i, n_streams);
+ p->stream = streams[i];
+ p->stream_idx = i;
+ break;
+ }
+ }
+
+ talloc_free(streams);
+
+ if (p->stream_idx < 0) {
+ MP_ERR(ao, "No useable substream found.\n");
+ goto coreaudio_error;
+ }
+
+ return 0;
+
+coreaudio_error:
+ return -1;
+}
+
+static int find_best_format(struct ao *ao, AudioStreamBasicDescription *out_fmt)
+{
+ struct priv *p = ao->priv;
+
+ // Build ASBD for the input format
+ AudioStreamBasicDescription asbd;
+ ca_fill_asbd(ao, &asbd);
+ ca_print_asbd(ao, "our format:", &asbd);
+
+ *out_fmt = (AudioStreamBasicDescription){0};
+
+ AudioStreamRangedDescription *formats;
+ size_t n_formats;
+ OSStatus err;
+
+ err = CA_GET_ARY(p->stream, kAudioStreamPropertyAvailablePhysicalFormats,
+ &formats, &n_formats);
+ CHECK_CA_ERROR("could not get number of stream formats");
+
+ for (int j = 0; j < n_formats; j++) {
+ AudioStreamBasicDescription *stream_asbd = &formats[j].mFormat;
+
+ ca_print_asbd(ao, "- ", stream_asbd);
+
+ if (!out_fmt->mFormatID || ca_asbd_is_better(&asbd, out_fmt, stream_asbd))
+ *out_fmt = *stream_asbd;
+ }
+
+ talloc_free(formats);
+
+ if (!out_fmt->mFormatID) {
+ MP_ERR(ao, "no format found\n");
+ return -1;
+ }
+
+ return 0;
+coreaudio_error:
+ return -1;
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ int original_format = ao->format;
+
+ OSStatus err = ca_select_device(ao, ao->device, &p->device);
+ CHECK_CA_ERROR_L(coreaudio_error_nounlock, "failed to select device");
+
+ ao->format = af_fmt_from_planar(ao->format);
+
+ if (!af_fmt_is_pcm(ao->format) && !af_fmt_is_spdif(ao->format)) {
+ MP_ERR(ao, "Unsupported format.\n");
+ goto coreaudio_error_nounlock;
+ }
+
+ if (af_fmt_is_pcm(ao->format))
+ p->spdif_hack = false;
+
+ if (p->spdif_hack) {
+ if (af_fmt_to_bytes(ao->format) != 2) {
+ MP_ERR(ao, "HD formats not supported with spdif hack.\n");
+ goto coreaudio_error_nounlock;
+ }
+ // Let the pure evil begin!
+ ao->format = AF_FORMAT_S16;
+ }
+
+ uint32_t is_alive = 1;
+ err = CA_GET(p->device, kAudioDevicePropertyDeviceIsAlive, &is_alive);
+ CHECK_CA_WARN("could not check whether device is alive");
+
+ if (!is_alive)
+ MP_WARN(ao, "device is not alive\n");
+
+ err = ca_lock_device(p->device, &p->hog_pid);
+ CHECK_CA_WARN("failed to set hogmode");
+
+ err = ca_disable_mixing(ao, p->device, &p->changed_mixing);
+ CHECK_CA_WARN("failed to disable mixing");
+
+ if (select_stream(ao) < 0)
+ goto coreaudio_error;
+
+ AudioStreamBasicDescription hwfmt;
+ if (find_best_format(ao, &hwfmt) < 0)
+ goto coreaudio_error;
+
+ err = CA_GET(p->stream, kAudioStreamPropertyPhysicalFormat,
+ &p->original_asbd);
+ CHECK_CA_ERROR("could not get stream's original physical format");
+
+ // Even if changing the physical format fails, we can try using the current
+ // virtual format.
+ ca_change_physical_format_sync(ao, p->stream, hwfmt);
+
+ if (!ca_init_chmap(ao, p->device))
+ goto coreaudio_error;
+
+ err = CA_GET(p->stream, kAudioStreamPropertyVirtualFormat, &p->stream_asbd);
+ CHECK_CA_ERROR("could not get stream's virtual format");
+
+ ca_print_asbd(ao, "virtual format", &p->stream_asbd);
+
+ if (p->stream_asbd.mChannelsPerFrame > MP_NUM_CHANNELS) {
+ MP_ERR(ao, "unsupported number of channels: %d > %d.\n",
+ p->stream_asbd.mChannelsPerFrame, MP_NUM_CHANNELS);
+ goto coreaudio_error;
+ }
+
+ int new_format = ca_asbd_to_mp_format(&p->stream_asbd);
+
+ // If both old and new formats are spdif, avoid changing it due to the
+ // imperfect mapping between mp and CA formats.
+ if (!(af_fmt_is_spdif(ao->format) && af_fmt_is_spdif(new_format)))
+ ao->format = new_format;
+
+ if (!ao->format || af_fmt_is_planar(ao->format)) {
+ MP_ERR(ao, "hardware format not supported\n");
+ goto coreaudio_error;
+ }
+
+ ao->samplerate = p->stream_asbd.mSampleRate;
+
+ if (ao->channels.num != p->stream_asbd.mChannelsPerFrame) {
+ ca_get_active_chmap(ao, p->device, p->stream_asbd.mChannelsPerFrame,
+ &ao->channels);
+ }
+ if (!ao->channels.num) {
+ MP_ERR(ao, "number of channels changed, and unknown channel layout!\n");
+ goto coreaudio_error;
+ }
+
+ if (p->spdif_hack) {
+ AudioStreamBasicDescription physical_format = {0};
+ err = CA_GET(p->stream, kAudioStreamPropertyPhysicalFormat,
+ &physical_format);
+ CHECK_CA_ERROR("could not get stream's physical format");
+ int ph_format = ca_asbd_to_mp_format(&physical_format);
+ if (ao->format != AF_FORMAT_FLOAT || ph_format != AF_FORMAT_S16) {
+ MP_ERR(ao, "Wrong parameters for spdif hack (%d / %d)\n",
+ ao->format, ph_format);
+ }
+ ao->format = original_format; // pretend AC3 or DTS *evil laughter*
+ MP_WARN(ao, "Using spdif passthrough hack. This could produce noise.\n");
+ }
+
+ p->hw_latency_ns = ca_get_device_latency_ns(ao, p->device);
+ MP_VERBOSE(ao, "base latency: %lld nanoseconds\n", p->hw_latency_ns);
+
+ err = enable_property_listener(ao, true);
+ CHECK_CA_ERROR("cannot install format change listener during init");
+
+ err = AudioDeviceCreateIOProcID(p->device,
+ (AudioDeviceIOProc)render_cb_compressed,
+ (void *)ao,
+ &p->render_cb);
+ CHECK_CA_ERROR("failed to register audio render callback");
+
+ return CONTROL_TRUE;
+
+coreaudio_error:
+ err = enable_property_listener(ao, false);
+ CHECK_CA_WARN("can't remove format change listener");
+ err = ca_unlock_device(p->device, &p->hog_pid);
+ CHECK_CA_WARN("can't release hog mode");
+coreaudio_error_nounlock:
+ return CONTROL_ERROR;
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ OSStatus err = noErr;
+
+ err = enable_property_listener(ao, false);
+ CHECK_CA_WARN("can't remove device listener, this may cause a crash");
+
+ err = AudioDeviceStop(p->device, p->render_cb);
+ CHECK_CA_WARN("failed to stop audio device");
+
+ err = AudioDeviceDestroyIOProcID(p->device, p->render_cb);
+ CHECK_CA_WARN("failed to remove device render callback");
+
+ if (!ca_change_physical_format_sync(ao, p->stream, p->original_asbd))
+ MP_WARN(ao, "can't revert to original device format\n");
+
+ err = ca_enable_mixing(ao, p->device, p->changed_mixing);
+ CHECK_CA_WARN("can't re-enable mixing");
+
+ err = ca_unlock_device(p->device, &p->hog_pid);
+ CHECK_CA_WARN("can't release hog mode");
+}
+
+static void audio_pause(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ OSStatus err = AudioDeviceStop(p->device, p->render_cb);
+ CHECK_CA_WARN("can't stop audio device");
+}
+
+static void audio_resume(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ OSStatus err = AudioDeviceStart(p->device, p->render_cb);
+ CHECK_CA_WARN("can't start audio device");
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_coreaudio_exclusive = {
+ .description = "CoreAudio Exclusive Mode",
+ .name = "coreaudio_exclusive",
+ .uninit = uninit,
+ .init = init,
+ .reset = audio_pause,
+ .start = audio_resume,
+ .list_devs = ca_get_device_list,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv){
+ .hog_pid = -1,
+ .stream = 0,
+ .stream_idx = -1,
+ .changed_mixing = false,
+ },
+ .options = (const struct m_option[]){
+ {"spdif-hack", OPT_BOOL(spdif_hack)},
+ {0}
+ },
+ .options_prefix = "coreaudio",
+};
diff --git a/audio/out/ao_coreaudio_properties.c b/audio/out/ao_coreaudio_properties.c
new file mode 100644
index 0000000..e25170a
--- /dev/null
+++ b/audio/out/ao_coreaudio_properties.c
@@ -0,0 +1,103 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Abstractions on the CoreAudio API to make property setting/getting suck less
+*/
+
+#include "audio/out/ao_coreaudio_properties.h"
+#include "audio/out/ao_coreaudio_utils.h"
+#include "mpv_talloc.h"
+
+OSStatus ca_get(AudioObjectID id, ca_scope scope, ca_sel selector,
+ uint32_t size, void *data)
+{
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = selector,
+ .mScope = scope,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+
+ return AudioObjectGetPropertyData(id, &p_addr, 0, NULL, &size, data);
+}
+
+OSStatus ca_set(AudioObjectID id, ca_scope scope, ca_sel selector,
+ uint32_t size, void *data)
+{
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = selector,
+ .mScope = scope,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+
+ return AudioObjectSetPropertyData(id, &p_addr, 0, NULL, size, data);
+}
+
+OSStatus ca_get_ary(AudioObjectID id, ca_scope scope, ca_sel selector,
+ uint32_t element_size, void **data, size_t *elements)
+{
+ OSStatus err;
+ uint32_t p_size;
+
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = selector,
+ .mScope = scope,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+
+ err = AudioObjectGetPropertyDataSize(id, &p_addr, 0, NULL, &p_size);
+ CHECK_CA_ERROR_SILENT_L(coreaudio_error);
+
+ *data = talloc_zero_size(NULL, p_size);
+ *elements = p_size / element_size;
+
+ err = ca_get(id, scope, selector, p_size, *data);
+ CHECK_CA_ERROR_SILENT_L(coreaudio_error_free);
+
+ return err;
+coreaudio_error_free:
+ talloc_free(*data);
+coreaudio_error:
+ return err;
+}
+
+OSStatus ca_get_str(AudioObjectID id, ca_scope scope, ca_sel selector,
+ char **data)
+{
+ CFStringRef string;
+ OSStatus err =
+ ca_get(id, scope, selector, sizeof(CFStringRef), (void **)&string);
+ CHECK_CA_ERROR_SILENT_L(coreaudio_error);
+
+ *data = cfstr_get_cstr(string);
+ CFRelease(string);
+coreaudio_error:
+ return err;
+}
+
+Boolean ca_settable(AudioObjectID id, ca_scope scope, ca_sel selector,
+ Boolean *data)
+{
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = selector,
+ .mScope = kAudioObjectPropertyScopeGlobal,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+
+ return AudioObjectIsPropertySettable(id, &p_addr, data);
+}
+
diff --git a/audio/out/ao_coreaudio_properties.h b/audio/out/ao_coreaudio_properties.h
new file mode 100644
index 0000000..f293968
--- /dev/null
+++ b/audio/out/ao_coreaudio_properties.h
@@ -0,0 +1,61 @@
+/*
+ * This file is part of mpv.
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_COREAUDIO_PROPERTIES_H
+#define MPV_COREAUDIO_PROPERTIES_H
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#include "internal.h"
+
+// CoreAudio names are way too verbose
+#define ca_sel AudioObjectPropertySelector
+#define ca_scope AudioObjectPropertyScope
+#define CA_GLOBAL kAudioObjectPropertyScopeGlobal
+#define CA_OUTPUT kAudioDevicePropertyScopeOutput
+
+OSStatus ca_get(AudioObjectID id, ca_scope scope, ca_sel selector,
+ uint32_t size, void *data);
+
+OSStatus ca_set(AudioObjectID id, ca_scope scope, ca_sel selector,
+ uint32_t size, void *data);
+
+#define CA_GET(id, sel, data) ca_get(id, CA_GLOBAL, sel, sizeof(*(data)), data)
+#define CA_SET(id, sel, data) ca_set(id, CA_GLOBAL, sel, sizeof(*(data)), data)
+#define CA_GET_O(id, sel, data) ca_get(id, CA_OUTPUT, sel, sizeof(*(data)), data)
+
+OSStatus ca_get_ary(AudioObjectID id, ca_scope scope, ca_sel selector,
+ uint32_t element_size, void **data, size_t *elements);
+
+#define CA_GET_ARY(id, sel, data, elements) \
+ ca_get_ary(id, CA_GLOBAL, sel, sizeof(**(data)), (void **)data, elements)
+
+#define CA_GET_ARY_O(id, sel, data, elements) \
+ ca_get_ary(id, CA_OUTPUT, sel, sizeof(**(data)), (void **)data, elements)
+
+OSStatus ca_get_str(AudioObjectID id, ca_scope scope,ca_sel selector,
+ char **data);
+
+#define CA_GET_STR(id, sel, data) ca_get_str(id, CA_GLOBAL, sel, data)
+
+Boolean ca_settable(AudioObjectID id, ca_scope scope, ca_sel selector,
+ Boolean *data);
+
+#define CA_SETTABLE(id, sel, data) ca_settable(id, CA_GLOBAL, sel, data)
+
+#endif /* MPV_COREAUDIO_PROPERTIES_H */
diff --git a/audio/out/ao_coreaudio_utils.c b/audio/out/ao_coreaudio_utils.c
new file mode 100644
index 0000000..14db8e3
--- /dev/null
+++ b/audio/out/ao_coreaudio_utils.c
@@ -0,0 +1,539 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * This file contains functions interacting with the CoreAudio framework
+ * that are not specific to the AUHAL. These are split in a separate file for
+ * the sake of readability. In the future the could be used by other AOs based
+ * on CoreAudio but not the AUHAL (such as using AudioQueue services).
+ */
+
+#include "audio/out/ao_coreaudio_utils.h"
+#include "osdep/timer.h"
+#include "osdep/endian.h"
+#include "osdep/semaphore.h"
+#include "audio/format.h"
+
+#if HAVE_COREAUDIO
+#include "audio/out/ao_coreaudio_properties.h"
+#include <CoreAudio/HostTime.h>
+#else
+#include <mach/mach_time.h>
+#endif
+
+#if HAVE_COREAUDIO
+static bool ca_is_output_device(struct ao *ao, AudioDeviceID dev)
+{
+ size_t n_buffers;
+ AudioBufferList *buffers;
+ const ca_scope scope = kAudioDevicePropertyStreamConfiguration;
+ OSStatus err = CA_GET_ARY_O(dev, scope, &buffers, &n_buffers);
+ if (err != noErr)
+ return false;
+ talloc_free(buffers);
+ return n_buffers > 0;
+}
+
+void ca_get_device_list(struct ao *ao, struct ao_device_list *list)
+{
+ AudioDeviceID *devs;
+ size_t n_devs;
+ OSStatus err =
+ CA_GET_ARY(kAudioObjectSystemObject, kAudioHardwarePropertyDevices,
+ &devs, &n_devs);
+ CHECK_CA_ERROR("Failed to get list of output devices.");
+ for (int i = 0; i < n_devs; i++) {
+ if (!ca_is_output_device(ao, devs[i]))
+ continue;
+ void *ta_ctx = talloc_new(NULL);
+ char *name;
+ char *desc;
+ err = CA_GET_STR(devs[i], kAudioDevicePropertyDeviceUID, &name);
+ if (err != noErr) {
+ MP_VERBOSE(ao, "skipping device %d, which has no UID\n", i);
+ talloc_free(ta_ctx);
+ continue;
+ }
+ talloc_steal(ta_ctx, name);
+ err = CA_GET_STR(devs[i], kAudioObjectPropertyName, &desc);
+ if (err != noErr)
+ desc = talloc_strdup(NULL, "Unknown");
+ talloc_steal(ta_ctx, desc);
+ ao_device_list_add(list, ao, &(struct ao_device_desc){name, desc});
+ talloc_free(ta_ctx);
+ }
+ talloc_free(devs);
+coreaudio_error:
+ return;
+}
+
+OSStatus ca_select_device(struct ao *ao, char* name, AudioDeviceID *device)
+{
+ OSStatus err = noErr;
+ *device = kAudioObjectUnknown;
+
+ if (name && name[0]) {
+ CFStringRef uid = cfstr_from_cstr(name);
+ AudioValueTranslation v = (AudioValueTranslation) {
+ .mInputData = &uid,
+ .mInputDataSize = sizeof(CFStringRef),
+ .mOutputData = device,
+ .mOutputDataSize = sizeof(*device),
+ };
+ uint32_t size = sizeof(AudioValueTranslation);
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = kAudioHardwarePropertyDeviceForUID,
+ .mScope = kAudioObjectPropertyScopeGlobal,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+ err = AudioObjectGetPropertyData(
+ kAudioObjectSystemObject, &p_addr, 0, 0, &size, &v);
+ CFRelease(uid);
+ CHECK_CA_ERROR("unable to query for device UID");
+
+ uint32_t is_alive = 1;
+ err = CA_GET(*device, kAudioDevicePropertyDeviceIsAlive, &is_alive);
+ CHECK_CA_ERROR("could not check whether device is alive (invalid device?)");
+
+ if (!is_alive)
+ MP_WARN(ao, "device is not alive!\n");
+ } else {
+ // device not set by user, get the default one
+ err = CA_GET(kAudioObjectSystemObject,
+ kAudioHardwarePropertyDefaultOutputDevice,
+ device);
+ CHECK_CA_ERROR("could not get default audio device");
+ }
+
+ if (mp_msg_test(ao->log, MSGL_V)) {
+ char *desc;
+ OSStatus err2 = CA_GET_STR(*device, kAudioObjectPropertyName, &desc);
+ if (err2 == noErr) {
+ MP_VERBOSE(ao, "selected audio output device: %s (%" PRIu32 ")\n",
+ desc, *device);
+ talloc_free(desc);
+ }
+ }
+
+coreaudio_error:
+ return err;
+}
+#endif
+
+bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message)
+{
+ if (code == noErr) return true;
+
+ mp_msg(ao->log, level, "%s (%s/%d)\n", message, mp_tag_str(code), (int)code);
+
+ return false;
+}
+
+static void ca_fill_asbd_raw(AudioStreamBasicDescription *asbd, int mp_format,
+ int samplerate, int num_channels)
+{
+ asbd->mSampleRate = samplerate;
+ // Set "AC3" for other spdif formats too - unknown if that works.
+ asbd->mFormatID = af_fmt_is_spdif(mp_format) ?
+ kAudioFormat60958AC3 :
+ kAudioFormatLinearPCM;
+ asbd->mChannelsPerFrame = num_channels;
+ asbd->mBitsPerChannel = af_fmt_to_bytes(mp_format) * 8;
+ asbd->mFormatFlags = kAudioFormatFlagIsPacked;
+
+ int channels_per_buffer = num_channels;
+ if (af_fmt_is_planar(mp_format)) {
+ asbd->mFormatFlags |= kAudioFormatFlagIsNonInterleaved;
+ channels_per_buffer = 1;
+ }
+
+ if (af_fmt_is_float(mp_format)) {
+ asbd->mFormatFlags |= kAudioFormatFlagIsFloat;
+ } else if (!af_fmt_is_unsigned(mp_format)) {
+ asbd->mFormatFlags |= kAudioFormatFlagIsSignedInteger;
+ }
+
+ if (BYTE_ORDER == BIG_ENDIAN)
+ asbd->mFormatFlags |= kAudioFormatFlagIsBigEndian;
+
+ asbd->mFramesPerPacket = 1;
+ asbd->mBytesPerPacket = asbd->mBytesPerFrame =
+ asbd->mFramesPerPacket * channels_per_buffer *
+ (asbd->mBitsPerChannel / 8);
+}
+
+void ca_fill_asbd(struct ao *ao, AudioStreamBasicDescription *asbd)
+{
+ ca_fill_asbd_raw(asbd, ao->format, ao->samplerate, ao->channels.num);
+}
+
+bool ca_formatid_is_compressed(uint32_t formatid)
+{
+ switch (formatid)
+ case 'IAC3':
+ case 'iac3':
+ case kAudioFormat60958AC3:
+ case kAudioFormatAC3:
+ return true;
+ return false;
+}
+
+// This might be wrong, but for now it's sufficient for us.
+static uint32_t ca_normalize_formatid(uint32_t formatID)
+{
+ return ca_formatid_is_compressed(formatID) ? kAudioFormat60958AC3 : formatID;
+}
+
+bool ca_asbd_equals(const AudioStreamBasicDescription *a,
+ const AudioStreamBasicDescription *b)
+{
+ int flags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsFloat |
+ kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian;
+ bool spdif = ca_formatid_is_compressed(a->mFormatID) &&
+ ca_formatid_is_compressed(b->mFormatID);
+
+ return (a->mFormatFlags & flags) == (b->mFormatFlags & flags) &&
+ a->mBitsPerChannel == b->mBitsPerChannel &&
+ ca_normalize_formatid(a->mFormatID) ==
+ ca_normalize_formatid(b->mFormatID) &&
+ (spdif || a->mBytesPerPacket == b->mBytesPerPacket) &&
+ (spdif || a->mChannelsPerFrame == b->mChannelsPerFrame) &&
+ a->mSampleRate == b->mSampleRate;
+}
+
+// Return the AF_FORMAT_* (AF_FORMAT_S16 etc.) corresponding to the asbd.
+int ca_asbd_to_mp_format(const AudioStreamBasicDescription *asbd)
+{
+ for (int fmt = 1; fmt < AF_FORMAT_COUNT; fmt++) {
+ AudioStreamBasicDescription mp_asbd = {0};
+ ca_fill_asbd_raw(&mp_asbd, fmt, asbd->mSampleRate, asbd->mChannelsPerFrame);
+ if (ca_asbd_equals(&mp_asbd, asbd))
+ return af_fmt_is_spdif(fmt) ? AF_FORMAT_S_AC3 : fmt;
+ }
+ return 0;
+}
+
+void ca_print_asbd(struct ao *ao, const char *description,
+ const AudioStreamBasicDescription *asbd)
+{
+ uint32_t flags = asbd->mFormatFlags;
+ char *format = mp_tag_str(asbd->mFormatID);
+ int mpfmt = ca_asbd_to_mp_format(asbd);
+
+ MP_VERBOSE(ao,
+ "%s %7.1fHz %" PRIu32 "bit %s "
+ "[%" PRIu32 "][%" PRIu32 "bpp][%" PRIu32 "fbp]"
+ "[%" PRIu32 "bpf][%" PRIu32 "ch] "
+ "%s %s %s%s%s%s (%s)\n",
+ description, asbd->mSampleRate, asbd->mBitsPerChannel, format,
+ asbd->mFormatFlags, asbd->mBytesPerPacket, asbd->mFramesPerPacket,
+ asbd->mBytesPerFrame, asbd->mChannelsPerFrame,
+ (flags & kAudioFormatFlagIsFloat) ? "float" : "int",
+ (flags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE",
+ (flags & kAudioFormatFlagIsSignedInteger) ? "S" : "U",
+ (flags & kAudioFormatFlagIsPacked) ? " packed" : "",
+ (flags & kAudioFormatFlagIsAlignedHigh) ? " aligned" : "",
+ (flags & kAudioFormatFlagIsNonInterleaved) ? " P" : "",
+ mpfmt ? af_fmt_to_str(mpfmt) : "-");
+}
+
+// Return whether new is an improvement over old. Assume a higher value means
+// better quality, and we always prefer the value closest to the requested one,
+// which is still larger than the requested one.
+// Equal values prefer the new one (so ca_asbd_is_better() checks other params).
+static bool value_is_better(double req, double old, double new)
+{
+ if (new >= req) {
+ return old < req || new <= old;
+ } else {
+ return old < req && new >= old;
+ }
+}
+
+// Return whether new is an improvement over old (req is the requested format).
+bool ca_asbd_is_better(AudioStreamBasicDescription *req,
+ AudioStreamBasicDescription *old,
+ AudioStreamBasicDescription *new)
+{
+ if (new->mChannelsPerFrame > MP_NUM_CHANNELS)
+ return false;
+ if (old->mChannelsPerFrame > MP_NUM_CHANNELS)
+ return true;
+ if (req->mFormatID != new->mFormatID)
+ return false;
+ if (req->mFormatID != old->mFormatID)
+ return true;
+
+ if (!value_is_better(req->mBitsPerChannel, old->mBitsPerChannel,
+ new->mBitsPerChannel))
+ return false;
+
+ if (!value_is_better(req->mSampleRate, old->mSampleRate, new->mSampleRate))
+ return false;
+
+ if (!value_is_better(req->mChannelsPerFrame, old->mChannelsPerFrame,
+ new->mChannelsPerFrame))
+ return false;
+
+ return true;
+}
+
+int64_t ca_frames_to_ns(struct ao *ao, uint32_t frames)
+{
+ return MP_TIME_S_TO_NS(frames / (double)ao->samplerate);
+}
+
+int64_t ca_get_latency(const AudioTimeStamp *ts)
+{
+#if HAVE_COREAUDIO
+ uint64_t out = AudioConvertHostTimeToNanos(ts->mHostTime);
+ uint64_t now = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime());
+
+ if (now > out)
+ return 0;
+
+ return out - now;
+#else
+ static mach_timebase_info_data_t timebase;
+ if (timebase.denom == 0)
+ mach_timebase_info(&timebase);
+
+ uint64_t out = ts->mHostTime;
+ uint64_t now = mach_absolute_time();
+
+ if (now > out)
+ return 0;
+
+ return (out - now) * timebase.numer / timebase.denom;
+#endif
+}
+
+#if HAVE_COREAUDIO
+bool ca_stream_supports_compressed(struct ao *ao, AudioStreamID stream)
+{
+ AudioStreamRangedDescription *formats = NULL;
+ size_t n_formats;
+
+ OSStatus err =
+ CA_GET_ARY(stream, kAudioStreamPropertyAvailablePhysicalFormats,
+ &formats, &n_formats);
+
+ CHECK_CA_ERROR("Could not get number of stream formats.");
+
+ for (int i = 0; i < n_formats; i++) {
+ AudioStreamBasicDescription asbd = formats[i].mFormat;
+
+ ca_print_asbd(ao, "- ", &asbd);
+
+ if (ca_formatid_is_compressed(asbd.mFormatID)) {
+ talloc_free(formats);
+ return true;
+ }
+ }
+
+ talloc_free(formats);
+coreaudio_error:
+ return false;
+}
+
+OSStatus ca_lock_device(AudioDeviceID device, pid_t *pid)
+{
+ *pid = getpid();
+ OSStatus err = CA_SET(device, kAudioDevicePropertyHogMode, pid);
+ if (err != noErr)
+ *pid = -1;
+
+ return err;
+}
+
+OSStatus ca_unlock_device(AudioDeviceID device, pid_t *pid)
+{
+ if (*pid == getpid()) {
+ *pid = -1;
+ return CA_SET(device, kAudioDevicePropertyHogMode, &pid);
+ }
+ return noErr;
+}
+
+static OSStatus ca_change_mixing(struct ao *ao, AudioDeviceID device,
+ uint32_t val, bool *changed)
+{
+ *changed = false;
+
+ AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) {
+ .mSelector = kAudioDevicePropertySupportsMixing,
+ .mScope = kAudioObjectPropertyScopeGlobal,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+
+ if (AudioObjectHasProperty(device, &p_addr)) {
+ OSStatus err;
+ Boolean writeable = 0;
+ err = CA_SETTABLE(device, kAudioDevicePropertySupportsMixing,
+ &writeable);
+
+ if (!CHECK_CA_WARN("can't tell if mixing property is settable")) {
+ return err;
+ }
+
+ if (!writeable)
+ return noErr;
+
+ err = CA_SET(device, kAudioDevicePropertySupportsMixing, &val);
+ if (err != noErr)
+ return err;
+
+ if (!CHECK_CA_WARN("can't set mix mode")) {
+ return err;
+ }
+
+ *changed = true;
+ }
+
+ return noErr;
+}
+
+OSStatus ca_disable_mixing(struct ao *ao, AudioDeviceID device, bool *changed)
+{
+ return ca_change_mixing(ao, device, 0, changed);
+}
+
+OSStatus ca_enable_mixing(struct ao *ao, AudioDeviceID device, bool changed)
+{
+ if (changed) {
+ bool dont_care = false;
+ return ca_change_mixing(ao, device, 1, &dont_care);
+ }
+
+ return noErr;
+}
+
+int64_t ca_get_device_latency_ns(struct ao *ao, AudioDeviceID device)
+{
+ uint32_t latency_frames = 0;
+ uint32_t latency_properties[] = {
+ kAudioDevicePropertyLatency,
+ kAudioDevicePropertyBufferFrameSize,
+ kAudioDevicePropertySafetyOffset,
+ };
+ for (int n = 0; n < MP_ARRAY_SIZE(latency_properties); n++) {
+ uint32_t temp;
+ OSStatus err = CA_GET_O(device, latency_properties[n], &temp);
+ CHECK_CA_WARN("cannot get device latency");
+ if (err == noErr) {
+ latency_frames += temp;
+ MP_VERBOSE(ao, "Latency property %s: %d frames\n",
+ mp_tag_str(latency_properties[n]), (int)temp);
+ }
+ }
+
+ double sample_rate = ao->samplerate;
+ OSStatus err = CA_GET_O(device, kAudioDevicePropertyNominalSampleRate,
+ &sample_rate);
+ CHECK_CA_WARN("cannot get device sample rate, falling back to AO sample rate!");
+ if (err == noErr) {
+ MP_VERBOSE(ao, "Device sample rate: %f\n", sample_rate);
+ }
+
+ return MP_TIME_S_TO_NS(latency_frames / sample_rate);
+}
+
+static OSStatus ca_change_format_listener(
+ AudioObjectID object, uint32_t n_addresses,
+ const AudioObjectPropertyAddress addresses[],
+ void *data)
+{
+ mp_sem_t *sem = data;
+ mp_sem_post(sem);
+ return noErr;
+}
+
+bool ca_change_physical_format_sync(struct ao *ao, AudioStreamID stream,
+ AudioStreamBasicDescription change_format)
+{
+ OSStatus err = noErr;
+ bool format_set = false;
+
+ ca_print_asbd(ao, "setting stream physical format:", &change_format);
+
+ sem_t wakeup;
+ if (mp_sem_init(&wakeup, 0, 0)) {
+ MP_WARN(ao, "OOM\n");
+ return false;
+ }
+
+ AudioStreamBasicDescription prev_format;
+ err = CA_GET(stream, kAudioStreamPropertyPhysicalFormat, &prev_format);
+ CHECK_CA_ERROR("can't get current physical format");
+
+ ca_print_asbd(ao, "format in use before switching:", &prev_format);
+
+ /* Install the callback. */
+ AudioObjectPropertyAddress p_addr = {
+ .mSelector = kAudioStreamPropertyPhysicalFormat,
+ .mScope = kAudioObjectPropertyScopeGlobal,
+ .mElement = kAudioObjectPropertyElementMaster,
+ };
+
+ err = AudioObjectAddPropertyListener(stream, &p_addr,
+ ca_change_format_listener,
+ &wakeup);
+ CHECK_CA_ERROR("can't add property listener during format change");
+
+ /* Change the format. */
+ err = CA_SET(stream, kAudioStreamPropertyPhysicalFormat, &change_format);
+ CHECK_CA_WARN("error changing physical format");
+
+ /* The AudioStreamSetProperty is not only asynchronous,
+ * it is also not Atomic, in its behaviour. */
+ int64_t wait_until = mp_time_ns() + MP_TIME_S_TO_NS(2);
+ AudioStreamBasicDescription actual_format = {0};
+ while (1) {
+ err = CA_GET(stream, kAudioStreamPropertyPhysicalFormat, &actual_format);
+ if (!CHECK_CA_WARN("could not retrieve physical format"))
+ break;
+
+ format_set = ca_asbd_equals(&change_format, &actual_format);
+ if (format_set)
+ break;
+
+ if (mp_sem_timedwait(&wakeup, wait_until)) {
+ MP_VERBOSE(ao, "reached timeout\n");
+ break;
+ }
+ }
+
+ ca_print_asbd(ao, "actual format in use:", &actual_format);
+
+ if (!format_set) {
+ MP_WARN(ao, "changing physical format failed\n");
+ // Some drivers just fuck up and get into a broken state. Restore the
+ // old format in this case.
+ err = CA_SET(stream, kAudioStreamPropertyPhysicalFormat, &prev_format);
+ CHECK_CA_WARN("error restoring physical format");
+ }
+
+ err = AudioObjectRemovePropertyListener(stream, &p_addr,
+ ca_change_format_listener,
+ &wakeup);
+ CHECK_CA_ERROR("can't remove property listener");
+
+coreaudio_error:
+ mp_sem_destroy(&wakeup);
+ return format_set;
+}
+#endif
diff --git a/audio/out/ao_coreaudio_utils.h b/audio/out/ao_coreaudio_utils.h
new file mode 100644
index 0000000..0e2b8b1
--- /dev/null
+++ b/audio/out/ao_coreaudio_utils.h
@@ -0,0 +1,79 @@
+/*
+ * This file is part of mpv.
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_COREAUDIO_UTILS_H
+#define MPV_COREAUDIO_UTILS_H
+
+#include <AudioToolbox/AudioToolbox.h>
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "config.h"
+#include "common/msg.h"
+#include "audio/out/ao.h"
+#include "internal.h"
+#include "osdep/apple_utils.h"
+
+bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message);
+
+#define CHECK_CA_ERROR_L(label, message) \
+ do { \
+ if (!check_ca_st(ao, MSGL_ERR, err, message)) { \
+ goto label; \
+ } \
+ } while (0)
+
+#define CHECK_CA_ERROR(message) CHECK_CA_ERROR_L(coreaudio_error, message)
+#define CHECK_CA_WARN(message) check_ca_st(ao, MSGL_WARN, err, message)
+
+#define CHECK_CA_ERROR_SILENT_L(label) \
+ do { \
+ if (err != noErr) goto label; \
+ } while (0)
+
+void ca_get_device_list(struct ao *ao, struct ao_device_list *list);
+#if HAVE_COREAUDIO
+OSStatus ca_select_device(struct ao *ao, char* name, AudioDeviceID *device);
+#endif
+
+bool ca_formatid_is_compressed(uint32_t formatid);
+void ca_fill_asbd(struct ao *ao, AudioStreamBasicDescription *asbd);
+void ca_print_asbd(struct ao *ao, const char *description,
+ const AudioStreamBasicDescription *asbd);
+bool ca_asbd_equals(const AudioStreamBasicDescription *a,
+ const AudioStreamBasicDescription *b);
+int ca_asbd_to_mp_format(const AudioStreamBasicDescription *asbd);
+bool ca_asbd_is_better(AudioStreamBasicDescription *req,
+ AudioStreamBasicDescription *old,
+ AudioStreamBasicDescription *new);
+
+int64_t ca_frames_to_ns(struct ao *ao, uint32_t frames);
+int64_t ca_get_latency(const AudioTimeStamp *ts);
+
+#if HAVE_COREAUDIO
+bool ca_stream_supports_compressed(struct ao *ao, AudioStreamID stream);
+OSStatus ca_lock_device(AudioDeviceID device, pid_t *pid);
+OSStatus ca_unlock_device(AudioDeviceID device, pid_t *pid);
+OSStatus ca_disable_mixing(struct ao *ao, AudioDeviceID device, bool *changed);
+OSStatus ca_enable_mixing(struct ao *ao, AudioDeviceID device, bool changed);
+int64_t ca_get_device_latency_ns(struct ao *ao, AudioDeviceID device);
+bool ca_change_physical_format_sync(struct ao *ao, AudioStreamID stream,
+ AudioStreamBasicDescription change_format);
+#endif
+
+#endif /* MPV_COREAUDIO_UTILS_H */
diff --git a/audio/out/ao_jack.c b/audio/out/ao_jack.c
new file mode 100644
index 0000000..412e91d
--- /dev/null
+++ b/audio/out/ao_jack.c
@@ -0,0 +1,284 @@
+/*
+ * JACK audio output driver for MPlayer
+ *
+ * Copyleft 2001 by Felix Bünemann (atmosfear@users.sf.net)
+ * and Reimar Döffinger (Reimar.Doeffinger@stud.uni-karlsruhe.de)
+ *
+ * Copyleft 2013 by William Light <wrl@illest.net> for the mpv project
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "common/msg.h"
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+#include "osdep/timer.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+
+#include <jack/jack.h>
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+struct jack_opts {
+ char *port;
+ char *client_name;
+ bool connect;
+ bool autostart;
+ int stdlayout;
+};
+
+#define OPT_BASE_STRUCT struct jack_opts
+static const struct m_sub_options ao_jack_conf = {
+ .opts = (const struct m_option[]){
+ {"jack-port", OPT_STRING(port)},
+ {"jack-name", OPT_STRING(client_name)},
+ {"jack-autostart", OPT_BOOL(autostart)},
+ {"jack-connect", OPT_BOOL(connect)},
+ {"jack-std-channel-layout", OPT_CHOICE(stdlayout,
+ {"waveext", 0}, {"any", 1})},
+ {0}
+ },
+ .defaults = &(const struct jack_opts) {
+ .client_name = "mpv",
+ .connect = true,
+ },
+ .size = sizeof(struct jack_opts),
+};
+
+struct priv {
+ jack_client_t *client;
+
+ atomic_uint graph_latency_max;
+ atomic_uint buffer_size;
+
+ int last_chunk;
+
+ int num_ports;
+ jack_port_t *ports[MP_NUM_CHANNELS];
+
+ int activated;
+
+ struct jack_opts *opts;
+};
+
+static int graph_order_cb(void *arg)
+{
+ struct ao *ao = arg;
+ struct priv *p = ao->priv;
+
+ jack_latency_range_t jack_latency_range;
+ jack_port_get_latency_range(p->ports[0], JackPlaybackLatency,
+ &jack_latency_range);
+ atomic_store(&p->graph_latency_max, jack_latency_range.max);
+
+ return 0;
+}
+
+static int buffer_size_cb(jack_nframes_t nframes, void *arg)
+{
+ struct ao *ao = arg;
+ struct priv *p = ao->priv;
+
+ atomic_store(&p->buffer_size, nframes);
+
+ return 0;
+}
+
+static int process(jack_nframes_t nframes, void *arg)
+{
+ struct ao *ao = arg;
+ struct priv *p = ao->priv;
+
+ void *buffers[MP_NUM_CHANNELS];
+
+ for (int i = 0; i < p->num_ports; i++)
+ buffers[i] = jack_port_get_buffer(p->ports[i], nframes);
+
+ jack_nframes_t jack_latency =
+ atomic_load(&p->graph_latency_max) + atomic_load(&p->buffer_size);
+
+ int64_t end_time = mp_time_ns();
+ end_time += MP_TIME_S_TO_NS((jack_latency + nframes) / (double)ao->samplerate);
+
+ ao_read_data(ao, buffers, nframes, end_time);
+
+ return 0;
+}
+
+static int
+connect_to_outports(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ char *port_name = (p->opts->port && p->opts->port[0]) ? p->opts->port : NULL;
+ const char **matching_ports = NULL;
+ int port_flags = JackPortIsInput;
+ int i;
+
+ if (!port_name)
+ port_flags |= JackPortIsPhysical;
+
+ const char *port_type = JACK_DEFAULT_AUDIO_TYPE; // exclude MIDI ports
+ matching_ports = jack_get_ports(p->client, port_name, port_type, port_flags);
+
+ if (!matching_ports || !matching_ports[0]) {
+ MP_FATAL(ao, "no ports to connect to\n");
+ goto err_get_ports;
+ }
+
+ for (i = 0; i < p->num_ports && matching_ports[i]; i++) {
+ if (jack_connect(p->client, jack_port_name(p->ports[i]),
+ matching_ports[i]))
+ {
+ MP_FATAL(ao, "connecting failed\n");
+ goto err_connect;
+ }
+ }
+
+ free(matching_ports);
+ return 0;
+
+err_connect:
+ free(matching_ports);
+err_get_ports:
+ return -1;
+}
+
+static int
+create_ports(struct ao *ao, int nports)
+{
+ struct priv *p = ao->priv;
+ char pname[30];
+ int i;
+
+ for (i = 0; i < nports; i++) {
+ snprintf(pname, sizeof(pname), "out_%d", i);
+ p->ports[i] = jack_port_register(p->client, pname, JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsOutput, 0);
+
+ if (!p->ports[i]) {
+ MP_FATAL(ao, "not enough ports available\n");
+ goto err_port_register;
+ }
+ }
+
+ p->num_ports = nports;
+ return 0;
+
+err_port_register:
+ return -1;
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ if (!p->activated) {
+ p->activated = true;
+
+ if (jack_activate(p->client))
+ MP_FATAL(ao, "activate failed\n");
+
+ if (p->opts->connect)
+ connect_to_outports(ao);
+ }
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ struct mp_chmap_sel sel = {0};
+ jack_options_t open_options;
+
+ p->opts = mp_get_config_group(ao, ao->global, &ao_jack_conf);
+
+ ao->format = AF_FORMAT_FLOATP;
+
+ switch (p->opts->stdlayout) {
+ case 0:
+ mp_chmap_sel_add_waveext(&sel);
+ break;
+
+ default:
+ mp_chmap_sel_add_any(&sel);
+ }
+
+ if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
+ goto err_chmap;
+
+ open_options = JackNullOption;
+ if (!p->opts->autostart)
+ open_options |= JackNoStartServer;
+
+ p->client = jack_client_open(p->opts->client_name, open_options, NULL);
+ if (!p->client) {
+ MP_FATAL(ao, "cannot open server\n");
+ goto err_client_open;
+ }
+
+ if (create_ports(ao, ao->channels.num))
+ goto err_create_ports;
+
+ jack_set_process_callback(p->client, process, ao);
+
+ ao->samplerate = jack_get_sample_rate(p->client);
+ // The actual device buffer can change, but this is enough for pre-buffer
+ ao->device_buffer = jack_get_buffer_size(p->client);
+
+ jack_set_buffer_size_callback(p->client, buffer_size_cb, ao);
+ jack_set_graph_order_callback(p->client, graph_order_cb, ao);
+
+ if (!ao_chmap_sel_get_def(ao, &sel, &ao->channels, p->num_ports))
+ goto err_chmap_sel_get_def;
+
+ return 0;
+
+err_chmap_sel_get_def:
+err_create_ports:
+ jack_client_close(p->client);
+err_client_open:
+err_chmap:
+ return -1;
+}
+
+// close audio device
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ jack_client_close(p->client);
+}
+
+const struct ao_driver audio_out_jack = {
+ .description = "JACK audio output",
+ .name = "jack",
+ .init = init,
+ .uninit = uninit,
+ .start = start,
+ .priv_size = sizeof(struct priv),
+ .global_opts = &ao_jack_conf,
+};
diff --git a/audio/out/ao_lavc.c b/audio/out/ao_lavc.c
new file mode 100644
index 0000000..163fdca
--- /dev/null
+++ b/audio/out/ao_lavc.c
@@ -0,0 +1,337 @@
+/*
+ * audio encoding using libavformat
+ *
+ * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org>
+ * NOTE: this file is partially based on ao_pcm.c by Atmosfear
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <limits.h>
+
+#include <libavutil/common.h>
+
+#include "config.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "audio/aframe.h"
+#include "audio/chmap_avchannel.h"
+#include "audio/format.h"
+#include "audio/fmt-conversion.h"
+#include "filters/filter_internal.h"
+#include "filters/f_utils.h"
+#include "mpv_talloc.h"
+#include "ao.h"
+#include "internal.h"
+#include "common/msg.h"
+
+#include "common/encode_lavc.h"
+
+struct priv {
+ struct encoder_context *enc;
+
+ int pcmhack;
+ int aframesize;
+ int framecount;
+ int64_t lastpts;
+ int sample_size;
+ double expected_next_pts;
+ struct mp_filter *filter_root;
+ struct mp_filter *fix_frame_size;
+
+ AVRational worst_time_base;
+
+ bool shutdown;
+};
+
+static bool write_frame(struct ao *ao, struct mp_frame frame);
+
+static bool supports_format(const AVCodec *codec, int format)
+{
+ for (const enum AVSampleFormat *sampleformat = codec->sample_fmts;
+ sampleformat && *sampleformat != AV_SAMPLE_FMT_NONE;
+ sampleformat++)
+ {
+ if (af_from_avformat(*sampleformat) == format)
+ return true;
+ }
+ return false;
+}
+
+static void select_format(struct ao *ao, const AVCodec *codec)
+{
+ int formats[AF_FORMAT_COUNT + 1];
+ af_get_best_sample_formats(ao->format, formats);
+
+ for (int n = 0; formats[n]; n++) {
+ if (supports_format(codec, formats[n])) {
+ ao->format = formats[n];
+ break;
+ }
+ }
+}
+
+static void on_ready(void *ptr)
+{
+ struct ao *ao = ptr;
+ struct priv *ac = ao->priv;
+
+ ac->worst_time_base = encoder_get_mux_timebase_unlocked(ac->enc);
+
+ ao_add_events(ao, AO_EVENT_INITIAL_UNBLOCK);
+}
+
+// open & setup audio device
+static int init(struct ao *ao)
+{
+ struct priv *ac = ao->priv;
+
+ ac->enc = encoder_context_alloc(ao->encode_lavc_ctx, STREAM_AUDIO, ao->log);
+ if (!ac->enc)
+ return -1;
+ talloc_steal(ac, ac->enc);
+
+ AVCodecContext *encoder = ac->enc->encoder;
+ const AVCodec *codec = encoder->codec;
+
+ int samplerate = af_select_best_samplerate(ao->samplerate,
+ codec->supported_samplerates);
+ if (samplerate > 0)
+ ao->samplerate = samplerate;
+
+ encoder->time_base.num = 1;
+ encoder->time_base.den = ao->samplerate;
+
+ encoder->sample_rate = ao->samplerate;
+
+ struct mp_chmap_sel sel = {0};
+ mp_chmap_sel_add_any(&sel);
+ if (!ao_chmap_sel_adjust2(ao, &sel, &ao->channels, false))
+ goto fail;
+ mp_chmap_reorder_to_lavc(&ao->channels);
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ encoder->channels = ao->channels.num;
+ encoder->channel_layout = mp_chmap_to_lavc(&ao->channels);
+#else
+ mp_chmap_to_av_layout(&encoder->ch_layout, &ao->channels);
+#endif
+
+ encoder->sample_fmt = AV_SAMPLE_FMT_NONE;
+
+ select_format(ao, codec);
+
+ ac->sample_size = af_fmt_to_bytes(ao->format);
+ encoder->sample_fmt = af_to_avformat(ao->format);
+ encoder->bits_per_raw_sample = ac->sample_size * 8;
+
+ if (!encoder_init_codec_and_muxer(ac->enc, on_ready, ao))
+ goto fail;
+
+ ac->pcmhack = 0;
+ if (encoder->frame_size <= 1)
+ ac->pcmhack = av_get_bits_per_sample(encoder->codec_id) / 8;
+
+ if (ac->pcmhack) {
+ ac->aframesize = 16384; // "enough"
+ } else {
+ ac->aframesize = encoder->frame_size;
+ }
+
+ // enough frames for at least 0.25 seconds
+ ac->framecount = ceil(ao->samplerate * 0.25 / ac->aframesize);
+ // but at least one!
+ ac->framecount = MPMAX(ac->framecount, 1);
+
+ ac->lastpts = AV_NOPTS_VALUE;
+
+ ao->untimed = true;
+
+ ao->device_buffer = ac->aframesize * ac->framecount;
+
+ ac->filter_root = mp_filter_create_root(ao->global);
+ ac->fix_frame_size = mp_fixed_aframe_size_create(ac->filter_root,
+ ac->aframesize, true);
+ MP_HANDLE_OOM(ac->fix_frame_size);
+
+ return 0;
+
+fail:
+ mp_mutex_unlock(&ao->encode_lavc_ctx->lock);
+ ac->shutdown = true;
+ return -1;
+}
+
+// close audio device
+static void uninit(struct ao *ao)
+{
+ struct priv *ac = ao->priv;
+
+ if (!ac->shutdown) {
+ if (!write_frame(ao, MP_EOF_FRAME))
+ MP_WARN(ao, "could not flush last frame\n");
+ encoder_encode(ac->enc, NULL);
+ }
+
+ talloc_free(ac->filter_root);
+}
+
+// must get exactly ac->aframesize amount of data
+static void encode(struct ao *ao, struct mp_aframe *af)
+{
+ struct priv *ac = ao->priv;
+ AVCodecContext *encoder = ac->enc->encoder;
+ double outpts = mp_aframe_get_pts(af);
+
+ AVFrame *frame = mp_aframe_to_avframe(af);
+ MP_HANDLE_OOM(frame);
+
+ frame->pts = rint(outpts * av_q2d(av_inv_q(encoder->time_base)));
+
+ int64_t frame_pts = av_rescale_q(frame->pts, encoder->time_base,
+ ac->worst_time_base);
+ if (ac->lastpts != AV_NOPTS_VALUE && frame_pts <= ac->lastpts) {
+ // whatever the fuck this code does?
+ MP_WARN(ao, "audio frame pts went backwards (%d <- %d), autofixed\n",
+ (int)frame->pts, (int)ac->lastpts);
+ frame_pts = ac->lastpts + 1;
+ ac->lastpts = frame_pts;
+ frame->pts = av_rescale_q(frame_pts, ac->worst_time_base,
+ encoder->time_base);
+ frame_pts = av_rescale_q(frame->pts, encoder->time_base,
+ ac->worst_time_base);
+ }
+ ac->lastpts = frame_pts;
+
+ frame->quality = encoder->global_quality;
+ encoder_encode(ac->enc, frame);
+ av_frame_free(&frame);
+}
+
+static bool write_frame(struct ao *ao, struct mp_frame frame)
+{
+ struct priv *ac = ao->priv;
+
+ // Can't push in frame if it doesn't want it output one.
+ mp_pin_out_request_data(ac->fix_frame_size->pins[1]);
+
+ if (!mp_pin_in_write(ac->fix_frame_size->pins[0], frame))
+ return false; // shouldn't happen™
+
+ while (1) {
+ struct mp_frame fr = mp_pin_out_read(ac->fix_frame_size->pins[1]);
+ if (!fr.type)
+ break;
+ if (fr.type != MP_FRAME_AUDIO)
+ continue;
+ struct mp_aframe *af = fr.data;
+ encode(ao, af);
+ mp_frame_unref(&fr);
+ }
+
+ return true;
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *ac = ao->priv;
+ struct encode_lavc_context *ectx = ao->encode_lavc_ctx;
+
+ // See ao_driver.write_frames.
+ struct mp_aframe *af = mp_aframe_new_ref(*(struct mp_aframe **)data);
+
+ double nextpts;
+ double pts = mp_aframe_get_pts(af);
+ double outpts = pts;
+
+ // for ectx PTS fields
+ mp_mutex_lock(&ectx->lock);
+
+ if (!ectx->options->rawts) {
+ // Fix and apply the discontinuity pts offset.
+ nextpts = pts;
+ if (ectx->discontinuity_pts_offset == MP_NOPTS_VALUE) {
+ ectx->discontinuity_pts_offset = ectx->next_in_pts - nextpts;
+ } else if (fabs(nextpts + ectx->discontinuity_pts_offset -
+ ectx->next_in_pts) > 30)
+ {
+ MP_WARN(ao, "detected an unexpected discontinuity (pts jumped by "
+ "%f seconds)\n",
+ nextpts + ectx->discontinuity_pts_offset - ectx->next_in_pts);
+ ectx->discontinuity_pts_offset = ectx->next_in_pts - nextpts;
+ }
+
+ outpts = pts + ectx->discontinuity_pts_offset;
+ }
+
+ // Calculate expected pts of next audio frame (input side).
+ ac->expected_next_pts = pts + mp_aframe_get_size(af) / (double) ao->samplerate;
+
+ // Set next allowed input pts value (input side).
+ if (!ectx->options->rawts) {
+ nextpts = ac->expected_next_pts + ectx->discontinuity_pts_offset;
+ if (nextpts > ectx->next_in_pts)
+ ectx->next_in_pts = nextpts;
+ }
+
+ mp_mutex_unlock(&ectx->lock);
+
+ mp_aframe_set_pts(af, outpts);
+
+ return write_frame(ao, MAKE_FRAME(MP_FRAME_AUDIO, af));
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ state->free_samples = 1;
+ state->queued_samples = 0;
+ state->delay = 0;
+}
+
+static bool set_pause(struct ao *ao, bool paused)
+{
+ return true; // signal support so common code doesn't write silence
+}
+
+static void start(struct ao *ao)
+{
+ // we use data immediately
+}
+
+static void reset(struct ao *ao)
+{
+}
+
+const struct ao_driver audio_out_lavc = {
+ .encode = true,
+ .description = "audio encoding using libavcodec",
+ .name = "lavc",
+ .initially_blocked = true,
+ .write_frames = true,
+ .priv_size = sizeof(struct priv),
+ .init = init,
+ .uninit = uninit,
+ .get_state = get_state,
+ .set_pause = set_pause,
+ .write = audio_write,
+ .start = start,
+ .reset = reset,
+};
+
+// vim: sw=4 ts=4 et tw=80
diff --git a/audio/out/ao_null.c b/audio/out/ao_null.c
new file mode 100644
index 0000000..fcb61d2
--- /dev/null
+++ b/audio/out/ao_null.c
@@ -0,0 +1,230 @@
+/*
+ * null audio output driver
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Note: this does much more than just ignoring audio output. It simulates
+ * (to some degree) an ideal AO.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include "mpv_talloc.h"
+
+#include "osdep/timer.h"
+#include "options/m_option.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "audio/format.h"
+#include "ao.h"
+#include "internal.h"
+
+struct priv {
+ bool paused;
+ double last_time;
+ float buffered; // samples
+ int buffersize; // samples
+ bool playing;
+
+ bool untimed;
+ float bufferlen; // seconds
+ float speed; // multiplier
+ float latency_sec; // seconds
+ float latency; // samples
+ bool broken_eof;
+ bool broken_delay;
+
+ // Minimal unit of audio samples that can be written at once. If play() is
+ // called with sizes not aligned to this, a rounded size will be returned.
+ // (This is not needed by the AO API, but many AOs behave this way.)
+ int outburst; // samples
+
+ struct m_channels channel_layouts;
+ int format;
+};
+
+static void drain(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ if (ao->untimed) {
+ priv->buffered = 0;
+ return;
+ }
+
+ if (priv->paused)
+ return;
+
+ double now = mp_time_sec();
+ if (priv->buffered > 0) {
+ priv->buffered -= (now - priv->last_time) * ao->samplerate * priv->speed;
+ if (priv->buffered < 0)
+ priv->buffered = 0;
+ }
+ priv->last_time = now;
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ if (priv->format)
+ ao->format = priv->format;
+
+ ao->untimed = priv->untimed;
+
+ struct mp_chmap_sel sel = {.tmp = ao};
+ if (priv->channel_layouts.num_chmaps) {
+ for (int n = 0; n < priv->channel_layouts.num_chmaps; n++)
+ mp_chmap_sel_add_map(&sel, &priv->channel_layouts.chmaps[n]);
+ } else {
+ mp_chmap_sel_add_any(&sel);
+ }
+ if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
+ mp_chmap_from_channels(&ao->channels, 2);
+
+ priv->latency = priv->latency_sec * ao->samplerate;
+
+ // A "buffer" for this many seconds of audio
+ int bursts = (int)(ao->samplerate * priv->bufferlen + 1) / priv->outburst;
+ ao->device_buffer = priv->outburst * bursts + priv->latency;
+
+ priv->last_time = mp_time_sec();
+
+ return 0;
+}
+
+// close audio device
+static void uninit(struct ao *ao)
+{
+}
+
+// stop playing and empty buffers (for seeking/pause)
+static void reset(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ priv->buffered = 0;
+ priv->playing = false;
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ if (priv->paused)
+ MP_ERR(ao, "illegal state: start() while paused\n");
+
+ drain(ao);
+ priv->paused = false;
+ priv->last_time = mp_time_sec();
+ priv->playing = true;
+}
+
+static bool set_pause(struct ao *ao, bool paused)
+{
+ struct priv *priv = ao->priv;
+
+ if (!priv->playing)
+ MP_ERR(ao, "illegal state: set_pause() while not playing\n");
+
+ if (priv->paused != paused) {
+
+ drain(ao);
+ priv->paused = paused;
+ if (!priv->paused)
+ priv->last_time = mp_time_sec();
+ }
+
+ return true;
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *priv = ao->priv;
+
+ if (priv->buffered <= 0)
+ priv->buffered = priv->latency; // emulate fixed latency
+
+ priv->buffered += samples;
+ return true;
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct priv *priv = ao->priv;
+
+ drain(ao);
+
+ state->free_samples = ao->device_buffer - priv->latency - priv->buffered;
+ state->free_samples = state->free_samples / priv->outburst * priv->outburst;
+ state->queued_samples = priv->buffered;
+
+ // Note how get_state returns the delay in audio device time (instead of
+ // adjusting for speed), since most AOs seem to also do that.
+ state->delay = priv->buffered;
+
+ // Drivers with broken EOF handling usually always report the same device-
+ // level delay that is additional to the buffer time.
+ if (priv->broken_eof && priv->buffered < priv->latency)
+ state->delay = priv->latency;
+
+ state->delay /= ao->samplerate;
+
+ if (priv->broken_delay) { // Report only multiples of outburst
+ double q = priv->outburst / (double)ao->samplerate;
+ if (state->delay > 0)
+ state->delay = (int)(state->delay / q) * q;
+ }
+
+ state->playing = priv->playing && priv->buffered > 0;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_null = {
+ .description = "Null audio output",
+ .name = "null",
+ .init = init,
+ .uninit = uninit,
+ .reset = reset,
+ .get_state = get_state,
+ .set_pause = set_pause,
+ .write = audio_write,
+ .start = start,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .bufferlen = 0.2,
+ .outburst = 256,
+ .speed = 1,
+ },
+ .options = (const struct m_option[]) {
+ {"untimed", OPT_BOOL(untimed)},
+ {"buffer", OPT_FLOAT(bufferlen), M_RANGE(0, 100)},
+ {"outburst", OPT_INT(outburst), M_RANGE(1, 100000)},
+ {"speed", OPT_FLOAT(speed), M_RANGE(0, 10000)},
+ {"latency", OPT_FLOAT(latency_sec), M_RANGE(0, 100)},
+ {"broken-eof", OPT_BOOL(broken_eof)},
+ {"broken-delay", OPT_BOOL(broken_delay)},
+ {"channel-layouts", OPT_CHANNELS(channel_layouts)},
+ {"format", OPT_AUDIOFORMAT(format)},
+ {0}
+ },
+ .options_prefix = "ao-null",
+};
diff --git a/audio/out/ao_openal.c b/audio/out/ao_openal.c
new file mode 100644
index 0000000..7172908
--- /dev/null
+++ b/audio/out/ao_openal.c
@@ -0,0 +1,401 @@
+/*
+ * OpenAL audio output driver for MPlayer
+ *
+ * Copyleft 2006 by Reimar Döffinger (Reimar.Doeffinger@stud.uni-karlsruhe.de)
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <inttypes.h>
+#ifdef OPENAL_AL_H
+#include <OpenAL/alc.h>
+#include <OpenAL/al.h>
+#include <OpenAL/alext.h>
+#else
+#include <AL/alc.h>
+#include <AL/al.h>
+#include <AL/alext.h>
+#endif
+
+#include "common/msg.h"
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/format.h"
+#include "osdep/timer.h"
+#include "options/m_option.h"
+
+#define MAX_CHANS MP_NUM_CHANNELS
+#define MAX_BUF 128
+#define MAX_SAMPLES 32768
+static ALuint buffers[MAX_BUF];
+static ALuint buffer_size[MAX_BUF];
+static ALuint source;
+
+static int cur_buf;
+static int unqueue_buf;
+
+static struct ao *ao_data;
+
+struct priv {
+ ALenum al_format;
+ int num_buffers;
+ int num_samples;
+ bool direct_channels;
+};
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME:
+ case AOCONTROL_SET_VOLUME: {
+ ALfloat volume;
+ float *vol = arg;
+ if (cmd == AOCONTROL_SET_VOLUME) {
+ volume = *vol / 100.0;
+ alListenerf(AL_GAIN, volume);
+ }
+ alGetListenerf(AL_GAIN, &volume);
+ *vol = volume * 100;
+ return CONTROL_TRUE;
+ }
+ case AOCONTROL_GET_MUTE:
+ case AOCONTROL_SET_MUTE: {
+ bool mute = *(bool *)arg;
+
+ // openal has no mute control, only gain.
+ // Thus reverse the muted state to get required gain
+ ALfloat al_mute = (ALfloat)(!mute);
+ if (cmd == AOCONTROL_SET_MUTE) {
+ alSourcef(source, AL_GAIN, al_mute);
+ }
+ alGetSourcef(source, AL_GAIN, &al_mute);
+ *(bool *)arg = !((bool)al_mute);
+ return CONTROL_TRUE;
+ }
+
+ }
+ return CONTROL_UNKNOWN;
+}
+
+static enum af_format get_supported_format(int format)
+{
+ switch (format) {
+ case AF_FORMAT_U8:
+ if (alGetEnumValue((ALchar*)"AL_FORMAT_MONO8"))
+ return AF_FORMAT_U8;
+ break;
+
+ case AF_FORMAT_S16:
+ if (alGetEnumValue((ALchar*)"AL_FORMAT_MONO16"))
+ return AF_FORMAT_S16;
+ break;
+
+ case AF_FORMAT_S32:
+ if (strstr(alGetString(AL_RENDERER), "X-Fi") != NULL)
+ return AF_FORMAT_S32;
+ break;
+
+ case AF_FORMAT_FLOAT:
+ if (alIsExtensionPresent((ALchar*)"AL_EXT_float32") == AL_TRUE)
+ return AF_FORMAT_FLOAT;
+ break;
+ }
+ return AL_FALSE;
+}
+
+static ALenum get_supported_layout(int format, int channels)
+{
+ const char *channel_str[] = {
+ [1] = "MONO",
+ [2] = "STEREO",
+ [4] = "QUAD",
+ [6] = "51CHN",
+ [7] = "61CHN",
+ [8] = "71CHN",
+ };
+ const char *format_str[] = {
+ [AF_FORMAT_U8] = "8",
+ [AF_FORMAT_S16] = "16",
+ [AF_FORMAT_S32] = "32",
+ [AF_FORMAT_FLOAT] = "_FLOAT32",
+ };
+ if (channel_str[channels] == NULL || format_str[format] == NULL)
+ return AL_FALSE;
+
+ char enum_name[32];
+ // AF_FORMAT_FLOAT uses same enum name as AF_FORMAT_S32 for multichannel
+ // playback, while it is different for mono and stereo.
+ // OpenAL Soft does not support AF_FORMAT_S32 and seems to reuse the names.
+ if (channels > 2 && format == AF_FORMAT_FLOAT)
+ format = AF_FORMAT_S32;
+ snprintf(enum_name, sizeof(enum_name), "AL_FORMAT_%s%s", channel_str[channels],
+ format_str[format]);
+
+ if (alGetEnumValue((ALchar*)enum_name)) {
+ return alGetEnumValue((ALchar*)enum_name);
+ }
+ return AL_FALSE;
+}
+
+// close audio device
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ alSourceStop(source);
+ alSourcei(source, AL_BUFFER, 0);
+
+ alDeleteBuffers(p->num_buffers, buffers);
+ alDeleteSources(1, &source);
+
+ ALCcontext *ctx = alcGetCurrentContext();
+ ALCdevice *dev = alcGetContextsDevice(ctx);
+ alcMakeContextCurrent(NULL);
+ alcDestroyContext(ctx);
+ alcCloseDevice(dev);
+ ao_data = NULL;
+}
+
+static int init(struct ao *ao)
+{
+ float position[3] = {0, 0, 0};
+ float direction[6] = {0, 0, -1, 0, 1, 0};
+ ALCdevice *dev = NULL;
+ ALCcontext *ctx = NULL;
+ ALCint freq = 0;
+ ALCint attribs[] = {ALC_FREQUENCY, ao->samplerate, 0, 0};
+ struct priv *p = ao->priv;
+ if (ao_data) {
+ MP_FATAL(ao, "Not reentrant!\n");
+ return -1;
+ }
+ ao_data = ao;
+ char *dev_name = ao->device;
+ dev = alcOpenDevice(dev_name && dev_name[0] ? dev_name : NULL);
+ if (!dev) {
+ MP_FATAL(ao, "could not open device\n");
+ goto err_out;
+ }
+ ctx = alcCreateContext(dev, attribs);
+ alcMakeContextCurrent(ctx);
+ alListenerfv(AL_POSITION, position);
+ alListenerfv(AL_ORIENTATION, direction);
+
+ alGenSources(1, &source);
+ if (p->direct_channels) {
+ if (alIsExtensionPresent("AL_SOFT_direct_channels_remix")) {
+ alSourcei(source,
+ alGetEnumValue((ALchar*)"AL_DIRECT_CHANNELS_SOFT"),
+ alcGetEnumValue(dev, "AL_REMIX_UNMATCHED_SOFT"));
+ } else {
+ MP_WARN(ao, "Direct channels aren't supported by this version of OpenAL\n");
+ }
+ }
+
+ cur_buf = 0;
+ unqueue_buf = 0;
+ for (int i = 0; i < p->num_buffers; ++i) {
+ buffer_size[i] = 0;
+ }
+
+ alGenBuffers(p->num_buffers, buffers);
+
+ alcGetIntegerv(dev, ALC_FREQUENCY, 1, &freq);
+ if (alcGetError(dev) == ALC_NO_ERROR && freq)
+ ao->samplerate = freq;
+
+ // Check sample format
+ int try_formats[AF_FORMAT_COUNT + 1];
+ enum af_format sample_format = 0;
+ af_get_best_sample_formats(ao->format, try_formats);
+ for (int n = 0; try_formats[n]; n++) {
+ sample_format = get_supported_format(try_formats[n]);
+ if (sample_format != AF_FORMAT_UNKNOWN) {
+ ao->format = try_formats[n];
+ break;
+ }
+ }
+
+ if (sample_format == AF_FORMAT_UNKNOWN) {
+ MP_FATAL(ao, "Can't find appropriate sample format.\n");
+ uninit(ao);
+ goto err_out;
+ }
+
+ // Check if OpenAL driver supports the desired number of channels.
+ int num_channels = ao->channels.num;
+ do {
+ p->al_format = get_supported_layout(sample_format, num_channels);
+ if (p->al_format == AL_FALSE) {
+ num_channels = num_channels - 1;
+ }
+ } while (p->al_format == AL_FALSE && num_channels > 1);
+
+ // Request number of speakers for output from ao.
+ const struct mp_chmap possible_layouts[] = {
+ {0}, // empty
+ MP_CHMAP_INIT_MONO, // mono
+ MP_CHMAP_INIT_STEREO, // stereo
+ {0}, // 2.1
+ MP_CHMAP4(FL, FR, BL, BR), // 4.0
+ {0}, // 5.0
+ MP_CHMAP6(FL, FR, FC, LFE, BL, BR), // 5.1
+ MP_CHMAP7(FL, FR, FC, LFE, SL, SR, BC), // 6.1
+ MP_CHMAP8(FL, FR, FC, LFE, BL, BR, SL, SR), // 7.1
+ };
+ ao->channels = possible_layouts[num_channels];
+ if (!ao->channels.num)
+ mp_chmap_set_unknown(&ao->channels, num_channels);
+
+ if (p->al_format == AL_FALSE || !mp_chmap_is_valid(&ao->channels)) {
+ MP_FATAL(ao, "Can't find appropriate channel layout.\n");
+ uninit(ao);
+ goto err_out;
+ }
+
+ ao->device_buffer = p->num_buffers * p->num_samples;
+ return 0;
+
+err_out:
+ ao_data = NULL;
+ return -1;
+}
+
+static void unqueue_buffers(struct ao *ao)
+{
+ struct priv *q = ao->priv;
+ ALint p;
+ int till_wrap = q->num_buffers - unqueue_buf;
+ alGetSourcei(source, AL_BUFFERS_PROCESSED, &p);
+ if (p >= till_wrap) {
+ alSourceUnqueueBuffers(source, till_wrap, &buffers[unqueue_buf]);
+ unqueue_buf = 0;
+ p -= till_wrap;
+ }
+ if (p) {
+ alSourceUnqueueBuffers(source, p, &buffers[unqueue_buf]);
+ unqueue_buf += p;
+ }
+}
+
+static void reset(struct ao *ao)
+{
+ alSourceStop(source);
+ unqueue_buffers(ao);
+}
+
+static bool audio_set_pause(struct ao *ao, bool pause)
+{
+ if (pause) {
+ alSourcePause(source);
+ } else {
+ alSourcePlay(source);
+ }
+ return true;
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *p = ao->priv;
+
+ int num = (samples + p->num_samples - 1) / p->num_samples;
+
+ for (int i = 0; i < num; i++) {
+ char *d = *data;
+ buffer_size[cur_buf] =
+ MPMIN(samples - i * p->num_samples, p->num_samples);
+ d += i * buffer_size[cur_buf] * ao->sstride;
+ alBufferData(buffers[cur_buf], p->al_format, d,
+ buffer_size[cur_buf] * ao->sstride, ao->samplerate);
+ alSourceQueueBuffers(source, 1, &buffers[cur_buf]);
+ cur_buf = (cur_buf + 1) % p->num_buffers;
+ }
+
+ return true;
+}
+
+static void audio_start(struct ao *ao)
+{
+ alSourcePlay(source);
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct priv *p = ao->priv;
+
+ ALint queued;
+ unqueue_buffers(ao);
+ alGetSourcei(source, AL_BUFFERS_QUEUED, &queued);
+
+ double source_offset = 0;
+ if(alIsExtensionPresent("AL_SOFT_source_latency")) {
+ ALdouble offsets[2];
+ LPALGETSOURCEDVSOFT alGetSourcedvSOFT = alGetProcAddress("alGetSourcedvSOFT");
+ alGetSourcedvSOFT(source, AL_SEC_OFFSET_LATENCY_SOFT, offsets);
+ // Additional latency to the play buffer, the remaining seconds to be
+ // played minus the offset (seconds already played)
+ source_offset = offsets[1] - offsets[0];
+ } else {
+ float offset = 0;
+ alGetSourcef(source, AL_SEC_OFFSET, &offset);
+ source_offset = -offset;
+ }
+
+ int queued_samples = 0;
+ for (int i = 0, index = cur_buf; i < queued; ++i) {
+ queued_samples += buffer_size[index];
+ index = (index + 1) % p->num_buffers;
+ }
+
+ state->delay = queued_samples / (double)ao->samplerate + source_offset;
+
+ state->queued_samples = queued_samples;
+ state->free_samples = MPMAX(p->num_buffers - queued, 0) * p->num_samples;
+
+ ALint source_state = 0;
+ alGetSourcei(source, AL_SOURCE_STATE, &source_state);
+ state->playing = source_state == AL_PLAYING;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_openal = {
+ .description = "OpenAL audio output",
+ .name = "openal",
+ .init = init,
+ .uninit = uninit,
+ .control = control,
+ .get_state = get_state,
+ .write = audio_write,
+ .start = audio_start,
+ .set_pause = audio_set_pause,
+ .reset = reset,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .num_buffers = 4,
+ .num_samples = 8192,
+ .direct_channels = true,
+ },
+ .options = (const struct m_option[]) {
+ {"num-buffers", OPT_INT(num_buffers), M_RANGE(2, MAX_BUF)},
+ {"num-samples", OPT_INT(num_samples), M_RANGE(256, MAX_SAMPLES)},
+ {"direct-channels", OPT_BOOL(direct_channels)},
+ {0}
+ },
+ .options_prefix = "openal",
+};
diff --git a/audio/out/ao_opensles.c b/audio/out/ao_opensles.c
new file mode 100644
index 0000000..ddcff19
--- /dev/null
+++ b/audio/out/ao_opensles.c
@@ -0,0 +1,265 @@
+/*
+ * OpenSL ES audio output driver.
+ * Copyright (C) 2016 Ilya Zhuravlev <whatever@xyz.is>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ao.h"
+#include "internal.h"
+#include "common/msg.h"
+#include "audio/format.h"
+#include "options/m_option.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+
+struct priv {
+ SLObjectItf sl, output_mix, player;
+ SLBufferQueueItf buffer_queue;
+ SLEngineItf engine;
+ SLPlayItf play;
+ void *buf;
+ int bytes_per_enqueue;
+ mp_mutex buffer_lock;
+ double audio_latency;
+
+ int frames_per_enqueue;
+ int buffer_size_in_ms;
+};
+
+#define DESTROY(thing) \
+ if (p->thing) { \
+ (*p->thing)->Destroy(p->thing); \
+ p->thing = NULL; \
+ }
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ DESTROY(player);
+ DESTROY(output_mix);
+ DESTROY(sl);
+
+ p->buffer_queue = NULL;
+ p->engine = NULL;
+ p->play = NULL;
+
+ mp_mutex_destroy(&p->buffer_lock);
+
+ free(p->buf);
+ p->buf = NULL;
+}
+
+#undef DESTROY
+
+static void buffer_callback(SLBufferQueueItf buffer_queue, void *context)
+{
+ struct ao *ao = context;
+ struct priv *p = ao->priv;
+ SLresult res;
+ double delay;
+
+ mp_mutex_lock(&p->buffer_lock);
+
+ delay = p->frames_per_enqueue / (double)ao->samplerate;
+ delay += p->audio_latency;
+ ao_read_data(ao, &p->buf, p->frames_per_enqueue,
+ mp_time_ns() + MP_TIME_S_TO_NS(delay));
+
+ res = (*buffer_queue)->Enqueue(buffer_queue, p->buf, p->bytes_per_enqueue);
+ if (res != SL_RESULT_SUCCESS)
+ MP_ERR(ao, "Failed to Enqueue: %d\n", res);
+
+ mp_mutex_unlock(&p->buffer_lock);
+}
+
+#define CHK(stmt) \
+ { \
+ SLresult res = stmt; \
+ if (res != SL_RESULT_SUCCESS) { \
+ MP_ERR(ao, "%s: %d\n", #stmt, res); \
+ goto error; \
+ } \
+ }
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ SLDataLocator_BufferQueue locator_buffer_queue;
+ SLDataLocator_OutputMix locator_output_mix;
+ SLAndroidDataFormat_PCM_EX pcm;
+ SLDataSource audio_source;
+ SLDataSink audio_sink;
+
+ // This AO only supports two channels at the moment
+ mp_chmap_from_channels(&ao->channels, 2);
+ // Upstream "Wilhelm" supports only 8000 <= rate <= 192000
+ ao->samplerate = MPCLAMP(ao->samplerate, 8000, 192000);
+
+ CHK(slCreateEngine(&p->sl, 0, NULL, 0, NULL, NULL));
+ CHK((*p->sl)->Realize(p->sl, SL_BOOLEAN_FALSE));
+ CHK((*p->sl)->GetInterface(p->sl, SL_IID_ENGINE, (void*)&p->engine));
+ CHK((*p->engine)->CreateOutputMix(p->engine, &p->output_mix, 0, NULL, NULL));
+ CHK((*p->output_mix)->Realize(p->output_mix, SL_BOOLEAN_FALSE));
+
+ locator_buffer_queue.locatorType = SL_DATALOCATOR_BUFFERQUEUE;
+ locator_buffer_queue.numBuffers = 8;
+
+ if (af_fmt_is_int(ao->format)) {
+ // Be future-proof
+ if (af_fmt_to_bytes(ao->format) > 2)
+ ao->format = AF_FORMAT_S32;
+ else
+ ao->format = af_fmt_from_planar(ao->format);
+ pcm.formatType = SL_DATAFORMAT_PCM;
+ } else {
+ ao->format = AF_FORMAT_FLOAT;
+ pcm.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
+ pcm.representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT;
+ }
+ pcm.numChannels = ao->channels.num;
+ pcm.containerSize = pcm.bitsPerSample = 8 * af_fmt_to_bytes(ao->format);
+ pcm.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+ pcm.endianness = SL_BYTEORDER_LITTLEENDIAN;
+ pcm.sampleRate = ao->samplerate * 1000;
+
+ if (p->buffer_size_in_ms) {
+ ao->device_buffer = ao->samplerate * p->buffer_size_in_ms / 1000;
+ // As the purpose of buffer_size_in_ms is to request a specific
+ // soft buffer size:
+ ao->def_buffer = 0;
+ }
+
+ // But it does not make sense if it is smaller than the enqueue size:
+ if (p->frames_per_enqueue) {
+ ao->device_buffer = MPMAX(ao->device_buffer, p->frames_per_enqueue);
+ } else {
+ if (ao->device_buffer) {
+ p->frames_per_enqueue = ao->device_buffer;
+ } else if (ao->def_buffer) {
+ p->frames_per_enqueue = ao->def_buffer * ao->samplerate;
+ } else {
+ MP_ERR(ao, "Enqueue size is not set and can neither be derived\n");
+ goto error;
+ }
+ }
+
+ p->bytes_per_enqueue = p->frames_per_enqueue * ao->channels.num *
+ af_fmt_to_bytes(ao->format);
+ p->buf = calloc(1, p->bytes_per_enqueue);
+ if (!p->buf) {
+ MP_ERR(ao, "Failed to allocate device buffer\n");
+ goto error;
+ }
+
+ int r = mp_mutex_init(&p->buffer_lock);
+ if (r) {
+ MP_ERR(ao, "Failed to initialize the mutex: %d\n", r);
+ goto error;
+ }
+
+ audio_source.pFormat = (void*)&pcm;
+ audio_source.pLocator = (void*)&locator_buffer_queue;
+
+ locator_output_mix.locatorType = SL_DATALOCATOR_OUTPUTMIX;
+ locator_output_mix.outputMix = p->output_mix;
+
+ audio_sink.pLocator = (void*)&locator_output_mix;
+ audio_sink.pFormat = NULL;
+
+ SLInterfaceID iid_array[] = { SL_IID_BUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION };
+ SLboolean required[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE };
+ CHK((*p->engine)->CreateAudioPlayer(p->engine, &p->player, &audio_source,
+ &audio_sink, 2, iid_array, required));
+
+ CHK((*p->player)->Realize(p->player, SL_BOOLEAN_FALSE));
+ CHK((*p->player)->GetInterface(p->player, SL_IID_PLAY, (void*)&p->play));
+ CHK((*p->player)->GetInterface(p->player, SL_IID_BUFFERQUEUE,
+ (void*)&p->buffer_queue));
+ CHK((*p->buffer_queue)->RegisterCallback(p->buffer_queue,
+ buffer_callback, ao));
+ CHK((*p->play)->SetPlayState(p->play, SL_PLAYSTATE_PLAYING));
+
+ SLAndroidConfigurationItf android_config;
+ SLuint32 audio_latency = 0, value_size = sizeof(SLuint32);
+
+ SLint32 get_interface_result = (*p->player)->GetInterface(
+ p->player,
+ SL_IID_ANDROIDCONFIGURATION,
+ &android_config
+ );
+
+ if (get_interface_result == SL_RESULT_SUCCESS) {
+ SLint32 get_configuration_result = (*android_config)->GetConfiguration(
+ android_config,
+ (const SLchar *)"androidGetAudioLatency",
+ &value_size,
+ &audio_latency
+ );
+
+ if (get_configuration_result == SL_RESULT_SUCCESS) {
+ p->audio_latency = (double)audio_latency / 1000.0;
+ MP_INFO(ao, "Device latency is %f\n", p->audio_latency);
+ }
+ }
+
+ return 1;
+error:
+ uninit(ao);
+ return -1;
+}
+
+#undef CHK
+
+static void reset(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ (*p->buffer_queue)->Clear(p->buffer_queue);
+}
+
+static void resume(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ buffer_callback(p->buffer_queue, ao);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_opensles = {
+ .description = "OpenSL ES audio output",
+ .name = "opensles",
+ .init = init,
+ .uninit = uninit,
+ .reset = reset,
+ .start = resume,
+
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .buffer_size_in_ms = 250,
+ },
+ .options = (const struct m_option[]) {
+ {"frames-per-enqueue", OPT_INT(frames_per_enqueue),
+ M_RANGE(1, 96000)},
+ {"buffer-size-in-ms", OPT_INT(buffer_size_in_ms),
+ M_RANGE(0, 500)},
+ {0}
+ },
+ .options_prefix = "opensles",
+};
diff --git a/audio/out/ao_oss.c b/audio/out/ao_oss.c
new file mode 100644
index 0000000..5c0b8c9
--- /dev/null
+++ b/audio/out/ao_oss.c
@@ -0,0 +1,400 @@
+/*
+ * OSS audio output driver
+ *
+ * Original author: A'rpi
+ * Support for >2 output channels added 2001-11-25
+ * - Steve Davies <steve@daviesfam.org>
+ * Rozhuk Ivan <rozhuk.im@gmail.com> 2020-2023
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/soundcard.h>
+#include <sys/stat.h>
+#if defined(__DragonFly__) || defined(__FreeBSD__)
+#include <sys/sysctl.h>
+#endif
+#include <sys/types.h>
+
+#include "audio/format.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "osdep/endian.h"
+#include "osdep/io.h"
+#include "ao.h"
+#include "internal.h"
+
+#ifndef AFMT_AC3
+#define AFMT_AC3 -1
+#endif
+
+#define PATH_DEV_DSP "/dev/dsp"
+#define PATH_DEV_MIXER "/dev/mixer"
+
+struct priv {
+ int dsp_fd;
+ double bps; /* Bytes per second. */
+};
+
+/* like alsa except for 6.1 and 7.1, from pcm/matrix_map.h */
+static const struct mp_chmap oss_layouts[MP_NUM_CHANNELS + 1] = {
+ {0}, /* empty */
+ MP_CHMAP_INIT_MONO, /* mono */
+ MP_CHMAP2(FL, FR), /* stereo */
+ MP_CHMAP3(FL, FR, LFE), /* 2.1 */
+ MP_CHMAP4(FL, FR, BL, BR), /* 4.0 */
+ MP_CHMAP5(FL, FR, BL, BR, FC), /* 5.0 */
+ MP_CHMAP6(FL, FR, BL, BR, FC, LFE), /* 5.1 */
+ MP_CHMAP7(FL, FR, BL, BR, FC, LFE, BC), /* 6.1 */
+ MP_CHMAP8(FL, FR, BL, BR, FC, LFE, SL, SR), /* 7.1 */
+};
+
+#if !defined(AFMT_S32_NE) && defined(AFMT_S32_LE) && defined(AFMT_S32_BE)
+#define AFMT_S32_NE AFMT_S32MP_SELECT_LE_BE(AFMT_S32_LE, AFMT_S32_BE)
+#endif
+
+static const int format_table[][2] = {
+ {AFMT_U8, AF_FORMAT_U8},
+ {AFMT_S16_NE, AF_FORMAT_S16},
+#ifdef AFMT_S32_NE
+ {AFMT_S32_NE, AF_FORMAT_S32},
+#endif
+#ifdef AFMT_FLOAT
+ {AFMT_FLOAT, AF_FORMAT_FLOAT},
+#endif
+#ifdef AFMT_MPEG
+ {AFMT_MPEG, AF_FORMAT_S_MP3},
+#endif
+ {-1, -1}
+};
+
+#define MP_WARN_IOCTL_ERR(__ao) \
+ MP_WARN((__ao), "%s: ioctl() fail, err = %i: %s\n", \
+ __FUNCTION__, errno, strerror(errno))
+
+
+static void uninit(struct ao *ao);
+
+
+static void device_descr_get(size_t dev_idx, char *buf, size_t buf_size)
+{
+#if defined(__DragonFly__) || defined(__FreeBSD__)
+ char dev_path[32];
+ size_t tmp = (buf_size - 1);
+
+ snprintf(dev_path, sizeof(dev_path), "dev.pcm.%zu.%%desc", dev_idx);
+ if (sysctlbyname(dev_path, buf, &tmp, NULL, 0) != 0) {
+ tmp = 0;
+ }
+ buf[tmp] = 0x00;
+#elif defined(SOUND_MIXER_INFO)
+ size_t tmp = 0;
+ char dev_path[32];
+ mixer_info mi;
+
+ snprintf(dev_path, sizeof(dev_path), PATH_DEV_MIXER"%zu", dev_idx);
+ int fd = open(dev_path, O_RDONLY);
+ if (ioctl(fd, SOUND_MIXER_INFO, &mi) == 0) {
+ strncpy(buf, mi.name, buf_size - 1);
+ tmp = (buf_size - 1);
+ }
+ close(fd);
+ buf[tmp] = 0x00;
+#else
+ buf[0] = 0x00;
+#endif
+}
+
+static int format2oss(int format)
+{
+ for (size_t i = 0; format_table[i][0] != -1; i++) {
+ if (format_table[i][1] == format)
+ return format_table[i][0];
+ }
+ return -1;
+}
+
+static bool try_format(struct ao *ao, int *format)
+{
+ struct priv *p = ao->priv;
+ int oss_format = format2oss(*format);
+
+ if (oss_format == -1 && af_fmt_is_spdif(*format))
+ oss_format = AFMT_AC3;
+
+ if (oss_format == -1) {
+ MP_VERBOSE(ao, "Unknown/not supported internal format: %s\n",
+ af_fmt_to_str(*format));
+ *format = 0;
+ return false;
+ }
+
+ return (ioctl(p->dsp_fd, SNDCTL_DSP_SETFMT, &oss_format) != -1);
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ struct mp_chmap channels = ao->channels;
+ audio_buf_info info;
+ size_t i;
+ int format, samplerate, nchannels, reqchannels, trig = 0;
+ int best_sample_formats[AF_FORMAT_COUNT + 1];
+ const char *device = ((ao->device) ? ao->device : PATH_DEV_DSP);
+
+ /* Opening device. */
+ MP_VERBOSE(ao, "Using '%s' audio device.\n", device);
+ p->dsp_fd = open(device, (O_WRONLY | O_CLOEXEC));
+ if (p->dsp_fd < 0) {
+ MP_ERR(ao, "Can't open audio device %s: %s.\n",
+ device, mp_strerror(errno));
+ goto err_out;
+ }
+
+ /* Selecting sound format. */
+ format = af_fmt_from_planar(ao->format);
+ af_get_best_sample_formats(format, best_sample_formats);
+ for (i = 0; best_sample_formats[i]; i++) {
+ format = best_sample_formats[i];
+ if (try_format(ao, &format))
+ break;
+ }
+ if (!format) {
+ MP_ERR(ao, "Can't set sample format.\n");
+ goto err_out;
+ }
+ MP_VERBOSE(ao, "Sample format: %s\n", af_fmt_to_str(format));
+
+ /* Channels count. */
+ if (af_fmt_is_spdif(format)) {
+ nchannels = reqchannels = channels.num;
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_CHANNELS, &nchannels) == -1) {
+ MP_ERR(ao, "Failed to set audio device to %d channels.\n",
+ reqchannels);
+ goto err_out_ioctl;
+ }
+ } else {
+ struct mp_chmap_sel sel = {0};
+ for (i = 0; i < MP_ARRAY_SIZE(oss_layouts); i++) {
+ mp_chmap_sel_add_map(&sel, &oss_layouts[i]);
+ }
+ if (!ao_chmap_sel_adjust(ao, &sel, &channels))
+ goto err_out;
+ nchannels = reqchannels = channels.num;
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_CHANNELS, &nchannels) == -1) {
+ MP_ERR(ao, "Failed to set audio device to %d channels.\n",
+ reqchannels);
+ goto err_out_ioctl;
+ }
+ if (nchannels != reqchannels) {
+ /* Update number of channels to OSS suggested value. */
+ if (!ao_chmap_sel_get_def(ao, &sel, &channels, nchannels))
+ goto err_out;
+ }
+ MP_VERBOSE(ao, "Using %d channels (requested: %d).\n",
+ channels.num, reqchannels);
+ }
+
+ /* Sample rate. */
+ samplerate = ao->samplerate;
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_SPEED, &samplerate) == -1)
+ goto err_out_ioctl;
+ MP_VERBOSE(ao, "Using %d Hz samplerate.\n", samplerate);
+
+ /* Get buffer size. */
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1)
+ goto err_out_ioctl;
+ /* See ao.c ao->sstride initializations and get_state(). */
+ ao->device_buffer = ((info.fragstotal * info.fragsize) /
+ af_fmt_to_bytes(format));
+ if (!af_fmt_is_planar(format)) {
+ ao->device_buffer /= channels.num;
+ }
+
+ /* Do not start playback after data written. */
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1)
+ goto err_out_ioctl;
+
+ /* Update sound params. */
+ ao->format = format;
+ ao->samplerate = samplerate;
+ ao->channels = channels;
+ p->bps = (channels.num * samplerate * af_fmt_to_bytes(format));
+
+ return 0;
+
+err_out_ioctl:
+ MP_WARN_IOCTL_ERR(ao);
+err_out:
+ uninit(ao);
+ return -1;
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ if (p->dsp_fd == -1)
+ return;
+ ioctl(p->dsp_fd, SNDCTL_DSP_HALT, NULL);
+ close(p->dsp_fd);
+ p->dsp_fd = -1;
+}
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct priv *p = ao->priv;
+ float *vol = arg;
+ int v;
+
+ if (p->dsp_fd < 0)
+ return CONTROL_ERROR;
+
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME:
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_GETPLAYVOL, &v) == -1) {
+ MP_WARN_IOCTL_ERR(ao);
+ return CONTROL_ERROR;
+ }
+ *vol = ((v & 0x00ff) + ((v & 0xff00) >> 8)) / 2.0;
+ return CONTROL_OK;
+ case AOCONTROL_SET_VOLUME:
+ v = ((int)*vol << 8) | (int)*vol;
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_SETPLAYVOL, &v) == -1) {
+ MP_WARN_IOCTL_ERR(ao);
+ return CONTROL_ERROR;
+ }
+ return CONTROL_OK;
+ }
+
+ return CONTROL_UNKNOWN;
+}
+
+static void reset(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ int trig = 0;
+
+ /* Clear buf and do not start playback after data written. */
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_HALT, NULL) == -1 ||
+ ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1)
+ {
+ MP_WARN_IOCTL_ERR(ao);
+ MP_WARN(ao, "Force reinitialize audio device.\n");
+ uninit(ao);
+ init(ao);
+ }
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ int trig = PCM_ENABLE_OUTPUT;
+
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1) {
+ MP_WARN_IOCTL_ERR(ao);
+ return;
+ }
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *p = ao->priv;
+ ssize_t rc;
+ const size_t size = (samples * ao->sstride);
+
+ if (size == 0)
+ return true;
+
+ while ((rc = write(p->dsp_fd, data[0], size)) == -1) {
+ if (errno == EINTR)
+ continue;
+ MP_WARN(ao, "audio_write: write() fail, err = %i: %s.\n",
+ errno, strerror(errno));
+ return false;
+ }
+ if ((size_t)rc != size) {
+ MP_WARN(ao, "audio_write: unexpected partial write: required: %zu, written: %zu.\n",
+ size, (size_t)rc);
+ return false;
+ }
+
+ return true;
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct priv *p = ao->priv;
+ audio_buf_info info;
+ int odelay;
+
+ if (ioctl(p->dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1 ||
+ ioctl(p->dsp_fd, SNDCTL_DSP_GETODELAY, &odelay) == -1)
+ {
+ MP_WARN_IOCTL_ERR(ao);
+ memset(state, 0x00, sizeof(struct mp_pcm_state));
+ state->delay = 0.0;
+ return;
+ }
+ state->free_samples = (info.bytes / ao->sstride);
+ state->queued_samples = (ao->device_buffer - state->free_samples);
+ state->delay = (odelay / p->bps);
+ state->playing = (state->queued_samples != 0);
+}
+
+static void list_devs(struct ao *ao, struct ao_device_list *list)
+{
+ struct stat st;
+ char dev_path[32] = PATH_DEV_DSP, dev_descr[256] = "Default";
+ struct ao_device_desc dev = {.name = dev_path, .desc = dev_descr};
+
+ if (stat(PATH_DEV_DSP, &st) == 0) {
+ ao_device_list_add(list, ao, &dev);
+ }
+
+ /* Auto detect. */
+ for (size_t i = 0, fail_cnt = 0; fail_cnt < 8; i ++, fail_cnt ++) {
+ snprintf(dev_path, sizeof(dev_path), PATH_DEV_DSP"%zu", i);
+ if (stat(dev_path, &st) != 0)
+ continue;
+ device_descr_get(i, dev_descr, sizeof(dev_descr));
+ ao_device_list_add(list, ao, &dev);
+ fail_cnt = 0; /* Reset fail counter. */
+ }
+}
+
+const struct ao_driver audio_out_oss = {
+ .name = "oss",
+ .description = "OSS/ioctl audio output",
+ .init = init,
+ .uninit = uninit,
+ .control = control,
+ .reset = reset,
+ .start = start,
+ .write = audio_write,
+ .get_state = get_state,
+ .list_devs = list_devs,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .dsp_fd = -1,
+ },
+};
diff --git a/audio/out/ao_pcm.c b/audio/out/ao_pcm.c
new file mode 100644
index 0000000..4097aa3
--- /dev/null
+++ b/audio/out/ao_pcm.c
@@ -0,0 +1,248 @@
+/*
+ * PCM audio output driver
+ *
+ * Original author: Atmosfear
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <libavutil/common.h>
+
+#include "mpv_talloc.h"
+
+#include "options/m_option.h"
+#include "audio/format.h"
+#include "ao.h"
+#include "internal.h"
+#include "common/msg.h"
+#include "osdep/endian.h"
+
+#ifdef __MINGW32__
+// for GetFileType to detect pipes
+#include <windows.h>
+#include <io.h>
+#endif
+
+struct priv {
+ char *outputfilename;
+ bool waveheader;
+ bool append;
+ uint64_t data_length;
+ FILE *fp;
+};
+
+#define WAV_ID_RIFF 0x46464952 /* "RIFF" */
+#define WAV_ID_WAVE 0x45564157 /* "WAVE" */
+#define WAV_ID_FMT 0x20746d66 /* "fmt " */
+#define WAV_ID_DATA 0x61746164 /* "data" */
+#define WAV_ID_PCM 0x0001
+#define WAV_ID_FLOAT_PCM 0x0003
+#define WAV_ID_FORMAT_EXTENSIBLE 0xfffe
+
+static void fput16le(uint16_t val, FILE *fp)
+{
+ uint8_t bytes[2] = {val, val >> 8};
+ fwrite(bytes, 1, 2, fp);
+}
+
+static void fput32le(uint32_t val, FILE *fp)
+{
+ uint8_t bytes[4] = {val, val >> 8, val >> 16, val >> 24};
+ fwrite(bytes, 1, 4, fp);
+}
+
+static void write_wave_header(struct ao *ao, FILE *fp, uint64_t data_length)
+{
+ uint16_t fmt = ao->format == AF_FORMAT_FLOAT ? WAV_ID_FLOAT_PCM : WAV_ID_PCM;
+ int bits = af_fmt_to_bytes(ao->format) * 8;
+
+ // Master RIFF chunk
+ fput32le(WAV_ID_RIFF, fp);
+ // RIFF chunk size: 'WAVE' + 'fmt ' + 4 + 40 +
+ // data chunk hdr (8) + data length
+ fput32le(12 + 40 + 8 + data_length, fp);
+ fput32le(WAV_ID_WAVE, fp);
+
+ // Format chunk
+ fput32le(WAV_ID_FMT, fp);
+ fput32le(40, fp);
+ fput16le(WAV_ID_FORMAT_EXTENSIBLE, fp);
+ fput16le(ao->channels.num, fp);
+ fput32le(ao->samplerate, fp);
+ fput32le(ao->bps, fp);
+ fput16le(ao->channels.num * (bits / 8), fp);
+ fput16le(bits, fp);
+
+ // Extension chunk
+ fput16le(22, fp);
+ fput16le(bits, fp);
+ fput32le(mp_chmap_to_waveext(&ao->channels), fp);
+ // 2 bytes format + 14 bytes guid
+ fput32le(fmt, fp);
+ fput32le(0x00100000, fp);
+ fput32le(0xAA000080, fp);
+ fput32le(0x719B3800, fp);
+
+ // Data chunk
+ fput32le(WAV_ID_DATA, fp);
+ fput32le(data_length, fp);
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ char *outputfilename = priv->outputfilename;
+ if (!outputfilename) {
+ outputfilename = talloc_strdup(priv, priv->waveheader ? "audiodump.wav"
+ : "audiodump.pcm");
+ }
+
+ ao->format = af_fmt_from_planar(ao->format);
+
+ if (priv->waveheader) {
+ // WAV files must have one of the following formats
+
+ // And they don't work in big endian; fixing it would be simple, but
+ // nobody cares.
+ if (BYTE_ORDER == BIG_ENDIAN) {
+ MP_FATAL(ao, "Not supported on big endian.\n");
+ return -1;
+ }
+
+ switch (ao->format) {
+ case AF_FORMAT_U8:
+ case AF_FORMAT_S16:
+ case AF_FORMAT_S32:
+ case AF_FORMAT_FLOAT:
+ break;
+ default:
+ if (!af_fmt_is_spdif(ao->format))
+ ao->format = AF_FORMAT_S16;
+ break;
+ }
+ }
+
+ struct mp_chmap_sel sel = {0};
+ mp_chmap_sel_add_waveext(&sel);
+ if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
+ return -1;
+
+ ao->bps = ao->channels.num * ao->samplerate * af_fmt_to_bytes(ao->format);
+
+ MP_INFO(ao, "File: %s (%s)\nPCM: Samplerate: %d Hz Channels: %d Format: %s\n",
+ outputfilename,
+ priv->waveheader ? "WAVE" : "RAW PCM", ao->samplerate,
+ ao->channels.num, af_fmt_to_str(ao->format));
+
+ priv->fp = fopen(outputfilename, priv->append ? "ab" : "wb");
+ if (!priv->fp) {
+ MP_ERR(ao, "Failed to open %s for writing!\n", outputfilename);
+ return -1;
+ }
+ if (priv->waveheader) // Reserve space for wave header
+ write_wave_header(ao, priv->fp, 0x7ffff000);
+ ao->untimed = true;
+ ao->device_buffer = 1 << 16;
+
+ return 0;
+}
+
+// close audio device
+static void uninit(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ if (priv->waveheader) { // Rewrite wave header
+ bool broken_seek = false;
+#ifdef __MINGW32__
+ // Windows, in its usual idiocy "emulates" seeks on pipes so it always
+ // looks like they work. So we have to detect them brute-force.
+ broken_seek = FILE_TYPE_DISK !=
+ GetFileType((HANDLE)_get_osfhandle(_fileno(priv->fp)));
+#endif
+ if (broken_seek || fseek(priv->fp, 0, SEEK_SET) != 0)
+ MP_ERR(ao, "Could not seek to start, WAV size headers not updated!\n");
+ else {
+ if (priv->data_length > 0xfffff000) {
+ MP_ERR(ao, "File larger than allowed for "
+ "WAV files, may play truncated!\n");
+ priv->data_length = 0xfffff000;
+ }
+ write_wave_header(ao, priv->fp, priv->data_length);
+ }
+ }
+ fclose(priv->fp);
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *priv = ao->priv;
+ int len = samples * ao->sstride;
+
+ fwrite(data[0], len, 1, priv->fp);
+ priv->data_length += len;
+
+ return true;
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ state->free_samples = ao->device_buffer;
+ state->queued_samples = 0;
+ state->delay = 0;
+}
+
+static bool set_pause(struct ao *ao, bool paused)
+{
+ return true; // signal support so common code doesn't write silence
+}
+
+static void start(struct ao *ao)
+{
+ // we use data immediately
+}
+
+static void reset(struct ao *ao)
+{
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_pcm = {
+ .description = "RAW PCM/WAVE file writer audio output",
+ .name = "pcm",
+ .init = init,
+ .uninit = uninit,
+ .get_state = get_state,
+ .set_pause = set_pause,
+ .write = audio_write,
+ .start = start,
+ .reset = reset,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) { .waveheader = true },
+ .options = (const struct m_option[]) {
+ {"file", OPT_STRING(outputfilename), .flags = M_OPT_FILE},
+ {"waveheader", OPT_BOOL(waveheader)},
+ {"append", OPT_BOOL(append)},
+ {0}
+ },
+ .options_prefix = "ao-pcm",
+};
diff --git a/audio/out/ao_pipewire.c b/audio/out/ao_pipewire.c
new file mode 100644
index 0000000..3fbcbf6
--- /dev/null
+++ b/audio/out/ao_pipewire.c
@@ -0,0 +1,883 @@
+/*
+ * PipeWire audio output driver.
+ * Copyright (C) 2021 Thomas Weißschuh <thomas@t-8ch.de>
+ * Copyright (C) 2021 Oschowa <oschowa@web.de>
+ * Copyright (C) 2020 Andreas Kempf <aakempf@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <pipewire/pipewire.h>
+#include <pipewire/global.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/utils/result.h>
+#include <math.h>
+
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "ao.h"
+#include "audio/format.h"
+#include "internal.h"
+#include "osdep/timer.h"
+
+#if !PW_CHECK_VERSION(0, 3, 50)
+static inline int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size) {
+ return pw_stream_get_time(stream, time);
+}
+#endif
+
+#if !PW_CHECK_VERSION(0, 3, 57)
+// Earlier versions segfault on zeroed hooks
+#define spa_hook_remove(hook) if ((hook)->link.prev) spa_hook_remove(hook)
+#endif
+
+enum init_state {
+ INIT_STATE_NONE,
+ INIT_STATE_SUCCESS,
+ INIT_STATE_ERROR,
+};
+
+enum {
+ VOLUME_MODE_CHANNEL,
+ VOLUME_MODE_GLOBAL,
+};
+
+struct priv {
+ struct pw_thread_loop *loop;
+ struct pw_stream *stream;
+ struct pw_core *core;
+ struct spa_hook stream_listener;
+ struct spa_hook core_listener;
+ enum init_state init_state;
+
+ bool muted;
+ float volume;
+
+ struct {
+ int buffer_msec;
+ char *remote;
+ int volume_mode;
+ } options;
+
+ struct {
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+ struct spa_list sinks;
+ } hotplug;
+};
+
+struct id_list {
+ uint32_t id;
+ struct spa_list node;
+};
+
+static enum spa_audio_format af_fmt_to_pw(struct ao *ao, enum af_format format)
+{
+ switch (format) {
+ case AF_FORMAT_U8: return SPA_AUDIO_FORMAT_U8;
+ case AF_FORMAT_S16: return SPA_AUDIO_FORMAT_S16;
+ case AF_FORMAT_S32: return SPA_AUDIO_FORMAT_S32;
+ case AF_FORMAT_FLOAT: return SPA_AUDIO_FORMAT_F32;
+ case AF_FORMAT_DOUBLE: return SPA_AUDIO_FORMAT_F64;
+ case AF_FORMAT_U8P: return SPA_AUDIO_FORMAT_U8P;
+ case AF_FORMAT_S16P: return SPA_AUDIO_FORMAT_S16P;
+ case AF_FORMAT_S32P: return SPA_AUDIO_FORMAT_S32P;
+ case AF_FORMAT_FLOATP: return SPA_AUDIO_FORMAT_F32P;
+ case AF_FORMAT_DOUBLEP: return SPA_AUDIO_FORMAT_F64P;
+ default:
+ MP_WARN(ao, "Unhandled format %d\n", format);
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+ }
+}
+
+static enum spa_audio_channel mp_speaker_id_to_spa(struct ao *ao, enum mp_speaker_id mp_speaker_id)
+{
+ switch (mp_speaker_id) {
+ case MP_SPEAKER_ID_FL: return SPA_AUDIO_CHANNEL_FL;
+ case MP_SPEAKER_ID_FR: return SPA_AUDIO_CHANNEL_FR;
+ case MP_SPEAKER_ID_FC: return SPA_AUDIO_CHANNEL_FC;
+ case MP_SPEAKER_ID_LFE: return SPA_AUDIO_CHANNEL_LFE;
+ case MP_SPEAKER_ID_BL: return SPA_AUDIO_CHANNEL_RL;
+ case MP_SPEAKER_ID_BR: return SPA_AUDIO_CHANNEL_RR;
+ case MP_SPEAKER_ID_FLC: return SPA_AUDIO_CHANNEL_FLC;
+ case MP_SPEAKER_ID_FRC: return SPA_AUDIO_CHANNEL_FRC;
+ case MP_SPEAKER_ID_BC: return SPA_AUDIO_CHANNEL_RC;
+ case MP_SPEAKER_ID_SL: return SPA_AUDIO_CHANNEL_SL;
+ case MP_SPEAKER_ID_SR: return SPA_AUDIO_CHANNEL_SR;
+ case MP_SPEAKER_ID_TC: return SPA_AUDIO_CHANNEL_TC;
+ case MP_SPEAKER_ID_TFL: return SPA_AUDIO_CHANNEL_TFL;
+ case MP_SPEAKER_ID_TFC: return SPA_AUDIO_CHANNEL_TFC;
+ case MP_SPEAKER_ID_TFR: return SPA_AUDIO_CHANNEL_TFR;
+ case MP_SPEAKER_ID_TBL: return SPA_AUDIO_CHANNEL_TRL;
+ case MP_SPEAKER_ID_TBC: return SPA_AUDIO_CHANNEL_TRC;
+ case MP_SPEAKER_ID_TBR: return SPA_AUDIO_CHANNEL_TRR;
+ case MP_SPEAKER_ID_DL: return SPA_AUDIO_CHANNEL_FL;
+ case MP_SPEAKER_ID_DR: return SPA_AUDIO_CHANNEL_FR;
+ case MP_SPEAKER_ID_WL: return SPA_AUDIO_CHANNEL_FL;
+ case MP_SPEAKER_ID_WR: return SPA_AUDIO_CHANNEL_FR;
+ case MP_SPEAKER_ID_SDL: return SPA_AUDIO_CHANNEL_SL;
+ case MP_SPEAKER_ID_SDR: return SPA_AUDIO_CHANNEL_SL;
+ case MP_SPEAKER_ID_LFE2: return SPA_AUDIO_CHANNEL_LFE2;
+ case MP_SPEAKER_ID_TSL: return SPA_AUDIO_CHANNEL_TSL;
+ case MP_SPEAKER_ID_TSR: return SPA_AUDIO_CHANNEL_TSR;
+ case MP_SPEAKER_ID_BFC: return SPA_AUDIO_CHANNEL_BC;
+ case MP_SPEAKER_ID_BFL: return SPA_AUDIO_CHANNEL_BLC;
+ case MP_SPEAKER_ID_BFR: return SPA_AUDIO_CHANNEL_BRC;
+ case MP_SPEAKER_ID_NA: return SPA_AUDIO_CHANNEL_NA;
+ default:
+ MP_WARN(ao, "Unhandled channel %d\n", mp_speaker_id);
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+ };
+}
+
+static void on_process(void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *p = ao->priv;
+ struct pw_time time;
+ struct pw_buffer *b;
+ void *data[MP_NUM_CHANNELS];
+
+ if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) {
+ MP_WARN(ao, "out of buffers: %s\n", strerror(errno));
+ return;
+ }
+
+ struct spa_buffer *buf = b->buffer;
+
+ int bytes_per_channel = buf->datas[0].maxsize / ao->channels.num;
+ int nframes = bytes_per_channel / ao->sstride;
+#if PW_CHECK_VERSION(0, 3, 49)
+ if (b->requested != 0)
+ nframes = MPMIN(b->requested, nframes);
+#endif
+
+ for (int i = 0; i < buf->n_datas; i++)
+ data[i] = buf->datas[i].data;
+
+ pw_stream_get_time_n(p->stream, &time, sizeof(time));
+ if (time.rate.denom == 0)
+ time.rate.denom = ao->samplerate;
+ if (time.rate.num == 0)
+ time.rate.num = 1;
+
+ int64_t end_time = mp_time_ns();
+ /* time.queued is always going to be 0, so we don't need to care */
+ end_time += (nframes * 1e9 / ao->samplerate) +
+ ((double) time.delay * SPA_NSEC_PER_SEC * time.rate.num / time.rate.denom);
+
+ int samples = ao_read_data_nonblocking(ao, data, nframes, end_time);
+ b->size = samples;
+
+ for (int i = 0; i < buf->n_datas; i++) {
+ buf->datas[i].chunk->size = samples * ao->sstride;
+ buf->datas[i].chunk->offset = 0;
+ buf->datas[i].chunk->stride = ao->sstride;
+ }
+
+ pw_stream_queue_buffer(p->stream, b);
+
+ MP_TRACE(ao, "queued %d of %d samples\n", samples, nframes);
+}
+
+static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
+{
+ struct ao *ao = userdata;
+ struct priv *p = ao->priv;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ /* We want to know when our node is linked.
+ * As there is no proper callback for this we use the Latency param for this
+ */
+ if (id == SPA_PARAM_Latency) {
+ p->init_state = INIT_STATE_SUCCESS;
+ pw_thread_loop_signal(p->loop, false);
+ }
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ int buffer_size = ao->device_buffer * af_fmt_to_bytes(ao->format) * ao->channels.num;
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(ao->num_planes),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ buffer_size, 0, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(ao->sstride));
+ if (!params[0]) {
+ MP_ERR(ao, "Could not build parameter pod\n");
+ return;
+ }
+
+ if (pw_stream_update_params(p->stream, params, 1) < 0) {
+ MP_ERR(ao, "Could not update stream parameters\n");
+ return;
+ }
+}
+
+static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error)
+{
+ struct ao *ao = userdata;
+ struct priv *p = ao->priv;
+ MP_DBG(ao, "Stream state changed: old_state=%s state=%s error=%s\n",
+ pw_stream_state_as_string(old), pw_stream_state_as_string(state), error);
+
+ if (state == PW_STREAM_STATE_ERROR) {
+ MP_WARN(ao, "Stream in error state, trying to reload...\n");
+ p->init_state = INIT_STATE_ERROR;
+ pw_thread_loop_signal(p->loop, false);
+ ao_request_reload(ao);
+ }
+
+ if (state == PW_STREAM_STATE_UNCONNECTED && old != PW_STREAM_STATE_UNCONNECTED) {
+ MP_WARN(ao, "Stream disconnected, trying to reload...\n");
+ ao_request_reload(ao);
+ }
+}
+
+static float spa_volume_to_mp_volume(float vol)
+{
+ return vol * 100;
+}
+
+static float mp_volume_to_spa_volume(float vol)
+{
+ return vol / 100;
+}
+
+static float volume_avg(float* vols, uint32_t n)
+{
+ float sum = 0.0;
+ for (int i = 0; i < n; i++)
+ sum += vols[i];
+ return sum / n;
+}
+
+static void on_control_info(void *userdata, uint32_t id,
+ const struct pw_stream_control *control)
+{
+ struct ao *ao = userdata;
+ struct priv *p = ao->priv;
+
+ switch (id) {
+ case SPA_PROP_mute:
+ if (control->n_values == 1)
+ p->muted = control->values[0] >= 0.5;
+ break;
+ case SPA_PROP_channelVolumes:
+ if (p->options.volume_mode != VOLUME_MODE_CHANNEL)
+ break;
+ if (control->n_values > 0)
+ p->volume = volume_avg(control->values, control->n_values);
+ break;
+ case SPA_PROP_volume:
+ if (p->options.volume_mode != VOLUME_MODE_GLOBAL)
+ break;
+ if (control->n_values > 0)
+ p->volume = control->values[0];
+ break;
+ }
+}
+
+static const struct pw_stream_events stream_events = {
+ .version = PW_VERSION_STREAM_EVENTS,
+ .param_changed = on_param_changed,
+ .process = on_process,
+ .state_changed = on_state_changed,
+ .control_info = on_control_info,
+};
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ if (p->loop)
+ pw_thread_loop_stop(p->loop);
+ spa_hook_remove(&p->stream_listener);
+ spa_zero(p->stream_listener);
+ if (p->stream)
+ pw_stream_destroy(p->stream);
+ p->stream = NULL;
+ if (p->core)
+ pw_context_destroy(pw_core_get_context(p->core));
+ p->core = NULL;
+ if (p->loop)
+ pw_thread_loop_destroy(p->loop);
+ p->loop = NULL;
+ pw_deinit();
+}
+
+struct registry_event_global_ctx {
+ struct ao *ao;
+ void (*sink_cb) (struct ao *ao, uint32_t id, const struct spa_dict *props, void *sink_cb_ctx);
+ void *sink_cb_ctx;
+};
+
+static bool is_sink_node(const char *type, const struct spa_dict *props)
+{
+ if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0)
+ return false;
+
+ if (!props)
+ return false;
+
+ const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+ if (!class || strcmp(class, "Audio/Sink") != 0)
+ return false;
+
+ return true;
+}
+
+static void for_each_sink_registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const
+ char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct registry_event_global_ctx *ctx = data;
+
+ if (!is_sink_node(type, props))
+ return;
+
+ ctx->sink_cb(ctx->ao, id, props, ctx->sink_cb_ctx);
+}
+
+
+struct for_each_done_ctx {
+ struct pw_thread_loop *loop;
+ bool done;
+};
+
+static const struct pw_registry_events for_each_sink_registry_events = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = for_each_sink_registry_event_global,
+};
+
+static void for_each_sink_done(void *data, uint32_t it, int seq)
+{
+ struct for_each_done_ctx *ctx = data;
+ ctx->done = true;
+ pw_thread_loop_signal(ctx->loop, false);
+}
+
+static const struct pw_core_events for_each_sink_core_events = {
+ .version = PW_VERSION_CORE_EVENTS,
+ .done = for_each_sink_done,
+};
+
+static int for_each_sink(struct ao *ao, void (cb) (struct ao *ao, uint32_t id,
+ const struct spa_dict *props, void *ctx), void *cb_ctx)
+{
+ struct priv *priv = ao->priv;
+ struct pw_registry *registry;
+ struct spa_hook core_listener;
+ struct for_each_done_ctx done_ctx = {
+ .loop = priv->loop,
+ .done = false,
+ };
+ int ret = -1;
+
+ pw_thread_loop_lock(priv->loop);
+
+ spa_zero(core_listener);
+ if (pw_core_add_listener(priv->core, &core_listener, &for_each_sink_core_events, &done_ctx) < 0)
+ goto unlock_loop;
+
+ registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0);
+ if (!registry)
+ goto remove_core_listener;
+
+ pw_core_sync(priv->core, 0, 0);
+
+ struct spa_hook registry_listener;
+ struct registry_event_global_ctx revents_ctx = {
+ .ao = ao,
+ .sink_cb = cb,
+ .sink_cb_ctx = cb_ctx,
+ };
+ spa_zero(registry_listener);
+ if (pw_registry_add_listener(registry, &registry_listener, &for_each_sink_registry_events, &revents_ctx) < 0)
+ goto destroy_registry;
+
+ while (!done_ctx.done)
+ pw_thread_loop_wait(priv->loop);
+
+ spa_hook_remove(&registry_listener);
+
+ ret = 0;
+
+destroy_registry:
+ pw_proxy_destroy((struct pw_proxy *)registry);
+
+remove_core_listener:
+ spa_hook_remove(&core_listener);
+
+unlock_loop:
+ pw_thread_loop_unlock(priv->loop);
+
+ return ret;
+}
+
+static void have_sink(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx)
+{
+ bool *b = ctx;
+ *b = true;
+}
+
+static bool session_has_sinks(struct ao *ao)
+{
+ bool b = false;
+
+ if (for_each_sink(ao, have_sink, &b) < 0)
+ MP_WARN(ao, "Could not list devices, sink detection may be wrong\n");
+
+ return b;
+}
+
+static void on_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct ao *ao = data;
+
+ MP_WARN(ao, "Error during playback: %s, %s\n", spa_strerror(res), message);
+}
+
+static void on_core_info(void *data, const struct pw_core_info *info)
+{
+ struct ao *ao = data;
+
+ MP_VERBOSE(ao, "Core user: %s\n", info->user_name);
+ MP_VERBOSE(ao, "Core host: %s\n", info->host_name);
+ MP_VERBOSE(ao, "Core version: %s\n", info->version);
+ MP_VERBOSE(ao, "Core name: %s\n", info->name);
+}
+
+static const struct pw_core_events core_events = {
+ .version = PW_VERSION_CORE_EVENTS,
+ .error = on_error,
+ .info = on_core_info,
+};
+
+static int pipewire_init_boilerplate(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ struct pw_context *context;
+
+ pw_init(NULL, NULL);
+
+ MP_VERBOSE(ao, "Headers version: %s\n", pw_get_headers_version());
+ MP_VERBOSE(ao, "Library version: %s\n", pw_get_library_version());
+
+ p->loop = pw_thread_loop_new("mpv/ao/pipewire", NULL);
+ if (p->loop == NULL)
+ return -1;
+
+ pw_thread_loop_lock(p->loop);
+
+ if (pw_thread_loop_start(p->loop) < 0)
+ goto error;
+
+ context = pw_context_new(
+ pw_thread_loop_get_loop(p->loop),
+ pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL),
+ 0);
+ if (!context)
+ goto error;
+
+ p->core = pw_context_connect(
+ context,
+ pw_properties_new(PW_KEY_REMOTE_NAME, p->options.remote, NULL),
+ 0);
+ if (!p->core) {
+ MP_MSG(ao, ao->probing ? MSGL_V : MSGL_ERR,
+ "Could not connect to context '%s': %s\n",
+ p->options.remote, strerror(errno));
+ pw_context_destroy(context);
+ goto error;
+ }
+
+ if (pw_core_add_listener(p->core, &p->core_listener, &core_events, ao) < 0)
+ goto error;
+
+ pw_thread_loop_unlock(p->loop);
+
+ if (!session_has_sinks(ao)) {
+ MP_VERBOSE(ao, "PipeWire does not have any audio sinks, skipping\n");
+ return -1;
+ }
+
+ return 0;
+
+error:
+ pw_thread_loop_unlock(p->loop);
+ return -1;
+}
+
+static void wait_for_init_done(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ struct timespec abstime;
+ int r;
+
+ r = pw_thread_loop_get_time(p->loop, &abstime, 50 * SPA_NSEC_PER_MSEC);
+ if (r < 0) {
+ MP_WARN(ao, "Could not get timeout for initialization: %s\n", spa_strerror(r));
+ return;
+ }
+
+ while (p->init_state == INIT_STATE_NONE) {
+ r = pw_thread_loop_timed_wait_full(p->loop, &abstime);
+ if (r < 0) {
+ MP_WARN(ao, "Could not wait for initialization: %s\n", spa_strerror(r));
+ return;
+ }
+ }
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[1];
+ struct pw_properties *props = pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC ? "Music" : "Movie",
+ PW_KEY_NODE_NAME, ao->client_name,
+ PW_KEY_NODE_DESCRIPTION, ao->client_name,
+ PW_KEY_APP_NAME, ao->client_name,
+ PW_KEY_APP_ID, ao->client_name,
+ PW_KEY_APP_ICON_NAME, ao->client_name,
+ PW_KEY_NODE_ALWAYS_PROCESS, "true",
+ PW_KEY_TARGET_OBJECT, ao->device,
+ NULL
+ );
+
+ if (pipewire_init_boilerplate(ao) < 0)
+ goto error_props;
+
+ if (p->options.buffer_msec) {
+ ao->device_buffer = p->options.buffer_msec * ao->samplerate / 1000;
+
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d", ao->device_buffer, ao->samplerate);
+ }
+
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", ao->samplerate);
+
+ enum spa_audio_format spa_format = af_fmt_to_pw(ao, ao->format);
+ if (spa_format == SPA_AUDIO_FORMAT_UNKNOWN) {
+ ao->format = AF_FORMAT_FLOATP;
+ spa_format = SPA_AUDIO_FORMAT_F32P;
+ }
+
+ struct spa_audio_info_raw audio_info = {
+ .format = spa_format,
+ .rate = ao->samplerate,
+ .channels = ao->channels.num,
+ };
+
+ for (int i = 0; i < ao->channels.num; i++)
+ audio_info.position[i] = mp_speaker_id_to_spa(ao, ao->channels.speaker[i]);
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audio_info);
+ if (!params[0])
+ goto error_props;
+
+ if (af_fmt_is_planar(ao->format)) {
+ ao->num_planes = ao->channels.num;
+ ao->sstride = af_fmt_to_bytes(ao->format);
+ } else {
+ ao->num_planes = 1;
+ ao->sstride = ao->channels.num * af_fmt_to_bytes(ao->format);
+ }
+
+ pw_thread_loop_lock(p->loop);
+
+ p->stream = pw_stream_new(p->core, "audio-src", props);
+ if (p->stream == NULL) {
+ pw_thread_loop_unlock(p->loop);
+ goto error;
+ }
+
+ pw_stream_add_listener(p->stream, &p->stream_listener, &stream_events, ao);
+
+ enum pw_stream_flags flags = PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_INACTIVE |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS;
+
+ if (ao->init_flags & AO_INIT_EXCLUSIVE)
+ flags |= PW_STREAM_FLAG_EXCLUSIVE;
+
+ if (pw_stream_connect(p->stream,
+ PW_DIRECTION_OUTPUT, PW_ID_ANY, flags, params, 1) < 0) {
+ pw_thread_loop_unlock(p->loop);
+ goto error;
+ }
+
+ wait_for_init_done(ao);
+
+ pw_thread_loop_unlock(p->loop);
+
+ if (p->init_state == INIT_STATE_ERROR)
+ goto error;
+
+ return 0;
+
+error_props:
+ pw_properties_free(props);
+error:
+ uninit(ao);
+ return -1;
+}
+
+static void reset(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ pw_thread_loop_lock(p->loop);
+ pw_stream_set_active(p->stream, false);
+ pw_stream_flush(p->stream, false);
+ pw_thread_loop_unlock(p->loop);
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ pw_thread_loop_lock(p->loop);
+ pw_stream_set_active(p->stream, true);
+ pw_thread_loop_unlock(p->loop);
+}
+
+#define CONTROL_RET(r) (!r ? CONTROL_OK : CONTROL_ERROR)
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct priv *p = ao->priv;
+
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME: {
+ float *vol = arg;
+ *vol = spa_volume_to_mp_volume(p->volume);
+ return CONTROL_OK;
+ }
+ case AOCONTROL_GET_MUTE: {
+ bool *muted = arg;
+ *muted = p->muted;
+ return CONTROL_OK;
+ }
+ case AOCONTROL_SET_VOLUME:
+ case AOCONTROL_SET_MUTE:
+ case AOCONTROL_UPDATE_STREAM_TITLE: {
+ int ret;
+
+ pw_thread_loop_lock(p->loop);
+ switch (cmd) {
+ case AOCONTROL_SET_VOLUME: {
+ float *vol = arg;
+ uint8_t n = ao->channels.num;
+ if (p->options.volume_mode == VOLUME_MODE_CHANNEL) {
+ float values[MP_NUM_CHANNELS] = {0};
+ for (int i = 0; i < n; i++)
+ values[i] = mp_volume_to_spa_volume(*vol);
+ ret = CONTROL_RET(pw_stream_set_control(
+ p->stream, SPA_PROP_channelVolumes, n, values, 0));
+ } else {
+ float value = mp_volume_to_spa_volume(*vol);
+ ret = CONTROL_RET(pw_stream_set_control(
+ p->stream, SPA_PROP_volume, 1, &value, 0));
+ }
+ break;
+ }
+ case AOCONTROL_SET_MUTE: {
+ bool *muted = arg;
+ float value = *muted ? 1.f : 0.f;
+ ret = CONTROL_RET(pw_stream_set_control(p->stream, SPA_PROP_mute, 1, &value, 0));
+ break;
+ }
+ case AOCONTROL_UPDATE_STREAM_TITLE: {
+ char *title = arg;
+ struct spa_dict_item items[1];
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, title);
+ ret = CONTROL_RET(pw_stream_update_properties(p->stream, &SPA_DICT_INIT(items, MP_ARRAY_SIZE(items))));
+ break;
+ }
+ default:
+ ret = CONTROL_NA;
+ }
+ pw_thread_loop_unlock(p->loop);
+ return ret;
+ }
+ default:
+ return CONTROL_UNKNOWN;
+ }
+}
+
+static void add_device_to_list(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx)
+{
+ struct ao_device_list *list = ctx;
+ const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+
+ if (!name)
+ return;
+
+ const char *description = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
+
+ ao_device_list_add(list, ao, &(struct ao_device_desc){name, description});
+}
+
+static void hotplug_registry_global_cb(void *data, uint32_t id,
+ uint32_t permissions, const char *type,
+ uint32_t version, const struct spa_dict *props)
+{
+ struct ao *ao = data;
+ struct priv *priv = ao->priv;
+
+ if (!is_sink_node(type, props))
+ return;
+
+ pw_thread_loop_lock(priv->loop);
+ struct id_list *item = talloc(ao, struct id_list);
+ item->id = id;
+ spa_list_init(&item->node);
+ spa_list_append(&priv->hotplug.sinks, &item->node);
+ pw_thread_loop_unlock(priv->loop);
+
+ ao_hotplug_event(ao);
+}
+
+static void hotplug_registry_global_remove_cb(void *data, uint32_t id)
+{
+ struct ao *ao = data;
+ struct priv *priv = ao->priv;
+ bool removed_sink = false;
+
+ struct id_list *e;
+
+ pw_thread_loop_lock(priv->loop);
+ spa_list_for_each(e, &priv->hotplug.sinks, node) {
+ if (e->id == id) {
+ removed_sink = true;
+ spa_list_remove(&e->node);
+ talloc_free(e);
+ break;
+ }
+ }
+
+ pw_thread_loop_unlock(priv->loop);
+
+ if (removed_sink)
+ ao_hotplug_event(ao);
+}
+
+static const struct pw_registry_events hotplug_registry_events = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = hotplug_registry_global_cb,
+ .global_remove = hotplug_registry_global_remove_cb,
+};
+
+static int hotplug_init(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ int res = pipewire_init_boilerplate(ao);
+ if (res)
+ goto error_no_unlock;
+
+ pw_thread_loop_lock(priv->loop);
+
+ spa_zero(priv->hotplug);
+ spa_list_init(&priv->hotplug.sinks);
+
+ priv->hotplug.registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0);
+ if (!priv->hotplug.registry)
+ goto error;
+
+ if (pw_registry_add_listener(priv->hotplug.registry, &priv->hotplug.registry_listener, &hotplug_registry_events, ao) < 0) {
+ pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry);
+ goto error;
+ }
+
+ pw_thread_loop_unlock(priv->loop);
+
+ return res;
+
+error:
+ pw_thread_loop_unlock(priv->loop);
+error_no_unlock:
+ uninit(ao);
+ return -1;
+}
+
+static void hotplug_uninit(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ pw_thread_loop_lock(priv->loop);
+
+ spa_hook_remove(&priv->hotplug.registry_listener);
+ pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry);
+
+ pw_thread_loop_unlock(priv->loop);
+ uninit(ao);
+}
+
+static void list_devs(struct ao *ao, struct ao_device_list *list)
+{
+ ao_device_list_add(list, ao, &(struct ao_device_desc){});
+
+ if (for_each_sink(ao, add_device_to_list, list) < 0)
+ MP_WARN(ao, "Could not list devices, list may be incomplete\n");
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_pipewire = {
+ .description = "PipeWire audio output",
+ .name = "pipewire",
+
+ .init = init,
+ .uninit = uninit,
+ .reset = reset,
+ .start = start,
+
+ .control = control,
+
+ .hotplug_init = hotplug_init,
+ .hotplug_uninit = hotplug_uninit,
+ .list_devs = list_devs,
+
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv)
+ {
+ .loop = NULL,
+ .stream = NULL,
+ .init_state = INIT_STATE_NONE,
+ .options.buffer_msec = 0,
+ .options.volume_mode = VOLUME_MODE_CHANNEL,
+ },
+ .options_prefix = "pipewire",
+ .options = (const struct m_option[]) {
+ {"buffer", OPT_CHOICE(options.buffer_msec, {"native", 0}),
+ M_RANGE(1, 2000)},
+ {"remote", OPT_STRING(options.remote) },
+ {"volume-mode", OPT_CHOICE(options.volume_mode,
+ {"channel", VOLUME_MODE_CHANNEL}, {"global", VOLUME_MODE_GLOBAL})},
+ {0}
+ },
+};
diff --git a/audio/out/ao_pulse.c b/audio/out/ao_pulse.c
new file mode 100644
index 0000000..3b29b1a
--- /dev/null
+++ b/audio/out/ao_pulse.c
@@ -0,0 +1,817 @@
+/*
+ * PulseAudio audio output driver.
+ * Copyright (C) 2006 Lennart Poettering
+ * Copyright (C) 2007 Reimar Doeffinger
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdint.h>
+#include <math.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "audio/format.h"
+#include "common/msg.h"
+#include "options/m_option.h"
+#include "ao.h"
+#include "internal.h"
+
+#define VOL_PA2MP(v) ((v) * 100.0 / PA_VOLUME_NORM)
+#define VOL_MP2PA(v) lrint((v) * PA_VOLUME_NORM / 100)
+
+struct priv {
+ // PulseAudio playback stream object
+ struct pa_stream *stream;
+
+ // PulseAudio connection context
+ struct pa_context *context;
+
+ // Main event loop object
+ struct pa_threaded_mainloop *mainloop;
+
+ // temporary during control()
+ struct pa_sink_input_info pi;
+
+ int retval;
+ bool playing;
+ bool underrun_signalled;
+
+ char *cfg_host;
+ int cfg_buffer;
+ bool cfg_latency_hacks;
+ bool cfg_allow_suspended;
+};
+
+#define GENERIC_ERR_MSG(str) \
+ MP_ERR(ao, str": %s\n", \
+ pa_strerror(pa_context_errno(((struct priv *)ao->priv)->context)))
+
+static void context_state_cb(pa_context *c, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_READY:
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+ break;
+ }
+}
+
+static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t,
+ uint32_t idx, void *userdata)
+{
+ struct ao *ao = userdata;
+ int type = t & PA_SUBSCRIPTION_MASK_SINK;
+ int fac = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
+ if ((type == PA_SUBSCRIPTION_EVENT_NEW || type == PA_SUBSCRIPTION_EVENT_REMOVE)
+ && fac == PA_SUBSCRIPTION_EVENT_SINK)
+ {
+ ao_hotplug_event(ao);
+ }
+}
+
+static void context_success_cb(pa_context *c, int success, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ priv->retval = success;
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+}
+
+static void stream_state_cb(pa_stream *s, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ switch (pa_stream_get_state(s)) {
+ case PA_STREAM_FAILED:
+ MP_VERBOSE(ao, "Stream failed.\n");
+ ao_request_reload(ao);
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+ break;
+ case PA_STREAM_READY:
+ case PA_STREAM_TERMINATED:
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+ break;
+ }
+}
+
+static void stream_request_cb(pa_stream *s, size_t length, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ ao_wakeup_playthread(ao);
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+}
+
+static void stream_latency_update_cb(pa_stream *s, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+}
+
+static void underflow_cb(pa_stream *s, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ priv->playing = false;
+ priv->underrun_signalled = true;
+ ao_wakeup_playthread(ao);
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+}
+
+static void success_cb(pa_stream *s, int success, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ priv->retval = success;
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+}
+
+// Like waitop(), but keep the lock (even if it may unlock temporarily).
+static bool waitop_no_unlock(struct priv *priv, pa_operation *op)
+{
+ if (!op)
+ return false;
+ pa_operation_state_t state = pa_operation_get_state(op);
+ while (state == PA_OPERATION_RUNNING) {
+ pa_threaded_mainloop_wait(priv->mainloop);
+ state = pa_operation_get_state(op);
+ }
+ pa_operation_unref(op);
+ return state == PA_OPERATION_DONE;
+}
+
+/**
+ * \brief waits for a pulseaudio operation to finish, frees it and
+ * unlocks the mainloop
+ * \param op operation to wait for
+ * \return 1 if operation has finished normally (DONE state), 0 otherwise
+ */
+static bool waitop(struct priv *priv, pa_operation *op)
+{
+ bool r = waitop_no_unlock(priv, op);
+ pa_threaded_mainloop_unlock(priv->mainloop);
+ return r;
+}
+
+static const struct format_map {
+ int mp_format;
+ pa_sample_format_t pa_format;
+} format_maps[] = {
+ {AF_FORMAT_FLOAT, PA_SAMPLE_FLOAT32NE},
+ {AF_FORMAT_S32, PA_SAMPLE_S32NE},
+ {AF_FORMAT_S16, PA_SAMPLE_S16NE},
+ {AF_FORMAT_U8, PA_SAMPLE_U8},
+ {AF_FORMAT_UNKNOWN, 0}
+};
+
+static pa_encoding_t map_digital_format(int format)
+{
+ switch (format) {
+ case AF_FORMAT_S_AC3: return PA_ENCODING_AC3_IEC61937;
+ case AF_FORMAT_S_EAC3: return PA_ENCODING_EAC3_IEC61937;
+ case AF_FORMAT_S_MP3: return PA_ENCODING_MPEG_IEC61937;
+ case AF_FORMAT_S_DTS: return PA_ENCODING_DTS_IEC61937;
+#ifdef PA_ENCODING_DTSHD_IEC61937
+ case AF_FORMAT_S_DTSHD: return PA_ENCODING_DTSHD_IEC61937;
+#endif
+#ifdef PA_ENCODING_MPEG2_AAC_IEC61937
+ case AF_FORMAT_S_AAC: return PA_ENCODING_MPEG2_AAC_IEC61937;
+#endif
+#ifdef PA_ENCODING_TRUEHD_IEC61937
+ case AF_FORMAT_S_TRUEHD: return PA_ENCODING_TRUEHD_IEC61937;
+#endif
+ default:
+ if (af_fmt_is_spdif(format))
+ return PA_ENCODING_ANY;
+ return PA_ENCODING_PCM;
+ }
+}
+
+static const int speaker_map[][2] = {
+ {PA_CHANNEL_POSITION_FRONT_LEFT, MP_SPEAKER_ID_FL},
+ {PA_CHANNEL_POSITION_FRONT_RIGHT, MP_SPEAKER_ID_FR},
+ {PA_CHANNEL_POSITION_FRONT_CENTER, MP_SPEAKER_ID_FC},
+ {PA_CHANNEL_POSITION_REAR_CENTER, MP_SPEAKER_ID_BC},
+ {PA_CHANNEL_POSITION_REAR_LEFT, MP_SPEAKER_ID_BL},
+ {PA_CHANNEL_POSITION_REAR_RIGHT, MP_SPEAKER_ID_BR},
+ {PA_CHANNEL_POSITION_LFE, MP_SPEAKER_ID_LFE},
+ {PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER, MP_SPEAKER_ID_FLC},
+ {PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER, MP_SPEAKER_ID_FRC},
+ {PA_CHANNEL_POSITION_SIDE_LEFT, MP_SPEAKER_ID_SL},
+ {PA_CHANNEL_POSITION_SIDE_RIGHT, MP_SPEAKER_ID_SR},
+ {PA_CHANNEL_POSITION_TOP_CENTER, MP_SPEAKER_ID_TC},
+ {PA_CHANNEL_POSITION_TOP_FRONT_LEFT, MP_SPEAKER_ID_TFL},
+ {PA_CHANNEL_POSITION_TOP_FRONT_RIGHT, MP_SPEAKER_ID_TFR},
+ {PA_CHANNEL_POSITION_TOP_FRONT_CENTER, MP_SPEAKER_ID_TFC},
+ {PA_CHANNEL_POSITION_TOP_REAR_LEFT, MP_SPEAKER_ID_TBL},
+ {PA_CHANNEL_POSITION_TOP_REAR_RIGHT, MP_SPEAKER_ID_TBR},
+ {PA_CHANNEL_POSITION_TOP_REAR_CENTER, MP_SPEAKER_ID_TBC},
+ {PA_CHANNEL_POSITION_INVALID, -1}
+};
+
+static bool chmap_pa_from_mp(pa_channel_map *dst, struct mp_chmap *src)
+{
+ if (src->num > PA_CHANNELS_MAX)
+ return false;
+ dst->channels = src->num;
+ if (mp_chmap_equals(src, &(const struct mp_chmap)MP_CHMAP_INIT_MONO)) {
+ dst->map[0] = PA_CHANNEL_POSITION_MONO;
+ return true;
+ }
+ for (int n = 0; n < src->num; n++) {
+ int mp_speaker = src->speaker[n];
+ int pa_speaker = PA_CHANNEL_POSITION_INVALID;
+ for (int i = 0; speaker_map[i][1] != -1; i++) {
+ if (speaker_map[i][1] == mp_speaker) {
+ pa_speaker = speaker_map[i][0];
+ break;
+ }
+ }
+ if (pa_speaker == PA_CHANNEL_POSITION_INVALID)
+ return false;
+ dst->map[n] = pa_speaker;
+ }
+ return true;
+}
+
+static bool select_chmap(struct ao *ao, pa_channel_map *dst)
+{
+ struct mp_chmap_sel sel = {0};
+ for (int n = 0; speaker_map[n][1] != -1; n++)
+ mp_chmap_sel_add_speaker(&sel, speaker_map[n][1]);
+ return ao_chmap_sel_adjust(ao, &sel, &ao->channels) &&
+ chmap_pa_from_mp(dst, &ao->channels);
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+
+ if (priv->mainloop)
+ pa_threaded_mainloop_stop(priv->mainloop);
+
+ if (priv->stream) {
+ pa_stream_disconnect(priv->stream);
+ pa_stream_unref(priv->stream);
+ priv->stream = NULL;
+ }
+
+ if (priv->context) {
+ pa_context_disconnect(priv->context);
+ pa_context_unref(priv->context);
+ priv->context = NULL;
+ }
+
+ if (priv->mainloop) {
+ pa_threaded_mainloop_free(priv->mainloop);
+ priv->mainloop = NULL;
+ }
+}
+
+static int pa_init_boilerplate(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ char *host = priv->cfg_host && priv->cfg_host[0] ? priv->cfg_host : NULL;
+ bool locked = false;
+
+ if (!(priv->mainloop = pa_threaded_mainloop_new())) {
+ MP_ERR(ao, "Failed to allocate main loop\n");
+ goto fail;
+ }
+
+ if (pa_threaded_mainloop_start(priv->mainloop) < 0)
+ goto fail;
+
+ pa_threaded_mainloop_lock(priv->mainloop);
+ locked = true;
+
+ if (!(priv->context = pa_context_new(pa_threaded_mainloop_get_api(
+ priv->mainloop), ao->client_name)))
+ {
+ MP_ERR(ao, "Failed to allocate context\n");
+ goto fail;
+ }
+
+ MP_VERBOSE(ao, "Library version: %s\n", pa_get_library_version());
+ MP_VERBOSE(ao, "Proto: %lu\n",
+ (long)pa_context_get_protocol_version(priv->context));
+ MP_VERBOSE(ao, "Server proto: %lu\n",
+ (long)pa_context_get_server_protocol_version(priv->context));
+
+ pa_context_set_state_callback(priv->context, context_state_cb, ao);
+ pa_context_set_subscribe_callback(priv->context, subscribe_cb, ao);
+
+ if (pa_context_connect(priv->context, host, 0, NULL) < 0)
+ goto fail;
+
+ /* Wait until the context is ready */
+ while (1) {
+ int state = pa_context_get_state(priv->context);
+ if (state == PA_CONTEXT_READY)
+ break;
+ if (!PA_CONTEXT_IS_GOOD(state))
+ goto fail;
+ pa_threaded_mainloop_wait(priv->mainloop);
+ }
+
+ pa_threaded_mainloop_unlock(priv->mainloop);
+ return 0;
+
+fail:
+ if (locked)
+ pa_threaded_mainloop_unlock(priv->mainloop);
+
+ if (priv->context) {
+ pa_threaded_mainloop_lock(priv->mainloop);
+ if (!(pa_context_errno(priv->context) == PA_ERR_CONNECTIONREFUSED
+ && ao->probing))
+ GENERIC_ERR_MSG("Init failed");
+ pa_threaded_mainloop_unlock(priv->mainloop);
+ }
+ uninit(ao);
+ return -1;
+}
+
+static bool set_format(struct ao *ao, pa_format_info *format)
+{
+ ao->format = af_fmt_from_planar(ao->format);
+
+ format->encoding = map_digital_format(ao->format);
+ if (format->encoding == PA_ENCODING_PCM) {
+ const struct format_map *fmt_map = format_maps;
+
+ while (fmt_map->mp_format != ao->format) {
+ if (fmt_map->mp_format == AF_FORMAT_UNKNOWN) {
+ MP_VERBOSE(ao, "Unsupported format, using default\n");
+ fmt_map = format_maps;
+ break;
+ }
+ fmt_map++;
+ }
+ ao->format = fmt_map->mp_format;
+
+ pa_format_info_set_sample_format(format, fmt_map->pa_format);
+ }
+
+ struct pa_channel_map map;
+
+ if (!select_chmap(ao, &map))
+ return false;
+
+ pa_format_info_set_rate(format, ao->samplerate);
+ pa_format_info_set_channels(format, ao->channels.num);
+ pa_format_info_set_channel_map(format, &map);
+
+ return ao->samplerate < PA_RATE_MAX && pa_format_info_valid(format);
+}
+
+static int init(struct ao *ao)
+{
+ pa_proplist *proplist = NULL;
+ pa_format_info *format = NULL;
+ struct priv *priv = ao->priv;
+ char *sink = ao->device;
+
+ if (pa_init_boilerplate(ao) < 0)
+ return -1;
+
+ pa_threaded_mainloop_lock(priv->mainloop);
+
+ if (!(proplist = pa_proplist_new())) {
+ MP_ERR(ao, "Failed to allocate proplist\n");
+ goto unlock_and_fail;
+ }
+ (void)pa_proplist_sets(proplist, PA_PROP_MEDIA_ICON_NAME, ao->client_name);
+
+ if (!(format = pa_format_info_new()))
+ goto unlock_and_fail;
+
+ if (!set_format(ao, format)) {
+ ao->channels = (struct mp_chmap) MP_CHMAP_INIT_STEREO;
+ ao->samplerate = 48000;
+ ao->format = AF_FORMAT_FLOAT;
+ if (!set_format(ao, format)) {
+ MP_ERR(ao, "Invalid audio format\n");
+ goto unlock_and_fail;
+ }
+ }
+
+ if (!(priv->stream = pa_stream_new_extended(priv->context, "audio stream",
+ &format, 1, proplist)))
+ goto unlock_and_fail;
+
+ pa_format_info_free(format);
+ format = NULL;
+
+ pa_proplist_free(proplist);
+ proplist = NULL;
+
+ pa_stream_set_state_callback(priv->stream, stream_state_cb, ao);
+ pa_stream_set_write_callback(priv->stream, stream_request_cb, ao);
+ pa_stream_set_latency_update_callback(priv->stream,
+ stream_latency_update_cb, ao);
+ pa_stream_set_underflow_callback(priv->stream, underflow_cb, ao);
+ uint32_t buf_size = ao->samplerate * (priv->cfg_buffer / 1000.0) *
+ af_fmt_to_bytes(ao->format) * ao->channels.num;
+ pa_buffer_attr bufattr = {
+ .maxlength = -1,
+ .tlength = buf_size > 0 ? buf_size : -1,
+ .prebuf = 0,
+ .minreq = -1,
+ .fragsize = -1,
+ };
+
+ int flags = PA_STREAM_NOT_MONOTONIC | PA_STREAM_START_CORKED;
+ if (!priv->cfg_latency_hacks)
+ flags |= PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_AUTO_TIMING_UPDATE;
+
+ if (pa_stream_connect_playback(priv->stream, sink, &bufattr,
+ flags, NULL, NULL) < 0)
+ goto unlock_and_fail;
+
+ /* Wait until the stream is ready */
+ while (1) {
+ int state = pa_stream_get_state(priv->stream);
+ if (state == PA_STREAM_READY)
+ break;
+ if (!PA_STREAM_IS_GOOD(state))
+ goto unlock_and_fail;
+ pa_threaded_mainloop_wait(priv->mainloop);
+ }
+
+ if (pa_stream_is_suspended(priv->stream) && !priv->cfg_allow_suspended) {
+ MP_ERR(ao, "The stream is suspended. Bailing out.\n");
+ goto unlock_and_fail;
+ }
+
+ const pa_buffer_attr* final_bufattr = pa_stream_get_buffer_attr(priv->stream);
+ if(!final_bufattr) {
+ MP_ERR(ao, "PulseAudio didn't tell us what buffer sizes it set. Bailing out.\n");
+ goto unlock_and_fail;
+ }
+ ao->device_buffer = final_bufattr->tlength /
+ af_fmt_to_bytes(ao->format) / ao->channels.num;
+
+ pa_threaded_mainloop_unlock(priv->mainloop);
+ return 0;
+
+unlock_and_fail:
+ pa_threaded_mainloop_unlock(priv->mainloop);
+
+ if (format)
+ pa_format_info_free(format);
+
+ if (proplist)
+ pa_proplist_free(proplist);
+
+ uninit(ao);
+ return -1;
+}
+
+static void cork(struct ao *ao, bool pause)
+{
+ struct priv *priv = ao->priv;
+ pa_threaded_mainloop_lock(priv->mainloop);
+ priv->retval = 0;
+ if (waitop_no_unlock(priv, pa_stream_cork(priv->stream, pause, success_cb, ao))
+ && priv->retval)
+ {
+ if (!pause)
+ priv->playing = true;
+ } else {
+ GENERIC_ERR_MSG("pa_stream_cork() failed");
+ priv->playing = false;
+ }
+ pa_threaded_mainloop_unlock(priv->mainloop);
+}
+
+// Play the specified data to the pulseaudio server
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *priv = ao->priv;
+ bool res = true;
+ pa_threaded_mainloop_lock(priv->mainloop);
+ if (pa_stream_write(priv->stream, data[0], samples * ao->sstride, NULL, 0,
+ PA_SEEK_RELATIVE) < 0) {
+ GENERIC_ERR_MSG("pa_stream_write() failed");
+ res = false;
+ }
+ pa_threaded_mainloop_unlock(priv->mainloop);
+ return res;
+}
+
+static void start(struct ao *ao)
+{
+ cork(ao, false);
+}
+
+// Reset the audio stream, i.e. flush the playback buffer on the server side
+static void reset(struct ao *ao)
+{
+ // pa_stream_flush() works badly if not corked
+ cork(ao, true);
+ struct priv *priv = ao->priv;
+ pa_threaded_mainloop_lock(priv->mainloop);
+ priv->playing = false;
+ priv->retval = 0;
+ if (!waitop(priv, pa_stream_flush(priv->stream, success_cb, ao)) ||
+ !priv->retval)
+ GENERIC_ERR_MSG("pa_stream_flush() failed");
+}
+
+static bool set_pause(struct ao *ao, bool paused)
+{
+ cork(ao, paused);
+ return true;
+}
+
+static double get_delay_hackfixed(struct ao *ao)
+{
+ /* This code basically does what pa_stream_get_latency() _should_
+ * do, but doesn't due to multiple known bugs in PulseAudio (at
+ * PulseAudio version 2.1). In particular, the timing interpolation
+ * mode (PA_STREAM_INTERPOLATE_TIMING) can return completely bogus
+ * values, and the non-interpolating code has a bug causing too
+ * large results at end of stream (so a stream never seems to finish).
+ * This code can still return wrong values in some cases due to known
+ * PulseAudio bugs that can not be worked around on the client side.
+ *
+ * We always query the server for latest timing info. This may take
+ * too long to work well with remote audio servers, but at least
+ * this should be enough to fix the normal local playback case.
+ */
+ struct priv *priv = ao->priv;
+ if (!waitop_no_unlock(priv, pa_stream_update_timing_info(priv->stream,
+ NULL, NULL)))
+ {
+ GENERIC_ERR_MSG("pa_stream_update_timing_info() failed");
+ return 0;
+ }
+ const pa_timing_info *ti = pa_stream_get_timing_info(priv->stream);
+ if (!ti) {
+ GENERIC_ERR_MSG("pa_stream_get_timing_info() failed");
+ return 0;
+ }
+ const struct pa_sample_spec *ss = pa_stream_get_sample_spec(priv->stream);
+ if (!ss) {
+ GENERIC_ERR_MSG("pa_stream_get_sample_spec() failed");
+ return 0;
+ }
+ // data left in PulseAudio's main buffers (not written to sink yet)
+ int64_t latency = pa_bytes_to_usec(ti->write_index - ti->read_index, ss);
+ // since this info may be from a while ago, playback has progressed since
+ latency -= ti->transport_usec;
+ // data already moved from buffers to sink, but not played yet
+ int64_t sink_latency = ti->sink_usec;
+ if (!ti->playing)
+ /* At the end of a stream, part of the data "left" in the sink may
+ * be padding silence after the end; that should be subtracted to
+ * get the amount of real audio from our stream. This adjustment
+ * is missing from Pulseaudio's own get_latency calculations
+ * (as of PulseAudio 2.1). */
+ sink_latency -= pa_bytes_to_usec(ti->since_underrun, ss);
+ if (sink_latency > 0)
+ latency += sink_latency;
+ if (latency < 0)
+ latency = 0;
+ return latency / 1e6;
+}
+
+static double get_delay_pulse(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ pa_usec_t latency = (pa_usec_t) -1;
+ while (pa_stream_get_latency(priv->stream, &latency, NULL) < 0) {
+ if (pa_context_errno(priv->context) != PA_ERR_NODATA) {
+ GENERIC_ERR_MSG("pa_stream_get_latency() failed");
+ break;
+ }
+ /* Wait until latency data is available again */
+ pa_threaded_mainloop_wait(priv->mainloop);
+ }
+ return latency == (pa_usec_t) -1 ? 0 : latency / 1000000.0;
+}
+
+static void audio_get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct priv *priv = ao->priv;
+
+ pa_threaded_mainloop_lock(priv->mainloop);
+
+ size_t space = pa_stream_writable_size(priv->stream);
+ state->free_samples = space == (size_t)-1 ? 0 : space / ao->sstride;
+
+ state->queued_samples = ao->device_buffer - state->free_samples; // dunno
+
+ if (priv->cfg_latency_hacks) {
+ state->delay = get_delay_hackfixed(ao);
+ } else {
+ state->delay = get_delay_pulse(ao);
+ }
+
+ state->playing = priv->playing;
+
+ pa_threaded_mainloop_unlock(priv->mainloop);
+
+ // Otherwise, PA will keep hammering us for underruns (which it does instead
+ // of stopping the stream automatically).
+ if (!state->playing && priv->underrun_signalled) {
+ reset(ao);
+ priv->underrun_signalled = false;
+ }
+}
+
+/* A callback function that is called when the
+ * pa_context_get_sink_input_info() operation completes. Saves the
+ * volume field of the specified structure to the global variable volume.
+ */
+static void info_func(struct pa_context *c, const struct pa_sink_input_info *i,
+ int is_last, void *userdata)
+{
+ struct ao *ao = userdata;
+ struct priv *priv = ao->priv;
+ if (is_last < 0) {
+ GENERIC_ERR_MSG("Failed to get sink input info");
+ return;
+ }
+ if (!i)
+ return;
+ priv->pi = *i;
+ pa_threaded_mainloop_signal(priv->mainloop, 0);
+}
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct priv *priv = ao->priv;
+ switch (cmd) {
+ case AOCONTROL_GET_MUTE:
+ case AOCONTROL_GET_VOLUME: {
+ uint32_t devidx = pa_stream_get_index(priv->stream);
+ pa_threaded_mainloop_lock(priv->mainloop);
+ if (!waitop(priv, pa_context_get_sink_input_info(priv->context, devidx,
+ info_func, ao))) {
+ GENERIC_ERR_MSG("pa_context_get_sink_input_info() failed");
+ return CONTROL_ERROR;
+ }
+ // Warning: some information in pi might be unaccessible, because
+ // we naively copied the struct, without updating pointers etc.
+ // Pointers might point to invalid data, accessors might fail.
+ if (cmd == AOCONTROL_GET_VOLUME) {
+ float *vol = arg;
+ *vol = VOL_PA2MP(pa_cvolume_avg(&priv->pi.volume));
+ } else if (cmd == AOCONTROL_GET_MUTE) {
+ bool *mute = arg;
+ *mute = priv->pi.mute;
+ }
+ return CONTROL_OK;
+ }
+
+ case AOCONTROL_SET_MUTE:
+ case AOCONTROL_SET_VOLUME: {
+ pa_threaded_mainloop_lock(priv->mainloop);
+ priv->retval = 0;
+ uint32_t stream_index = pa_stream_get_index(priv->stream);
+ if (cmd == AOCONTROL_SET_VOLUME) {
+ const float *vol = arg;
+ struct pa_cvolume volume;
+
+ pa_cvolume_reset(&volume, ao->channels.num);
+ pa_cvolume_set(&volume, volume.channels, VOL_MP2PA(*vol));
+ if (!waitop(priv, pa_context_set_sink_input_volume(priv->context,
+ stream_index,
+ &volume,
+ context_success_cb, ao)) ||
+ !priv->retval) {
+ GENERIC_ERR_MSG("pa_context_set_sink_input_volume() failed");
+ return CONTROL_ERROR;
+ }
+ } else if (cmd == AOCONTROL_SET_MUTE) {
+ const bool *mute = arg;
+ if (!waitop(priv, pa_context_set_sink_input_mute(priv->context,
+ stream_index,
+ *mute,
+ context_success_cb, ao)) ||
+ !priv->retval) {
+ GENERIC_ERR_MSG("pa_context_set_sink_input_mute() failed");
+ return CONTROL_ERROR;
+ }
+ } else {
+ MP_ASSERT_UNREACHABLE();
+ }
+ return CONTROL_OK;
+ }
+
+ case AOCONTROL_UPDATE_STREAM_TITLE: {
+ char *title = (char *)arg;
+ pa_threaded_mainloop_lock(priv->mainloop);
+ if (!waitop(priv, pa_stream_set_name(priv->stream, title,
+ success_cb, ao)))
+ {
+ GENERIC_ERR_MSG("pa_stream_set_name() failed");
+ return CONTROL_ERROR;
+ }
+ return CONTROL_OK;
+ }
+
+ default:
+ return CONTROL_UNKNOWN;
+ }
+}
+
+struct sink_cb_ctx {
+ struct ao *ao;
+ struct ao_device_list *list;
+};
+
+static void sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, void *ud)
+{
+ struct sink_cb_ctx *ctx = ud;
+ struct priv *priv = ctx->ao->priv;
+
+ if (eol) {
+ pa_threaded_mainloop_signal(priv->mainloop, 0); // wakeup waitop()
+ return;
+ }
+
+ struct ao_device_desc entry = {.name = i->name, .desc = i->description};
+ ao_device_list_add(ctx->list, ctx->ao, &entry);
+}
+
+static int hotplug_init(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ if (pa_init_boilerplate(ao) < 0)
+ return -1;
+
+ pa_threaded_mainloop_lock(priv->mainloop);
+ waitop(priv, pa_context_subscribe(priv->context, PA_SUBSCRIPTION_MASK_SINK,
+ context_success_cb, ao));
+
+ return 0;
+}
+
+static void list_devs(struct ao *ao, struct ao_device_list *list)
+{
+ struct priv *priv = ao->priv;
+ struct sink_cb_ctx ctx = {ao, list};
+
+ pa_threaded_mainloop_lock(priv->mainloop);
+ waitop(priv, pa_context_get_sink_info_list(priv->context, sink_info_cb, &ctx));
+}
+
+static void hotplug_uninit(struct ao *ao)
+{
+ uninit(ao);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_pulse = {
+ .description = "PulseAudio audio output",
+ .name = "pulse",
+ .control = control,
+ .init = init,
+ .uninit = uninit,
+ .reset = reset,
+ .get_state = audio_get_state,
+ .write = audio_write,
+ .start = start,
+ .set_pause = set_pause,
+ .hotplug_init = hotplug_init,
+ .hotplug_uninit = hotplug_uninit,
+ .list_devs = list_devs,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .cfg_buffer = 100,
+ },
+ .options = (const struct m_option[]) {
+ {"host", OPT_STRING(cfg_host)},
+ {"buffer", OPT_CHOICE(cfg_buffer, {"native", 0}),
+ M_RANGE(1, 2000)},
+ {"latency-hacks", OPT_BOOL(cfg_latency_hacks)},
+ {"allow-suspended", OPT_BOOL(cfg_allow_suspended)},
+ {0}
+ },
+ .options_prefix = "pulse",
+};
diff --git a/audio/out/ao_sdl.c b/audio/out/ao_sdl.c
new file mode 100644
index 0000000..5a6a58b
--- /dev/null
+++ b/audio/out/ao_sdl.c
@@ -0,0 +1,216 @@
+/*
+ * audio output driver for SDL 1.2+
+ * Copyright (C) 2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "audio/format.h"
+#include "mpv_talloc.h"
+#include "ao.h"
+#include "internal.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_option.h"
+#include "osdep/timer.h"
+
+#include <SDL.h>
+
+struct priv
+{
+ bool paused;
+
+ float buflen;
+};
+
+static const int fmtmap[][2] = {
+ {AF_FORMAT_U8, AUDIO_U8},
+ {AF_FORMAT_S16, AUDIO_S16SYS},
+#ifdef AUDIO_S32SYS
+ {AF_FORMAT_S32, AUDIO_S32SYS},
+#endif
+#ifdef AUDIO_F32SYS
+ {AF_FORMAT_FLOAT, AUDIO_F32SYS},
+#endif
+ {0}
+};
+
+static void audio_callback(void *userdata, Uint8 *stream, int len)
+{
+ struct ao *ao = userdata;
+
+ void *data[1] = {stream};
+
+ if (len % ao->sstride)
+ MP_ERR(ao, "SDL audio callback not sample aligned");
+
+ // Time this buffer will take, plus assume 1 period (1 callback invocation)
+ // fixed latency.
+ double delay = 2 * len / (double)ao->bps;
+
+ ao_read_data(ao, data, len / ao->sstride, mp_time_ns() + MP_TIME_S_TO_NS(delay));
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ if (!priv)
+ return;
+
+ if (SDL_WasInit(SDL_INIT_AUDIO)) {
+ // make sure the callback exits
+ SDL_LockAudio();
+
+ // close audio device
+ SDL_QuitSubSystem(SDL_INIT_AUDIO);
+ }
+}
+
+static unsigned int ceil_power_of_two(unsigned int x)
+{
+ int y = 1;
+ while (y < x)
+ y *= 2;
+ return y;
+}
+
+static int init(struct ao *ao)
+{
+ if (SDL_WasInit(SDL_INIT_AUDIO)) {
+ MP_ERR(ao, "already initialized\n");
+ return -1;
+ }
+
+ struct priv *priv = ao->priv;
+
+ if (SDL_InitSubSystem(SDL_INIT_AUDIO)) {
+ if (!ao->probing)
+ MP_ERR(ao, "SDL_Init failed\n");
+ uninit(ao);
+ return -1;
+ }
+
+ struct mp_chmap_sel sel = {0};
+ mp_chmap_sel_add_waveext_def(&sel);
+ if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) {
+ uninit(ao);
+ return -1;
+ }
+
+ ao->format = af_fmt_from_planar(ao->format);
+
+ SDL_AudioSpec desired = {0};
+ desired.format = AUDIO_S16SYS;
+ for (int n = 0; fmtmap[n][0]; n++) {
+ if (ao->format == fmtmap[n][0]) {
+ desired.format = fmtmap[n][1];
+ break;
+ }
+ }
+ desired.freq = ao->samplerate;
+ desired.channels = ao->channels.num;
+ if (priv->buflen) {
+ desired.samples = MPMIN(32768, ceil_power_of_two(ao->samplerate *
+ priv->buflen));
+ }
+ desired.callback = audio_callback;
+ desired.userdata = ao;
+
+ MP_VERBOSE(ao, "requested format: %d Hz, %d channels, %x, "
+ "buffer size: %d samples\n",
+ (int) desired.freq, (int) desired.channels,
+ (int) desired.format, (int) desired.samples);
+
+ SDL_AudioSpec obtained = desired;
+ if (SDL_OpenAudio(&desired, &obtained)) {
+ if (!ao->probing)
+ MP_ERR(ao, "could not open audio: %s\n", SDL_GetError());
+ uninit(ao);
+ return -1;
+ }
+
+ MP_VERBOSE(ao, "obtained format: %d Hz, %d channels, %x, "
+ "buffer size: %d samples\n",
+ (int) obtained.freq, (int) obtained.channels,
+ (int) obtained.format, (int) obtained.samples);
+
+ // The sample count is usually the number of samples the callback requests,
+ // which we assume is the period size. Normally, ao.c will allocate a large
+ // enough buffer. But in case the period size should be pathologically
+ // large, this will help.
+ ao->device_buffer = 3 * obtained.samples;
+
+ ao->format = 0;
+ for (int n = 0; fmtmap[n][0]; n++) {
+ if (obtained.format == fmtmap[n][1]) {
+ ao->format = fmtmap[n][0];
+ break;
+ }
+ }
+ if (!ao->format) {
+ if (!ao->probing)
+ MP_ERR(ao, "could not find matching format\n");
+ uninit(ao);
+ return -1;
+ }
+
+ if (!ao_chmap_sel_get_def(ao, &sel, &ao->channels, obtained.channels)) {
+ uninit(ao);
+ return -1;
+ }
+
+ ao->samplerate = obtained.freq;
+
+ priv->paused = 1;
+
+ return 1;
+}
+
+static void reset(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ if (!priv->paused)
+ SDL_PauseAudio(SDL_TRUE);
+ priv->paused = 1;
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *priv = ao->priv;
+ if (priv->paused)
+ SDL_PauseAudio(SDL_FALSE);
+ priv->paused = 0;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct ao_driver audio_out_sdl = {
+ .description = "SDL Audio",
+ .name = "sdl",
+ .init = init,
+ .uninit = uninit,
+ .reset = reset,
+ .start = start,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .buflen = 0, // use SDL default
+ },
+ .options = (const struct m_option[]) {
+ {"buflen", OPT_FLOAT(buflen)},
+ {0}
+ },
+ .options_prefix = "sdl",
+};
diff --git a/audio/out/ao_sndio.c b/audio/out/ao_sndio.c
new file mode 100644
index 0000000..fce7139
--- /dev/null
+++ b/audio/out/ao_sndio.c
@@ -0,0 +1,321 @@
+/*
+ * Copyright (c) 2008 Alexandre Ratchov <alex@caoua.org>
+ * Copyright (c) 2013 Christian Neukirchen <chneukirchen@gmail.com>
+ * Copyright (c) 2020 Rozhuk Ivan <rozhuk.im@gmail.com>
+ * Copyright (c) 2021 Andrew Krasavin <noiseless-ak@yandex.ru>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <poll.h>
+#include <errno.h>
+#include <sndio.h>
+
+#include "options/m_option.h"
+#include "common/msg.h"
+
+#include "audio/format.h"
+#include "ao.h"
+#include "internal.h"
+
+struct priv {
+ struct sio_hdl *hdl;
+ struct sio_par par;
+ int delay;
+ bool playing;
+ int vol;
+ int havevol;
+ struct pollfd *pfd;
+};
+
+
+static const struct mp_chmap sndio_layouts[] = {
+ {0}, /* empty */
+ {1, {MP_SPEAKER_ID_FL}}, /* mono */
+ MP_CHMAP2(FL, FR), /* stereo */
+ {0}, /* 2.1 */
+ MP_CHMAP4(FL, FR, BL, BR), /* 4.0 */
+ {0}, /* 5.0 */
+ MP_CHMAP6(FL, FR, BL, BR, FC, LFE), /* 5.1 */
+ {0}, /* 6.1 */
+ MP_CHMAP8(FL, FR, BL, BR, FC, LFE, SL, SR), /* 7.1 */
+ /* Above is the fixed channel assignment for sndio, since we need to
+ * fill all channels and cannot insert silence, not all layouts are
+ * supported.
+ * NOTE: MP_SPEAKER_ID_NA could be used to add padding channels. */
+};
+
+static void uninit(struct ao *ao);
+
+
+/* Make libsndio call movecb(). */
+static void process_events(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ int n = sio_pollfd(p->hdl, p->pfd, POLLOUT);
+ while (poll(p->pfd, n, 0) < 0 && errno == EINTR);
+
+ sio_revents(p->hdl, p->pfd);
+}
+
+/* Call-back invoked to notify of the hardware position. */
+static void movecb(void *addr, int delta)
+{
+ struct ao *ao = addr;
+ struct priv *p = ao->priv;
+
+ p->delay -= delta;
+}
+
+/* Call-back invoked to notify about volume changes. */
+static void volcb(void *addr, unsigned newvol)
+{
+ struct ao *ao = addr;
+ struct priv *p = ao->priv;
+
+ p->vol = newvol;
+}
+
+static int init(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+ struct mp_chmap_sel sel = {0};
+ size_t i;
+ struct af_to_par {
+ int format, bits, sig;
+ };
+ static const struct af_to_par af_to_par[] = {
+ {AF_FORMAT_U8, 8, 0},
+ {AF_FORMAT_S16, 16, 1},
+ {AF_FORMAT_S32, 32, 1},
+ };
+ const struct af_to_par *ap;
+ const char *device = ((ao->device) ? ao->device : SIO_DEVANY);
+
+ /* Opening device. */
+ MP_VERBOSE(ao, "Using '%s' audio device.\n", device);
+ p->hdl = sio_open(device, SIO_PLAY, 0);
+ if (p->hdl == NULL) {
+ MP_ERR(ao, "Can't open audio device %s.\n", device);
+ goto err_out;
+ }
+
+ sio_initpar(&p->par);
+
+ /* Selecting sound format. */
+ ao->format = af_fmt_from_planar(ao->format);
+
+ p->par.bits = 16;
+ p->par.sig = 1;
+ p->par.le = SIO_LE_NATIVE;
+ for (i = 0; i < MP_ARRAY_SIZE(af_to_par); i++) {
+ ap = &af_to_par[i];
+ if (ap->format == ao->format) {
+ p->par.bits = ap->bits;
+ p->par.sig = ap->sig;
+ break;
+ }
+ }
+
+ p->par.rate = ao->samplerate;
+
+ /* Channels count. */
+ for (i = 0; i < MP_ARRAY_SIZE(sndio_layouts); i++) {
+ mp_chmap_sel_add_map(&sel, &sndio_layouts[i]);
+ }
+ if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
+ goto err_out;
+
+ p->par.pchan = ao->channels.num;
+ p->par.appbufsz = p->par.rate * 250 / 1000; /* 250ms buffer */
+ p->par.round = p->par.rate * 10 / 1000; /* 10ms block size */
+
+ if (!sio_setpar(p->hdl, &p->par)) {
+ MP_ERR(ao, "couldn't set params\n");
+ goto err_out;
+ }
+
+ /* Get current sound params. */
+ if (!sio_getpar(p->hdl, &p->par)) {
+ MP_ERR(ao, "couldn't get params\n");
+ goto err_out;
+ }
+ if (p->par.bps > 1 && p->par.le != SIO_LE_NATIVE) {
+ MP_ERR(ao, "swapped endian output not supported\n");
+ goto err_out;
+ }
+
+ /* Update sound params. */
+ if (p->par.bits == 8 && p->par.bps == 1 && !p->par.sig) {
+ ao->format = AF_FORMAT_U8;
+ } else if (p->par.bits == 16 && p->par.bps == 2 && p->par.sig) {
+ ao->format = AF_FORMAT_S16;
+ } else if ((p->par.bits == 32 || p->par.msb) && p->par.bps == 4 && p->par.sig) {
+ ao->format = AF_FORMAT_S32;
+ } else {
+ MP_ERR(ao, "couldn't set format\n");
+ goto err_out;
+ }
+
+ p->havevol = sio_onvol(p->hdl, volcb, ao);
+ sio_onmove(p->hdl, movecb, ao);
+
+ p->pfd = talloc_array_ptrtype(p, p->pfd, sio_nfds(p->hdl));
+ if (!p->pfd)
+ goto err_out;
+
+ ao->device_buffer = p->par.bufsz;
+ MP_VERBOSE(ao, "bufsz = %i, appbufsz = %i, round = %i\n",
+ p->par.bufsz, p->par.appbufsz, p->par.round);
+
+ p->delay = 0;
+ p->playing = false;
+ if (!sio_start(p->hdl)) {
+ MP_ERR(ao, "start: sio_start() fail.\n");
+ goto err_out;
+ }
+
+ return 0;
+
+err_out:
+ uninit(ao);
+ return -1;
+}
+
+static void uninit(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ if (p->hdl) {
+ sio_close(p->hdl);
+ p->hdl = NULL;
+ }
+ p->pfd = NULL;
+ p->playing = false;
+}
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct priv *p = ao->priv;
+ float *vol = arg;
+
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME:
+ if (!p->havevol)
+ return CONTROL_FALSE;
+ *vol = p->vol * 100 / SIO_MAXVOL;
+ break;
+ case AOCONTROL_SET_VOLUME:
+ if (!p->havevol)
+ return CONTROL_FALSE;
+ sio_setvol(p->hdl, *vol * SIO_MAXVOL / 100);
+ break;
+ default:
+ return CONTROL_UNKNOWN;
+ }
+ return CONTROL_OK;
+}
+
+static void reset(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ if (p->playing) {
+ p->playing = false;
+
+#if HAVE_SNDIO_1_9
+ if (!sio_flush(p->hdl)) {
+ MP_ERR(ao, "reset: couldn't sio_flush()\n");
+#else
+ if (!sio_stop(p->hdl)) {
+ MP_ERR(ao, "reset: couldn't sio_stop()\n");
+#endif
+ }
+ p->delay = 0;
+ if (!sio_start(p->hdl)) {
+ MP_ERR(ao, "reset: sio_start() fail.\n");
+ }
+ }
+}
+
+static void start(struct ao *ao)
+{
+ struct priv *p = ao->priv;
+
+ p->playing = true;
+ process_events(ao);
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+ struct priv *p = ao->priv;
+ const size_t size = (samples * ao->sstride);
+ size_t rc;
+
+ rc = sio_write(p->hdl, data[0], size);
+ if (rc != size) {
+ MP_WARN(ao, "audio_write: unexpected partial write: required: %zu, written: %zu.\n",
+ size, rc);
+ reset(ao);
+ p->playing = false;
+ return false;
+ }
+ p->delay += samples;
+
+ return true;
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct priv *p = ao->priv;
+
+ process_events(ao);
+
+ /* how many samples we can play without blocking */
+ state->free_samples = ao->device_buffer - p->delay;
+ state->free_samples = state->free_samples / p->par.round * p->par.round;
+ /* how many samples are already in the buffer to be played */
+ state->queued_samples = p->delay;
+ /* delay in seconds between first and last sample in buffer */
+ state->delay = p->delay / (double)p->par.rate;
+
+ /* report unexpected EOF / underrun */
+ if ((state->queued_samples && state->queued_samples &&
+ (state->queued_samples < state->free_samples) &&
+ p->playing) || sio_eof(p->hdl))
+ {
+ MP_VERBOSE(ao, "get_state: EOF/underrun detected.\n");
+ MP_VERBOSE(ao, "get_state: free: %d, queued: %d, delay: %lf\n", \
+ state->free_samples, state->queued_samples, state->delay);
+ p->playing = false;
+ state->playing = p->playing;
+ ao_wakeup_playthread(ao);
+ } else {
+ state->playing = p->playing;
+ }
+}
+
+const struct ao_driver audio_out_sndio = {
+ .name = "sndio",
+ .description = "sndio audio output",
+ .init = init,
+ .uninit = uninit,
+ .control = control,
+ .reset = reset,
+ .start = start,
+ .write = audio_write,
+ .get_state = get_state,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/audio/out/ao_wasapi.c b/audio/out/ao_wasapi.c
new file mode 100644
index 0000000..b201f26
--- /dev/null
+++ b/audio/out/ao_wasapi.c
@@ -0,0 +1,504 @@
+/*
+ * This file is part of mpv.
+ *
+ * Original author: Jonathan Yong <10walls@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <inttypes.h>
+#include <libavutil/mathematics.h>
+
+#include "options/m_option.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "osdep/io.h"
+#include "misc/dispatch.h"
+#include "ao_wasapi.h"
+
+// naive av_rescale for unsigned
+static UINT64 uint64_scale(UINT64 x, UINT64 num, UINT64 den)
+{
+ return (x / den) * num
+ + ((x % den) * (num / den))
+ + ((x % den) * (num % den)) / den;
+}
+
+static HRESULT get_device_delay(struct wasapi_state *state, double *delay_ns)
+{
+ UINT64 sample_count = atomic_load(&state->sample_count);
+ UINT64 position, qpc_position;
+ HRESULT hr;
+
+ hr = IAudioClock_GetPosition(state->pAudioClock, &position, &qpc_position);
+ EXIT_ON_ERROR(hr);
+ // GetPosition succeeded, but the result may be
+ // inaccurate due to the length of the call
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/dd370889%28v=vs.85%29.aspx
+ if (hr == S_FALSE)
+ MP_VERBOSE(state, "Possibly inaccurate device position.\n");
+
+ // convert position to number of samples careful to avoid overflow
+ UINT64 sample_position = uint64_scale(position,
+ state->format.Format.nSamplesPerSec,
+ state->clock_frequency);
+ INT64 diff = sample_count - sample_position;
+ *delay_ns = diff * 1e9 / state->format.Format.nSamplesPerSec;
+
+ // Correct for any delay in IAudioClock_GetPosition above.
+ // This should normally be very small (<1 us), but just in case. . .
+ LARGE_INTEGER qpc;
+ QueryPerformanceCounter(&qpc);
+ INT64 qpc_diff = av_rescale(qpc.QuadPart, 10000000, state->qpc_frequency.QuadPart)
+ - qpc_position;
+ // ignore the above calculation if it yields more than 10 seconds (due to
+ // possible overflow inside IAudioClock_GetPosition)
+ if (qpc_diff < 10 * 10000000) {
+ *delay_ns -= qpc_diff * 100.0; // convert to ns
+ } else {
+ MP_VERBOSE(state, "Insane qpc delay correction of %g seconds. "
+ "Ignoring it.\n", qpc_diff / 10000000.0);
+ }
+
+ if (sample_count > 0 && *delay_ns <= 0) {
+ MP_WARN(state, "Under-run: Device delay: %g ns\n", *delay_ns);
+ } else {
+ MP_TRACE(state, "Device delay: %g ns\n", *delay_ns);
+ }
+
+ return S_OK;
+exit_label:
+ MP_ERR(state, "Error getting device delay: %s\n", mp_HRESULT_to_str(hr));
+ return hr;
+}
+
+static bool thread_feed(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ HRESULT hr;
+
+ UINT32 frame_count = state->bufferFrameCount;
+ UINT32 padding;
+ hr = IAudioClient_GetCurrentPadding(state->pAudioClient, &padding);
+ EXIT_ON_ERROR(hr);
+ bool refill = false;
+ if (state->share_mode == AUDCLNT_SHAREMODE_SHARED) {
+ // Return if there's nothing to do.
+ if (frame_count <= padding)
+ return false;
+ // In shared mode, there is only one buffer of size bufferFrameCount.
+ // We must therefore take care not to overwrite the samples that have
+ // yet to play.
+ frame_count -= padding;
+ } else if (padding >= 2 * frame_count) {
+ // In exclusive mode, we exchange entire buffers of size
+ // bufferFrameCount with the device. If there are already two such
+ // full buffers waiting to play, there is no work to do.
+ return false;
+ } else if (padding < frame_count) {
+ // If there is not at least one full buffer of audio queued to play in
+ // exclusive mode, call this function again immediately to try and catch
+ // up and avoid a cascade of under-runs. WASAPI doesn't seem to be smart
+ // enough to send more feed events when it gets behind.
+ refill = true;
+ }
+ MP_TRACE(ao, "Frame to fill: %"PRIu32". Padding: %"PRIu32"\n",
+ frame_count, padding);
+
+ double delay_ns;
+ hr = get_device_delay(state, &delay_ns);
+ EXIT_ON_ERROR(hr);
+ // add the buffer delay
+ delay_ns += frame_count * 1e9 / state->format.Format.nSamplesPerSec;
+
+ BYTE *pData;
+ hr = IAudioRenderClient_GetBuffer(state->pRenderClient,
+ frame_count, &pData);
+ EXIT_ON_ERROR(hr);
+
+ BYTE *data[1] = {pData};
+
+ ao_read_data_converted(ao, &state->convert_format,
+ (void **)data, frame_count,
+ mp_time_ns() + (int64_t)llrint(delay_ns));
+
+ // note, we can't use ao_read_data return value here since we already
+ // committed to frame_count above in the GetBuffer call
+ hr = IAudioRenderClient_ReleaseBuffer(state->pRenderClient,
+ frame_count, 0);
+ EXIT_ON_ERROR(hr);
+
+ atomic_fetch_add(&state->sample_count, frame_count);
+
+ return refill;
+exit_label:
+ MP_ERR(state, "Error feeding audio: %s\n", mp_HRESULT_to_str(hr));
+ MP_VERBOSE(ao, "Requesting ao reload\n");
+ ao_request_reload(ao);
+ return false;
+}
+
+static void thread_reset(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ HRESULT hr;
+ MP_DBG(state, "Thread Reset\n");
+ hr = IAudioClient_Stop(state->pAudioClient);
+ if (FAILED(hr))
+ MP_ERR(state, "IAudioClient_Stop returned: %s\n", mp_HRESULT_to_str(hr));
+
+ hr = IAudioClient_Reset(state->pAudioClient);
+ if (FAILED(hr))
+ MP_ERR(state, "IAudioClient_Reset returned: %s\n", mp_HRESULT_to_str(hr));
+
+ atomic_store(&state->sample_count, 0);
+}
+
+static void thread_resume(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ MP_DBG(state, "Thread Resume\n");
+ thread_reset(ao);
+ thread_feed(ao);
+
+ HRESULT hr = IAudioClient_Start(state->pAudioClient);
+ if (FAILED(hr)) {
+ MP_ERR(state, "IAudioClient_Start returned %s\n",
+ mp_HRESULT_to_str(hr));
+ }
+}
+
+static void thread_wakeup(void *ptr)
+{
+ struct ao *ao = ptr;
+ struct wasapi_state *state = ao->priv;
+ SetEvent(state->hWake);
+}
+
+static void set_thread_state(struct ao *ao,
+ enum wasapi_thread_state thread_state)
+{
+ struct wasapi_state *state = ao->priv;
+ atomic_store(&state->thread_state, thread_state);
+ thread_wakeup(ao);
+}
+
+static DWORD __stdcall AudioThread(void *lpParameter)
+{
+ struct ao *ao = lpParameter;
+ struct wasapi_state *state = ao->priv;
+ mp_thread_set_name("ao/wasapi");
+ CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+
+ state->init_ok = wasapi_thread_init(ao);
+ SetEvent(state->hInitDone);
+ if (!state->init_ok)
+ goto exit_label;
+
+ MP_DBG(ao, "Entering dispatch loop\n");
+ while (true) {
+ if (WaitForSingleObject(state->hWake, INFINITE) != WAIT_OBJECT_0)
+ MP_ERR(ao, "Unexpected return value from WaitForSingleObject\n");
+
+ mp_dispatch_queue_process(state->dispatch, 0);
+
+ int thread_state = atomic_load(&state->thread_state);
+ switch (thread_state) {
+ case WASAPI_THREAD_FEED:
+ // fill twice on under-full buffer (see comment in thread_feed)
+ if (thread_feed(ao) && thread_feed(ao))
+ MP_ERR(ao, "Unable to fill buffer fast enough\n");
+ break;
+ case WASAPI_THREAD_RESET:
+ thread_reset(ao);
+ break;
+ case WASAPI_THREAD_RESUME:
+ thread_resume(ao);
+ break;
+ case WASAPI_THREAD_SHUTDOWN:
+ thread_reset(ao);
+ goto exit_label;
+ default:
+ MP_ERR(ao, "Unhandled thread state: %d\n", thread_state);
+ }
+ // the default is to feed unless something else is requested
+ atomic_compare_exchange_strong(&state->thread_state, &thread_state,
+ WASAPI_THREAD_FEED);
+ }
+exit_label:
+ wasapi_thread_uninit(ao);
+
+ CoUninitialize();
+ MP_DBG(ao, "Thread return\n");
+ return 0;
+}
+
+static void uninit(struct ao *ao)
+{
+ MP_DBG(ao, "Uninit wasapi\n");
+ struct wasapi_state *state = ao->priv;
+ if (state->hWake)
+ set_thread_state(ao, WASAPI_THREAD_SHUTDOWN);
+
+ if (state->hAudioThread &&
+ WaitForSingleObject(state->hAudioThread, INFINITE) != WAIT_OBJECT_0)
+ {
+ MP_ERR(ao, "Unexpected return value from WaitForSingleObject "
+ "while waiting for audio thread to terminate\n");
+ }
+
+ SAFE_DESTROY(state->hInitDone, CloseHandle(state->hInitDone));
+ SAFE_DESTROY(state->hWake, CloseHandle(state->hWake));
+ SAFE_DESTROY(state->hAudioThread,CloseHandle(state->hAudioThread));
+
+ wasapi_change_uninit(ao);
+
+ talloc_free(state->deviceID);
+
+ CoUninitialize();
+ MP_DBG(ao, "Uninit wasapi done\n");
+}
+
+static int init(struct ao *ao)
+{
+ MP_DBG(ao, "Init wasapi\n");
+ CoInitializeEx(NULL, COINIT_MULTITHREADED);
+
+ struct wasapi_state *state = ao->priv;
+ state->log = ao->log;
+
+ state->opt_exclusive |= ao->init_flags & AO_INIT_EXCLUSIVE;
+
+#if !HAVE_UWP
+ state->deviceID = wasapi_find_deviceID(ao);
+ if (!state->deviceID) {
+ uninit(ao);
+ return -1;
+ }
+#endif
+
+ if (state->deviceID)
+ wasapi_change_init(ao, false);
+
+ state->hInitDone = CreateEventW(NULL, FALSE, FALSE, NULL);
+ state->hWake = CreateEventW(NULL, FALSE, FALSE, NULL);
+ if (!state->hInitDone || !state->hWake) {
+ MP_FATAL(ao, "Error creating events\n");
+ uninit(ao);
+ return -1;
+ }
+
+ state->dispatch = mp_dispatch_create(state);
+ mp_dispatch_set_wakeup_fn(state->dispatch, thread_wakeup, ao);
+
+ state->init_ok = false;
+ state->hAudioThread = CreateThread(NULL, 0, &AudioThread, ao, 0, NULL);
+ if (!state->hAudioThread) {
+ MP_FATAL(ao, "Failed to create audio thread\n");
+ uninit(ao);
+ return -1;
+ }
+
+ WaitForSingleObject(state->hInitDone, INFINITE); // wait on init complete
+ SAFE_DESTROY(state->hInitDone,CloseHandle(state->hInitDone));
+ if (!state->init_ok) {
+ if (!ao->probing)
+ MP_FATAL(ao, "Received failure from audio thread\n");
+ uninit(ao);
+ return -1;
+ }
+
+ MP_DBG(ao, "Init wasapi done\n");
+ return 0;
+}
+
+static int thread_control_exclusive(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct wasapi_state *state = ao->priv;
+ if (!state->pEndpointVolume)
+ return CONTROL_UNKNOWN;
+
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME:
+ case AOCONTROL_SET_VOLUME:
+ if (!(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_VOLUME))
+ return CONTROL_FALSE;
+ break;
+ case AOCONTROL_GET_MUTE:
+ case AOCONTROL_SET_MUTE:
+ if (!(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_MUTE))
+ return CONTROL_FALSE;
+ break;
+ }
+
+ float volume;
+ BOOL mute;
+ switch (cmd) {
+ case AOCONTROL_GET_VOLUME:
+ IAudioEndpointVolume_GetMasterVolumeLevelScalar(
+ state->pEndpointVolume, &volume);
+ *(float *)arg = volume;
+ return CONTROL_OK;
+ case AOCONTROL_SET_VOLUME:
+ volume = (*(float *)arg) / 100.f;
+ IAudioEndpointVolume_SetMasterVolumeLevelScalar(
+ state->pEndpointVolume, volume, NULL);
+ return CONTROL_OK;
+ case AOCONTROL_GET_MUTE:
+ IAudioEndpointVolume_GetMute(state->pEndpointVolume, &mute);
+ *(bool *)arg = mute;
+ return CONTROL_OK;
+ case AOCONTROL_SET_MUTE:
+ mute = *(bool *)arg;
+ IAudioEndpointVolume_SetMute(state->pEndpointVolume, mute, NULL);
+ return CONTROL_OK;
+ }
+ return CONTROL_UNKNOWN;
+}
+
+static int thread_control_shared(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct wasapi_state *state = ao->priv;
+ if (!state->pAudioVolume)
+ return CONTROL_UNKNOWN;
+
+ float volume;
+ BOOL mute;
+ switch(cmd) {
+ case AOCONTROL_GET_VOLUME:
+ ISimpleAudioVolume_GetMasterVolume(state->pAudioVolume, &volume);
+ *(float *)arg = volume;
+ return CONTROL_OK;
+ case AOCONTROL_SET_VOLUME:
+ volume = (*(float *)arg) / 100.f;
+ ISimpleAudioVolume_SetMasterVolume(state->pAudioVolume, volume, NULL);
+ return CONTROL_OK;
+ case AOCONTROL_GET_MUTE:
+ ISimpleAudioVolume_GetMute(state->pAudioVolume, &mute);
+ *(bool *)arg = mute;
+ return CONTROL_OK;
+ case AOCONTROL_SET_MUTE:
+ mute = *(bool *)arg;
+ ISimpleAudioVolume_SetMute(state->pAudioVolume, mute, NULL);
+ return CONTROL_OK;
+ }
+ return CONTROL_UNKNOWN;
+}
+
+static int thread_control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct wasapi_state *state = ao->priv;
+
+ // common to exclusive and shared
+ switch (cmd) {
+ case AOCONTROL_UPDATE_STREAM_TITLE:
+ if (!state->pSessionControl)
+ return CONTROL_FALSE;
+
+ wchar_t *title = mp_from_utf8(NULL, (const char *)arg);
+ HRESULT hr = IAudioSessionControl_SetDisplayName(state->pSessionControl,
+ title,NULL);
+ talloc_free(title);
+
+ if (SUCCEEDED(hr))
+ return CONTROL_OK;
+
+ MP_WARN(ao, "Error setting audio session name: %s\n",
+ mp_HRESULT_to_str(hr));
+
+ assert(ao->client_name);
+ if (!ao->client_name)
+ return CONTROL_ERROR;
+
+ // Fallback to client name
+ title = mp_from_utf8(NULL, ao->client_name);
+ IAudioSessionControl_SetDisplayName(state->pSessionControl,
+ title, NULL);
+ talloc_free(title);
+
+ return CONTROL_ERROR;
+ }
+
+ return state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE ?
+ thread_control_exclusive(ao, cmd, arg) :
+ thread_control_shared(ao, cmd, arg);
+}
+
+static void run_control(void *p)
+{
+ void **pp = p;
+ struct ao *ao = pp[0];
+ enum aocontrol cmd = *(enum aocontrol *)pp[1];
+ void *arg = pp[2];
+ *(int *)pp[3] = thread_control(ao, cmd, arg);
+}
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct wasapi_state *state = ao->priv;
+ int ret;
+ void *p[] = {ao, &cmd, arg, &ret};
+ mp_dispatch_run(state->dispatch, run_control, p);
+ return ret;
+}
+
+static void audio_reset(struct ao *ao)
+{
+ set_thread_state(ao, WASAPI_THREAD_RESET);
+}
+
+static void audio_resume(struct ao *ao)
+{
+ set_thread_state(ao, WASAPI_THREAD_RESUME);
+}
+
+static void hotplug_uninit(struct ao *ao)
+{
+ MP_DBG(ao, "Hotplug uninit\n");
+ wasapi_change_uninit(ao);
+ CoUninitialize();
+}
+
+static int hotplug_init(struct ao *ao)
+{
+ MP_DBG(ao, "Hotplug init\n");
+ struct wasapi_state *state = ao->priv;
+ state->log = ao->log;
+ CoInitializeEx(NULL, COINIT_MULTITHREADED);
+ HRESULT hr = wasapi_change_init(ao, true);
+ EXIT_ON_ERROR(hr);
+
+ return 0;
+ exit_label:
+ MP_FATAL(state, "Error setting up audio hotplug: %s\n", mp_HRESULT_to_str(hr));
+ hotplug_uninit(ao);
+ return -1;
+}
+
+#define OPT_BASE_STRUCT struct wasapi_state
+
+const struct ao_driver audio_out_wasapi = {
+ .description = "Windows WASAPI audio output (event mode)",
+ .name = "wasapi",
+ .init = init,
+ .uninit = uninit,
+ .control = control,
+ .reset = audio_reset,
+ .start = audio_resume,
+ .list_devs = wasapi_list_devs,
+ .hotplug_init = hotplug_init,
+ .hotplug_uninit = hotplug_uninit,
+ .priv_size = sizeof(wasapi_state),
+};
diff --git a/audio/out/ao_wasapi.h b/audio/out/ao_wasapi.h
new file mode 100644
index 0000000..17b8f7a
--- /dev/null
+++ b/audio/out/ao_wasapi.h
@@ -0,0 +1,116 @@
+/*
+ * This file is part of mpv.
+ *
+ * Original author: Jonathan Yong <10walls@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_AO_WASAPI_H_
+#define MP_AO_WASAPI_H_
+
+#include <stdatomic.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#include <windows.h>
+#include <mmdeviceapi.h>
+#include <audioclient.h>
+#include <audiopolicy.h>
+#include <endpointvolume.h>
+
+#include "common/msg.h"
+#include "osdep/windows_utils.h"
+#include "internal.h"
+#include "ao.h"
+
+typedef struct change_notify {
+ IMMNotificationClient client; // this must be first in the structure!
+ IMMDeviceEnumerator *pEnumerator; // object where client is registered
+ LPWSTR monitored; // Monitored device
+ bool is_hotplug;
+ struct ao *ao;
+} change_notify;
+
+HRESULT wasapi_change_init(struct ao* ao, bool is_hotplug);
+void wasapi_change_uninit(struct ao* ao);
+
+enum wasapi_thread_state {
+ WASAPI_THREAD_FEED = 0,
+ WASAPI_THREAD_RESUME,
+ WASAPI_THREAD_RESET,
+ WASAPI_THREAD_SHUTDOWN
+};
+
+typedef struct wasapi_state {
+ struct mp_log *log;
+
+ bool init_ok; // status of init phase
+ // Thread handles
+ HANDLE hInitDone; // set when init is complete in audio thread
+ HANDLE hAudioThread; // the audio thread itself
+ HANDLE hWake; // thread wakeup event
+ atomic_int thread_state; // enum wasapi_thread_state (what to do on wakeup)
+ struct mp_dispatch_queue *dispatch; // for volume/mute/session display
+
+ // for setting the audio thread priority
+ HANDLE hTask;
+
+ // ID of the device to use
+ LPWSTR deviceID;
+ // WASAPI object handles owned and used by audio thread
+ IMMDevice *pDevice;
+ IAudioClient *pAudioClient;
+ IAudioRenderClient *pRenderClient;
+
+ // WASAPI internal clock information, for estimating delay
+ IAudioClock *pAudioClock;
+ atomic_ullong sample_count; // samples per channel written by GetBuffer
+ UINT64 clock_frequency; // scale for position returned by GetPosition
+ LARGE_INTEGER qpc_frequency; // frequency of Windows' high resolution timer
+
+ // WASAPI control
+ IAudioSessionControl *pSessionControl; // setting the stream title
+ IAudioEndpointVolume *pEndpointVolume; // exclusive mode volume/mute
+ ISimpleAudioVolume *pAudioVolume; // shared mode volume/mute
+ DWORD vol_hw_support; // is hardware volume supported for exclusive-mode?
+
+ // ao options
+ int opt_exclusive;
+
+ // format info
+ WAVEFORMATEXTENSIBLE format;
+ AUDCLNT_SHAREMODE share_mode; // AUDCLNT_SHAREMODE_EXCLUSIVE / SHARED
+ UINT32 bufferFrameCount; // number of frames in buffer
+ struct ao_convert_fmt convert_format;
+
+ change_notify change;
+} wasapi_state;
+
+char *mp_PKEY_to_str_buf(char *buf, size_t buf_size, const PROPERTYKEY *pkey);
+#define mp_PKEY_to_str(pkey) mp_PKEY_to_str_buf((char[42]){0}, 42, (pkey))
+
+void wasapi_list_devs(struct ao *ao, struct ao_device_list *list);
+bstr wasapi_get_specified_device_string(struct ao *ao);
+LPWSTR wasapi_find_deviceID(struct ao *ao);
+
+bool wasapi_thread_init(struct ao *ao);
+void wasapi_thread_uninit(struct ao *ao);
+
+#define EXIT_ON_ERROR(hres) \
+ do { if (FAILED(hres)) { goto exit_label; } } while(0)
+#define SAFE_DESTROY(unk, release) \
+ do { if ((unk) != NULL) { release; (unk) = NULL; } } while(0)
+
+#endif
diff --git a/audio/out/ao_wasapi_changenotify.c b/audio/out/ao_wasapi_changenotify.c
new file mode 100644
index 0000000..f0e1895
--- /dev/null
+++ b/audio/out/ao_wasapi_changenotify.c
@@ -0,0 +1,246 @@
+/*
+ * This file is part of mpv.
+ *
+ * Original author: Jonathan Yong <10walls@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <wchar.h>
+
+#include "ao_wasapi.h"
+
+static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_QueryInterface(
+ IMMNotificationClient* This, REFIID riid, void **ppvObject)
+{
+ // Compatible with IMMNotificationClient and IUnknown
+ if (IsEqualGUID(&IID_IMMNotificationClient, riid) ||
+ IsEqualGUID(&IID_IUnknown, riid))
+ {
+ *ppvObject = (void *)This;
+ return S_OK;
+ } else {
+ *ppvObject = NULL;
+ return E_NOINTERFACE;
+ }
+}
+
+// these are required, but not actually used
+static ULONG STDMETHODCALLTYPE sIMMNotificationClient_AddRef(
+ IMMNotificationClient *This)
+{
+ return 1;
+}
+
+// MSDN says it should free itself, but we're static
+static ULONG STDMETHODCALLTYPE sIMMNotificationClient_Release(
+ IMMNotificationClient *This)
+{
+ return 1;
+}
+
+static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDeviceStateChanged(
+ IMMNotificationClient *This,
+ LPCWSTR pwstrDeviceId,
+ DWORD dwNewState)
+{
+ change_notify *change = (change_notify *)This;
+ struct ao *ao = change->ao;
+
+ if (change->is_hotplug) {
+ MP_VERBOSE(ao,
+ "OnDeviceStateChanged triggered: sending hotplug event\n");
+ ao_hotplug_event(ao);
+ } else if (pwstrDeviceId && !wcscmp(pwstrDeviceId, change->monitored)) {
+ switch (dwNewState) {
+ case DEVICE_STATE_DISABLED:
+ case DEVICE_STATE_NOTPRESENT:
+ case DEVICE_STATE_UNPLUGGED:
+ MP_VERBOSE(ao, "OnDeviceStateChanged triggered on device %ls: "
+ "requesting ao reload\n", pwstrDeviceId);
+ ao_request_reload(ao);
+ break;
+ case DEVICE_STATE_ACTIVE:
+ break;
+ }
+ }
+
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDeviceAdded(
+ IMMNotificationClient *This,
+ LPCWSTR pwstrDeviceId)
+{
+ change_notify *change = (change_notify *)This;
+ struct ao *ao = change->ao;
+
+ if (change->is_hotplug) {
+ MP_VERBOSE(ao, "OnDeviceAdded triggered: sending hotplug event\n");
+ ao_hotplug_event(ao);
+ }
+
+ return S_OK;
+}
+
+// maybe MPV can go over to the preferred device once it is plugged in?
+static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDeviceRemoved(
+ IMMNotificationClient *This,
+ LPCWSTR pwstrDeviceId)
+{
+ change_notify *change = (change_notify *)This;
+ struct ao *ao = change->ao;
+
+ if (change->is_hotplug) {
+ MP_VERBOSE(ao, "OnDeviceRemoved triggered: sending hotplug event\n");
+ ao_hotplug_event(ao);
+ } else if (pwstrDeviceId && !wcscmp(pwstrDeviceId, change->monitored)) {
+ MP_VERBOSE(ao, "OnDeviceRemoved triggered for device %ls: "
+ "requesting ao reload\n", pwstrDeviceId);
+ ao_request_reload(ao);
+ }
+
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDefaultDeviceChanged(
+ IMMNotificationClient *This,
+ EDataFlow flow,
+ ERole role,
+ LPCWSTR pwstrDeviceId)
+{
+ change_notify *change = (change_notify *)This;
+ struct ao *ao = change->ao;
+
+ // don't care about "eCapture" or non-"eMultimedia" roles
+ if (flow == eCapture || role != eMultimedia) return S_OK;
+
+ if (change->is_hotplug) {
+ MP_VERBOSE(ao,
+ "OnDefaultDeviceChanged triggered: sending hotplug event\n");
+ ao_hotplug_event(ao);
+ } else {
+ // stay on the device the user specified
+ bstr device = wasapi_get_specified_device_string(ao);
+ if (device.len) {
+ MP_VERBOSE(ao, "OnDefaultDeviceChanged triggered: "
+ "staying on specified device %.*s\n", BSTR_P(device));
+ return S_OK;
+ }
+
+ // don't reload if already on the new default
+ if (pwstrDeviceId && !wcscmp(pwstrDeviceId, change->monitored)) {
+ MP_VERBOSE(ao, "OnDefaultDeviceChanged triggered: "
+ "already using default device, no reload required\n");
+ return S_OK;
+ }
+
+ // if we got here, we need to reload
+ MP_VERBOSE(ao,
+ "OnDefaultDeviceChanged triggered: requesting ao reload\n");
+ ao_request_reload(ao);
+ }
+
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnPropertyValueChanged(
+ IMMNotificationClient *This,
+ LPCWSTR pwstrDeviceId,
+ const PROPERTYKEY key)
+{
+ change_notify *change = (change_notify *)This;
+ struct ao *ao = change->ao;
+
+ if (!change->is_hotplug && pwstrDeviceId &&
+ !wcscmp(pwstrDeviceId, change->monitored))
+ {
+ MP_VERBOSE(ao, "OnPropertyValueChanged triggered on device %ls\n",
+ pwstrDeviceId);
+ if (IsEqualPropertyKey(PKEY_AudioEngine_DeviceFormat, key)) {
+ MP_VERBOSE(change->ao,
+ "Changed property: PKEY_AudioEngine_DeviceFormat "
+ "- requesting ao reload\n");
+ ao_request_reload(change->ao);
+ } else {
+ MP_VERBOSE(ao, "Changed property: %s\n", mp_PKEY_to_str(&key));
+ }
+ }
+
+ return S_OK;
+}
+
+static CONST_VTBL IMMNotificationClientVtbl sIMMNotificationClientVtbl = {
+ .QueryInterface = sIMMNotificationClient_QueryInterface,
+ .AddRef = sIMMNotificationClient_AddRef,
+ .Release = sIMMNotificationClient_Release,
+ .OnDeviceStateChanged = sIMMNotificationClient_OnDeviceStateChanged,
+ .OnDeviceAdded = sIMMNotificationClient_OnDeviceAdded,
+ .OnDeviceRemoved = sIMMNotificationClient_OnDeviceRemoved,
+ .OnDefaultDeviceChanged = sIMMNotificationClient_OnDefaultDeviceChanged,
+ .OnPropertyValueChanged = sIMMNotificationClient_OnPropertyValueChanged,
+};
+
+
+HRESULT wasapi_change_init(struct ao *ao, bool is_hotplug)
+{
+ struct wasapi_state *state = ao->priv;
+ struct change_notify *change = &state->change;
+ HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
+ &IID_IMMDeviceEnumerator,
+ (void **)&change->pEnumerator);
+ EXIT_ON_ERROR(hr);
+
+ // so the callbacks can access the ao
+ change->ao = ao;
+
+ // whether or not this is the hotplug instance
+ change->is_hotplug = is_hotplug;
+
+ if (is_hotplug) {
+ MP_DBG(ao, "Monitoring for hotplug events\n");
+ } else {
+ // Get the device string to compare with the pwstrDeviceId
+ change->monitored = state->deviceID;
+ MP_VERBOSE(ao, "Monitoring changes in device %ls\n", change->monitored);
+ }
+
+ // COM voodoo to emulate c++ class
+ change->client.lpVtbl = &sIMMNotificationClientVtbl;
+
+ // register the change notification client
+ hr = IMMDeviceEnumerator_RegisterEndpointNotificationCallback(
+ change->pEnumerator, (IMMNotificationClient *)change);
+ EXIT_ON_ERROR(hr);
+
+ return hr;
+exit_label:
+ MP_ERR(state, "Error setting up device change monitoring: %s\n",
+ mp_HRESULT_to_str(hr));
+ wasapi_change_uninit(ao);
+ return hr;
+}
+
+void wasapi_change_uninit(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ struct change_notify *change = &state->change;
+
+ if (change->pEnumerator && change->client.lpVtbl) {
+ IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(
+ change->pEnumerator, (IMMNotificationClient *)change);
+ }
+
+ SAFE_RELEASE(change->pEnumerator);
+}
diff --git a/audio/out/ao_wasapi_utils.c b/audio/out/ao_wasapi_utils.c
new file mode 100644
index 0000000..731fe8a
--- /dev/null
+++ b/audio/out/ao_wasapi_utils.c
@@ -0,0 +1,1063 @@
+/*
+ * This file is part of mpv.
+ *
+ * Original author: Jonathan Yong <10walls@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <wchar.h>
+#include <windows.h>
+#include <errors.h>
+#include <ksguid.h>
+#include <ksmedia.h>
+#include <avrt.h>
+
+#include "audio/format.h"
+#include "osdep/timer.h"
+#include "osdep/io.h"
+#include "osdep/strnlen.h"
+#include "ao_wasapi.h"
+
+DEFINE_PROPERTYKEY(mp_PKEY_Device_FriendlyName,
+ 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20,
+ 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);
+DEFINE_PROPERTYKEY(mp_PKEY_Device_DeviceDesc,
+ 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20,
+ 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2);
+// CEA 861 subformats
+// should work on vista
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS,
+ 0x00000008, 0x0000, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL,
+ 0x00000092, 0x0000, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+// might require 7+
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_AAC,
+ 0x00000006, 0x0cea, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_MPEG3,
+ 0x00000004, 0x0cea, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS,
+ 0x0000000a, 0x0cea, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD,
+ 0x0000000b, 0x0cea, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP,
+ 0x0000000c, 0x0cea, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+
+struct wasapi_sample_fmt {
+ int mp_format; // AF_FORMAT_*
+ int bits; // aka wBitsPerSample
+ int used_msb; // aka wValidBitsPerSample
+ const GUID *subtype;
+};
+
+// some common bit depths / container sizes (requests welcome)
+// Entries that have the same mp_format must be:
+// 1. consecutive
+// 2. sorted by preferred format (worst comes last)
+static const struct wasapi_sample_fmt wasapi_formats[] = {
+ {AF_FORMAT_U8, 8, 8, &KSDATAFORMAT_SUBTYPE_PCM},
+ {AF_FORMAT_S16, 16, 16, &KSDATAFORMAT_SUBTYPE_PCM},
+ {AF_FORMAT_S32, 32, 32, &KSDATAFORMAT_SUBTYPE_PCM},
+ // compatible, assume LSBs are ignored
+ {AF_FORMAT_S32, 32, 24, &KSDATAFORMAT_SUBTYPE_PCM},
+ // aka S24 (with conversion on output)
+ {AF_FORMAT_S32, 24, 24, &KSDATAFORMAT_SUBTYPE_PCM},
+ {AF_FORMAT_FLOAT, 32, 32, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT},
+ {AF_FORMAT_S_AC3, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL},
+ {AF_FORMAT_S_DTS, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS},
+ {AF_FORMAT_S_AAC, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_AAC},
+ {AF_FORMAT_S_MP3, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_MPEG3},
+ {AF_FORMAT_S_TRUEHD, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP},
+ {AF_FORMAT_S_EAC3, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS},
+ {AF_FORMAT_S_DTSHD, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD},
+ {0},
+};
+
+static void wasapi_get_best_sample_formats(
+ int src_format, struct wasapi_sample_fmt *out_formats)
+{
+ int mp_formats[AF_FORMAT_COUNT + 1];
+ af_get_best_sample_formats(src_format, mp_formats);
+ for (int n = 0; mp_formats[n]; n++) {
+ for (int i = 0; wasapi_formats[i].mp_format; i++) {
+ if (wasapi_formats[i].mp_format == mp_formats[n])
+ *out_formats++ = wasapi_formats[i];
+ }
+ }
+ *out_formats = (struct wasapi_sample_fmt) {0};
+}
+
+static const GUID *format_to_subtype(int format)
+{
+ for (int i = 0; wasapi_formats[i].mp_format; i++) {
+ if (format == wasapi_formats[i].mp_format)
+ return wasapi_formats[i].subtype;
+ }
+ return &KSDATAFORMAT_SPECIFIER_NONE;
+}
+
+char *mp_PKEY_to_str_buf(char *buf, size_t buf_size, const PROPERTYKEY *pkey)
+{
+ buf = mp_GUID_to_str_buf(buf, buf_size, &pkey->fmtid);
+ size_t guid_len = strnlen(buf, buf_size);
+ snprintf(buf + guid_len, buf_size - guid_len, ",%"PRIu32,
+ (uint32_t) pkey->pid);
+ return buf;
+}
+
+static void update_waveformat_datarate(WAVEFORMATEXTENSIBLE *wformat)
+{
+ WAVEFORMATEX *wf = &wformat->Format;
+ wf->nBlockAlign = wf->nChannels * wf->wBitsPerSample / 8;
+ wf->nAvgBytesPerSec = wf->nSamplesPerSec * wf->nBlockAlign;
+}
+
+static void set_waveformat(WAVEFORMATEXTENSIBLE *wformat,
+ struct wasapi_sample_fmt *format,
+ DWORD samplerate, struct mp_chmap *channels)
+{
+ wformat->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wformat->Format.nChannels = channels->num;
+ wformat->Format.nSamplesPerSec = samplerate;
+ wformat->Format.wBitsPerSample = format->bits;
+ wformat->Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+
+ wformat->SubFormat = *format_to_subtype(format->mp_format);
+ wformat->Samples.wValidBitsPerSample = format->used_msb;
+
+ uint64_t chans = mp_chmap_to_waveext(channels);
+ wformat->dwChannelMask = chans;
+
+ if (wformat->Format.nChannels > 8 || wformat->dwChannelMask != chans) {
+ // IAudioClient::IsFormatSupported tend to fallback to stereo for closest
+ // format match when there are more channels. Remix to standard layout.
+ // Also if input channel mask has channels outside 32-bits override it
+ // and hope for the best...
+ wformat->dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+ wformat->Format.nChannels = 8;
+ }
+
+ update_waveformat_datarate(wformat);
+}
+
+// other wformat parameters must already be set with set_waveformat
+static void change_waveformat_samplerate(WAVEFORMATEXTENSIBLE *wformat,
+ DWORD samplerate)
+{
+ wformat->Format.nSamplesPerSec = samplerate;
+ update_waveformat_datarate(wformat);
+}
+
+// other wformat parameters must already be set with set_waveformat
+static void change_waveformat_channels(WAVEFORMATEXTENSIBLE *wformat,
+ struct mp_chmap *channels)
+{
+ wformat->Format.nChannels = channels->num;
+ wformat->dwChannelMask = mp_chmap_to_waveext(channels);
+ update_waveformat_datarate(wformat);
+}
+
+static struct wasapi_sample_fmt format_from_waveformat(WAVEFORMATEX *wf)
+{
+ struct wasapi_sample_fmt res = {0};
+
+ for (int n = 0; wasapi_formats[n].mp_format; n++) {
+ const struct wasapi_sample_fmt *fmt = &wasapi_formats[n];
+ int valid_bits = 0;
+
+ if (wf->wBitsPerSample != fmt->bits)
+ continue;
+
+ const GUID *wf_guid = NULL;
+
+ switch (wf->wFormatTag) {
+ case WAVE_FORMAT_EXTENSIBLE: {
+ WAVEFORMATEXTENSIBLE *wformat = (WAVEFORMATEXTENSIBLE *)wf;
+ wf_guid = &wformat->SubFormat;
+ if (IsEqualGUID(wf_guid, &KSDATAFORMAT_SUBTYPE_PCM))
+ valid_bits = wformat->Samples.wValidBitsPerSample;
+ break;
+ }
+ case WAVE_FORMAT_PCM:
+ wf_guid = &KSDATAFORMAT_SUBTYPE_PCM;
+ break;
+ case WAVE_FORMAT_IEEE_FLOAT:
+ wf_guid = &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+ break;
+ }
+
+ if (!wf_guid || !IsEqualGUID(wf_guid, fmt->subtype))
+ continue;
+
+ res = *fmt;
+ if (valid_bits > 0 && valid_bits < fmt->bits)
+ res.used_msb = valid_bits;
+ break;
+ }
+
+ return res;
+}
+
+static bool chmap_from_waveformat(struct mp_chmap *channels,
+ const WAVEFORMATEX *wf)
+{
+ if (wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
+ WAVEFORMATEXTENSIBLE *wformat = (WAVEFORMATEXTENSIBLE *)wf;
+ mp_chmap_from_waveext(channels, wformat->dwChannelMask);
+ } else {
+ mp_chmap_from_channels(channels, wf->nChannels);
+ }
+
+ if (channels->num != wf->nChannels) {
+ mp_chmap_from_str(channels, bstr0("empty"));
+ return false;
+ }
+
+ return true;
+}
+
+static char *waveformat_to_str_buf(char *buf, size_t buf_size, WAVEFORMATEX *wf)
+{
+ struct mp_chmap channels;
+ chmap_from_waveformat(&channels, wf);
+
+ struct wasapi_sample_fmt format = format_from_waveformat(wf);
+
+ snprintf(buf, buf_size, "%s %s (%d/%d bits) @ %uhz",
+ mp_chmap_to_str(&channels),
+ af_fmt_to_str(format.mp_format), format.bits, format.used_msb,
+ (unsigned) wf->nSamplesPerSec);
+ return buf;
+}
+#define waveformat_to_str_(wf, sz) waveformat_to_str_buf((char[sz]){0}, sz, (wf))
+#define waveformat_to_str(wf) waveformat_to_str_(wf, MP_NUM_CHANNELS * 4 + 42)
+
+static void waveformat_copy(WAVEFORMATEXTENSIBLE* dst, WAVEFORMATEX* src)
+{
+ if (src->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
+ *dst = *(WAVEFORMATEXTENSIBLE *)src;
+ } else {
+ dst->Format = *src;
+ }
+}
+
+static bool set_ao_format(struct ao *ao, WAVEFORMATEX *wf,
+ AUDCLNT_SHAREMODE share_mode)
+{
+ struct wasapi_state *state = ao->priv;
+ struct wasapi_sample_fmt format = format_from_waveformat(wf);
+ if (!format.mp_format) {
+ MP_ERR(ao, "Unable to construct sample format from WAVEFORMAT %s\n",
+ waveformat_to_str(wf));
+ return false;
+ }
+
+ // Do not touch the ao for passthrough, just assume that we set WAVEFORMATEX
+ // correctly.
+ if (af_fmt_is_pcm(format.mp_format)) {
+ struct mp_chmap channels;
+ if (!chmap_from_waveformat(&channels, wf)) {
+ MP_ERR(ao, "Unable to construct channel map from WAVEFORMAT %s\n",
+ waveformat_to_str(wf));
+ return false;
+ }
+
+ struct ao_convert_fmt conv = {
+ .src_fmt = format.mp_format,
+ .channels = channels.num,
+ .dst_bits = format.bits,
+ .pad_lsb = format.bits - format.used_msb,
+ };
+ if (!ao_can_convert_inplace(&conv)) {
+ MP_ERR(ao, "Unable to convert to %s\n", waveformat_to_str(wf));
+ return false;
+ }
+
+ state->convert_format = conv;
+ ao->samplerate = wf->nSamplesPerSec;
+ ao->format = format.mp_format;
+ ao->channels = channels;
+ }
+ waveformat_copy(&state->format, wf);
+ state->share_mode = share_mode;
+
+ MP_VERBOSE(ao, "Accepted as %s %s @ %dhz -> %s (%s)\n",
+ mp_chmap_to_str(&ao->channels),
+ af_fmt_to_str(ao->format), ao->samplerate,
+ waveformat_to_str(wf),
+ state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE
+ ? "exclusive" : "shared");
+ return true;
+}
+
+#define mp_format_res_str(hres) \
+ (SUCCEEDED(hres) ? ((hres) == S_OK) ? "ok" : "close" \
+ : ((hres) == AUDCLNT_E_UNSUPPORTED_FORMAT) \
+ ? "unsupported" : mp_HRESULT_to_str(hres))
+
+static bool try_format_exclusive(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat)
+{
+ struct wasapi_state *state = ao->priv;
+ HRESULT hr = IAudioClient_IsFormatSupported(state->pAudioClient,
+ AUDCLNT_SHAREMODE_EXCLUSIVE,
+ &wformat->Format, NULL);
+ MP_VERBOSE(ao, "Trying %s (exclusive) -> %s\n",
+ waveformat_to_str(&wformat->Format), mp_format_res_str(hr));
+ return SUCCEEDED(hr);
+}
+
+static bool search_sample_formats(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat,
+ int samplerate, struct mp_chmap *channels)
+{
+ struct wasapi_sample_fmt alt_formats[MP_ARRAY_SIZE(wasapi_formats)];
+ wasapi_get_best_sample_formats(ao->format, alt_formats);
+ for (int n = 0; alt_formats[n].mp_format; n++) {
+ set_waveformat(wformat, &alt_formats[n], samplerate, channels);
+ if (try_format_exclusive(ao, wformat))
+ return true;
+ }
+
+ wformat->Format.wBitsPerSample = 0;
+ return false;
+}
+
+static bool search_samplerates(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat,
+ struct mp_chmap *channels)
+{
+ // put common samplerates first so that we find format early
+ int try[] = {48000, 44100, 96000, 88200, 192000, 176400,
+ 32000, 22050, 11025, 8000, 16000, 352800, 384000, 0};
+
+ // get a list of supported rates
+ int n = 0;
+ int supported[MP_ARRAY_SIZE(try)] = {0};
+
+ wformat->Format.wBitsPerSample = 0;
+ for (int i = 0; try[i]; i++) {
+ if (!wformat->Format.wBitsPerSample) {
+ if (search_sample_formats(ao, wformat, try[i], channels))
+ supported[n++] = try[i];
+ } else {
+ change_waveformat_samplerate(wformat, try[i]);
+ if (try_format_exclusive(ao, wformat))
+ supported[n++] = try[i];
+ }
+ }
+
+ int samplerate = af_select_best_samplerate(ao->samplerate, supported);
+ if (samplerate > 0) {
+ change_waveformat_samplerate(wformat, samplerate);
+ return true;
+ }
+
+ // otherwise, this is probably an unsupported channel map
+ wformat->Format.nSamplesPerSec = 0;
+ return false;
+}
+
+static bool search_channels(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat)
+{
+ struct wasapi_state *state = ao->priv;
+ struct mp_chmap_sel chmap_sel = {.tmp = state};
+ struct mp_chmap entry;
+ // put common layouts first so that we find sample rate/format early
+ char *channel_layouts[] =
+ {"stereo", "5.1", "7.1", "6.1", "mono", "2.1", "4.0", "5.0",
+ "3.0", "3.0(back)",
+ "quad", "quad(side)", "3.1",
+ "5.0(side)", "4.1",
+ "5.1(side)", "6.0", "6.0(front)", "hexagonal",
+ "6.1(back)", "6.1(front)", "7.0", "7.0(front)",
+ "7.1(wide)", "7.1(wide-side)", "7.1(rear)", "octagonal", NULL};
+
+ wformat->Format.nSamplesPerSec = 0;
+ for (int j = 0; channel_layouts[j]; j++) {
+ mp_chmap_from_str(&entry, bstr0(channel_layouts[j]));
+ if (!wformat->Format.nSamplesPerSec) {
+ if (search_samplerates(ao, wformat, &entry))
+ mp_chmap_sel_add_map(&chmap_sel, &entry);
+ } else {
+ change_waveformat_channels(wformat, &entry);
+ if (try_format_exclusive(ao, wformat))
+ mp_chmap_sel_add_map(&chmap_sel, &entry);
+ }
+ }
+
+ entry = ao->channels;
+ if (ao_chmap_sel_adjust2(ao, &chmap_sel, &entry, !state->opt_exclusive)){
+ change_waveformat_channels(wformat, &entry);
+ return true;
+ }
+
+ MP_ERR(ao, "No suitable audio format found\n");
+ return false;
+}
+
+static bool find_formats_exclusive(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat)
+{
+ // Try the specified format as is
+ if (try_format_exclusive(ao, wformat))
+ return true;
+
+ if (af_fmt_is_spdif(ao->format)) {
+ if (ao->format != AF_FORMAT_S_AC3) {
+ // If the requested format failed and it is passthrough, but not
+ // AC3, try lying and saying it is.
+ MP_VERBOSE(ao, "Retrying as AC3.\n");
+ wformat->SubFormat = *format_to_subtype(AF_FORMAT_S_AC3);
+ if (try_format_exclusive(ao, wformat))
+ return true;
+ }
+ return false;
+ }
+
+ // Fallback on the PCM format search
+ return search_channels(ao, wformat);
+}
+
+static bool find_formats_shared(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat)
+{
+ struct wasapi_state *state = ao->priv;
+
+ struct mp_chmap channels;
+ if (!chmap_from_waveformat(&channels, &wformat->Format)) {
+ MP_ERR(ao, "Error converting channel map\n");
+ return false;
+ }
+
+ HRESULT hr;
+ WAVEFORMATEX *mix_format;
+ hr = IAudioClient_GetMixFormat(state->pAudioClient, &mix_format);
+ EXIT_ON_ERROR(hr);
+
+ // WASAPI doesn't do any sample rate conversion on its own and
+ // will typically only accept the mix format samplerate. Although
+ // it will accept any PCM sample format, everything gets converted
+ // to the mix format anyway (pretty much always float32), so just
+ // use that.
+ WAVEFORMATEXTENSIBLE try_format;
+ waveformat_copy(&try_format, mix_format);
+ CoTaskMemFree(mix_format);
+
+ // WASAPI may accept channel maps other than the mix format
+ // if a surround emulator is enabled.
+ change_waveformat_channels(&try_format, &channels);
+
+ hr = IAudioClient_IsFormatSupported(state->pAudioClient,
+ AUDCLNT_SHAREMODE_SHARED,
+ &try_format.Format,
+ &mix_format);
+ MP_VERBOSE(ao, "Trying %s (shared) -> %s\n",
+ waveformat_to_str(&try_format.Format), mp_format_res_str(hr));
+ if (hr != AUDCLNT_E_UNSUPPORTED_FORMAT)
+ EXIT_ON_ERROR(hr);
+
+ switch (hr) {
+ case S_OK:
+ waveformat_copy(wformat, &try_format.Format);
+ break;
+ case S_FALSE:
+ waveformat_copy(wformat, mix_format);
+ CoTaskMemFree(mix_format);
+ MP_VERBOSE(ao, "Closest match is %s\n",
+ waveformat_to_str(&wformat->Format));
+ break;
+ default:
+ hr = IAudioClient_GetMixFormat(state->pAudioClient, &mix_format);
+ EXIT_ON_ERROR(hr);
+ waveformat_copy(wformat, mix_format);
+ CoTaskMemFree(mix_format);
+ MP_VERBOSE(ao, "Fallback to mix format %s\n",
+ waveformat_to_str(&wformat->Format));
+
+ }
+
+ return true;
+exit_label:
+ MP_ERR(state, "Error finding shared mode format: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+}
+
+static bool find_formats(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ struct mp_chmap channels = ao->channels;
+
+ if (mp_chmap_is_unknown(&channels))
+ mp_chmap_from_channels(&channels, channels.num);
+ mp_chmap_reorder_to_waveext(&channels);
+ if (!mp_chmap_is_valid(&channels))
+ mp_chmap_from_channels(&channels, 2);
+
+ struct wasapi_sample_fmt alt_formats[MP_ARRAY_SIZE(wasapi_formats)];
+ wasapi_get_best_sample_formats(ao->format, alt_formats);
+ struct wasapi_sample_fmt wasapi_format =
+ {AF_FORMAT_S16, 16, 16, &KSDATAFORMAT_SUBTYPE_PCM};;
+ if (alt_formats[0].mp_format)
+ wasapi_format = alt_formats[0];
+
+ AUDCLNT_SHAREMODE share_mode;
+ WAVEFORMATEXTENSIBLE wformat;
+ set_waveformat(&wformat, &wasapi_format, ao->samplerate, &channels);
+
+ if (state->opt_exclusive || af_fmt_is_spdif(ao->format)) {
+ share_mode = AUDCLNT_SHAREMODE_EXCLUSIVE;
+ if(!find_formats_exclusive(ao, &wformat))
+ return false;
+ } else {
+ share_mode = AUDCLNT_SHAREMODE_SHARED;
+ if(!find_formats_shared(ao, &wformat))
+ return false;
+ }
+
+ return set_ao_format(ao, &wformat.Format, share_mode);
+}
+
+static HRESULT init_clock(struct wasapi_state *state) {
+ HRESULT hr = IAudioClient_GetService(state->pAudioClient,
+ &IID_IAudioClock,
+ (void **)&state->pAudioClock);
+ EXIT_ON_ERROR(hr);
+ hr = IAudioClock_GetFrequency(state->pAudioClock, &state->clock_frequency);
+ EXIT_ON_ERROR(hr);
+
+ QueryPerformanceFrequency(&state->qpc_frequency);
+
+ atomic_store(&state->sample_count, 0);
+
+ MP_VERBOSE(state,
+ "IAudioClock::GetFrequency gave a frequency of %"PRIu64".\n",
+ (uint64_t) state->clock_frequency);
+
+ return S_OK;
+exit_label:
+ MP_ERR(state, "Error obtaining the audio device's timing: %s\n",
+ mp_HRESULT_to_str(hr));
+ return hr;
+}
+
+static void init_session_display(struct wasapi_state *state, const char *name) {
+ HRESULT hr = IAudioClient_GetService(state->pAudioClient,
+ &IID_IAudioSessionControl,
+ (void **)&state->pSessionControl);
+ EXIT_ON_ERROR(hr);
+
+ wchar_t path[MAX_PATH] = {0};
+ GetModuleFileNameW(NULL, path, MAX_PATH);
+ hr = IAudioSessionControl_SetIconPath(state->pSessionControl, path, NULL);
+ if (FAILED(hr)) {
+ // don't goto exit_label here since SetDisplayName might still work
+ MP_WARN(state, "Error setting audio session icon: %s\n",
+ mp_HRESULT_to_str(hr));
+ }
+
+ assert(name);
+ if (!name)
+ return;
+
+ wchar_t *title = mp_from_utf8(NULL, name);
+ hr = IAudioSessionControl_SetDisplayName(state->pSessionControl, title, NULL);
+ talloc_free(title);
+
+ EXIT_ON_ERROR(hr);
+ return;
+exit_label:
+ // if we got here then the session control is useless - release it
+ SAFE_RELEASE(state->pSessionControl);
+ MP_WARN(state, "Error setting audio session name: %s\n",
+ mp_HRESULT_to_str(hr));
+ return;
+}
+
+static void init_volume_control(struct wasapi_state *state)
+{
+ HRESULT hr;
+ if (state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
+ MP_DBG(state, "Activating pEndpointVolume interface\n");
+ hr = IMMDeviceActivator_Activate(state->pDevice,
+ &IID_IAudioEndpointVolume,
+ CLSCTX_ALL, NULL,
+ (void **)&state->pEndpointVolume);
+ EXIT_ON_ERROR(hr);
+
+ MP_DBG(state, "IAudioEndpointVolume::QueryHardwareSupport\n");
+ hr = IAudioEndpointVolume_QueryHardwareSupport(state->pEndpointVolume,
+ &state->vol_hw_support);
+ EXIT_ON_ERROR(hr);
+ } else {
+ MP_DBG(state, "IAudioClient::Initialize pAudioVolume\n");
+ hr = IAudioClient_GetService(state->pAudioClient,
+ &IID_ISimpleAudioVolume,
+ (void **)&state->pAudioVolume);
+ EXIT_ON_ERROR(hr);
+ }
+ return;
+exit_label:
+ state->vol_hw_support = 0;
+ SAFE_RELEASE(state->pEndpointVolume);
+ SAFE_RELEASE(state->pAudioVolume);
+ MP_WARN(state, "Error setting up volume control: %s\n",
+ mp_HRESULT_to_str(hr));
+}
+
+static HRESULT fix_format(struct ao *ao, bool align_hack)
+{
+ struct wasapi_state *state = ao->priv;
+
+ MP_DBG(state, "IAudioClient::GetDevicePeriod\n");
+ REFERENCE_TIME devicePeriod;
+ HRESULT hr = IAudioClient_GetDevicePeriod(state->pAudioClient,&devicePeriod,
+ NULL);
+ MP_VERBOSE(state, "Device period: %.2g ms\n",
+ (double) devicePeriod / 10000.0 );
+
+ REFERENCE_TIME bufferDuration = devicePeriod;
+ if (state->share_mode == AUDCLNT_SHAREMODE_SHARED) {
+ // for shared mode, use integer multiple of device period close to 50ms
+ bufferDuration = devicePeriod * ceil(50.0 * 10000.0 / devicePeriod);
+ }
+
+ // handle unsupported buffer size if AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED was
+ // returned in a previous attempt. hopefully this shouldn't happen because
+ // of the above integer device period
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/dd370875%28v=vs.85%29.aspx
+ if (align_hack) {
+ bufferDuration = (REFERENCE_TIME) (0.5 +
+ (10000.0 * 1000 / state->format.Format.nSamplesPerSec
+ * state->bufferFrameCount));
+ }
+
+ REFERENCE_TIME bufferPeriod =
+ state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE ? bufferDuration : 0;
+
+ MP_DBG(state, "IAudioClient::Initialize\n");
+ hr = IAudioClient_Initialize(state->pAudioClient,
+ state->share_mode,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
+ bufferDuration,
+ bufferPeriod,
+ &(state->format.Format),
+ NULL);
+ EXIT_ON_ERROR(hr);
+
+ MP_DBG(state, "IAudioClient::Initialize pRenderClient\n");
+ hr = IAudioClient_GetService(state->pAudioClient,
+ &IID_IAudioRenderClient,
+ (void **)&state->pRenderClient);
+ EXIT_ON_ERROR(hr);
+
+ MP_DBG(state, "IAudioClient::Initialize IAudioClient_SetEventHandle\n");
+ hr = IAudioClient_SetEventHandle(state->pAudioClient, state->hWake);
+ EXIT_ON_ERROR(hr);
+
+ MP_DBG(state, "IAudioClient::Initialize IAudioClient_GetBufferSize\n");
+ hr = IAudioClient_GetBufferSize(state->pAudioClient,
+ &state->bufferFrameCount);
+ EXIT_ON_ERROR(hr);
+
+ ao->device_buffer = state->bufferFrameCount;
+ bufferDuration = (REFERENCE_TIME) (0.5 +
+ (10000.0 * 1000 / state->format.Format.nSamplesPerSec
+ * state->bufferFrameCount));
+ MP_VERBOSE(state, "Buffer frame count: %"PRIu32" (%.2g ms)\n",
+ state->bufferFrameCount, (double) bufferDuration / 10000.0 );
+
+ hr = init_clock(state);
+ EXIT_ON_ERROR(hr);
+
+ init_session_display(state, ao->client_name);
+ init_volume_control(state);
+
+#if !HAVE_UWP
+ state->hTask = AvSetMmThreadCharacteristics(L"Pro Audio", &(DWORD){0});
+ if (!state->hTask) {
+ MP_WARN(state, "Failed to set AV thread to Pro Audio: %s\n",
+ mp_LastError_to_str());
+ }
+#endif
+
+ return S_OK;
+exit_label:
+ MP_ERR(state, "Error initializing device: %s\n", mp_HRESULT_to_str(hr));
+ return hr;
+}
+
+struct device_desc {
+ LPWSTR deviceID;
+ char *id;
+ char *name;
+};
+
+static char* get_device_name(struct mp_log *l, void *talloc_ctx, IMMDevice *pDevice)
+{
+ char *namestr = NULL;
+ IPropertyStore *pProps = NULL;
+ PROPVARIANT devname;
+ PropVariantInit(&devname);
+
+ HRESULT hr = IMMDevice_OpenPropertyStore(pDevice, STGM_READ, &pProps);
+ EXIT_ON_ERROR(hr);
+
+ hr = IPropertyStore_GetValue(pProps, &mp_PKEY_Device_FriendlyName,
+ &devname);
+ EXIT_ON_ERROR(hr);
+
+ namestr = mp_to_utf8(talloc_ctx, devname.pwszVal);
+
+exit_label:
+ if (FAILED(hr))
+ mp_warn(l, "Failed getting device name: %s\n", mp_HRESULT_to_str(hr));
+ PropVariantClear(&devname);
+ SAFE_RELEASE(pProps);
+ return namestr ? namestr : talloc_strdup(talloc_ctx, "");
+}
+
+static struct device_desc *get_device_desc(struct mp_log *l, IMMDevice *pDevice)
+{
+ LPWSTR deviceID;
+ HRESULT hr = IMMDevice_GetId(pDevice, &deviceID);
+ if (FAILED(hr)) {
+ mp_err(l, "Failed getting device id: %s\n", mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+ struct device_desc *d = talloc_zero(NULL, struct device_desc);
+ d->deviceID = talloc_memdup(d, deviceID,
+ (wcslen(deviceID) + 1) * sizeof(wchar_t));
+ SAFE_DESTROY(deviceID, CoTaskMemFree(deviceID));
+
+ char *full_id = mp_to_utf8(NULL, d->deviceID);
+ bstr id = bstr0(full_id);
+ bstr_eatstart0(&id, "{0.0.0.00000000}.");
+ d->id = bstrdup0(d, id);
+ talloc_free(full_id);
+
+ d->name = get_device_name(l, d, pDevice);
+ return d;
+}
+
+struct enumerator {
+ struct mp_log *log;
+ IMMDeviceEnumerator *pEnumerator;
+ IMMDeviceCollection *pDevices;
+ UINT count;
+};
+
+static void destroy_enumerator(struct enumerator *e)
+{
+ if (!e)
+ return;
+ SAFE_RELEASE(e->pDevices);
+ SAFE_RELEASE(e->pEnumerator);
+ talloc_free(e);
+}
+
+static struct enumerator *create_enumerator(struct mp_log *log)
+{
+ struct enumerator *e = talloc_zero(NULL, struct enumerator);
+ e->log = log;
+ HRESULT hr = CoCreateInstance(
+ &CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &IID_IMMDeviceEnumerator,
+ (void **)&e->pEnumerator);
+ EXIT_ON_ERROR(hr);
+
+ hr = IMMDeviceEnumerator_EnumAudioEndpoints(
+ e->pEnumerator, eRender, DEVICE_STATE_ACTIVE, &e->pDevices);
+ EXIT_ON_ERROR(hr);
+
+ hr = IMMDeviceCollection_GetCount(e->pDevices, &e->count);
+ EXIT_ON_ERROR(hr);
+
+ return e;
+exit_label:
+ mp_err(log, "Error getting device enumerator: %s\n", mp_HRESULT_to_str(hr));
+ destroy_enumerator(e);
+ return NULL;
+}
+
+static struct device_desc *device_desc_for_num(struct enumerator *e, UINT i)
+{
+ IMMDevice *pDevice = NULL;
+ HRESULT hr = IMMDeviceCollection_Item(e->pDevices, i, &pDevice);
+ if (FAILED(hr)) {
+ MP_ERR(e, "Failed getting device #%d: %s\n", i, mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+ struct device_desc *d = get_device_desc(e->log, pDevice);
+ SAFE_RELEASE(pDevice);
+ return d;
+}
+
+static struct device_desc *default_device_desc(struct enumerator *e)
+{
+ IMMDevice *pDevice = NULL;
+ HRESULT hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(
+ e->pEnumerator, eRender, eMultimedia, &pDevice);
+ if (FAILED(hr)) {
+ MP_ERR(e, "Error from GetDefaultAudioEndpoint: %s\n",
+ mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+ struct device_desc *d = get_device_desc(e->log, pDevice);
+ SAFE_RELEASE(pDevice);
+ return d;
+}
+
+void wasapi_list_devs(struct ao *ao, struct ao_device_list *list)
+{
+ struct enumerator *enumerator = create_enumerator(ao->log);
+ if (!enumerator)
+ return;
+
+ for (UINT i = 0; i < enumerator->count; i++) {
+ struct device_desc *d = device_desc_for_num(enumerator, i);
+ if (!d)
+ goto exit_label;
+ ao_device_list_add(list, ao, &(struct ao_device_desc){d->id, d->name});
+ talloc_free(d);
+ }
+
+exit_label:
+ destroy_enumerator(enumerator);
+}
+
+static bool load_device(struct mp_log *l,
+ IMMDevice **ppDevice, LPWSTR deviceID)
+{
+ IMMDeviceEnumerator *pEnumerator = NULL;
+ HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
+ &IID_IMMDeviceEnumerator,
+ (void **)&pEnumerator);
+ EXIT_ON_ERROR(hr);
+
+ hr = IMMDeviceEnumerator_GetDevice(pEnumerator, deviceID, ppDevice);
+ EXIT_ON_ERROR(hr);
+
+exit_label:
+ if (FAILED(hr))
+ mp_err(l, "Error loading selected device: %s\n", mp_HRESULT_to_str(hr));
+ SAFE_RELEASE(pEnumerator);
+ return SUCCEEDED(hr);
+}
+
+static LPWSTR select_device(struct mp_log *l, struct device_desc *d)
+{
+ if (!d)
+ return NULL;
+ mp_verbose(l, "Selecting device \'%s\' (%s)\n", d->id, d->name);
+ return talloc_memdup(NULL, d->deviceID,
+ (wcslen(d->deviceID) + 1) * sizeof(wchar_t));
+}
+
+bstr wasapi_get_specified_device_string(struct ao *ao)
+{
+ return bstr_strip(bstr0(ao->device));
+}
+
+LPWSTR wasapi_find_deviceID(struct ao *ao)
+{
+ LPWSTR deviceID = NULL;
+ bstr device = wasapi_get_specified_device_string(ao);
+ MP_DBG(ao, "Find device \'%.*s\'\n", BSTR_P(device));
+
+ struct device_desc *d = NULL;
+ struct enumerator *enumerator = create_enumerator(ao->log);
+ if (!enumerator)
+ goto exit_label;
+
+ if (!enumerator->count) {
+ MP_ERR(ao, "There are no playback devices available\n");
+ goto exit_label;
+ }
+
+ if (!device.len) {
+ MP_VERBOSE(ao, "No device specified. Selecting default.\n");
+ d = default_device_desc(enumerator);
+ deviceID = select_device(ao->log, d);
+ goto exit_label;
+ }
+
+ // try selecting by number
+ bstr rest;
+ long long devno = bstrtoll(device, &rest, 10);
+ if (!rest.len && 0 <= devno && devno < (long long)enumerator->count) {
+ MP_VERBOSE(ao, "Selecting device by number: #%lld\n", devno);
+ d = device_desc_for_num(enumerator, devno);
+ deviceID = select_device(ao->log, d);
+ goto exit_label;
+ }
+
+ // select by id or name
+ bstr_eatstart0(&device, "{0.0.0.00000000}.");
+ for (UINT i = 0; i < enumerator->count; i++) {
+ d = device_desc_for_num(enumerator, i);
+ if (!d)
+ goto exit_label;
+
+ if (bstrcmp(device, bstr_strip(bstr0(d->id))) == 0) {
+ MP_VERBOSE(ao, "Selecting device by id: \'%.*s\'\n", BSTR_P(device));
+ deviceID = select_device(ao->log, d);
+ goto exit_label;
+ }
+
+ if (bstrcmp(device, bstr_strip(bstr0(d->name))) == 0) {
+ if (!deviceID) {
+ MP_VERBOSE(ao, "Selecting device by name: \'%.*s\'\n", BSTR_P(device));
+ deviceID = select_device(ao->log, d);
+ } else {
+ MP_WARN(ao, "Multiple devices matched \'%.*s\'."
+ "Ignoring device \'%s\' (%s).\n",
+ BSTR_P(device), d->id, d->name);
+ }
+ }
+ SAFE_DESTROY(d, talloc_free(d));
+ }
+
+ if (!deviceID)
+ MP_ERR(ao, "Failed to find device \'%.*s\'\n", BSTR_P(device));
+
+exit_label:
+ talloc_free(d);
+ destroy_enumerator(enumerator);
+ return deviceID;
+}
+
+bool wasapi_thread_init(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ MP_DBG(ao, "Init wasapi thread\n");
+ int64_t retry_wait = MP_TIME_US_TO_NS(1);
+ bool align_hack = false;
+ HRESULT hr;
+
+ ao->format = af_fmt_from_planar(ao->format);
+
+retry:
+ if (state->deviceID) {
+ if (!load_device(ao->log, &state->pDevice, state->deviceID))
+ return false;
+
+ MP_DBG(ao, "Activating pAudioClient interface\n");
+ hr = IMMDeviceActivator_Activate(state->pDevice, &IID_IAudioClient,
+ CLSCTX_ALL, NULL,
+ (void **)&state->pAudioClient);
+ if (FAILED(hr)) {
+ MP_FATAL(ao, "Error activating device: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+ } else {
+ MP_VERBOSE(ao, "Trying UWP wrapper.\n");
+
+ HRESULT (*wuCreateDefaultAudioRenderer)(IUnknown **res) = NULL;
+ HANDLE lib = LoadLibraryW(L"wasapiuwp2.dll");
+ if (!lib) {
+ MP_ERR(ao, "Wrapper not found: %d\n", (int)GetLastError());
+ return false;
+ }
+
+ wuCreateDefaultAudioRenderer =
+ (void*)GetProcAddress(lib, "wuCreateDefaultAudioRenderer");
+ if (!wuCreateDefaultAudioRenderer) {
+ MP_ERR(ao, "Function not found.\n");
+ return false;
+ }
+ IUnknown *res = NULL;
+ hr = wuCreateDefaultAudioRenderer(&res);
+ MP_VERBOSE(ao, "Device: %s %p\n", mp_HRESULT_to_str(hr), res);
+ if (FAILED(hr)) {
+ MP_FATAL(ao, "Error activating device: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+ hr = IUnknown_QueryInterface(res, &IID_IAudioClient,
+ (void **)&state->pAudioClient);
+ IUnknown_Release(res);
+ if (FAILED(hr)) {
+ MP_FATAL(ao, "Failed to get UWP audio client: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+ }
+
+ // In the event of an align hack, we've already done this.
+ if (!align_hack) {
+ MP_DBG(ao, "Probing formats\n");
+ if (!find_formats(ao))
+ return false;
+ }
+
+ MP_DBG(ao, "Fixing format\n");
+ hr = fix_format(ao, align_hack);
+ switch (hr) {
+ case AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED:
+ if (align_hack) {
+ MP_FATAL(ao, "Align hack failed\n");
+ break;
+ }
+ // According to MSDN, we must use this as base after the failure.
+ hr = IAudioClient_GetBufferSize(state->pAudioClient,
+ &state->bufferFrameCount);
+ if (FAILED(hr)) {
+ MP_FATAL(ao, "Error getting buffer size for align hack: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+ wasapi_thread_uninit(ao);
+ align_hack = true;
+ MP_WARN(ao, "This appears to require a weird Windows 7 hack. Retrying.\n");
+ goto retry;
+ case AUDCLNT_E_DEVICE_IN_USE:
+ case AUDCLNT_E_DEVICE_INVALIDATED:
+ if (retry_wait > MP_TIME_US_TO_NS(8)) {
+ MP_FATAL(ao, "Bad device retry failed\n");
+ return false;
+ }
+ wasapi_thread_uninit(ao);
+ MP_WARN(ao, "Retrying in %"PRId64" ns\n", retry_wait);
+ mp_sleep_ns(retry_wait);
+ retry_wait *= 2;
+ goto retry;
+ }
+ return SUCCEEDED(hr);
+}
+
+void wasapi_thread_uninit(struct ao *ao)
+{
+ struct wasapi_state *state = ao->priv;
+ MP_DBG(ao, "Thread shutdown\n");
+
+ if (state->pAudioClient)
+ IAudioClient_Stop(state->pAudioClient);
+
+ SAFE_RELEASE(state->pRenderClient);
+ SAFE_RELEASE(state->pAudioClock);
+ SAFE_RELEASE(state->pAudioVolume);
+ SAFE_RELEASE(state->pEndpointVolume);
+ SAFE_RELEASE(state->pSessionControl);
+ SAFE_RELEASE(state->pAudioClient);
+ SAFE_RELEASE(state->pDevice);
+#if !HAVE_UWP
+ SAFE_DESTROY(state->hTask, AvRevertMmThreadCharacteristics(state->hTask));
+#endif
+ MP_DBG(ao, "Thread uninit done\n");
+}
diff --git a/audio/out/buffer.c b/audio/out/buffer.c
new file mode 100644
index 0000000..5b8b523
--- /dev/null
+++ b/audio/out/buffer.c
@@ -0,0 +1,736 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <inttypes.h>
+#include <math.h>
+#include <unistd.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "ao.h"
+#include "internal.h"
+#include "audio/aframe.h"
+#include "audio/format.h"
+
+#include "common/msg.h"
+#include "common/common.h"
+
+#include "filters/f_async_queue.h"
+#include "filters/filter_internal.h"
+
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+
+struct buffer_state {
+ // Buffer and AO
+ mp_mutex lock;
+ mp_cond wakeup;
+
+ // Playthread sleep
+ mp_mutex pt_lock;
+ mp_cond pt_wakeup;
+
+ // Access from AO driver's thread only.
+ char *convert_buffer;
+
+ // Immutable.
+ struct mp_async_queue *queue;
+
+ // --- protected by lock
+
+ struct mp_filter *filter_root;
+ struct mp_filter *input; // connected to queue
+ struct mp_aframe *pending; // last, not fully consumed output
+
+ bool streaming; // AO streaming active
+ bool playing; // logically playing audio from buffer
+ bool paused; // logically paused
+
+ int64_t end_time_ns; // absolute output time of last played sample
+
+ bool initial_unblocked;
+
+ // "Push" AOs only (AOs with driver->write).
+ bool hw_paused; // driver->set_pause() was used successfully
+ bool recover_pause; // non-hw_paused: needs to recover delay
+ struct mp_pcm_state prepause_state;
+ mp_thread thread; // thread shoveling data to AO
+ bool thread_valid; // thread is running
+ struct mp_aframe *temp_buf;
+
+ // --- protected by pt_lock
+ bool need_wakeup;
+ bool terminate; // exit thread
+};
+
+static MP_THREAD_VOID playthread(void *arg);
+
+void ao_wakeup_playthread(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+ mp_mutex_lock(&p->pt_lock);
+ p->need_wakeup = true;
+ mp_cond_broadcast(&p->pt_wakeup);
+ mp_mutex_unlock(&p->pt_lock);
+}
+
+// called locked
+static void get_dev_state(struct ao *ao, struct mp_pcm_state *state)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ if (p->paused && p->playing && !ao->stream_silence) {
+ *state = p->prepause_state;
+ return;
+ }
+
+ *state = (struct mp_pcm_state){
+ .free_samples = -1,
+ .queued_samples = -1,
+ .delay = -1,
+ };
+ ao->driver->get_state(ao, state);
+}
+
+struct mp_async_queue *ao_get_queue(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+ return p->queue;
+}
+
+// Special behavior with data==NULL: caller uses p->pending.
+static int read_buffer(struct ao *ao, void **data, int samples, bool *eof,
+ bool pad_silence)
+{
+ struct buffer_state *p = ao->buffer_state;
+ int pos = 0;
+ *eof = false;
+
+ while (p->playing && !p->paused && pos < samples) {
+ if (!p->pending || !mp_aframe_get_size(p->pending)) {
+ TA_FREEP(&p->pending);
+ struct mp_frame frame = mp_pin_out_read(p->input->pins[0]);
+ if (!frame.type)
+ break; // we can't/don't want to block
+ if (frame.type != MP_FRAME_AUDIO) {
+ if (frame.type == MP_FRAME_EOF)
+ *eof = true;
+ mp_frame_unref(&frame);
+ continue;
+ }
+ p->pending = frame.data;
+ }
+
+ if (!data)
+ break;
+
+ int copy = mp_aframe_get_size(p->pending);
+ uint8_t **fdata = mp_aframe_get_data_ro(p->pending);
+ copy = MPMIN(copy, samples - pos);
+ for (int n = 0; n < ao->num_planes; n++) {
+ memcpy((char *)data[n] + pos * ao->sstride,
+ fdata[n], copy * ao->sstride);
+ }
+ mp_aframe_skip_samples(p->pending, copy);
+ pos += copy;
+ *eof = false;
+ }
+
+ if (!data) {
+ if (!p->pending)
+ return 0;
+ void **pd = (void *)mp_aframe_get_data_rw(p->pending);
+ if (pd)
+ ao_post_process_data(ao, pd, mp_aframe_get_size(p->pending));
+ return 1;
+ }
+
+ // pad with silence (underflow/paused/eof)
+ if (pad_silence) {
+ for (int n = 0; n < ao->num_planes; n++) {
+ af_fill_silence((char *)data[n] + pos * ao->sstride,
+ (samples - pos) * ao->sstride,
+ ao->format);
+ }
+ }
+
+ ao_post_process_data(ao, data, pos);
+ return pos;
+}
+
+static int ao_read_data_unlocked(struct ao *ao, void **data, int samples,
+ int64_t out_time_ns, bool pad_silence)
+{
+ struct buffer_state *p = ao->buffer_state;
+ assert(!ao->driver->write);
+
+ int pos = read_buffer(ao, data, samples, &(bool){0}, pad_silence);
+
+ if (pos > 0)
+ p->end_time_ns = out_time_ns;
+
+ if (pos < samples && p->playing && !p->paused) {
+ p->playing = false;
+ ao->wakeup_cb(ao->wakeup_ctx);
+ // For ao_drain().
+ mp_cond_broadcast(&p->wakeup);
+ }
+
+ return pos;
+}
+
+// Read the given amount of samples in the user-provided data buffer. Returns
+// the number of samples copied. If there is not enough data (buffer underrun
+// or EOF), return the number of samples that could be copied, and fill the
+// rest of the user-provided buffer with silence.
+// This basically assumes that the audio device doesn't care about underruns.
+// If this is called in paused mode, it will always return 0.
+// The caller should set out_time_ns to the expected delay until the last sample
+// reaches the speakers, in nanoseconds, using mp_time_ns() as reference.
+int ao_read_data(struct ao *ao, void **data, int samples, int64_t out_time_ns)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ mp_mutex_lock(&p->lock);
+
+ int pos = ao_read_data_unlocked(ao, data, samples, out_time_ns, true);
+
+ mp_mutex_unlock(&p->lock);
+
+ return pos;
+}
+
+// Like ao_read_data() but does not block and also may return partial data.
+// Callers have to check the return value.
+int ao_read_data_nonblocking(struct ao *ao, void **data, int samples, int64_t out_time_ns)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ if (mp_mutex_trylock(&p->lock))
+ return 0;
+
+ int pos = ao_read_data_unlocked(ao, data, samples, out_time_ns, false);
+
+ mp_mutex_unlock(&p->lock);
+
+ return pos;
+}
+
+// Same as ao_read_data(), but convert data according to *fmt.
+// fmt->src_fmt and fmt->channels must be the same as the AO parameters.
+int ao_read_data_converted(struct ao *ao, struct ao_convert_fmt *fmt,
+ void **data, int samples, int64_t out_time_ns)
+{
+ struct buffer_state *p = ao->buffer_state;
+ void *ndata[MP_NUM_CHANNELS] = {0};
+
+ if (!ao_need_conversion(fmt))
+ return ao_read_data(ao, data, samples, out_time_ns);
+
+ assert(ao->format == fmt->src_fmt);
+ assert(ao->channels.num == fmt->channels);
+
+ bool planar = af_fmt_is_planar(fmt->src_fmt);
+ int planes = planar ? fmt->channels : 1;
+ int plane_samples = samples * (planar ? 1: fmt->channels);
+ int src_plane_size = plane_samples * af_fmt_to_bytes(fmt->src_fmt);
+ int dst_plane_size = plane_samples * fmt->dst_bits / 8;
+
+ int needed = src_plane_size * planes;
+ if (needed > talloc_get_size(p->convert_buffer) || !p->convert_buffer) {
+ talloc_free(p->convert_buffer);
+ p->convert_buffer = talloc_size(NULL, needed);
+ }
+
+ for (int n = 0; n < planes; n++)
+ ndata[n] = p->convert_buffer + n * src_plane_size;
+
+ int res = ao_read_data(ao, ndata, samples, out_time_ns);
+
+ ao_convert_inplace(fmt, ndata, samples);
+ for (int n = 0; n < planes; n++)
+ memcpy(data[n], ndata[n], dst_plane_size);
+
+ return res;
+}
+
+int ao_control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+ struct buffer_state *p = ao->buffer_state;
+ int r = CONTROL_UNKNOWN;
+ if (ao->driver->control) {
+ // Only need to lock in push mode.
+ if (ao->driver->write)
+ mp_mutex_lock(&p->lock);
+
+ r = ao->driver->control(ao, cmd, arg);
+
+ if (ao->driver->write)
+ mp_mutex_unlock(&p->lock);
+ }
+ return r;
+}
+
+double ao_get_delay(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ mp_mutex_lock(&p->lock);
+
+ double driver_delay;
+ if (ao->driver->write) {
+ struct mp_pcm_state state;
+ get_dev_state(ao, &state);
+ driver_delay = state.delay;
+ } else {
+ int64_t end = p->end_time_ns;
+ int64_t now = mp_time_ns();
+ driver_delay = MPMAX(0, MP_TIME_NS_TO_S(end - now));
+ }
+
+ int pending = mp_async_queue_get_samples(p->queue);
+ if (p->pending)
+ pending += mp_aframe_get_size(p->pending);
+
+ mp_mutex_unlock(&p->lock);
+ return driver_delay + pending / (double)ao->samplerate;
+}
+
+// Fully stop playback; clear buffers, including queue.
+void ao_reset(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+ bool wakeup = false;
+ bool do_reset = false;
+
+ mp_mutex_lock(&p->lock);
+
+ TA_FREEP(&p->pending);
+ mp_async_queue_reset(p->queue);
+ mp_filter_reset(p->filter_root);
+ mp_async_queue_resume_reading(p->queue);
+
+ if (!ao->stream_silence && ao->driver->reset) {
+ if (ao->driver->write) {
+ ao->driver->reset(ao);
+ } else {
+ // Pull AOs may wait for ao_read_data() to return.
+ // That would deadlock if called from within the lock.
+ do_reset = true;
+ }
+ p->streaming = false;
+ }
+ wakeup = p->playing;
+ p->playing = false;
+ p->recover_pause = false;
+ p->hw_paused = false;
+ p->end_time_ns = 0;
+
+ mp_mutex_unlock(&p->lock);
+
+ if (do_reset)
+ ao->driver->reset(ao);
+
+ if (wakeup)
+ ao_wakeup_playthread(ao);
+}
+
+// Initiate playback. This moves from the stop/underrun state to actually
+// playing (orthogonally taking the paused state into account). Plays all
+// data in the queue, and goes into underrun state if no more data available.
+// No-op if already running.
+void ao_start(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+ bool do_start = false;
+
+ mp_mutex_lock(&p->lock);
+
+ p->playing = true;
+
+ if (!ao->driver->write && !p->paused && !p->streaming) {
+ p->streaming = true;
+ do_start = true;
+ }
+
+ mp_mutex_unlock(&p->lock);
+
+ // Pull AOs might call ao_read_data() so do this outside the lock.
+ if (do_start)
+ ao->driver->start(ao);
+
+ ao_wakeup_playthread(ao);
+}
+
+void ao_set_paused(struct ao *ao, bool paused, bool eof)
+{
+ struct buffer_state *p = ao->buffer_state;
+ bool wakeup = false;
+ bool do_reset = false, do_start = false;
+
+ // If we are going to pause on eof and ao is still playing,
+ // be sure to drain the ao first for gapless.
+ if (eof && paused && ao_is_playing(ao))
+ ao_drain(ao);
+
+ mp_mutex_lock(&p->lock);
+
+ if ((p->playing || !ao->driver->write) && !p->paused && paused) {
+ if (p->streaming && !ao->stream_silence) {
+ if (ao->driver->write) {
+ if (!p->recover_pause)
+ get_dev_state(ao, &p->prepause_state);
+ if (ao->driver->set_pause && ao->driver->set_pause(ao, true)) {
+ p->hw_paused = true;
+ } else {
+ ao->driver->reset(ao);
+ p->streaming = false;
+ p->recover_pause = !ao->untimed;
+ }
+ } else if (ao->driver->reset) {
+ // See ao_reset() why this is done outside of the lock.
+ do_reset = true;
+ p->streaming = false;
+ }
+ }
+ wakeup = true;
+ } else if (p->playing && p->paused && !paused) {
+ if (ao->driver->write) {
+ if (p->hw_paused)
+ ao->driver->set_pause(ao, false);
+ p->hw_paused = false;
+ } else {
+ if (!p->streaming)
+ do_start = true;
+ p->streaming = true;
+ }
+ wakeup = true;
+ }
+ p->paused = paused;
+
+ mp_mutex_unlock(&p->lock);
+
+ if (do_reset)
+ ao->driver->reset(ao);
+ if (do_start)
+ ao->driver->start(ao);
+
+ if (wakeup)
+ ao_wakeup_playthread(ao);
+}
+
+// Whether audio is playing. This means that there is still data in the buffers,
+// and ao_start() was called. This returns true even if playback was logically
+// paused. On false, EOF was reached, or an underrun happened, or ao_reset()
+// was called.
+bool ao_is_playing(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ mp_mutex_lock(&p->lock);
+ bool playing = p->playing;
+ mp_mutex_unlock(&p->lock);
+
+ return playing;
+}
+
+// Block until the current audio buffer has played completely.
+void ao_drain(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ mp_mutex_lock(&p->lock);
+ while (!p->paused && p->playing) {
+ mp_mutex_unlock(&p->lock);
+ double delay = ao_get_delay(ao);
+ mp_mutex_lock(&p->lock);
+
+ // Limit to buffer + arbitrary ~250ms max. waiting for robustness.
+ delay += mp_async_queue_get_samples(p->queue) / (double)ao->samplerate;
+
+ // Wait for EOF signal from AO.
+ if (mp_cond_timedwait(&p->wakeup, &p->lock,
+ MP_TIME_S_TO_NS(MPMAX(delay, 0) + 0.25)))
+ {
+ MP_VERBOSE(ao, "drain timeout\n");
+ break;
+ }
+
+ if (!p->playing && mp_async_queue_get_samples(p->queue)) {
+ MP_WARN(ao, "underrun during draining\n");
+ mp_mutex_unlock(&p->lock);
+ ao_start(ao);
+ mp_mutex_lock(&p->lock);
+ }
+ }
+ mp_mutex_unlock(&p->lock);
+
+ ao_reset(ao);
+}
+
+static void wakeup_filters(void *ctx)
+{
+ struct ao *ao = ctx;
+ ao_wakeup_playthread(ao);
+}
+
+void ao_uninit(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ if (p && p->thread_valid) {
+ mp_mutex_lock(&p->pt_lock);
+ p->terminate = true;
+ mp_cond_broadcast(&p->pt_wakeup);
+ mp_mutex_unlock(&p->pt_lock);
+
+ mp_thread_join(p->thread);
+ p->thread_valid = false;
+ }
+
+ if (ao->driver_initialized)
+ ao->driver->uninit(ao);
+
+ if (p) {
+ talloc_free(p->filter_root);
+ talloc_free(p->queue);
+ talloc_free(p->pending);
+ talloc_free(p->convert_buffer);
+ talloc_free(p->temp_buf);
+
+ mp_cond_destroy(&p->wakeup);
+ mp_mutex_destroy(&p->lock);
+
+ mp_cond_destroy(&p->pt_wakeup);
+ mp_mutex_destroy(&p->pt_lock);
+ }
+
+ talloc_free(ao);
+}
+
+void init_buffer_pre(struct ao *ao)
+{
+ ao->buffer_state = talloc_zero(ao, struct buffer_state);
+}
+
+bool init_buffer_post(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ assert(ao->driver->start);
+ if (ao->driver->write) {
+ assert(ao->driver->reset);
+ assert(ao->driver->get_state);
+ }
+
+ mp_mutex_init(&p->lock);
+ mp_cond_init(&p->wakeup);
+
+ mp_mutex_init(&p->pt_lock);
+ mp_cond_init(&p->pt_wakeup);
+
+ p->queue = mp_async_queue_create();
+ p->filter_root = mp_filter_create_root(ao->global);
+ p->input = mp_async_queue_create_filter(p->filter_root, MP_PIN_OUT, p->queue);
+
+ mp_async_queue_resume_reading(p->queue);
+
+ struct mp_async_queue_config cfg = {
+ .sample_unit = AQUEUE_UNIT_SAMPLES,
+ .max_samples = ao->buffer,
+ .max_bytes = INT64_MAX,
+ };
+ mp_async_queue_set_config(p->queue, cfg);
+
+ if (ao->driver->write) {
+ mp_filter_graph_set_wakeup_cb(p->filter_root, wakeup_filters, ao);
+
+ p->thread_valid = true;
+ if (mp_thread_create(&p->thread, playthread, ao)) {
+ p->thread_valid = false;
+ return false;
+ }
+ } else {
+ if (ao->stream_silence) {
+ ao->driver->start(ao);
+ p->streaming = true;
+ }
+ }
+
+ if (ao->stream_silence) {
+ MP_WARN(ao, "The --audio-stream-silence option is set. This will break "
+ "certain player behavior.\n");
+ }
+
+ return true;
+}
+
+static bool realloc_buf(struct ao *ao, int samples)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ samples = MPMAX(1, samples);
+
+ if (!p->temp_buf || samples > mp_aframe_get_size(p->temp_buf)) {
+ TA_FREEP(&p->temp_buf);
+ p->temp_buf = mp_aframe_create();
+ if (!mp_aframe_set_format(p->temp_buf, ao->format) ||
+ !mp_aframe_set_chmap(p->temp_buf, &ao->channels) ||
+ !mp_aframe_set_rate(p->temp_buf, ao->samplerate) ||
+ !mp_aframe_alloc_data(p->temp_buf, samples))
+ {
+ TA_FREEP(&p->temp_buf);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// called locked
+static bool ao_play_data(struct ao *ao)
+{
+ struct buffer_state *p = ao->buffer_state;
+
+ if ((!p->playing || p->paused) && !ao->stream_silence)
+ return false;
+
+ struct mp_pcm_state state;
+ get_dev_state(ao, &state);
+
+ if (p->streaming && !state.playing && !ao->untimed)
+ goto eof;
+
+ void **planes = NULL;
+ int space = state.free_samples;
+ if (!space)
+ return false;
+ assert(space >= 0);
+
+ int samples = 0;
+ bool got_eof = false;
+ if (ao->driver->write_frames) {
+ TA_FREEP(&p->pending);
+ samples = read_buffer(ao, NULL, 1, &got_eof, false);
+ planes = (void **)&p->pending;
+ } else {
+ if (!realloc_buf(ao, space)) {
+ MP_ERR(ao, "Failed to allocate buffer.\n");
+ return false;
+ }
+ planes = (void **)mp_aframe_get_data_rw(p->temp_buf);
+ assert(planes);
+
+ if (p->recover_pause) {
+ samples = MPCLAMP(p->prepause_state.delay * ao->samplerate, 0, space);
+ p->recover_pause = false;
+ mp_aframe_set_silence(p->temp_buf, 0, space);
+ }
+
+ if (!samples) {
+ samples = read_buffer(ao, planes, space, &got_eof, true);
+ if (p->paused || (ao->stream_silence && !p->playing))
+ samples = space; // read_buffer() sets remainder to silent
+ }
+ }
+
+ if (samples) {
+ MP_STATS(ao, "start ao fill");
+ if (!ao->driver->write(ao, planes, samples))
+ MP_ERR(ao, "Error writing audio to device.\n");
+ MP_STATS(ao, "end ao fill");
+
+ if (!p->streaming) {
+ MP_VERBOSE(ao, "starting AO\n");
+ ao->driver->start(ao);
+ p->streaming = true;
+ state.playing = true;
+ }
+ }
+
+ MP_TRACE(ao, "in=%d space=%d(%d) pl=%d, eof=%d\n",
+ samples, space, state.free_samples, p->playing, got_eof);
+
+ if (got_eof)
+ goto eof;
+
+ return samples > 0 && (samples < space || ao->untimed);
+
+eof:
+ MP_VERBOSE(ao, "audio end or underrun\n");
+ // Normal AOs signal EOF on underrun, untimed AOs never signal underruns.
+ if (ao->untimed || !state.playing || ao->stream_silence) {
+ p->streaming = state.playing && !ao->untimed;
+ p->playing = false;
+ }
+ ao->wakeup_cb(ao->wakeup_ctx);
+ // For ao_drain().
+ mp_cond_broadcast(&p->wakeup);
+ return true;
+}
+
+static MP_THREAD_VOID playthread(void *arg)
+{
+ struct ao *ao = arg;
+ struct buffer_state *p = ao->buffer_state;
+ mp_thread_set_name("ao");
+ while (1) {
+ mp_mutex_lock(&p->lock);
+
+ bool retry = false;
+ if (!ao->driver->initially_blocked || p->initial_unblocked)
+ retry = ao_play_data(ao);
+
+ // Wait until the device wants us to write more data to it.
+ // Fallback to guessing.
+ int64_t timeout = INT64_MAX;
+ if (p->streaming && !retry && (!p->paused || ao->stream_silence)) {
+ // Wake up again if half of the audio buffer has been played.
+ // Since audio could play at a faster or slower pace, wake up twice
+ // as often as ideally needed.
+ timeout = MP_TIME_S_TO_NS(ao->device_buffer / (double)ao->samplerate * 0.25);
+ }
+
+ mp_mutex_unlock(&p->lock);
+
+ mp_mutex_lock(&p->pt_lock);
+ if (p->terminate) {
+ mp_mutex_unlock(&p->pt_lock);
+ break;
+ }
+ if (!p->need_wakeup && !retry) {
+ MP_STATS(ao, "start audio wait");
+ mp_cond_timedwait(&p->pt_wakeup, &p->pt_lock, timeout);
+ MP_STATS(ao, "end audio wait");
+ }
+ p->need_wakeup = false;
+ mp_mutex_unlock(&p->pt_lock);
+ }
+ MP_THREAD_RETURN();
+}
+
+void ao_unblock(struct ao *ao)
+{
+ if (ao->driver->write) {
+ struct buffer_state *p = ao->buffer_state;
+ mp_mutex_lock(&p->lock);
+ p->initial_unblocked = true;
+ mp_mutex_unlock(&p->lock);
+ ao_wakeup_playthread(ao);
+ }
+}
diff --git a/audio/out/internal.h b/audio/out/internal.h
new file mode 100644
index 0000000..7951b38
--- /dev/null
+++ b/audio/out/internal.h
@@ -0,0 +1,237 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_AO_INTERNAL_H_
+#define MP_AO_INTERNAL_H_
+
+#include <stdatomic.h>
+#include <stdbool.h>
+
+#include "audio/out/ao.h"
+
+/* global data used by ao.c and ao drivers */
+struct ao {
+ int samplerate;
+ struct mp_chmap channels;
+ int format; // one of AF_FORMAT_...
+ int bps; // bytes per second (per plane)
+ int sstride; // size of a sample on each plane
+ // (format_size*num_channels/num_planes)
+ int num_planes;
+ bool probing; // if true, don't fail loudly on init
+ bool untimed; // don't assume realtime playback
+ int device_buffer; // device buffer in samples (guessed by
+ // common init code if not set by driver)
+ const struct ao_driver *driver;
+ bool driver_initialized;
+ void *priv;
+ struct mpv_global *global;
+ struct encode_lavc_context *encode_lavc_ctx;
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_ctx;
+ struct mp_log *log; // Using e.g. "[ao/coreaudio]" as prefix
+ int init_flags; // AO_INIT_* flags
+ bool stream_silence; // if audio inactive, just play silence
+
+ // The device as selected by the user, usually using ao_device_desc.name
+ // from an entry from the list returned by driver->list_devices. If the
+ // default device should be used, this is set to NULL.
+ char *device;
+
+ // Application name to report to the audio API.
+ char *client_name;
+
+ // Used during init: if init fails, redirect to this ao
+ char *redirect;
+
+ // Internal events (use ao_request_reload(), ao_hotplug_event())
+ atomic_uint events_;
+
+ // Float gain multiplicator
+ _Atomic float gain;
+
+ int buffer;
+ double def_buffer;
+ struct buffer_state *buffer_state;
+};
+
+void init_buffer_pre(struct ao *ao);
+bool init_buffer_post(struct ao *ao);
+
+struct mp_pcm_state {
+ // Note: free_samples+queued_samples <= ao->device_buffer; the sum may be
+ // less if the audio API can report partial periods played, while
+ // free_samples should be period-size aligned. If free_samples is not
+ // period-size aligned, the AO thread might get into a situation where
+ // it writes a very small number of samples in each iteration, leading
+ // to extremely inefficient behavior.
+ // Keep in mind that write() may write less than free_samples (or your
+ // period size alignment) anyway.
+ int free_samples; // number of free space in ring buffer
+ int queued_samples; // number of samples to play in ring buffer
+ double delay; // total latency in seconds (includes queued_samples)
+ bool playing; // set if underlying API is actually playing audio;
+ // the AO must unset it on underrun (accidental
+ // underrun and EOF are indistinguishable; the upper
+ // layers decide what it was)
+ // real pausing may assume playing=true
+};
+
+/* Note:
+ *
+ * In general, there are two types of audio drivers:
+ * a) push based (the user queues data that should be played)
+ * b) pull callback based (the audio API calls a callback to get audio)
+ *
+ * The ao.c code can handle both. It basically implements two audio paths
+ * and provides a uniform API for them. If ao_driver->write is NULL, it assumes
+ * that the driver uses a callback based audio API, otherwise push based.
+ *
+ * Requirements:
+ * a+b) Mandatory for both types:
+ * init
+ * uninit
+ * start
+ * Optional for both types:
+ * control
+ * a) ->write is called to queue audio. push.c creates a thread to regularly
+ * refill audio device buffers with ->write, but all driver functions are
+ * always called under an exclusive lock.
+ * Mandatory:
+ * reset
+ * write
+ * get_state
+ * Optional:
+ * set_pause
+ * b) ->write must be NULL. ->start must be provided, and should make the
+ * audio API start calling the audio callback. Your audio callback should
+ * in turn call ao_read_data() to get audio data. Most functions are
+ * optional and will be emulated if missing (e.g. pausing is emulated as
+ * silence).
+ * Also, the following optional callbacks can be provided:
+ * reset (stops the audio callback, start() restarts it)
+ */
+struct ao_driver {
+ // If true, use with encoding only.
+ bool encode;
+ // Name used for --ao.
+ const char *name;
+ // Description shown with --ao=help.
+ const char *description;
+ // This requires waiting for a AO_EVENT_INITIAL_UNBLOCK event before the
+ // first write() call is done. Encode mode uses this, and push mode
+ // respects it automatically (don't use with pull mode).
+ bool initially_blocked;
+ // If true, write units of entire frames. The write() call is modified to
+ // use data==mp_aframe. Useful for encoding AO only.
+ bool write_frames;
+ // Init the device using ao->format/ao->channels/ao->samplerate. If the
+ // device doesn't accept these parameters, you can attempt to negotiate
+ // fallback parameters, and set the ao format fields accordingly.
+ int (*init)(struct ao *ao);
+ // Optional. See ao_control() etc. in ao.c
+ int (*control)(struct ao *ao, enum aocontrol cmd, void *arg);
+ void (*uninit)(struct ao *ao);
+ // Stop all audio playback, clear buffers, back to state after init().
+ // Optional for pull AOs.
+ void (*reset)(struct ao *ao);
+ // push based: set pause state. Only called after start() and before reset().
+ // returns success (this is intended for paused=true; if it
+ // returns false, playback continues, and the core emulates via
+ // reset(); unpausing always works)
+ // The pausing state is also cleared by reset().
+ bool (*set_pause)(struct ao *ao, bool paused);
+ // pull based: start the audio callback
+ // push based: start playing queued data
+ // AO should call ao_wakeup_playthread() if a period boundary
+ // is crossed, or playback stops due to external reasons
+ // (including underruns or device removal)
+ // must set mp_pcm_state.playing; unset on error/underrun/end
+ void (*start)(struct ao *ao);
+ // push based: queue new data. This won't try to write more data than the
+ // reported free space (samples <= mp_pcm_state.free_samples).
+ // This must NOT start playback. start() does that, and write() may be
+ // called multiple times before start() is called. It may also happen that
+ // reset() is called to discard the buffer. start() without write() will
+ // immediately reported an underrun.
+ // Return false on failure.
+ bool (*write)(struct ao *ao, void **data, int samples);
+ // push based: return mandatory stream information
+ void (*get_state)(struct ao *ao, struct mp_pcm_state *state);
+
+ // Return the list of devices currently available in the system. Use
+ // ao_device_list_add() to add entries. The selected device will be set as
+ // ao->device (using ao_device_desc.name).
+ // Warning: the ao struct passed is not initialized with ao_driver->init().
+ // Instead, hotplug_init/hotplug_uninit is called. If these
+ // callbacks are not set, no driver initialization call is done
+ // on the ao struct.
+ void (*list_devs)(struct ao *ao, struct ao_device_list *list);
+
+ // If set, these are called before/after ao_driver->list_devs is called.
+ // It is also assumed that the driver can do hotplugging - which means
+ // it is expected to call ao_hotplug_event(ao) whenever the system's
+ // audio device list changes. The player will then call list_devs() again.
+ int (*hotplug_init)(struct ao *ao);
+ void (*hotplug_uninit)(struct ao *ao);
+
+ // For option parsing (see vo.h)
+ int priv_size;
+ const void *priv_defaults;
+ const struct m_option *options;
+ const char *options_prefix;
+ const struct m_sub_options *global_opts;
+};
+
+// These functions can be called by AOs.
+
+int ao_read_data(struct ao *ao, void **data, int samples, int64_t out_time_ns);
+MP_WARN_UNUSED_RESULT
+int ao_read_data_nonblocking(struct ao *ao, void **data, int samples, int64_t out_time_ns);
+
+bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s,
+ struct mp_chmap *map);
+bool ao_chmap_sel_adjust2(struct ao *ao, const struct mp_chmap_sel *s,
+ struct mp_chmap *map, bool safe_multichannel);
+bool ao_chmap_sel_get_def(struct ao *ao, const struct mp_chmap_sel *s,
+ struct mp_chmap *map, int num);
+
+// Add a deep copy of e to the list.
+// Call from ao_driver->list_devs callback only.
+void ao_device_list_add(struct ao_device_list *list, struct ao *ao,
+ struct ao_device_desc *e);
+
+void ao_post_process_data(struct ao *ao, void **data, int num_samples);
+
+struct ao_convert_fmt {
+ int src_fmt; // source AF_FORMAT_*
+ int channels; // number of channels
+ int dst_bits; // total target data sample size
+ int pad_msb; // padding in the MSB (i.e. required shifting)
+ int pad_lsb; // padding in LSB (required 0 bits) (ignored)
+};
+
+bool ao_can_convert_inplace(struct ao_convert_fmt *fmt);
+bool ao_need_conversion(struct ao_convert_fmt *fmt);
+void ao_convert_inplace(struct ao_convert_fmt *fmt, void **data, int num_samples);
+
+void ao_wakeup_playthread(struct ao *ao);
+
+int ao_read_data_converted(struct ao *ao, struct ao_convert_fmt *fmt,
+ void **data, int samples, int64_t out_time_ns);
+
+#endif
diff --git a/ci/build-freebsd.sh b/ci/build-freebsd.sh
new file mode 100755
index 0000000..bc68a25
--- /dev/null
+++ b/ci/build-freebsd.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+export CFLAGS="$CFLAGS -isystem/usr/local/include"
+export CXXFLAGS="$CXXFLAGS -isystem/usr/local/include"
+export LDFLAGS="$LDFLAGS -L/usr/local/lib"
+
+meson setup build \
+ --werror \
+ -Dlibplacebo:werror=false \
+ -Dc_args="-Wno-error=deprecated -Wno-error=deprecated-declarations" \
+ -Diconv=disabled \
+ -Dlibmpv=true \
+ -Dlua=enabled \
+ -Degl-drm=enabled \
+ -Dopenal=enabled \
+ -Dsndio=enabled \
+ -Dtests=true \
+ -Dvdpau=enabled \
+ -Dvulkan=enabled \
+ -Doss-audio=enabled \
+ $(pkg info -q v4l_compat && echo -Ddvbin=enabled) \
+ $(pkg info -q libdvdnav && echo -Ddvdnav=enabled) \
+ $(pkg info -q libcdio-paranoia && echo -Dcdda=enabled) \
+ $(pkg info -q pipewire && echo -Dpipewire=enabled) \
+ $NULL
+
+meson compile -C build
+./build/mpv -v --no-config
diff --git a/ci/build-macos.sh b/ci/build-macos.sh
new file mode 100755
index 0000000..14b3a1b
--- /dev/null
+++ b/ci/build-macos.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -e
+
+FFMPEG_SYSROOT="${HOME}/deps/sysroot"
+MPV_INSTALL_PREFIX="${HOME}/out/mpv"
+MPV_VARIANT="${TRAVIS_OS_NAME}"
+
+if [[ -d "./build/${MPV_VARIANT}" ]] ; then
+ rm -rf "./build/${MPV_VARIANT}"
+fi
+
+PKG_CONFIG_PATH="${FFMPEG_SYSROOT}/lib/pkgconfig/" CC="${CC}" CXX="${CXX}" \
+meson setup build \
+ -Dprefix="${MPV_INSTALL_PREFIX}" \
+ -D{libmpv,tests}=true \
+ -D{gl,iconv,lcms2,lua,jpeg,plain-gl,zlib}=enabled \
+ -D{cocoa,coreaudio,gl-cocoa,macos-cocoa-cb,macos-touchbar,videotoolbox-gl}=enabled
+
+meson compile -C build -j4
+meson install -C build
+./build/mpv -v --no-config
diff --git a/ci/build-mingw64.sh b/ci/build-mingw64.sh
new file mode 100755
index 0000000..adca649
--- /dev/null
+++ b/ci/build-mingw64.sh
@@ -0,0 +1,306 @@
+#!/bin/bash -e
+
+prefix_dir=$PWD/mingw_prefix
+mkdir -p "$prefix_dir"
+ln -snf . "$prefix_dir/usr"
+ln -snf . "$prefix_dir/local"
+
+wget="wget -nc --progress=bar:force"
+gitclone="git clone --depth=1 --recursive"
+
+# -posix is Ubuntu's variant with pthreads support
+export CC=$TARGET-gcc-posix
+export CXX=$TARGET-g++-posix
+export AR=$TARGET-ar
+export NM=$TARGET-nm
+export RANLIB=$TARGET-ranlib
+
+export CFLAGS="-O2 -pipe -Wall -D_FORTIFY_SOURCE=2"
+export LDFLAGS="-fstack-protector-strong"
+
+# anything that uses pkg-config
+export PKG_CONFIG_SYSROOT_DIR="$prefix_dir"
+export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig"
+
+# autotools(-like)
+commonflags="--disable-static --enable-shared"
+
+# meson
+fam=x86_64
+[[ "$TARGET" == "i686-"* ]] && fam=x86
+cat >"$prefix_dir/crossfile" <<EOF
+[built-in options]
+buildtype = 'release'
+wrap_mode = 'nodownload'
+[binaries]
+c = ['ccache', '${CC}']
+cpp = ['ccache', '${CXX}']
+ar = '${AR}'
+strip = '${TARGET}-strip'
+pkgconfig = 'pkg-config'
+windres = '${TARGET}-windres'
+dlltool = '${TARGET}-dlltool'
+[host_machine]
+system = 'windows'
+cpu_family = '${fam}'
+cpu = '${TARGET%%-*}'
+endian = 'little'
+EOF
+
+# CMake
+cmake_args=(
+ -Wno-dev
+ -DCMAKE_SYSTEM_NAME=Windows
+ -DCMAKE_FIND_ROOT_PATH="$PKG_CONFIG_SYSROOT_DIR"
+ -DCMAKE_RC_COMPILER="${TARGET}-windres"
+ -DCMAKE_BUILD_TYPE=Release
+)
+
+export CC="ccache $CC"
+export CXX="ccache $CXX"
+
+function builddir {
+ [ -d "$1/builddir" ] && rm -rf "$1/builddir"
+ mkdir -p "$1/builddir"
+ pushd "$1/builddir"
+}
+
+function makeplusinstall {
+ if [ -f build.ninja ]; then
+ ninja
+ DESTDIR="$prefix_dir" ninja install
+ else
+ make -j$(nproc)
+ make DESTDIR="$prefix_dir" install
+ fi
+}
+
+function gettar {
+ local name="${1##*/}"
+ [ -d "${name%%.*}" ] && return 0
+ $wget "$1"
+ tar -xaf "$name"
+}
+
+function build_if_missing {
+ local name=${1//-/_}
+ local mark_var=_${name}_mark
+ local mark_file=$prefix_dir/${!mark_var}
+ [ -e "$mark_file" ] && return 0
+ echo "::group::Building $1"
+ _$name
+ echo "::endgroup::"
+ if [ ! -e "$mark_file" ]; then
+ echo "Error: Build of $1 completed but $mark_file was not created."
+ return 2
+ fi
+}
+
+
+## mpv's dependencies
+
+_iconv () {
+ local ver=1.17
+ gettar "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-${ver}.tar.gz"
+ builddir libiconv-${ver}
+ ../configure --host=$TARGET $commonflags
+ makeplusinstall
+ popd
+}
+_iconv_mark=lib/libiconv.dll.a
+
+_zlib () {
+ local ver=1.3
+ gettar "https://zlib.net/fossils/zlib-${ver}.tar.gz"
+ pushd zlib-${ver}
+ make -fwin32/Makefile.gcc clean
+ make -fwin32/Makefile.gcc PREFIX=$TARGET- CC="$CC" SHARED_MODE=1 \
+ DESTDIR="$prefix_dir" install \
+ BINARY_PATH=/bin INCLUDE_PATH=/include LIBRARY_PATH=/lib
+ popd
+}
+_zlib_mark=lib/libz.dll.a
+
+_ffmpeg () {
+ [ -d ffmpeg ] || $gitclone https://github.com/FFmpeg/FFmpeg.git ffmpeg
+ builddir ffmpeg
+ local args=(
+ --pkg-config=pkg-config --target-os=mingw32
+ --enable-cross-compile --cross-prefix=$TARGET- --arch=${TARGET%%-*}
+ --cc="$CC" --cxx="$CXX" $commonflags
+ --disable-{doc,programs,muxers,encoders}
+ --enable-encoder=mjpeg,png
+ )
+ pkg-config vulkan && args+=(--enable-vulkan --enable-libshaderc)
+ ../configure "${args[@]}"
+ makeplusinstall
+ popd
+}
+_ffmpeg_mark=lib/libavcodec.dll.a
+
+_shaderc () {
+ if [ ! -d shaderc ]; then
+ $gitclone https://github.com/google/shaderc.git
+ (cd shaderc && ./utils/git-sync-deps)
+ fi
+ builddir shaderc
+ cmake .. "${cmake_args[@]}" \
+ -DBUILD_SHARED_LIBS=OFF -DSHADERC_SKIP_TESTS=ON
+ makeplusinstall
+ popd
+}
+_shaderc_mark=lib/libshaderc_shared.dll.a
+
+_spirv_cross () {
+ [ -d SPIRV-Cross ] || $gitclone https://github.com/KhronosGroup/SPIRV-Cross
+ builddir SPIRV-Cross
+ cmake .. "${cmake_args[@]}" \
+ -DSPIRV_CROSS_SHARED=ON -DSPIRV_CROSS_{CLI,STATIC}=OFF
+ makeplusinstall
+ popd
+}
+_spirv_cross_mark=lib/libspirv-cross-c-shared.dll.a
+
+_vulkan_headers () {
+ [ -d Vulkan-Headers ] || $gitclone https://github.com/KhronosGroup/Vulkan-Headers -b v1.3.266
+ builddir Vulkan-Headers
+ cmake .. "${cmake_args[@]}"
+ makeplusinstall
+ popd
+}
+_vulkan_headers_mark=include/vulkan/vulkan.h
+
+_vulkan_loader () {
+ [ -d Vulkan-Loader ] || $gitclone https://github.com/KhronosGroup/Vulkan-Loader -b v1.3.266
+ builddir Vulkan-Loader
+ cmake .. "${cmake_args[@]}" \
+ -DENABLE_WERROR=OFF
+ makeplusinstall
+ popd
+}
+_vulkan_loader_mark=lib/libvulkan-1.dll.a
+
+_libplacebo () {
+ [ -d libplacebo ] || $gitclone https://code.videolan.org/videolan/libplacebo.git
+ builddir libplacebo
+ meson setup .. --cross-file "$prefix_dir/crossfile" \
+ -Ddemos=false -D{opengl,d3d11}=enabled
+ makeplusinstall
+ popd
+}
+_libplacebo_mark=lib/libplacebo.dll.a
+
+_freetype () {
+ local ver=2.13.1
+ gettar "https://mirror.netcologne.de/savannah/freetype/freetype-${ver}.tar.xz"
+ builddir freetype-${ver}
+ meson setup .. --cross-file "$prefix_dir/crossfile"
+ makeplusinstall
+ popd
+}
+_freetype_mark=lib/libfreetype.dll.a
+
+_fribidi () {
+ local ver=1.0.13
+ gettar "https://github.com/fribidi/fribidi/releases/download/v${ver}/fribidi-${ver}.tar.xz"
+ builddir fribidi-${ver}
+ meson setup .. --cross-file "$prefix_dir/crossfile" \
+ -D{tests,docs}=false
+ makeplusinstall
+ popd
+}
+_fribidi_mark=lib/libfribidi.dll.a
+
+_harfbuzz () {
+ local ver=8.1.1
+ gettar "https://github.com/harfbuzz/harfbuzz/releases/download/${ver}/harfbuzz-${ver}.tar.xz"
+ builddir harfbuzz-${ver}
+ meson setup .. --cross-file "$prefix_dir/crossfile" \
+ -Dtests=disabled
+ makeplusinstall
+ popd
+}
+_harfbuzz_mark=lib/libharfbuzz.dll.a
+
+_libass () {
+ [ -d libass ] || $gitclone https://github.com/libass/libass.git
+ builddir libass
+ [ -f ../configure ] || (cd .. && ./autogen.sh)
+ ../configure --host=$TARGET $commonflags
+ makeplusinstall
+ popd
+}
+_libass_mark=lib/libass.dll.a
+
+_luajit () {
+ [ -d LuaJIT ] || $gitclone https://github.com/LuaJIT/LuaJIT.git
+ pushd LuaJIT
+ local hostcc="ccache cc"
+ local flags=
+ [[ "$TARGET" == "i686-"* ]] && { hostcc="$hostcc -m32"; flags=XCFLAGS=-DLUAJIT_NO_UNWIND; }
+ make TARGET_SYS=Windows clean
+ make TARGET_SYS=Windows HOST_CC="$hostcc" CROSS="ccache $TARGET-" \
+ BUILDMODE=static $flags amalg
+ make DESTDIR="$prefix_dir" INSTALL_DEP= FILE_T=luajit.exe install
+ popd
+}
+_luajit_mark=lib/libluajit-5.1.a
+
+for x in iconv zlib shaderc spirv-cross; do
+ build_if_missing $x
+done
+if [[ "$TARGET" != "i686-"* ]]; then
+ build_if_missing vulkan-headers
+ build_if_missing vulkan-loader
+fi
+for x in ffmpeg libplacebo freetype fribidi harfbuzz libass luajit; do
+ build_if_missing $x
+done
+
+## mpv
+
+[ -z "$1" ] && exit 0
+
+CFLAGS+=" -I'$prefix_dir/include'"
+LDFLAGS+=" -L'$prefix_dir/lib'"
+export CFLAGS LDFLAGS
+build=mingw_build
+rm -rf $build
+
+meson setup $build --cross-file "$prefix_dir/crossfile" \
+ --werror \
+ -Dlibplacebo:werror=false \
+ -Dc_args="-Wno-error=deprecated -Wno-error=deprecated-declarations" \
+ --buildtype debugoptimized \
+ -Dlibmpv=true -Dlua=luajit \
+ -D{shaderc,spirv-cross,d3d11}=enabled
+
+meson compile -C $build
+
+if [ "$2" = pack ]; then
+ mkdir -p artifact/tmp
+ echo "Copying:"
+ cp -pv $build/player/mpv.com $build/mpv.exe artifact/
+ # copy everything we can get our hands on
+ cp -p "$prefix_dir/bin/"*.dll artifact/tmp/
+ shopt -s nullglob
+ for file in /usr/lib/gcc/$TARGET/*-posix/*.dll /usr/$TARGET/lib/*.dll; do
+ cp -p "$file" artifact/tmp/
+ done
+ # pick DLLs we need
+ pushd artifact/tmp
+ dlls=(
+ libgcc_*.dll lib{ssp,stdc++,winpthread}-[0-9]*.dll # compiler runtime
+ av*.dll sw*.dll lib{ass,freetype,fribidi,harfbuzz,iconv,placebo}-[0-9]*.dll
+ lib{shaderc_shared,spirv-cross-c-shared}.dll zlib1.dll
+ # note: vulkan-1.dll is not here since drivers provide it
+ )
+ mv -v "${dlls[@]}" ..
+ popd
+
+ echo "Archiving:"
+ pushd artifact
+ rm -rf tmp
+ zip -9r "../mpv-git-$(date +%F)-$(git rev-parse --short HEAD)-${TARGET%%-*}.zip" -- *
+ popd
+fi
diff --git a/ci/build-msys2.sh b/ci/build-msys2.sh
new file mode 100755
index 0000000..d3e1ce9
--- /dev/null
+++ b/ci/build-msys2.sh
@@ -0,0 +1,39 @@
+#!/bin/sh -e
+
+mkdir subprojects
+cat > subprojects/libplacebo.wrap <<EOF
+[wrap-git]
+url = https://code.videolan.org/videolan/libplacebo.git
+revision = v6.338.1
+depth = 1
+clone-recursive = true
+EOF
+
+meson setup build \
+ --werror \
+ -Dlibplacebo:werror=false \
+ -Dlibplacebo:demos=false \
+ -Dlibplacebo:default_library=static \
+ -Dc_args="-Wno-error=deprecated -Wno-error=deprecated-declarations" \
+ -D cdda=enabled \
+ -D d3d-hwaccel=enabled \
+ -D d3d11=enabled \
+ -D dvdnav=enabled \
+ -D egl-angle-lib=enabled \
+ -D egl-angle-win32=enabled \
+ -D jpeg=enabled \
+ -D lcms2=enabled \
+ -D libarchive=enabled \
+ -D libbluray=enabled \
+ -D libmpv=true \
+ -D lua=enabled \
+ -D pdf-build=enabled \
+ -D rubberband=enabled \
+ -D shaderc=enabled \
+ -D spirv-cross=enabled \
+ -D tests=true \
+ -D uchardet=enabled \
+ -D vapoursynth=enabled
+meson compile -C build
+cp ./build/player/mpv.com ./build
+./build/mpv.com -v --no-config
diff --git a/ci/build-tumbleweed.sh b/ci/build-tumbleweed.sh
new file mode 100755
index 0000000..03e8d1a
--- /dev/null
+++ b/ci/build-tumbleweed.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -e
+
+meson setup build \
+ --werror \
+ -Dlibplacebo:werror=false \
+ -Dc_args="-Wno-error=deprecated -Wno-error=deprecated-declarations" \
+ -Db_sanitize=address,undefined \
+ -Dcdda=enabled \
+ -Ddvbin=enabled \
+ -Ddvdnav=enabled \
+ -Dlibarchive=enabled \
+ -Dlibmpv=true \
+ -Dmanpage-build=enabled \
+ -Dpipewire=enabled \
+ -Dshaderc=enabled \
+ -Dtests=true \
+ -Dvulkan=enabled
+meson compile -C build
+./build/mpv -v --no-config
diff --git a/ci/lint-commit-msg.py b/ci/lint-commit-msg.py
new file mode 100755
index 0000000..4198ed4
--- /dev/null
+++ b/ci/lint-commit-msg.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+import os, sys, json, subprocess, re
+
+def call(cmd) -> str:
+ sys.stdout.flush()
+ ret = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
+ return ret.stdout
+
+lint_rules = {}
+
+def lint_rule(description: str):
+ def f(func):
+ assert func.__name__ not in lint_rules.keys()
+ lint_rules[func.__name__] = (func, description)
+ return f
+
+def get_commit_range() -> str:
+ if len(sys.argv) > 1:
+ return sys.argv[1]
+ # https://github.com/actions/runner/issues/342#issuecomment-590670059
+ event_name = os.environ["GITHUB_EVENT_NAME"]
+ with open(os.environ["GITHUB_EVENT_PATH"], "rb") as f:
+ event = json.load(f)
+ if event_name == "push":
+ if event["created"] or event["forced"]:
+ print("Skipping logic on branch creation or force-push")
+ return None
+ return event["before"] + "..." + event["after"]
+ elif event_name == "pull_request":
+ return event["pull_request"]["base"]["sha"] + ".." + event["pull_request"]["head"]["sha"]
+
+def do_lint(commit_range: str) -> bool:
+ commits = call(["git", "log", "--pretty=format:%h %s", commit_range]).splitlines()
+ print(f"Linting {len(commits)} commit(s):")
+ any_failed = False
+ for commit in commits:
+ sha, _, _ = commit.partition(' ')
+ #print(commit)
+ body = call(["git", "show", "-s", "--format=%B", sha]).splitlines()
+ failed = []
+ if len(body) == 0:
+ failed.append("* Commit message must not be empty")
+ else:
+ for k, v in lint_rules.items():
+ if not v[0](body):
+ failed.append(f"* {v[1]} [{k}]")
+ if failed:
+ any_failed = True
+ print("-" * 40)
+ sys.stdout.flush()
+ subprocess.run(["git", "-P", "show", "-s", sha])
+ print("\nhas the following issues:")
+ print("\n".join(failed))
+ print("-" * 40)
+ return any_failed
+
+################################################################################
+
+NO_PREFIX_WHITELIST = r"^Revert \"(.*)\"|^Release [0-9]|^Update VERSION$"
+
+@lint_rule("Subject line must contain a prefix identifying the sub system")
+def subsystem_prefix(body):
+ if re.search(NO_PREFIX_WHITELIST, body[0]):
+ return True
+ m = re.search(r"^([^:]+):\s", body[0])
+ if not m:
+ return False
+ # a comma-separated list is okay
+ s = re.sub(r",\s+", "", m.group(1))
+ # but no spaces otherwise
+ return not " " in s
+
+@lint_rule("First word after : must be lower case")
+def description_lowercase(body):
+ if re.search(NO_PREFIX_WHITELIST, body[0]):
+ return True
+ # Allow all caps for acronyms and such
+ if re.search(r":\s[A-Z]{2,}\s", body[0]):
+ return True
+ return re.search(r":\s+[a-z0-9]", body[0])
+
+@lint_rule("Subject line must not end with a full stop")
+def no_dot(body):
+ return not body[0].rstrip().endswith('.')
+
+@lint_rule("There must be an empty line between subject and extended description")
+def empty_line(body):
+ return len(body) == 1 or body[1].strip() == ""
+
+# been seeing this one all over github lately, must be the webshits
+@lint_rule("Do not use 'conventional commits' style")
+def no_cc(body):
+ return not re.search(r"(?i)^(feat|fix|chore|refactor)[!:(]", body[0])
+
+@lint_rule("History must be linear, no merge commits")
+def no_merge(body):
+ return not body[0].startswith("Merge ")
+
+@lint_rule("Subject line should be shorter than 72 characters")
+def line_too_long(body):
+ revert = re.search(r"^Revert \"(.*)\"", body[0])
+ return True if revert else len(body[0]) <= 72
+
+@lint_rule("Prefix should not include C file extensions (use `vo_gpu: ...` not `vo_gpu.c: ...`)")
+def no_file_exts(body):
+ return not re.search(r"[a-z0-9]\.[ch]:\s", body[0])
+
+################################################################################
+
+if __name__ == "__main__":
+ commit_range = get_commit_range()
+ if commit_range is None:
+ exit(0)
+ print("Commit range:", commit_range)
+ any_failed = do_lint(commit_range)
+ exit(1 if any_failed else 0)
diff --git a/common/av_common.c b/common/av_common.c
new file mode 100644
index 0000000..5d07349
--- /dev/null
+++ b/common/av_common.c
@@ -0,0 +1,404 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <math.h>
+#include <limits.h>
+
+#include <libavutil/common.h>
+#include <libavutil/log.h>
+#include <libavutil/dict.h>
+#include <libavutil/opt.h>
+#include <libavutil/error.h>
+#include <libavutil/cpu.h>
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+
+#include "config.h"
+
+#include "audio/chmap_avchannel.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "demux/packet.h"
+#include "demux/stheader.h"
+#include "misc/bstr.h"
+#include "video/fmt-conversion.h"
+#include "av_common.h"
+#include "codecs.h"
+
+enum AVMediaType mp_to_av_stream_type(int type)
+{
+ switch (type) {
+ case STREAM_VIDEO: return AVMEDIA_TYPE_VIDEO;
+ case STREAM_AUDIO: return AVMEDIA_TYPE_AUDIO;
+ case STREAM_SUB: return AVMEDIA_TYPE_SUBTITLE;
+ default: return AVMEDIA_TYPE_UNKNOWN;
+ }
+}
+
+AVCodecParameters *mp_codec_params_to_av(const struct mp_codec_params *c)
+{
+ AVCodecParameters *avp = avcodec_parameters_alloc();
+ if (!avp)
+ return NULL;
+
+ // If we have lavf demuxer params, they overwrite by definition any others.
+ if (c->lav_codecpar) {
+ if (avcodec_parameters_copy(avp, c->lav_codecpar) < 0)
+ goto error;
+ return avp;
+ }
+
+ avp->codec_type = mp_to_av_stream_type(c->type);
+ avp->codec_id = mp_codec_to_av_codec_id(c->codec);
+ avp->codec_tag = c->codec_tag;
+ if (c->extradata_size) {
+ uint8_t *extradata = c->extradata;
+ int size = c->extradata_size;
+
+ if (avp->codec_id == AV_CODEC_ID_FLAC) {
+ // ffmpeg expects FLAC extradata to be just the STREAMINFO,
+ // so grab only that (and assume it'll be the first block)
+ if (size >= 8 && !memcmp(c->extradata, "fLaC", 4)) {
+ extradata += 8;
+ size = MPMIN(34, size - 8); // FLAC_STREAMINFO_SIZE
+ }
+ }
+
+ avp->extradata = av_mallocz(size + AV_INPUT_BUFFER_PADDING_SIZE);
+ if (!avp->extradata)
+ goto error;
+ avp->extradata_size = size;
+ memcpy(avp->extradata, extradata, size);
+ }
+ avp->bits_per_coded_sample = c->bits_per_coded_sample;
+
+ // Video only
+ avp->width = c->disp_w;
+ avp->height = c->disp_h;
+
+ // Audio only
+ avp->sample_rate = c->samplerate;
+ avp->bit_rate = c->bitrate;
+ avp->block_align = c->block_align;
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ avp->channels = c->channels.num;
+ if (!mp_chmap_is_unknown(&c->channels))
+ avp->channel_layout = mp_chmap_to_lavc(&c->channels);
+#else
+ mp_chmap_to_av_layout(&avp->ch_layout, &c->channels);
+#endif
+
+ return avp;
+error:
+ avcodec_parameters_free(&avp);
+ return NULL;
+}
+
+// Set avctx codec headers for decoding. Returns <0 on failure.
+int mp_set_avctx_codec_headers(AVCodecContext *avctx, const struct mp_codec_params *c)
+{
+ enum AVMediaType codec_type = avctx->codec_type;
+ enum AVCodecID codec_id = avctx->codec_id;
+ AVCodecParameters *avp = mp_codec_params_to_av(c);
+ if (!avp)
+ return -1;
+
+ int r = avcodec_parameters_to_context(avctx, avp) < 0 ? -1 : 0;
+ avcodec_parameters_free(&avp);
+
+ if (avctx->codec_type != AVMEDIA_TYPE_UNKNOWN)
+ avctx->codec_type = codec_type;
+ if (avctx->codec_id != AV_CODEC_ID_NONE)
+ avctx->codec_id = codec_id;
+ return r;
+}
+
+// Pick a "good" timebase, which will be used to convert double timestamps
+// back to fractions for passing them through libavcodec.
+AVRational mp_get_codec_timebase(const struct mp_codec_params *c)
+{
+ AVRational tb = {c->native_tb_num, c->native_tb_den};
+ if (tb.num < 1 || tb.den < 1) {
+ if (c->reliable_fps)
+ tb = av_inv_q(av_d2q(c->fps, 1000000));
+ if (tb.num < 1 || tb.den < 1)
+ tb = AV_TIME_BASE_Q;
+ }
+
+ // If the timebase is too coarse, raise its precision, or small adjustments
+ // to timestamps done between decoder and demuxer could be lost.
+ if (av_q2d(tb) > 0.001) {
+ AVRational r = av_div_q(tb, (AVRational){1, 1000});
+ tb.den *= (r.num + r.den - 1) / r.den;
+ }
+
+ av_reduce(&tb.num, &tb.den, tb.num, tb.den, INT_MAX);
+
+ if (tb.num < 1 || tb.den < 1)
+ tb = AV_TIME_BASE_Q;
+
+ return tb;
+}
+
+static AVRational get_def_tb(AVRational *tb)
+{
+ return tb && tb->num > 0 && tb->den > 0 ? *tb : AV_TIME_BASE_Q;
+}
+
+// Convert the mpv style timestamp (seconds as double) to a libavcodec style
+// timestamp (integer units in a given timebase).
+int64_t mp_pts_to_av(double mp_pts, AVRational *tb)
+{
+ AVRational b = get_def_tb(tb);
+ return mp_pts == MP_NOPTS_VALUE ? AV_NOPTS_VALUE : llrint(mp_pts / av_q2d(b));
+}
+
+// Inverse of mp_pts_to_av(). (The timebases must be exactly the same.)
+double mp_pts_from_av(int64_t av_pts, AVRational *tb)
+{
+ AVRational b = get_def_tb(tb);
+ return av_pts == AV_NOPTS_VALUE ? MP_NOPTS_VALUE : av_pts * av_q2d(b);
+}
+
+// Set dst from mpkt. Note that dst is not refcountable.
+// mpkt can be NULL to generate empty packets (used to flush delayed data).
+// Sets pts/dts using mp_pts_to_av(ts, tb). (Be aware of the implications.)
+// Set duration field only if tb is set.
+void mp_set_av_packet(AVPacket *dst, struct demux_packet *mpkt, AVRational *tb)
+{
+ dst->side_data = NULL;
+ dst->side_data_elems = 0;
+ dst->buf = NULL;
+ av_packet_unref(dst);
+
+ dst->data = mpkt ? mpkt->buffer : NULL;
+ dst->size = mpkt ? mpkt->len : 0;
+ /* Some codecs (ZeroCodec, some cases of PNG) may want keyframe info
+ * from demuxer. */
+ if (mpkt && mpkt->keyframe)
+ dst->flags |= AV_PKT_FLAG_KEY;
+ if (mpkt && mpkt->avpacket) {
+ dst->side_data = mpkt->avpacket->side_data;
+ dst->side_data_elems = mpkt->avpacket->side_data_elems;
+ if (dst->data == mpkt->avpacket->data)
+ dst->buf = mpkt->avpacket->buf;
+ dst->flags |= mpkt->avpacket->flags;
+ }
+ if (mpkt && tb && tb->num > 0 && tb->den > 0)
+ dst->duration = mpkt->duration / av_q2d(*tb);
+ dst->pts = mp_pts_to_av(mpkt ? mpkt->pts : MP_NOPTS_VALUE, tb);
+ dst->dts = mp_pts_to_av(mpkt ? mpkt->dts : MP_NOPTS_VALUE, tb);
+}
+
+void mp_set_avcodec_threads(struct mp_log *l, AVCodecContext *avctx, int threads)
+{
+ if (threads == 0) {
+ threads = av_cpu_count();
+ if (threads < 1) {
+ mp_warn(l, "Could not determine thread count to use, defaulting to 1.\n");
+ threads = 1;
+ } else {
+ mp_verbose(l, "Detected %d logical cores.\n", threads);
+ if (threads > 1)
+ threads += 1; // extra thread for better load balancing
+ }
+ // Apparently some libavcodec versions have or had trouble with more
+ // than 16 threads, and/or print a warning when using > 16.
+ threads = MPMIN(threads, 16);
+ }
+ mp_verbose(l, "Requesting %d threads for decoding.\n", threads);
+ avctx->thread_count = threads;
+}
+
+static void add_codecs(struct mp_decoder_list *list, enum AVMediaType type,
+ bool decoders)
+{
+ void *iter = NULL;
+ for (;;) {
+ const AVCodec *cur = av_codec_iterate(&iter);
+ if (!cur)
+ break;
+ if (av_codec_is_decoder(cur) == decoders &&
+ (type == AVMEDIA_TYPE_UNKNOWN || cur->type == type))
+ {
+ mp_add_decoder(list, mp_codec_from_av_codec_id(cur->id),
+ cur->name, cur->long_name);
+ }
+ }
+}
+
+void mp_add_lavc_decoders(struct mp_decoder_list *list, enum AVMediaType type)
+{
+ add_codecs(list, type, true);
+}
+
+// (Abuses the decoder list data structures.)
+void mp_add_lavc_encoders(struct mp_decoder_list *list)
+{
+ add_codecs(list, AVMEDIA_TYPE_UNKNOWN, false);
+}
+
+char **mp_get_lavf_demuxers(void)
+{
+ char **list = NULL;
+ void *iter = NULL;
+ int num = 0;
+ for (;;) {
+ const AVInputFormat *cur = av_demuxer_iterate(&iter);
+ if (!cur)
+ break;
+ MP_TARRAY_APPEND(NULL, list, num, talloc_strdup(NULL, cur->name));
+ }
+ MP_TARRAY_APPEND(NULL, list, num, NULL);
+ return list;
+}
+
+int mp_codec_to_av_codec_id(const char *codec)
+{
+ int id = AV_CODEC_ID_NONE;
+ if (codec) {
+ const AVCodecDescriptor *desc = avcodec_descriptor_get_by_name(codec);
+ if (desc)
+ id = desc->id;
+ if (id == AV_CODEC_ID_NONE) {
+ const AVCodec *avcodec = avcodec_find_decoder_by_name(codec);
+ if (avcodec)
+ id = avcodec->id;
+ }
+ }
+ return id;
+}
+
+const char *mp_codec_from_av_codec_id(int codec_id)
+{
+ const char *name = NULL;
+ const AVCodecDescriptor *desc = avcodec_descriptor_get(codec_id);
+ if (desc)
+ name = desc->name;
+ if (!name) {
+ const AVCodec *avcodec = avcodec_find_decoder(codec_id);
+ if (avcodec)
+ name = avcodec->name;
+ }
+ return name;
+}
+
+bool mp_codec_is_lossless(const char *codec)
+{
+ const AVCodecDescriptor *desc =
+ avcodec_descriptor_get(mp_codec_to_av_codec_id(codec));
+ return desc && (desc->props & AV_CODEC_PROP_LOSSLESS);
+}
+
+// kv is in the format as by OPT_KEYVALUELIST(): kv[0]=key0, kv[1]=val0, ...
+// Copy them to the dict.
+void mp_set_avdict(AVDictionary **dict, char **kv)
+{
+ for (int n = 0; kv && kv[n * 2]; n++)
+ av_dict_set(dict, kv[n * 2 + 0], kv[n * 2 + 1], 0);
+}
+
+// For use with libav* APIs that take AVDictionaries of options.
+// Print options remaining in the dict as unset.
+void mp_avdict_print_unset(struct mp_log *log, int msgl, AVDictionary *dict)
+{
+ AVDictionaryEntry *t = NULL;
+ while ((t = av_dict_get(dict, "", t, AV_DICT_IGNORE_SUFFIX)))
+ mp_msg(log, msgl, "Could not set AVOption %s='%s'\n", t->key, t->value);
+}
+
+// If the name starts with "@", try to interpret it as a number, and set *name
+// to the name of the n-th parameter.
+static void resolve_positional_arg(void *avobj, char **name)
+{
+ if (!*name || (*name)[0] != '@' || !avobj)
+ return;
+
+ char *end = NULL;
+ int pos = strtol(*name + 1, &end, 10);
+ if (!end || *end)
+ return;
+
+ const AVOption *opt = NULL;
+ int offset = -1;
+ while (1) {
+ opt = av_opt_next(avobj, opt);
+ if (!opt)
+ return;
+ // This is what libavfilter's parser does to skip aliases.
+ if (opt->offset != offset && opt->type != AV_OPT_TYPE_CONST)
+ pos--;
+ if (pos < 0) {
+ *name = (char *)opt->name;
+ return;
+ }
+ offset = opt->offset;
+ }
+}
+
+// kv is in the format as by OPT_KEYVALUELIST(): kv[0]=key0, kv[1]=val0, ...
+// Set these options on given avobj (using av_opt_set..., meaning avobj must
+// point to a struct that has AVClass as first member).
+// Options which fail to set (error or not found) are printed to log.
+// Returns: >=0 success, <0 failed to set an option
+int mp_set_avopts(struct mp_log *log, void *avobj, char **kv)
+{
+ return mp_set_avopts_pos(log, avobj, avobj, kv);
+}
+
+// Like mp_set_avopts(), but the posargs argument is used to resolve positional
+// arguments. If posargs==NULL, positional args are disabled.
+int mp_set_avopts_pos(struct mp_log *log, void *avobj, void *posargs, char **kv)
+{
+ int success = 0;
+ for (int n = 0; kv && kv[n * 2]; n++) {
+ char *k = kv[n * 2 + 0];
+ char *v = kv[n * 2 + 1];
+ resolve_positional_arg(posargs, &k);
+ int r = av_opt_set(avobj, k, v, AV_OPT_SEARCH_CHILDREN);
+ if (r == AVERROR_OPTION_NOT_FOUND) {
+ mp_err(log, "AVOption '%s' not found.\n", k);
+ success = -1;
+ } else if (r < 0) {
+ char errstr[80];
+ av_strerror(r, errstr, sizeof(errstr));
+ mp_err(log, "Could not set AVOption %s='%s' (%s)\n", k, v, errstr);
+ success = -1;
+ }
+ }
+ return success;
+}
+
+/**
+ * Must be used to free an AVPacket that was used with mp_set_av_packet().
+ *
+ * We have a particular pattern where we "borrow" buffers and set them
+ * into an AVPacket to pass data to ffmpeg without extra copies.
+ * This applies to buf and side_data, so this function clears them before
+ * freeing.
+ */
+void mp_free_av_packet(AVPacket **pkt)
+{
+ if (*pkt) {
+ (*pkt)->side_data = NULL;
+ (*pkt)->side_data_elems = 0;
+ (*pkt)->buf = NULL;
+ }
+ av_packet_free(pkt);
+}
diff --git a/common/av_common.h b/common/av_common.h
new file mode 100644
index 0000000..1f05e14
--- /dev/null
+++ b/common/av_common.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_AVCOMMON_H
+#define MP_AVCOMMON_H
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include <libavutil/avutil.h>
+#include <libavutil/rational.h>
+#include <libavcodec/avcodec.h>
+
+struct mp_decoder_list;
+struct demux_packet;
+struct mp_codec_params;
+struct AVDictionary;
+struct mp_log;
+
+enum AVMediaType mp_to_av_stream_type(int type);
+AVCodecParameters *mp_codec_params_to_av(const struct mp_codec_params *c);
+int mp_set_avctx_codec_headers(AVCodecContext *avctx, const struct mp_codec_params *c);
+AVRational mp_get_codec_timebase(const struct mp_codec_params *c);
+void mp_set_av_packet(AVPacket *dst, struct demux_packet *mpkt, AVRational *tb);
+int64_t mp_pts_to_av(double mp_pts, AVRational *tb);
+double mp_pts_from_av(int64_t av_pts, AVRational *tb);
+void mp_set_avcodec_threads(struct mp_log *l, AVCodecContext *avctx, int threads);
+void mp_add_lavc_decoders(struct mp_decoder_list *list, enum AVMediaType type);
+void mp_add_lavc_encoders(struct mp_decoder_list *list);
+char **mp_get_lavf_demuxers(void);
+int mp_codec_to_av_codec_id(const char *codec);
+const char *mp_codec_from_av_codec_id(int codec_id);
+bool mp_codec_is_lossless(const char *codec);
+void mp_set_avdict(struct AVDictionary **dict, char **kv);
+void mp_avdict_print_unset(struct mp_log *log, int msgl, struct AVDictionary *d);
+int mp_set_avopts(struct mp_log *log, void *avobj, char **kv);
+int mp_set_avopts_pos(struct mp_log *log, void *avobj, void *posargs, char **kv);
+void mp_free_av_packet(AVPacket **pkt);
+
+#endif
diff --git a/common/av_log.c b/common/av_log.c
new file mode 100644
index 0000000..b6bad04
--- /dev/null
+++ b/common/av_log.c
@@ -0,0 +1,215 @@
+/*
+ * av_log to mp_msg converter
+ * Copyright (C) 2006 Michael Niedermayer
+ * Copyright (C) 2009 Uoti Urpala
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+
+#include "av_log.h"
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "config.h"
+#include "osdep/threads.h"
+
+#include <libavutil/avutil.h>
+#include <libavutil/log.h>
+#include <libavutil/version.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavcodec/version.h>
+#include <libavformat/avformat.h>
+#include <libavformat/version.h>
+#include <libswresample/swresample.h>
+#include <libswresample/version.h>
+#include <libswscale/swscale.h>
+#include <libswscale/version.h>
+#include <libavfilter/avfilter.h>
+#include <libavfilter/version.h>
+
+#if HAVE_LIBAVDEVICE
+#include <libavdevice/avdevice.h>
+#endif
+
+// Needed because the av_log callback does not provide a library-safe message
+// callback.
+static mp_static_mutex log_lock = MP_STATIC_MUTEX_INITIALIZER;
+static struct mpv_global *log_mpv_instance;
+static struct mp_log *log_root, *log_decaudio, *log_decvideo, *log_demuxer;
+static bool log_print_prefix = true;
+
+static int av_log_level_to_mp_level(int av_level)
+{
+ if (av_level > AV_LOG_VERBOSE)
+ return MSGL_TRACE;
+ if (av_level > AV_LOG_INFO)
+ return MSGL_DEBUG;
+ if (av_level > AV_LOG_WARNING)
+ return MSGL_V;
+ if (av_level > AV_LOG_ERROR)
+ return MSGL_WARN;
+ if (av_level > AV_LOG_FATAL)
+ return MSGL_ERR;
+ return MSGL_FATAL;
+}
+
+static struct mp_log *get_av_log(void *ptr)
+{
+ if (!ptr)
+ return log_root;
+
+ AVClass *avc = *(AVClass **)ptr;
+ if (!avc) {
+ mp_warn(log_root,
+ "av_log callback called with bad parameters (NULL AVClass).\n"
+ "This is a bug in one of Libav/FFmpeg libraries used.\n");
+ return log_root;
+ }
+
+ if (!strcmp(avc->class_name, "AVCodecContext")) {
+ AVCodecContext *s = ptr;
+ if (s->codec) {
+ if (s->codec->type == AVMEDIA_TYPE_AUDIO) {
+ if (av_codec_is_decoder(s->codec))
+ return log_decaudio;
+ } else if (s->codec->type == AVMEDIA_TYPE_VIDEO) {
+ if (av_codec_is_decoder(s->codec))
+ return log_decvideo;
+ }
+ }
+ }
+
+ if (!strcmp(avc->class_name, "AVFormatContext")) {
+ AVFormatContext *s = ptr;
+ if (s->iformat)
+ return log_demuxer;
+ }
+
+ return log_root;
+}
+
+static void mp_msg_av_log_callback(void *ptr, int level, const char *fmt,
+ va_list vl)
+{
+ AVClass *avc = ptr ? *(AVClass **)ptr : NULL;
+ int mp_level = av_log_level_to_mp_level(level);
+
+ // Note: mp_log is thread-safe, but destruction of the log instances is not.
+ mp_mutex_lock(&log_lock);
+
+ if (!log_mpv_instance) {
+ mp_mutex_unlock(&log_lock);
+ // Fallback to stderr
+ vfprintf(stderr, fmt, vl);
+ return;
+ }
+
+ struct mp_log *log = get_av_log(ptr);
+
+ if (mp_msg_test(log, mp_level)) {
+ char buffer[4096] = "";
+ int pos = 0;
+ const char *prefix = avc ? avc->item_name(ptr) : NULL;
+ if (log_print_prefix && prefix)
+ pos = snprintf(buffer, sizeof(buffer), "%s: ", prefix);
+ log_print_prefix = fmt[strlen(fmt) - 1] == '\n';
+
+ pos = MPMIN(MPMAX(pos, 0), sizeof(buffer));
+ vsnprintf(buffer + pos, sizeof(buffer) - pos, fmt, vl);
+
+ mp_msg(log, mp_level, "%s", buffer);
+ }
+
+ mp_mutex_unlock(&log_lock);
+}
+
+void init_libav(struct mpv_global *global)
+{
+ mp_mutex_lock(&log_lock);
+ if (!log_mpv_instance) {
+ log_mpv_instance = global;
+ log_root = mp_log_new(NULL, global->log, "ffmpeg");
+ log_decaudio = mp_log_new(log_root, log_root, "audio");
+ log_decvideo = mp_log_new(log_root, log_root, "video");
+ log_demuxer = mp_log_new(log_root, log_root, "demuxer");
+ av_log_set_callback(mp_msg_av_log_callback);
+ }
+ mp_mutex_unlock(&log_lock);
+
+ avformat_network_init();
+
+#if HAVE_LIBAVDEVICE
+ avdevice_register_all();
+#endif
+}
+
+void uninit_libav(struct mpv_global *global)
+{
+ mp_mutex_lock(&log_lock);
+ if (log_mpv_instance == global) {
+ av_log_set_callback(av_log_default_callback);
+ log_mpv_instance = NULL;
+ talloc_free(log_root);
+ }
+ mp_mutex_unlock(&log_lock);
+}
+
+#define V(x) AV_VERSION_MAJOR(x), \
+ AV_VERSION_MINOR(x), \
+ AV_VERSION_MICRO(x)
+
+struct lib {
+ const char *name;
+ unsigned buildv;
+ unsigned runv;
+};
+
+void check_library_versions(struct mp_log *log, int v)
+{
+ const struct lib libs[] = {
+ {"libavutil", LIBAVUTIL_VERSION_INT, avutil_version()},
+ {"libavcodec", LIBAVCODEC_VERSION_INT, avcodec_version()},
+ {"libavformat", LIBAVFORMAT_VERSION_INT, avformat_version()},
+ {"libswscale", LIBSWSCALE_VERSION_INT, swscale_version()},
+ {"libavfilter", LIBAVFILTER_VERSION_INT, avfilter_version()},
+ {"libswresample", LIBSWRESAMPLE_VERSION_INT, swresample_version()},
+ };
+
+ mp_msg(log, v, "FFmpeg version: %s\n", av_version_info());
+ mp_msg(log, v, "FFmpeg library versions:\n");
+
+ for (int n = 0; n < MP_ARRAY_SIZE(libs); n++) {
+ const struct lib *l = &libs[n];
+ mp_msg(log, v, " %-15s %d.%d.%d", l->name, V(l->buildv));
+ if (l->buildv != l->runv)
+ mp_msg(log, v, " (runtime %d.%d.%d)", V(l->runv));
+ mp_msg(log, v, "\n");
+ if (l->buildv > l->runv ||
+ AV_VERSION_MAJOR(l->buildv) != AV_VERSION_MAJOR(l->runv))
+ {
+ fprintf(stderr, "%s: %d.%d.%d -> %d.%d.%d\n",
+ l->name, V(l->buildv), V(l->runv));
+ abort();
+ }
+ }
+}
+
+#undef V
diff --git a/common/av_log.h b/common/av_log.h
new file mode 100644
index 0000000..ae12838
--- /dev/null
+++ b/common/av_log.h
@@ -0,0 +1,11 @@
+#ifndef MP_AV_LOG_H
+#define MP_AV_LOG_H
+
+#include <stdbool.h>
+
+struct mpv_global;
+struct mp_log;
+void init_libav(struct mpv_global *global);
+void uninit_libav(struct mpv_global *global);
+void check_library_versions(struct mp_log *log, int v);
+#endif
diff --git a/common/codecs.c b/common/codecs.c
new file mode 100644
index 0000000..8101b50
--- /dev/null
+++ b/common/codecs.c
@@ -0,0 +1,107 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include "mpv_talloc.h"
+#include "misc/bstr.h"
+#include "common/msg.h"
+#include "codecs.h"
+
+void mp_add_decoder(struct mp_decoder_list *list, const char *codec,
+ const char *decoder, const char *desc)
+{
+ struct mp_decoder_entry entry = {
+ .codec = talloc_strdup(list, codec),
+ .decoder = talloc_strdup(list, decoder),
+ .desc = talloc_strdup(list, desc),
+ };
+ MP_TARRAY_APPEND(list, list->entries, list->num_entries, entry);
+}
+
+// Add entry, but only if it's not yet on the list, and if the codec matches.
+// If codec == NULL, don't compare codecs.
+static void add_new(struct mp_decoder_list *to, struct mp_decoder_entry *entry,
+ const char *codec)
+{
+ if (!entry || (codec && strcmp(entry->codec, codec) != 0))
+ return;
+ mp_add_decoder(to, entry->codec, entry->decoder, entry->desc);
+}
+
+// Select a decoder from the given list for the given codec. The selection
+// can be influenced by the selection string, which can specify a priority
+// list of preferred decoders.
+// This returns a list of decoders to try, with the preferred decoders first.
+// The selection string corresponds to --vd/--ad directly, and has the
+// following syntax:
+// selection = [<entry> ("," <entry>)*]
+// entry = <decoder> // prefer decoder
+// entry = "-" <decoder> // exclude a decoder
+// entry = "-" // don't add fallback decoders
+// Forcing a decoder means it's added even if the codec mismatches.
+struct mp_decoder_list *mp_select_decoders(struct mp_log *log,
+ struct mp_decoder_list *all,
+ const char *codec,
+ const char *selection)
+{
+ struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list);
+ if (!codec)
+ codec = "unknown";
+ bool stop = false;
+ bstr sel = bstr0(selection);
+ while (sel.len) {
+ bstr entry;
+ bstr_split_tok(sel, ",", &entry, &sel);
+ if (bstr_equals0(entry, "-")) {
+ mp_warn(log, "Excluding codecs is deprecated.\n");
+ stop = true;
+ break;
+ }
+ for (int n = 0; n < all->num_entries; n++) {
+ struct mp_decoder_entry *cur = &all->entries[n];
+ if (bstr_equals0(entry, cur->decoder))
+ add_new(list, cur, codec);
+ }
+ }
+ if (!stop) {
+ // Add the remaining codecs which haven't been added yet
+ for (int n = 0; n < all->num_entries; n++)
+ add_new(list, &all->entries[n], codec);
+ }
+ return list;
+}
+
+void mp_append_decoders(struct mp_decoder_list *list, struct mp_decoder_list *a)
+{
+ for (int n = 0; n < a->num_entries; n++)
+ add_new(list, &a->entries[n], NULL);
+}
+
+void mp_print_decoders(struct mp_log *log, int msgl, const char *header,
+ struct mp_decoder_list *list)
+{
+ mp_msg(log, msgl, "%s\n", header);
+ for (int n = 0; n < list->num_entries; n++) {
+ struct mp_decoder_entry *entry = &list->entries[n];
+ mp_msg(log, msgl, " %s", entry->decoder);
+ if (strcmp(entry->decoder, entry->codec) != 0)
+ mp_msg(log, msgl, " (%s)", entry->codec);
+ mp_msg(log, msgl, " - %s\n", entry->desc);
+ }
+ if (list->num_entries == 0)
+ mp_msg(log, msgl, " (no decoders)\n");
+}
diff --git a/common/codecs.h b/common/codecs.h
new file mode 100644
index 0000000..367e9f6
--- /dev/null
+++ b/common/codecs.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_CODECS_H
+#define MP_CODECS_H
+
+struct mp_log;
+
+struct mp_decoder_entry {
+ const char *codec; // name of the codec (e.g. "mp3")
+ const char *decoder; // decoder name (e.g. "mp3float")
+ const char *desc; // human readable description
+};
+
+struct mp_decoder_list {
+ struct mp_decoder_entry *entries;
+ int num_entries;
+};
+
+void mp_add_decoder(struct mp_decoder_list *list, const char *codec,
+ const char *decoder, const char *desc);
+
+struct mp_decoder_list *mp_select_decoders(struct mp_log *log,
+ struct mp_decoder_list *all,
+ const char *codec,
+ const char *selection);
+
+void mp_append_decoders(struct mp_decoder_list *list, struct mp_decoder_list *a);
+
+struct mp_log;
+void mp_print_decoders(struct mp_log *log, int msgl, const char *header,
+ struct mp_decoder_list *list);
+
+#endif
diff --git a/common/common.c b/common/common.c
new file mode 100644
index 0000000..9f8230f
--- /dev/null
+++ b/common/common.c
@@ -0,0 +1,413 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdarg.h>
+#include <math.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+#include <libavutil/error.h>
+#include <libavutil/mathematics.h>
+
+#include "mpv_talloc.h"
+#include "misc/bstr.h"
+#include "misc/ctype.h"
+#include "common/common.h"
+#include "osdep/strnlen.h"
+
+#define appendf(ptr, ...) \
+ do {(*(ptr)) = talloc_asprintf_append_buffer(*(ptr), __VA_ARGS__);} while(0)
+
+// Return a talloc'ed string formatted according to the format string in fmt.
+// On error, return NULL.
+// Valid formats:
+// %H, %h: hour (%H is padded with 0 to two digits)
+// %M: minutes from 00-59 (hours are subtracted)
+// %m: total minutes (includes hours, unlike %M)
+// %S: seconds from 00-59 (minutes and hours are subtracted)
+// %s: total seconds (includes hours and minutes)
+// %f: like %s, but as float
+// %T: milliseconds (000-999)
+char *mp_format_time_fmt(const char *fmt, double time)
+{
+ if (time == MP_NOPTS_VALUE)
+ return talloc_strdup(NULL, "unknown");
+ char *sign = time < 0 ? "-" : "";
+ time = time < 0 ? -time : time;
+ long long int itime = time;
+ long long int h, m, tm, s;
+ int ms = lrint((time - itime) * 1000);
+ if (ms >= 1000) {
+ ms -= 1000;
+ itime += 1;
+ }
+ s = itime;
+ tm = s / 60;
+ h = s / 3600;
+ s -= h * 3600;
+ m = s / 60;
+ s -= m * 60;
+ char *res = talloc_strdup(NULL, "");
+ while (*fmt) {
+ if (fmt[0] == '%') {
+ fmt++;
+ switch (fmt[0]) {
+ case 'h': appendf(&res, "%s%lld", sign, h); break;
+ case 'H': appendf(&res, "%s%02lld", sign, h); break;
+ case 'm': appendf(&res, "%s%lld", sign, tm); break;
+ case 'M': appendf(&res, "%02lld", m); break;
+ case 's': appendf(&res, "%s%lld", sign, itime); break;
+ case 'S': appendf(&res, "%02lld", s); break;
+ case 'T': appendf(&res, "%03d", ms); break;
+ case 'f': appendf(&res, "%f", time); break;
+ case '%': appendf(&res, "%s", "%"); break;
+ default: goto error;
+ }
+ fmt++;
+ } else {
+ appendf(&res, "%c", *fmt);
+ fmt++;
+ }
+ }
+ return res;
+error:
+ talloc_free(res);
+ return NULL;
+}
+
+char *mp_format_time(double time, bool fractions)
+{
+ return mp_format_time_fmt(fractions ? "%H:%M:%S.%T" : "%H:%M:%S", time);
+}
+
+// Set rc to the union of rc and rc2
+void mp_rect_union(struct mp_rect *rc, const struct mp_rect *rc2)
+{
+ rc->x0 = MPMIN(rc->x0, rc2->x0);
+ rc->y0 = MPMIN(rc->y0, rc2->y0);
+ rc->x1 = MPMAX(rc->x1, rc2->x1);
+ rc->y1 = MPMAX(rc->y1, rc2->y1);
+}
+
+// Returns whether or not a point is contained by rc
+bool mp_rect_contains(struct mp_rect *rc, int x, int y)
+{
+ return rc->x0 <= x && x < rc->x1 && rc->y0 <= y && y < rc->y1;
+}
+
+// Set rc to the intersection of rc and src.
+// Return false if the result is empty.
+bool mp_rect_intersection(struct mp_rect *rc, const struct mp_rect *rc2)
+{
+ rc->x0 = MPMAX(rc->x0, rc2->x0);
+ rc->y0 = MPMAX(rc->y0, rc2->y0);
+ rc->x1 = MPMIN(rc->x1, rc2->x1);
+ rc->y1 = MPMIN(rc->y1, rc2->y1);
+
+ return rc->x1 > rc->x0 && rc->y1 > rc->y0;
+}
+
+bool mp_rect_equals(const struct mp_rect *rc1, const struct mp_rect *rc2)
+{
+ return rc1->x0 == rc2->x0 && rc1->y0 == rc2->y0 &&
+ rc1->x1 == rc2->x1 && rc1->y1 == rc2->y1;
+}
+
+// Rotate mp_rect by 90 degrees increments
+void mp_rect_rotate(struct mp_rect *rc, int w, int h, int rotation)
+{
+ rotation %= 360;
+
+ if (rotation >= 180) {
+ rotation -= 180;
+ MPSWAP(int, rc->x0, rc->x1);
+ MPSWAP(int, rc->y0, rc->y1);
+ }
+
+ if (rotation == 90) {
+ *rc = (struct mp_rect) {
+ .x0 = rc->y1,
+ .y0 = rc->x0,
+ .x1 = rc->y0,
+ .y1 = rc->x1,
+ };
+ }
+
+ if (rc->x1 < rc->x0) {
+ rc->x0 = w - rc->x0;
+ rc->x1 = w - rc->x1;
+ }
+
+ if (rc->y1 < rc->y0) {
+ rc->y0 = h - rc->y0;
+ rc->y1 = h - rc->y1;
+ }
+}
+
+// Compute rc1-rc2, put result in res_array, return number of rectangles in
+// res_array. In the worst case, there are 4 rectangles, so res_array must
+// provide that much storage space.
+int mp_rect_subtract(const struct mp_rect *rc1, const struct mp_rect *rc2,
+ struct mp_rect res[4])
+{
+ struct mp_rect rc = *rc1;
+ if (!mp_rect_intersection(&rc, rc2))
+ return 0;
+
+ int cnt = 0;
+ if (rc1->y0 < rc.y0)
+ res[cnt++] = (struct mp_rect){rc1->x0, rc1->y0, rc1->x1, rc.y0};
+ if (rc1->x0 < rc.x0)
+ res[cnt++] = (struct mp_rect){rc1->x0, rc.y0, rc.x0, rc.y1};
+ if (rc1->x1 > rc.x1)
+ res[cnt++] = (struct mp_rect){rc.x1, rc.y0, rc1->x1, rc.y1};
+ if (rc1->y1 > rc.y1)
+ res[cnt++] = (struct mp_rect){rc1->x0, rc.y1, rc1->x1, rc1->y1};
+ return cnt;
+}
+
+// This works like snprintf(), except that it starts writing the first output
+// character to str[strlen(str)]. This returns the number of characters the
+// string would have *appended* assuming a large enough buffer, will make sure
+// str is null-terminated, and will never write to str[size] or past.
+// Example:
+// int example(char *buf, size_t buf_size, double num, char *str) {
+// int n = 0;
+// n += mp_snprintf_cat(buf, size, "%f", num);
+// n += mp_snprintf_cat(buf, size, "%s", str);
+// return n; }
+// Note how this can be chained with functions similar in style.
+int mp_snprintf_cat(char *str, size_t size, const char *format, ...)
+{
+ size_t len = strnlen(str, size);
+ assert(!size || len < size); // str with no 0-termination is not allowed
+ int r;
+ va_list ap;
+ va_start(ap, format);
+ r = vsnprintf(str + len, size - len, format, ap);
+ va_end(ap);
+ return r;
+}
+
+// Encode the unicode codepoint as UTF-8, and append to the end of the
+// talloc'ed buffer. All guarantees bstr_xappend() give applies, such as
+// implicit \0-termination for convenience.
+void mp_append_utf8_bstr(void *talloc_ctx, struct bstr *buf, uint32_t codepoint)
+{
+ char data[8];
+ uint8_t tmp;
+ char *output = data;
+ PUT_UTF8(codepoint, tmp, *output++ = tmp;);
+ bstr_xappend(talloc_ctx, buf, (bstr){data, output - data});
+}
+
+// Parse a C/JSON-style escape beginning at code, and append the result to *str
+// using talloc. The input string (*code) must point to the first character
+// after the initial '\', and after parsing *code is set to the first character
+// after the current escape.
+// On error, false is returned, and all input remains unchanged.
+static bool mp_parse_escape(void *talloc_ctx, bstr *dst, bstr *code)
+{
+ if (code->len < 1)
+ return false;
+ char replace = 0;
+ switch (code->start[0]) {
+ case '"': replace = '"'; break;
+ case '\\': replace = '\\'; break;
+ case '/': replace = '/'; break;
+ case 'b': replace = '\b'; break;
+ case 'f': replace = '\f'; break;
+ case 'n': replace = '\n'; break;
+ case 'r': replace = '\r'; break;
+ case 't': replace = '\t'; break;
+ case 'e': replace = '\x1b'; break;
+ case '\'': replace = '\''; break;
+ }
+ if (replace) {
+ bstr_xappend(talloc_ctx, dst, (bstr){&replace, 1});
+ *code = bstr_cut(*code, 1);
+ return true;
+ }
+ if (code->start[0] == 'x' && code->len >= 3) {
+ bstr num = bstr_splice(*code, 1, 3);
+ char c = bstrtoll(num, &num, 16);
+ if (num.len)
+ return false;
+ bstr_xappend(talloc_ctx, dst, (bstr){&c, 1});
+ *code = bstr_cut(*code, 3);
+ return true;
+ }
+ if (code->start[0] == 'u' && code->len >= 5) {
+ bstr num = bstr_splice(*code, 1, 5);
+ uint32_t c = bstrtoll(num, &num, 16);
+ if (num.len)
+ return false;
+ if (c >= 0xd800 && c <= 0xdbff) {
+ if (code->len < 5 + 6 // udddd + \udddd
+ || code->start[5] != '\\' || code->start[6] != 'u')
+ return false;
+ *code = bstr_cut(*code, 5 + 1);
+ bstr num2 = bstr_splice(*code, 1, 5);
+ uint32_t c2 = bstrtoll(num2, &num2, 16);
+ if (num2.len || c2 < 0xdc00 || c2 > 0xdfff)
+ return false;
+ c = ((c - 0xd800) << 10) + 0x10000 + (c2 - 0xdc00);
+ }
+ mp_append_utf8_bstr(talloc_ctx, dst, c);
+ *code = bstr_cut(*code, 5);
+ return true;
+ }
+ return false;
+}
+
+// Like mp_append_escaped_string, but set *dst to sliced *src if no escape
+// sequences have to be parsed (i.e. no memory allocation is required), and
+// if dst->start was NULL on function entry.
+bool mp_append_escaped_string_noalloc(void *talloc_ctx, bstr *dst, bstr *src)
+{
+ bstr t = *src;
+ int cur = 0;
+ while (1) {
+ if (cur >= t.len || t.start[cur] == '"') {
+ *src = bstr_cut(t, cur);
+ t = bstr_splice(t, 0, cur);
+ if (dst->start == NULL) {
+ *dst = t;
+ } else {
+ bstr_xappend(talloc_ctx, dst, t);
+ }
+ return true;
+ } else if (t.start[cur] == '\\') {
+ bstr_xappend(talloc_ctx, dst, bstr_splice(t, 0, cur));
+ t = bstr_cut(t, cur + 1);
+ cur = 0;
+ if (!mp_parse_escape(talloc_ctx, dst, &t))
+ goto error;
+ } else {
+ cur++;
+ }
+ }
+error:
+ return false;
+}
+
+// src is expected to point to a C-style string literal, *src pointing to the
+// first char after the starting '"'. It will append the contents of the literal
+// to *dst (using talloc_ctx) until the first '"' or the end of *str is found.
+// See bstr_xappend() how data is appended to *dst.
+// On success, *src will either start with '"', or be empty.
+// On error, return false, and *dst will contain the string until the first
+// error, *src is not changed.
+// Note that dst->start will be implicitly \0-terminated on successful return,
+// and if it was NULL or \0-terminated before calling the function.
+// As mentioned above, the caller is responsible for skipping the '"' chars.
+bool mp_append_escaped_string(void *talloc_ctx, bstr *dst, bstr *src)
+{
+ if (mp_append_escaped_string_noalloc(talloc_ctx, dst, src)) {
+ // Guarantee copy (or allocation).
+ if (!dst->start || dst->start == src->start) {
+ bstr res = *dst;
+ *dst = (bstr){0};
+ bstr_xappend(talloc_ctx, dst, res);
+ }
+ return true;
+ }
+ return false;
+}
+
+// Behaves like strerror()/strerror_r(), but is thread- and GNU-safe.
+char *mp_strerror_buf(char *buf, size_t buf_size, int errnum)
+{
+ // This handles the nasty details of calling the right function for us.
+ av_strerror(AVERROR(errnum), buf, buf_size);
+ return buf;
+}
+
+char *mp_tag_str_buf(char *buf, size_t buf_size, uint32_t tag)
+{
+ if (buf_size < 1)
+ return buf;
+ buf[0] = '\0';
+ for (int n = 0; n < 4; n++) {
+ uint8_t val = (tag >> (n * 8)) & 0xFF;
+ if (mp_isalnum(val) || val == '_' || val == ' ') {
+ mp_snprintf_cat(buf, buf_size, "%c", val);
+ } else {
+ mp_snprintf_cat(buf, buf_size, "[%d]", val);
+ }
+ }
+ return buf;
+}
+
+char *mp_tprintf_buf(char *buf, size_t buf_size, const char *format, ...)
+{
+ va_list ap;
+ va_start(ap, format);
+ vsnprintf(buf, buf_size, format, ap);
+ va_end(ap);
+ return buf;
+}
+
+char **mp_dup_str_array(void *tctx, char **s)
+{
+ char **r = NULL;
+ int num_r = 0;
+ for (int n = 0; s && s[n]; n++)
+ MP_TARRAY_APPEND(tctx, r, num_r, talloc_strdup(tctx, s[n]));
+ if (r)
+ MP_TARRAY_APPEND(tctx, r, num_r, NULL);
+ return r;
+}
+
+// Return rounded down integer log 2 of v, i.e. position of highest set bit.
+// mp_log2(0) == 0
+// mp_log2(1) == 0
+// mp_log2(31) == 4
+// mp_log2(32) == 5
+unsigned int mp_log2(uint32_t v)
+{
+#if defined(__GNUC__) && __GNUC__ >= 4
+ return v ? 31 - __builtin_clz(v) : 0;
+#else
+ for (int x = 31; x >= 0; x--) {
+ if (v & (((uint32_t)1) << x))
+ return x;
+ }
+ return 0;
+#endif
+}
+
+// If a power of 2, return it, otherwise return the next highest one, or 0.
+// mp_round_next_power_of_2(65) == 128
+// mp_round_next_power_of_2(64) == 64
+// mp_round_next_power_of_2(0) == 1
+// mp_round_next_power_of_2(UINT32_MAX) == 0
+uint32_t mp_round_next_power_of_2(uint32_t v)
+{
+ if (!v)
+ return 1;
+ if (!(v & (v - 1)))
+ return v;
+ int l = mp_log2(v) + 1;
+ return l == 32 ? 0 : (uint32_t)1 << l;
+}
+
+int mp_lcm(int x, int y)
+{
+ assert(x && y);
+ return x * (y / av_gcd(x, y));
+}
diff --git a/common/common.h b/common/common.h
new file mode 100644
index 0000000..ccdd94b
--- /dev/null
+++ b/common/common.h
@@ -0,0 +1,161 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_MPCOMMON_H
+#define MPLAYER_MPCOMMON_H
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "osdep/compiler.h"
+#include "mpv_talloc.h"
+
+// double should be able to represent this exactly
+#define MP_NOPTS_VALUE (-0x1p+63)
+
+#define MP_CONCAT_(a, b) a ## b
+#define MP_CONCAT(a, b) MP_CONCAT_(a, b)
+
+#define MPMAX(a, b) ((a) > (b) ? (a) : (b))
+#define MPMIN(a, b) ((a) > (b) ? (b) : (a))
+#define MPCLAMP(a, min, max) (((a) < (min)) ? (min) : (((a) > (max)) ? (max) : (a)))
+#define MPSWAP(type, a, b) \
+ do { type SWAP_tmp = b; b = a; a = SWAP_tmp; } while (0)
+#define MP_ARRAY_SIZE(s) (sizeof(s) / sizeof((s)[0]))
+#define MP_DIV_UP(x, y) (((x) + (y) - 1) / (y))
+
+// align must be a power of two (align >= 1), x >= 0
+#define MP_ALIGN_UP(x, align) (((x) + (align) - 1) & ~((align) - 1))
+#define MP_ALIGN_DOWN(x, align) ((x) & ~((align) - 1))
+#define MP_IS_ALIGNED(x, align) (!((x) & ((align) - 1)))
+#define MP_IS_POWER_OF_2(x) ((x) > 0 && !((x) & ((x) - 1)))
+
+// align to non power of two
+#define MP_ALIGN_NPOT(x, align) ((align) ? MP_DIV_UP(x, align) * (align) : (x))
+
+// Return "a", or if that is NOPTS, return "def".
+#define MP_PTS_OR_DEF(a, def) ((a) == MP_NOPTS_VALUE ? (def) : (a))
+// If one of the values is NOPTS, always pick the other one.
+#define MP_PTS_MIN(a, b) MPMIN(MP_PTS_OR_DEF(a, b), MP_PTS_OR_DEF(b, a))
+#define MP_PTS_MAX(a, b) MPMAX(MP_PTS_OR_DEF(a, b), MP_PTS_OR_DEF(b, a))
+// Return a+b, unless a is NOPTS. b must not be NOPTS.
+#define MP_ADD_PTS(a, b) ((a) == MP_NOPTS_VALUE ? (a) : ((a) + (b)))
+
+#define CONTROL_OK 1
+#define CONTROL_TRUE 1
+#define CONTROL_FALSE 0
+#define CONTROL_UNKNOWN -1
+#define CONTROL_ERROR -2
+#define CONTROL_NA -3
+
+enum stream_type {
+ STREAM_VIDEO,
+ STREAM_AUDIO,
+ STREAM_SUB,
+ STREAM_TYPE_COUNT,
+};
+
+enum video_sync {
+ VS_DEFAULT = 0,
+ VS_DISP_RESAMPLE,
+ VS_DISP_RESAMPLE_VDROP,
+ VS_DISP_RESAMPLE_NONE,
+ VS_DISP_TEMPO,
+ VS_DISP_ADROP,
+ VS_DISP_VDROP,
+ VS_DISP_NONE,
+ VS_NONE,
+};
+
+#define VS_IS_DISP(x) ((x) == VS_DISP_RESAMPLE || \
+ (x) == VS_DISP_RESAMPLE_VDROP || \
+ (x) == VS_DISP_RESAMPLE_NONE || \
+ (x) == VS_DISP_TEMPO || \
+ (x) == VS_DISP_ADROP || \
+ (x) == VS_DISP_VDROP || \
+ (x) == VS_DISP_NONE)
+
+extern const char mpv_version[];
+extern const char mpv_builddate[];
+extern const char mpv_copyright[];
+
+char *mp_format_time(double time, bool fractions);
+char *mp_format_time_fmt(const char *fmt, double time);
+
+struct mp_rect {
+ int x0, y0;
+ int x1, y1;
+};
+
+#define mp_rect_w(r) ((r).x1 - (r).x0)
+#define mp_rect_h(r) ((r).y1 - (r).y0)
+
+void mp_rect_union(struct mp_rect *rc, const struct mp_rect *src);
+bool mp_rect_intersection(struct mp_rect *rc, const struct mp_rect *rc2);
+bool mp_rect_contains(struct mp_rect *rc, int x, int y);
+bool mp_rect_equals(const struct mp_rect *rc1, const struct mp_rect *rc2);
+int mp_rect_subtract(const struct mp_rect *rc1, const struct mp_rect *rc2,
+ struct mp_rect res_array[4]);
+void mp_rect_rotate(struct mp_rect *rc, int w, int h, int rotation);
+
+unsigned int mp_log2(uint32_t v);
+uint32_t mp_round_next_power_of_2(uint32_t v);
+int mp_lcm(int x, int y);
+
+int mp_snprintf_cat(char *str, size_t size, const char *format, ...)
+ PRINTF_ATTRIBUTE(3, 4);
+
+struct bstr;
+
+void mp_append_utf8_bstr(void *talloc_ctx, struct bstr *buf, uint32_t codepoint);
+
+bool mp_append_escaped_string_noalloc(void *talloc_ctx, struct bstr *dst,
+ struct bstr *src);
+bool mp_append_escaped_string(void *talloc_ctx, struct bstr *dst,
+ struct bstr *src);
+
+char *mp_strerror_buf(char *buf, size_t buf_size, int errnum);
+#define mp_strerror(e) mp_strerror_buf((char[80]){0}, 80, e)
+
+char *mp_tag_str_buf(char *buf, size_t buf_size, uint32_t tag);
+#define mp_tag_str(t) mp_tag_str_buf((char[22]){0}, 22, t)
+
+// Return a printf(format, ...) formatted string of the given SIZE. SIZE must
+// be a compile time constant. The result is allocated on the stack and valid
+// only within the current block scope.
+#define mp_tprintf(SIZE, format, ...) \
+ mp_tprintf_buf((char[SIZE]){0}, (SIZE), (format), __VA_ARGS__)
+char *mp_tprintf_buf(char *buf, size_t buf_size, const char *format, ...)
+ PRINTF_ATTRIBUTE(3, 4);
+
+char **mp_dup_str_array(void *tctx, char **s);
+
+// We generally do not handle allocation failure of small malloc()s. This would
+// create a large number of rarely tested code paths, which would probably
+// regress and cause security issues. We prefer to fail fast.
+// This macro generally behaves like an assert(), except it will make sure to
+// kill the process even with NDEBUG.
+#define MP_HANDLE_OOM(x) do { \
+ assert(x); \
+ if (!(x)) \
+ abort(); \
+ } while (0)
+
+#endif /* MPLAYER_MPCOMMON_H */
diff --git a/common/encode.h b/common/encode.h
new file mode 100644
index 0000000..33d778c
--- /dev/null
+++ b/common/encode.h
@@ -0,0 +1,61 @@
+/*
+ * Muxing/encoding API; ffmpeg specific implementation is in encode_lavc.*.
+ *
+ * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_ENCODE_H
+#define MPLAYER_ENCODE_H
+
+#include <stdbool.h>
+
+#include "demux/demux.h"
+
+struct mpv_global;
+struct mp_log;
+struct encode_lavc_context;
+
+struct encode_opts {
+ char *file;
+ char *format;
+ char **fopts;
+ char *vcodec;
+ char **vopts;
+ char *acodec;
+ char **aopts;
+ bool rawts;
+ bool copy_metadata;
+ char **set_metadata;
+ char **remove_metadata;
+};
+
+// interface for player core
+struct encode_lavc_context *encode_lavc_init(struct mpv_global *global);
+bool encode_lavc_free(struct encode_lavc_context *ctx);
+void encode_lavc_discontinuity(struct encode_lavc_context *ctx);
+bool encode_lavc_showhelp(struct mp_log *log, struct encode_opts *options);
+int encode_lavc_getstatus(struct encode_lavc_context *ctx, char *buf, int bufsize, float relative_position);
+bool encode_lavc_stream_type_ok(struct encode_lavc_context *ctx,
+ enum stream_type type);
+void encode_lavc_expect_stream(struct encode_lavc_context *ctx,
+ enum stream_type type);
+void encode_lavc_set_metadata(struct encode_lavc_context *ctx,
+ struct mp_tags *metadata);
+bool encode_lavc_didfail(struct encode_lavc_context *ctx); // check if encoding failed
+
+#endif
diff --git a/common/encode_lavc.c b/common/encode_lavc.c
new file mode 100644
index 0000000..898545d
--- /dev/null
+++ b/common/encode_lavc.c
@@ -0,0 +1,949 @@
+/*
+ * muxing using libavformat
+ *
+ * Copyright (C) 2010 Nicolas George <george@nsup.org>
+ * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/avutil.h>
+#include <libavutil/timestamp.h>
+
+#include "encode_lavc.h"
+#include "common/av_common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/options.h"
+#include "osdep/timer.h"
+#include "video/out/vo.h"
+#include "mpv_talloc.h"
+#include "stream/stream.h"
+
+struct encode_priv {
+ struct mp_log *log;
+
+ // --- All fields are protected by encode_lavc_context.lock
+
+ bool failed;
+
+ struct mp_tags *metadata;
+
+ AVFormatContext *muxer;
+
+ bool header_written; // muxer was initialized
+
+ struct mux_stream **streams;
+ int num_streams;
+
+ // Statistics
+ double t0;
+
+ long long abytes;
+ long long vbytes;
+
+ unsigned int frames;
+ double audioseconds;
+};
+
+struct mux_stream {
+ int index; // index of this into p->streams[]
+ char name[80];
+ struct encode_lavc_context *ctx;
+ enum AVMediaType codec_type;
+ AVRational encoder_timebase; // packet timestamps from encoder
+ AVStream *st;
+ void (*on_ready)(void *ctx); // when finishing muxer init
+ void *on_ready_ctx;
+};
+
+#define OPT_BASE_STRUCT struct encode_opts
+const struct m_sub_options encode_config = {
+ .opts = (const m_option_t[]) {
+ {"o", OPT_STRING(file), .flags = CONF_NOCFG | CONF_PRE_PARSE | M_OPT_FILE},
+ {"of", OPT_STRING(format)},
+ {"ofopts", OPT_KEYVALUELIST(fopts), .flags = M_OPT_HAVE_HELP},
+ {"ovc", OPT_STRING(vcodec)},
+ {"ovcopts", OPT_KEYVALUELIST(vopts), .flags = M_OPT_HAVE_HELP},
+ {"oac", OPT_STRING(acodec)},
+ {"oacopts", OPT_KEYVALUELIST(aopts), .flags = M_OPT_HAVE_HELP},
+ {"orawts", OPT_BOOL(rawts)},
+ {"ocopy-metadata", OPT_BOOL(copy_metadata)},
+ {"oset-metadata", OPT_KEYVALUELIST(set_metadata)},
+ {"oremove-metadata", OPT_STRINGLIST(remove_metadata)},
+ {0}
+ },
+ .size = sizeof(struct encode_opts),
+ .defaults = &(const struct encode_opts){
+ .copy_metadata = true,
+ },
+};
+
+struct encode_lavc_context *encode_lavc_init(struct mpv_global *global)
+{
+ struct encode_lavc_context *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct encode_lavc_context){
+ .global = global,
+ .options = mp_get_config_group(ctx, global, &encode_config),
+ .priv = talloc_zero(ctx, struct encode_priv),
+ .log = mp_log_new(ctx, global->log, "encode"),
+ };
+ mp_mutex_init(&ctx->lock);
+
+ struct encode_priv *p = ctx->priv;
+ p->log = ctx->log;
+
+ const char *filename = ctx->options->file;
+
+ // STUPID STUPID STUPID STUPID avio
+ // does not support "-" as file name to mean stdin/stdout
+ // ffmpeg.c works around this too, the same way
+ if (!strcmp(filename, "-"))
+ filename = "pipe:1";
+
+ if (filename && (
+ !strcmp(filename, "/dev/stdout") ||
+ !strcmp(filename, "pipe:") ||
+ !strcmp(filename, "pipe:1")))
+ mp_msg_force_stderr(global, true);
+
+ encode_lavc_discontinuity(ctx);
+
+ p->muxer = avformat_alloc_context();
+ MP_HANDLE_OOM(p->muxer);
+
+ if (ctx->options->format && ctx->options->format[0]) {
+ ctx->oformat = av_guess_format(ctx->options->format, filename, NULL);
+ } else {
+ ctx->oformat = av_guess_format(NULL, filename, NULL);
+ }
+
+ if (!ctx->oformat) {
+ MP_FATAL(ctx, "format not found\n");
+ goto fail;
+ }
+
+ p->muxer->oformat = ctx->oformat;
+
+ p->muxer->url = av_strdup(filename);
+ MP_HANDLE_OOM(p->muxer->url);
+
+ return ctx;
+
+fail:
+ p->failed = true;
+ encode_lavc_free(ctx);
+ return NULL;
+}
+
+void encode_lavc_set_metadata(struct encode_lavc_context *ctx,
+ struct mp_tags *metadata)
+{
+ struct encode_priv *p = ctx->priv;
+
+ mp_mutex_lock(&ctx->lock);
+
+ if (ctx->options->copy_metadata) {
+ p->metadata = mp_tags_dup(ctx, metadata);
+ } else {
+ p->metadata = talloc_zero(ctx, struct mp_tags);
+ }
+
+ if (ctx->options->set_metadata) {
+ char **kv = ctx->options->set_metadata;
+ // Set all user-provided metadata tags
+ for (int n = 0; kv[n * 2]; n++) {
+ MP_VERBOSE(ctx, "setting metadata value '%s' for key '%s'\n",
+ kv[n*2 + 0], kv[n*2 +1]);
+ mp_tags_set_str(p->metadata, kv[n*2 + 0], kv[n*2 +1]);
+ }
+ }
+
+ if (ctx->options->remove_metadata) {
+ char **k = ctx->options->remove_metadata;
+ // Remove all user-provided metadata tags
+ for (int n = 0; k[n]; n++) {
+ MP_VERBOSE(ctx, "removing metadata key '%s'\n", k[n]);
+ mp_tags_remove_str(p->metadata, k[n]);
+ }
+ }
+
+ mp_mutex_unlock(&ctx->lock);
+}
+
+bool encode_lavc_free(struct encode_lavc_context *ctx)
+{
+ bool res = true;
+ if (!ctx)
+ return res;
+
+ struct encode_priv *p = ctx->priv;
+
+ if (!p->failed && !p->header_written) {
+ MP_FATAL(p, "no data written to target file\n");
+ p->failed = true;
+ }
+
+ if (!p->failed && p->header_written) {
+ if (av_write_trailer(p->muxer) < 0)
+ MP_ERR(p, "error writing trailer\n");
+
+ MP_INFO(p, "video: encoded %lld bytes\n", p->vbytes);
+ MP_INFO(p, "audio: encoded %lld bytes\n", p->abytes);
+
+ MP_INFO(p, "muxing overhead %lld bytes\n",
+ (long long)(avio_size(p->muxer->pb) - p->vbytes - p->abytes));
+ }
+
+ if (avio_closep(&p->muxer->pb) < 0 && !p->failed) {
+ MP_ERR(p, "Closing file failed\n");
+ p->failed = true;
+ }
+
+ avformat_free_context(p->muxer);
+
+ res = !p->failed;
+
+ mp_mutex_destroy(&ctx->lock);
+ talloc_free(ctx);
+
+ return res;
+}
+
+// called locked
+static void maybe_init_muxer(struct encode_lavc_context *ctx)
+{
+ struct encode_priv *p = ctx->priv;
+
+ if (p->header_written || p->failed)
+ return;
+
+ // Check if all streams were initialized yet. We need data to know the
+ // AVStream parameters, so we wait for data from _all_ streams before
+ // starting.
+ for (int n = 0; n < p->num_streams; n++) {
+ if (!p->streams[n]->st)
+ return;
+ }
+
+ if (!(p->muxer->oformat->flags & AVFMT_NOFILE)) {
+ MP_INFO(p, "Opening output file: %s\n", p->muxer->url);
+
+ if (avio_open(&p->muxer->pb, p->muxer->url, AVIO_FLAG_WRITE) < 0) {
+ MP_FATAL(p, "could not open '%s'\n", p->muxer->url);
+ goto failed;
+ }
+ }
+
+ p->t0 = mp_time_sec();
+
+ MP_INFO(p, "Opening muxer: %s [%s]\n",
+ p->muxer->oformat->long_name, p->muxer->oformat->name);
+
+ if (p->metadata) {
+ for (int i = 0; i < p->metadata->num_keys; i++) {
+ av_dict_set(&p->muxer->metadata,
+ p->metadata->keys[i], p->metadata->values[i], 0);
+ }
+ }
+
+ AVDictionary *opts = NULL;
+ mp_set_avdict(&opts, ctx->options->fopts);
+
+ if (avformat_write_header(p->muxer, &opts) < 0) {
+ MP_FATAL(p, "Failed to initialize muxer.\n");
+ p->failed = true;
+ } else {
+ mp_avdict_print_unset(p->log, MSGL_WARN, opts);
+ }
+
+ av_dict_free(&opts);
+
+ if (p->failed)
+ goto failed;
+
+ p->header_written = true;
+
+ for (int n = 0; n < p->num_streams; n++) {
+ struct mux_stream *s = p->streams[n];
+
+ if (s->on_ready)
+ s->on_ready(s->on_ready_ctx);
+ }
+
+ return;
+
+failed:
+ p->failed = true;
+}
+
+// called locked
+static struct mux_stream *find_mux_stream(struct encode_lavc_context *ctx,
+ enum AVMediaType codec_type)
+{
+ struct encode_priv *p = ctx->priv;
+
+ for (int n = 0; n < p->num_streams; n++) {
+ struct mux_stream *s = p->streams[n];
+ if (s->codec_type == codec_type)
+ return s;
+ }
+
+ return NULL;
+}
+
+void encode_lavc_expect_stream(struct encode_lavc_context *ctx,
+ enum stream_type type)
+{
+ struct encode_priv *p = ctx->priv;
+
+ mp_mutex_lock(&ctx->lock);
+
+ enum AVMediaType codec_type = mp_to_av_stream_type(type);
+
+ // These calls are idempotent.
+ if (find_mux_stream(ctx, codec_type))
+ goto done;
+
+ if (p->header_written) {
+ MP_ERR(p, "Cannot add a stream during encoding.\n");
+ p->failed = true;
+ goto done;
+ }
+
+ struct mux_stream *dst = talloc_ptrtype(p, dst);
+ *dst = (struct mux_stream){
+ .index = p->num_streams,
+ .ctx = ctx,
+ .codec_type = mp_to_av_stream_type(type),
+ };
+ snprintf(dst->name, sizeof(dst->name), "%s", stream_type_name(type));
+ MP_TARRAY_APPEND(p, p->streams, p->num_streams, dst);
+
+done:
+ mp_mutex_unlock(&ctx->lock);
+}
+
+// Signal that you are ready to encode (you provide the codec params etc. too).
+// This returns a muxing handle which you can use to add encodec packets.
+// Can be called only once per stream. info is copied by callee as needed.
+static void encode_lavc_add_stream(struct encoder_context *enc,
+ struct encode_lavc_context *ctx,
+ struct encoder_stream_info *info,
+ void (*on_ready)(void *ctx),
+ void *on_ready_ctx)
+{
+ struct encode_priv *p = ctx->priv;
+
+ mp_mutex_lock(&ctx->lock);
+
+ struct mux_stream *dst = find_mux_stream(ctx, info->codecpar->codec_type);
+ if (!dst) {
+ MP_ERR(p, "Cannot add a stream at runtime.\n");
+ p->failed = true;
+ goto done;
+ }
+ if (dst->st) {
+ // Possibly via --gapless-audio, or explicitly recreating AO/VO.
+ MP_ERR(p, "Encoder was reinitialized; this is not allowed.\n");
+ p->failed = true;
+ dst = NULL;
+ goto done;
+ }
+
+ dst->st = avformat_new_stream(p->muxer, NULL);
+ MP_HANDLE_OOM(dst->st);
+
+ dst->encoder_timebase = info->timebase;
+ dst->st->time_base = info->timebase; // lavf will change this on muxer init
+ // Some muxers (e.g. Matroska one) expect the sample_aspect_ratio to be
+ // set on the AVStream.
+ if (info->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ dst->st->sample_aspect_ratio = info->codecpar->sample_aspect_ratio;
+
+ if (avcodec_parameters_copy(dst->st->codecpar, info->codecpar) < 0)
+ MP_HANDLE_OOM(0);
+
+ dst->on_ready = on_ready;
+ dst->on_ready_ctx = on_ready_ctx;
+ enc->mux_stream = dst;
+
+ maybe_init_muxer(ctx);
+
+done:
+ mp_mutex_unlock(&ctx->lock);
+}
+
+// Write a packet. This will take over ownership of `pkt`
+static void encode_lavc_add_packet(struct mux_stream *dst, AVPacket *pkt)
+{
+ struct encode_lavc_context *ctx = dst->ctx;
+ struct encode_priv *p = ctx->priv;
+
+ assert(dst->st);
+
+ mp_mutex_lock(&ctx->lock);
+
+ if (p->failed)
+ goto done;
+
+ if (!p->header_written) {
+ MP_ERR(p, "Encoder trying to write packet before muxer was initialized.\n");
+ p->failed = true;
+ goto done;
+ }
+
+ pkt->stream_index = dst->st->index;
+ assert(dst->st == p->muxer->streams[pkt->stream_index]);
+
+ av_packet_rescale_ts(pkt, dst->encoder_timebase, dst->st->time_base);
+
+ switch (dst->st->codecpar->codec_type) {
+ case AVMEDIA_TYPE_VIDEO:
+ p->vbytes += pkt->size;
+ p->frames += 1;
+ break;
+ case AVMEDIA_TYPE_AUDIO:
+ p->abytes += pkt->size;
+ p->audioseconds += pkt->duration
+ * (double)dst->st->time_base.num
+ / (double)dst->st->time_base.den;
+ break;
+ }
+
+ if (av_interleaved_write_frame(p->muxer, pkt) < 0) {
+ MP_ERR(p, "Writing packet failed.\n");
+ p->failed = true;
+ }
+
+ pkt = NULL;
+
+done:
+ mp_mutex_unlock(&ctx->lock);
+ if (pkt)
+ av_packet_unref(pkt);
+}
+
+AVRational encoder_get_mux_timebase_unlocked(struct encoder_context *p)
+{
+ return p->mux_stream->st->time_base;
+}
+
+void encode_lavc_discontinuity(struct encode_lavc_context *ctx)
+{
+ if (!ctx)
+ return;
+
+ mp_mutex_lock(&ctx->lock);
+ ctx->discontinuity_pts_offset = MP_NOPTS_VALUE;
+ mp_mutex_unlock(&ctx->lock);
+}
+
+static void encode_lavc_printoptions(struct mp_log *log, const void *obj,
+ const char *indent, const char *subindent,
+ const char *unit, int filter_and,
+ int filter_eq)
+{
+ const AVOption *opt = NULL;
+ char optbuf[32];
+ while ((opt = av_opt_next(obj, opt))) {
+ // if flags are 0, it simply hasn't been filled in yet and may be
+ // potentially useful
+ if (opt->flags)
+ if ((opt->flags & filter_and) != filter_eq)
+ continue;
+ /* Don't print CONST's on level one.
+ * Don't print anything but CONST's on level two.
+ * Only print items from the requested unit.
+ */
+ if (!unit && opt->type == AV_OPT_TYPE_CONST) {
+ continue;
+ } else if (unit && opt->type != AV_OPT_TYPE_CONST) {
+ continue;
+ } else if (unit && opt->type == AV_OPT_TYPE_CONST
+ && strcmp(unit, opt->unit))
+ {
+ continue;
+ } else if (unit && opt->type == AV_OPT_TYPE_CONST) {
+ mp_info(log, "%s", subindent);
+ } else {
+ mp_info(log, "%s", indent);
+ }
+
+ switch (opt->type) {
+ case AV_OPT_TYPE_FLAGS:
+ snprintf(optbuf, sizeof(optbuf), "%s=<flags>", opt->name);
+ break;
+ case AV_OPT_TYPE_INT:
+ snprintf(optbuf, sizeof(optbuf), "%s=<int>", opt->name);
+ break;
+ case AV_OPT_TYPE_INT64:
+ snprintf(optbuf, sizeof(optbuf), "%s=<int64>", opt->name);
+ break;
+ case AV_OPT_TYPE_DOUBLE:
+ snprintf(optbuf, sizeof(optbuf), "%s=<double>", opt->name);
+ break;
+ case AV_OPT_TYPE_FLOAT:
+ snprintf(optbuf, sizeof(optbuf), "%s=<float>", opt->name);
+ break;
+ case AV_OPT_TYPE_STRING:
+ snprintf(optbuf, sizeof(optbuf), "%s=<string>", opt->name);
+ break;
+ case AV_OPT_TYPE_RATIONAL:
+ snprintf(optbuf, sizeof(optbuf), "%s=<rational>", opt->name);
+ break;
+ case AV_OPT_TYPE_BINARY:
+ snprintf(optbuf, sizeof(optbuf), "%s=<binary>", opt->name);
+ break;
+ case AV_OPT_TYPE_CONST:
+ snprintf(optbuf, sizeof(optbuf), " [+-]%s", opt->name);
+ break;
+ default:
+ snprintf(optbuf, sizeof(optbuf), "%s", opt->name);
+ break;
+ }
+ optbuf[sizeof(optbuf) - 1] = 0;
+ mp_info(log, "%-32s ", optbuf);
+ if (opt->help)
+ mp_info(log, " %s", opt->help);
+ mp_info(log, "\n");
+ if (opt->unit && opt->type != AV_OPT_TYPE_CONST)
+ encode_lavc_printoptions(log, obj, indent, subindent, opt->unit,
+ filter_and, filter_eq);
+ }
+}
+
+bool encode_lavc_showhelp(struct mp_log *log, struct encode_opts *opts)
+{
+ bool help_output = false;
+#define CHECKS(str) ((str) && \
+ strcmp((str), "help") == 0 ? (help_output |= 1) : 0)
+#define CHECKV(strv) ((strv) && (strv)[0] && \
+ strcmp((strv)[0], "help") == 0 ? (help_output |= 1) : 0)
+ if (CHECKS(opts->format)) {
+ const AVOutputFormat *c = NULL;
+ void *iter = NULL;
+ mp_info(log, "Available output formats:\n");
+ while ((c = av_muxer_iterate(&iter))) {
+ mp_info(log, " --of=%-13s %s\n", c->name,
+ c->long_name ? c->long_name : "");
+ }
+ }
+ if (CHECKV(opts->fopts)) {
+ AVFormatContext *c = avformat_alloc_context();
+ const AVOutputFormat *format = NULL;
+ mp_info(log, "Available output format ctx->options:\n");
+ encode_lavc_printoptions(log, c, " --ofopts=", " ", NULL,
+ AV_OPT_FLAG_ENCODING_PARAM,
+ AV_OPT_FLAG_ENCODING_PARAM);
+ av_free(c);
+ void *iter = NULL;
+ while ((format = av_muxer_iterate(&iter))) {
+ if (format->priv_class) {
+ mp_info(log, "Additionally, for --of=%s:\n",
+ format->name);
+ encode_lavc_printoptions(log, &format->priv_class, " --ofopts=",
+ " ", NULL,
+ AV_OPT_FLAG_ENCODING_PARAM,
+ AV_OPT_FLAG_ENCODING_PARAM);
+ }
+ }
+ }
+ if (CHECKV(opts->vopts)) {
+ AVCodecContext *c = avcodec_alloc_context3(NULL);
+ const AVCodec *codec = NULL;
+ mp_info(log, "Available output video codec ctx->options:\n");
+ encode_lavc_printoptions(log,
+ c, " --ovcopts=", " ", NULL,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_VIDEO_PARAM,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_VIDEO_PARAM);
+ av_free(c);
+ void *iter = NULL;
+ while ((codec = av_codec_iterate(&iter))) {
+ if (!av_codec_is_encoder(codec))
+ continue;
+ if (codec->type != AVMEDIA_TYPE_VIDEO)
+ continue;
+ if (opts->vcodec && opts->vcodec[0] &&
+ strcmp(opts->vcodec, codec->name) != 0)
+ continue;
+ if (codec->priv_class) {
+ mp_info(log, "Additionally, for --ovc=%s:\n",
+ codec->name);
+ encode_lavc_printoptions(log,
+ &codec->priv_class, " --ovcopts=",
+ " ", NULL,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_VIDEO_PARAM,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_VIDEO_PARAM);
+ }
+ }
+ }
+ if (CHECKV(opts->aopts)) {
+ AVCodecContext *c = avcodec_alloc_context3(NULL);
+ const AVCodec *codec = NULL;
+ mp_info(log, "Available output audio codec ctx->options:\n");
+ encode_lavc_printoptions(log,
+ c, " --oacopts=", " ", NULL,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_AUDIO_PARAM,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_AUDIO_PARAM);
+ av_free(c);
+ void *iter = NULL;
+ while ((codec = av_codec_iterate(&iter))) {
+ if (!av_codec_is_encoder(codec))
+ continue;
+ if (codec->type != AVMEDIA_TYPE_AUDIO)
+ continue;
+ if (opts->acodec && opts->acodec[0] &&
+ strcmp(opts->acodec, codec->name) != 0)
+ continue;
+ if (codec->priv_class) {
+ mp_info(log, "Additionally, for --oac=%s:\n",
+ codec->name);
+ encode_lavc_printoptions(log,
+ &codec->priv_class, " --oacopts=",
+ " ", NULL,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_AUDIO_PARAM,
+ AV_OPT_FLAG_ENCODING_PARAM |
+ AV_OPT_FLAG_AUDIO_PARAM);
+ }
+ }
+ }
+ if (CHECKS(opts->vcodec)) {
+ const AVCodec *c = NULL;
+ void *iter = NULL;
+ mp_info(log, "Available output video codecs:\n");
+ while ((c = av_codec_iterate(&iter))) {
+ if (!av_codec_is_encoder(c))
+ continue;
+ if (c->type != AVMEDIA_TYPE_VIDEO)
+ continue;
+ mp_info(log, " --ovc=%-12s %s\n", c->name,
+ c->long_name ? c->long_name : "");
+ }
+ }
+ if (CHECKS(opts->acodec)) {
+ const AVCodec *c = NULL;
+ void *iter = NULL;
+ mp_info(log, "Available output audio codecs:\n");
+ while ((c = av_codec_iterate(&iter))) {
+ if (!av_codec_is_encoder(c))
+ continue;
+ if (c->type != AVMEDIA_TYPE_AUDIO)
+ continue;
+ mp_info(log, " --oac=%-12s %s\n", c->name,
+ c->long_name ? c->long_name : "");
+ }
+ }
+ return help_output;
+}
+
+int encode_lavc_getstatus(struct encode_lavc_context *ctx,
+ char *buf, int bufsize,
+ float relative_position)
+{
+ if (!ctx)
+ return -1;
+
+ struct encode_priv *p = ctx->priv;
+
+ double now = mp_time_sec();
+ float minutes, megabytes, fps, x;
+ float f = MPMAX(0.0001, relative_position);
+
+ mp_mutex_lock(&ctx->lock);
+
+ if (p->failed) {
+ snprintf(buf, bufsize, "(failed)\n");
+ goto done;
+ }
+
+ minutes = (now - p->t0) / 60.0 * (1 - f) / f;
+ megabytes = p->muxer->pb ? (avio_size(p->muxer->pb) / 1048576.0 / f) : 0;
+ fps = p->frames / (now - p->t0);
+ x = p->audioseconds / (now - p->t0);
+ if (p->frames) {
+ snprintf(buf, bufsize, "{%.1fmin %.1ffps %.1fMB}",
+ minutes, fps, megabytes);
+ } else if (p->audioseconds) {
+ snprintf(buf, bufsize, "{%.1fmin %.2fx %.1fMB}",
+ minutes, x, megabytes);
+ } else {
+ snprintf(buf, bufsize, "{%.1fmin %.1fMB}",
+ minutes, megabytes);
+ }
+ buf[bufsize - 1] = 0;
+
+done:
+ mp_mutex_unlock(&ctx->lock);
+ return 0;
+}
+
+bool encode_lavc_didfail(struct encode_lavc_context *ctx)
+{
+ if (!ctx)
+ return false;
+ mp_mutex_lock(&ctx->lock);
+ bool fail = ctx->priv->failed;
+ mp_mutex_unlock(&ctx->lock);
+ return fail;
+}
+
+static void encoder_destroy(void *ptr)
+{
+ struct encoder_context *p = ptr;
+
+ av_packet_free(&p->pkt);
+ avcodec_parameters_free(&p->info.codecpar);
+ avcodec_free_context(&p->encoder);
+ free_stream(p->twopass_bytebuffer);
+}
+
+static const AVCodec *find_codec_for(struct encode_lavc_context *ctx,
+ enum stream_type type, bool *used_auto)
+{
+ char *codec_name = type == STREAM_VIDEO
+ ? ctx->options->vcodec
+ : ctx->options->acodec;
+ enum AVMediaType codec_type = mp_to_av_stream_type(type);
+ const char *tname = stream_type_name(type);
+
+ *used_auto = !(codec_name && codec_name[0]);
+
+ const AVCodec *codec;
+ if (*used_auto) {
+ codec = avcodec_find_encoder(av_guess_codec(ctx->oformat, NULL,
+ ctx->options->file, NULL,
+ codec_type));
+ } else {
+ codec = avcodec_find_encoder_by_name(codec_name);
+ if (!codec)
+ MP_FATAL(ctx, "codec '%s' not found.\n", codec_name);
+ }
+
+ if (codec && codec->type != codec_type) {
+ MP_FATAL(ctx, "codec for %s has wrong media type\n", tname);
+ codec = NULL;
+ }
+
+ return codec;
+}
+
+// Return whether the stream type is "supposed" to work.
+bool encode_lavc_stream_type_ok(struct encode_lavc_context *ctx,
+ enum stream_type type)
+{
+ // If a codec was forced, let it proceed to actual encoding, and then error
+ // if it doesn't work. (Worried that av_guess_codec() may return NULL for
+ // some formats where a specific codec works anyway.)
+ bool auto_codec;
+ return !!find_codec_for(ctx, type, &auto_codec) || !auto_codec;
+}
+
+struct encoder_context *encoder_context_alloc(struct encode_lavc_context *ctx,
+ enum stream_type type,
+ struct mp_log *log)
+{
+ if (!ctx) {
+ mp_err(log, "the option --o (output file) must be specified\n");
+ return NULL;
+ }
+
+ struct encoder_context *p = talloc_ptrtype(NULL, p);
+ talloc_set_destructor(p, encoder_destroy);
+ *p = (struct encoder_context){
+ .global = ctx->global,
+ .options = ctx->options,
+ .oformat = ctx->oformat,
+ .type = type,
+ .log = log,
+ .encode_lavc_ctx = ctx,
+ };
+
+ bool auto_codec;
+ const AVCodec *codec = find_codec_for(ctx, type, &auto_codec);
+ const char *tname = stream_type_name(type);
+
+ if (!codec) {
+ if (auto_codec)
+ MP_FATAL(p, "codec for %s not found\n", tname);
+ goto fail;
+ }
+
+ p->encoder = avcodec_alloc_context3(codec);
+ MP_HANDLE_OOM(p->encoder);
+
+ return p;
+
+fail:
+ talloc_free(p);
+ return NULL;
+}
+
+static void encoder_2pass_prepare(struct encoder_context *p)
+{
+ char *filename = talloc_asprintf(NULL, "%s-%s-pass1.log",
+ p->options->file,
+ stream_type_name(p->type));
+
+ if (p->encoder->flags & AV_CODEC_FLAG_PASS2) {
+ MP_INFO(p, "Reading 2-pass log: %s\n", filename);
+ struct stream *s = stream_create(filename,
+ STREAM_ORIGIN_DIRECT | STREAM_READ,
+ NULL, p->global);
+ if (s) {
+ struct bstr content = stream_read_complete(s, p, 1000000000);
+ if (content.start) {
+ p->encoder->stats_in = content.start;
+ } else {
+ MP_WARN(p, "could not read '%s', "
+ "disabling 2-pass encoding at pass 1\n", filename);
+ }
+ free_stream(s);
+ } else {
+ MP_WARN(p, "could not open '%s', "
+ "disabling 2-pass encoding at pass 2\n", filename);
+ p->encoder->flags &= ~(unsigned)AV_CODEC_FLAG_PASS2;
+ }
+ }
+
+ if (p->encoder->flags & AV_CODEC_FLAG_PASS1) {
+ MP_INFO(p, "Writing to 2-pass log: %s\n", filename);
+ p->twopass_bytebuffer = open_output_stream(filename, p->global);
+ if (!p->twopass_bytebuffer) {
+ MP_WARN(p, "could not open '%s', "
+ "disabling 2-pass encoding at pass 1\n", filename);
+ p->encoder->flags &= ~(unsigned)AV_CODEC_FLAG_PASS1;
+ }
+ }
+
+ talloc_free(filename);
+}
+
+bool encoder_init_codec_and_muxer(struct encoder_context *p,
+ void (*on_ready)(void *ctx), void *ctx)
+{
+ assert(!avcodec_is_open(p->encoder));
+
+ char **copts = p->type == STREAM_VIDEO
+ ? p->options->vopts
+ : p->options->aopts;
+
+ // Set these now, so the code below can read back parsed settings from it.
+ mp_set_avopts(p->log, p->encoder, copts);
+
+ encoder_2pass_prepare(p);
+
+ if (p->oformat->flags & AVFMT_GLOBALHEADER)
+ p->encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+
+ MP_INFO(p, "Opening encoder: %s [%s]\n",
+ p->encoder->codec->long_name, p->encoder->codec->name);
+
+ if (p->encoder->codec->capabilities & AV_CODEC_CAP_EXPERIMENTAL) {
+ p->encoder->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
+ MP_WARN(p, "\n\n"
+ " ********************************************\n"
+ " **** Experimental codec selected! ****\n"
+ " ********************************************\n\n"
+ "This means the output file may be broken or bad.\n"
+ "Possible reasons, problems, workarounds:\n"
+ "- Codec implementation in ffmpeg/libav is not finished yet.\n"
+ " Try updating ffmpeg or libav.\n"
+ "- Bad picture quality, blocks, blurriness.\n"
+ " Experiment with codec settings to maybe still get the\n"
+ " desired quality output at the expense of bitrate.\n"
+ "- Broken files.\n"
+ " May not work at all, or break with other software.\n"
+ "- Slow compression.\n"
+ " Bear with it.\n"
+ "- Crashes.\n"
+ " Happens. Try varying options to work around.\n"
+ "If none of this helps you, try another codec in place of %s.\n\n",
+ p->encoder->codec->name);
+ }
+
+ if (avcodec_open2(p->encoder, p->encoder->codec, NULL) < 0) {
+ MP_FATAL(p, "Could not initialize encoder.\n");
+ goto fail;
+ }
+
+ p->info.timebase = p->encoder->time_base; // (_not_ changed by enc. init)
+ p->info.codecpar = avcodec_parameters_alloc();
+ MP_HANDLE_OOM(p->info.codecpar);
+ if (avcodec_parameters_from_context(p->info.codecpar, p->encoder) < 0)
+ goto fail;
+
+ p->pkt = av_packet_alloc();
+ MP_HANDLE_OOM(p->pkt);
+
+ encode_lavc_add_stream(p, p->encode_lavc_ctx, &p->info, on_ready, ctx);
+ if (!p->mux_stream)
+ goto fail;
+
+ return true;
+
+fail:
+ avcodec_close(p->encoder);
+ return false;
+}
+
+bool encoder_encode(struct encoder_context *p, AVFrame *frame)
+{
+ int status = avcodec_send_frame(p->encoder, frame);
+ if (status < 0) {
+ if (frame && status == AVERROR_EOF)
+ MP_ERR(p, "new data after sending EOF to encoder\n");
+ goto fail;
+ }
+
+ AVPacket *packet = p->pkt;
+ for (;;) {
+ status = avcodec_receive_packet(p->encoder, packet);
+ if (status == AVERROR(EAGAIN))
+ break;
+ if (status < 0 && status != AVERROR_EOF)
+ goto fail;
+
+ if (p->twopass_bytebuffer && p->encoder->stats_out) {
+ stream_write_buffer(p->twopass_bytebuffer, p->encoder->stats_out,
+ strlen(p->encoder->stats_out));
+ }
+
+ if (status == AVERROR_EOF)
+ break;
+
+ encode_lavc_add_packet(p->mux_stream, packet);
+ }
+
+ return true;
+
+fail:
+ MP_ERR(p, "error encoding at %s\n",
+ frame ? av_ts2timestr(frame->pts, &p->encoder->time_base) : "EOF");
+ return false;
+}
+
+// vim: ts=4 sw=4 et
diff --git a/common/encode_lavc.h b/common/encode_lavc.h
new file mode 100644
index 0000000..8517726
--- /dev/null
+++ b/common/encode_lavc.h
@@ -0,0 +1,114 @@
+/*
+ * muxing using libavformat
+ *
+ * Copyright (C) 2011 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_ENCODE_LAVC_H
+#define MPLAYER_ENCODE_LAVC_H
+
+
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/avstring.h>
+#include <libavutil/pixfmt.h>
+#include <libavutil/opt.h>
+#include <libavutil/mathematics.h>
+
+#include "common/common.h"
+#include "encode.h"
+#include "osdep/threads.h"
+#include "video/csputils.h"
+
+struct encode_lavc_context {
+ // --- Immutable after init
+ struct mpv_global *global;
+ struct encode_opts *options;
+ struct mp_log *log;
+ struct encode_priv *priv;
+ const AVOutputFormat *oformat;
+ const char *filename;
+
+ // All entry points must be guarded with the lock. Functions called by
+ // the playback core lock this automatically, but ao_lavc.c and vo_lavc.c
+ // must lock manually before accessing state.
+ mp_mutex lock;
+
+ // anti discontinuity mode
+ double next_in_pts;
+ double discontinuity_pts_offset;
+};
+
+// --- interface for vo/ao drivers
+
+// Static information after encoder init. This never changes (even if there are
+// dynamic runtime changes, they have to work over AVPacket side data).
+// For use in encoder_context, most fields are copied from encoder_context.encoder
+// by encoder_init_codec_and_muxer().
+struct encoder_stream_info {
+ AVRational timebase; // timebase used by the encoder (in frames/out packets)
+ AVCodecParameters *codecpar;
+};
+
+// The encoder parts for each stream (no muxing parts included).
+// This is private to each stream.
+struct encoder_context {
+ struct mpv_global *global;
+ struct encode_opts *options;
+ struct mp_log *log;
+ const AVOutputFormat *oformat;
+
+ // (avoid using this)
+ struct encode_lavc_context *encode_lavc_ctx;
+
+ enum stream_type type;
+
+ // (different access restrictions before/after encoder init)
+ struct encoder_stream_info info;
+ AVCodecContext *encoder;
+ struct mux_stream *mux_stream;
+
+ // (essentially private)
+ struct stream *twopass_bytebuffer;
+ AVPacket *pkt;
+};
+
+// Free with talloc_free(). (Keep in mind actual deinitialization requires
+// sending a flush packet.)
+// This can fail and return NULL.
+struct encoder_context *encoder_context_alloc(struct encode_lavc_context *ctx,
+ enum stream_type type,
+ struct mp_log *log);
+
+// After setting your codec parameters on p->encoder, you call this to "open"
+// the encoder. This also initializes p->mux_stream. Returns false on failure.
+// on_ready is called as soon as the muxer has been initialized. Then you are
+// allowed to write packets with encoder_encode().
+// Warning: the on_ready callback is called asynchronously, so you need to
+// make sure to properly synchronize everything.
+bool encoder_init_codec_and_muxer(struct encoder_context *p,
+ void (*on_ready)(void *ctx), void *ctx);
+
+// Encode the frame and write the packet. frame is ref'ed as need.
+bool encoder_encode(struct encoder_context *p, AVFrame *frame);
+
+// Return muxer timebase (only available after on_ready() has been called).
+// Caller needs to acquire encode_lavc_context.lock (or call it from on_ready).
+AVRational encoder_get_mux_timebase_unlocked(struct encoder_context *p);
+
+#endif
diff --git a/common/global.h b/common/global.h
new file mode 100644
index 0000000..f95cf28
--- /dev/null
+++ b/common/global.h
@@ -0,0 +1,15 @@
+#ifndef MPV_MPV_H
+#define MPV_MPV_H
+
+// This should be accessed by glue code only, never normal code.
+// The only purpose of this is to make mpv library-safe.
+// Think hard before adding new members.
+struct mpv_global {
+ struct mp_log *log;
+ struct m_config_shadow *config;
+ struct mp_client_api *client_api;
+ char *configdir;
+ struct stats_base *stats;
+};
+
+#endif
diff --git a/common/meson.build b/common/meson.build
new file mode 100644
index 0000000..4bca5ea
--- /dev/null
+++ b/common/meson.build
@@ -0,0 +1,8 @@
+version_h = vcs_tag(
+ command: ['git', 'describe', '--always', '--tags', '--dirty'],
+ input: 'version.h.in',
+ output: 'version.h',
+ replace_string: '@VERSION@',
+)
+
+sources += version_h
diff --git a/common/msg.c b/common/msg.c
new file mode 100644
index 0000000..b14bd5a
--- /dev/null
+++ b/common/msg.c
@@ -0,0 +1,1069 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdarg.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "mpv_talloc.h"
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/global.h"
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "osdep/terminal.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "libmpv/client.h"
+
+#include "msg.h"
+#include "msg_control.h"
+
+// log buffer size (lines) for terminal level and logfile level
+#define TERM_BUF 100
+#define FILE_BUF 100
+
+// logfile lines to accumulate during init before we know the log file name.
+// thousands of logfile lines during init can happen (especially with many
+// scripts, big config, etc), so we set 5000. If it cycles and messages are
+// overwritten, then the first (virtual) log line indicates how many were lost.
+#define EARLY_FILE_BUF 5000
+
+struct mp_log_root {
+ struct mpv_global *global;
+ mp_mutex lock;
+ mp_mutex log_file_lock;
+ mp_cond log_file_wakeup;
+ // --- protected by lock
+ char **msg_levels;
+ bool use_terminal; // make accesses to stderr/stdout
+ bool module;
+ bool show_time;
+ int blank_lines; // number of lines usable by status
+ int status_lines; // number of current status lines
+ bool color[STDERR_FILENO + 1];
+ bool isatty[STDERR_FILENO + 1];
+ int verbose;
+ bool really_quiet;
+ bool force_stderr;
+ struct mp_log_buffer **buffers;
+ int num_buffers;
+ struct mp_log_buffer *early_buffer;
+ struct mp_log_buffer *early_filebuffer;
+ FILE *stats_file;
+ bstr buffer;
+ bstr term_msg;
+ bstr term_msg_tmp;
+ bstr status_line;
+ struct mp_log *status_log;
+ bstr term_status_msg;
+ // --- must be accessed atomically
+ /* This is incremented every time the msglevels must be reloaded.
+ * (This is perhaps better than maintaining a globally accessible and
+ * synchronized mp_log tree.) */
+ atomic_ulong reload_counter;
+ // --- owner thread only (caller of mp_msg_init() etc.)
+ char *log_path;
+ char *stats_path;
+ mp_thread log_file_thread;
+ // --- owner thread only, but frozen while log_file_thread is running
+ FILE *log_file;
+ struct mp_log_buffer *log_file_buffer;
+ // --- protected by log_file_lock
+ bool log_file_thread_active; // also termination signal for the thread
+ int module_indent;
+};
+
+struct mp_log {
+ struct mp_log_root *root;
+ const char *prefix;
+ const char *verbose_prefix;
+ int max_level; // minimum log level for this instance
+ int level; // minimum log level for any outputs
+ int terminal_level; // minimum log level for terminal output
+ atomic_ulong reload_counter;
+ bstr partial[MSGL_MAX + 1];
+};
+
+struct mp_log_buffer {
+ struct mp_log_root *root;
+ mp_mutex lock;
+ // --- protected by lock
+ struct mp_log_buffer_entry **entries; // ringbuffer
+ int capacity; // total space in entries[]
+ int entry0; // first (oldest) entry index
+ int num_entries; // number of valid entries after entry0
+ uint64_t dropped; // number of skipped entries
+ bool silent;
+ // --- immutable
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_cb_ctx;
+ int level;
+};
+
+static const struct mp_log null_log = {0};
+struct mp_log *const mp_null_log = (struct mp_log *)&null_log;
+
+static bool match_mod(const char *name, const char *mod)
+{
+ if (!strcmp(mod, "all"))
+ return true;
+ // Path prefix matches
+ bstr b = bstr0(name);
+ return bstr_eatstart0(&b, mod) && (bstr_eatstart0(&b, "/") || !b.len);
+}
+
+static void update_loglevel(struct mp_log *log)
+{
+ struct mp_log_root *root = log->root;
+ mp_mutex_lock(&root->lock);
+ log->level = MSGL_STATUS + root->verbose; // default log level
+ if (root->really_quiet)
+ log->level = -1;
+ for (int n = 0; root->msg_levels && root->msg_levels[n * 2 + 0]; n++) {
+ if (match_mod(log->verbose_prefix, root->msg_levels[n * 2 + 0]))
+ log->level = mp_msg_find_level(root->msg_levels[n * 2 + 1]);
+ }
+ log->terminal_level = log->level;
+ for (int n = 0; n < log->root->num_buffers; n++) {
+ int buffer_level = log->root->buffers[n]->level;
+ if (buffer_level == MP_LOG_BUFFER_MSGL_LOGFILE)
+ buffer_level = MSGL_DEBUG;
+ if (buffer_level != MP_LOG_BUFFER_MSGL_TERM)
+ log->level = MPMAX(log->level, buffer_level);
+ }
+ if (log->root->log_file)
+ log->level = MPMAX(log->level, MSGL_DEBUG);
+ if (log->root->stats_file)
+ log->level = MPMAX(log->level, MSGL_STATS);
+ log->level = MPMIN(log->level, log->max_level);
+ atomic_store(&log->reload_counter, atomic_load(&log->root->reload_counter));
+ mp_mutex_unlock(&root->lock);
+}
+
+// Set (numerically) the maximum level that should still be output for this log
+// instances. E.g. lev=MSGL_WARN => show only warnings and errors.
+void mp_msg_set_max_level(struct mp_log *log, int lev)
+{
+ if (!log->root)
+ return;
+ mp_mutex_lock(&log->root->lock);
+ log->max_level = MPCLAMP(lev, -1, MSGL_MAX);
+ mp_mutex_unlock(&log->root->lock);
+ update_loglevel(log);
+}
+
+// Get the current effective msg level.
+// Thread-safety: see mp_msg().
+int mp_msg_level(struct mp_log *log)
+{
+ struct mp_log_root *root = log->root;
+ if (!root)
+ return -1;
+ if (atomic_load_explicit(&log->reload_counter, memory_order_relaxed) !=
+ atomic_load_explicit(&root->reload_counter, memory_order_relaxed))
+ {
+ update_loglevel(log);
+ }
+ return log->level;
+}
+
+static inline int term_msg_fileno(struct mp_log_root *root, int lev)
+{
+ return (root->force_stderr || lev == MSGL_STATUS || lev == MSGL_FATAL ||
+ lev == MSGL_ERR || lev == MSGL_WARN) ? STDERR_FILENO : STDOUT_FILENO;
+}
+
+// Reposition cursor and clear lines for outputting the status line. In certain
+// cases, like term OSD and subtitle display, the status can consist of
+// multiple lines.
+static void prepare_prefix(struct mp_log_root *root, bstr *out, int lev, int term_lines)
+{
+ int new_lines = lev == MSGL_STATUS ? term_lines : 0;
+ out->len = 0;
+
+ if (!root->isatty[term_msg_fileno(root, lev)]) {
+ if (root->status_lines)
+ bstr_xappend(root, out, bstr0("\n"));
+ root->status_lines = new_lines;
+ return;
+ }
+
+ // Set cursor state
+ if (new_lines && !root->status_lines) {
+ bstr_xappend(root, out, bstr0("\033[?25l"));
+ } else if (!new_lines && root->status_lines) {
+ bstr_xappend(root, out, bstr0("\033[?25h"));
+ }
+
+ int line_skip = 0;
+ if (root->status_lines) {
+ // Clear previous status line
+ bstr_xappend(root, out, bstr0("\033[1K\r"));
+ bstr up_clear = bstr0("\033[A\033[K");
+ for (int i = 1; i < root->status_lines; ++i)
+ bstr_xappend(root, out, up_clear);
+ // Reposition cursor after last message
+ line_skip = (new_lines ? new_lines : root->blank_lines) - root->status_lines;
+ line_skip = MPMIN(root->blank_lines - root->status_lines, line_skip);
+ if (line_skip)
+ bstr_xappend_asprintf(root, out, "\033[%dA", line_skip);
+ } else if (new_lines) {
+ line_skip = new_lines - root->blank_lines;
+ }
+
+ if (line_skip < 0) {
+ // Reposition cursor to keep status line at the same line
+ line_skip = MPMIN(root->blank_lines, -line_skip);
+ if (line_skip)
+ bstr_xappend_asprintf(root, out, "\033[%dB", line_skip);
+ }
+
+ root->blank_lines = MPMAX(0, root->blank_lines - term_lines);
+ root->status_lines = new_lines;
+ root->blank_lines += root->status_lines;
+}
+
+void mp_msg_flush_status_line(struct mp_log *log)
+{
+ if (log->root) {
+ mp_mutex_lock(&log->root->lock);
+ if (log->root->status_lines) {
+ bstr term_msg = (bstr){0};
+ prepare_prefix(log->root, &term_msg, MSGL_STATUS, 0);
+ if (term_msg.len) {
+ fprintf(stderr, "%.*s", BSTR_P(term_msg));
+ talloc_free(term_msg.start);
+ }
+ }
+ mp_mutex_unlock(&log->root->lock);
+ }
+}
+
+void mp_msg_set_term_title(struct mp_log *log, const char *title)
+{
+ if (log->root && title) {
+ // Lock because printf to terminal is not necessarily atomic.
+ mp_mutex_lock(&log->root->lock);
+ fprintf(stderr, "\e]0;%s\007", title);
+ mp_mutex_unlock(&log->root->lock);
+ }
+}
+
+bool mp_msg_has_status_line(struct mpv_global *global)
+{
+ struct mp_log_root *root = global->log->root;
+ mp_mutex_lock(&root->lock);
+ bool r = root->status_lines > 0;
+ mp_mutex_unlock(&root->lock);
+ return r;
+}
+
+static void set_term_color(void *talloc_ctx, bstr *text, int c)
+{
+ return c == -1 ? bstr_xappend(talloc_ctx, text, bstr0("\033[0m"))
+ : bstr_xappend_asprintf(talloc_ctx, text,
+ "\033[%d;3%dm", c >> 3, c & 7);
+}
+
+static void set_msg_color(void *talloc_ctx, bstr *text, int lev)
+{
+ static const int v_colors[] = {9, 1, 3, -1, -1, 2, 8, 8, 8, -1};
+ return set_term_color(talloc_ctx, text, v_colors[lev]);
+}
+
+static void pretty_print_module(struct mp_log_root *root, bstr *text,
+ const char *prefix, int lev)
+{
+ size_t prefix_len = strlen(prefix);
+ root->module_indent = MPMAX(10, MPMAX(root->module_indent, prefix_len));
+ bool color = root->color[term_msg_fileno(root, lev)];
+
+ // Use random color based on the name of the module
+ if (color) {
+ unsigned int mod = 0;
+ for (int i = 0; i < prefix_len; ++i)
+ mod = mod * 33 + prefix[i];
+ set_term_color(root, text, (mod + 1) % 15 + 1);
+ }
+
+ bstr_xappend_asprintf(root, text, "%*s", root->module_indent, prefix);
+ if (color)
+ set_term_color(root, text, -1);
+ bstr_xappend(root, text, bstr0(": "));
+ if (color)
+ set_msg_color(root, text, lev);
+}
+
+static bool test_terminal_level(struct mp_log *log, int lev)
+{
+ return lev <= log->terminal_level && log->root->use_terminal &&
+ !(lev == MSGL_STATUS && terminal_in_background());
+}
+
+// This is very basic way to infer needed width for a string.
+static int term_disp_width(bstr str, size_t start, size_t end)
+{
+ int width = 0;
+ bool escape = false;
+
+ const char *line = str.start;
+ for (size_t i = start; i < end && i < str.len; ++i) {
+ if (escape) {
+ escape = !(line[i] >= '@' && line[i] <= '~');
+ continue;
+ }
+
+ if (line[i] == '\033' && line[i + 1] == '[') {
+ escape = true;
+ ++i;
+ continue;
+ }
+
+ if (line[i] == '\n')
+ continue;
+
+ width++;
+
+ // Assume that everything before \r should be discarded for simplicity
+ if (line[i] == '\r')
+ width = 0;
+ }
+
+ return width;
+}
+
+static void append_terminal_line(struct mp_log *log, int lev,
+ bstr text, bstr *term_msg, int *line_w)
+{
+ struct mp_log_root *root = log->root;
+
+ size_t start = term_msg->len;
+
+ if (root->show_time)
+ bstr_xappend_asprintf(root, term_msg, "[%10.6f] ", mp_time_sec());
+
+ const char *log_prefix = (lev >= MSGL_V) || root->verbose || root->module
+ ? log->verbose_prefix : log->prefix;
+ if (log_prefix) {
+ if (root->module) {
+ pretty_print_module(root, term_msg, log_prefix, lev);
+ } else {
+ bstr_xappend_asprintf(root, term_msg, "[%s] ", log_prefix);
+ }
+ }
+
+ bstr_xappend(root, term_msg, text);
+ *line_w = root->isatty[term_msg_fileno(root, lev)]
+ ? term_disp_width(*term_msg, start, term_msg->len) : 0;
+}
+
+static struct mp_log_buffer_entry *log_buffer_read(struct mp_log_buffer *buffer)
+{
+ assert(buffer->num_entries);
+ struct mp_log_buffer_entry *res = buffer->entries[buffer->entry0];
+ buffer->entry0 = (buffer->entry0 + 1) % buffer->capacity;
+ buffer->num_entries -= 1;
+ return res;
+}
+
+static void write_msg_to_buffers(struct mp_log *log, int lev, bstr text)
+{
+ struct mp_log_root *root = log->root;
+ for (int n = 0; n < root->num_buffers; n++) {
+ struct mp_log_buffer *buffer = root->buffers[n];
+ bool wakeup = false;
+ mp_mutex_lock(&buffer->lock);
+ int buffer_level = buffer->level;
+ if (buffer_level == MP_LOG_BUFFER_MSGL_TERM)
+ buffer_level = log->terminal_level;
+ if (buffer_level == MP_LOG_BUFFER_MSGL_LOGFILE)
+ buffer_level = MPMAX(log->terminal_level, MSGL_DEBUG);
+ if (lev <= buffer_level && lev != MSGL_STATUS) {
+ if (buffer->level == MP_LOG_BUFFER_MSGL_LOGFILE) {
+ // If the buffer is full, block until we can write again,
+ // unless there's no write thread (died, or early filebuffer)
+ bool dead = false;
+ while (buffer->num_entries == buffer->capacity && !dead) {
+ // Temporary unlock is OK; buffer->level is immutable, and
+ // buffer can't go away because the global log lock is held.
+ mp_mutex_unlock(&buffer->lock);
+ mp_mutex_lock(&root->log_file_lock);
+ if (root->log_file_thread_active) {
+ mp_cond_wait(&root->log_file_wakeup,
+ &root->log_file_lock);
+ } else {
+ dead = true;
+ }
+ mp_mutex_unlock(&root->log_file_lock);
+ mp_mutex_lock(&buffer->lock);
+ }
+ }
+ if (buffer->num_entries == buffer->capacity) {
+ struct mp_log_buffer_entry *skip = log_buffer_read(buffer);
+ talloc_free(skip);
+ buffer->dropped += 1;
+ }
+ struct mp_log_buffer_entry *entry = talloc_ptrtype(NULL, entry);
+ *entry = (struct mp_log_buffer_entry) {
+ .prefix = talloc_strdup(entry, log->verbose_prefix),
+ .level = lev,
+ .text = bstrdup0(entry, text),
+ };
+ int pos = (buffer->entry0 + buffer->num_entries) % buffer->capacity;
+ buffer->entries[pos] = entry;
+ buffer->num_entries += 1;
+ if (buffer->wakeup_cb && !buffer->silent)
+ wakeup = true;
+ }
+ mp_mutex_unlock(&buffer->lock);
+ if (wakeup)
+ buffer->wakeup_cb(buffer->wakeup_cb_ctx);
+ }
+}
+
+static void dump_stats(struct mp_log *log, int lev, bstr text)
+{
+ struct mp_log_root *root = log->root;
+ if (lev == MSGL_STATS && root->stats_file)
+ fprintf(root->stats_file, "%"PRId64" %.*s\n", mp_time_ns(), BSTR_P(text));
+}
+
+static void write_term_msg(struct mp_log *log, int lev, bstr text, bstr *out)
+{
+ struct mp_log_root *root = log->root;
+ bool print_term = test_terminal_level(log, lev);
+ int fileno = term_msg_fileno(root, lev);
+ int term_w = 0, term_h = 0;
+ if (print_term && root->isatty[fileno])
+ terminal_get_size(&term_w, &term_h);
+
+ out->len = 0;
+
+ // Split away each line. Normally we require full lines; buffer partial
+ // lines if they happen.
+ root->term_msg_tmp.len = 0;
+ int term_msg_lines = 0;
+
+ bstr str = text;
+ while (str.len) {
+ bstr line = bstr_getline(str, &str);
+ if (line.start[line.len - 1] != '\n') {
+ assert(str.len == 0);
+ str = line;
+ break;
+ }
+
+ if (print_term) {
+ int line_w;
+ append_terminal_line(log, lev, line, &root->term_msg_tmp, &line_w);
+ term_msg_lines += (!line_w || !term_w)
+ ? 1 : (line_w + term_w - 1) / term_w;
+ }
+ write_msg_to_buffers(log, lev, line);
+ }
+
+ if (lev == MSGL_STATUS && print_term) {
+ int line_w = 0;
+ if (str.len)
+ append_terminal_line(log, lev, str, &root->term_msg_tmp, &line_w);
+ term_msg_lines += !term_w ? (str.len ? 1 : 0)
+ : (line_w + term_w - 1) / term_w;
+ } else if (str.len) {
+ bstr_xappend(NULL, &log->partial[lev], str);
+ }
+
+ if (print_term && (root->term_msg_tmp.len || lev == MSGL_STATUS)) {
+ prepare_prefix(root, out, lev, term_msg_lines);
+ if (root->color[fileno] && root->term_msg_tmp.len) {
+ set_msg_color(root, out, lev);
+ set_term_color(root, &root->term_msg_tmp, -1);
+ }
+ bstr_xappend(root, out, root->term_msg_tmp);
+ }
+}
+
+void mp_msg_va(struct mp_log *log, int lev, const char *format, va_list va)
+{
+ if (!mp_msg_test(log, lev))
+ return; // do not display
+
+ struct mp_log_root *root = log->root;
+
+ mp_mutex_lock(&root->lock);
+
+ root->buffer.len = 0;
+
+ if (log->partial[lev].len)
+ bstr_xappend(root, &root->buffer, log->partial[lev]);
+ log->partial[lev].len = 0;
+
+ bstr_xappend_vasprintf(root, &root->buffer, format, va);
+
+ // Remember last status message and restore it to ensure that it is
+ // always displayed
+ if (lev == MSGL_STATUS) {
+ root->status_log = log;
+ root->status_line.len = 0;
+ // Use bstr_xappend instead bstrdup to reuse allocated memory
+ if (root->buffer.len)
+ bstr_xappend(root, &root->status_line, root->buffer);
+ }
+
+ if (lev == MSGL_STATS) {
+ dump_stats(log, lev, root->buffer);
+ } else if (lev == MSGL_STATUS && !test_terminal_level(log, lev)) {
+ /* discard */
+ } else {
+ write_term_msg(log, lev, root->buffer, &root->term_msg);
+
+ root->term_status_msg.len = 0;
+ if (lev != MSGL_STATUS && root->status_line.len && root->status_log &&
+ test_terminal_level(root->status_log, MSGL_STATUS))
+ {
+ write_term_msg(root->status_log, MSGL_STATUS, root->status_line,
+ &root->term_status_msg);
+ }
+
+ int fileno = term_msg_fileno(root, lev);
+ FILE *stream = fileno == STDERR_FILENO ? stderr : stdout;
+ if (root->term_msg.len) {
+ if (root->term_status_msg.len) {
+ fprintf(stream, "%.*s%.*s", BSTR_P(root->term_msg),
+ BSTR_P(root->term_status_msg));
+ } else {
+ fprintf(stream, "%.*s", BSTR_P(root->term_msg));
+ }
+ fflush(stream);
+ }
+ }
+
+ mp_mutex_unlock(&root->lock);
+}
+
+static void destroy_log(void *ptr)
+{
+ struct mp_log *log = ptr;
+ // This is not managed via talloc itself, because mp_msg calls must be
+ // thread-safe, while talloc is not thread-safe.
+ for (int lvl = 0; lvl <= MSGL_MAX; ++lvl)
+ talloc_free(log->partial[lvl].start);
+}
+
+// Create a new log context, which uses talloc_ctx as talloc parent, and parent
+// as logical parent.
+// The name is the prefix put before the output. It's usually prefixed by the
+// parent's name. If the name starts with "/", the parent's name is not
+// prefixed (except in verbose mode), and if it starts with "!", the name is
+// not printed at all (except in verbose mode).
+// If name is NULL, the parent's name/prefix is used.
+// Thread-safety: fully thread-safe, but keep in mind that talloc is not (so
+// talloc_ctx must be owned by the current thread).
+struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent,
+ const char *name)
+{
+ assert(parent);
+ struct mp_log *log = talloc_zero(talloc_ctx, struct mp_log);
+ if (!parent->root)
+ return log; // same as null_log
+ talloc_set_destructor(log, destroy_log);
+ log->root = parent->root;
+ log->max_level = MSGL_MAX;
+ if (name) {
+ if (name[0] == '!') {
+ name = &name[1];
+ } else if (name[0] == '/') {
+ name = &name[1];
+ log->prefix = talloc_strdup(log, name);
+ } else {
+ log->prefix = parent->prefix
+ ? talloc_asprintf(log, "%s/%s", parent->prefix, name)
+ : talloc_strdup(log, name);
+ }
+ log->verbose_prefix = parent->prefix
+ ? talloc_asprintf(log, "%s/%s", parent->prefix, name)
+ : talloc_strdup(log, name);
+ if (log->prefix && !log->prefix[0])
+ log->prefix = NULL;
+ if (!log->verbose_prefix[0])
+ log->verbose_prefix = "global";
+ } else {
+ log->prefix = talloc_strdup(log, parent->prefix);
+ log->verbose_prefix = talloc_strdup(log, parent->verbose_prefix);
+ }
+ return log;
+}
+
+void mp_msg_init(struct mpv_global *global)
+{
+ assert(!global->log);
+
+ struct mp_log_root *root = talloc_zero(NULL, struct mp_log_root);
+ *root = (struct mp_log_root){
+ .global = global,
+ .reload_counter = 1,
+ };
+
+ mp_mutex_init(&root->lock);
+ mp_mutex_init(&root->log_file_lock);
+ mp_cond_init(&root->log_file_wakeup);
+
+ struct mp_log dummy = { .root = root };
+ struct mp_log *log = mp_log_new(root, &dummy, "");
+
+ global->log = log;
+}
+
+static MP_THREAD_VOID log_file_thread(void *p)
+{
+ struct mp_log_root *root = p;
+
+ mp_thread_set_name("log");
+
+ mp_mutex_lock(&root->log_file_lock);
+
+ while (root->log_file_thread_active) {
+ struct mp_log_buffer_entry *e =
+ mp_msg_log_buffer_read(root->log_file_buffer);
+ if (e) {
+ mp_mutex_unlock(&root->log_file_lock);
+ fprintf(root->log_file, "[%8.3f][%c][%s] %s",
+ mp_time_sec(),
+ mp_log_levels[e->level][0], e->prefix, e->text);
+ fflush(root->log_file);
+ mp_mutex_lock(&root->log_file_lock);
+ talloc_free(e);
+ // Multiple threads might be blocked if the log buffer was full.
+ mp_cond_broadcast(&root->log_file_wakeup);
+ } else {
+ mp_cond_wait(&root->log_file_wakeup, &root->log_file_lock);
+ }
+ }
+
+ mp_mutex_unlock(&root->log_file_lock);
+
+ MP_THREAD_RETURN();
+}
+
+static void wakeup_log_file(void *p)
+{
+ struct mp_log_root *root = p;
+
+ mp_mutex_lock(&root->log_file_lock);
+ mp_cond_broadcast(&root->log_file_wakeup);
+ mp_mutex_unlock(&root->log_file_lock);
+}
+
+// Only to be called from the main thread.
+static void terminate_log_file_thread(struct mp_log_root *root)
+{
+ bool wait_terminate = false;
+
+ mp_mutex_lock(&root->log_file_lock);
+ if (root->log_file_thread_active) {
+ root->log_file_thread_active = false;
+ mp_cond_broadcast(&root->log_file_wakeup);
+ wait_terminate = true;
+ }
+ mp_mutex_unlock(&root->log_file_lock);
+
+ if (wait_terminate)
+ mp_thread_join(root->log_file_thread);
+
+ mp_msg_log_buffer_destroy(root->log_file_buffer);
+ root->log_file_buffer = NULL;
+
+ if (root->log_file)
+ fclose(root->log_file);
+ root->log_file = NULL;
+}
+
+// If opt is different from *current_path, update *current_path and return true.
+// No lock must be held; passed values must be accessible without.
+static bool check_new_path(struct mpv_global *global, char *opt,
+ char **current_path)
+{
+ void *tmp = talloc_new(NULL);
+ bool res = false;
+
+ char *new_path = mp_get_user_path(tmp, global, opt);
+ if (!new_path)
+ new_path = "";
+
+ char *old_path = *current_path ? *current_path : "";
+ if (strcmp(old_path, new_path) != 0) {
+ talloc_free(*current_path);
+ *current_path = NULL;
+ if (new_path[0])
+ *current_path = talloc_strdup(NULL, new_path);
+ res = true;
+ }
+
+ talloc_free(tmp);
+
+ return res;
+}
+
+void mp_msg_update_msglevels(struct mpv_global *global, struct MPOpts *opts)
+{
+ struct mp_log_root *root = global->log->root;
+
+ mp_mutex_lock(&root->lock);
+
+ root->verbose = opts->verbose;
+ root->really_quiet = opts->msg_really_quiet;
+ root->module = opts->msg_module;
+ root->use_terminal = opts->use_terminal;
+ root->show_time = opts->msg_time;
+ for (int i = STDOUT_FILENO; i <= STDERR_FILENO && root->use_terminal; ++i) {
+ root->isatty[i] = isatty(i);
+ root->color[i] = opts->msg_color && root->isatty[i];
+ }
+
+ m_option_type_msglevels.free(&root->msg_levels);
+ m_option_type_msglevels.copy(NULL, &root->msg_levels, &opts->msg_levels);
+
+ atomic_fetch_add(&root->reload_counter, 1);
+ mp_mutex_unlock(&root->lock);
+
+ if (check_new_path(global, opts->log_file, &root->log_path)) {
+ terminate_log_file_thread(root);
+ if (root->log_path) {
+ root->log_file = fopen(root->log_path, "wb");
+ if (root->log_file) {
+
+ // if a logfile is created and the early filebuf still exists,
+ // flush and destroy the early buffer
+ mp_mutex_lock(&root->lock);
+ struct mp_log_buffer *earlybuf = root->early_filebuffer;
+ if (earlybuf)
+ root->early_filebuffer = NULL; // but it still logs msgs
+ mp_mutex_unlock(&root->lock);
+
+ if (earlybuf) {
+ // flush, destroy before creating the normal logfile buf,
+ // as once the new one is created (specifically, its write
+ // thread), then MSGL_LOGFILE messages become blocking, but
+ // the early logfile buf is without dequeue - can deadlock.
+ // note: timestamp is unknown, we use 0.000 as indication.
+ // note: new messages while iterating are still flushed.
+ struct mp_log_buffer_entry *e;
+ while ((e = mp_msg_log_buffer_read(earlybuf))) {
+ fprintf(root->log_file, "[%8.3f][%c][%s] %s", 0.0,
+ mp_log_levels[e->level][0], e->prefix, e->text);
+ talloc_free(e);
+ }
+ mp_msg_log_buffer_destroy(earlybuf); // + remove from root
+ }
+
+ root->log_file_buffer =
+ mp_msg_log_buffer_new(global, FILE_BUF, MP_LOG_BUFFER_MSGL_LOGFILE,
+ wakeup_log_file, root);
+ root->log_file_thread_active = true;
+ if (mp_thread_create(&root->log_file_thread, log_file_thread,
+ root))
+ {
+ root->log_file_thread_active = false;
+ terminate_log_file_thread(root);
+ }
+ } else {
+ mp_err(global->log, "Failed to open log file '%s'\n",
+ root->log_path);
+ }
+ }
+ }
+
+ if (check_new_path(global, opts->dump_stats, &root->stats_path)) {
+ bool open_error = false;
+
+ mp_mutex_lock(&root->lock);
+ if (root->stats_file)
+ fclose(root->stats_file);
+ root->stats_file = NULL;
+ if (root->stats_path) {
+ root->stats_file = fopen(root->stats_path, "wb");
+ open_error = !root->stats_file;
+ }
+ mp_mutex_unlock(&root->lock);
+
+ if (open_error) {
+ mp_err(global->log, "Failed to open stats file '%s'\n",
+ root->stats_path);
+ }
+ }
+}
+
+void mp_msg_force_stderr(struct mpv_global *global, bool force_stderr)
+{
+ struct mp_log_root *root = global->log->root;
+
+ mp_mutex_lock(&root->lock);
+ root->force_stderr = force_stderr;
+ mp_mutex_unlock(&root->lock);
+}
+
+// Only to be called from the main thread.
+bool mp_msg_has_log_file(struct mpv_global *global)
+{
+ struct mp_log_root *root = global->log->root;
+
+ return !!root->log_file;
+}
+
+void mp_msg_uninit(struct mpv_global *global)
+{
+ struct mp_log_root *root = global->log->root;
+ mp_msg_flush_status_line(global->log);
+ terminate_log_file_thread(root);
+ mp_msg_log_buffer_destroy(root->early_buffer);
+ mp_msg_log_buffer_destroy(root->early_filebuffer);
+ assert(root->num_buffers == 0);
+ if (root->stats_file)
+ fclose(root->stats_file);
+ talloc_free(root->stats_path);
+ talloc_free(root->log_path);
+ m_option_type_msglevels.free(&root->msg_levels);
+ mp_mutex_destroy(&root->lock);
+ mp_mutex_destroy(&root->log_file_lock);
+ mp_cond_destroy(&root->log_file_wakeup);
+ talloc_free(root);
+ global->log = NULL;
+}
+
+// early logging store log messages before they have a known destination.
+// there are two early log buffers which are similar logically, and both cease
+// function (if still exist, independently) once the log destination is known,
+// or mpv init is complete (typically, after all clients/scripts init is done).
+//
+// - "normal" early_buffer, holds early terminal-level logs, and is handed over
+// to the first client which requests such log buffer, so that it sees older
+// messages too. further clients which request a log buffer get a new one
+// which accumulates messages starting at this point in time.
+//
+// - early_filebuffer - early log-file messages until a log file name is known.
+// main cases where meaningful messages are accumulated before the filename
+// is known are when log-file is set at mpv.conf, or from script/client init.
+// once a file name is known, the early buffer is flushed and destroyed.
+// unlike the "proper" log-file buffer, the early filebuffer is not backed by
+// a write thread, and hence non-blocking (can overwrite old messages).
+// it's also bigger than the actual file buffer (early: 5000, actual: 100).
+
+static void mp_msg_set_early_logging_raw(struct mpv_global *global, bool enable,
+ struct mp_log_buffer **root_logbuf,
+ int size, int level)
+{
+ struct mp_log_root *root = global->log->root;
+ mp_mutex_lock(&root->lock);
+
+ if (enable != !!*root_logbuf) {
+ if (enable) {
+ mp_mutex_unlock(&root->lock);
+ struct mp_log_buffer *buf =
+ mp_msg_log_buffer_new(global, size, level, NULL, NULL);
+ mp_mutex_lock(&root->lock);
+ assert(!*root_logbuf); // no concurrent calls to this function
+ *root_logbuf = buf;
+ } else {
+ struct mp_log_buffer *buf = *root_logbuf;
+ *root_logbuf = NULL;
+ mp_mutex_unlock(&root->lock);
+ mp_msg_log_buffer_destroy(buf);
+ return;
+ }
+ }
+
+ mp_mutex_unlock(&root->lock);
+}
+
+void mp_msg_set_early_logging(struct mpv_global *global, bool enable)
+{
+ struct mp_log_root *root = global->log->root;
+
+ mp_msg_set_early_logging_raw(global, enable, &root->early_buffer,
+ TERM_BUF, MP_LOG_BUFFER_MSGL_TERM);
+
+ // normally MSGL_LOGFILE buffer gets a write thread, but not the early buf
+ mp_msg_set_early_logging_raw(global, enable, &root->early_filebuffer,
+ EARLY_FILE_BUF, MP_LOG_BUFFER_MSGL_LOGFILE);
+}
+
+struct mp_log_buffer *mp_msg_log_buffer_new(struct mpv_global *global,
+ int size, int level,
+ void (*wakeup_cb)(void *ctx),
+ void *wakeup_cb_ctx)
+{
+ struct mp_log_root *root = global->log->root;
+
+ mp_mutex_lock(&root->lock);
+
+ if (level == MP_LOG_BUFFER_MSGL_TERM) {
+ size = TERM_BUF;
+
+ // The first thing which creates a terminal-level log buffer gets the
+ // early log buffer, if it exists. This is supposed to enable a script
+ // to grab log messages from before it was initialized. It's OK that
+ // this works only for 1 script and only once.
+ if (root->early_buffer) {
+ struct mp_log_buffer *buffer = root->early_buffer;
+ root->early_buffer = NULL;
+ buffer->wakeup_cb = wakeup_cb;
+ buffer->wakeup_cb_ctx = wakeup_cb_ctx;
+ mp_mutex_unlock(&root->lock);
+ return buffer;
+ }
+ }
+
+ assert(size > 0);
+
+ struct mp_log_buffer *buffer = talloc_ptrtype(NULL, buffer);
+ *buffer = (struct mp_log_buffer) {
+ .root = root,
+ .level = level,
+ .entries = talloc_array(buffer, struct mp_log_buffer_entry *, size),
+ .capacity = size,
+ .wakeup_cb = wakeup_cb,
+ .wakeup_cb_ctx = wakeup_cb_ctx,
+ };
+
+ mp_mutex_init(&buffer->lock);
+
+ MP_TARRAY_APPEND(root, root->buffers, root->num_buffers, buffer);
+
+ atomic_fetch_add(&root->reload_counter, 1);
+ mp_mutex_unlock(&root->lock);
+
+ return buffer;
+}
+
+void mp_msg_log_buffer_set_silent(struct mp_log_buffer *buffer, bool silent)
+{
+ mp_mutex_lock(&buffer->lock);
+ buffer->silent = silent;
+ mp_mutex_unlock(&buffer->lock);
+}
+
+void mp_msg_log_buffer_destroy(struct mp_log_buffer *buffer)
+{
+ if (!buffer)
+ return;
+
+ struct mp_log_root *root = buffer->root;
+
+ mp_mutex_lock(&root->lock);
+
+ for (int n = 0; n < root->num_buffers; n++) {
+ if (root->buffers[n] == buffer) {
+ MP_TARRAY_REMOVE_AT(root->buffers, root->num_buffers, n);
+ goto found;
+ }
+ }
+
+ MP_ASSERT_UNREACHABLE();
+
+found:
+
+ while (buffer->num_entries)
+ talloc_free(log_buffer_read(buffer));
+
+ mp_mutex_destroy(&buffer->lock);
+ talloc_free(buffer);
+
+ atomic_fetch_add(&root->reload_counter, 1);
+ mp_mutex_unlock(&root->lock);
+}
+
+// Return a queued message, or if the buffer is empty, NULL.
+// Thread-safety: one buffer can be read by a single thread only.
+struct mp_log_buffer_entry *mp_msg_log_buffer_read(struct mp_log_buffer *buffer)
+{
+ struct mp_log_buffer_entry *res = NULL;
+
+ mp_mutex_lock(&buffer->lock);
+
+ if (!buffer->silent && buffer->num_entries) {
+ if (buffer->dropped) {
+ res = talloc_ptrtype(NULL, res);
+ *res = (struct mp_log_buffer_entry) {
+ .prefix = "overflow",
+ .level = MSGL_FATAL,
+ .text = talloc_asprintf(res,
+ "log message buffer overflow: %"PRId64" messages skipped\n",
+ buffer->dropped),
+ };
+ buffer->dropped = 0;
+ } else {
+ res = log_buffer_read(buffer);
+ }
+ }
+
+ mp_mutex_unlock(&buffer->lock);
+
+ return res;
+}
+
+// Thread-safety: fully thread-safe, but keep in mind that the lifetime of
+// log must be guaranteed during the call.
+// Never call this from signal handlers.
+void mp_msg(struct mp_log *log, int lev, const char *format, ...)
+{
+ va_list va;
+ va_start(va, format);
+ mp_msg_va(log, lev, format, va);
+ va_end(va);
+}
+
+const char *const mp_log_levels[MSGL_MAX + 1] = {
+ [MSGL_FATAL] = "fatal",
+ [MSGL_ERR] = "error",
+ [MSGL_WARN] = "warn",
+ [MSGL_INFO] = "info",
+ [MSGL_STATUS] = "status",
+ [MSGL_V] = "v",
+ [MSGL_DEBUG] = "debug",
+ [MSGL_TRACE] = "trace",
+ [MSGL_STATS] = "stats",
+};
+
+const int mp_mpv_log_levels[MSGL_MAX + 1] = {
+ [MSGL_FATAL] = MPV_LOG_LEVEL_FATAL,
+ [MSGL_ERR] = MPV_LOG_LEVEL_ERROR,
+ [MSGL_WARN] = MPV_LOG_LEVEL_WARN,
+ [MSGL_INFO] = MPV_LOG_LEVEL_INFO,
+ [MSGL_STATUS] = 0, // never used
+ [MSGL_V] = MPV_LOG_LEVEL_V,
+ [MSGL_DEBUG] = MPV_LOG_LEVEL_DEBUG,
+ [MSGL_TRACE] = MPV_LOG_LEVEL_TRACE,
+ [MSGL_STATS] = 0, // never used
+};
+
+int mp_msg_find_level(const char *s)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(mp_log_levels); n++) {
+ if (mp_log_levels[n] && !strcasecmp(s, mp_log_levels[n]))
+ return n;
+ }
+ return -1;
+}
diff --git a/common/msg.h b/common/msg.h
new file mode 100644
index 0000000..b0cec7b
--- /dev/null
+++ b/common/msg.h
@@ -0,0 +1,92 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_MP_MSG_H
+#define MPLAYER_MP_MSG_H
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "osdep/compiler.h"
+
+struct mp_log;
+
+// A mp_log instance that never outputs anything.
+extern struct mp_log *const mp_null_log;
+
+// Verbosity levels.
+enum {
+ MSGL_FATAL, // only errors (difference to MSGL_ERR isn't too clear)
+ MSGL_ERR, // only errors
+ MSGL_WARN, // only warnings
+ MSGL_INFO, // what you normally see on the terminal
+ MSGL_STATUS, // exclusively for the playback status line (-quiet disables)
+ MSGL_V, // -v | slightly more information than default
+ MSGL_DEBUG, // -v -v | full debug information; this and numerically below
+ // should not produce "per frame" output
+ MSGL_TRACE, // -v -v -v | anything that might flood the terminal
+ MSGL_STATS, // dumping fine grained stats (--dump-stats)
+
+ MSGL_MAX = MSGL_STATS,
+};
+
+struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent,
+ const char *name);
+
+void mp_msg(struct mp_log *log, int lev, const char *format, ...)
+ PRINTF_ATTRIBUTE(3, 4);
+void mp_msg_va(struct mp_log *log, int lev, const char *format, va_list va);
+
+int mp_msg_level(struct mp_log *log);
+
+static inline bool mp_msg_test(struct mp_log *log, int lev)
+{
+ return lev <= mp_msg_level(log);
+}
+
+void mp_msg_set_max_level(struct mp_log *log, int lev);
+
+// Convenience macros.
+#define mp_fatal(log, ...) mp_msg(log, MSGL_FATAL, __VA_ARGS__)
+#define mp_err(log, ...) mp_msg(log, MSGL_ERR, __VA_ARGS__)
+#define mp_warn(log, ...) mp_msg(log, MSGL_WARN, __VA_ARGS__)
+#define mp_info(log, ...) mp_msg(log, MSGL_INFO, __VA_ARGS__)
+#define mp_verbose(log, ...) mp_msg(log, MSGL_V, __VA_ARGS__)
+#define mp_dbg(log, ...) mp_msg(log, MSGL_DEBUG, __VA_ARGS__)
+#define mp_trace(log, ...) mp_msg(log, MSGL_TRACE, __VA_ARGS__)
+
+// Convenience macros, typically called with a pointer to a context struct
+// as first argument, which has a "struct mp_log *log;" member.
+
+#define MP_MSG(obj, lev, ...) mp_msg((obj)->log, lev, __VA_ARGS__)
+
+#define MP_FATAL(obj, ...) MP_MSG(obj, MSGL_FATAL, __VA_ARGS__)
+#define MP_ERR(obj, ...) MP_MSG(obj, MSGL_ERR, __VA_ARGS__)
+#define MP_WARN(obj, ...) MP_MSG(obj, MSGL_WARN, __VA_ARGS__)
+#define MP_INFO(obj, ...) MP_MSG(obj, MSGL_INFO, __VA_ARGS__)
+#define MP_VERBOSE(obj, ...) MP_MSG(obj, MSGL_V, __VA_ARGS__)
+#define MP_DBG(obj, ...) MP_MSG(obj, MSGL_DEBUG, __VA_ARGS__)
+#define MP_TRACE(obj, ...) MP_MSG(obj, MSGL_TRACE, __VA_ARGS__)
+
+// This is a bit special. See TOOLS/stats-conv.py what rules text passed
+// to these functions should follow. Also see --dump-stats.
+#define mp_stats(obj, ...) mp_msg(obj, MSGL_STATS, __VA_ARGS__)
+#define MP_STATS(obj, ...) MP_MSG(obj, MSGL_STATS, __VA_ARGS__)
+
+#endif /* MPLAYER_MP_MSG_H */
diff --git a/common/msg_control.h b/common/msg_control.h
new file mode 100644
index 0000000..e4da59e
--- /dev/null
+++ b/common/msg_control.h
@@ -0,0 +1,45 @@
+#ifndef MP_MSG_CONTROL_H
+#define MP_MSG_CONTROL_H
+
+#include <stdbool.h>
+#include "common/msg.h"
+
+struct mpv_global;
+struct MPOpts;
+void mp_msg_init(struct mpv_global *global);
+void mp_msg_uninit(struct mpv_global *global);
+void mp_msg_update_msglevels(struct mpv_global *global, struct MPOpts *opts);
+void mp_msg_force_stderr(struct mpv_global *global, bool force_stderr);
+bool mp_msg_has_status_line(struct mpv_global *global);
+bool mp_msg_has_log_file(struct mpv_global *global);
+void mp_msg_set_early_logging(struct mpv_global *global, bool enable);
+
+void mp_msg_flush_status_line(struct mp_log *log);
+void mp_msg_set_term_title(struct mp_log *log, const char *title);
+
+struct mp_log_buffer_entry {
+ char *prefix;
+ int level;
+ char *text;
+};
+
+// Use --msg-level option for log level of this log buffer
+#define MP_LOG_BUFFER_MSGL_TERM (MSGL_MAX + 1)
+// For --log-file; --msg-level, but at least MSGL_DEBUG
+#define MP_LOG_BUFFER_MSGL_LOGFILE (MSGL_MAX + 2)
+
+struct mp_log_buffer;
+struct mp_log_buffer *mp_msg_log_buffer_new(struct mpv_global *global,
+ int size, int level,
+ void (*wakeup_cb)(void *ctx),
+ void *wakeup_cb_ctx);
+void mp_msg_log_buffer_destroy(struct mp_log_buffer *buffer);
+struct mp_log_buffer_entry *mp_msg_log_buffer_read(struct mp_log_buffer *buffer);
+void mp_msg_log_buffer_set_silent(struct mp_log_buffer *buffer, bool silent);
+
+int mp_msg_find_level(const char *s);
+
+extern const char *const mp_log_levels[MSGL_MAX + 1];
+extern const int mp_mpv_log_levels[MSGL_MAX + 1];
+
+#endif
diff --git a/common/playlist.c b/common/playlist.c
new file mode 100644
index 0000000..c1636bc
--- /dev/null
+++ b/common/playlist.c
@@ -0,0 +1,413 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include "playlist.h"
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "misc/random.h"
+#include "mpv_talloc.h"
+#include "options/path.h"
+
+#include "demux/demux.h"
+#include "stream/stream.h"
+
+struct playlist_entry *playlist_entry_new(const char *filename)
+{
+ struct playlist_entry *e = talloc_zero(NULL, struct playlist_entry);
+ char *local_filename = mp_file_url_to_filename(e, bstr0(filename));
+ e->filename = local_filename ? local_filename : talloc_strdup(e, filename);
+ e->stream_flags = STREAM_ORIGIN_DIRECT;
+ e->original_index = -1;
+ return e;
+}
+
+void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value)
+{
+ struct playlist_param p = {bstrdup(e, name), bstrdup(e, value)};
+ MP_TARRAY_APPEND(e, e->params, e->num_params, p);
+}
+
+void playlist_entry_add_params(struct playlist_entry *e,
+ struct playlist_param *params,
+ int num_params)
+{
+ for (int n = 0; n < num_params; n++)
+ playlist_entry_add_param(e, params[n].name, params[n].value);
+}
+
+static void playlist_update_indexes(struct playlist *pl, int start, int end)
+{
+ start = MPMAX(start, 0);
+ end = end < 0 ? pl->num_entries : MPMIN(end, pl->num_entries);
+
+ for (int n = start; n < end; n++)
+ pl->entries[n]->pl_index = n;
+}
+
+void playlist_add(struct playlist *pl, struct playlist_entry *add)
+{
+ assert(add->filename);
+ MP_TARRAY_APPEND(pl, pl->entries, pl->num_entries, add);
+ add->pl = pl;
+ add->pl_index = pl->num_entries - 1;
+ add->id = ++pl->id_alloc;
+ talloc_steal(pl, add);
+}
+
+void playlist_entry_unref(struct playlist_entry *e)
+{
+ e->reserved--;
+ if (e->reserved < 0) {
+ assert(!e->pl);
+ talloc_free(e);
+ }
+}
+
+void playlist_remove(struct playlist *pl, struct playlist_entry *entry)
+{
+ assert(pl && entry->pl == pl);
+
+ if (pl->current == entry) {
+ pl->current = playlist_entry_get_rel(entry, 1);
+ pl->current_was_replaced = true;
+ }
+
+ MP_TARRAY_REMOVE_AT(pl->entries, pl->num_entries, entry->pl_index);
+ playlist_update_indexes(pl, entry->pl_index, -1);
+
+ entry->pl = NULL;
+ entry->pl_index = -1;
+ ta_set_parent(entry, NULL);
+
+ entry->removed = true;
+ playlist_entry_unref(entry);
+}
+
+void playlist_clear(struct playlist *pl)
+{
+ for (int n = pl->num_entries - 1; n >= 0; n--)
+ playlist_remove(pl, pl->entries[n]);
+ assert(!pl->current);
+ pl->current_was_replaced = false;
+}
+
+void playlist_clear_except_current(struct playlist *pl)
+{
+ for (int n = pl->num_entries - 1; n >= 0; n--) {
+ if (pl->entries[n] != pl->current)
+ playlist_remove(pl, pl->entries[n]);
+ }
+}
+
+// Moves the entry so that it takes "at"'s place (or move to end, if at==NULL).
+void playlist_move(struct playlist *pl, struct playlist_entry *entry,
+ struct playlist_entry *at)
+{
+ if (entry == at)
+ return;
+
+ assert(entry && entry->pl == pl);
+ assert(!at || at->pl == pl);
+
+ int index = at ? at->pl_index : pl->num_entries;
+ MP_TARRAY_INSERT_AT(pl, pl->entries, pl->num_entries, index, entry);
+
+ int old_index = entry->pl_index;
+ if (old_index >= index)
+ old_index += 1;
+ MP_TARRAY_REMOVE_AT(pl->entries, pl->num_entries, old_index);
+
+ playlist_update_indexes(pl, MPMIN(index - 1, old_index - 1),
+ MPMAX(index + 1, old_index + 1));
+}
+
+void playlist_add_file(struct playlist *pl, const char *filename)
+{
+ playlist_add(pl, playlist_entry_new(filename));
+}
+
+void playlist_populate_playlist_path(struct playlist *pl, const char *path)
+{
+ for (int n = 0; n < pl->num_entries; n++) {
+ struct playlist_entry *e = pl->entries[n];
+ e->playlist_path = talloc_strdup(e, path);
+ }
+}
+
+void playlist_shuffle(struct playlist *pl)
+{
+ for (int n = 0; n < pl->num_entries; n++)
+ pl->entries[n]->original_index = n;
+ for (int n = 0; n < pl->num_entries - 1; n++) {
+ size_t j = (size_t)((pl->num_entries - n) * mp_rand_next_double());
+ MPSWAP(struct playlist_entry *, pl->entries[n], pl->entries[n + j]);
+ }
+ playlist_update_indexes(pl, 0, -1);
+}
+
+#define CMP_INT(a, b) ((a) == (b) ? 0 : ((a) > (b) ? 1 : -1))
+
+static int cmp_unshuffle(const void *a, const void *b)
+{
+ struct playlist_entry *ea = *(struct playlist_entry **)a;
+ struct playlist_entry *eb = *(struct playlist_entry **)b;
+
+ if (ea->original_index >= 0 && ea->original_index != eb->original_index)
+ return CMP_INT(ea->original_index, eb->original_index);
+ return CMP_INT(ea->pl_index, eb->pl_index);
+}
+
+void playlist_unshuffle(struct playlist *pl)
+{
+ if (pl->num_entries)
+ qsort(pl->entries, pl->num_entries, sizeof(pl->entries[0]), cmp_unshuffle);
+ playlist_update_indexes(pl, 0, -1);
+}
+
+// (Explicitly ignores current_was_replaced.)
+struct playlist_entry *playlist_get_first(struct playlist *pl)
+{
+ return pl->num_entries ? pl->entries[0] : NULL;
+}
+
+// (Explicitly ignores current_was_replaced.)
+struct playlist_entry *playlist_get_last(struct playlist *pl)
+{
+ return pl->num_entries ? pl->entries[pl->num_entries - 1] : NULL;
+}
+
+struct playlist_entry *playlist_get_next(struct playlist *pl, int direction)
+{
+ assert(direction == -1 || direction == +1);
+ if (!pl->current)
+ return NULL;
+ assert(pl->current->pl == pl);
+ if (direction < 0)
+ return playlist_entry_get_rel(pl->current, -1);
+ return pl->current_was_replaced ? pl->current :
+ playlist_entry_get_rel(pl->current, 1);
+}
+
+// (Explicitly ignores current_was_replaced.)
+struct playlist_entry *playlist_entry_get_rel(struct playlist_entry *e,
+ int direction)
+{
+ assert(direction == -1 || direction == +1);
+ if (!e->pl)
+ return NULL;
+ return playlist_entry_from_index(e->pl, e->pl_index + direction);
+}
+
+struct playlist_entry *playlist_get_first_in_next_playlist(struct playlist *pl,
+ int direction)
+{
+ struct playlist_entry *entry = playlist_get_next(pl, direction);
+ if (!entry)
+ return NULL;
+
+ while (entry && entry->playlist_path && pl->current->playlist_path &&
+ strcmp(entry->playlist_path, pl->current->playlist_path) == 0)
+ entry = playlist_entry_get_rel(entry, direction);
+
+ if (direction < 0)
+ entry = playlist_get_first_in_same_playlist(entry,
+ pl->current->playlist_path);
+
+ return entry;
+}
+
+struct playlist_entry *playlist_get_first_in_same_playlist(
+ struct playlist_entry *entry, char *current_playlist_path)
+{
+ void *tmp = talloc_new(NULL);
+
+ if (!entry || !entry->playlist_path)
+ goto exit;
+
+ // Don't go to the beginning of the playlist when the current playlist-path
+ // starts with the previous playlist-path, e.g. with mpv --loop-playlist
+ // archive_dir/, which expands to archive_dir/{1..9}.zip, the current
+ // playlist path "archive_dir/1.zip" begins with the playlist-path
+ // "archive_dir/" of {2..9}.zip, so go to 9.zip instead of 2.zip. But
+ // playlist-prev-playlist from e.g. the directory "foobar" to the directory
+ // "foo" should still go to the first entry in "foo/", and this should all
+ // work whether mpv's arguments have trailing slashes or not, e.g. in the
+ // first example:
+ // mpv archive_dir results in the playlist-paths "archive_dir/1.zip" and
+ // "archive_dir"
+ // mpv archive_dir/ in "archive_dir/1.zip" and "archive_dir/"
+ // mpv archive_dir// in "archive_dir//1.zip" and "archive_dir//"
+ // Always adding a separator to entry->playlist_path to fix the foobar foo
+ // case would break the previous 2 cases instead. Stripping the separator
+ // from entry->playlist_path if present and appending it again makes this
+ // work in all cases.
+ char* playlist_path = talloc_strdup(tmp, entry->playlist_path);
+ mp_path_strip_trailing_separator(playlist_path);
+ if (bstr_startswith(bstr0(current_playlist_path),
+ bstr0(talloc_strdup_append(playlist_path, "/")))
+#if HAVE_DOS_PATHS
+ ||
+ bstr_startswith(bstr0(current_playlist_path),
+ bstr0(talloc_strdup_append(playlist_path, "\\")))
+#endif
+ )
+ goto exit;
+
+ struct playlist_entry *prev = playlist_entry_get_rel(entry, -1);
+
+ while (prev && prev->playlist_path &&
+ strcmp(prev->playlist_path, entry->playlist_path) == 0) {
+ entry = prev;
+ prev = playlist_entry_get_rel(entry, -1);
+ }
+
+exit:
+ talloc_free(tmp);
+ return entry;
+}
+
+void playlist_add_base_path(struct playlist *pl, bstr base_path)
+{
+ if (base_path.len == 0 || bstrcmp0(base_path, ".") == 0)
+ return;
+ for (int n = 0; n < pl->num_entries; n++) {
+ struct playlist_entry *e = pl->entries[n];
+ if (!mp_is_url(bstr0(e->filename))) {
+ char *new_file = mp_path_join_bstr(e, base_path, bstr0(e->filename));
+ talloc_free(e->filename);
+ e->filename = new_file;
+ }
+ }
+}
+
+void playlist_set_stream_flags(struct playlist *pl, int flags)
+{
+ for (int n = 0; n < pl->num_entries; n++)
+ pl->entries[n]->stream_flags = flags;
+}
+
+static int64_t playlist_transfer_entries_to(struct playlist *pl, int dst_index,
+ struct playlist *source_pl)
+{
+ assert(pl != source_pl);
+ struct playlist_entry *first = playlist_get_first(source_pl);
+
+ int count = source_pl->num_entries;
+ MP_TARRAY_INSERT_N_AT(pl, pl->entries, pl->num_entries, dst_index, count);
+
+ for (int n = 0; n < count; n++) {
+ struct playlist_entry *e = source_pl->entries[n];
+ e->pl = pl;
+ e->pl_index = dst_index + n;
+ e->id = ++pl->id_alloc;
+ pl->entries[e->pl_index] = e;
+ talloc_steal(pl, e);
+ }
+
+ playlist_update_indexes(pl, dst_index + count, -1);
+ source_pl->num_entries = 0;
+
+ return first ? first->id : 0;
+}
+
+// Move all entries from source_pl to pl, appending them after the current entry
+// of pl. source_pl will be empty, and all entries have changed ownership to pl.
+// Return the new ID of the first added entry within pl (0 if source_pl was
+// empty). The IDs of all added entries increase by 1 each entry (you can
+// predict the ID of the last entry).
+int64_t playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl)
+{
+
+ int add_at = pl->num_entries;
+ if (pl->current) {
+ add_at = pl->current->pl_index + 1;
+ if (pl->current_was_replaced)
+ add_at += 1;
+ }
+ assert(add_at >= 0);
+ assert(add_at <= pl->num_entries);
+
+ return playlist_transfer_entries_to(pl, add_at, source_pl);
+}
+
+int64_t playlist_append_entries(struct playlist *pl, struct playlist *source_pl)
+{
+ return playlist_transfer_entries_to(pl, pl->num_entries, source_pl);
+}
+
+// Return number of entries between list start and e.
+// Return -1 if e is not on the list, or if e is NULL.
+int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e)
+{
+ if (!e || e->pl != pl)
+ return -1;
+ return e->pl_index;
+}
+
+int playlist_entry_count(struct playlist *pl)
+{
+ return pl->num_entries;
+}
+
+// Return entry for which playlist_entry_to_index() would return index.
+// Return NULL if not found.
+struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index)
+{
+ return index >= 0 && index < pl->num_entries ? pl->entries[index] : NULL;
+}
+
+struct playlist *playlist_parse_file(const char *file, struct mp_cancel *cancel,
+ struct mpv_global *global)
+{
+ struct mp_log *log = mp_log_new(NULL, global->log, "!playlist_parser");
+ mp_verbose(log, "Parsing playlist file %s...\n", file);
+
+ struct demuxer_params p = {
+ .force_format = "playlist",
+ .stream_flags = STREAM_ORIGIN_DIRECT,
+ };
+ struct demuxer *d = demux_open_url(file, &p, cancel, global);
+ if (!d) {
+ talloc_free(log);
+ return NULL;
+ }
+
+ struct playlist *ret = NULL;
+ if (d && d->playlist) {
+ ret = talloc_zero(NULL, struct playlist);
+ playlist_transfer_entries(ret, d->playlist);
+ if (d->filetype && strcmp(d->filetype, "hls") == 0) {
+ mp_warn(log, "This might be a HLS stream. For correct operation, "
+ "pass it to the player\ndirectly. Don't use --playlist.\n");
+ }
+ }
+ demux_free(d);
+
+ if (ret) {
+ mp_verbose(log, "Playlist successfully parsed\n");
+ } else {
+ mp_err(log, "Error while parsing playlist\n");
+ }
+
+ if (ret && !ret->num_entries)
+ mp_warn(log, "Warning: empty playlist\n");
+
+ talloc_free(log);
+ return ret;
+}
diff --git a/common/playlist.h b/common/playlist.h
new file mode 100644
index 0000000..aecd539
--- /dev/null
+++ b/common/playlist.h
@@ -0,0 +1,121 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_PLAYLIST_H
+#define MPLAYER_PLAYLIST_H
+
+#include <stdbool.h>
+#include "misc/bstr.h"
+
+struct playlist_param {
+ bstr name, value;
+};
+
+struct playlist_entry {
+ // Invariant: (pl && pl->entries[pl_index] == this) || (!pl && pl_index < 0)
+ struct playlist *pl;
+ int pl_index;
+
+ uint64_t id;
+
+ char *filename;
+ char *playlist_path;
+
+ struct playlist_param *params;
+ int num_params;
+
+ char *title;
+
+ // Used for unshuffling: the pl_index before it was shuffled. -1 => unknown.
+ int original_index;
+
+ // Set to true if this playlist entry was selected while trying to go backwards
+ // in the playlist. If this is true and the playlist entry fails to play later,
+ // then mpv tries to go to the next previous entry. This flag is always cleared
+ // regardless if the attempt was successful or not.
+ bool playlist_prev_attempt : 1;
+
+ // Set to true if not at least 1 frame (audio or video) could be played.
+ bool init_failed : 1;
+ // Entry was removed with playlist_remove (etc.), but not deallocated.
+ bool removed : 1;
+ // Additional refcount. Normally (reserved==0), the entry is owned by the
+ // playlist, and this can be used to keep the entry alive.
+ int reserved;
+ // Any flags from STREAM_ORIGIN_FLAGS. 0 if unknown.
+ // Used to reject loading of unsafe entries from external playlists.
+ int stream_flags;
+};
+
+struct playlist {
+ struct playlist_entry **entries;
+ int num_entries;
+
+ // This provides some sort of stable iterator. If this entry is removed from
+ // the playlist, current is set to the next element (or NULL), and
+ // current_was_replaced is set to true.
+ struct playlist_entry *current;
+ bool current_was_replaced;
+
+ uint64_t id_alloc;
+};
+
+void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value);
+void playlist_entry_add_params(struct playlist_entry *e,
+ struct playlist_param *params,
+ int params_count);
+
+struct playlist_entry *playlist_entry_new(const char *filename);
+
+void playlist_add(struct playlist *pl, struct playlist_entry *add);
+void playlist_remove(struct playlist *pl, struct playlist_entry *entry);
+void playlist_clear(struct playlist *pl);
+void playlist_clear_except_current(struct playlist *pl);
+
+void playlist_move(struct playlist *pl, struct playlist_entry *entry,
+ struct playlist_entry *at);
+
+void playlist_add_file(struct playlist *pl, const char *filename);
+void playlist_populate_playlist_path(struct playlist *pl, const char *path);
+void playlist_shuffle(struct playlist *pl);
+void playlist_unshuffle(struct playlist *pl);
+struct playlist_entry *playlist_get_first(struct playlist *pl);
+struct playlist_entry *playlist_get_last(struct playlist *pl);
+struct playlist_entry *playlist_get_next(struct playlist *pl, int direction);
+struct playlist_entry *playlist_entry_get_rel(struct playlist_entry *e,
+ int direction);
+struct playlist_entry *playlist_get_first_in_next_playlist(struct playlist *pl,
+ int direction);
+struct playlist_entry *playlist_get_first_in_same_playlist(struct playlist_entry *entry,
+ char *current_playlist_path);
+void playlist_add_base_path(struct playlist *pl, bstr base_path);
+void playlist_set_stream_flags(struct playlist *pl, int flags);
+int64_t playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl);
+int64_t playlist_append_entries(struct playlist *pl, struct playlist *source_pl);
+
+int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e);
+int playlist_entry_count(struct playlist *pl);
+struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index);
+
+struct mp_cancel;
+struct mpv_global;
+struct playlist *playlist_parse_file(const char *file, struct mp_cancel *cancel,
+ struct mpv_global *global);
+
+void playlist_entry_unref(struct playlist_entry *e);
+
+#endif
diff --git a/common/recorder.c b/common/recorder.c
new file mode 100644
index 0000000..42ae7f8
--- /dev/null
+++ b/common/recorder.c
@@ -0,0 +1,422 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include <libavformat/avformat.h>
+
+#include "common/av_common.h"
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "demux/demux.h"
+#include "demux/packet.h"
+#include "demux/stheader.h"
+
+#include "recorder.h"
+
+// Maximum number of packets we buffer at most to attempt to resync streams.
+// Essentially, this should be higher than the highest supported keyframe
+// interval.
+#define QUEUE_MAX_PACKETS 256
+// Number of packets we should buffer at least to determine timestamps (due to
+// codec delay and frame reordering, and potentially lack of DTS).
+// Keyframe flags can trigger this earlier.
+#define QUEUE_MIN_PACKETS 16
+
+struct mp_recorder {
+ struct mpv_global *global;
+ struct mp_log *log;
+
+ struct mp_recorder_sink **streams;
+ int num_streams;
+
+ bool opened; // mux context is valid
+ bool muxing; // we're currently recording (instead of preparing)
+ bool muxing_from_start; // no discontinuity at start
+ bool dts_warning;
+
+ // The start timestamp of the currently recorded segment (the timestamp of
+ // the first packet of the incoming packet stream).
+ double base_ts;
+ // The output packet timestamp corresponding to base_ts. It's the timestamp
+ // of the first packet of the current segment written to the output.
+ double rebase_ts;
+
+ AVFormatContext *mux;
+};
+
+struct mp_recorder_sink {
+ struct mp_recorder *owner;
+ struct sh_stream *sh;
+ AVStream *av_stream;
+ AVPacket *avpkt;
+ double max_out_pts;
+ bool discont;
+ bool proper_eof;
+ struct demux_packet **packets;
+ int num_packets;
+};
+
+static int add_stream(struct mp_recorder *priv, struct sh_stream *sh)
+{
+ enum AVMediaType av_type = mp_to_av_stream_type(sh->type);
+ int ret = -1;
+ AVCodecParameters *avp = NULL;
+ if (av_type == AVMEDIA_TYPE_UNKNOWN)
+ goto done;
+
+ struct mp_recorder_sink *rst = talloc(priv, struct mp_recorder_sink);
+ *rst = (struct mp_recorder_sink) {
+ .owner = priv,
+ .sh = sh,
+ .av_stream = avformat_new_stream(priv->mux, NULL),
+ .avpkt = av_packet_alloc(),
+ .max_out_pts = MP_NOPTS_VALUE,
+ };
+
+ if (!rst->av_stream || !rst->avpkt)
+ goto done;
+
+ avp = mp_codec_params_to_av(sh->codec);
+ if (!avp)
+ goto done;
+
+ // Check if we get the same codec_id for the output format;
+ // otherwise clear it to have a chance at muxing
+ if (av_codec_get_id(priv->mux->oformat->codec_tag,
+ avp->codec_tag) != avp->codec_id)
+ avp->codec_tag = 0;
+
+ // We don't know the delay, so make something up. If the format requires
+ // DTS, the result will probably be broken. FFmpeg provides nothing better
+ // yet (unless you demux with libavformat, which contains tons of hacks
+ // that try to determine a PTS).
+ if (!sh->codec->lav_codecpar)
+ avp->video_delay = 16;
+
+ if (avp->codec_id == AV_CODEC_ID_NONE)
+ goto done;
+
+ if (avcodec_parameters_copy(rst->av_stream->codecpar, avp) < 0)
+ goto done;
+
+ ret = 0;
+ rst->av_stream->time_base = mp_get_codec_timebase(sh->codec);
+
+ MP_TARRAY_APPEND(priv, priv->streams, priv->num_streams, rst);
+
+done:
+ if (avp)
+ avcodec_parameters_free(&avp);
+ return ret;
+}
+
+struct mp_recorder *mp_recorder_create(struct mpv_global *global,
+ const char *target_file,
+ struct sh_stream **streams,
+ int num_streams,
+ struct demux_attachment **attachments,
+ int num_attachments)
+{
+ struct mp_recorder *priv = talloc_zero(NULL, struct mp_recorder);
+
+ priv->global = global;
+ priv->log = mp_log_new(priv, global->log, "recorder");
+
+ if (!num_streams) {
+ MP_ERR(priv, "No streams.\n");
+ goto error;
+ }
+
+ priv->mux = avformat_alloc_context();
+ if (!priv->mux)
+ goto error;
+
+ priv->mux->oformat = av_guess_format(NULL, target_file, NULL);
+ if (!priv->mux->oformat) {
+ MP_ERR(priv, "Output format not found.\n");
+ goto error;
+ }
+
+ if (avio_open2(&priv->mux->pb, target_file, AVIO_FLAG_WRITE, NULL, NULL) < 0) {
+ MP_ERR(priv, "Failed opening output file.\n");
+ goto error;
+ }
+
+ for (int n = 0; n < num_streams; n++) {
+ if (add_stream(priv, streams[n]) < 0) {
+ MP_ERR(priv, "Can't mux one of the input streams.\n");
+ goto error;
+ }
+ }
+
+ if (!strcmp(priv->mux->oformat->name, "matroska")) {
+ // Only attach attachments (fonts) to matroska - mp4, nut, mpegts don't
+ // like them, and we find that out too late in the muxing process.
+ AVStream *a_stream = NULL;
+ for (int i = 0; i < num_attachments; ++i) {
+ a_stream = avformat_new_stream(priv->mux, NULL);
+ if (!a_stream) {
+ MP_ERR(priv, "Can't mux one of the attachments.\n");
+ goto error;
+ }
+ struct demux_attachment *attachment = attachments[i];
+
+ a_stream->codecpar->codec_type = AVMEDIA_TYPE_ATTACHMENT;
+
+ a_stream->codecpar->extradata = av_mallocz(
+ attachment->data_size + AV_INPUT_BUFFER_PADDING_SIZE
+ );
+ if (!a_stream->codecpar->extradata) {
+ goto error;
+ }
+ memcpy(a_stream->codecpar->extradata,
+ attachment->data, attachment->data_size);
+ a_stream->codecpar->extradata_size = attachment->data_size;
+
+ av_dict_set(&a_stream->metadata, "filename", attachment->name, 0);
+ av_dict_set(&a_stream->metadata, "mimetype", attachment->type, 0);
+ }
+ }
+
+ // Not sure how to write this in a "standard" way. It appears only mkv
+ // and mp4 support this directly.
+ char version[200];
+ snprintf(version, sizeof(version), "%s experimental stream recording "
+ "feature (can generate broken files - please report bugs)",
+ mpv_version);
+ av_dict_set(&priv->mux->metadata, "encoding_tool", version, 0);
+
+ if (avformat_write_header(priv->mux, NULL) < 0) {
+ MP_ERR(priv, "Writing header failed.\n");
+ goto error;
+ }
+
+ priv->opened = true;
+ priv->muxing_from_start = true;
+
+ priv->base_ts = MP_NOPTS_VALUE;
+ priv->rebase_ts = 0;
+
+ MP_WARN(priv, "This is an experimental feature. Output files might be "
+ "broken or not play correctly with various players "
+ "(including mpv itself).\n");
+
+ return priv;
+
+error:
+ mp_recorder_destroy(priv);
+ return NULL;
+}
+
+static void flush_packets(struct mp_recorder *priv)
+{
+ for (int n = 0; n < priv->num_streams; n++) {
+ struct mp_recorder_sink *rst = priv->streams[n];
+ for (int i = 0; i < rst->num_packets; i++)
+ talloc_free(rst->packets[i]);
+ rst->num_packets = 0;
+ }
+}
+
+static void mux_packet(struct mp_recorder_sink *rst,
+ struct demux_packet *pkt)
+{
+ struct mp_recorder *priv = rst->owner;
+ struct demux_packet mpkt = *pkt;
+
+ double diff = priv->rebase_ts - priv->base_ts;
+ mpkt.pts = MP_ADD_PTS(mpkt.pts, diff);
+ mpkt.dts = MP_ADD_PTS(mpkt.dts, diff);
+
+ rst->max_out_pts = MP_PTS_MAX(rst->max_out_pts, pkt->pts);
+
+ mp_set_av_packet(rst->avpkt, &mpkt, &rst->av_stream->time_base);
+
+ rst->avpkt->stream_index = rst->av_stream->index;
+
+ if (rst->avpkt->duration < 0 && rst->sh->type != STREAM_SUB)
+ rst->avpkt->duration = 0;
+
+ AVPacket *new_packet = av_packet_clone(rst->avpkt);
+ if (!new_packet) {
+ MP_ERR(priv, "Failed to allocate packet.\n");
+ return;
+ }
+
+ if (av_interleaved_write_frame(priv->mux, new_packet) < 0)
+ MP_ERR(priv, "Failed writing packet.\n");
+
+ av_packet_free(&new_packet);
+}
+
+// Write all packets available in the stream queue
+static void mux_packets(struct mp_recorder_sink *rst)
+{
+ struct mp_recorder *priv = rst->owner;
+ if (!priv->muxing || !rst->num_packets)
+ return;
+
+ for (int n = 0; n < rst->num_packets; n++) {
+ mux_packet(rst, rst->packets[n]);
+ talloc_free(rst->packets[n]);
+ }
+
+ rst->num_packets = 0;
+}
+
+// If there was a discontinuity, check whether we can resume muxing (and from
+// where).
+static void check_restart(struct mp_recorder *priv)
+{
+ if (priv->muxing)
+ return;
+
+ double min_ts = MP_NOPTS_VALUE;
+ double rebase_ts = 0;
+ for (int n = 0; n < priv->num_streams; n++) {
+ struct mp_recorder_sink *rst = priv->streams[n];
+ int min_packets = rst->sh->type == STREAM_VIDEO ? QUEUE_MIN_PACKETS : 1;
+
+ rebase_ts = MP_PTS_MAX(rebase_ts, rst->max_out_pts);
+
+ if (rst->num_packets < min_packets) {
+ if (!rst->proper_eof && rst->sh->type != STREAM_SUB)
+ return;
+ continue;
+ }
+
+ for (int i = 0; i < min_packets; i++)
+ min_ts = MP_PTS_MIN(min_ts, rst->packets[i]->pts);
+ }
+
+ // Subtitle only stream (wait longer) or stream without any PTS (fuck it).
+ if (min_ts == MP_NOPTS_VALUE)
+ return;
+
+ priv->rebase_ts = rebase_ts;
+ priv->base_ts = min_ts;
+
+ for (int n = 0; n < priv->num_streams; n++) {
+ struct mp_recorder_sink *rst = priv->streams[n];
+ rst->max_out_pts = min_ts;
+ }
+
+ priv->muxing = true;
+
+ if (!priv->muxing_from_start)
+ MP_WARN(priv, "Discontinuity at timestamp %f.\n", priv->rebase_ts);
+}
+
+void mp_recorder_destroy(struct mp_recorder *priv)
+{
+ if (priv->opened) {
+ for (int n = 0; n < priv->num_streams; n++) {
+ struct mp_recorder_sink *rst = priv->streams[n];
+ mux_packets(rst);
+ mp_free_av_packet(&rst->avpkt);
+ }
+
+ if (av_write_trailer(priv->mux) < 0)
+ MP_ERR(priv, "Writing trailer failed.\n");
+ }
+
+ if (priv->mux) {
+ if (avio_closep(&priv->mux->pb) < 0)
+ MP_ERR(priv, "Closing file failed\n");
+
+ avformat_free_context(priv->mux);
+ }
+
+ flush_packets(priv);
+ talloc_free(priv);
+}
+
+// This is called on a seek, or when recording was started mid-stream.
+void mp_recorder_mark_discontinuity(struct mp_recorder *priv)
+{
+
+ for (int n = 0; n < priv->num_streams; n++) {
+ struct mp_recorder_sink *rst = priv->streams[n];
+ mux_packets(rst);
+ rst->discont = true;
+ rst->proper_eof = false;
+ }
+
+ flush_packets(priv);
+ priv->muxing = false;
+ priv->muxing_from_start = false;
+}
+
+// Get a stream for writing. The pointer is valid until mp_recorder is
+// destroyed. The stream ptr. is the same as one passed to
+// mp_recorder_create() (returns NULL if it wasn't).
+struct mp_recorder_sink *mp_recorder_get_sink(struct mp_recorder *r,
+ struct sh_stream *stream)
+{
+ for (int n = 0; n < r->num_streams; n++) {
+ struct mp_recorder_sink *rst = r->streams[n];
+ if (rst->sh == stream)
+ return rst;
+ }
+ return NULL;
+}
+
+// Pass a packet to the given stream. The function does not own the packet, but
+// can create a new reference to it if it needs to retain it. Can be NULL to
+// signal proper end of stream.
+void mp_recorder_feed_packet(struct mp_recorder_sink *rst,
+ struct demux_packet *pkt)
+{
+ struct mp_recorder *priv = rst->owner;
+
+ if (!pkt) {
+ rst->proper_eof = true;
+ check_restart(priv);
+ mux_packets(rst);
+ return;
+ }
+
+ if (pkt->dts == MP_NOPTS_VALUE && !priv->dts_warning) {
+ // No, FFmpeg has no actually usable helpers to generate correct DTS.
+ // No, FFmpeg doesn't tell us which formats need DTS at all.
+ // No, we can not shut up the FFmpeg warning, which will follow.
+ MP_WARN(priv, "Source stream misses DTS on at least some packets!\n"
+ "If the target file format requires DTS, the written "
+ "file will be invalid.\n");
+ priv->dts_warning = true;
+ }
+
+ if (rst->discont && !pkt->keyframe)
+ return;
+ rst->discont = false;
+
+ if (rst->num_packets >= QUEUE_MAX_PACKETS) {
+ MP_ERR(priv, "Stream %d has too many queued packets; dropping.\n",
+ rst->av_stream->index);
+ return;
+ }
+
+ pkt = demux_copy_packet(pkt);
+ if (!pkt)
+ return;
+ MP_TARRAY_APPEND(rst, rst->packets, rst->num_packets, pkt);
+
+ check_restart(priv);
+ mux_packets(rst);
+}
diff --git a/common/recorder.h b/common/recorder.h
new file mode 100644
index 0000000..e86d978
--- /dev/null
+++ b/common/recorder.h
@@ -0,0 +1,25 @@
+#ifndef MP_RECORDER_H_
+#define MP_RECORDER_H_
+
+struct mp_recorder;
+struct mpv_global;
+struct demux_packet;
+struct sh_stream;
+struct demux_attachment;
+struct mp_recorder_sink;
+
+struct mp_recorder *mp_recorder_create(struct mpv_global *global,
+ const char *target_file,
+ struct sh_stream **streams,
+ int num_streams,
+ struct demux_attachment **demux_attachments,
+ int num_attachments);
+void mp_recorder_destroy(struct mp_recorder *r);
+void mp_recorder_mark_discontinuity(struct mp_recorder *r);
+
+struct mp_recorder_sink *mp_recorder_get_sink(struct mp_recorder *r,
+ struct sh_stream *stream);
+void mp_recorder_feed_packet(struct mp_recorder_sink *s,
+ struct demux_packet *pkt);
+
+#endif
diff --git a/common/stats.c b/common/stats.c
new file mode 100644
index 0000000..c5f1e50
--- /dev/null
+++ b/common/stats.c
@@ -0,0 +1,325 @@
+#include <stdatomic.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "common.h"
+#include "global.h"
+#include "misc/linked_list.h"
+#include "misc/node.h"
+#include "msg.h"
+#include "options/m_option.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "stats.h"
+
+struct stats_base {
+ struct mpv_global *global;
+
+ atomic_bool active;
+
+ mp_mutex lock;
+
+ struct {
+ struct stats_ctx *head, *tail;
+ } list;
+
+ struct stat_entry **entries;
+ int num_entries;
+
+ int64_t last_time;
+};
+
+struct stats_ctx {
+ struct stats_base *base;
+ const char *prefix;
+
+ struct {
+ struct stats_ctx *prev, *next;
+ } list;
+
+ struct stat_entry **entries;
+ int num_entries;
+};
+
+enum val_type {
+ VAL_UNSET = 0,
+ VAL_STATIC,
+ VAL_STATIC_SIZE,
+ VAL_INC,
+ VAL_TIME,
+ VAL_THREAD_CPU_TIME,
+};
+
+struct stat_entry {
+ char name[32];
+ const char *full_name; // including stats_ctx.prefix
+
+ enum val_type type;
+ double val_d;
+ int64_t val_rt;
+ int64_t val_th;
+ int64_t time_start_ns;
+ int64_t cpu_start_ns;
+ mp_thread_id thread_id;
+};
+
+#define IS_ACTIVE(ctx) \
+ (atomic_load_explicit(&(ctx)->base->active, memory_order_relaxed))
+
+static void stats_destroy(void *p)
+{
+ struct stats_base *stats = p;
+
+ // All entries must have been destroyed before this.
+ assert(!stats->list.head);
+
+ mp_mutex_destroy(&stats->lock);
+}
+
+void stats_global_init(struct mpv_global *global)
+{
+ assert(!global->stats);
+ struct stats_base *stats = talloc_zero(global, struct stats_base);
+ ta_set_destructor(stats, stats_destroy);
+ mp_mutex_init(&stats->lock);
+
+ global->stats = stats;
+ stats->global = global;
+}
+
+static void add_stat(struct mpv_node *list, struct stat_entry *e,
+ const char *suffix, double num_val, char *text)
+{
+ struct mpv_node *ne = node_array_add(list, MPV_FORMAT_NODE_MAP);
+
+ node_map_add_string(ne, "name", suffix ?
+ mp_tprintf(80, "%s/%s", e->full_name, suffix) : e->full_name);
+ node_map_add_double(ne, "value", num_val);
+ if (text)
+ node_map_add_string(ne, "text", text);
+}
+
+static int cmp_entry(const void *p1, const void *p2)
+{
+ struct stat_entry **e1 = (void *)p1;
+ struct stat_entry **e2 = (void *)p2;
+ return strcmp((*e1)->full_name, (*e2)->full_name);
+}
+
+void stats_global_query(struct mpv_global *global, struct mpv_node *out)
+{
+ struct stats_base *stats = global->stats;
+ assert(stats);
+
+ mp_mutex_lock(&stats->lock);
+
+ atomic_store(&stats->active, true);
+
+ if (!stats->num_entries) {
+ for (struct stats_ctx *ctx = stats->list.head; ctx; ctx = ctx->list.next)
+ {
+ for (int n = 0; n < ctx->num_entries; n++) {
+ MP_TARRAY_APPEND(stats, stats->entries, stats->num_entries,
+ ctx->entries[n]);
+ }
+ }
+ if (stats->num_entries) {
+ qsort(stats->entries, stats->num_entries, sizeof(stats->entries[0]),
+ cmp_entry);
+ }
+ }
+
+ node_init(out, MPV_FORMAT_NODE_ARRAY, NULL);
+
+ int64_t now = mp_time_ns();
+ if (stats->last_time) {
+ double t_ms = MP_TIME_NS_TO_MS(now - stats->last_time);
+ struct mpv_node *ne = node_array_add(out, MPV_FORMAT_NODE_MAP);
+ node_map_add_string(ne, "name", "poll-time");
+ node_map_add_double(ne, "value", t_ms);
+ node_map_add_string(ne, "text", mp_tprintf(80, "%.2f ms", t_ms));
+
+ // Very dirty way to reset everything if the stats.lua page was probably
+ // closed. Not enough energy left for clean solution. Fuck it.
+ if (t_ms > 2000) {
+ for (int n = 0; n < stats->num_entries; n++) {
+ struct stat_entry *e = stats->entries[n];
+
+ e->cpu_start_ns = 0;
+ e->val_rt = e->val_th = 0;
+ if (e->type != VAL_THREAD_CPU_TIME)
+ e->type = 0;
+ }
+ }
+ }
+ stats->last_time = now;
+
+ for (int n = 0; n < stats->num_entries; n++) {
+ struct stat_entry *e = stats->entries[n];
+
+ switch (e->type) {
+ case VAL_STATIC:
+ add_stat(out, e, NULL, e->val_d, NULL);
+ break;
+ case VAL_STATIC_SIZE: {
+ char *s = format_file_size(e->val_d);
+ add_stat(out, e, NULL, e->val_d, s);
+ talloc_free(s);
+ break;
+ }
+ case VAL_INC:
+ add_stat(out, e, NULL, e->val_d, NULL);
+ e->val_d = 0;
+ break;
+ case VAL_TIME: {
+ double t_cpu = MP_TIME_NS_TO_MS(e->val_th);
+ add_stat(out, e, "cpu", t_cpu, mp_tprintf(80, "%.2f ms", t_cpu));
+ double t_rt = MP_TIME_NS_TO_MS(e->val_rt);
+ add_stat(out, e, "time", t_rt, mp_tprintf(80, "%.2f ms", t_rt));
+ e->val_rt = e->val_th = 0;
+ break;
+ }
+ case VAL_THREAD_CPU_TIME: {
+ int64_t t = mp_thread_cpu_time_ns(e->thread_id);
+ if (!e->cpu_start_ns)
+ e->cpu_start_ns = t;
+ double t_msec = MP_TIME_NS_TO_MS(t - e->cpu_start_ns);
+ add_stat(out, e, NULL, t_msec, mp_tprintf(80, "%.2f ms", t_msec));
+ e->cpu_start_ns = t;
+ break;
+ }
+ default: ;
+ }
+ }
+
+ mp_mutex_unlock(&stats->lock);
+}
+
+static void stats_ctx_destroy(void *p)
+{
+ struct stats_ctx *ctx = p;
+
+ mp_mutex_lock(&ctx->base->lock);
+ LL_REMOVE(list, &ctx->base->list, ctx);
+ ctx->base->num_entries = 0; // invalidate
+ mp_mutex_unlock(&ctx->base->lock);
+}
+
+struct stats_ctx *stats_ctx_create(void *ta_parent, struct mpv_global *global,
+ const char *prefix)
+{
+ struct stats_base *base = global->stats;
+ assert(base);
+
+ struct stats_ctx *ctx = talloc_zero(ta_parent, struct stats_ctx);
+ ctx->base = base;
+ ctx->prefix = talloc_strdup(ctx, prefix);
+ ta_set_destructor(ctx, stats_ctx_destroy);
+
+ mp_mutex_lock(&base->lock);
+ LL_APPEND(list, &base->list, ctx);
+ base->num_entries = 0; // invalidate
+ mp_mutex_unlock(&base->lock);
+
+ return ctx;
+}
+
+static struct stat_entry *find_entry(struct stats_ctx *ctx, const char *name)
+{
+ for (int n = 0; n < ctx->num_entries; n++) {
+ if (strcmp(ctx->entries[n]->name, name) == 0)
+ return ctx->entries[n];
+ }
+
+ struct stat_entry *e = talloc_zero(ctx, struct stat_entry);
+ snprintf(e->name, sizeof(e->name), "%s", name);
+ assert(strcmp(e->name, name) == 0); // make e->name larger and don't complain
+
+ e->full_name = talloc_asprintf(e, "%s/%s", ctx->prefix, e->name);
+
+ MP_TARRAY_APPEND(ctx, ctx->entries, ctx->num_entries, e);
+ ctx->base->num_entries = 0; // invalidate
+
+ return e;
+}
+
+static void static_value(struct stats_ctx *ctx, const char *name, double val,
+ enum val_type type)
+{
+ if (!IS_ACTIVE(ctx))
+ return;
+ mp_mutex_lock(&ctx->base->lock);
+ struct stat_entry *e = find_entry(ctx, name);
+ e->val_d = val;
+ e->type = type;
+ mp_mutex_unlock(&ctx->base->lock);
+}
+
+void stats_value(struct stats_ctx *ctx, const char *name, double val)
+{
+ static_value(ctx, name, val, VAL_STATIC);
+}
+
+void stats_size_value(struct stats_ctx *ctx, const char *name, double val)
+{
+ static_value(ctx, name, val, VAL_STATIC_SIZE);
+}
+
+void stats_time_start(struct stats_ctx *ctx, const char *name)
+{
+ MP_STATS(ctx->base->global, "start %s", name);
+ if (!IS_ACTIVE(ctx))
+ return;
+ mp_mutex_lock(&ctx->base->lock);
+ struct stat_entry *e = find_entry(ctx, name);
+ e->cpu_start_ns = mp_thread_cpu_time_ns(mp_thread_current_id());
+ e->time_start_ns = mp_time_ns();
+ mp_mutex_unlock(&ctx->base->lock);
+}
+
+void stats_time_end(struct stats_ctx *ctx, const char *name)
+{
+ MP_STATS(ctx->base->global, "end %s", name);
+ if (!IS_ACTIVE(ctx))
+ return;
+ mp_mutex_lock(&ctx->base->lock);
+ struct stat_entry *e = find_entry(ctx, name);
+ if (e->time_start_ns) {
+ e->type = VAL_TIME;
+ e->val_rt += mp_time_ns() - e->time_start_ns;
+ e->val_th += mp_thread_cpu_time_ns(mp_thread_current_id()) - e->cpu_start_ns;
+ e->time_start_ns = 0;
+ }
+ mp_mutex_unlock(&ctx->base->lock);
+}
+
+void stats_event(struct stats_ctx *ctx, const char *name)
+{
+ if (!IS_ACTIVE(ctx))
+ return;
+ mp_mutex_lock(&ctx->base->lock);
+ struct stat_entry *e = find_entry(ctx, name);
+ e->val_d += 1;
+ e->type = VAL_INC;
+ mp_mutex_unlock(&ctx->base->lock);
+}
+
+static void register_thread(struct stats_ctx *ctx, const char *name,
+ enum val_type type)
+{
+ mp_mutex_lock(&ctx->base->lock);
+ struct stat_entry *e = find_entry(ctx, name);
+ e->type = type;
+ e->thread_id = mp_thread_current_id();
+ mp_mutex_unlock(&ctx->base->lock);
+}
+
+void stats_register_thread_cputime(struct stats_ctx *ctx, const char *name)
+{
+ register_thread(ctx, name, VAL_THREAD_CPU_TIME);
+}
+
+void stats_unregister_thread(struct stats_ctx *ctx, const char *name)
+{
+ register_thread(ctx, name, 0);
+}
diff --git a/common/stats.h b/common/stats.h
new file mode 100644
index 0000000..c3e136e
--- /dev/null
+++ b/common/stats.h
@@ -0,0 +1,34 @@
+#pragma once
+
+struct mpv_global;
+struct mpv_node;
+struct stats_ctx;
+
+void stats_global_init(struct mpv_global *global);
+void stats_global_query(struct mpv_global *global, struct mpv_node *out);
+
+// stats_ctx can be free'd with ta_free(), or by using the ta_parent.
+struct stats_ctx *stats_ctx_create(void *ta_parent, struct mpv_global *global,
+ const char *prefix);
+
+// A static numeric value.
+void stats_value(struct stats_ctx *ctx, const char *name, double val);
+
+// Like stats_value(), but render as size in bytes.
+void stats_size_value(struct stats_ctx *ctx, const char *name, double val);
+
+// Report the real time and CPU time in seconds between _start and _end calls
+// as value, and report the average and number of all times.
+void stats_time_start(struct stats_ctx *ctx, const char *name);
+void stats_time_end(struct stats_ctx *ctx, const char *name);
+
+// Display number of events per poll period.
+void stats_event(struct stats_ctx *ctx, const char *name);
+
+// Report the thread's CPU time. This needs to be called only once per thread.
+// The current thread is assumed to stay valid until the stats_ctx is destroyed
+// or stats_unregister_thread() is called, otherwise UB will occur.
+void stats_register_thread_cputime(struct stats_ctx *ctx, const char *name);
+
+// Remove reference to the current thread.
+void stats_unregister_thread(struct stats_ctx *ctx, const char *name);
diff --git a/common/tags.c b/common/tags.c
new file mode 100644
index 0000000..43f557d
--- /dev/null
+++ b/common/tags.c
@@ -0,0 +1,151 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <limits.h>
+#include <strings.h>
+#include <assert.h>
+#include <libavutil/dict.h>
+#include "tags.h"
+#include "misc/bstr.h"
+
+void mp_tags_set_str(struct mp_tags *tags, const char *key, const char *value)
+{
+ mp_tags_set_bstr(tags, bstr0(key), bstr0(value));
+}
+
+void mp_tags_set_bstr(struct mp_tags *tags, bstr key, bstr value)
+{
+ for (int n = 0; n < tags->num_keys; n++) {
+ if (bstrcasecmp0(key, tags->keys[n]) == 0) {
+ talloc_free(tags->values[n]);
+ tags->values[n] = bstrto0(tags, value);
+ return;
+ }
+ }
+
+ MP_RESIZE_ARRAY(tags, tags->keys, tags->num_keys + 1);
+ MP_RESIZE_ARRAY(tags, tags->values, tags->num_keys + 1);
+ tags->keys[tags->num_keys] = bstrto0(tags, key);
+ tags->values[tags->num_keys] = bstrto0(tags, value);
+ tags->num_keys++;
+}
+
+void mp_tags_remove_str(struct mp_tags *tags, const char *key)
+{
+ mp_tags_remove_bstr(tags, bstr0(key));
+}
+
+void mp_tags_remove_bstr(struct mp_tags *tags, bstr key)
+{
+ for (int n = 0; n < tags->num_keys; n++) {
+ if (bstrcasecmp0(key, tags->keys[n]) == 0) {
+ talloc_free(tags->keys[n]);
+ talloc_free(tags->values[n]);
+ int num_keys = tags->num_keys; // copy so it's only decremented once
+ MP_TARRAY_REMOVE_AT(tags->keys, num_keys, n);
+ MP_TARRAY_REMOVE_AT(tags->values, tags->num_keys, n);
+ }
+ }
+}
+
+char *mp_tags_get_str(struct mp_tags *tags, const char *key)
+{
+ return mp_tags_get_bstr(tags, bstr0(key));
+}
+
+char *mp_tags_get_bstr(struct mp_tags *tags, bstr key)
+{
+ for (int n = 0; n < tags->num_keys; n++) {
+ if (bstrcasecmp0(key, tags->keys[n]) == 0)
+ return tags->values[n];
+ }
+ return NULL;
+}
+
+void mp_tags_clear(struct mp_tags *tags)
+{
+ *tags = (struct mp_tags){0};
+ talloc_free_children(tags);
+}
+
+
+
+struct mp_tags *mp_tags_dup(void *tparent, struct mp_tags *tags)
+{
+ struct mp_tags *new = talloc_zero(tparent, struct mp_tags);
+ mp_tags_replace(new, tags);
+ return new;
+}
+
+void mp_tags_replace(struct mp_tags *dst, struct mp_tags *src)
+{
+ mp_tags_clear(dst);
+ MP_RESIZE_ARRAY(dst, dst->keys, src->num_keys);
+ MP_RESIZE_ARRAY(dst, dst->values, src->num_keys);
+ dst->num_keys = src->num_keys;
+ for (int n = 0; n < src->num_keys; n++) {
+ dst->keys[n] = talloc_strdup(dst, src->keys[n]);
+ dst->values[n] = talloc_strdup(dst, src->values[n]);
+ }
+}
+
+// Return a copy of the tags, but containing only keys in list. Also forces
+// the order and casing of the keys (for cosmetic reasons).
+// A trailing '*' matches the rest.
+struct mp_tags *mp_tags_filtered(void *tparent, struct mp_tags *tags, char **list)
+{
+ struct mp_tags *new = talloc_zero(tparent, struct mp_tags);
+ for (int n = 0; list && list[n]; n++) {
+ char *key = list[n];
+ size_t keylen = strlen(key);
+ if (keylen >= INT_MAX)
+ continue;
+ bool prefix = keylen && key[keylen - 1] == '*';
+ int matchlen = prefix ? keylen - 1 : keylen + 1;
+ for (int x = 0; x < tags->num_keys; x++) {
+ if (strncasecmp(tags->keys[x], key, matchlen) == 0) {
+ char skey[320];
+ snprintf(skey, sizeof(skey), "%.*s%s", matchlen, key,
+ prefix ? tags->keys[x] + keylen - 1 : "");
+ mp_tags_set_str(new, skey, tags->values[x]);
+ }
+ }
+ }
+ return new;
+}
+
+void mp_tags_merge(struct mp_tags *tags, struct mp_tags *src)
+{
+ for (int n = 0; n < src->num_keys; n++)
+ mp_tags_set_str(tags, src->keys[n], src->values[n]);
+}
+
+void mp_tags_copy_from_av_dictionary(struct mp_tags *tags,
+ struct AVDictionary *av_dict)
+{
+ AVDictionaryEntry *entry = NULL;
+ while ((entry = av_dict_get(av_dict, "", entry, AV_DICT_IGNORE_SUFFIX)))
+ mp_tags_set_str(tags, entry->key, entry->value);
+}
+
+void mp_tags_move_from_av_dictionary(struct mp_tags *tags,
+ struct AVDictionary **av_dict_ptr)
+{
+ mp_tags_copy_from_av_dictionary(tags, *av_dict_ptr);
+ av_dict_free(av_dict_ptr);
+}
diff --git a/common/tags.h b/common/tags.h
new file mode 100644
index 0000000..e7eb4b3
--- /dev/null
+++ b/common/tags.h
@@ -0,0 +1,31 @@
+#ifndef MP_TAGS_H
+#define MP_TAGS_H
+
+#include <stdint.h>
+
+#include "misc/bstr.h"
+
+struct mp_tags {
+ char **keys;
+ char **values;
+ int num_keys;
+};
+
+void mp_tags_set_str(struct mp_tags *tags, const char *key, const char *value);
+void mp_tags_set_bstr(struct mp_tags *tags, bstr key, bstr value);
+void mp_tags_remove_str(struct mp_tags *tags, const char *key);
+void mp_tags_remove_bstr(struct mp_tags *tags, bstr key);
+char *mp_tags_get_str(struct mp_tags *tags, const char *key);
+char *mp_tags_get_bstr(struct mp_tags *tags, bstr key);
+void mp_tags_clear(struct mp_tags *tags);
+struct mp_tags *mp_tags_dup(void *tparent, struct mp_tags *tags);
+void mp_tags_replace(struct mp_tags *dst, struct mp_tags *src);
+struct mp_tags *mp_tags_filtered(void *tparent, struct mp_tags *tags, char **list);
+void mp_tags_merge(struct mp_tags *tags, struct mp_tags *src);
+struct AVDictionary;
+void mp_tags_copy_from_av_dictionary(struct mp_tags *tags,
+ struct AVDictionary *av_dict);
+void mp_tags_move_from_av_dictionary(struct mp_tags *tags,
+ struct AVDictionary **av_dict_ptr);
+
+#endif
diff --git a/common/version.c b/common/version.c
new file mode 100644
index 0000000..228e3ee
--- /dev/null
+++ b/common/version.c
@@ -0,0 +1,23 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common.h"
+#include "version.h"
+
+const char mpv_version[] = "mpv " VERSION;
+const char mpv_builddate[] = BUILDDATE;
+const char mpv_copyright[] = MPVCOPYRIGHT;
diff --git a/common/version.h.in b/common/version.h.in
new file mode 100644
index 0000000..b09718f
--- /dev/null
+++ b/common/version.h.in
@@ -0,0 +1,7 @@
+#define VERSION "@VERSION@"
+#define MPVCOPYRIGHT "Copyright © 2000-2023 mpv/MPlayer/mplayer2 projects"
+#ifndef NO_BUILD_TIMESTAMPS
+#define BUILDDATE __DATE__ " " __TIME__
+#else
+#define BUILDDATE "UNKNOWN"
+#endif
diff --git a/demux/cache.c b/demux/cache.c
new file mode 100644
index 0000000..562eab0
--- /dev/null
+++ b/demux/cache.c
@@ -0,0 +1,331 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "cache.h"
+#include "common/msg.h"
+#include "common/av_common.h"
+#include "demux.h"
+#include "options/path.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "osdep/io.h"
+
+struct demux_cache_opts {
+ char *cache_dir;
+ int unlink_files;
+};
+
+#define OPT_BASE_STRUCT struct demux_cache_opts
+
+const struct m_sub_options demux_cache_conf = {
+ .opts = (const struct m_option[]){
+ {"demuxer-cache-dir", OPT_STRING(cache_dir), .flags = M_OPT_FILE},
+ {"demuxer-cache-unlink-files", OPT_CHOICE(unlink_files,
+ {"immediate", 2}, {"whendone", 1}, {"no", 0}),
+ },
+ {"cache-dir", OPT_REPLACED("demuxer-cache-dir")},
+ {"cache-unlink-files", OPT_REPLACED("demuxer-cache-unlink-files")},
+ {0}
+ },
+ .size = sizeof(struct demux_cache_opts),
+ .defaults = &(const struct demux_cache_opts){
+ .unlink_files = 2,
+ },
+};
+
+struct demux_cache {
+ struct mp_log *log;
+ struct demux_cache_opts *opts;
+
+ char *filename;
+ bool need_unlink;
+ int fd;
+ int64_t file_pos;
+ uint64_t file_size;
+};
+
+struct pkt_header {
+ uint32_t data_len;
+ uint32_t av_flags;
+ uint32_t num_sd;
+};
+
+struct sd_header {
+ uint32_t av_type;
+ uint32_t len;
+};
+
+static void cache_destroy(void *p)
+{
+ struct demux_cache *cache = p;
+
+ if (cache->fd >= 0)
+ close(cache->fd);
+
+ if (cache->need_unlink && cache->opts->unlink_files >= 1) {
+ if (unlink(cache->filename))
+ MP_ERR(cache, "Failed to delete cache temporary file.\n");
+ }
+}
+
+// Create a cache. This also initializes the cache file from the options. The
+// log parameter must stay valid until demux_cache is destroyed.
+// Free with talloc_free().
+struct demux_cache *demux_cache_create(struct mpv_global *global,
+ struct mp_log *log)
+{
+ struct demux_cache *cache = talloc_zero(NULL, struct demux_cache);
+ talloc_set_destructor(cache, cache_destroy);
+ cache->opts = mp_get_config_group(cache, global, &demux_cache_conf);
+ cache->log = log;
+ cache->fd = -1;
+
+ char *cache_dir = cache->opts->cache_dir;
+ if (cache_dir && cache_dir[0]) {
+ cache_dir = mp_get_user_path(NULL, global, cache_dir);
+ } else {
+ cache_dir = mp_find_user_file(NULL, global, "cache", "");
+ }
+
+ if (!cache_dir || !cache_dir[0])
+ goto fail;
+
+ mp_mkdirp(cache_dir);
+ cache->filename = mp_path_join(cache, cache_dir, "mpv-cache-XXXXXX.dat");
+ cache->fd = mp_mkostemps(cache->filename, 4, O_CLOEXEC);
+ if (cache->fd < 0) {
+ MP_ERR(cache, "Failed to create cache temporary file.\n");
+ goto fail;
+ }
+ cache->need_unlink = true;
+ if (cache->opts->unlink_files >= 2) {
+ if (unlink(cache->filename)) {
+ MP_ERR(cache, "Failed to unlink cache temporary file after creation.\n");
+ } else {
+ cache->need_unlink = false;
+ }
+ }
+
+ return cache;
+fail:
+ talloc_free(cache);
+ return NULL;
+}
+
+uint64_t demux_cache_get_size(struct demux_cache *cache)
+{
+ return cache->file_size;
+}
+
+static bool do_seek(struct demux_cache *cache, uint64_t pos)
+{
+ if (cache->file_pos == pos)
+ return true;
+
+ off_t res = lseek(cache->fd, pos, SEEK_SET);
+
+ if (res == (off_t)-1) {
+ MP_ERR(cache, "Failed to seek in cache file.\n");
+ cache->file_pos = -1;
+ } else {
+ cache->file_pos = res;
+ }
+
+ return cache->file_pos >= 0;
+}
+
+static bool write_raw(struct demux_cache *cache, void *ptr, size_t len)
+{
+ ssize_t res = write(cache->fd, ptr, len);
+
+ if (res < 0) {
+ MP_ERR(cache, "Failed to write to cache file: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ cache->file_pos += res;
+ cache->file_size = MPMAX(cache->file_size, cache->file_pos);
+
+ // Should never happen, unless the disk is full, or someone succeeded to
+ // trick us to write into a pipe or a socket.
+ if (res != len) {
+ MP_ERR(cache, "Could not write all data.\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool read_raw(struct demux_cache *cache, void *ptr, size_t len)
+{
+ ssize_t res = read(cache->fd, ptr, len);
+
+ if (res < 0) {
+ MP_ERR(cache, "Failed to read cache file: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ cache->file_pos += res;
+
+ // Should never happen, unless the file was cut short, or someone succeeded
+ // to rick us to write into a pipe or a socket.
+ if (res != len) {
+ MP_ERR(cache, "Could not read all data.\n");
+ return false;
+ }
+
+ return true;
+}
+
+// Serialize a packet to the cache file. Returns the packet position, which can
+// be passed to demux_cache_read() to read the packet again.
+// Returns a negative value on errors, i.e. writing the file failed.
+int64_t demux_cache_write(struct demux_cache *cache, struct demux_packet *dp)
+{
+ assert(dp->avpacket);
+
+ // AV_PKT_FLAG_TRUSTED usually means there are embedded pointers and such
+ // in the packet data. The pointer will become invalid if the packet is
+ // unreferenced.
+ if (dp->avpacket->flags & AV_PKT_FLAG_TRUSTED) {
+ MP_ERR(cache, "Cannot serialize this packet to cache file.\n");
+ return -1;
+ }
+
+ assert(!dp->is_cached);
+ assert(dp->len >= 0 && dp->len <= INT32_MAX);
+ assert(dp->avpacket->flags >= 0 && dp->avpacket->flags <= INT32_MAX);
+ assert(dp->avpacket->side_data_elems >= 0 &&
+ dp->avpacket->side_data_elems <= INT32_MAX);
+
+ if (!do_seek(cache, cache->file_size))
+ return -1;
+
+ uint64_t pos = cache->file_pos;
+
+ struct pkt_header hd = {
+ .data_len = dp->len,
+ .av_flags = dp->avpacket->flags,
+ .num_sd = dp->avpacket->side_data_elems,
+ };
+
+ if (!write_raw(cache, &hd, sizeof(hd)))
+ goto fail;
+
+ if (!write_raw(cache, dp->buffer, dp->len))
+ goto fail;
+
+ // The handling of FFmpeg side data requires an extra long comment to
+ // explain why this code is fragile and insane.
+ // FFmpeg packet side data is per-packet out of band data, that contains
+ // further information for the decoder (extra metadata and such), which is
+ // not part of the codec itself and thus isn't contained in the packet
+ // payload. All types use a flat byte array. The format of this byte array
+ // is non-standard and FFmpeg-specific, and depends on the side data type
+ // field. The side data type is of course a FFmpeg ABI artifact.
+ // In some cases, the format is described as fixed byte layout. In others,
+ // it contains a struct, i.e. is bound to FFmpeg ABI. Some newer types make
+ // the format explicitly internal (and _not_ part of the ABI), and you need
+ // to use separate accessors to turn it into complex data structures.
+ // As of now, FFmpeg fortunately adheres to the idea that side data can not
+ // contain embedded pointers (due to API rules, but also because they forgot
+ // adding a refcount field, and can't change this until they break ABI).
+ // We rely on this. We hope that FFmpeg won't silently change their
+ // semantics, and add refcounting and embedded pointers. This way we can
+ // for example dump the data in a disk cache, even though we can't use the
+ // data from another process or if this process is restarted (unless we're
+ // absolutely sure the FFmpeg internals didn't change). The data has to be
+ // treated as a memory dump.
+ for (int n = 0; n < dp->avpacket->side_data_elems; n++) {
+ AVPacketSideData *sd = &dp->avpacket->side_data[n];
+
+ assert(sd->size >= 0 && sd->size <= INT32_MAX);
+ assert(sd->type >= 0 && sd->type <= INT32_MAX);
+
+ struct sd_header sd_hd = {
+ .av_type = sd->type,
+ .len = sd->size,
+ };
+
+ if (!write_raw(cache, &sd_hd, sizeof(sd_hd)))
+ goto fail;
+ if (!write_raw(cache, sd->data, sd->size))
+ goto fail;
+ }
+
+ return pos;
+
+fail:
+ // Reset file_size (try not to append crap forever).
+ do_seek(cache, pos);
+ cache->file_size = cache->file_pos;
+ return -1;
+}
+
+struct demux_packet *demux_cache_read(struct demux_cache *cache, uint64_t pos)
+{
+ if (!do_seek(cache, pos))
+ return NULL;
+
+ struct pkt_header hd;
+
+ if (!read_raw(cache, &hd, sizeof(hd)))
+ return NULL;
+
+ if (hd.data_len >= (size_t)-1)
+ return NULL;
+
+ struct demux_packet *dp = new_demux_packet(hd.data_len);
+ if (!dp)
+ goto fail;
+
+ if (!read_raw(cache, dp->buffer, dp->len))
+ goto fail;
+
+ dp->avpacket->flags = hd.av_flags;
+
+ for (uint32_t n = 0; n < hd.num_sd; n++) {
+ struct sd_header sd_hd;
+
+ if (!read_raw(cache, &sd_hd, sizeof(sd_hd)))
+ goto fail;
+
+ if (sd_hd.len > INT_MAX)
+ goto fail;
+
+ uint8_t *sd = av_packet_new_side_data(dp->avpacket, sd_hd.av_type,
+ sd_hd.len);
+ if (!sd)
+ goto fail;
+
+ if (!read_raw(cache, sd, sd_hd.len))
+ goto fail;
+ }
+
+ return dp;
+
+fail:
+ talloc_free(dp);
+ return NULL;
+}
diff --git a/demux/cache.h b/demux/cache.h
new file mode 100644
index 0000000..95ea964
--- /dev/null
+++ b/demux/cache.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <stdint.h>
+
+struct demux_packet;
+struct mp_log;
+struct mpv_global;
+
+struct demux_cache;
+
+struct demux_cache *demux_cache_create(struct mpv_global *global,
+ struct mp_log *log);
+
+int64_t demux_cache_write(struct demux_cache *cache, struct demux_packet *pkt);
+struct demux_packet *demux_cache_read(struct demux_cache *cache, uint64_t pos);
+uint64_t demux_cache_get_size(struct demux_cache *cache);
diff --git a/demux/codec_tags.c b/demux/codec_tags.c
new file mode 100644
index 0000000..55700d9
--- /dev/null
+++ b/demux/codec_tags.c
@@ -0,0 +1,280 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/common.h>
+#include <libavutil/intreadwrite.h>
+
+#include "codec_tags.h"
+#include "stheader.h"
+#include "common/av_common.h"
+
+static const char *lookup_tag(int type, uint32_t tag)
+{
+ const struct AVCodecTag *av_tags[3] = {0};
+ switch (type) {
+ case STREAM_VIDEO: {
+ av_tags[0] = avformat_get_riff_video_tags();
+ av_tags[1] = avformat_get_mov_video_tags();
+ break;
+ }
+ case STREAM_AUDIO: {
+ av_tags[0] = avformat_get_riff_audio_tags();
+ av_tags[1] = avformat_get_mov_audio_tags();
+ break;
+ }
+ }
+
+ int id = av_codec_get_id(av_tags, tag);
+ return id == AV_CODEC_ID_NONE ? NULL : mp_codec_from_av_codec_id(id);
+}
+
+
+/*
+ * As seen in the following page:
+ *
+ * https://web.archive.org/web/20220406060153/
+ * http://dream.cs.bath.ac.uk/researchdev/wave-ex/bformat.html
+ *
+ * Note that the GUID struct in the above citation has its
+ * integers encoded in little-endian format, which means that
+ * the unsigned short and unsigned long entries need to be
+ * byte-flipped for this encoding.
+ *
+ * In theory only the first element of this array should be used,
+ * however some encoders incorrectly encoded the GUID byte-for-byte
+ * and thus the second one exists as a fallback.
+ */
+static const unsigned char guid_ext_base[][16] = {
+ // MEDIASUBTYPE_BASE_GUID
+ {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
+ 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71},
+ // SUBTYPE_AMBISONIC_B_FORMAT_PCM
+ {0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xD3, 0x11,
+ 0x86, 0x44, 0xC8, 0xC1, 0xCA, 0x00, 0x00, 0x00}
+};
+
+struct mp_waveformatex_guid {
+ const char *codec;
+ const unsigned char guid[16];
+};
+
+static const struct mp_waveformatex_guid guid_ext_other[] = {
+ {"ac3",
+ {0x2C, 0x80, 0x6D, 0xE0, 0x46, 0xDB, 0xCF, 0x11,
+ 0xB4, 0xD1, 0x00, 0x80, 0x5F, 0x6C, 0xBB, 0xEA}},
+ {"adpcm_agm",
+ {0x82, 0xEC, 0x1F, 0x6A, 0xCA, 0xDB, 0x19, 0x45,
+ 0xBD, 0xE7, 0x56, 0xD3, 0xB3, 0xEF, 0x98, 0x1D}},
+ {"atrac3p",
+ {0xBF, 0xAA, 0x23, 0xE9, 0x58, 0xCB, 0x71, 0x44,
+ 0xA1, 0x19, 0xFF, 0xFA, 0x01, 0xE4, 0xCE, 0x62}},
+ {"atrac9",
+ {0xD2, 0x42, 0xE1, 0x47, 0xBA, 0x36, 0x8D, 0x4D,
+ 0x88, 0xFC, 0x61, 0x65, 0x4F, 0x8C, 0x83, 0x6C}},
+ {"dfpwm",
+ {0x3A, 0xC1, 0xFA, 0x38, 0x81, 0x1D, 0x43, 0x61,
+ 0xA4, 0x0D, 0xCE, 0x53, 0xCA, 0x60, 0x7C, 0xD1}},
+ {"eac3",
+ {0xAF, 0x87, 0xFB, 0xA7, 0x02, 0x2D, 0xFB, 0x42,
+ 0xA4, 0xD4, 0x05, 0xCD, 0x93, 0x84, 0x3B, 0xDD}},
+ {"mp2",
+ {0x2B, 0x80, 0x6D, 0xE0, 0x46, 0xDB, 0xCF, 0x11,
+ 0xB4, 0xD1, 0x00, 0x80, 0x5F, 0x6C, 0xBB, 0xEA}}
+};
+
+static void map_audio_pcm_tag(struct mp_codec_params *c)
+{
+ // MS PCM, Extended
+ if (c->codec_tag == 0xfffe && c->extradata_size >= 22) {
+ // WAVEFORMATEXTENSIBLE.wBitsPerSample
+ int bits_per_sample = AV_RL16(c->extradata);
+ if (bits_per_sample)
+ c->bits_per_coded_sample = bits_per_sample;
+
+ // WAVEFORMATEXTENSIBLE.dwChannelMask
+ uint64_t chmask = AV_RL32(c->extradata + 2);
+ struct mp_chmap chmap;
+ mp_chmap_from_waveext(&chmap, chmask);
+ if (c->channels.num == chmap.num)
+ c->channels = chmap;
+
+ // WAVEFORMATEXTENSIBLE.SubFormat
+ unsigned char *subformat = c->extradata + 6;
+ for (int i = 0; i < MP_ARRAY_SIZE(guid_ext_base); i++) {
+ if (memcmp(subformat + 4, guid_ext_base[i] + 4, 12) == 0) {
+ c->codec_tag = AV_RL32(subformat);
+ c->codec = lookup_tag(c->type, c->codec_tag);
+ break;
+ }
+ }
+
+ // extra subformat, not a base one
+ if (c->codec_tag == 0xfffe) {
+ for (int i = 0; i < MP_ARRAY_SIZE(guid_ext_other); i++) {
+ if (memcmp(subformat, &guid_ext_other[i].guid, 16) == 0) {
+ c->codec = guid_ext_other[i].codec;
+ c->codec_tag = mp_codec_to_av_codec_id(c->codec);
+ break;
+ }
+ }
+ }
+
+ // Compressed formats might use this.
+ c->extradata += 22;
+ c->extradata_size -= 22;
+ }
+
+ int bits = c->bits_per_coded_sample;
+ if (!bits)
+ return;
+
+ int bytes = (bits + 7) / 8;
+ switch (c->codec_tag) {
+ case 0x0: // Microsoft PCM
+ case 0x1:
+ if (bytes >= 1 && bytes <= 4)
+ mp_set_pcm_codec(c, bytes > 1, false, bytes * 8, false);
+ break;
+ case 0x3: // IEEE float
+ mp_set_pcm_codec(c, true, true, bits == 64 ? 64 : 32, false);
+ break;
+ }
+}
+
+void mp_set_codec_from_tag(struct mp_codec_params *c)
+{
+ c->codec = lookup_tag(c->type, c->codec_tag);
+ if (c->type == STREAM_AUDIO)
+ map_audio_pcm_tag(c);
+}
+
+void mp_set_pcm_codec(struct mp_codec_params *c, bool sign, bool is_float,
+ int bits, bool is_be)
+{
+ // This uses libavcodec pcm codec names, e.g. "pcm_u16le".
+ char codec[64] = "pcm_";
+ if (is_float) {
+ mp_snprintf_cat(codec, sizeof(codec), "f");
+ } else {
+ mp_snprintf_cat(codec, sizeof(codec), sign ? "s" : "u");
+ }
+ mp_snprintf_cat(codec, sizeof(codec), "%d", bits);
+ if (bits != 8)
+ mp_snprintf_cat(codec, sizeof(codec), is_be ? "be" : "le");
+ c->codec = talloc_strdup(c, codec);
+}
+
+// map file extension/type to an image codec name
+static const char *const type_to_codec[][2] = {
+ { "bmp", "bmp" },
+ { "dpx", "dpx" },
+ { "j2c", "jpeg2000" },
+ { "j2k", "jpeg2000" },
+ { "jp2", "jpeg2000" },
+ { "jpc", "jpeg2000" },
+ { "jpeg", "mjpeg" },
+ { "jpg", "mjpeg" },
+ { "jps", "mjpeg" },
+ { "jls", "ljpeg" },
+ { "thm", "mjpeg" },
+ { "db", "mjpeg" },
+ { "pcd", "photocd" },
+ { "pfm", "pfm" },
+ { "phm", "phm" },
+ { "hdr", "hdr" },
+ { "pcx", "pcx" },
+ { "png", "png" },
+ { "pns", "png" },
+ { "ptx", "ptx" },
+ { "tga", "targa" },
+ { "tif", "tiff" },
+ { "tiff", "tiff" },
+ { "sgi", "sgi" },
+ { "sun", "sunrast" },
+ { "ras", "sunrast" },
+ { "rs", "sunrast" },
+ { "ra", "sunrast" },
+ { "im1", "sunrast" },
+ { "im8", "sunrast" },
+ { "im24", "sunrast" },
+ { "im32", "sunrast" },
+ { "sunras", "sunrast" },
+ { "xbm", "xbm" },
+ { "pam", "pam" },
+ { "pbm", "pbm" },
+ { "pgm", "pgm" },
+ { "pgmyuv", "pgmyuv" },
+ { "ppm", "ppm" },
+ { "pnm", "ppm" },
+ { "gif", "gif" },
+ { "pix", "brender_pix" },
+ { "exr", "exr" },
+ { "pic", "pictor" },
+ { "qoi", "qoi" },
+ { "xface", "xface" },
+ { "xwd", "xwd" },
+ { "svg", "svg" },
+ {0}
+};
+
+bool mp_codec_is_image(const char *codec)
+{
+ if (codec) {
+ for (int n = 0; type_to_codec[n][0]; n++) {
+ if (strcasecmp(type_to_codec[n][1], codec) == 0)
+ return true;
+ }
+ }
+ return false;
+}
+
+const char *mp_map_type_to_image_codec(const char *type)
+{
+ if (type) {
+ for (int n = 0; type_to_codec[n][0]; n++) {
+ if (strcasecmp(type_to_codec[n][0], type) == 0)
+ return type_to_codec[n][1];
+ }
+ }
+ return NULL;
+};
+
+static const char *const mimetype_to_codec[][2] = {
+ {"image/apng", "apng"},
+ {"image/avif", "av1"},
+ {"image/bmp", "bmp"},
+ {"image/gif", "gif"},
+ {"image/jpeg", "mjpeg"},
+ {"image/jxl", "jpegxl"},
+ {"image/png", "png"},
+ {"image/tiff", "tiff"},
+ {"image/webp", "webp"},
+ {0}
+};
+
+const char *mp_map_mimetype_to_video_codec(const char *mimetype)
+{
+ if (mimetype) {
+ for (int n = 0; mimetype_to_codec[n][0]; n++) {
+ if (strcasecmp(mimetype_to_codec[n][0], mimetype) == 0)
+ return mimetype_to_codec[n][1];
+ }
+ }
+ return NULL;
+}
diff --git a/demux/codec_tags.h b/demux/codec_tags.h
new file mode 100644
index 0000000..8fc49df
--- /dev/null
+++ b/demux/codec_tags.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_CODEC_TAGS_H
+#define MP_CODEC_TAGS_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+struct mp_codec_params;
+
+void mp_set_codec_from_tag(struct mp_codec_params *c);
+
+void mp_set_pcm_codec(struct mp_codec_params *c, bool sign, bool is_float,
+ int bits, bool is_be);
+
+bool mp_codec_is_image(const char *codec);
+const char *mp_map_type_to_image_codec(const char *type);
+const char *mp_map_mimetype_to_video_codec(const char *mimetype);
+
+#endif
diff --git a/demux/cue.c b/demux/cue.c
new file mode 100644
index 0000000..104c598
--- /dev/null
+++ b/demux/cue.c
@@ -0,0 +1,270 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <inttypes.h>
+
+#include "mpv_talloc.h"
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/tags.h"
+
+#include "cue.h"
+
+#define SECS_PER_CUE_FRAME (1.0/75.0)
+
+enum cue_command {
+ CUE_ERROR = -1, // not a valid CUE command, or an unknown extension
+ CUE_EMPTY, // line with whitespace only
+ CUE_UNUSED, // valid CUE command, but ignored by this code
+ CUE_FILE,
+ CUE_TRACK,
+ CUE_INDEX,
+ CUE_TITLE,
+ CUE_PERFORMER,
+};
+
+static const struct {
+ enum cue_command command;
+ const char *text;
+} cue_command_strings[] = {
+ { CUE_FILE, "FILE" },
+ { CUE_TRACK, "TRACK" },
+ { CUE_INDEX, "INDEX" },
+ { CUE_TITLE, "TITLE" },
+ { CUE_UNUSED, "CATALOG" },
+ { CUE_UNUSED, "CDTEXTFILE" },
+ { CUE_UNUSED, "FLAGS" },
+ { CUE_UNUSED, "ISRC" },
+ { CUE_PERFORMER, "PERFORMER" },
+ { CUE_UNUSED, "POSTGAP" },
+ { CUE_UNUSED, "PREGAP" },
+ { CUE_UNUSED, "REM" },
+ { CUE_UNUSED, "SONGWRITER" },
+ { CUE_UNUSED, "MESSAGE" },
+ { -1 },
+};
+
+static const uint8_t spaces[] = {' ', '\f', '\n', '\r', '\t', '\v', 0xA0};
+
+static struct bstr lstrip_whitespace(struct bstr data)
+{
+ while (data.len) {
+ bstr rest = data;
+ int code = bstr_decode_utf8(data, &rest);
+ if (code < 0) {
+ // Tolerate Latin1 => probing works (which doesn't convert charsets).
+ code = data.start[0];
+ rest.start += 1;
+ rest.len -= 1;
+ }
+ for (size_t n = 0; n < MP_ARRAY_SIZE(spaces); n++) {
+ if (spaces[n] == code) {
+ data = rest;
+ goto next;
+ }
+ }
+ break;
+ next: ;
+ }
+ return data;
+}
+
+static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
+{
+ struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
+ line = lstrip_whitespace(line);
+ if (line.len == 0)
+ return CUE_EMPTY;
+ for (int n = 0; cue_command_strings[n].command != -1; n++) {
+ struct bstr name = bstr0(cue_command_strings[n].text);
+ if (bstr_case_startswith(line, name)) {
+ struct bstr rest = bstr_cut(line, name.len);
+ struct bstr par = lstrip_whitespace(rest);
+ if (rest.len && par.len == rest.len)
+ continue;
+ if (out_params)
+ *out_params = par;
+ return cue_command_strings[n].command;
+ }
+ }
+ return CUE_ERROR;
+}
+
+static bool eat_char(struct bstr *data, char ch)
+{
+ if (data->len && data->start[0] == ch) {
+ *data = bstr_cut(*data, 1);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+static char *read_quoted(void *talloc_ctx, struct bstr *data)
+{
+ *data = lstrip_whitespace(*data);
+ if (!eat_char(data, '"'))
+ return NULL;
+ int end = bstrchr(*data, '"');
+ if (end < 0)
+ return NULL;
+ struct bstr res = bstr_splice(*data, 0, end);
+ *data = bstr_cut(*data, end + 1);
+ return bstrto0(talloc_ctx, res);
+}
+
+static struct bstr strip_quotes(struct bstr data)
+{
+ bstr s = data;
+ if (bstr_eatstart0(&s, "\"") && bstr_eatend0(&s, "\""))
+ return s;
+ return data;
+}
+
+// Read an unsigned decimal integer.
+// Optionally check if it is 2 digit.
+// Return -1 on failure.
+static int read_int(struct bstr *data, bool two_digit)
+{
+ *data = lstrip_whitespace(*data);
+ if (data->len && data->start[0] == '-')
+ return -1;
+ struct bstr s = *data;
+ int res = (int)bstrtoll(s, &s, 10);
+ if (data->len == s.len || (two_digit && data->len - s.len > 2))
+ return -1;
+ *data = s;
+ return res;
+}
+
+static double read_time(struct bstr *data)
+{
+ struct bstr s = *data;
+ bool ok = true;
+ double t1 = read_int(&s, false);
+ ok = eat_char(&s, ':') && ok;
+ double t2 = read_int(&s, true);
+ ok = eat_char(&s, ':') && ok;
+ double t3 = read_int(&s, true);
+ ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
+ return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
+}
+
+static struct bstr skip_utf8_bom(struct bstr data)
+{
+ return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
+}
+
+// Check if the text in data is most likely CUE data. This is used by the
+// demuxer code to check the file type.
+// data is the start of the probed file, possibly cut off at a random point.
+bool mp_probe_cue(struct bstr data)
+{
+ bool valid = false;
+ data = skip_utf8_bom(data);
+ for (;;) {
+ enum cue_command cmd = read_cmd(&data, NULL);
+ // End reached. Since the line was most likely cut off, don't use the
+ // result of the last parsing call.
+ if (data.len == 0)
+ break;
+ if (cmd == CUE_ERROR)
+ return false;
+ if (cmd != CUE_EMPTY)
+ valid = true;
+ }
+ return valid;
+}
+
+struct cue_file *mp_parse_cue(struct bstr data)
+{
+ struct cue_file *f = talloc_zero(NULL, struct cue_file);
+ f->tags = talloc_zero(f, struct mp_tags);
+
+ data = skip_utf8_bom(data);
+
+ char *filename = NULL;
+ // Global metadata, and copied into new tracks.
+ struct cue_track proto_track = {0};
+ struct cue_track *cur_track = NULL;
+
+ while (data.len) {
+ struct bstr param;
+ int cmd = read_cmd(&data, &param);
+ switch (cmd) {
+ case CUE_ERROR:
+ talloc_free(f);
+ return NULL;
+ case CUE_TRACK: {
+ MP_TARRAY_GROW(f, f->tracks, f->num_tracks);
+ f->num_tracks += 1;
+ cur_track = &f->tracks[f->num_tracks - 1];
+ *cur_track = proto_track;
+ cur_track->tags = talloc_zero(f, struct mp_tags);
+ break;
+ }
+ case CUE_TITLE:
+ case CUE_PERFORMER: {
+ static const char *metanames[] = {
+ [CUE_TITLE] = "title",
+ [CUE_PERFORMER] = "performer",
+ };
+ struct mp_tags *tags = cur_track ? cur_track->tags : f->tags;
+ mp_tags_set_bstr(tags, bstr0(metanames[cmd]), strip_quotes(param));
+ break;
+ }
+ case CUE_INDEX: {
+ int type = read_int(&param, true);
+ double time = read_time(&param);
+ if (cur_track) {
+ if (type == 1) {
+ cur_track->start = time;
+ cur_track->filename = filename;
+ } else if (type == 0) {
+ cur_track->pregap_start = time;
+ }
+ }
+ break;
+ }
+ case CUE_FILE:
+ // NOTE: FILE comes before TRACK, so don't use cur_track->filename
+ filename = read_quoted(f, &param);
+ break;
+ }
+ }
+
+ return f;
+}
+
+int mp_check_embedded_cue(struct cue_file *f)
+{
+ char *fn0 = f->tracks[0].filename;
+ for (int n = 1; n < f->num_tracks; n++) {
+ char *fn = f->tracks[n].filename;
+ // both filenames have the same address (including NULL)
+ if (fn0 == fn)
+ continue;
+ // only one filename is NULL, or the strings don't match
+ if (!fn0 || !fn || strcmp(fn0, fn) != 0)
+ return -1;
+ }
+ return 0;
+}
diff --git a/demux/cue.h b/demux/cue.h
new file mode 100644
index 0000000..61f18e6
--- /dev/null
+++ b/demux/cue.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_CUE_H_
+#define MP_CUE_H_
+
+#include <stdbool.h>
+
+#include "misc/bstr.h"
+
+struct cue_file {
+ struct cue_track *tracks;
+ int num_tracks;
+ struct mp_tags *tags;
+};
+
+struct cue_track {
+ double pregap_start; // corresponds to INDEX 00
+ double start; // corresponds to INDEX 01
+ char *filename;
+ int source;
+ struct mp_tags *tags;
+};
+
+bool mp_probe_cue(struct bstr data);
+struct cue_file *mp_parse_cue(struct bstr data);
+int mp_check_embedded_cue(struct cue_file *f);
+
+#endif
diff --git a/demux/demux.c b/demux/demux.c
new file mode 100644
index 0000000..256e1b6
--- /dev/null
+++ b/demux/demux.c
@@ -0,0 +1,4624 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <float.h>
+#include <limits.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "cache.h"
+#include "config.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "mpv_talloc.h"
+#include "common/av_common.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "common/recorder.h"
+#include "common/stats.h"
+#include "misc/charset_conv.h"
+#include "misc/thread_tools.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+
+#include "stream/stream.h"
+#include "demux.h"
+#include "timeline.h"
+#include "stheader.h"
+#include "cue.h"
+
+// Demuxer list
+extern const struct demuxer_desc demuxer_desc_edl;
+extern const struct demuxer_desc demuxer_desc_cue;
+extern const demuxer_desc_t demuxer_desc_rawaudio;
+extern const demuxer_desc_t demuxer_desc_rawvideo;
+extern const demuxer_desc_t demuxer_desc_mf;
+extern const demuxer_desc_t demuxer_desc_matroska;
+extern const demuxer_desc_t demuxer_desc_lavf;
+extern const demuxer_desc_t demuxer_desc_playlist;
+extern const demuxer_desc_t demuxer_desc_disc;
+extern const demuxer_desc_t demuxer_desc_rar;
+extern const demuxer_desc_t demuxer_desc_libarchive;
+extern const demuxer_desc_t demuxer_desc_null;
+extern const demuxer_desc_t demuxer_desc_timeline;
+
+static const demuxer_desc_t *const demuxer_list[] = {
+ &demuxer_desc_disc,
+ &demuxer_desc_edl,
+ &demuxer_desc_cue,
+ &demuxer_desc_rawaudio,
+ &demuxer_desc_rawvideo,
+ &demuxer_desc_matroska,
+#if HAVE_LIBARCHIVE
+ &demuxer_desc_libarchive,
+#endif
+ &demuxer_desc_lavf,
+ &demuxer_desc_mf,
+ &demuxer_desc_playlist,
+ &demuxer_desc_null,
+ NULL
+};
+
+#define OPT_BASE_STRUCT struct demux_opts
+
+static bool get_demux_sub_opts(int index, const struct m_sub_options **sub);
+
+const struct m_sub_options demux_conf = {
+ .opts = (const struct m_option[]){
+ {"cache", OPT_CHOICE(enable_cache,
+ {"no", 0}, {"auto", -1}, {"yes", 1})},
+ {"cache-on-disk", OPT_BOOL(disk_cache)},
+ {"demuxer-readahead-secs", OPT_DOUBLE(min_secs), M_RANGE(0, DBL_MAX)},
+ {"demuxer-hysteresis-secs", OPT_DOUBLE(hyst_secs), M_RANGE(0, DBL_MAX)},
+ {"demuxer-max-bytes", OPT_BYTE_SIZE(max_bytes),
+ M_RANGE(0, M_MAX_MEM_BYTES)},
+ {"demuxer-max-back-bytes", OPT_BYTE_SIZE(max_bytes_bw),
+ M_RANGE(0, M_MAX_MEM_BYTES)},
+ {"demuxer-donate-buffer", OPT_BOOL(donate_fw)},
+ {"force-seekable", OPT_BOOL(force_seekable)},
+ {"cache-secs", OPT_DOUBLE(min_secs_cache), M_RANGE(0, DBL_MAX)},
+ {"access-references", OPT_BOOL(access_references)},
+ {"demuxer-seekable-cache", OPT_CHOICE(seekable_cache,
+ {"auto", -1}, {"no", 0}, {"yes", 1})},
+ {"index", OPT_CHOICE(index_mode, {"default", 1}, {"recreate", 0})},
+ {"mf-fps", OPT_DOUBLE(mf_fps)},
+ {"mf-type", OPT_STRING(mf_type)},
+ {"sub-create-cc-track", OPT_BOOL(create_ccs)},
+ {"stream-record", OPT_STRING(record_file)},
+ {"video-backward-overlap", OPT_CHOICE(video_back_preroll, {"auto", -1}),
+ M_RANGE(0, 1024)},
+ {"audio-backward-overlap", OPT_CHOICE(audio_back_preroll, {"auto", -1}),
+ M_RANGE(0, 1024)},
+ {"video-backward-batch", OPT_INT(back_batch[STREAM_VIDEO]),
+ M_RANGE(0, 1024)},
+ {"audio-backward-batch", OPT_INT(back_batch[STREAM_AUDIO]),
+ M_RANGE(0, 1024)},
+ {"demuxer-backward-playback-step", OPT_DOUBLE(back_seek_size),
+ M_RANGE(0, DBL_MAX)},
+ {"metadata-codepage", OPT_STRING(meta_cp)},
+ {0}
+ },
+ .size = sizeof(struct demux_opts),
+ .defaults = &(const struct demux_opts){
+ .enable_cache = -1, // auto
+ .max_bytes = 150 * 1024 * 1024,
+ .max_bytes_bw = 50 * 1024 * 1024,
+ .donate_fw = true,
+ .min_secs = 1.0,
+ .min_secs_cache = 1000.0 * 60 * 60,
+ .seekable_cache = -1,
+ .index_mode = 1,
+ .mf_fps = 1.0,
+ .access_references = true,
+ .video_back_preroll = -1,
+ .audio_back_preroll = -1,
+ .back_seek_size = 60,
+ .back_batch = {
+ [STREAM_VIDEO] = 1,
+ [STREAM_AUDIO] = 10,
+ },
+ .meta_cp = "auto",
+ },
+ .get_sub_options = get_demux_sub_opts,
+};
+
+struct demux_internal {
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct stats_ctx *stats;
+
+ bool can_cache; // not a slave demuxer; caching makes sense
+ bool can_record; // stream recording is allowed
+
+ // The demuxer runs potentially in another thread, so we keep two demuxer
+ // structs; the real demuxer can access the shadow struct only.
+ struct demuxer *d_thread; // accessed by demuxer impl. (producer)
+ struct demuxer *d_user; // accessed by player (consumer)
+
+ // The lock protects the packet queues (struct demux_stream),
+ // and the fields below.
+ mp_mutex lock;
+ mp_cond wakeup;
+ mp_thread thread;
+
+ // -- All the following fields are protected by lock.
+
+ bool thread_terminate;
+ bool threading;
+ bool shutdown_async;
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_cb_ctx;
+
+ struct sh_stream **streams;
+ int num_streams;
+
+ char *meta_charset;
+
+ // If non-NULL, a stream which is used for global (timed) metadata. It will
+ // be an arbitrary stream, which hopefully will happen to work.
+ struct sh_stream *metadata_stream;
+
+ int events;
+
+ struct demux_cache *cache;
+
+ bool warned_queue_overflow;
+ bool eof; // whether we're in EOF state
+ double min_secs;
+ double hyst_secs; // stop reading till there's hyst_secs remaining
+ bool hyst_active;
+ size_t max_bytes;
+ size_t max_bytes_bw;
+ bool seekable_cache;
+ bool using_network_cache_opts;
+ char *record_filename;
+
+ // Whether the demuxer thread should prefetch packets. This is set to false
+ // if EOF was reached or the demuxer cache is full. This is also important
+ // in the initial state: the decoder thread needs to select streams before
+ // the first packet is read, so this is set to true by packet reading only.
+ // Reset to false again on EOF or if prefetching is done.
+ bool reading;
+
+ // Set if we just performed a seek, without reading packets yet. Used to
+ // avoid a redundant initial seek after enabling streams. We could just
+ // allow it, but to avoid buggy seeking affecting normal playback, we don't.
+ bool after_seek;
+ // Set in addition to after_seek if we think we seeked to the start of the
+ // file (or if the demuxer was just opened).
+ bool after_seek_to_start;
+
+ // Demuxing backwards. Since demuxer implementations don't support this
+ // directly, it is emulated by seeking backwards for every packet run. Also,
+ // packets between keyframes are demuxed forwards (you can't decode that
+ // stuff otherwise), which adds complexity on top of it.
+ bool back_demuxing;
+
+ // For backward demuxing:
+ bool need_back_seek; // back-step seek needs to be triggered
+ bool back_any_need_recheck; // at least 1 ds->back_need_recheck set
+
+ bool tracks_switched; // thread needs to inform demuxer of this
+
+ bool seeking; // there's a seek queued
+ int seek_flags; // flags for next seek (if seeking==true)
+ double seek_pts;
+
+ // (fields for debugging)
+ double seeking_in_progress; // low level seek state
+ int low_level_seeks; // number of started low level seeks
+ double demux_ts; // last demuxed DTS or PTS
+
+ double ts_offset; // timestamp offset to apply to everything
+
+ // (sorted by least recent use: index 0 is least recently used)
+ struct demux_cached_range **ranges;
+ int num_ranges;
+
+ size_t total_bytes; // total sum of packet data buffered
+ // Range from which decoder is reading, and to which demuxer is appending.
+ // This is normally never NULL. This is always ranges[num_ranges - 1].
+ // This is can be NULL during initialization or deinitialization.
+ struct demux_cached_range *current_range;
+
+ double highest_av_pts; // highest non-subtitle PTS seen - for duration
+
+ bool blocked;
+
+ // Transient state.
+ double duration;
+ // Cached state.
+ int64_t stream_size;
+ int64_t last_speed_query;
+ double speed_query_prev_sample;
+ uint64_t bytes_per_second;
+ int64_t next_cache_update;
+
+ // demux user state (user thread, somewhat similar to reader/decoder state)
+ double last_playback_pts; // last playback_pts from demux_update()
+ bool force_metadata_update;
+ int cached_metadata_index; // speed up repeated lookups
+
+ struct mp_recorder *dumper;
+ int dumper_status;
+
+ bool owns_stream;
+
+ // -- Access from demuxer thread only
+ bool enable_recording;
+ struct mp_recorder *recorder;
+ int64_t slave_unbuffered_read_bytes; // value repoted from demuxer impl.
+ int64_t hack_unbuffered_read_bytes; // for demux_get_bytes_read_hack()
+ int64_t cache_unbuffered_read_bytes; // for demux_reader_state.bytes_per_second
+ int64_t byte_level_seeks; // for demux_reader_state.byte_level_seeks
+};
+
+struct timed_metadata {
+ double pts;
+ struct mp_tags *tags;
+ bool from_stream;
+};
+
+// A continuous range of cached packets for all enabled streams.
+// (One demux_queue for each known stream.)
+struct demux_cached_range {
+ // streams[] is indexed by demux_stream->index
+ struct demux_queue **streams;
+ int num_streams;
+
+ // Computed from the stream queue's values. These fields (unlike as with
+ // demux_queue) are always either NOPTS, or fully valid.
+ double seek_start, seek_end;
+
+ bool is_bof; // set if the file begins with this range
+ bool is_eof; // set if the file ends with this range
+
+ struct timed_metadata **metadata;
+ int num_metadata;
+};
+
+#define QUEUE_INDEX_SIZE_MASK(queue) ((queue)->index_size - 1)
+
+// Access the idx-th entry in the given demux_queue.
+// Requirement: idx >= 0 && idx < queue->num_index
+#define QUEUE_INDEX_ENTRY(queue, idx) \
+ ((queue)->index[((queue)->index0 + (idx)) & QUEUE_INDEX_SIZE_MASK(queue)])
+
+// Don't index packets whose timestamps that are within the last index entry by
+// this amount of time (it's better to seek them manually).
+#define INDEX_STEP_SIZE 1.0
+
+struct index_entry {
+ double pts;
+ struct demux_packet *pkt;
+};
+
+// A continuous list of cached packets for a single stream/range. There is one
+// for each stream and range. Also contains some state for use during demuxing
+// (keeping it across seeks makes it easier to resume demuxing).
+struct demux_queue {
+ struct demux_stream *ds;
+ struct demux_cached_range *range;
+
+ struct demux_packet *head;
+ struct demux_packet *tail;
+
+ uint64_t tail_cum_pos; // cumulative size including tail packet
+
+ bool correct_dts; // packet DTS is strictly monotonically increasing
+ bool correct_pos; // packet pos is strictly monotonically increasing
+ int64_t last_pos; // for determining correct_pos
+ int64_t last_pos_fixup; // for filling in unset dp->pos values
+ double last_dts; // for determining correct_dts
+ double last_ts; // timestamp of the last packet added to queue
+
+ // for incrementally determining seek PTS range
+ struct demux_packet *keyframe_latest;
+ struct demux_packet *keyframe_first; // cached value of first KF packet
+
+ // incrementally maintained seek range, possibly invalid
+ double seek_start, seek_end;
+ double last_pruned; // timestamp of last pruned keyframe
+
+ bool is_bof; // started demuxing at beginning of file
+ bool is_eof; // received true EOF here
+
+ // Complete index, though it may skip some entries to reduce density.
+ struct index_entry *index; // ring buffer
+ size_t index_size; // size of index[] (0 or a power of 2)
+ size_t index0; // first index entry
+ size_t num_index; // number of index entries (wraps on index_size)
+};
+
+struct demux_stream {
+ struct demux_internal *in;
+ struct sh_stream *sh; // ds->sh->ds == ds
+ enum stream_type type; // equals to sh->type
+ int index; // equals to sh->index
+ // --- all fields are protected by in->lock
+
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_cb_ctx;
+
+ // demuxer state
+ bool selected; // user wants packets from this stream
+ bool eager; // try to keep at least 1 packet queued
+ // if false, this stream is disabled, or passively
+ // read (like subtitles)
+ bool still_image; // stream has still video images
+ bool refreshing; // finding old position after track switches
+ bool eof; // end of demuxed stream? (true if no more packets)
+
+ bool global_correct_dts;// all observed so far
+ bool global_correct_pos;
+
+ // current queue - used both for reading and demuxing (this is never NULL)
+ struct demux_queue *queue;
+
+ // reader (decoder) state (bitrate calculations are part of it because we
+ // want to return the bitrate closest to the "current position")
+ double base_ts; // timestamp of the last packet returned to decoder
+ double last_br_ts; // timestamp of last packet bitrate was calculated
+ size_t last_br_bytes; // summed packet sizes since last bitrate calculation
+ double bitrate;
+ struct demux_packet *reader_head; // points at current decoder position
+ bool skip_to_keyframe;
+ bool attached_picture_added;
+ bool need_wakeup; // call wakeup_cb on next reader_head state change
+ double force_read_until;// eager=false streams (subs): force read-ahead
+
+ // For demux_internal.dumper. Currently, this is used only temporarily
+ // during blocking dumping.
+ struct demux_packet *dump_pos;
+
+ // for refresh seeks: pos/dts of last packet returned to reader
+ int64_t last_ret_pos;
+ double last_ret_dts;
+
+ // Backwards demuxing.
+ bool back_need_recheck; // flag for incremental find_backward_restart_pos work
+ // pos/dts of the previous keyframe packet returned; always valid if back-
+ // demuxing is enabled, and back_restart_eof/back_restart_next are false.
+ int64_t back_restart_pos;
+ double back_restart_dts;
+ bool back_restart_eof; // restart position is at EOF; overrides pos/dts
+ bool back_restart_next; // restart before next keyframe; overrides above
+ bool back_restarting; // searching keyframe before restart pos
+ // Current PTS lower bound for back demuxing.
+ double back_seek_pos;
+ // pos/dts of the packet to resume demuxing from when another stream caused
+ // a seek backward to get more packets. reader_head will be reset to this
+ // packet as soon as it's encountered again.
+ int64_t back_resume_pos;
+ double back_resume_dts;
+ bool back_resuming; // resuming mode (above fields are valid/used)
+ // Set to true if the first packet (keyframe) of a range was returned.
+ bool back_range_started;
+ // Number of KF packets at start of range yet to return. -1 is used for BOF.
+ int back_range_count;
+ // Number of KF packets yet to return that are marked as preroll.
+ int back_range_preroll;
+ // Static packet preroll count.
+ int back_preroll;
+
+ // for closed captions (demuxer_feed_caption)
+ struct sh_stream *cc;
+ bool ignore_eof; // ignore stream in underrun detection
+};
+
+static void switch_to_fresh_cache_range(struct demux_internal *in);
+static void demuxer_sort_chapters(demuxer_t *demuxer);
+static MP_THREAD_VOID demux_thread(void *pctx);
+static void update_cache(struct demux_internal *in);
+static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp);
+static struct demux_packet *advance_reader_head(struct demux_stream *ds);
+static bool queue_seek(struct demux_internal *in, double seek_pts, int flags,
+ bool clear_back_state);
+static struct demux_packet *compute_keyframe_times(struct demux_packet *pkt,
+ double *out_kf_min,
+ double *out_kf_max);
+static void find_backward_restart_pos(struct demux_stream *ds);
+static struct demux_packet *find_seek_target(struct demux_queue *queue,
+ double pts, int flags);
+static void prune_old_packets(struct demux_internal *in);
+static void dumper_close(struct demux_internal *in);
+static void demux_convert_tags_charset(struct demuxer *demuxer);
+
+static uint64_t get_forward_buffered_bytes(struct demux_stream *ds)
+{
+ if (!ds->reader_head)
+ return 0;
+ return ds->queue->tail_cum_pos - ds->reader_head->cum_pos;
+}
+
+#if 0
+// very expensive check for redundant cached queue state
+static void check_queue_consistency(struct demux_internal *in)
+{
+ uint64_t total_bytes = 0;
+
+ assert(in->current_range && in->num_ranges > 0);
+ assert(in->current_range == in->ranges[in->num_ranges - 1]);
+
+ for (int n = 0; n < in->num_ranges; n++) {
+ struct demux_cached_range *range = in->ranges[n];
+ int range_num_packets = 0;
+
+ assert(range->num_streams == in->num_streams);
+
+ for (int i = 0; i < range->num_streams; i++) {
+ struct demux_queue *queue = range->streams[i];
+
+ assert(queue->range == range);
+
+ size_t fw_bytes = 0;
+ bool is_forward = false;
+ bool kf_found = false;
+ bool kf1_found = false;
+ size_t next_index = 0;
+ uint64_t queue_total_bytes = 0;
+ for (struct demux_packet *dp = queue->head; dp; dp = dp->next) {
+ is_forward |= dp == queue->ds->reader_head;
+ kf_found |= dp == queue->keyframe_latest;
+ kf1_found |= dp == queue->keyframe_first;
+
+ size_t bytes = demux_packet_estimate_total_size(dp);
+ total_bytes += bytes;
+ queue_total_bytes += bytes;
+ if (is_forward) {
+ fw_bytes += bytes;
+ assert(range == in->current_range);
+ assert(queue->ds->queue == queue);
+ }
+ range_num_packets += 1;
+
+ if (!dp->next)
+ assert(queue->tail == dp);
+
+ if (next_index < queue->num_index &&
+ QUEUE_INDEX_ENTRY(queue, next_index).pkt == dp)
+ next_index += 1;
+ }
+ if (!queue->head)
+ assert(!queue->tail);
+ assert(next_index == queue->num_index);
+
+ uint64_t queue_total_bytes2 = 0;
+ if (queue->head)
+ queue_total_bytes2 = queue->tail_cum_pos - queue->head->cum_pos;
+
+ assert(queue_total_bytes == queue_total_bytes2);
+
+ // If the queue is currently used...
+ if (queue->ds->queue == queue) {
+ // ...reader_head and others must be in the queue.
+ assert(is_forward == !!queue->ds->reader_head);
+ assert(kf_found == !!queue->keyframe_latest);
+ uint64_t fw_bytes2 = get_forward_buffered_bytes(queue->ds);
+ assert(fw_bytes == fw_bytes2);
+ }
+
+ assert(kf1_found == !!queue->keyframe_first);
+
+ if (range != in->current_range) {
+ assert(fw_bytes == 0);
+ }
+
+ if (queue->keyframe_latest)
+ assert(queue->keyframe_latest->keyframe);
+
+ total_bytes += queue->index_size * sizeof(struct index_entry);
+ }
+
+ // Invariant needed by pruning; violation has worse effects than just
+ // e.g. broken seeking due to incorrect seek ranges.
+ if (range->seek_start != MP_NOPTS_VALUE)
+ assert(range_num_packets > 0);
+ }
+
+ assert(in->total_bytes == total_bytes);
+}
+#endif
+
+// (this doesn't do most required things for a switch, like updating ds->queue)
+static void set_current_range(struct demux_internal *in,
+ struct demux_cached_range *range)
+{
+ in->current_range = range;
+
+ // Move to in->ranges[in->num_ranges-1] (for LRU sorting/invariant)
+ for (int n = 0; n < in->num_ranges; n++) {
+ if (in->ranges[n] == range) {
+ MP_TARRAY_REMOVE_AT(in->ranges, in->num_ranges, n);
+ break;
+ }
+ }
+ MP_TARRAY_APPEND(in, in->ranges, in->num_ranges, range);
+}
+
+static void prune_metadata(struct demux_cached_range *range)
+{
+ int first_needed = 0;
+
+ if (range->seek_start == MP_NOPTS_VALUE) {
+ first_needed = range->num_metadata;
+ } else {
+ for (int n = 0; n < range->num_metadata ; n++) {
+ if (range->metadata[n]->pts > range->seek_start)
+ break;
+ first_needed = n;
+ }
+ }
+
+ // Always preserve the last entry.
+ first_needed = MPMIN(first_needed, range->num_metadata - 1);
+
+ // (Could make this significantly more efficient for large first_needed,
+ // however that might be very rare and even then it might not matter.)
+ for (int n = 0; n < first_needed; n++) {
+ talloc_free(range->metadata[0]);
+ MP_TARRAY_REMOVE_AT(range->metadata, range->num_metadata, 0);
+ }
+}
+
+// Refresh range->seek_start/end. Idempotent.
+static void update_seek_ranges(struct demux_cached_range *range)
+{
+ range->seek_start = range->seek_end = MP_NOPTS_VALUE;
+ range->is_bof = true;
+ range->is_eof = true;
+
+ double min_start_pts = MP_NOPTS_VALUE;
+ double max_end_pts = MP_NOPTS_VALUE;
+
+ for (int n = 0; n < range->num_streams; n++) {
+ struct demux_queue *queue = range->streams[n];
+
+ if (queue->ds->selected && queue->ds->eager) {
+ if (queue->is_bof) {
+ min_start_pts = MP_PTS_MIN(min_start_pts, queue->seek_start);
+ } else {
+ range->seek_start =
+ MP_PTS_MAX(range->seek_start, queue->seek_start);
+ }
+
+ if (queue->is_eof) {
+ max_end_pts = MP_PTS_MAX(max_end_pts, queue->seek_end);
+ } else {
+ range->seek_end = MP_PTS_MIN(range->seek_end, queue->seek_end);
+ }
+
+ range->is_eof &= queue->is_eof;
+ range->is_bof &= queue->is_bof;
+
+ bool empty = queue->is_eof && !queue->head;
+ if (queue->seek_start >= queue->seek_end && !empty &&
+ !(queue->seek_start == queue->seek_end &&
+ queue->seek_start != MP_NOPTS_VALUE))
+ goto broken;
+ }
+ }
+
+ if (range->is_eof)
+ range->seek_end = max_end_pts;
+ if (range->is_bof)
+ range->seek_start = min_start_pts;
+
+ // Sparse (subtitle) stream behavior is not very clearly defined, but
+ // usually we don't want it to restrict the range of other streams. For
+ // example, if there are subtitle packets at position 5 and 10 seconds, and
+ // the demuxer demuxed the other streams until position 7 seconds, the seek
+ // range end position is 7.
+ // Assume that reading a non-sparse (audio/video) packet gets all sparse
+ // packets that are needed before that non-sparse packet.
+ // This is incorrect in any of these cases:
+ // - sparse streams only (it's unknown how to determine an accurate range)
+ // - if sparse streams have non-keyframe packets (we set queue->last_pruned
+ // to the start of the pruned keyframe range - we'd need the end or so)
+ // We also assume that ds->eager equals to a stream not being sparse
+ // (usually true, except if only sparse streams are selected).
+ // We also rely on the fact that the demuxer position will always be ahead
+ // of the seek_end for audio/video, because they need to prefetch at least
+ // 1 packet to detect the end of a keyframe range. This means that there's
+ // a relatively high guarantee to have all sparse (subtitle) packets within
+ // the seekable range.
+ // As a consequence, the code _never_ checks queue->seek_end for a sparse
+ // queue, as the end of it is implied by the highest PTS of a non-sparse
+ // stream (i.e. the latest demuxer position).
+ // On the other hand, if a sparse packet was pruned, and that packet has
+ // a higher PTS than seek_start for non-sparse queues, that packet is
+ // missing. So the range's seek_start needs to be adjusted accordingly.
+ for (int n = 0; n < range->num_streams; n++) {
+ struct demux_queue *queue = range->streams[n];
+ if (queue->ds->selected && !queue->ds->eager &&
+ queue->last_pruned != MP_NOPTS_VALUE &&
+ range->seek_start != MP_NOPTS_VALUE)
+ {
+ // (last_pruned is _exclusive_ to the seekable range, so add a small
+ // value to exclude it from the valid range.)
+ range->seek_start =
+ MP_PTS_MAX(range->seek_start, queue->last_pruned + 0.1);
+ }
+ }
+
+ if (range->seek_start >= range->seek_end)
+ goto broken;
+
+ prune_metadata(range);
+ return;
+
+broken:
+ range->seek_start = range->seek_end = MP_NOPTS_VALUE;
+ prune_metadata(range);
+}
+
+// Remove queue->head from the queue.
+static void remove_head_packet(struct demux_queue *queue)
+{
+ struct demux_packet *dp = queue->head;
+
+ assert(queue->ds->reader_head != dp);
+ if (queue->keyframe_first == dp)
+ queue->keyframe_first = NULL;
+ if (queue->keyframe_latest == dp)
+ queue->keyframe_latest = NULL;
+ queue->is_bof = false;
+
+ uint64_t end_pos = dp->next ? dp->next->cum_pos : queue->tail_cum_pos;
+ queue->ds->in->total_bytes -= end_pos - dp->cum_pos;
+
+ if (queue->num_index && queue->index[queue->index0].pkt == dp) {
+ queue->index0 = (queue->index0 + 1) & QUEUE_INDEX_SIZE_MASK(queue);
+ queue->num_index -= 1;
+ }
+
+ queue->head = dp->next;
+ if (!queue->head)
+ queue->tail = NULL;
+
+ talloc_free(dp);
+}
+
+static void free_index(struct demux_queue *queue)
+{
+ struct demux_stream *ds = queue->ds;
+ struct demux_internal *in = ds->in;
+
+ in->total_bytes -= queue->index_size * sizeof(queue->index[0]);
+ queue->index_size = 0;
+ queue->index0 = 0;
+ queue->num_index = 0;
+ TA_FREEP(&queue->index);
+}
+
+static void clear_queue(struct demux_queue *queue)
+{
+ struct demux_stream *ds = queue->ds;
+ struct demux_internal *in = ds->in;
+
+ if (queue->head)
+ in->total_bytes -= queue->tail_cum_pos - queue->head->cum_pos;
+
+ free_index(queue);
+
+ struct demux_packet *dp = queue->head;
+ while (dp) {
+ struct demux_packet *dn = dp->next;
+ assert(ds->reader_head != dp);
+ talloc_free(dp);
+ dp = dn;
+ }
+ queue->head = queue->tail = NULL;
+ queue->keyframe_first = NULL;
+ queue->keyframe_latest = NULL;
+ queue->seek_start = queue->seek_end = queue->last_pruned = MP_NOPTS_VALUE;
+
+ queue->correct_dts = queue->correct_pos = true;
+ queue->last_pos = -1;
+ queue->last_ts = queue->last_dts = MP_NOPTS_VALUE;
+ queue->last_pos_fixup = -1;
+
+ queue->is_eof = false;
+ queue->is_bof = false;
+}
+
+static void clear_cached_range(struct demux_internal *in,
+ struct demux_cached_range *range)
+{
+ for (int n = 0; n < range->num_streams; n++)
+ clear_queue(range->streams[n]);
+
+ for (int n = 0; n < range->num_metadata; n++)
+ talloc_free(range->metadata[n]);
+ range->num_metadata = 0;
+
+ update_seek_ranges(range);
+}
+
+// Remove ranges with no data (except in->current_range). Also remove excessive
+// ranges.
+static void free_empty_cached_ranges(struct demux_internal *in)
+{
+ while (1) {
+ struct demux_cached_range *worst = NULL;
+
+ int end = in->num_ranges - 1;
+
+ // (Not set during early init or late destruction.)
+ if (in->current_range) {
+ assert(in->current_range && in->num_ranges > 0);
+ assert(in->current_range == in->ranges[in->num_ranges - 1]);
+ end -= 1;
+ }
+
+ for (int n = end; n >= 0; n--) {
+ struct demux_cached_range *range = in->ranges[n];
+ if (range->seek_start == MP_NOPTS_VALUE || !in->seekable_cache) {
+ clear_cached_range(in, range);
+ MP_TARRAY_REMOVE_AT(in->ranges, in->num_ranges, n);
+ for (int i = 0; i < range->num_streams; i++)
+ talloc_free(range->streams[i]);
+ talloc_free(range);
+ } else {
+ if (!worst || (range->seek_end - range->seek_start <
+ worst->seek_end - worst->seek_start))
+ worst = range;
+ }
+ }
+
+ if (in->num_ranges <= MAX_SEEK_RANGES || !worst)
+ break;
+
+ clear_cached_range(in, worst);
+ }
+}
+
+static void ds_clear_reader_queue_state(struct demux_stream *ds)
+{
+ ds->reader_head = NULL;
+ ds->eof = false;
+ ds->need_wakeup = true;
+}
+
+static void ds_clear_reader_state(struct demux_stream *ds,
+ bool clear_back_state)
+{
+ ds_clear_reader_queue_state(ds);
+
+ ds->base_ts = ds->last_br_ts = MP_NOPTS_VALUE;
+ ds->last_br_bytes = 0;
+ ds->bitrate = -1;
+ ds->skip_to_keyframe = false;
+ ds->attached_picture_added = false;
+ ds->last_ret_pos = -1;
+ ds->last_ret_dts = MP_NOPTS_VALUE;
+ ds->force_read_until = MP_NOPTS_VALUE;
+
+ if (clear_back_state) {
+ ds->back_restart_pos = -1;
+ ds->back_restart_dts = MP_NOPTS_VALUE;
+ ds->back_restart_eof = false;
+ ds->back_restart_next = ds->in->back_demuxing;
+ ds->back_restarting = ds->in->back_demuxing && ds->eager;
+ ds->back_seek_pos = MP_NOPTS_VALUE;
+ ds->back_resume_pos = -1;
+ ds->back_resume_dts = MP_NOPTS_VALUE;
+ ds->back_resuming = false;
+ ds->back_range_started = false;
+ ds->back_range_count = 0;
+ ds->back_range_preroll = 0;
+ }
+}
+
+// called locked, from user thread only
+static void clear_reader_state(struct demux_internal *in,
+ bool clear_back_state)
+{
+ for (int n = 0; n < in->num_streams; n++)
+ ds_clear_reader_state(in->streams[n]->ds, clear_back_state);
+ in->warned_queue_overflow = false;
+ in->d_user->filepos = -1; // implicitly synchronized
+ in->blocked = false;
+ in->need_back_seek = false;
+}
+
+// Call if the observed reader state on this stream somehow changes. The wakeup
+// is skipped if the reader successfully read a packet, because that means we
+// expect it to come back and ask for more.
+static void wakeup_ds(struct demux_stream *ds)
+{
+ if (ds->need_wakeup) {
+ if (ds->wakeup_cb) {
+ ds->wakeup_cb(ds->wakeup_cb_ctx);
+ } else if (ds->in->wakeup_cb) {
+ ds->in->wakeup_cb(ds->in->wakeup_cb_ctx);
+ }
+ ds->need_wakeup = false;
+ mp_cond_signal(&ds->in->wakeup);
+ }
+}
+
+static void update_stream_selection_state(struct demux_internal *in,
+ struct demux_stream *ds)
+{
+ ds->eof = false;
+ ds->refreshing = false;
+
+ // We still have to go over the whole stream list to update ds->eager for
+ // other streams too, because they depend on other stream's selections.
+
+ bool any_av_streams = false;
+ bool any_streams = false;
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *s = in->streams[n]->ds;
+
+ s->still_image = s->sh->still_image;
+ s->eager = s->selected && !s->sh->attached_picture;
+ if (s->eager && !s->still_image)
+ any_av_streams |= s->type != STREAM_SUB;
+ any_streams |= s->selected;
+ }
+
+ // Subtitles are only eagerly read if there are no other eagerly read
+ // streams.
+ if (any_av_streams) {
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *s = in->streams[n]->ds;
+
+ if (s->type == STREAM_SUB)
+ s->eager = false;
+ }
+ }
+
+ if (!any_streams)
+ in->blocked = false;
+
+ ds_clear_reader_state(ds, true);
+
+ // Make sure any stream reselection or addition is reflected in the seek
+ // ranges, and also get rid of data that is not needed anymore (or
+ // rather, which can't be kept consistent). This has to happen after we've
+ // updated all the subtle state (like s->eager).
+ for (int n = 0; n < in->num_ranges; n++) {
+ struct demux_cached_range *range = in->ranges[n];
+
+ if (!ds->selected)
+ clear_queue(range->streams[ds->index]);
+
+ update_seek_ranges(range);
+ }
+
+ free_empty_cached_ranges(in);
+
+ wakeup_ds(ds);
+}
+
+void demux_set_ts_offset(struct demuxer *demuxer, double offset)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ in->ts_offset = offset;
+ mp_mutex_unlock(&in->lock);
+}
+
+static void add_missing_streams(struct demux_internal *in,
+ struct demux_cached_range *range)
+{
+ for (int n = range->num_streams; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ struct demux_queue *queue = talloc_ptrtype(NULL, queue);
+ *queue = (struct demux_queue){
+ .ds = ds,
+ .range = range,
+ };
+ clear_queue(queue);
+ MP_TARRAY_APPEND(range, range->streams, range->num_streams, queue);
+ assert(range->streams[ds->index] == queue);
+ }
+}
+
+// Allocate a new sh_stream of the given type. It either has to be released
+// with talloc_free(), or added to a demuxer with demux_add_sh_stream(). You
+// cannot add or read packets from the stream before it has been added.
+// type may be changed later, but only before demux_add_sh_stream().
+struct sh_stream *demux_alloc_sh_stream(enum stream_type type)
+{
+ struct sh_stream *sh = talloc_ptrtype(NULL, sh);
+ *sh = (struct sh_stream) {
+ .type = type,
+ .index = -1,
+ .ff_index = -1, // may be overwritten by demuxer
+ .demuxer_id = -1, // ... same
+ .program_id = -1, // ... same
+ .codec = talloc_zero(sh, struct mp_codec_params),
+ .tags = talloc_zero(sh, struct mp_tags),
+ };
+ sh->codec->type = type;
+ return sh;
+}
+
+// Add a new sh_stream to the demuxer. Note that as soon as the stream has been
+// added, it must be immutable, and must not be released (this will happen when
+// the demuxer is destroyed).
+static void demux_add_sh_stream_locked(struct demux_internal *in,
+ struct sh_stream *sh)
+{
+ assert(!sh->ds); // must not be added yet
+
+ sh->index = in->num_streams;
+
+ sh->ds = talloc(sh, struct demux_stream);
+ *sh->ds = (struct demux_stream) {
+ .in = in,
+ .sh = sh,
+ .type = sh->type,
+ .index = sh->index,
+ .global_correct_dts = true,
+ .global_correct_pos = true,
+ };
+
+ struct demux_stream *ds = sh->ds;
+
+ if (!sh->codec->codec)
+ sh->codec->codec = "";
+
+ if (sh->ff_index < 0)
+ sh->ff_index = sh->index;
+
+ MP_TARRAY_APPEND(in, in->streams, in->num_streams, sh);
+ assert(in->streams[sh->index] == sh);
+
+ if (in->current_range) {
+ for (int n = 0; n < in->num_ranges; n++)
+ add_missing_streams(in, in->ranges[n]);
+
+ sh->ds->queue = in->current_range->streams[sh->ds->index];
+ }
+
+ update_stream_selection_state(in, sh->ds);
+
+ switch (ds->type) {
+ case STREAM_AUDIO:
+ ds->back_preroll = in->d_user->opts->audio_back_preroll;
+ if (ds->back_preroll < 0) { // auto
+ ds->back_preroll = mp_codec_is_lossless(sh->codec->codec) ? 0 : 1;
+ if (sh->codec->codec && (strcmp(sh->codec->codec, "opus") == 0 ||
+ strcmp(sh->codec->codec, "vorbis") == 0 ||
+ strcmp(sh->codec->codec, "mp3") == 0))
+ ds->back_preroll = 2;
+ }
+ break;
+ case STREAM_VIDEO:
+ ds->back_preroll = in->d_user->opts->video_back_preroll;
+ if (ds->back_preroll < 0)
+ ds->back_preroll = 0; // auto
+ break;
+ }
+
+ if (!ds->sh->attached_picture) {
+ // Typically this is used for webradio, so any stream will do.
+ if (!in->metadata_stream)
+ in->metadata_stream = sh;
+ }
+
+ in->events |= DEMUX_EVENT_STREAMS;
+ if (in->wakeup_cb)
+ in->wakeup_cb(in->wakeup_cb_ctx);
+}
+
+// For demuxer implementations only.
+void demux_add_sh_stream(struct demuxer *demuxer, struct sh_stream *sh)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_thread);
+ mp_mutex_lock(&in->lock);
+ demux_add_sh_stream_locked(in, sh);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Return a stream with the given index. Since streams can only be added during
+// the lifetime of the demuxer, it is guaranteed that an index within the valid
+// range [0, demux_get_num_stream()) always returns a valid sh_stream pointer,
+// which will be valid until the demuxer is destroyed.
+struct sh_stream *demux_get_stream(struct demuxer *demuxer, int index)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ assert(index >= 0 && index < in->num_streams);
+ struct sh_stream *r = in->streams[index];
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+// See demux_get_stream().
+int demux_get_num_stream(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ int r = in->num_streams;
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+// It's UB to call anything but demux_dealloc() on the demuxer after this.
+static void demux_shutdown(struct demux_internal *in)
+{
+ struct demuxer *demuxer = in->d_user;
+
+ if (in->recorder) {
+ mp_recorder_destroy(in->recorder);
+ in->recorder = NULL;
+ }
+
+ dumper_close(in);
+
+ if (demuxer->desc->close)
+ demuxer->desc->close(in->d_thread);
+ demuxer->priv = NULL;
+ in->d_thread->priv = NULL;
+
+ demux_flush(demuxer);
+ assert(in->total_bytes == 0);
+
+ in->current_range = NULL;
+ free_empty_cached_ranges(in);
+
+ talloc_free(in->cache);
+ in->cache = NULL;
+
+ if (in->owns_stream)
+ free_stream(demuxer->stream);
+ demuxer->stream = NULL;
+}
+
+static void demux_dealloc(struct demux_internal *in)
+{
+ for (int n = 0; n < in->num_streams; n++)
+ talloc_free(in->streams[n]);
+ mp_mutex_destroy(&in->lock);
+ mp_cond_destroy(&in->wakeup);
+ talloc_free(in->d_user);
+}
+
+void demux_free(struct demuxer *demuxer)
+{
+ if (!demuxer)
+ return;
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ demux_stop_thread(demuxer);
+ demux_shutdown(in);
+ demux_dealloc(in);
+}
+
+// Start closing the demuxer and eventually freeing the demuxer asynchronously.
+// You must not access the demuxer once this has been started. Once the demuxer
+// is shutdown, the wakeup callback is invoked. Then you need to call
+// demux_free_async_finish() to end the operation (it must not be called from
+// the wakeup callback).
+// This can return NULL. Then the demuxer cannot be free'd asynchronously, and
+// you need to call demux_free() instead.
+struct demux_free_async_state *demux_free_async(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ if (!in->threading)
+ return NULL;
+
+ mp_mutex_lock(&in->lock);
+ in->thread_terminate = true;
+ in->shutdown_async = true;
+ mp_cond_signal(&in->wakeup);
+ mp_mutex_unlock(&in->lock);
+
+ return (struct demux_free_async_state *)demuxer->in; // lies
+}
+
+// As long as state is valid, you can call this to request immediate abort.
+// Roughly behaves as demux_cancel_and_free(), except you still need to wait
+// for the result.
+void demux_free_async_force(struct demux_free_async_state *state)
+{
+ struct demux_internal *in = (struct demux_internal *)state; // reverse lies
+
+ mp_cancel_trigger(in->d_user->cancel);
+}
+
+// Check whether the demuxer is shutdown yet. If not, return false, and you
+// need to call this again in the future (preferably after you were notified by
+// the wakeup callback). If yes, deallocate all state, and return true (in
+// particular, the state ptr becomes invalid, and the wakeup callback will never
+// be called again).
+bool demux_free_async_finish(struct demux_free_async_state *state)
+{
+ struct demux_internal *in = (struct demux_internal *)state; // reverse lies
+
+ mp_mutex_lock(&in->lock);
+ bool busy = in->shutdown_async;
+ mp_mutex_unlock(&in->lock);
+
+ if (busy)
+ return false;
+
+ demux_stop_thread(in->d_user);
+ demux_dealloc(in);
+ return true;
+}
+
+// Like demux_free(), but trigger an abort, which will force the demuxer to
+// terminate immediately. If this wasn't opened with demux_open_url(), there is
+// some chance this will accidentally abort other things via demuxer->cancel.
+void demux_cancel_and_free(struct demuxer *demuxer)
+{
+ if (!demuxer)
+ return;
+ mp_cancel_trigger(demuxer->cancel);
+ demux_free(demuxer);
+}
+
+// Start the demuxer thread, which reads ahead packets on its own.
+void demux_start_thread(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ if (!in->threading) {
+ in->threading = true;
+ if (mp_thread_create(&in->thread, demux_thread, in))
+ in->threading = false;
+ }
+}
+
+void demux_stop_thread(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ if (in->threading) {
+ mp_mutex_lock(&in->lock);
+ in->thread_terminate = true;
+ mp_cond_signal(&in->wakeup);
+ mp_mutex_unlock(&in->lock);
+ mp_thread_join(in->thread);
+ in->threading = false;
+ in->thread_terminate = false;
+ }
+}
+
+// The demuxer thread will call cb(ctx) if there's a new packet, or EOF is reached.
+void demux_set_wakeup_cb(struct demuxer *demuxer, void (*cb)(void *ctx), void *ctx)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ in->wakeup_cb = cb;
+ in->wakeup_cb_ctx = ctx;
+ mp_mutex_unlock(&in->lock);
+}
+
+void demux_start_prefetch(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ mp_mutex_lock(&in->lock);
+ in->reading = true;
+ mp_cond_signal(&in->wakeup);
+ mp_mutex_unlock(&in->lock);
+}
+
+const char *stream_type_name(enum stream_type type)
+{
+ switch (type) {
+ case STREAM_VIDEO: return "video";
+ case STREAM_AUDIO: return "audio";
+ case STREAM_SUB: return "sub";
+ default: return "unknown";
+ }
+}
+
+static struct sh_stream *demuxer_get_cc_track_locked(struct sh_stream *stream)
+{
+ struct sh_stream *sh = stream->ds->cc;
+
+ if (!sh) {
+ sh = demux_alloc_sh_stream(STREAM_SUB);
+ if (!sh)
+ return NULL;
+ sh->codec->codec = "eia_608";
+ sh->default_track = true;
+ sh->hls_bitrate = stream->hls_bitrate;
+ sh->program_id = stream->program_id;
+ stream->ds->cc = sh;
+ demux_add_sh_stream_locked(stream->ds->in, sh);
+ sh->ds->ignore_eof = true;
+ }
+
+ return sh;
+}
+
+void demuxer_feed_caption(struct sh_stream *stream, demux_packet_t *dp)
+{
+ struct demux_internal *in = stream->ds->in;
+
+ mp_mutex_lock(&in->lock);
+ struct sh_stream *sh = demuxer_get_cc_track_locked(stream);
+ if (!sh) {
+ mp_mutex_unlock(&in->lock);
+ talloc_free(dp);
+ return;
+ }
+
+ dp->keyframe = true;
+ dp->pts = MP_ADD_PTS(dp->pts, -in->ts_offset);
+ dp->dts = MP_ADD_PTS(dp->dts, -in->ts_offset);
+ dp->stream = sh->index;
+ add_packet_locked(sh, dp);
+ mp_mutex_unlock(&in->lock);
+}
+
+static void error_on_backward_demuxing(struct demux_internal *in)
+{
+ if (!in->back_demuxing)
+ return;
+ MP_ERR(in, "Disabling backward demuxing.\n");
+ in->back_demuxing = false;
+ clear_reader_state(in, true);
+}
+
+static void perform_backward_seek(struct demux_internal *in)
+{
+ double target = MP_NOPTS_VALUE;
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ if (ds->reader_head && !ds->back_restarting && !ds->back_resuming &&
+ ds->eager)
+ {
+ ds->back_resuming = true;
+ ds->back_resume_pos = ds->reader_head->pos;
+ ds->back_resume_dts = ds->reader_head->dts;
+ }
+
+ target = MP_PTS_MIN(target, ds->back_seek_pos);
+ }
+
+ target = MP_PTS_OR_DEF(target, in->d_thread->start_time);
+
+ MP_VERBOSE(in, "triggering backward seek to get more packets\n");
+ queue_seek(in, target, SEEK_SATAN | SEEK_HR, false);
+ in->reading = true;
+
+ // Don't starve other threads.
+ mp_mutex_unlock(&in->lock);
+ mp_mutex_lock(&in->lock);
+}
+
+// For incremental backward demuxing search work.
+static void check_backward_seek(struct demux_internal *in)
+{
+ in->back_any_need_recheck = false;
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ if (ds->back_need_recheck)
+ find_backward_restart_pos(ds);
+ }
+}
+
+// Search for a packet to resume demuxing from.
+// The implementation of this function is quite awkward, because the packet
+// queue is a singly linked list without back links, while it needs to search
+// backwards.
+// This is the core of backward demuxing.
+static void find_backward_restart_pos(struct demux_stream *ds)
+{
+ struct demux_internal *in = ds->in;
+
+ ds->back_need_recheck = false;
+ if (!ds->back_restarting)
+ return;
+
+ struct demux_packet *first = ds->reader_head;
+ struct demux_packet *last = ds->queue->tail;
+
+ if (first && !first->keyframe)
+ MP_WARN(in, "Queue not starting on keyframe.\n");
+
+ // Packet at back_restart_pos. (Note: we don't actually need it, only the
+ // packet immediately before it. But same effort.)
+ // If this is NULL, look for EOF (resume from very last keyframe).
+ struct demux_packet *back_restart = NULL;
+
+ if (ds->back_restart_next) {
+ // Initial state. Switch to one of the other modi.
+
+ for (struct demux_packet *cur = first; cur; cur = cur->next) {
+ // Restart for next keyframe after reader_head.
+ if (cur != first && cur->keyframe) {
+ ds->back_restart_dts = cur->dts;
+ ds->back_restart_pos = cur->pos;
+ ds->back_restart_eof = false;
+ ds->back_restart_next = false;
+ break;
+ }
+ }
+
+ if (ds->back_restart_next && ds->eof) {
+ // Restart from end if nothing was found.
+ ds->back_restart_eof = true;
+ ds->back_restart_next = false;
+ }
+
+ if (ds->back_restart_next)
+ return;
+ }
+
+ if (ds->back_restart_eof) {
+ // We're trying to find EOF (without discarding packets). Only continue
+ // if we really reach EOF.
+ if (!ds->eof)
+ return;
+ } else if (!first && ds->eof) {
+ // Reached EOF during normal backward demuxing. We probably returned the
+ // last keyframe range to user. Need to resume at an earlier position.
+ // Fall through, hit the no-keyframe case (and possibly the BOF check
+ // if there are no packets at all), and then resume_earlier.
+ } else if (!first) {
+ return; // no packets yet
+ } else {
+ assert(last);
+
+ if ((ds->global_correct_dts && last->dts < ds->back_restart_dts) ||
+ (ds->global_correct_pos && last->pos < ds->back_restart_pos))
+ return; // restart pos not reached yet
+
+ // The target we're searching for is apparently before the start of the
+ // queue.
+ if ((ds->global_correct_dts && first->dts > ds->back_restart_dts) ||
+ (ds->global_correct_pos && first->pos > ds->back_restart_pos))
+ goto resume_earlier; // current position is too late; seek back
+
+
+ for (struct demux_packet *cur = first; cur; cur = cur->next) {
+ if ((ds->global_correct_dts && cur->dts == ds->back_restart_dts) ||
+ (ds->global_correct_pos && cur->pos == ds->back_restart_pos))
+ {
+ back_restart = cur;
+ break;
+ }
+ }
+
+ if (!back_restart) {
+ // The packet should have been in the searched range; maybe dts/pos
+ // determinism assumptions were broken.
+ MP_ERR(in, "Demuxer not cooperating.\n");
+ error_on_backward_demuxing(in);
+ return;
+ }
+ }
+
+ // Find where to restart demuxing. It's usually the last keyframe packet
+ // before restart_pos, but might be up to back_preroll + batch keyframe
+ // packets earlier.
+
+ // (Normally, we'd just iterate backwards, but no back links.)
+ int num_kf = 0;
+ struct demux_packet *pre_1 = NULL; // idiotic "optimization" for total=1
+ for (struct demux_packet *dp = first; dp != back_restart; dp = dp->next) {
+ if (dp->keyframe) {
+ num_kf++;
+ pre_1 = dp;
+ }
+ }
+
+ // Number of renderable keyframes to return to user.
+ // (Excludes preroll, which is decoded by user, but then discarded.)
+ int batch = MPMAX(in->d_user->opts->back_batch[ds->type], 1);
+ // Number of keyframes to return to the user in total.
+ int total = batch + ds->back_preroll;
+
+ assert(total >= 1);
+
+ bool is_bof = ds->queue->is_bof &&
+ (first == ds->queue->head || ds->back_seek_pos < ds->queue->seek_start);
+
+ struct demux_packet *target = NULL; // resume pos
+ // nr. of keyframes, incl. target, excl. restart_pos
+ int got_total = num_kf < total && is_bof ? num_kf : total;
+ int got_preroll = MPMAX(got_total - batch, 0);
+
+ if (got_total == 1) {
+ target = pre_1;
+ } else if (got_total <= num_kf) {
+ int cur_kf = 0;
+ for (struct demux_packet *dp = first; dp != back_restart; dp = dp->next) {
+ if (dp->keyframe) {
+ if (num_kf - cur_kf == got_total) {
+ target = dp;
+ break;
+ }
+ cur_kf++;
+ }
+ }
+ }
+
+ if (!target) {
+ if (is_bof) {
+ MP_VERBOSE(in, "BOF for stream %d\n", ds->index);
+ ds->back_restarting = false;
+ ds->back_range_started = false;
+ ds->back_range_count = -1;
+ ds->back_range_preroll = 0;
+ ds->need_wakeup = true;
+ wakeup_ds(ds);
+ return;
+ }
+ goto resume_earlier;
+ }
+
+ // Skip reader_head from previous keyframe to current one.
+ // Or if preroll is involved, the first preroll packet.
+ while (ds->reader_head != target) {
+ if (!advance_reader_head(ds))
+ MP_ASSERT_UNREACHABLE(); // target must be in list
+ }
+
+ double seek_pts;
+ compute_keyframe_times(target, &seek_pts, NULL);
+ if (seek_pts != MP_NOPTS_VALUE)
+ ds->back_seek_pos = seek_pts;
+
+ // For next backward adjust action.
+ struct demux_packet *restart_pkt = NULL;
+ int kf_pos = 0;
+ for (struct demux_packet *dp = target; dp; dp = dp->next) {
+ if (dp->keyframe) {
+ if (kf_pos == got_preroll) {
+ restart_pkt = dp;
+ break;
+ }
+ kf_pos++;
+ }
+ }
+ assert(restart_pkt);
+ ds->back_restart_dts = restart_pkt->dts;
+ ds->back_restart_pos = restart_pkt->pos;
+
+ ds->back_restarting = false;
+ ds->back_range_started = false;
+ ds->back_range_count = got_total;
+ ds->back_range_preroll = got_preroll;
+ ds->need_wakeup = true;
+ wakeup_ds(ds);
+ return;
+
+resume_earlier:
+ // We want to seek back to get earlier packets. But before we do this, we
+ // must be sure that other streams have initialized their state. The only
+ // time when this state is not initialized is right after the seek that
+ // started backward demuxing (not any subsequent backstep seek). If this
+ // initialization is omitted, the stream would try to start demuxing from
+ // the "current" position. If another stream backstepped before that, the
+ // other stream will miss the original seek target, and start playback from
+ // a position that is too early.
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds2 = in->streams[n]->ds;
+ if (ds2 == ds || !ds2->eager)
+ continue;
+
+ if (ds2->back_restarting && ds2->back_restart_next) {
+ MP_VERBOSE(in, "delaying stream %d for %d\n", ds->index, ds2->index);
+ return;
+ }
+ }
+
+ if (ds->back_seek_pos != MP_NOPTS_VALUE) {
+ struct demux_packet *t =
+ find_seek_target(ds->queue, ds->back_seek_pos - 0.001, 0);
+ if (t && t != ds->reader_head) {
+ double pts;
+ compute_keyframe_times(t, &pts, NULL);
+ ds->back_seek_pos = MP_PTS_MIN(ds->back_seek_pos, pts);
+ ds_clear_reader_state(ds, false);
+ ds->reader_head = t;
+ ds->back_need_recheck = true;
+ in->back_any_need_recheck = true;
+ mp_cond_signal(&in->wakeup);
+ } else {
+ ds->back_seek_pos -= in->d_user->opts->back_seek_size;
+ in->need_back_seek = true;
+ }
+ }
+}
+
+// Process that one or multiple packets were added.
+static void back_demux_see_packets(struct demux_stream *ds)
+{
+ struct demux_internal *in = ds->in;
+
+ if (!ds->selected || !in->back_demuxing || !ds->eager)
+ return;
+
+ assert(!(ds->back_resuming && ds->back_restarting));
+
+ if (!ds->global_correct_dts && !ds->global_correct_pos) {
+ MP_ERR(in, "Can't demux backward due to demuxer problems.\n");
+ error_on_backward_demuxing(in);
+ return;
+ }
+
+ while (ds->back_resuming && ds->reader_head) {
+ struct demux_packet *head = ds->reader_head;
+ if ((ds->global_correct_dts && head->dts == ds->back_resume_dts) ||
+ (ds->global_correct_pos && head->pos == ds->back_resume_pos))
+ {
+ ds->back_resuming = false;
+ ds->need_wakeup = true;
+ wakeup_ds(ds); // probably
+ break;
+ }
+ advance_reader_head(ds);
+ }
+
+ if (ds->back_restarting)
+ find_backward_restart_pos(ds);
+}
+
+// Add the keyframe to the end of the index. Not all packets are actually added.
+static void add_index_entry(struct demux_queue *queue, struct demux_packet *dp,
+ double pts)
+{
+ struct demux_internal *in = queue->ds->in;
+
+ assert(dp->keyframe && pts != MP_NOPTS_VALUE);
+
+ if (queue->num_index > 0) {
+ struct index_entry *last = &QUEUE_INDEX_ENTRY(queue, queue->num_index - 1);
+ if (pts - last->pts < INDEX_STEP_SIZE)
+ return;
+ }
+
+ if (queue->num_index == queue->index_size) {
+ // Needs to honor power-of-2 requirement.
+ size_t new_size = MPMAX(128, queue->index_size * 2);
+ assert(!(new_size & (new_size - 1)));
+ MP_DBG(in, "stream %d: resize index to %zu\n", queue->ds->index,
+ new_size);
+ // Note: we could tolerate allocation failure, and just discard the
+ // entire index (and prevent the index from being recreated).
+ MP_RESIZE_ARRAY(NULL, queue->index, new_size);
+ size_t highest_index = queue->index0 + queue->num_index;
+ for (size_t n = queue->index_size; n < highest_index; n++)
+ queue->index[n] = queue->index[n - queue->index_size];
+ in->total_bytes +=
+ (new_size - queue->index_size) * sizeof(queue->index[0]);
+ queue->index_size = new_size;
+ }
+
+ assert(queue->num_index < queue->index_size);
+
+ queue->num_index += 1;
+
+ QUEUE_INDEX_ENTRY(queue, queue->num_index - 1) = (struct index_entry){
+ .pts = pts,
+ .pkt = dp,
+ };
+}
+
+// Check whether the next range in the list is, and if it appears to overlap,
+// try joining it into a single range.
+static void attempt_range_joining(struct demux_internal *in)
+{
+ struct demux_cached_range *current = in->current_range;
+ struct demux_cached_range *next = NULL;
+ double next_dist = INFINITY;
+
+ assert(current && in->num_ranges > 0);
+ assert(current == in->ranges[in->num_ranges - 1]);
+
+ for (int n = 0; n < in->num_ranges - 1; n++) {
+ struct demux_cached_range *range = in->ranges[n];
+
+ if (current->seek_start <= range->seek_start) {
+ // This uses ">" to get some non-0 overlap.
+ double dist = current->seek_end - range->seek_start;
+ if (dist > 0 && dist < next_dist) {
+ next = range;
+ next_dist = dist;
+ }
+ }
+ }
+
+ if (!next)
+ return;
+
+ MP_VERBOSE(in, "going to join ranges %f-%f + %f-%f\n",
+ current->seek_start, current->seek_end,
+ next->seek_start, next->seek_end);
+
+ // Try to find a join point, where packets obviously overlap. (It would be
+ // better and faster to do this incrementally, but probably too complex.)
+ // The current range can overlap arbitrarily with the next one, not only by
+ // the seek overlap, but for arbitrary packet readahead as well.
+ // We also drop the overlapping packets (if joining fails, we discard the
+ // entire next range anyway, so this does no harm).
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ struct demux_queue *q1 = current->streams[n];
+ struct demux_queue *q2 = next->streams[n];
+
+ if (!ds->global_correct_pos && !ds->global_correct_dts) {
+ MP_WARN(in, "stream %d: ranges unjoinable\n", n);
+ goto failed;
+ }
+
+ struct demux_packet *end = q1->tail;
+ bool join_point_found = !end; // no packets yet -> joining will work
+ if (end) {
+ while (q2->head) {
+ struct demux_packet *dp = q2->head;
+
+ // Some weird corner-case. We'd have to search the equivalent
+ // packet in q1 to update it correctly. Better just give up.
+ if (dp == q2->keyframe_latest) {
+ MP_VERBOSE(in, "stream %d: not enough keyframes for join\n", n);
+ goto failed;
+ }
+
+ if ((ds->global_correct_dts && dp->dts == end->dts) ||
+ (ds->global_correct_pos && dp->pos == end->pos))
+ {
+ // Do some additional checks as a (imperfect) sanity check
+ // in case pos/dts are not "correct" across the ranges (we
+ // never actually check that).
+ if (dp->dts != end->dts || dp->pos != end->pos ||
+ dp->pts != end->pts)
+ {
+ MP_WARN(in,
+ "stream %d: non-repeatable demuxer behavior\n", n);
+ goto failed;
+ }
+
+ remove_head_packet(q2);
+ join_point_found = true;
+ break;
+ }
+
+ // This happens if the next range misses the end packet. For
+ // normal streams (ds->eager==true), this is a failure to find
+ // an overlap. For subtitles, this can mean the current_range
+ // has a subtitle somewhere before the end of its range, and
+ // next has another subtitle somewhere after the start of its
+ // range.
+ if ((ds->global_correct_dts && dp->dts > end->dts) ||
+ (ds->global_correct_pos && dp->pos > end->pos))
+ break;
+
+ remove_head_packet(q2);
+ }
+ }
+
+ // For enabled non-sparse streams, always require an overlap packet.
+ if (ds->eager && !join_point_found) {
+ MP_WARN(in, "stream %d: no join point found\n", n);
+ goto failed;
+ }
+ }
+
+ // Actually join the ranges. Now that we think it will work, mutate the
+ // data associated with the current range.
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_queue *q1 = current->streams[n];
+ struct demux_queue *q2 = next->streams[n];
+
+ struct demux_stream *ds = in->streams[n]->ds;
+ assert(ds->queue == q1);
+
+ // First new packet that is appended to the current range.
+ struct demux_packet *join_point = q2->head;
+
+ if (q2->head) {
+ if (q1->head) {
+ q1->tail->next = q2->head;
+ } else {
+ q1->head = q2->head;
+ }
+ q1->tail = q2->tail;
+ }
+
+ q1->seek_end = q2->seek_end;
+ q1->correct_dts &= q2->correct_dts;
+ q1->correct_pos &= q2->correct_pos;
+ q1->last_pos = q2->last_pos;
+ q1->last_dts = q2->last_dts;
+ q1->last_ts = q2->last_ts;
+ q1->keyframe_latest = q2->keyframe_latest;
+ q1->is_eof = q2->is_eof;
+
+ q1->last_pos_fixup = -1;
+
+ q2->head = q2->tail = NULL;
+ q2->keyframe_first = NULL;
+ q2->keyframe_latest = NULL;
+
+ if (ds->selected && !ds->reader_head)
+ ds->reader_head = join_point;
+ ds->skip_to_keyframe = false;
+
+ // Make the cum_pos values in all q2 packets continuous.
+ for (struct demux_packet *dp = join_point; dp; dp = dp->next) {
+ uint64_t next_pos = dp->next ? dp->next->cum_pos : q2->tail_cum_pos;
+ uint64_t size = next_pos - dp->cum_pos;
+ dp->cum_pos = q1->tail_cum_pos;
+ q1->tail_cum_pos += size;
+ }
+
+ // And update the index with packets from q2.
+ for (size_t i = 0; i < q2->num_index; i++) {
+ struct index_entry *e = &QUEUE_INDEX_ENTRY(q2, i);
+ add_index_entry(q1, e->pkt, e->pts);
+ }
+ free_index(q2);
+
+ // For moving demuxer position.
+ ds->refreshing = ds->selected;
+ }
+
+ for (int n = 0; n < next->num_metadata; n++) {
+ MP_TARRAY_APPEND(current, current->metadata, current->num_metadata,
+ next->metadata[n]);
+ }
+ next->num_metadata = 0;
+
+ update_seek_ranges(current);
+
+ // Move demuxing position to after the current range.
+ in->seeking = true;
+ in->seek_flags = SEEK_HR;
+ in->seek_pts = next->seek_end - 1.0;
+
+ MP_VERBOSE(in, "ranges joined!\n");
+
+ for (int n = 0; n < in->num_streams; n++)
+ back_demux_see_packets(in->streams[n]->ds);
+
+failed:
+ clear_cached_range(in, next);
+ free_empty_cached_ranges(in);
+}
+
+// Compute the assumed first and last frame timestamp for keyframe range
+// starting at pkt. To get valid results, pkt->keyframe must be true, otherwise
+// nonsense will be returned.
+// Always sets *out_kf_min and *out_kf_max without reading them. Both are set
+// to NOPTS if there are no timestamps at all in the stream. *kf_max will not
+// be set to the actual end time of the decoded output, just the last frame
+// (audio will typically end up with kf_min==kf_max).
+// Either of out_kf_min and out_kf_max can be NULL, which discards the result.
+// Return the next keyframe packet after pkt, or NULL if there's none.
+static struct demux_packet *compute_keyframe_times(struct demux_packet *pkt,
+ double *out_kf_min,
+ double *out_kf_max)
+{
+ struct demux_packet *start = pkt;
+ double min = MP_NOPTS_VALUE;
+ double max = MP_NOPTS_VALUE;
+
+ while (pkt) {
+ if (pkt->keyframe && pkt != start)
+ break;
+
+ double ts = MP_PTS_OR_DEF(pkt->pts, pkt->dts);
+ if (pkt->segmented && ((pkt->start != MP_NOPTS_VALUE && ts < pkt->start) ||
+ (pkt->end != MP_NOPTS_VALUE && ts > pkt->end)))
+ ts = MP_NOPTS_VALUE;
+
+ min = MP_PTS_MIN(min, ts);
+ max = MP_PTS_MAX(max, ts);
+
+ pkt = pkt->next;
+ }
+
+ if (out_kf_min)
+ *out_kf_min = min;
+ if (out_kf_max)
+ *out_kf_max = max;
+ return pkt;
+}
+
+// Determine seekable range when a packet is added. If dp==NULL, treat it as
+// EOF (i.e. closes the current block).
+// This has to deal with a number of corner cases, such as demuxers potentially
+// starting output at non-keyframes.
+// Can join seek ranges, which messes with in->current_range and all.
+static void adjust_seek_range_on_packet(struct demux_stream *ds,
+ struct demux_packet *dp)
+{
+ struct demux_queue *queue = ds->queue;
+
+ if (!ds->in->seekable_cache)
+ return;
+
+ bool new_eof = !dp;
+ bool update_ranges = queue->is_eof != new_eof;
+ queue->is_eof = new_eof;
+
+ if (!dp || dp->keyframe) {
+ if (queue->keyframe_latest) {
+ double kf_min, kf_max;
+ compute_keyframe_times(queue->keyframe_latest, &kf_min, &kf_max);
+
+ if (kf_min != MP_NOPTS_VALUE) {
+ add_index_entry(queue, queue->keyframe_latest, kf_min);
+
+ // Initialize the queue's start if it's unset.
+ if (queue->seek_start == MP_NOPTS_VALUE) {
+ update_ranges = true;
+ queue->seek_start = kf_min + ds->sh->seek_preroll;
+ }
+ }
+
+ if (kf_max != MP_NOPTS_VALUE &&
+ (queue->seek_end == MP_NOPTS_VALUE || kf_max > queue->seek_end))
+ {
+ // If the queue was past the current range's end even before
+ // this update, it means _other_ streams are not there yet,
+ // and the seek range doesn't need to be updated. This means
+ // if the _old_ queue->seek_end was already after the range end,
+ // then the new seek_end won't extend the range either.
+ if (queue->range->seek_end == MP_NOPTS_VALUE ||
+ queue->seek_end <= queue->range->seek_end)
+ {
+ update_ranges = true;
+ }
+
+ queue->seek_end = kf_max;
+ }
+ }
+
+ queue->keyframe_latest = dp;
+ }
+
+ // Adding a sparse packet never changes the seek range.
+ if (update_ranges && ds->eager) {
+ update_seek_ranges(queue->range);
+ attempt_range_joining(ds->in);
+ }
+}
+
+static struct mp_recorder *recorder_create(struct demux_internal *in,
+ const char *dst)
+{
+ struct sh_stream **streams = NULL;
+ int num_streams = 0;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct sh_stream *stream = in->streams[n];
+ if (stream->ds->selected)
+ MP_TARRAY_APPEND(NULL, streams, num_streams, stream);
+ }
+
+ struct demuxer *demuxer = in->d_thread;
+ struct demux_attachment **attachments = talloc_array(NULL, struct demux_attachment*, demuxer->num_attachments);
+ for (int n = 0; n < demuxer->num_attachments; n++) {
+ attachments[n] = &demuxer->attachments[n];
+ }
+
+ struct mp_recorder *res = mp_recorder_create(in->d_thread->global, dst,
+ streams, num_streams,
+ attachments, demuxer->num_attachments);
+ talloc_free(streams);
+ talloc_free(attachments);
+ return res;
+}
+
+static void write_dump_packet(struct demux_internal *in, struct demux_packet *dp)
+{
+ assert(in->dumper);
+ assert(in->dumper_status == CONTROL_TRUE);
+
+ struct mp_recorder_sink *sink =
+ mp_recorder_get_sink(in->dumper, in->streams[dp->stream]);
+ if (sink) {
+ mp_recorder_feed_packet(sink, dp);
+ } else {
+ MP_ERR(in, "New stream appeared; stopping recording.\n");
+ in->dumper_status = CONTROL_ERROR;
+ }
+}
+
+static void record_packet(struct demux_internal *in, struct demux_packet *dp)
+{
+ // (should preferably be outside of the lock)
+ if (in->enable_recording && !in->recorder &&
+ in->d_user->opts->record_file && in->d_user->opts->record_file[0])
+ {
+ // Later failures shouldn't make it retry and overwrite the previously
+ // recorded file.
+ in->enable_recording = false;
+
+ in->recorder = recorder_create(in, in->d_user->opts->record_file);
+ if (!in->recorder)
+ MP_ERR(in, "Disabling recording.\n");
+ }
+
+ if (in->recorder) {
+ struct mp_recorder_sink *sink =
+ mp_recorder_get_sink(in->recorder, in->streams[dp->stream]);
+ if (sink) {
+ mp_recorder_feed_packet(sink, dp);
+ } else {
+ MP_ERR(in, "New stream appeared; stopping recording.\n");
+ mp_recorder_destroy(in->recorder);
+ in->recorder = NULL;
+ }
+ }
+
+ if (in->dumper_status == CONTROL_OK)
+ write_dump_packet(in, dp);
+}
+
+static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp)
+{
+ struct demux_stream *ds = stream ? stream->ds : NULL;
+ assert(ds && ds->in);
+ if (!dp->len || demux_cancel_test(ds->in->d_thread)) {
+ talloc_free(dp);
+ return;
+ }
+
+ assert(dp->stream == stream->index);
+ assert(!dp->next);
+
+ struct demux_internal *in = ds->in;
+
+ in->after_seek = false;
+ in->after_seek_to_start = false;
+
+ double ts = dp->dts == MP_NOPTS_VALUE ? dp->pts : dp->dts;
+ if (dp->segmented)
+ ts = MP_PTS_MIN(ts, dp->end);
+
+ if (ts != MP_NOPTS_VALUE)
+ in->demux_ts = ts;
+
+ struct demux_queue *queue = ds->queue;
+
+ bool drop = !ds->selected || in->seeking || ds->sh->attached_picture;
+
+ if (!drop) {
+ // If libavformat splits packets, some packets will have pos unset, so
+ // make up one based on the first packet => makes refresh seeks work.
+ if ((dp->pos < 0 || dp->pos == queue->last_pos_fixup) &&
+ !dp->keyframe && queue->last_pos_fixup >= 0)
+ dp->pos = queue->last_pos_fixup + 1;
+ queue->last_pos_fixup = dp->pos;
+ }
+
+ if (!drop && ds->refreshing) {
+ // Resume reading once the old position was reached (i.e. we start
+ // returning packets where we left off before the refresh).
+ // If it's the same position, drop, but continue normally next time.
+ if (queue->correct_dts) {
+ ds->refreshing = dp->dts < queue->last_dts;
+ } else if (queue->correct_pos) {
+ ds->refreshing = dp->pos < queue->last_pos;
+ } else {
+ ds->refreshing = false; // should not happen
+ MP_WARN(in, "stream %d: demux refreshing failed\n", ds->index);
+ }
+ drop = true;
+ }
+
+ if (drop) {
+ talloc_free(dp);
+ return;
+ }
+
+ record_packet(in, dp);
+
+ if (in->cache && in->d_user->opts->disk_cache) {
+ int64_t pos = demux_cache_write(in->cache, dp);
+ if (pos >= 0) {
+ demux_packet_unref_contents(dp);
+ dp->is_cached = true;
+ dp->cached_data.pos = pos;
+ }
+ }
+
+ queue->correct_pos &= dp->pos >= 0 && dp->pos > queue->last_pos;
+ queue->correct_dts &= dp->dts != MP_NOPTS_VALUE && dp->dts > queue->last_dts;
+ queue->last_pos = dp->pos;
+ queue->last_dts = dp->dts;
+ ds->global_correct_pos &= queue->correct_pos;
+ ds->global_correct_dts &= queue->correct_dts;
+
+ // (keep in mind that even if the reader went out of data, the queue is not
+ // necessarily empty due to the backbuffer)
+ if (!ds->reader_head && (!ds->skip_to_keyframe || dp->keyframe)) {
+ ds->reader_head = dp;
+ ds->skip_to_keyframe = false;
+ }
+
+ size_t bytes = demux_packet_estimate_total_size(dp);
+ in->total_bytes += bytes;
+ dp->cum_pos = queue->tail_cum_pos;
+ queue->tail_cum_pos += bytes;
+
+ if (queue->tail) {
+ // next packet in stream
+ queue->tail->next = dp;
+ queue->tail = dp;
+ } else {
+ // first packet in stream
+ queue->head = queue->tail = dp;
+ }
+
+ if (!ds->ignore_eof) {
+ // obviously not true anymore
+ ds->eof = false;
+ in->eof = false;
+ }
+
+ // For video, PTS determination is not trivial, but for other media types
+ // distinguishing PTS and DTS is not useful.
+ if (stream->type != STREAM_VIDEO && dp->pts == MP_NOPTS_VALUE)
+ dp->pts = dp->dts;
+
+ if (ts != MP_NOPTS_VALUE && (ts > queue->last_ts || ts + 10 < queue->last_ts))
+ queue->last_ts = ts;
+ if (ds->base_ts == MP_NOPTS_VALUE)
+ ds->base_ts = queue->last_ts;
+
+ const char *num_pkts = queue->head == queue->tail ? "1" : ">1";
+ uint64_t fw_bytes = get_forward_buffered_bytes(ds);
+ MP_TRACE(in, "append packet to %s: size=%zu pts=%f dts=%f pos=%"PRIi64" "
+ "[num=%s size=%zd]\n", stream_type_name(stream->type),
+ dp->len, dp->pts, dp->dts, dp->pos, num_pkts, (size_t)fw_bytes);
+
+ adjust_seek_range_on_packet(ds, dp);
+
+ // May need to reduce backward cache.
+ prune_old_packets(in);
+
+ // Possibly update duration based on highest TS demuxed (but ignore subs).
+ if (stream->type != STREAM_SUB) {
+ if (dp->segmented)
+ ts = MP_PTS_MIN(ts, dp->end);
+ if (ts > in->highest_av_pts) {
+ in->highest_av_pts = ts;
+ double duration = in->highest_av_pts - in->d_thread->start_time;
+ if (duration > in->d_thread->duration) {
+ in->d_thread->duration = duration;
+ // (Don't wakeup user thread, would be too noisy.)
+ in->events |= DEMUX_EVENT_DURATION;
+ in->duration = duration;
+ }
+ }
+ }
+
+ // Don't process the packet further if it's skipped by the previous seek
+ // (see reader_head check/assignment above).
+ if (!ds->reader_head)
+ return;
+
+ back_demux_see_packets(ds);
+
+ wakeup_ds(ds);
+}
+
+static void mark_stream_eof(struct demux_stream *ds)
+{
+ if (!ds->eof) {
+ ds->eof = true;
+ adjust_seek_range_on_packet(ds, NULL);
+ back_demux_see_packets(ds);
+ wakeup_ds(ds);
+ }
+}
+
+static bool lazy_stream_needs_wait(struct demux_stream *ds)
+{
+ struct demux_internal *in = ds->in;
+ // Attempt to read until force_read_until was reached, or reading has
+ // stopped for some reason (true EOF, queue overflow).
+ return !ds->eager && !in->back_demuxing &&
+ !in->eof && ds->force_read_until != MP_NOPTS_VALUE &&
+ (in->demux_ts == MP_NOPTS_VALUE ||
+ in->demux_ts <= ds->force_read_until);
+}
+
+// Returns true if there was "progress" (lock was released temporarily).
+static bool read_packet(struct demux_internal *in)
+{
+ bool was_reading = in->reading;
+ in->reading = false;
+
+ if (!was_reading || in->blocked || demux_cancel_test(in->d_thread))
+ return false;
+
+ // Check if we need to read a new packet. We do this if all queues are below
+ // the minimum, or if a stream explicitly needs new packets. Also includes
+ // safe-guards against packet queue overflow.
+ bool read_more = false, prefetch_more = false, refresh_more = false;
+ uint64_t total_fw_bytes = 0;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ if (ds->eager) {
+ read_more |= !ds->reader_head;
+ if (in->back_demuxing)
+ read_more |= ds->back_restarting || ds->back_resuming;
+ } else {
+ if (lazy_stream_needs_wait(ds)) {
+ read_more = true;
+ } else {
+ mark_stream_eof(ds); // let playback continue
+ }
+ }
+ refresh_more |= ds->refreshing;
+ if (ds->eager && ds->queue->last_ts != MP_NOPTS_VALUE &&
+ in->min_secs > 0 && ds->base_ts != MP_NOPTS_VALUE &&
+ ds->queue->last_ts >= ds->base_ts &&
+ !in->back_demuxing)
+ {
+ if (ds->queue->last_ts - ds->base_ts <= in->hyst_secs)
+ in->hyst_active = false;
+ if (!in->hyst_active)
+ prefetch_more |= ds->queue->last_ts - ds->base_ts < in->min_secs;
+ }
+ total_fw_bytes += get_forward_buffered_bytes(ds);
+ }
+
+ MP_TRACE(in, "bytes=%zd, read_more=%d prefetch_more=%d, refresh_more=%d\n",
+ (size_t)total_fw_bytes, read_more, prefetch_more, refresh_more);
+ if (total_fw_bytes >= in->max_bytes) {
+ // if we hit the limit just by prefetching, simply stop prefetching
+ if (!read_more) {
+ in->hyst_active = !!in->hyst_secs;
+ return false;
+ }
+ if (!in->warned_queue_overflow) {
+ in->warned_queue_overflow = true;
+ MP_WARN(in, "Too many packets in the demuxer packet queues:\n");
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ if (ds->selected) {
+ size_t num_pkts = 0;
+ for (struct demux_packet *dp = ds->reader_head;
+ dp; dp = dp->next)
+ num_pkts++;
+ uint64_t fw_bytes = get_forward_buffered_bytes(ds);
+ MP_WARN(in, " %s/%d: %zd packets, %zd bytes%s%s\n",
+ stream_type_name(ds->type), n,
+ num_pkts, (size_t)fw_bytes,
+ ds->eager ? "" : " (lazy)",
+ ds->refreshing ? " (refreshing)" : "");
+ }
+ }
+ if (in->back_demuxing)
+ MP_ERR(in, "Backward playback is likely stuck/broken now.\n");
+ }
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ if (!ds->reader_head)
+ mark_stream_eof(ds);
+ }
+ return false;
+ }
+
+ if (!read_more && !prefetch_more && !refresh_more) {
+ in->hyst_active = !!in->hyst_secs;
+ return false;
+ }
+
+ if (in->after_seek_to_start) {
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ in->current_range->streams[n]->is_bof =
+ ds->selected && !ds->refreshing;
+ }
+ }
+
+ // Actually read a packet. Drop the lock while doing so, because waiting
+ // for disk or network I/O can take time.
+ in->reading = true;
+ in->after_seek = false;
+ in->after_seek_to_start = false;
+ mp_mutex_unlock(&in->lock);
+
+ struct demuxer *demux = in->d_thread;
+ struct demux_packet *pkt = NULL;
+
+ bool eof = true;
+ if (demux->desc->read_packet && !demux_cancel_test(demux))
+ eof = !demux->desc->read_packet(demux, &pkt);
+
+ mp_mutex_lock(&in->lock);
+ update_cache(in);
+
+ if (pkt) {
+ assert(pkt->stream >= 0 && pkt->stream < in->num_streams);
+ add_packet_locked(in->streams[pkt->stream], pkt);
+ }
+
+ if (!in->seeking) {
+ if (eof) {
+ for (int n = 0; n < in->num_streams; n++)
+ mark_stream_eof(in->streams[n]->ds);
+ // If we had EOF previously, then don't wakeup (avoids wakeup loop)
+ if (!in->eof) {
+ if (in->wakeup_cb)
+ in->wakeup_cb(in->wakeup_cb_ctx);
+ mp_cond_signal(&in->wakeup);
+ MP_VERBOSE(in, "EOF reached.\n");
+ }
+ }
+ in->eof = eof;
+ in->reading = !eof;
+ }
+ return true;
+}
+
+static void prune_old_packets(struct demux_internal *in)
+{
+ assert(in->current_range == in->ranges[in->num_ranges - 1]);
+
+ // It's not clear what the ideal way to prune old packets is. For now, we
+ // prune the oldest packet runs, as long as the total cache amount is too
+ // big.
+ while (1) {
+ uint64_t fw_bytes = 0;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ fw_bytes += get_forward_buffered_bytes(ds);
+ }
+ uint64_t max_avail = in->max_bytes_bw;
+ // Backward cache (if enabled at all) can use unused forward cache.
+ // Still leave 1 byte free, so the read_packet logic doesn't get stuck.
+ if (max_avail && in->max_bytes > (fw_bytes + 1) && in->d_user->opts->donate_fw)
+ max_avail += in->max_bytes - (fw_bytes + 1);
+ if (in->total_bytes - fw_bytes <= max_avail)
+ break;
+
+ // (Start from least recently used range.)
+ struct demux_cached_range *range = in->ranges[0];
+ double earliest_ts = MP_NOPTS_VALUE;
+ struct demux_stream *earliest_stream = NULL;
+
+ for (int n = 0; n < range->num_streams; n++) {
+ struct demux_queue *queue = range->streams[n];
+ struct demux_stream *ds = queue->ds;
+
+ if (queue->head && queue->head != ds->reader_head) {
+ struct demux_packet *dp = queue->head;
+ double ts = queue->seek_start;
+ // If the ts is NOPTS, the queue has no retainable packets, so
+ // delete them all. This code is not run when there's enough
+ // free space, so normally the queue gets the chance to build up.
+ bool prune_always =
+ !in->seekable_cache || ts == MP_NOPTS_VALUE || !dp->keyframe;
+ if (prune_always || !earliest_stream || ts < earliest_ts) {
+ earliest_ts = ts;
+ earliest_stream = ds;
+ if (prune_always)
+ break;
+ }
+ }
+ }
+
+ // In some cases (like when the seek index became huge), there aren't
+ // any backwards packets, even if the total cache size is exceeded.
+ if (!earliest_stream)
+ break;
+
+ struct demux_stream *ds = earliest_stream;
+ struct demux_queue *queue = range->streams[ds->index];
+
+ bool non_kf_prune = queue->head && !queue->head->keyframe;
+ bool kf_was_pruned = false;
+
+ while (queue->head && queue->head != ds->reader_head) {
+ if (queue->head->keyframe) {
+ // If the cache is seekable, only delete until up the next
+ // keyframe. This is not always efficient, but ensures we
+ // prune all streams fairly.
+ // Also, if the first packet was _not_ a keyframe, we want it
+ // to remove all preceding non-keyframe packets first, before
+ // re-evaluating what to prune next.
+ if ((kf_was_pruned || non_kf_prune) && in->seekable_cache)
+ break;
+ kf_was_pruned = true;
+ }
+
+ remove_head_packet(queue);
+ }
+
+ // Need to update the seekable time range.
+ if (kf_was_pruned) {
+ assert(!queue->keyframe_first); // it was just deleted, supposedly
+
+ queue->keyframe_first = queue->head;
+ // (May happen if reader_head stopped pruning the range, and there's
+ // no next range.)
+ while (queue->keyframe_first && !queue->keyframe_first->keyframe)
+ queue->keyframe_first = queue->keyframe_first->next;
+
+ if (queue->seek_start != MP_NOPTS_VALUE)
+ queue->last_pruned = queue->seek_start;
+
+ double kf_min;
+ compute_keyframe_times(queue->keyframe_first, &kf_min, NULL);
+
+ bool update_range = true;
+
+ queue->seek_start = kf_min;
+
+ if (queue->seek_start != MP_NOPTS_VALUE) {
+ queue->seek_start += ds->sh->seek_preroll;
+
+ // Don't need to update if the new start is still before the
+ // range's start (or if the range was undefined anyway).
+ if (range->seek_start == MP_NOPTS_VALUE ||
+ queue->seek_start <= range->seek_start)
+ {
+ update_range = false;
+ }
+ }
+
+ if (update_range)
+ update_seek_ranges(range);
+ }
+
+ if (range != in->current_range && range->seek_start == MP_NOPTS_VALUE)
+ free_empty_cached_ranges(in);
+ }
+}
+
+static void execute_trackswitch(struct demux_internal *in)
+{
+ in->tracks_switched = false;
+
+ mp_mutex_unlock(&in->lock);
+
+ if (in->d_thread->desc->switched_tracks)
+ in->d_thread->desc->switched_tracks(in->d_thread);
+
+ mp_mutex_lock(&in->lock);
+}
+
+static void execute_seek(struct demux_internal *in)
+{
+ int flags = in->seek_flags;
+ double pts = in->seek_pts;
+ in->eof = false;
+ in->seeking = false;
+ in->seeking_in_progress = pts;
+ in->demux_ts = MP_NOPTS_VALUE;
+ in->low_level_seeks += 1;
+ in->after_seek = true;
+ in->after_seek_to_start =
+ !(flags & (SEEK_FORWARD | SEEK_FACTOR)) &&
+ pts <= in->d_thread->start_time;
+
+ for (int n = 0; n < in->num_streams; n++)
+ in->streams[n]->ds->queue->last_pos_fixup = -1;
+
+ if (in->recorder)
+ mp_recorder_mark_discontinuity(in->recorder);
+
+ mp_mutex_unlock(&in->lock);
+
+ MP_VERBOSE(in, "execute seek (to %f flags %d)\n", pts, flags);
+
+ if (in->d_thread->desc->seek)
+ in->d_thread->desc->seek(in->d_thread, pts, flags);
+
+ MP_VERBOSE(in, "seek done\n");
+
+ mp_mutex_lock(&in->lock);
+
+ in->seeking_in_progress = MP_NOPTS_VALUE;
+}
+
+static void update_opts(struct demuxer *demuxer)
+{
+ struct demux_opts *opts = demuxer->opts;
+ struct demux_internal *in = demuxer->in;
+
+ in->min_secs = opts->min_secs;
+ in->hyst_secs = opts->hyst_secs;
+ in->max_bytes = opts->max_bytes;
+ in->max_bytes_bw = opts->max_bytes_bw;
+
+ int seekable = opts->seekable_cache;
+ bool is_streaming = in->d_thread->is_streaming;
+ bool use_cache = is_streaming;
+ if (opts->enable_cache >= 0)
+ use_cache = opts->enable_cache == 1;
+
+ if (use_cache) {
+ in->min_secs = MPMAX(in->min_secs, opts->min_secs_cache);
+ if (seekable < 0)
+ seekable = 1;
+ }
+ in->seekable_cache = seekable == 1;
+ in->using_network_cache_opts = is_streaming && use_cache;
+
+ if (!in->seekable_cache)
+ in->max_bytes_bw = 0;
+
+ if (!in->can_cache) {
+ in->seekable_cache = false;
+ in->min_secs = 0;
+ in->max_bytes = 1;
+ in->max_bytes_bw = 0;
+ in->using_network_cache_opts = false;
+ }
+
+ if (in->seekable_cache && opts->disk_cache && !in->cache) {
+ in->cache = demux_cache_create(in->global, in->log);
+ if (!in->cache)
+ MP_ERR(in, "Failed to create file cache.\n");
+ }
+
+ // The filename option really decides whether recording should be active.
+ // So if the filename changes, act upon it.
+ char *old = in->record_filename ? in->record_filename : "";
+ char *new = opts->record_file ? opts->record_file : "";
+ if (strcmp(old, new) != 0) {
+ if (in->recorder) {
+ MP_WARN(in, "Stopping recording.\n");
+ mp_recorder_destroy(in->recorder);
+ in->recorder = NULL;
+ }
+ talloc_free(in->record_filename);
+ in->record_filename = talloc_strdup(in, opts->record_file);
+ // Note: actual recording only starts once packets are read. It may be
+ // important to delay creating in->recorder to that point, because the
+ // demuxer might detect more streams until finding the first packet.
+ in->enable_recording = in->can_record;
+ }
+
+ // In case the cache was reduced in size.
+ prune_old_packets(in);
+
+ // In case the seekable cache was disabled.
+ free_empty_cached_ranges(in);
+}
+
+// Make demuxing progress. Return whether progress was made.
+static bool thread_work(struct demux_internal *in)
+{
+ if (m_config_cache_update(in->d_user->opts_cache))
+ update_opts(in->d_user);
+ if (in->tracks_switched) {
+ execute_trackswitch(in);
+ return true;
+ }
+ if (in->need_back_seek) {
+ perform_backward_seek(in);
+ return true;
+ }
+ if (in->back_any_need_recheck) {
+ check_backward_seek(in);
+ return true;
+ }
+ if (in->seeking) {
+ execute_seek(in);
+ return true;
+ }
+ if (read_packet(in))
+ return true; // read_packet unlocked, so recheck conditions
+ if (mp_time_ns() >= in->next_cache_update) {
+ update_cache(in);
+ return true;
+ }
+ return false;
+}
+
+static MP_THREAD_VOID demux_thread(void *pctx)
+{
+ struct demux_internal *in = pctx;
+ mp_thread_set_name("demux");
+ mp_mutex_lock(&in->lock);
+
+ stats_register_thread_cputime(in->stats, "thread");
+
+ while (!in->thread_terminate) {
+ if (thread_work(in))
+ continue;
+ mp_cond_signal(&in->wakeup);
+ mp_cond_timedwait_until(&in->wakeup, &in->lock, in->next_cache_update);
+ }
+
+ if (in->shutdown_async) {
+ mp_mutex_unlock(&in->lock);
+ demux_shutdown(in);
+ mp_mutex_lock(&in->lock);
+ in->shutdown_async = false;
+ if (in->wakeup_cb)
+ in->wakeup_cb(in->wakeup_cb_ctx);
+ }
+
+ stats_unregister_thread(in->stats, "thread");
+
+ mp_mutex_unlock(&in->lock);
+ MP_THREAD_RETURN();
+}
+
+// Low-level part of dequeueing a packet.
+static struct demux_packet *advance_reader_head(struct demux_stream *ds)
+{
+ struct demux_packet *pkt = ds->reader_head;
+ if (!pkt)
+ return NULL;
+
+ ds->reader_head = pkt->next;
+
+ ds->last_ret_pos = pkt->pos;
+ ds->last_ret_dts = pkt->dts;
+
+ return pkt;
+}
+
+// Return a newly allocated new packet. The pkt parameter may be either a
+// in-memory packet (then a new reference is made), or a reference to
+// packet in the disk cache (then the packet is read from disk).
+static struct demux_packet *read_packet_from_cache(struct demux_internal *in,
+ struct demux_packet *pkt)
+{
+ if (!pkt)
+ return NULL;
+
+ if (pkt->is_cached) {
+ assert(in->cache);
+ struct demux_packet *meta = pkt;
+ pkt = demux_cache_read(in->cache, pkt->cached_data.pos);
+ if (pkt) {
+ demux_packet_copy_attribs(pkt, meta);
+ } else {
+ MP_ERR(in, "Failed to retrieve packet from cache.\n");
+ }
+ } else {
+ // The returned packet is mutated etc. and will be owned by the user.
+ pkt = demux_copy_packet(pkt);
+ }
+
+ return pkt;
+}
+
+// Returns:
+// < 0: EOF was reached, *res is not set
+// == 0: no new packet yet, wait, *res is not set
+// > 0: new packet is moved to *res
+static int dequeue_packet(struct demux_stream *ds, double min_pts,
+ struct demux_packet **res)
+{
+ struct demux_internal *in = ds->in;
+
+ if (!ds->selected)
+ return -1;
+ if (in->blocked)
+ return 0;
+
+ if (ds->sh->attached_picture) {
+ ds->eof = true;
+ if (ds->attached_picture_added)
+ return -1;
+ ds->attached_picture_added = true;
+ struct demux_packet *pkt = demux_copy_packet(ds->sh->attached_picture);
+ MP_HANDLE_OOM(pkt);
+ pkt->stream = ds->sh->index;
+ *res = pkt;
+ return 1;
+ }
+
+ if (!in->reading && !in->eof) {
+ in->reading = true; // enable demuxer thread prefetching
+ mp_cond_signal(&in->wakeup);
+ }
+
+ ds->force_read_until = min_pts;
+
+ if (ds->back_resuming || ds->back_restarting) {
+ assert(in->back_demuxing);
+ return 0;
+ }
+
+ bool eof = !ds->reader_head && ds->eof;
+
+ if (in->back_demuxing) {
+ // Subtitles not supported => EOF.
+ if (!ds->eager)
+ return -1;
+
+ // Next keyframe (or EOF) was reached => step back.
+ if (ds->back_range_started && !ds->back_range_count &&
+ ((ds->reader_head && ds->reader_head->keyframe) || eof))
+ {
+ ds->back_restarting = true;
+ ds->back_restart_eof = false;
+ ds->back_restart_next = false;
+
+ find_backward_restart_pos(ds);
+
+ if (ds->back_restarting)
+ return 0;
+ }
+
+ eof = ds->back_range_count < 0;
+ }
+
+ ds->need_wakeup = !ds->reader_head;
+ if (!ds->reader_head || eof) {
+ if (!ds->eager) {
+ // Non-eager streams temporarily return EOF. If they returned 0,
+ // the reader would have to wait for new packets, which does not
+ // make sense due to the sparseness and passiveness of non-eager
+ // streams.
+ // Unless the min_pts feature is used: then EOF is only signaled
+ // if read-ahead went above min_pts.
+ if (!lazy_stream_needs_wait(ds))
+ ds->eof = eof = true;
+ }
+ return eof ? -1 : 0;
+ }
+
+ struct demux_packet *pkt = advance_reader_head(ds);
+ assert(pkt);
+ pkt = read_packet_from_cache(in, pkt);
+ if (!pkt)
+ return 0;
+
+ if (in->back_demuxing) {
+ if (pkt->keyframe) {
+ assert(ds->back_range_count > 0);
+ ds->back_range_count -= 1;
+ if (ds->back_range_preroll >= 0)
+ ds->back_range_preroll -= 1;
+ }
+
+ if (ds->back_range_preroll >= 0)
+ pkt->back_preroll = true;
+
+ if (!ds->back_range_started) {
+ pkt->back_restart = true;
+ ds->back_range_started = true;
+ }
+ }
+
+ double ts = MP_PTS_OR_DEF(pkt->dts, pkt->pts);
+ if (ts != MP_NOPTS_VALUE)
+ ds->base_ts = ts;
+
+ if (pkt->keyframe && ts != MP_NOPTS_VALUE) {
+ // Update bitrate - only at keyframe points, because we use the
+ // (possibly) reordered packet timestamps instead of realtime.
+ double d = ts - ds->last_br_ts;
+ if (ds->last_br_ts == MP_NOPTS_VALUE || d < 0) {
+ ds->bitrate = -1;
+ ds->last_br_ts = ts;
+ ds->last_br_bytes = 0;
+ } else if (d >= 0.5) { // a window of least 500ms for UI purposes
+ ds->bitrate = ds->last_br_bytes / d;
+ ds->last_br_ts = ts;
+ ds->last_br_bytes = 0;
+ }
+ }
+ ds->last_br_bytes += pkt->len;
+
+ // This implies this function is actually called from "the" user thread.
+ if (pkt->pos >= in->d_user->filepos)
+ in->d_user->filepos = pkt->pos;
+ in->d_user->filesize = in->stream_size;
+
+ pkt->pts = MP_ADD_PTS(pkt->pts, in->ts_offset);
+ pkt->dts = MP_ADD_PTS(pkt->dts, in->ts_offset);
+
+ if (pkt->segmented) {
+ pkt->start = MP_ADD_PTS(pkt->start, in->ts_offset);
+ pkt->end = MP_ADD_PTS(pkt->end, in->ts_offset);
+ }
+
+ prune_old_packets(in);
+ *res = pkt;
+ return 1;
+}
+
+// Poll the demuxer queue, and if there's a packet, return it. Otherwise, just
+// make the demuxer thread read packets for this stream, and if there's at
+// least one packet, call the wakeup callback.
+// This enables readahead if it wasn't yet (except for interleaved subtitles).
+// Returns:
+// < 0: EOF was reached, *out_pkt=NULL
+// == 0: no new packet yet, but maybe later, *out_pkt=NULL
+// > 0: new packet read, *out_pkt is set
+// Note: when reading interleaved subtitles, the demuxer won't try to forcibly
+// read ahead to get the next subtitle packet (as the next packet could be
+// minutes away). In this situation, this function will just return -1.
+int demux_read_packet_async(struct sh_stream *sh, struct demux_packet **out_pkt)
+{
+ return demux_read_packet_async_until(sh, MP_NOPTS_VALUE, out_pkt);
+}
+
+// Like demux_read_packet_async(). They are the same for min_pts==MP_NOPTS_VALUE.
+// If min_pts is set, and the stream is lazily read (eager=false, interleaved
+// subtitles), then return 0 until demuxing has reached min_pts, or the queue
+// overflowed, or EOF was reached, or a packet was read for this stream.
+int demux_read_packet_async_until(struct sh_stream *sh, double min_pts,
+ struct demux_packet **out_pkt)
+{
+ struct demux_stream *ds = sh ? sh->ds : NULL;
+ *out_pkt = NULL;
+ if (!ds)
+ return -1;
+ struct demux_internal *in = ds->in;
+
+ mp_mutex_lock(&in->lock);
+ int r = -1;
+ while (1) {
+ r = dequeue_packet(ds, min_pts, out_pkt);
+ if (in->threading || in->blocked || r != 0)
+ break;
+ // Needs to actually read packets until we got a packet or EOF.
+ thread_work(in);
+ }
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+// Read and return any packet we find. NULL means EOF.
+// Does not work with threading (don't call demux_start_thread()).
+struct demux_packet *demux_read_any_packet(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ assert(!in->threading); // doesn't work with threading
+ struct demux_packet *out_pkt = NULL;
+ bool read_more = true;
+ while (read_more && !in->blocked) {
+ bool all_eof = true;
+ for (int n = 0; n < in->num_streams; n++) {
+ int r = dequeue_packet(in->streams[n]->ds, MP_NOPTS_VALUE, &out_pkt);
+ if (r > 0)
+ goto done;
+ if (r == 0)
+ all_eof = false;
+ }
+ // retry after calling this
+ read_more = thread_work(in);
+ read_more &= !all_eof;
+ }
+done:
+ mp_mutex_unlock(&in->lock);
+ return out_pkt;
+}
+
+int demuxer_help(struct mp_log *log, const m_option_t *opt, struct bstr name)
+{
+ int i;
+
+ mp_info(log, "Available demuxers:\n");
+ mp_info(log, " demuxer: info:\n");
+ for (i = 0; demuxer_list[i]; i++) {
+ mp_info(log, "%10s %s\n",
+ demuxer_list[i]->name, demuxer_list[i]->desc);
+ }
+ mp_info(log, "\n");
+
+ return M_OPT_EXIT;
+}
+
+static const char *d_level(enum demux_check level)
+{
+ switch (level) {
+ case DEMUX_CHECK_FORCE: return "force";
+ case DEMUX_CHECK_UNSAFE: return "unsafe";
+ case DEMUX_CHECK_REQUEST:return "request";
+ case DEMUX_CHECK_NORMAL: return "normal";
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+static int decode_float(char *str, float *out)
+{
+ char *rest;
+ float dec_val;
+
+ dec_val = strtod(str, &rest);
+ if (!rest || (rest == str) || !isfinite(dec_val))
+ return -1;
+
+ *out = dec_val;
+ return 0;
+}
+
+static int decode_gain(struct mp_log *log, struct mp_tags *tags,
+ const char *tag, float *out)
+{
+ char *tag_val = NULL;
+ float dec_val;
+
+ tag_val = mp_tags_get_str(tags, tag);
+ if (!tag_val)
+ return -1;
+
+ if (decode_float(tag_val, &dec_val) < 0) {
+ mp_msg(log, MSGL_ERR, "Invalid replaygain value\n");
+ return -1;
+ }
+
+ *out = dec_val;
+ return 0;
+}
+
+static int decode_peak(struct mp_log *log, struct mp_tags *tags,
+ const char *tag, float *out)
+{
+ char *tag_val = NULL;
+ float dec_val;
+
+ *out = 1.0;
+
+ tag_val = mp_tags_get_str(tags, tag);
+ if (!tag_val)
+ return 0;
+
+ if (decode_float(tag_val, &dec_val) < 0 || dec_val <= 0.0)
+ return -1;
+
+ *out = dec_val;
+ return 0;
+}
+
+static struct replaygain_data *decode_rgain(struct mp_log *log,
+ struct mp_tags *tags)
+{
+ struct replaygain_data rg = {0};
+
+ // Set values in *rg, using track gain as a fallback for album gain if the
+ // latter is not present. This behavior matches that in demux/demux_lavf.c's
+ // export_replaygain; if you change this, please make equivalent changes
+ // there too.
+ if (decode_gain(log, tags, "REPLAYGAIN_TRACK_GAIN", &rg.track_gain) >= 0 &&
+ decode_peak(log, tags, "REPLAYGAIN_TRACK_PEAK", &rg.track_peak) >= 0)
+ {
+ if (decode_gain(log, tags, "REPLAYGAIN_ALBUM_GAIN", &rg.album_gain) < 0 ||
+ decode_peak(log, tags, "REPLAYGAIN_ALBUM_PEAK", &rg.album_peak) < 0)
+ {
+ // Album gain is undefined; fall back to track gain.
+ rg.album_gain = rg.track_gain;
+ rg.album_peak = rg.track_peak;
+ }
+ return talloc_dup(NULL, &rg);
+ }
+
+ if (decode_gain(log, tags, "REPLAYGAIN_GAIN", &rg.track_gain) >= 0 &&
+ decode_peak(log, tags, "REPLAYGAIN_PEAK", &rg.track_peak) >= 0)
+ {
+ rg.album_gain = rg.track_gain;
+ rg.album_peak = rg.track_peak;
+ return talloc_dup(NULL, &rg);
+ }
+
+ // The r128 replaygain tags declared in RFC 7845 for opus files. The tags
+ // are generated with EBU-R128, which does not use peak meters. And the
+ // values are stored as a Q7.8 fixed point number in dB.
+ if (decode_gain(log, tags, "R128_TRACK_GAIN", &rg.track_gain) >= 0) {
+ if (decode_gain(log, tags, "R128_ALBUM_GAIN", &rg.album_gain) < 0) {
+ // Album gain is undefined; fall back to track gain.
+ rg.album_gain = rg.track_gain;
+ }
+ rg.track_gain /= 256.;
+ rg.album_gain /= 256.;
+
+ // Add 5dB to compensate for the different reference levels between
+ // our reference of ReplayGain 2 (-18 LUFS) and EBU R128 (-23 LUFS).
+ rg.track_gain += 5.;
+ rg.album_gain += 5.;
+ return talloc_dup(NULL, &rg);
+ }
+
+ return NULL;
+}
+
+static void demux_update_replaygain(demuxer_t *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct sh_stream *sh = in->streams[n];
+ if (sh->type == STREAM_AUDIO && !sh->codec->replaygain_data) {
+ struct replaygain_data *rg = decode_rgain(demuxer->log, sh->tags);
+ if (!rg)
+ rg = decode_rgain(demuxer->log, demuxer->metadata);
+ if (rg)
+ sh->codec->replaygain_data = talloc_steal(in, rg);
+ }
+ }
+}
+
+// Copy some fields from src to dst (for initialization).
+static void demux_copy(struct demuxer *dst, struct demuxer *src)
+{
+ // Note that we do as shallow copies as possible. We expect the data
+ // that is not-copied (only referenced) to be immutable.
+ // This implies e.g. that no chapters are added after initialization.
+ dst->chapters = src->chapters;
+ dst->num_chapters = src->num_chapters;
+ dst->editions = src->editions;
+ dst->num_editions = src->num_editions;
+ dst->edition = src->edition;
+ dst->attachments = src->attachments;
+ dst->num_attachments = src->num_attachments;
+ dst->matroska_data = src->matroska_data;
+ dst->playlist = src->playlist;
+ dst->seekable = src->seekable;
+ dst->partially_seekable = src->partially_seekable;
+ dst->filetype = src->filetype;
+ dst->ts_resets_possible = src->ts_resets_possible;
+ dst->fully_read = src->fully_read;
+ dst->start_time = src->start_time;
+ dst->duration = src->duration;
+ dst->is_network = src->is_network;
+ dst->is_streaming = src->is_streaming;
+ dst->stream_origin = src->stream_origin;
+ dst->priv = src->priv;
+ dst->metadata = mp_tags_dup(dst, src->metadata);
+}
+
+// Update metadata after initialization. If sh==NULL, it's global metadata,
+// otherwise it's bound to the stream. If pts==NOPTS, use the highest known pts
+// in the stream. Caller retains ownership of tags ptr. Called locked.
+static void add_timed_metadata(struct demux_internal *in, struct mp_tags *tags,
+ struct sh_stream *sh, double pts)
+{
+ struct demux_cached_range *r = in->current_range;
+ if (!r)
+ return;
+
+ // We don't expect this, nor do we find it useful.
+ if (sh && sh != in->metadata_stream)
+ return;
+
+ if (pts == MP_NOPTS_VALUE) {
+ for (int n = 0; n < r->num_streams; n++)
+ pts = MP_PTS_MAX(pts, r->streams[n]->last_ts);
+
+ // Tends to happen when doing the initial icy update.
+ if (pts == MP_NOPTS_VALUE)
+ pts = in->d_thread->start_time;
+ }
+
+ struct timed_metadata *tm = talloc_zero(NULL, struct timed_metadata);
+ *tm = (struct timed_metadata){
+ .pts = pts,
+ .tags = mp_tags_dup(tm, tags),
+ .from_stream = !!sh,
+ };
+ MP_TARRAY_APPEND(r, r->metadata, r->num_metadata, tm);
+}
+
+// This is called by demuxer implementations if sh->tags changed. Note that
+// sh->tags itself is never actually changed (it's immutable, because sh->tags
+// can be accessed by the playback thread, and there is no synchronization).
+// pts is the time at/after which the metadata becomes effective. You're
+// supposed to call this ordered by time, and only while a packet is being
+// read.
+// Ownership of tags goes to the function.
+void demux_stream_tags_changed(struct demuxer *demuxer, struct sh_stream *sh,
+ struct mp_tags *tags, double pts)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_thread);
+ struct demux_stream *ds = sh ? sh->ds : NULL;
+ assert(!sh || ds); // stream must have been added
+
+ mp_mutex_lock(&in->lock);
+
+ if (pts == MP_NOPTS_VALUE) {
+ MP_WARN(in, "Discarding timed metadata without timestamp.\n");
+ } else {
+ add_timed_metadata(in, tags, sh, pts);
+ }
+ talloc_free(tags);
+
+ mp_mutex_unlock(&in->lock);
+}
+
+// This is called by demuxer implementations if demuxer->metadata changed.
+// (It will be propagated to the user as timed metadata.)
+void demux_metadata_changed(demuxer_t *demuxer)
+{
+ assert(demuxer == demuxer->in->d_thread); // call from demuxer impl. only
+ struct demux_internal *in = demuxer->in;
+
+ mp_mutex_lock(&in->lock);
+ add_timed_metadata(in, demuxer->metadata, NULL, MP_NOPTS_VALUE);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Called locked, with user demuxer.
+static void update_final_metadata(demuxer_t *demuxer, struct timed_metadata *tm)
+{
+ assert(demuxer == demuxer->in->d_user);
+ struct demux_internal *in = demuxer->in;
+
+ struct mp_tags *dyn_tags = NULL;
+
+ // Often useful for audio-only files, which have metadata in the audio track
+ // metadata instead of the main metadata, but can also have cover art
+ // metadata (which libavformat likes to treat as video streams).
+ int astreams = 0;
+ int astream_id = -1;
+ int vstreams = 0;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct sh_stream *sh = in->streams[n];
+ if (sh->type == STREAM_VIDEO && !sh->attached_picture)
+ vstreams += 1;
+ if (sh->type == STREAM_AUDIO) {
+ astreams += 1;
+ astream_id = n;
+ }
+ }
+
+ // Use the metadata_stream tags only if this really seems to be an audio-
+ // only stream. Otherwise it will happen too often that "uninteresting"
+ // stream metadata will trash the actual file tags.
+ if (vstreams == 0 && astreams == 1 &&
+ in->streams[astream_id] == in->metadata_stream)
+ {
+ dyn_tags = in->metadata_stream->tags;
+ if (tm && tm->from_stream)
+ dyn_tags = tm->tags;
+ }
+
+ // Global metadata updates.
+ if (tm && !tm->from_stream)
+ dyn_tags = tm->tags;
+
+ if (dyn_tags)
+ mp_tags_merge(demuxer->metadata, dyn_tags);
+}
+
+static struct timed_metadata *lookup_timed_metadata(struct demux_internal *in,
+ double pts)
+{
+ struct demux_cached_range *r = in->current_range;
+
+ if (!r || !r->num_metadata || pts == MP_NOPTS_VALUE)
+ return NULL;
+
+ int start = 1;
+ int i = in->cached_metadata_index;
+ if (i >= 0 && i < r->num_metadata && r->metadata[i]->pts <= pts)
+ start = i + 1;
+
+ in->cached_metadata_index = r->num_metadata - 1;
+ for (int n = start; n < r->num_metadata; n++) {
+ if (r->metadata[n]->pts >= pts) {
+ in->cached_metadata_index = n - 1;
+ break;
+ }
+ }
+
+ return r->metadata[in->cached_metadata_index];
+}
+
+// Called by the user thread (i.e. player) to update metadata and other things
+// from the demuxer thread.
+// The pts parameter is the current playback position.
+void demux_update(demuxer_t *demuxer, double pts)
+{
+ assert(demuxer == demuxer->in->d_user);
+ struct demux_internal *in = demuxer->in;
+
+ mp_mutex_lock(&in->lock);
+
+ if (!in->threading)
+ update_cache(in);
+
+ // This implies this function is actually called from "the" user thread.
+ in->d_user->filesize = in->stream_size;
+
+ pts = MP_ADD_PTS(pts, -in->ts_offset);
+
+ struct timed_metadata *prev = lookup_timed_metadata(in, in->last_playback_pts);
+ struct timed_metadata *cur = lookup_timed_metadata(in, pts);
+ if (prev != cur || in->force_metadata_update) {
+ in->force_metadata_update = false;
+ update_final_metadata(demuxer, cur);
+ demuxer->events |= DEMUX_EVENT_METADATA;
+ }
+
+ in->last_playback_pts = pts;
+
+ demuxer->events |= in->events;
+ in->events = 0;
+ if (demuxer->events & (DEMUX_EVENT_METADATA | DEMUX_EVENT_STREAMS))
+ demux_update_replaygain(demuxer);
+ if (demuxer->events & DEMUX_EVENT_DURATION)
+ demuxer->duration = in->duration;
+
+ mp_mutex_unlock(&in->lock);
+}
+
+static void demux_init_cuesheet(struct demuxer *demuxer)
+{
+ char *cue = mp_tags_get_str(demuxer->metadata, "cuesheet");
+ if (cue && !demuxer->num_chapters) {
+ struct cue_file *f = mp_parse_cue(bstr0(cue));
+ if (f) {
+ if (mp_check_embedded_cue(f) < 0) {
+ MP_WARN(demuxer, "Embedded cue sheet references more than one file. "
+ "Ignoring it.\n");
+ } else {
+ for (int n = 0; n < f->num_tracks; n++) {
+ struct cue_track *t = &f->tracks[n];
+ int idx = demuxer_add_chapter(demuxer, "", t->start, -1);
+ mp_tags_merge(demuxer->chapters[idx].metadata, t->tags);
+ }
+ }
+ }
+ talloc_free(f);
+ }
+}
+
+// A demuxer can use this during opening if all data was read from the stream.
+// Calling this after opening was completed is not allowed. Also, if opening
+// failed, this must not be called (or trying another demuxer would fail).
+// Useful so that e.g. subtitles don't keep the file or socket open.
+// If there's ever the situation where we can't allow the demuxer to close
+// the stream, this function could ignore the request.
+void demux_close_stream(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(!in->threading && demuxer == in->d_thread);
+
+ if (!demuxer->stream || !in->owns_stream)
+ return;
+
+ MP_VERBOSE(demuxer, "demuxer read all data; closing stream\n");
+ free_stream(demuxer->stream);
+ demuxer->stream = NULL;
+ in->d_user->stream = NULL;
+}
+
+static void demux_init_ccs(struct demuxer *demuxer, struct demux_opts *opts)
+{
+ struct demux_internal *in = demuxer->in;
+ if (!opts->create_ccs)
+ return;
+ mp_mutex_lock(&in->lock);
+ for (int n = 0; n < in->num_streams; n++) {
+ struct sh_stream *sh = in->streams[n];
+ if (sh->type == STREAM_VIDEO && !sh->attached_picture)
+ demuxer_get_cc_track_locked(sh);
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+// Return whether "heavy" caching on this stream is enabled. By default, this
+// corresponds to whether the source stream is considered in the network. The
+// only effect should be adjusting display behavior (of cache stats etc.), and
+// possibly switching between which set of options influence cache settings.
+bool demux_is_network_cached(demuxer_t *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ bool r = in->using_network_cache_opts;
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct parent_stream_info {
+ bool seekable;
+ bool is_network;
+ bool is_streaming;
+ int stream_origin;
+ struct mp_cancel *cancel;
+ char *filename;
+};
+
+static struct demuxer *open_given_type(struct mpv_global *global,
+ struct mp_log *log,
+ const struct demuxer_desc *desc,
+ struct stream *stream,
+ struct parent_stream_info *sinfo,
+ struct demuxer_params *params,
+ enum demux_check check)
+{
+ if (mp_cancel_test(sinfo->cancel))
+ return NULL;
+
+ struct demuxer *demuxer = talloc_ptrtype(NULL, demuxer);
+ struct m_config_cache *opts_cache =
+ m_config_cache_alloc(demuxer, global, &demux_conf);
+ struct demux_opts *opts = opts_cache->opts;
+ *demuxer = (struct demuxer) {
+ .desc = desc,
+ .stream = stream,
+ .cancel = sinfo->cancel,
+ .seekable = sinfo->seekable,
+ .filepos = -1,
+ .global = global,
+ .log = mp_log_new(demuxer, log, desc->name),
+ .glog = log,
+ .filename = talloc_strdup(demuxer, sinfo->filename),
+ .is_network = sinfo->is_network,
+ .is_streaming = sinfo->is_streaming,
+ .stream_origin = sinfo->stream_origin,
+ .access_references = opts->access_references,
+ .opts = opts,
+ .opts_cache = opts_cache,
+ .events = DEMUX_EVENT_ALL,
+ .duration = -1,
+ };
+
+ struct demux_internal *in = demuxer->in = talloc_ptrtype(demuxer, in);
+ *in = (struct demux_internal){
+ .global = global,
+ .log = demuxer->log,
+ .stats = stats_ctx_create(in, global, "demuxer"),
+ .can_cache = params && params->is_top_level,
+ .can_record = params && params->stream_record,
+ .d_thread = talloc(demuxer, struct demuxer),
+ .d_user = demuxer,
+ .after_seek = true, // (assumed identical to initial demuxer state)
+ .after_seek_to_start = true,
+ .highest_av_pts = MP_NOPTS_VALUE,
+ .seeking_in_progress = MP_NOPTS_VALUE,
+ .demux_ts = MP_NOPTS_VALUE,
+ .owns_stream = !params->external_stream,
+ };
+ mp_mutex_init(&in->lock);
+ mp_cond_init(&in->wakeup);
+
+ *in->d_thread = *demuxer;
+
+ in->d_thread->metadata = talloc_zero(in->d_thread, struct mp_tags);
+
+ mp_dbg(log, "Trying demuxer: %s (force-level: %s)\n",
+ desc->name, d_level(check));
+
+ if (stream)
+ stream_seek(stream, 0);
+
+ in->d_thread->params = params; // temporary during open()
+ int ret = demuxer->desc->open(in->d_thread, check);
+ if (ret >= 0) {
+ in->d_thread->params = NULL;
+ if (in->d_thread->filetype)
+ mp_verbose(log, "Detected file format: %s (%s)\n",
+ in->d_thread->filetype, desc->desc);
+ else
+ mp_verbose(log, "Detected file format: %s\n", desc->desc);
+ if (!in->d_thread->seekable)
+ mp_verbose(log, "Stream is not seekable.\n");
+ if (!in->d_thread->seekable && opts->force_seekable) {
+ mp_warn(log, "Not seekable, but enabling seeking on user request.\n");
+ in->d_thread->seekable = true;
+ in->d_thread->partially_seekable = true;
+ }
+ demux_init_cuesheet(in->d_thread);
+ demux_init_ccs(demuxer, opts);
+ demux_convert_tags_charset(in->d_thread);
+ demux_copy(in->d_user, in->d_thread);
+ in->duration = in->d_thread->duration;
+ demuxer_sort_chapters(demuxer);
+ in->events = DEMUX_EVENT_ALL;
+
+ struct demuxer *sub = NULL;
+ if (!(params && params->disable_timeline)) {
+ struct timeline *tl = timeline_load(global, log, demuxer);
+ if (tl) {
+ struct demuxer_params params2 = {0};
+ params2.timeline = tl;
+ params2.is_top_level = params && params->is_top_level;
+ params2.stream_record = params && params->stream_record;
+ sub =
+ open_given_type(global, log, &demuxer_desc_timeline,
+ NULL, sinfo, &params2, DEMUX_CHECK_FORCE);
+ if (sub) {
+ in->can_cache = false;
+ in->can_record = false;
+ } else {
+ timeline_destroy(tl);
+ }
+ }
+ }
+
+ switch_to_fresh_cache_range(in);
+
+ update_opts(demuxer);
+
+ demux_update(demuxer, MP_NOPTS_VALUE);
+
+ demuxer = sub ? sub : demuxer;
+ return demuxer;
+ }
+
+ demuxer->stream = NULL;
+ demux_free(demuxer);
+ return NULL;
+}
+
+static const int d_normal[] = {DEMUX_CHECK_NORMAL, DEMUX_CHECK_UNSAFE, -1};
+static const int d_request[] = {DEMUX_CHECK_REQUEST, -1};
+static const int d_force[] = {DEMUX_CHECK_FORCE, -1};
+
+// params can be NULL
+// This may free the stream parameter on success.
+static struct demuxer *demux_open(struct stream *stream,
+ struct mp_cancel *cancel,
+ struct demuxer_params *params,
+ struct mpv_global *global)
+{
+ const int *check_levels = d_normal;
+ const struct demuxer_desc *check_desc = NULL;
+ struct mp_log *log = mp_log_new(NULL, global->log, "!demux");
+ struct demuxer *demuxer = NULL;
+ char *force_format = params ? params->force_format : NULL;
+
+ struct parent_stream_info sinfo = {
+ .seekable = stream->seekable,
+ .is_network = stream->is_network,
+ .is_streaming = stream->streaming,
+ .stream_origin = stream->stream_origin,
+ .cancel = cancel,
+ .filename = talloc_strdup(NULL, stream->url),
+ };
+
+ if (!force_format)
+ force_format = stream->demuxer;
+
+ if (force_format && force_format[0] && !stream->is_directory) {
+ check_levels = d_request;
+ if (force_format[0] == '+') {
+ force_format += 1;
+ check_levels = d_force;
+ }
+ for (int n = 0; demuxer_list[n]; n++) {
+ if (strcmp(demuxer_list[n]->name, force_format) == 0) {
+ check_desc = demuxer_list[n];
+ break;
+ }
+ }
+ if (!check_desc) {
+ mp_err(log, "Demuxer %s does not exist.\n", force_format);
+ goto done;
+ }
+ }
+
+ // Test demuxers from first to last, one pass for each check_levels[] entry
+ for (int pass = 0; check_levels[pass] != -1; pass++) {
+ enum demux_check level = check_levels[pass];
+ mp_verbose(log, "Trying demuxers for level=%s.\n", d_level(level));
+ for (int n = 0; demuxer_list[n]; n++) {
+ const struct demuxer_desc *desc = demuxer_list[n];
+ if (!check_desc || desc == check_desc) {
+ demuxer = open_given_type(global, log, desc, stream, &sinfo,
+ params, level);
+ if (demuxer) {
+ talloc_steal(demuxer, log);
+ log = NULL;
+ goto done;
+ }
+ }
+ }
+ }
+
+done:
+ talloc_free(sinfo.filename);
+ talloc_free(log);
+ return demuxer;
+}
+
+static struct stream *create_webshit_concat_stream(struct mpv_global *global,
+ struct mp_cancel *c,
+ bstr init, struct stream *real)
+{
+ struct stream *mem = stream_memory_open(global, init.start, init.len);
+ assert(mem);
+
+ struct stream *streams[2] = {mem, real};
+ struct stream *concat = stream_concat_open(global, c, streams, 2);
+ if (!concat) {
+ free_stream(mem);
+ free_stream(real);
+ }
+ return concat;
+}
+
+// Convenience function: open the stream, enable the cache (according to params
+// and global opts.), open the demuxer.
+// Also for some reason may close the opened stream if it's not needed.
+// demuxer->cancel is not the cancel parameter, but is its own object that will
+// be a slave (mp_cancel_set_parent()) to provided cancel object.
+// demuxer->cancel is automatically freed.
+struct demuxer *demux_open_url(const char *url,
+ struct demuxer_params *params,
+ struct mp_cancel *cancel,
+ struct mpv_global *global)
+{
+ if (!params)
+ return NULL;
+ struct mp_cancel *priv_cancel = mp_cancel_new(NULL);
+ if (cancel)
+ mp_cancel_set_parent(priv_cancel, cancel);
+ struct stream *s = params->external_stream;
+ if (!s) {
+ s = stream_create(url, STREAM_READ | params->stream_flags,
+ priv_cancel, global);
+ if (s && params->init_fragment.len) {
+ s = create_webshit_concat_stream(global, priv_cancel,
+ params->init_fragment, s);
+ }
+ }
+ if (!s) {
+ talloc_free(priv_cancel);
+ return NULL;
+ }
+ struct demuxer *d = demux_open(s, priv_cancel, params, global);
+ if (d) {
+ talloc_steal(d->in, priv_cancel);
+ assert(d->cancel);
+ } else {
+ params->demuxer_failed = true;
+ if (!params->external_stream)
+ free_stream(s);
+ talloc_free(priv_cancel);
+ }
+ return d;
+}
+
+// clear the packet queues
+void demux_flush(demuxer_t *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ mp_mutex_lock(&in->lock);
+ clear_reader_state(in, true);
+ for (int n = 0; n < in->num_ranges; n++)
+ clear_cached_range(in, in->ranges[n]);
+ free_empty_cached_ranges(in);
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ ds->refreshing = false;
+ ds->eof = false;
+ }
+ in->eof = false;
+ in->seeking = false;
+ mp_mutex_unlock(&in->lock);
+}
+
+// Does some (but not all) things for switching to another range.
+static void switch_current_range(struct demux_internal *in,
+ struct demux_cached_range *range)
+{
+ struct demux_cached_range *old = in->current_range;
+ assert(old != range);
+
+ set_current_range(in, range);
+
+ if (old) {
+ // Remove packets which can't be used when seeking back to the range.
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_queue *queue = old->streams[n];
+
+ // Remove all packets which cannot be involved in seeking.
+ while (queue->head && !queue->head->keyframe)
+ remove_head_packet(queue);
+ }
+
+ // Exclude weird corner cases that break resuming.
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ // This is needed to resume or join the range at all.
+ if (ds->selected && !(ds->global_correct_dts ||
+ ds->global_correct_pos))
+ {
+ MP_VERBOSE(in, "discarding unseekable range due to stream %d\n", n);
+ clear_cached_range(in, old);
+ break;
+ }
+ }
+ }
+
+ // Set up reading from new range (as well as writing to it).
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ ds->queue = range->streams[n];
+ ds->refreshing = false;
+ ds->eof = false;
+ }
+
+ // No point in keeping any junk (especially if old current_range is empty).
+ free_empty_cached_ranges(in);
+
+ // The change detection doesn't work across ranges.
+ in->force_metadata_update = true;
+}
+
+// Search for the entry with the highest index with entry.pts <= pts true.
+static struct demux_packet *search_index(struct demux_queue *queue, double pts)
+{
+ size_t a = 0;
+ size_t b = queue->num_index;
+
+ while (a < b) {
+ size_t m = a + (b - a) / 2;
+ struct index_entry *e = &QUEUE_INDEX_ENTRY(queue, m);
+
+ bool m_ok = e->pts <= pts;
+
+ if (a + 1 == b)
+ return m_ok ? e->pkt : NULL;
+
+ if (m_ok) {
+ a = m;
+ } else {
+ b = m;
+ }
+ }
+
+ return NULL;
+}
+
+static struct demux_packet *find_seek_target(struct demux_queue *queue,
+ double pts, int flags)
+{
+ pts -= queue->ds->sh->seek_preroll;
+
+ struct demux_packet *start = search_index(queue, pts);
+ if (!start)
+ start = queue->head;
+
+ struct demux_packet *target = NULL;
+ struct demux_packet *next = NULL;
+ for (struct demux_packet *dp = start; dp; dp = next) {
+ next = dp->next;
+ if (!dp->keyframe)
+ continue;
+
+ double range_pts;
+ next = compute_keyframe_times(dp, &range_pts, NULL);
+
+ if (range_pts == MP_NOPTS_VALUE)
+ continue;
+
+ if (flags & SEEK_FORWARD) {
+ // Stop on the first packet that is >= pts.
+ if (target)
+ break;
+ if (range_pts < pts)
+ continue;
+ } else {
+ // Stop before the first packet that is > pts.
+ // This still returns a packet with > pts if there's no better one.
+ if (target && range_pts > pts)
+ break;
+ }
+
+ target = dp;
+ }
+
+ return target;
+}
+
+// Return a cache range for the given pts/flags, or NULL if none available.
+// must be called locked
+static struct demux_cached_range *find_cache_seek_range(struct demux_internal *in,
+ double pts, int flags)
+{
+ // Note about queued low level seeks: in->seeking can be true here, and it
+ // might come from a previous resume seek to the current range. If we end
+ // up seeking into the current range (i.e. just changing time offset), the
+ // seek needs to continue. Otherwise, we override the queued seek anyway.
+ if ((flags & SEEK_FACTOR) || !in->seekable_cache)
+ return NULL;
+
+ struct demux_cached_range *res = NULL;
+
+ for (int n = 0; n < in->num_ranges; n++) {
+ struct demux_cached_range *r = in->ranges[n];
+ if (r->seek_start != MP_NOPTS_VALUE) {
+ MP_VERBOSE(in, "cached range %d: %f <-> %f (bof=%d, eof=%d)\n",
+ n, r->seek_start, r->seek_end, r->is_bof, r->is_eof);
+
+ if ((pts >= r->seek_start || r->is_bof) &&
+ (pts <= r->seek_end || r->is_eof))
+ {
+ MP_VERBOSE(in, "...using this range for in-cache seek.\n");
+ res = r;
+ break;
+ }
+ }
+ }
+
+ return res;
+}
+
+// Adjust the seek target to the found video key frames. Otherwise the
+// video will undershoot the seek target, while audio will be closer to it.
+// The player frontend will play the additional video without audio, so
+// you get silent audio for the amount of "undershoot". Adjusting the seek
+// target will make the audio seek to the video target or before.
+// (If hr-seeks are used, it's better to skip this, as it would only mean
+// that more audio data than necessary would have to be decoded.)
+static void adjust_cache_seek_target(struct demux_internal *in,
+ struct demux_cached_range *range,
+ double *pts, int *flags)
+{
+ if (*flags & SEEK_HR)
+ return;
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ struct demux_queue *queue = range->streams[n];
+ if (ds->selected && ds->type == STREAM_VIDEO) {
+ struct demux_packet *target = find_seek_target(queue, *pts, *flags);
+ if (target) {
+ double target_pts;
+ compute_keyframe_times(target, &target_pts, NULL);
+ if (target_pts != MP_NOPTS_VALUE) {
+ MP_VERBOSE(in, "adjust seek target %f -> %f\n",
+ *pts, target_pts);
+ // (We assume the find_seek_target() call will return
+ // the same target for the video stream.)
+ *pts = target_pts;
+ *flags &= ~SEEK_FORWARD;
+ }
+ }
+ break;
+ }
+ }
+}
+
+// must be called locked
+// range must be non-NULL and from find_cache_seek_range() using the same pts
+// and flags, before any other changes to the cached state
+static void execute_cache_seek(struct demux_internal *in,
+ struct demux_cached_range *range,
+ double pts, int flags)
+{
+ adjust_cache_seek_target(in, range, &pts, &flags);
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ struct demux_queue *queue = range->streams[n];
+
+ struct demux_packet *target = find_seek_target(queue, pts, flags);
+ ds->reader_head = target;
+ ds->skip_to_keyframe = !target;
+ if (ds->reader_head)
+ ds->base_ts = MP_PTS_OR_DEF(ds->reader_head->pts, ds->reader_head->dts);
+
+ MP_VERBOSE(in, "seeking stream %d (%s) to ",
+ n, stream_type_name(ds->type));
+
+ if (target) {
+ MP_VERBOSE(in, "packet %f/%f\n", target->pts, target->dts);
+ } else {
+ MP_VERBOSE(in, "nothing\n");
+ }
+ }
+
+ // If we seek to another range, we want to seek the low level demuxer to
+ // there as well, because reader and demuxer queue must be the same.
+ if (in->current_range != range) {
+ switch_current_range(in, range);
+
+ in->seeking = true;
+ in->seek_flags = SEEK_HR;
+ in->seek_pts = range->seek_end - 1.0;
+
+ // When new packets are being appended, they could overlap with the old
+ // range due to demuxer seek imprecisions, or because the queue contains
+ // packets past the seek target but before the next seek target. Don't
+ // append them twice, instead skip them until new packets are found.
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ ds->refreshing = ds->selected;
+ }
+
+ MP_VERBOSE(in, "resuming demuxer to end of cached range\n");
+ }
+}
+
+// Create a new blank cache range, and backup the old one. If the seekable
+// demuxer cache is disabled, merely reset the current range to a blank state.
+static void switch_to_fresh_cache_range(struct demux_internal *in)
+{
+ if (!in->seekable_cache && in->current_range) {
+ clear_cached_range(in, in->current_range);
+ return;
+ }
+
+ struct demux_cached_range *range = talloc_ptrtype(NULL, range);
+ *range = (struct demux_cached_range){
+ .seek_start = MP_NOPTS_VALUE,
+ .seek_end = MP_NOPTS_VALUE,
+ };
+ MP_TARRAY_APPEND(in, in->ranges, in->num_ranges, range);
+ add_missing_streams(in, range);
+
+ switch_current_range(in, range);
+}
+
+int demux_seek(demuxer_t *demuxer, double seek_pts, int flags)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ mp_mutex_lock(&in->lock);
+
+ if (!(flags & SEEK_FACTOR))
+ seek_pts = MP_ADD_PTS(seek_pts, -in->ts_offset);
+
+ int res = queue_seek(in, seek_pts, flags, true);
+
+ mp_cond_signal(&in->wakeup);
+ mp_mutex_unlock(&in->lock);
+
+ return res;
+}
+
+static bool queue_seek(struct demux_internal *in, double seek_pts, int flags,
+ bool clear_back_state)
+{
+ if (seek_pts == MP_NOPTS_VALUE)
+ return false;
+
+ MP_VERBOSE(in, "queuing seek to %f%s\n", seek_pts,
+ in->seeking ? " (cascade)" : "");
+
+ bool require_cache = flags & SEEK_CACHED;
+ flags &= ~(unsigned)SEEK_CACHED;
+
+ bool set_backwards = flags & SEEK_SATAN;
+ flags &= ~(unsigned)SEEK_SATAN;
+
+ bool force_seek = flags & SEEK_FORCE;
+ flags &= ~(unsigned)SEEK_FORCE;
+
+ bool block = flags & SEEK_BLOCK;
+ flags &= ~(unsigned)SEEK_BLOCK;
+
+ struct demux_cached_range *cache_target =
+ find_cache_seek_range(in, seek_pts, flags);
+
+ if (!cache_target) {
+ if (require_cache) {
+ MP_VERBOSE(in, "Cached seek not possible.\n");
+ return false;
+ }
+ if (!in->d_thread->seekable && !force_seek) {
+ MP_WARN(in, "Cannot seek in this file.\n");
+ return false;
+ }
+ }
+
+ in->eof = false;
+ in->reading = false;
+ in->back_demuxing = set_backwards;
+
+ clear_reader_state(in, clear_back_state);
+
+ in->blocked = block;
+
+ if (cache_target) {
+ execute_cache_seek(in, cache_target, seek_pts, flags);
+ } else {
+ switch_to_fresh_cache_range(in);
+
+ in->seeking = true;
+ in->seek_flags = flags;
+ in->seek_pts = seek_pts;
+ }
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ if (in->back_demuxing) {
+ if (ds->back_seek_pos == MP_NOPTS_VALUE)
+ ds->back_seek_pos = seek_pts;
+ // Process possibly cached packets.
+ back_demux_see_packets(in->streams[n]->ds);
+ }
+
+ wakeup_ds(ds);
+ }
+
+ if (!in->threading && in->seeking)
+ execute_seek(in);
+
+ return true;
+}
+
+struct sh_stream *demuxer_stream_by_demuxer_id(struct demuxer *d,
+ enum stream_type t, int id)
+{
+ if (id < 0)
+ return NULL;
+ int num = demux_get_num_stream(d);
+ for (int n = 0; n < num; n++) {
+ struct sh_stream *s = demux_get_stream(d, n);
+ if (s->type == t && s->demuxer_id == id)
+ return s;
+ }
+ return NULL;
+}
+
+// An obscure mechanism to get stream switching to be executed "faster" (as
+// perceived by the user), by making the stream return packets from the
+// current position
+// On a switch, it seeks back, and then grabs all packets that were
+// "missing" from the packet queue of the newly selected stream.
+static void initiate_refresh_seek(struct demux_internal *in,
+ struct demux_stream *stream,
+ double start_ts)
+{
+ struct demuxer *demux = in->d_thread;
+ bool seekable = demux->desc->seek && demux->seekable &&
+ !demux->partially_seekable;
+
+ bool normal_seek = true;
+ bool refresh_possible = true;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ if (!ds->selected)
+ continue;
+
+ if (ds->type == STREAM_VIDEO || ds->type == STREAM_AUDIO)
+ start_ts = MP_PTS_MIN(start_ts, ds->base_ts);
+
+ // If there were no other streams selected, we can use a normal seek.
+ normal_seek &= stream == ds;
+
+ refresh_possible &= ds->queue->correct_dts || ds->queue->correct_pos;
+ }
+
+ if (start_ts == MP_NOPTS_VALUE || !seekable)
+ return;
+
+ if (!normal_seek) {
+ if (!refresh_possible) {
+ MP_VERBOSE(in, "can't issue refresh seek\n");
+ return;
+ }
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+
+ bool correct_pos = ds->queue->correct_pos;
+ bool correct_dts = ds->queue->correct_dts;
+
+ // We need to re-read all packets anyway, so discard the buffered
+ // data. (In theory, we could keep the packets, and be able to use
+ // it for seeking if partially read streams are deselected again,
+ // but this causes other problems like queue overflows when
+ // selecting a new stream.)
+ ds_clear_reader_queue_state(ds);
+ clear_queue(ds->queue);
+
+ // Streams which didn't have any packets yet will return all packets,
+ // other streams return packets only starting from the last position.
+ if (ds->selected && (ds->last_ret_pos != -1 ||
+ ds->last_ret_dts != MP_NOPTS_VALUE))
+ {
+ ds->refreshing = true;
+ ds->queue->correct_dts = correct_dts;
+ ds->queue->correct_pos = correct_pos;
+ ds->queue->last_pos = ds->last_ret_pos;
+ ds->queue->last_dts = ds->last_ret_dts;
+ }
+
+ update_seek_ranges(in->current_range);
+ }
+
+ start_ts -= 1.0; // small offset to get correct overlap
+ }
+
+ MP_VERBOSE(in, "refresh seek to %f\n", start_ts);
+ in->seeking = true;
+ in->seek_flags = SEEK_HR;
+ in->seek_pts = start_ts;
+}
+
+// Set whether the given stream should return packets.
+// ref_pts is used only if the stream is enabled. Then it serves as approximate
+// start pts for this stream (in the worst case it is ignored).
+void demuxer_select_track(struct demuxer *demuxer, struct sh_stream *stream,
+ double ref_pts, bool selected)
+{
+ struct demux_internal *in = demuxer->in;
+ struct demux_stream *ds = stream->ds;
+ mp_mutex_lock(&in->lock);
+ ref_pts = MP_ADD_PTS(ref_pts, -in->ts_offset);
+ // don't flush buffers if stream is already selected / unselected
+ if (ds->selected != selected) {
+ MP_VERBOSE(in, "%sselect track %d\n", selected ? "" : "de", stream->index);
+ ds->selected = selected;
+ update_stream_selection_state(in, ds);
+ in->tracks_switched = true;
+ if (ds->selected) {
+ if (in->back_demuxing)
+ ds->back_seek_pos = ref_pts;
+ if (!in->after_seek)
+ initiate_refresh_seek(in, ds, ref_pts);
+ }
+ if (in->threading) {
+ mp_cond_signal(&in->wakeup);
+ } else {
+ execute_trackswitch(in);
+ }
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+// Execute a refresh seek on the given stream.
+// ref_pts has the same meaning as with demuxer_select_track()
+void demuxer_refresh_track(struct demuxer *demuxer, struct sh_stream *stream,
+ double ref_pts)
+{
+ struct demux_internal *in = demuxer->in;
+ struct demux_stream *ds = stream->ds;
+ mp_mutex_lock(&in->lock);
+ ref_pts = MP_ADD_PTS(ref_pts, -in->ts_offset);
+ if (ds->selected) {
+ MP_VERBOSE(in, "refresh track %d\n", stream->index);
+ update_stream_selection_state(in, ds);
+ if (in->back_demuxing)
+ ds->back_seek_pos = ref_pts;
+ if (!in->after_seek)
+ initiate_refresh_seek(in, ds, ref_pts);
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+// This is for demuxer implementations only. demuxer_select_track() sets the
+// logical state, while this function returns the actual state (in case the
+// demuxer attempts to cache even unselected packets for track switching - this
+// will potentially be done in the future).
+bool demux_stream_is_selected(struct sh_stream *stream)
+{
+ if (!stream)
+ return false;
+ bool r = false;
+ mp_mutex_lock(&stream->ds->in->lock);
+ r = stream->ds->selected;
+ mp_mutex_unlock(&stream->ds->in->lock);
+ return r;
+}
+
+void demux_set_stream_wakeup_cb(struct sh_stream *sh,
+ void (*cb)(void *ctx), void *ctx)
+{
+ mp_mutex_lock(&sh->ds->in->lock);
+ sh->ds->wakeup_cb = cb;
+ sh->ds->wakeup_cb_ctx = ctx;
+ sh->ds->need_wakeup = true;
+ mp_mutex_unlock(&sh->ds->in->lock);
+}
+
+int demuxer_add_attachment(demuxer_t *demuxer, char *name, char *type,
+ void *data, size_t data_size)
+{
+ if (!(demuxer->num_attachments % 32))
+ demuxer->attachments = talloc_realloc(demuxer, demuxer->attachments,
+ struct demux_attachment,
+ demuxer->num_attachments + 32);
+
+ struct demux_attachment *att = &demuxer->attachments[demuxer->num_attachments];
+ att->name = talloc_strdup(demuxer->attachments, name);
+ att->type = talloc_strdup(demuxer->attachments, type);
+ att->data = talloc_memdup(demuxer->attachments, data, data_size);
+ att->data_size = data_size;
+
+ return demuxer->num_attachments++;
+}
+
+static int chapter_compare(const void *p1, const void *p2)
+{
+ struct demux_chapter *c1 = (void *)p1;
+ struct demux_chapter *c2 = (void *)p2;
+
+ if (c1->pts > c2->pts)
+ return 1;
+ else if (c1->pts < c2->pts)
+ return -1;
+ return c1->original_index > c2->original_index ? 1 :-1; // never equal
+}
+
+static void demuxer_sort_chapters(demuxer_t *demuxer)
+{
+ if (demuxer->num_chapters) {
+ qsort(demuxer->chapters, demuxer->num_chapters,
+ sizeof(struct demux_chapter), chapter_compare);
+ }
+}
+
+int demuxer_add_chapter(demuxer_t *demuxer, char *name,
+ double pts, uint64_t demuxer_id)
+{
+ struct demux_chapter new = {
+ .original_index = demuxer->num_chapters,
+ .pts = pts,
+ .metadata = talloc_zero(demuxer, struct mp_tags),
+ .demuxer_id = demuxer_id,
+ };
+ mp_tags_set_str(new.metadata, "TITLE", name);
+ MP_TARRAY_APPEND(demuxer, demuxer->chapters, demuxer->num_chapters, new);
+ return demuxer->num_chapters - 1;
+}
+
+// Disallow reading any packets and make readers think there is no new data
+// yet, until a seek is issued.
+void demux_block_reading(struct demuxer *demuxer, bool block)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ mp_mutex_lock(&in->lock);
+ in->blocked = block;
+ for (int n = 0; n < in->num_streams; n++) {
+ in->streams[n]->ds->need_wakeup = true;
+ wakeup_ds(in->streams[n]->ds);
+ }
+ mp_cond_signal(&in->wakeup);
+ mp_mutex_unlock(&in->lock);
+}
+
+static void update_bytes_read(struct demux_internal *in)
+{
+ struct demuxer *demuxer = in->d_thread;
+
+ int64_t new = in->slave_unbuffered_read_bytes;
+ in->slave_unbuffered_read_bytes = 0;
+
+ int64_t new_seeks = 0;
+
+ struct stream *stream = demuxer->stream;
+ if (stream) {
+ new += stream->total_unbuffered_read_bytes;
+ stream->total_unbuffered_read_bytes = 0;
+ new_seeks += stream->total_stream_seeks;
+ stream->total_stream_seeks = 0;
+ }
+
+ in->cache_unbuffered_read_bytes += new;
+ in->hack_unbuffered_read_bytes += new;
+ in->byte_level_seeks += new_seeks;
+}
+
+// must be called locked, temporarily unlocks
+static void update_cache(struct demux_internal *in)
+{
+ struct demuxer *demuxer = in->d_thread;
+ struct stream *stream = demuxer->stream;
+
+ int64_t now = mp_time_ns();
+ int64_t diff = now - in->last_speed_query;
+ bool do_update = diff >= MP_TIME_S_TO_NS(1) || !in->last_speed_query;
+
+ // Don't lock while querying the stream.
+ mp_mutex_unlock(&in->lock);
+
+ int64_t stream_size = -1;
+ struct mp_tags *stream_metadata = NULL;
+ if (stream) {
+ if (do_update)
+ stream_size = stream_get_size(stream);
+ stream_control(stream, STREAM_CTRL_GET_METADATA, &stream_metadata);
+ }
+
+ mp_mutex_lock(&in->lock);
+
+ update_bytes_read(in);
+
+ if (do_update)
+ in->stream_size = stream_size;
+ if (stream_metadata) {
+ add_timed_metadata(in, stream_metadata, NULL, MP_NOPTS_VALUE);
+ talloc_free(stream_metadata);
+ }
+
+ in->next_cache_update = INT64_MAX;
+
+ if (do_update) {
+ uint64_t bytes = in->cache_unbuffered_read_bytes;
+ in->cache_unbuffered_read_bytes = 0;
+ in->last_speed_query = now;
+ double speed = bytes / (diff / (double)MP_TIME_S_TO_NS(1));
+ in->bytes_per_second = 0.5 * in->speed_query_prev_sample +
+ 0.5 * speed;
+ in->speed_query_prev_sample = speed;
+ }
+ // The idea is to update as long as there is "activity".
+ if (in->bytes_per_second)
+ in->next_cache_update = now + MP_TIME_S_TO_NS(1) + MP_TIME_US_TO_NS(1);
+}
+
+static void dumper_close(struct demux_internal *in)
+{
+ if (in->dumper)
+ mp_recorder_destroy(in->dumper);
+ in->dumper = NULL;
+ if (in->dumper_status == CONTROL_TRUE)
+ in->dumper_status = CONTROL_FALSE; // make abort equal to success
+}
+
+static int range_time_compare(const void *p1, const void *p2)
+{
+ struct demux_cached_range *r1 = *((struct demux_cached_range **)p1);
+ struct demux_cached_range *r2 = *((struct demux_cached_range **)p2);
+
+ if (r1->seek_start == r2->seek_start)
+ return 0;
+ return r1->seek_start < r2->seek_start ? -1 : 1;
+}
+
+static void dump_cache(struct demux_internal *in, double start, double end)
+{
+ in->dumper_status = in->dumper ? CONTROL_TRUE : CONTROL_ERROR;
+ if (!in->dumper)
+ return;
+
+ // (only in pathological cases there might be more ranges than allowed)
+ struct demux_cached_range *ranges[MAX_SEEK_RANGES];
+ int num_ranges = 0;
+ for (int n = 0; n < MPMIN(MP_ARRAY_SIZE(ranges), in->num_ranges); n++)
+ ranges[num_ranges++] = in->ranges[n];
+ qsort(ranges, num_ranges, sizeof(ranges[0]), range_time_compare);
+
+ for (int n = 0; n < num_ranges; n++) {
+ struct demux_cached_range *r = ranges[n];
+ if (r->seek_start == MP_NOPTS_VALUE)
+ continue;
+ if (r->seek_end <= start)
+ continue;
+ if (end != MP_NOPTS_VALUE && r->seek_start >= end)
+ continue;
+
+ mp_recorder_mark_discontinuity(in->dumper);
+
+ double pts = start;
+ int flags = 0;
+ adjust_cache_seek_target(in, r, &pts, &flags);
+
+ for (int i = 0; i < r->num_streams; i++) {
+ struct demux_queue *q = r->streams[i];
+ struct demux_stream *ds = q->ds;
+
+ ds->dump_pos = find_seek_target(q, pts, flags);
+ }
+
+ // We need to reinterleave the separate streams somehow, which makes
+ // everything more complex.
+ while (1) {
+ struct demux_packet *next = NULL;
+ double next_dts = MP_NOPTS_VALUE;
+
+ for (int i = 0; i < r->num_streams; i++) {
+ struct demux_stream *ds = r->streams[i]->ds;
+ struct demux_packet *dp = ds->dump_pos;
+
+ if (!dp)
+ continue;
+ assert(dp->stream == ds->index);
+
+ double pdts = MP_PTS_OR_DEF(dp->dts, dp->pts);
+
+ // Check for stream EOF. Note that we don't try to EOF
+ // streams at the same point (e.g. video can take longer
+ // to finish than audio, so the output file will have no
+ // audio for the last part of the video). Too much effort.
+ if (pdts != MP_NOPTS_VALUE && end != MP_NOPTS_VALUE &&
+ pdts >= end && dp->keyframe)
+ {
+ ds->dump_pos = NULL;
+ continue;
+ }
+
+ if (pdts == MP_NOPTS_VALUE || next_dts == MP_NOPTS_VALUE ||
+ pdts < next_dts)
+ {
+ next_dts = pdts;
+ next = dp;
+ }
+ }
+
+ if (!next)
+ break;
+
+ struct demux_stream *ds = in->streams[next->stream]->ds;
+ ds->dump_pos = next->next;
+
+ struct demux_packet *dp = read_packet_from_cache(in, next);
+ if (!dp) {
+ in->dumper_status = CONTROL_ERROR;
+ break;
+ }
+
+ write_dump_packet(in, dp);
+
+ talloc_free(dp);
+ }
+
+ if (in->dumper_status != CONTROL_OK)
+ break;
+ }
+
+ // (strictly speaking unnecessary; for clarity)
+ for (int n = 0; n < in->num_streams; n++)
+ in->streams[n]->ds->dump_pos = NULL;
+
+ // If dumping (in end==NOPTS mode) doesn't continue at the range that
+ // was written last, we have a discontinuity.
+ if (num_ranges && ranges[num_ranges - 1] != in->current_range)
+ mp_recorder_mark_discontinuity(in->dumper);
+
+ // end=NOPTS means the demuxer output continues to be written to the
+ // dump file.
+ if (end != MP_NOPTS_VALUE || in->dumper_status != CONTROL_OK)
+ dumper_close(in);
+}
+
+// Set the current cache dumping mode. There is only at most 1 dump process
+// active, so calling this aborts the previous dumping. Passing file==NULL
+// stops dumping.
+// This is synchronous with demux_cache_dump_get_status() (i.e. starting or
+// aborting is not asynchronous). On status change, the demuxer wakeup callback
+// is invoked (except for this call).
+// Returns whether dumping was logically started.
+bool demux_cache_dump_set(struct demuxer *demuxer, double start, double end,
+ char *file)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ bool res = false;
+
+ mp_mutex_lock(&in->lock);
+
+ start = MP_ADD_PTS(start, -in->ts_offset);
+ end = MP_ADD_PTS(end, -in->ts_offset);
+
+ dumper_close(in);
+
+ if (file && file[0] && start != MP_NOPTS_VALUE) {
+ res = true;
+
+ in->dumper = recorder_create(in, file);
+
+ // This is not asynchronous and will freeze the shit for a while if the
+ // user is unlucky. It could be moved to a thread with some effort.
+ // General idea: iterate over all cache ranges, dump what intersects.
+ // After that, and if the user requested it, make it dump all newly
+ // received packets, even if it's awkward (consider the case if the
+ // current range is not the last range).
+ dump_cache(in, start, end);
+ }
+
+ mp_mutex_unlock(&in->lock);
+
+ return res;
+}
+
+// Returns one of CONTROL_*. CONTROL_TRUE means dumping is in progress.
+int demux_cache_dump_get_status(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+ mp_mutex_lock(&in->lock);
+ int status = in->dumper_status;
+ mp_mutex_unlock(&in->lock);
+ return status;
+}
+
+// Return what range demux_cache_dump_set() would (probably) yield. This is a
+// conservative amount (in addition to internal consistency of this code, it
+// depends on what a player will do with the resulting file).
+// Use for_end==true to get the end of dumping, other the start.
+// Returns NOPTS if nothing was found.
+double demux_probe_cache_dump_target(struct demuxer *demuxer, double pts,
+ bool for_end)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ double res = MP_NOPTS_VALUE;
+ if (pts == MP_NOPTS_VALUE)
+ return pts;
+
+ mp_mutex_lock(&in->lock);
+
+ pts = MP_ADD_PTS(pts, -in->ts_offset);
+
+ // (When determining the end, look before the keyframe at pts, so subtract
+ // an arbitrary amount to round down.)
+ double seek_pts = for_end ? pts - 0.001 : pts;
+ int flags = 0;
+ struct demux_cached_range *r = find_cache_seek_range(in, seek_pts, flags);
+ if (r) {
+ if (!for_end)
+ adjust_cache_seek_target(in, r, &pts, &flags);
+
+ double t[STREAM_TYPE_COUNT];
+ for (int n = 0; n < STREAM_TYPE_COUNT; n++)
+ t[n] = MP_NOPTS_VALUE;
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ struct demux_queue *q = r->streams[n];
+
+ struct demux_packet *dp = find_seek_target(q, pts, flags);
+ if (dp) {
+ if (for_end) {
+ while (dp) {
+ double pdts = MP_PTS_OR_DEF(dp->dts, dp->pts);
+
+ if (pdts != MP_NOPTS_VALUE && pdts >= pts && dp->keyframe)
+ break;
+
+ t[ds->type] = MP_PTS_MAX(t[ds->type], pdts);
+
+ dp = dp->next;
+ }
+ } else {
+ double start;
+ compute_keyframe_times(dp, &start, NULL);
+ start = MP_PTS_MAX(start, r->seek_start);
+ t[ds->type] = MP_PTS_MAX(t[ds->type], start);
+ }
+ }
+ }
+
+ res = t[STREAM_VIDEO];
+ if (res == MP_NOPTS_VALUE)
+ res = t[STREAM_AUDIO];
+ if (res == MP_NOPTS_VALUE) {
+ for (int n = 0; n < STREAM_TYPE_COUNT; n++) {
+ res = t[n];
+ if (res != MP_NOPTS_VALUE)
+ break;
+ }
+ }
+ }
+
+ res = MP_ADD_PTS(res, in->ts_offset);
+
+ mp_mutex_unlock(&in->lock);
+
+ return res;
+}
+
+// Used by demuxers to report the amount of transferred bytes. This is for
+// streams which circumvent demuxer->stream (stream statistics are handled by
+// demux.c itself).
+void demux_report_unbuffered_read_bytes(struct demuxer *demuxer, int64_t new)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_thread);
+
+ in->slave_unbuffered_read_bytes += new;
+}
+
+// Return bytes read since last query. It's a hack because it works only if
+// the demuxer thread is disabled.
+int64_t demux_get_bytes_read_hack(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+
+ // Required because demuxer==in->d_user, and we access in->d_thread.
+ // Locking won't solve this, because we also need to access struct stream.
+ assert(!in->threading);
+
+ update_bytes_read(in);
+
+ int64_t res = in->hack_unbuffered_read_bytes;
+ in->hack_unbuffered_read_bytes = 0;
+ return res;
+}
+
+void demux_get_bitrate_stats(struct demuxer *demuxer, double *rates)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ mp_mutex_lock(&in->lock);
+
+ for (int n = 0; n < STREAM_TYPE_COUNT; n++)
+ rates[n] = -1;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ if (ds->selected && ds->bitrate >= 0)
+ rates[ds->type] = MPMAX(0, rates[ds->type]) + ds->bitrate;
+ }
+
+ mp_mutex_unlock(&in->lock);
+}
+
+void demux_get_reader_state(struct demuxer *demuxer, struct demux_reader_state *r)
+{
+ struct demux_internal *in = demuxer->in;
+ assert(demuxer == in->d_user);
+
+ mp_mutex_lock(&in->lock);
+
+ *r = (struct demux_reader_state){
+ .eof = in->eof,
+ .ts_reader = MP_NOPTS_VALUE,
+ .ts_end = MP_NOPTS_VALUE,
+ .ts_duration = -1,
+ .total_bytes = in->total_bytes,
+ .seeking = in->seeking_in_progress,
+ .low_level_seeks = in->low_level_seeks,
+ .ts_last = in->demux_ts,
+ .bytes_per_second = in->bytes_per_second,
+ .byte_level_seeks = in->byte_level_seeks,
+ .file_cache_bytes = in->cache ? demux_cache_get_size(in->cache) : -1,
+ };
+ bool any_packets = false;
+ for (int n = 0; n < in->num_streams; n++) {
+ struct demux_stream *ds = in->streams[n]->ds;
+ if (ds->eager && !(!ds->queue->head && ds->eof) && !ds->ignore_eof) {
+ r->underrun |= !ds->reader_head && !ds->eof && !ds->still_image;
+ r->ts_reader = MP_PTS_MAX(r->ts_reader, ds->base_ts);
+ r->ts_end = MP_PTS_MAX(r->ts_end, ds->queue->last_ts);
+ any_packets |= !!ds->reader_head;
+ }
+ r->fw_bytes += get_forward_buffered_bytes(ds);
+ }
+ r->idle = (!in->reading && !r->underrun) || r->eof;
+ r->underrun &= !r->idle && in->threading;
+ r->ts_reader = MP_ADD_PTS(r->ts_reader, in->ts_offset);
+ r->ts_end = MP_ADD_PTS(r->ts_end, in->ts_offset);
+ if (r->ts_reader != MP_NOPTS_VALUE && r->ts_reader <= r->ts_end)
+ r->ts_duration = r->ts_end - r->ts_reader;
+ if (in->seeking || !any_packets)
+ r->ts_duration = 0;
+ for (int n = 0; n < MPMIN(in->num_ranges, MAX_SEEK_RANGES); n++) {
+ struct demux_cached_range *range = in->ranges[n];
+ if (range->seek_start != MP_NOPTS_VALUE) {
+ r->seek_ranges[r->num_seek_ranges++] =
+ (struct demux_seek_range){
+ .start = MP_ADD_PTS(range->seek_start, in->ts_offset),
+ .end = MP_ADD_PTS(range->seek_end, in->ts_offset),
+ };
+ r->bof_cached |= range->is_bof;
+ r->eof_cached |= range->is_eof;
+ }
+ }
+
+ mp_mutex_unlock(&in->lock);
+}
+
+bool demux_cancel_test(struct demuxer *demuxer)
+{
+ return mp_cancel_test(demuxer->cancel);
+}
+
+struct demux_chapter *demux_copy_chapter_data(struct demux_chapter *c, int num)
+{
+ struct demux_chapter *new = talloc_array(NULL, struct demux_chapter, num);
+ for (int n = 0; n < num; n++) {
+ new[n] = c[n];
+ new[n].metadata = mp_tags_dup(new, new[n].metadata);
+ }
+ return new;
+}
+
+static void visit_tags(void *ctx, void (*visit)(void *ctx, void *ta, char **s),
+ struct mp_tags *tags)
+{
+ for (int n = 0; n < (tags ? tags->num_keys : 0); n++)
+ visit(ctx, tags, &tags->values[n]);
+}
+
+static void visit_meta(struct demuxer *demuxer, void *ctx,
+ void (*visit)(void *ctx, void *ta, char **s))
+{
+ struct demux_internal *in = demuxer->in;
+
+ for (int n = 0; n < in->num_streams; n++) {
+ struct sh_stream *sh = in->streams[n];
+
+ visit(ctx, sh, &sh->title);
+ visit_tags(ctx, visit, sh->tags);
+ }
+
+ for (int n = 0; n < demuxer->num_chapters; n++)
+ visit_tags(ctx, visit, demuxer->chapters[n].metadata);
+
+ visit_tags(ctx, visit, demuxer->metadata);
+}
+
+
+static void visit_detect(void *ctx, void *ta, char **s)
+{
+ char **all = ctx;
+
+ if (*s)
+ *all = talloc_asprintf_append_buffer(*all, "%s\n", *s);
+}
+
+static void visit_convert(void *ctx, void *ta, char **s)
+{
+ struct demuxer *demuxer = ctx;
+ struct demux_internal *in = demuxer->in;
+
+ if (!*s)
+ return;
+
+ bstr data = bstr0(*s);
+ bstr conv = mp_iconv_to_utf8(in->log, data, in->meta_charset,
+ MP_ICONV_VERBOSE);
+ if (conv.start && conv.start != data.start) {
+ char *ns = conv.start; // 0-termination is guaranteed
+ // (The old string might not be an alloc, but if it is, it's a talloc
+ // child, and will not leak, even if it stays allocated uselessly.)
+ *s = ns;
+ talloc_steal(ta, *s);
+ }
+}
+
+static void demux_convert_tags_charset(struct demuxer *demuxer)
+{
+ struct demux_internal *in = demuxer->in;
+
+ char *cp = demuxer->opts->meta_cp;
+ if (!cp || mp_charset_is_utf8(cp))
+ return;
+
+ char *data = talloc_strdup(NULL, "");
+ visit_meta(demuxer, &data, visit_detect);
+
+ in->meta_charset = (char *)mp_charset_guess(in, in->log, bstr0(data), cp, 0);
+ if (in->meta_charset && !mp_charset_is_utf8(in->meta_charset)) {
+ MP_INFO(demuxer, "Using tag charset: %s\n", in->meta_charset);
+ visit_meta(demuxer, demuxer, visit_convert);
+ }
+
+ talloc_free(data);
+}
+
+static bool get_demux_sub_opts(int index, const struct m_sub_options **sub)
+{
+ if (!demuxer_list[index])
+ return false;
+ *sub = demuxer_list[index]->options;
+ return true;
+}
diff --git a/demux/demux.h b/demux/demux.h
new file mode 100644
index 0000000..08904f2
--- /dev/null
+++ b/demux/demux.h
@@ -0,0 +1,361 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_DEMUXER_H
+#define MPLAYER_DEMUXER_H
+
+#include <sys/types.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/tags.h"
+#include "packet.h"
+#include "stheader.h"
+
+#define MAX_SEEK_RANGES 10
+
+struct demux_seek_range {
+ double start, end;
+};
+
+struct demux_reader_state {
+ bool eof, underrun, idle;
+ bool bof_cached, eof_cached;
+ double ts_duration;
+ double ts_reader; // approx. timerstamp of decoder position
+ double ts_end; // approx. timestamp of end of buffered range
+ int64_t total_bytes;
+ int64_t fw_bytes;
+ int64_t file_cache_bytes;
+ double seeking; // current low level seek target, or NOPTS
+ int low_level_seeks; // number of started low level seeks
+ uint64_t byte_level_seeks; // number of byte stream level seeks
+ double ts_last; // approx. timestamp of demuxer position
+ uint64_t bytes_per_second; // low level statistics
+ // Positions that can be seeked to without incurring the latency of a low
+ // level seek.
+ int num_seek_ranges;
+ struct demux_seek_range seek_ranges[MAX_SEEK_RANGES];
+};
+
+extern const struct m_sub_options demux_conf;
+
+struct demux_opts {
+ int enable_cache;
+ bool disk_cache;
+ int64_t max_bytes;
+ int64_t max_bytes_bw;
+ bool donate_fw;
+ double min_secs;
+ double hyst_secs;
+ bool force_seekable;
+ double min_secs_cache;
+ bool access_references;
+ int seekable_cache;
+ int index_mode;
+ double mf_fps;
+ char *mf_type;
+ bool create_ccs;
+ char *record_file;
+ int video_back_preroll;
+ int audio_back_preroll;
+ int back_batch[STREAM_TYPE_COUNT];
+ double back_seek_size;
+ char *meta_cp;
+ bool force_retry_eof;
+};
+
+#define SEEK_FACTOR (1 << 1) // argument is in range [0,1]
+#define SEEK_FORWARD (1 << 2) // prefer later time if not exact
+ // (if unset, prefer earlier time)
+#define SEEK_CACHED (1 << 3) // allow packet cache seeks only
+#define SEEK_SATAN (1 << 4) // enable backward demuxing
+#define SEEK_HR (1 << 5) // hr-seek (this is a weak hint only)
+#define SEEK_FORCE (1 << 6) // ignore unseekable flag
+#define SEEK_BLOCK (1 << 7) // upon successfully queued seek, block readers
+ // (simplifies syncing multiple reader threads)
+
+// Strictness of the demuxer open format check.
+// demux.c will try by default: NORMAL, UNSAFE (in this order)
+// Using "-demuxer format" will try REQUEST
+// Using "-demuxer +format" will try FORCE
+// REQUEST can be used as special value for raw demuxers which have no file
+// header check; then they should fail if check!=FORCE && check!=REQUEST.
+//
+// In general, the list is sorted from weakest check to normal check.
+// You can use relation operators to compare the check level.
+enum demux_check {
+ DEMUX_CHECK_FORCE, // force format if possible
+ DEMUX_CHECK_UNSAFE, // risky/fuzzy detection
+ DEMUX_CHECK_REQUEST,// requested by user or stream implementation
+ DEMUX_CHECK_NORMAL, // normal, safe detection
+};
+
+enum demux_event {
+ DEMUX_EVENT_INIT = 1 << 0, // complete (re-)initialization
+ DEMUX_EVENT_STREAMS = 1 << 1, // a stream was added
+ DEMUX_EVENT_METADATA = 1 << 2, // metadata or stream_metadata changed
+ DEMUX_EVENT_DURATION = 1 << 3, // duration updated
+ DEMUX_EVENT_ALL = 0xFFFF,
+};
+
+struct demuxer;
+struct timeline;
+
+/**
+ * Demuxer description structure
+ */
+typedef struct demuxer_desc {
+ const char *name; // Demuxer name, used with -demuxer switch
+ const char *desc; // Displayed to user
+
+ // If non-NULL, these are added to the global option list.
+ const struct m_sub_options *options;
+
+ // Return 0 on success, otherwise -1
+ int (*open)(struct demuxer *demuxer, enum demux_check check);
+ // The following functions are all optional
+ // Try to read a packet. Return false on EOF. If true is returned, the
+ // demuxer may set *pkt to a new packet (the reference goes to the caller).
+ // If *pkt is NULL (the value when this function is called), the call
+ // will be repeated.
+ bool (*read_packet)(struct demuxer *demuxer, struct demux_packet **pkt);
+ void (*close)(struct demuxer *demuxer);
+ void (*seek)(struct demuxer *demuxer, double rel_seek_secs, int flags);
+ void (*switched_tracks)(struct demuxer *demuxer);
+ // See timeline.c
+ void (*load_timeline)(struct timeline *tl);
+} demuxer_desc_t;
+
+typedef struct demux_chapter
+{
+ int original_index;
+ double pts;
+ struct mp_tags *metadata;
+ uint64_t demuxer_id; // for mapping to internal demuxer data structures
+} demux_chapter_t;
+
+struct demux_edition {
+ uint64_t demuxer_id;
+ bool default_edition;
+ struct mp_tags *metadata;
+};
+
+struct matroska_segment_uid {
+ unsigned char segment[16];
+ uint64_t edition;
+};
+
+struct matroska_data {
+ struct matroska_segment_uid uid;
+ // Ordered chapter information if any
+ struct matroska_chapter {
+ uint64_t start;
+ uint64_t end;
+ bool has_segment_uid;
+ struct matroska_segment_uid uid;
+ char *name;
+ } *ordered_chapters;
+ int num_ordered_chapters;
+};
+
+struct replaygain_data {
+ float track_gain;
+ float track_peak;
+ float album_gain;
+ float album_peak;
+};
+
+typedef struct demux_attachment
+{
+ char *name;
+ char *type;
+ void *data;
+ unsigned int data_size;
+} demux_attachment_t;
+
+struct demuxer_params {
+ bool is_top_level; // if true, it's not a sub-demuxer (enables cache etc.)
+ char *force_format;
+ int matroska_num_wanted_uids;
+ struct matroska_segment_uid *matroska_wanted_uids;
+ int matroska_wanted_segment;
+ bool *matroska_was_valid;
+ struct timeline *timeline;
+ bool disable_timeline;
+ bstr init_fragment;
+ bool skip_lavf_probing;
+ bool stream_record; // if true, enable stream recording if option is set
+ int stream_flags;
+ struct stream *external_stream; // if set, use this, don't open or close streams
+ // result
+ bool demuxer_failed;
+};
+
+typedef struct demuxer {
+ const demuxer_desc_t *desc; ///< Demuxer description structure
+ const char *filetype; // format name when not identified by demuxer (libavformat)
+ int64_t filepos; // input stream current pos.
+ int64_t filesize;
+ char *filename; // same as stream->url
+ bool seekable;
+ bool partially_seekable; // true if _maybe_ seekable; implies seekable=true
+ double start_time;
+ double duration; // -1 if unknown
+ // File format allows PTS resets (even if the current file is without)
+ bool ts_resets_possible;
+ // The file data was fully read, and there is no need to keep the stream
+ // open, keep the cache active, or to run the demuxer thread. Generating
+ // packets is not slow either (unlike e.g. libavdevice pseudo-demuxers).
+ // Typical examples: text subtitles, playlists
+ bool fully_read;
+ bool is_network; // opened directly from a network stream
+ bool is_streaming; // implies a "slow" input, such as network or FUSE
+ int stream_origin; // any STREAM_ORIGIN_* (set from source stream)
+ bool access_references; // allow opening other files/URLs
+
+ struct demux_opts *opts;
+ struct m_config_cache *opts_cache;
+
+ // Bitmask of DEMUX_EVENT_*
+ int events;
+
+ struct demux_edition *editions;
+ int num_editions;
+ int edition;
+
+ struct demux_chapter *chapters;
+ int num_chapters;
+
+ struct demux_attachment *attachments;
+ int num_attachments;
+
+ struct matroska_data matroska_data;
+
+ // If the file is a playlist file
+ struct playlist *playlist;
+
+ struct mp_tags *metadata;
+
+ void *priv; // demuxer-specific internal data
+ struct mpv_global *global;
+ struct mp_log *log, *glog;
+ struct demuxer_params *params;
+
+ // internal to demux.c
+ struct demux_internal *in;
+
+ // Triggered when ending demuxing forcefully. Usually bound to the stream too.
+ struct mp_cancel *cancel;
+
+ // Since the demuxer can run in its own thread, and the stream is not
+ // thread-safe, only the demuxer is allowed to access the stream directly.
+ // Also note that the stream can get replaced if fully_read is set.
+ struct stream *stream;
+} demuxer_t;
+
+void demux_free(struct demuxer *demuxer);
+void demux_cancel_and_free(struct demuxer *demuxer);
+
+struct demux_free_async_state;
+struct demux_free_async_state *demux_free_async(struct demuxer *demuxer);
+void demux_free_async_force(struct demux_free_async_state *state);
+bool demux_free_async_finish(struct demux_free_async_state *state);
+
+void demuxer_feed_caption(struct sh_stream *stream, demux_packet_t *dp);
+
+int demux_read_packet_async(struct sh_stream *sh, struct demux_packet **out_pkt);
+int demux_read_packet_async_until(struct sh_stream *sh, double min_pts,
+ struct demux_packet **out_pkt);
+bool demux_stream_is_selected(struct sh_stream *stream);
+void demux_set_stream_wakeup_cb(struct sh_stream *sh,
+ void (*cb)(void *ctx), void *ctx);
+struct demux_packet *demux_read_any_packet(struct demuxer *demuxer);
+
+struct sh_stream *demux_get_stream(struct demuxer *demuxer, int index);
+int demux_get_num_stream(struct demuxer *demuxer);
+
+struct sh_stream *demux_alloc_sh_stream(enum stream_type type);
+void demux_add_sh_stream(struct demuxer *demuxer, struct sh_stream *sh);
+
+struct mp_cancel;
+struct demuxer *demux_open_url(const char *url,
+ struct demuxer_params *params,
+ struct mp_cancel *cancel,
+ struct mpv_global *global);
+
+void demux_start_thread(struct demuxer *demuxer);
+void demux_stop_thread(struct demuxer *demuxer);
+void demux_set_wakeup_cb(struct demuxer *demuxer, void (*cb)(void *ctx), void *ctx);
+void demux_start_prefetch(struct demuxer *demuxer);
+
+bool demux_cancel_test(struct demuxer *demuxer);
+
+void demux_flush(struct demuxer *demuxer);
+int demux_seek(struct demuxer *demuxer, double rel_seek_secs, int flags);
+void demux_set_ts_offset(struct demuxer *demuxer, double offset);
+
+void demux_get_bitrate_stats(struct demuxer *demuxer, double *rates);
+void demux_get_reader_state(struct demuxer *demuxer, struct demux_reader_state *r);
+
+void demux_block_reading(struct demuxer *demuxer, bool block);
+
+void demuxer_select_track(struct demuxer *demuxer, struct sh_stream *stream,
+ double ref_pts, bool selected);
+void demuxer_refresh_track(struct demuxer *demuxer, struct sh_stream *stream,
+ double ref_pts);
+
+int demuxer_help(struct mp_log *log, const m_option_t *opt, struct bstr name);
+
+int demuxer_add_attachment(struct demuxer *demuxer, char *name,
+ char *type, void *data, size_t data_size);
+int demuxer_add_chapter(demuxer_t *demuxer, char *name,
+ double pts, uint64_t demuxer_id);
+void demux_stream_tags_changed(struct demuxer *demuxer, struct sh_stream *sh,
+ struct mp_tags *tags, double pts);
+void demux_close_stream(struct demuxer *demuxer);
+
+void demux_metadata_changed(demuxer_t *demuxer);
+void demux_update(demuxer_t *demuxer, double playback_pts);
+
+bool demux_cache_dump_set(struct demuxer *demuxer, double start, double end,
+ char *file);
+int demux_cache_dump_get_status(struct demuxer *demuxer);
+
+double demux_probe_cache_dump_target(struct demuxer *demuxer, double pts,
+ bool for_end);
+
+bool demux_is_network_cached(demuxer_t *demuxer);
+
+void demux_report_unbuffered_read_bytes(struct demuxer *demuxer, int64_t new);
+int64_t demux_get_bytes_read_hack(struct demuxer *demuxer);
+
+struct sh_stream *demuxer_stream_by_demuxer_id(struct demuxer *d,
+ enum stream_type t, int id);
+
+struct demux_chapter *demux_copy_chapter_data(struct demux_chapter *c, int num);
+
+bool demux_matroska_uid_cmp(struct matroska_segment_uid *a,
+ struct matroska_segment_uid *b);
+
+const char *stream_type_name(enum stream_type type);
+
+#endif /* MPLAYER_DEMUXER_H */
diff --git a/demux/demux_cue.c b/demux/demux_cue.c
new file mode 100644
index 0000000..4937ec9
--- /dev/null
+++ b/demux/demux_cue.c
@@ -0,0 +1,304 @@
+/*
+ * Original author: Uoti Urpala
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <dirent.h>
+#include <inttypes.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+
+#include "misc/bstr.h"
+#include "misc/charset_conv.h"
+#include "common/msg.h"
+#include "demux/demux.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "common/common.h"
+#include "stream/stream.h"
+#include "timeline.h"
+
+#include "cue.h"
+
+#define PROBE_SIZE 512
+
+const struct m_sub_options demux_cue_conf = {
+ .opts = (const m_option_t[]) {
+ {"codepage", OPT_REPLACED("metadata-codepage")},
+ {0}
+ },
+};
+
+struct priv {
+ struct cue_file *f;
+};
+
+static void add_source(struct timeline *tl, struct demuxer *d)
+{
+ MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, d);
+}
+
+static bool try_open(struct timeline *tl, char *filename)
+{
+ struct bstr bfilename = bstr0(filename);
+ // Avoid trying to open itself or another .cue file. Best would be
+ // to check the result of demuxer auto-detection, but the demuxer
+ // API doesn't allow this without opening a full demuxer.
+ if (bstr_case_endswith(bfilename, bstr0(".cue"))
+ || bstrcasecmp(bstr0(tl->demuxer->filename), bfilename) == 0)
+ return false;
+
+ struct demuxer_params p = {
+ .stream_flags = tl->stream_origin,
+ };
+
+ struct demuxer *d = demux_open_url(filename, &p, tl->cancel, tl->global);
+ // Since .bin files are raw PCM data with no headers, we have to explicitly
+ // open them. Also, try to avoid to open files that are most likely not .bin
+ // files, as that would only play noise. Checking the file extension is
+ // fragile, but it's about the only way we have.
+ // TODO: maybe also could check if the .bin file is a multiple of the Audio
+ // CD sector size (2352 bytes)
+ if (!d && bstr_case_endswith(bfilename, bstr0(".bin"))) {
+ MP_WARN(tl, "CUE: Opening as BIN file!\n");
+ p.force_format = "rawaudio";
+ d = demux_open_url(filename, &p, tl->cancel, tl->global);
+ }
+ if (d) {
+ add_source(tl, d);
+ return true;
+ }
+ MP_ERR(tl, "Could not open source '%s'!\n", filename);
+ return false;
+}
+
+static bool open_source(struct timeline *tl, char *filename)
+{
+ void *ctx = talloc_new(NULL);
+ bool res = false;
+
+ struct bstr dirname = mp_dirname(tl->demuxer->filename);
+
+ struct bstr base_filename = bstr0(mp_basename(filename));
+ if (!base_filename.len) {
+ MP_WARN(tl, "CUE: Invalid audio filename in .cue file!\n");
+ } else {
+ char *fullname = mp_path_join_bstr(ctx, dirname, base_filename);
+ if (try_open(tl, fullname)) {
+ res = true;
+ goto out;
+ }
+ }
+
+ // Try an audio file with the same name as the .cue file (but different
+ // extension).
+ // Rationale: this situation happens easily if the audio file or both files
+ // are renamed.
+
+ struct bstr cuefile =
+ bstr_strip_ext(bstr0(mp_basename(tl->demuxer->filename)));
+
+ DIR *d = opendir(bstrdup0(ctx, dirname));
+ if (!d)
+ goto out;
+ struct dirent *de;
+ while ((de = readdir(d))) {
+ char *dename0 = de->d_name;
+ struct bstr dename = bstr0(dename0);
+ if (bstr_case_startswith(dename, cuefile)) {
+ MP_WARN(tl, "CUE: No useful audio filename "
+ "in .cue file found, trying with '%s' instead!\n",
+ dename0);
+ if (try_open(tl, mp_path_join_bstr(ctx, dirname, dename))) {
+ res = true;
+ break;
+ }
+ }
+ }
+ closedir(d);
+
+out:
+ talloc_free(ctx);
+ if (!res)
+ MP_ERR(tl, "CUE: Could not open audio file!\n");
+ return res;
+}
+
+static void build_timeline(struct timeline *tl)
+{
+ struct priv *p = tl->demuxer->priv;
+
+ void *ctx = talloc_new(NULL);
+
+ add_source(tl, tl->demuxer);
+
+ struct cue_track *tracks = NULL;
+ size_t track_count = 0;
+
+ for (size_t n = 0; n < p->f->num_tracks; n++) {
+ struct cue_track *track = &p->f->tracks[n];
+ if (track->filename) {
+ MP_TARRAY_APPEND(ctx, tracks, track_count, *track);
+ } else {
+ MP_WARN(tl->demuxer, "No file specified for track entry %zd. "
+ "It will be removed\n", n + 1);
+ }
+ }
+
+ if (track_count == 0) {
+ MP_ERR(tl, "CUE: no tracks found!\n");
+ goto out;
+ }
+
+ // Remove duplicate file entries. This might be too sophisticated, since
+ // CUE files usually use either separate files for every single track, or
+ // only one file for all tracks.
+
+ char **files = 0;
+ size_t file_count = 0;
+
+ for (size_t n = 0; n < track_count; n++) {
+ struct cue_track *track = &tracks[n];
+ track->source = -1;
+ for (size_t file = 0; file < file_count; file++) {
+ if (strcmp(files[file], track->filename) == 0) {
+ track->source = file;
+ break;
+ }
+ }
+ if (track->source == -1) {
+ file_count++;
+ files = talloc_realloc(ctx, files, char *, file_count);
+ files[file_count - 1] = track->filename;
+ track->source = file_count - 1;
+ }
+ }
+
+ for (size_t i = 0; i < file_count; i++) {
+ if (!open_source(tl, files[i]))
+ goto out;
+ }
+
+ struct timeline_part *timeline = talloc_array_ptrtype(tl, timeline,
+ track_count + 1);
+ struct demux_chapter *chapters = talloc_array_ptrtype(tl, chapters,
+ track_count);
+ double starttime = 0;
+ for (int i = 0; i < track_count; i++) {
+ struct demuxer *source = tl->sources[1 + tracks[i].source];
+ double duration;
+ if (i + 1 < track_count && tracks[i].source == tracks[i + 1].source) {
+ duration = tracks[i + 1].start - tracks[i].start;
+ } else {
+ duration = source->duration;
+ // Two cases: 1) last track of a single-file cue, or 2) any track of
+ // a multi-file cue. We need to do this for 1) only because the
+ // timeline needs to be terminated with the length of the last
+ // track.
+ duration -= tracks[i].start;
+ }
+ if (duration < 0) {
+ MP_WARN(tl, "CUE: Can't get duration of source file!\n");
+ // xxx: do something more reasonable
+ duration = 0.0;
+ }
+ timeline[i] = (struct timeline_part) {
+ .start = starttime,
+ .end = starttime + duration,
+ .source_start = tracks[i].start,
+ .source = source,
+ };
+ chapters[i] = (struct demux_chapter) {
+ .pts = timeline[i].start,
+ .metadata = mp_tags_dup(tl, tracks[i].tags),
+ };
+ starttime = timeline[i].end;
+ }
+
+ struct timeline_par *par = talloc_ptrtype(tl, par);
+ *par = (struct timeline_par){
+ .parts = timeline,
+ .num_parts = track_count,
+ .track_layout = timeline[0].source,
+ };
+
+ tl->chapters = chapters;
+ tl->num_chapters = track_count;
+ MP_TARRAY_APPEND(tl, tl->pars, tl->num_pars, par);
+ tl->meta = par->track_layout;
+ tl->format = "cue";
+
+out:
+ talloc_free(ctx);
+}
+
+static int try_open_file(struct demuxer *demuxer, enum demux_check check)
+{
+ if (!demuxer->access_references)
+ return -1;
+
+ struct stream *s = demuxer->stream;
+ if (check >= DEMUX_CHECK_UNSAFE) {
+ char probe[PROBE_SIZE];
+ int len = stream_read_peek(s, probe, sizeof(probe));
+ if (len < 1 || !mp_probe_cue((bstr){probe, len}))
+ return -1;
+ }
+ struct priv *p = talloc_zero(demuxer, struct priv);
+ demuxer->priv = p;
+ demuxer->fully_read = true;
+ bstr data = stream_read_complete(s, p, 1000000);
+ if (data.start == NULL)
+ return -1;
+
+ struct demux_opts *opts = mp_get_config_group(p, demuxer->global, &demux_conf);
+ const char *charset = mp_charset_guess(p, demuxer->log, data, opts->meta_cp, 0);
+ if (charset && !mp_charset_is_utf8(charset)) {
+ MP_INFO(demuxer, "Using CUE charset: %s\n", charset);
+ bstr utf8 = mp_iconv_to_utf8(demuxer->log, data, charset, MP_ICONV_VERBOSE);
+ if (utf8.start && utf8.start != data.start) {
+ ta_steal(data.start, utf8.start);
+ data = utf8;
+ }
+ }
+ talloc_free(opts);
+
+ p->f = mp_parse_cue(data);
+ talloc_steal(p, p->f);
+ if (!p->f) {
+ MP_ERR(demuxer, "error parsing input file!\n");
+ return -1;
+ }
+
+ demux_close_stream(demuxer);
+
+ mp_tags_merge(demuxer->metadata, p->f->tags);
+ return 0;
+}
+
+const struct demuxer_desc demuxer_desc_cue = {
+ .name = "cue",
+ .desc = "CUE sheet",
+ .open = try_open_file,
+ .load_timeline = build_timeline,
+};
diff --git a/demux/demux_disc.c b/demux/demux_disc.c
new file mode 100644
index 0000000..3dfff45
--- /dev/null
+++ b/demux/demux_disc.c
@@ -0,0 +1,360 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+
+#include "stream/stream.h"
+#include "video/mp_image.h"
+#include "demux.h"
+#include "stheader.h"
+
+#include "video/csputils.h"
+
+struct priv {
+ struct demuxer *slave;
+ // streams[slave_stream_index] == our_stream
+ struct sh_stream **streams;
+ int num_streams;
+ // This contains each DVD sub stream, or NULL. Needed because DVD packets
+ // can come arbitrarily late in the MPEG stream, so the slave demuxer
+ // might add the streams only later.
+ struct sh_stream *dvd_subs[32];
+ // Used to rewrite the raw MPEG timestamps to playback time.
+ double base_time; // playback display start time of current segment
+ double base_dts; // packet DTS that maps to base_time
+ double last_dts; // DTS of previously demuxed packet
+ bool seek_reinit; // needs reinit after seek
+
+ bool is_dvd, is_cdda;
+};
+
+// If the timestamp difference between subsequent packets is this big, assume
+// a reset. It should be big enough to account for 1. low video framerates and
+// large audio frames, and 2. bad interleaving.
+#define DTS_RESET_THRESHOLD 5.0
+
+static void reselect_streams(demuxer_t *demuxer)
+{
+ struct priv *p = demuxer->priv;
+ int num_slave = demux_get_num_stream(p->slave);
+ for (int n = 0; n < MPMIN(num_slave, p->num_streams); n++) {
+ if (p->streams[n]) {
+ demuxer_select_track(p->slave, demux_get_stream(p->slave, n),
+ MP_NOPTS_VALUE, demux_stream_is_selected(p->streams[n]));
+ }
+ }
+}
+
+static void get_disc_lang(struct stream *stream, struct sh_stream *sh, bool dvd)
+{
+ struct stream_lang_req req = {.type = sh->type, .id = sh->demuxer_id};
+ if (dvd && sh->type == STREAM_SUB)
+ req.id = req.id & 0x1F; // mpeg ID to index
+ stream_control(stream, STREAM_CTRL_GET_LANG, &req);
+ if (req.name[0])
+ sh->lang = talloc_strdup(sh, req.name);
+}
+
+static void add_dvd_streams(demuxer_t *demuxer)
+{
+ struct priv *p = demuxer->priv;
+ struct stream *stream = demuxer->stream;
+ if (!p->is_dvd)
+ return;
+ struct stream_dvd_info_req info;
+ if (stream_control(stream, STREAM_CTRL_GET_DVD_INFO, &info) > 0) {
+ for (int n = 0; n < MPMIN(32, info.num_subs); n++) {
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_SUB);
+ sh->demuxer_id = n + 0x20;
+ sh->codec->codec = "dvd_subtitle";
+ get_disc_lang(stream, sh, true);
+ // p->streams _must_ match with p->slave->streams, so we can't add
+ // it yet - it has to be done when the real stream appears, which
+ // could be right on start, or any time later.
+ p->dvd_subs[n] = sh;
+
+ // emulate the extradata
+ struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS;
+ struct mp_cmat cmatrix;
+ mp_get_csp_matrix(&csp, &cmatrix);
+
+ char *s = talloc_strdup(sh, "");
+ s = talloc_asprintf_append(s, "palette: ");
+ for (int i = 0; i < 16; i++) {
+ int color = info.palette[i];
+ int y[3] = {(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff};
+ int c[3];
+ mp_map_fixp_color(&cmatrix, 8, y, 8, c);
+ color = (c[2] << 16) | (c[1] << 8) | c[0];
+
+ if (i != 0)
+ s = talloc_asprintf_append(s, ", ");
+ s = talloc_asprintf_append(s, "%06x", color);
+ }
+ s = talloc_asprintf_append(s, "\n");
+
+ sh->codec->extradata = s;
+ sh->codec->extradata_size = strlen(s);
+
+ demux_add_sh_stream(demuxer, sh);
+ }
+ }
+}
+
+static void add_streams(demuxer_t *demuxer)
+{
+ struct priv *p = demuxer->priv;
+
+ for (int n = p->num_streams; n < demux_get_num_stream(p->slave); n++) {
+ struct sh_stream *src = demux_get_stream(p->slave, n);
+ if (src->type == STREAM_SUB) {
+ struct sh_stream *sub = NULL;
+ if (src->demuxer_id >= 0x20 && src->demuxer_id <= 0x3F)
+ sub = p->dvd_subs[src->demuxer_id - 0x20];
+ if (sub) {
+ assert(p->num_streams == n); // directly mapped
+ MP_TARRAY_APPEND(p, p->streams, p->num_streams, sub);
+ continue;
+ }
+ }
+ struct sh_stream *sh = demux_alloc_sh_stream(src->type);
+ assert(p->num_streams == n); // directly mapped
+ MP_TARRAY_APPEND(p, p->streams, p->num_streams, sh);
+ // Copy all stream fields that might be relevant
+ *sh->codec = *src->codec;
+ sh->demuxer_id = src->demuxer_id;
+ if (src->type == STREAM_VIDEO) {
+ double ar;
+ if (stream_control(demuxer->stream, STREAM_CTRL_GET_ASPECT_RATIO, &ar)
+ == STREAM_OK)
+ {
+ struct mp_image_params f = {.w = src->codec->disp_w,
+ .h = src->codec->disp_h};
+ mp_image_params_set_dsize(&f, 1728 * ar, 1728);
+ sh->codec->par_w = f.p_w;
+ sh->codec->par_h = f.p_h;
+ }
+ }
+ get_disc_lang(demuxer->stream, sh, p->is_dvd);
+ demux_add_sh_stream(demuxer, sh);
+ }
+ reselect_streams(demuxer);
+}
+
+static void d_seek(demuxer_t *demuxer, double seek_pts, int flags)
+{
+ struct priv *p = demuxer->priv;
+
+ if (p->is_cdda) {
+ demux_seek(p->slave, seek_pts, flags);
+ return;
+ }
+
+ if (flags & SEEK_FACTOR) {
+ double tmp = 0;
+ stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &tmp);
+ seek_pts *= tmp;
+ }
+
+ MP_VERBOSE(demuxer, "seek to: %f\n", seek_pts);
+
+ // Supposed to induce a seek reset. Does it even work? I don't know.
+ // It will log some bogus error messages, since the demuxer will try a
+ // low level seek, which will obviously not work. But it will probably
+ // clear its internal buffers.
+ demux_seek(p->slave, 0, SEEK_FACTOR | SEEK_FORCE);
+ stream_drop_buffers(demuxer->stream);
+
+ double seek_arg[] = {seek_pts, flags};
+ stream_control(demuxer->stream, STREAM_CTRL_SEEK_TO_TIME, seek_arg);
+
+ p->seek_reinit = true;
+}
+
+static void reset_pts(demuxer_t *demuxer)
+{
+ struct priv *p = demuxer->priv;
+
+ double base;
+ if (stream_control(demuxer->stream, STREAM_CTRL_GET_CURRENT_TIME, &base) < 1)
+ base = 0;
+
+ MP_VERBOSE(demuxer, "reset to time: %f\n", base);
+
+ p->base_dts = p->last_dts = MP_NOPTS_VALUE;
+ p->base_time = base;
+ p->seek_reinit = false;
+}
+
+static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt)
+{
+ struct priv *p = demuxer->priv;
+
+ struct demux_packet *pkt = demux_read_any_packet(p->slave);
+ if (!pkt)
+ return false;
+
+ demux_update(p->slave, MP_NOPTS_VALUE);
+
+ if (p->seek_reinit)
+ reset_pts(demuxer);
+
+ add_streams(demuxer);
+ if (pkt->stream >= p->num_streams) { // out of memory?
+ talloc_free(pkt);
+ return true;
+ }
+
+ struct sh_stream *sh = p->streams[pkt->stream];
+ if (!demux_stream_is_selected(sh)) {
+ talloc_free(pkt);
+ return true;
+ }
+
+ pkt->stream = sh->index;
+
+ if (p->is_cdda) {
+ *out_pkt = pkt;
+ return true;
+ }
+
+ MP_TRACE(demuxer, "ipts: %d %f %f\n", sh->type, pkt->pts, pkt->dts);
+
+ if (sh->type == STREAM_SUB) {
+ if (p->base_dts == MP_NOPTS_VALUE)
+ MP_WARN(demuxer, "subtitle packet along PTS reset\n");
+ } else if (pkt->dts != MP_NOPTS_VALUE) {
+ // Use the very first DTS to rebase the start time of the MPEG stream
+ // to the playback time.
+ if (p->base_dts == MP_NOPTS_VALUE)
+ p->base_dts = pkt->dts;
+
+ if (p->last_dts == MP_NOPTS_VALUE)
+ p->last_dts = pkt->dts;
+
+ if (fabs(p->last_dts - pkt->dts) >= DTS_RESET_THRESHOLD) {
+ MP_WARN(demuxer, "PTS discontinuity: %f->%f\n", p->last_dts, pkt->dts);
+ p->base_time += p->last_dts - p->base_dts;
+ p->base_dts = pkt->dts - pkt->duration;
+ }
+ p->last_dts = pkt->dts;
+ }
+
+ if (p->base_dts != MP_NOPTS_VALUE) {
+ double delta = -p->base_dts + p->base_time;
+ if (pkt->pts != MP_NOPTS_VALUE)
+ pkt->pts += delta;
+ if (pkt->dts != MP_NOPTS_VALUE)
+ pkt->dts += delta;
+ }
+
+ MP_TRACE(demuxer, "opts: %d %f %f\n", sh->type, pkt->pts, pkt->dts);
+
+ *out_pkt = pkt;
+ return 1;
+}
+
+static void add_stream_chapters(struct demuxer *demuxer)
+{
+ int num = 0;
+ if (stream_control(demuxer->stream, STREAM_CTRL_GET_NUM_CHAPTERS, &num) < 1)
+ return;
+ for (int n = 0; n < num; n++) {
+ double p = n;
+ if (stream_control(demuxer->stream, STREAM_CTRL_GET_CHAPTER_TIME, &p) < 1)
+ continue;
+ demuxer_add_chapter(demuxer, "", p, 0);
+ }
+}
+
+static int d_open(demuxer_t *demuxer, enum demux_check check)
+{
+ struct priv *p = demuxer->priv = talloc_zero(demuxer, struct priv);
+
+ if (check != DEMUX_CHECK_FORCE)
+ return -1;
+
+ struct demuxer_params params = {
+ .force_format = "+lavf",
+ .external_stream = demuxer->stream,
+ .stream_flags = demuxer->stream_origin,
+ };
+
+ struct stream *cur = demuxer->stream;
+ const char *sname = "";
+ if (cur->info)
+ sname = cur->info->name;
+
+ p->is_cdda = strcmp(sname, "cdda") == 0;
+ p->is_dvd = strcmp(sname, "dvd") == 0 ||
+ strcmp(sname, "ifo") == 0 ||
+ strcmp(sname, "dvdnav") == 0 ||
+ strcmp(sname, "ifo_dvdnav") == 0;
+
+ if (p->is_cdda)
+ params.force_format = "+rawaudio";
+
+ char *t = NULL;
+ stream_control(demuxer->stream, STREAM_CTRL_GET_DISC_NAME, &t);
+ if (t) {
+ mp_tags_set_str(demuxer->metadata, "TITLE", t);
+ talloc_free(t);
+ }
+
+ // Initialize the playback time. We need to read _some_ data to get the
+ // correct stream-layer time (at least with libdvdnav).
+ stream_read_peek(demuxer->stream, &(char){0}, 1);
+ reset_pts(demuxer);
+
+ p->slave = demux_open_url("-", &params, demuxer->cancel, demuxer->global);
+ if (!p->slave)
+ return -1;
+
+ // Can be seekable even if the stream isn't.
+ demuxer->seekable = true;
+
+ add_dvd_streams(demuxer);
+ add_streams(demuxer);
+ add_stream_chapters(demuxer);
+
+ double len;
+ if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1)
+ demuxer->duration = len;
+
+ return 0;
+}
+
+static void d_close(demuxer_t *demuxer)
+{
+ struct priv *p = demuxer->priv;
+ demux_free(p->slave);
+}
+
+const demuxer_desc_t demuxer_desc_disc = {
+ .name = "disc",
+ .desc = "CD/DVD/BD wrapper",
+ .read_packet = d_read_packet,
+ .open = d_open,
+ .close = d_close,
+ .seek = d_seek,
+ .switched_tracks = reselect_streams,
+};
diff --git a/demux/demux_edl.c b/demux/demux_edl.c
new file mode 100644
index 0000000..356b7ee
--- /dev/null
+++ b/demux/demux_edl.c
@@ -0,0 +1,651 @@
+/*
+ * Original author: Uoti Urpala
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+
+#include "mpv_talloc.h"
+
+#include "demux.h"
+#include "timeline.h"
+#include "common/msg.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/tags.h"
+#include "stream/stream.h"
+
+#define HEADER "# mpv EDL v0\n"
+
+struct tl_part {
+ char *filename; // what is stream_open()ed
+ double offset; // offset into the source file
+ bool offset_set;
+ bool chapter_ts;
+ bool is_layout;
+ double length; // length of the part (-1 if rest of the file)
+ char *title;
+};
+
+struct tl_parts {
+ bool disable_chapters;
+ bool dash, no_clip, delay_open;
+ char *init_fragment_url;
+ struct sh_stream **sh_meta;
+ int num_sh_meta;
+ struct tl_part *parts;
+ int num_parts;
+ struct tl_parts *next;
+};
+
+struct tl_root {
+ struct tl_parts **pars;
+ int num_pars;
+ struct mp_tags *tags;
+};
+
+struct priv {
+ bstr data;
+};
+
+// Static allocation out of laziness.
+#define NUM_MAX_PARAMS 20
+
+struct parse_ctx {
+ struct mp_log *log;
+ bool error;
+ bstr param_vals[NUM_MAX_PARAMS];
+ bstr param_names[NUM_MAX_PARAMS];
+ int num_params;
+};
+
+// This returns a value with bstr.start==NULL if nothing found. If the parameter
+// was specified, bstr.str!=NULL, even if the string is empty (bstr.len==0).
+// The parameter is removed from the list if found.
+static bstr get_param(struct parse_ctx *ctx, const char *name)
+{
+ bstr bname = bstr0(name);
+ for (int n = 0; n < ctx->num_params; n++) {
+ if (bstr_equals(ctx->param_names[n], bname)) {
+ bstr res = ctx->param_vals[n];
+ int count = ctx->num_params;
+ MP_TARRAY_REMOVE_AT(ctx->param_names, count, n);
+ count = ctx->num_params;
+ MP_TARRAY_REMOVE_AT(ctx->param_vals, count, n);
+ ctx->num_params -= 1;
+ if (!res.start)
+ res = bstr0(""); // keep guarantees
+ return res;
+ }
+ }
+ return (bstr){0};
+}
+
+// Same as get_param(), but return C string. Return NULL if missing.
+static char *get_param0(struct parse_ctx *ctx, void *ta_ctx, const char *name)
+{
+ return bstrdup0(ta_ctx, get_param(ctx, name));
+}
+
+// Optional int parameter. Returns the parsed integer, or def if the parameter
+// is missing or on error (sets ctx.error on error).
+static int get_param_int(struct parse_ctx *ctx, const char *name, int def)
+{
+ bstr val = get_param(ctx, name);
+ if (val.start) {
+ bstr rest;
+ long long ival = bstrtoll(val, &rest, 0);
+ if (!val.len || rest.len || ival < INT_MIN || ival > INT_MAX) {
+ MP_ERR(ctx, "Invalid integer: '%.*s'\n", BSTR_P(val));
+ ctx->error = true;
+ return def;
+ }
+ return ival;
+ }
+ return def;
+}
+
+// Optional time parameter. Currently a number.
+// Returns true: parameter was present and valid, *t is set
+// Returns false: parameter was not present (or broken => ctx.error set)
+static bool get_param_time(struct parse_ctx *ctx, const char *name, double *t)
+{
+ bstr val = get_param(ctx, name);
+ if (val.start) {
+ bstr rest;
+ double time = bstrtod(val, &rest);
+ if (!val.len || rest.len || !isfinite(time)) {
+ MP_ERR(ctx, "Invalid time string: '%.*s'\n", BSTR_P(val));
+ ctx->error = true;
+ return false;
+ }
+ *t = time;
+ return true;
+ }
+ return false;
+}
+
+static struct tl_parts *add_part(struct tl_root *root)
+{
+ struct tl_parts *tl = talloc_zero(root, struct tl_parts);
+ MP_TARRAY_APPEND(root, root->pars, root->num_pars, tl);
+ return tl;
+}
+
+static struct sh_stream *get_meta(struct tl_parts *tl, int index)
+{
+ for (int n = 0; n < tl->num_sh_meta; n++) {
+ if (tl->sh_meta[n]->index == index)
+ return tl->sh_meta[n];
+ }
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_TYPE_COUNT);
+ talloc_steal(tl, sh);
+ MP_TARRAY_APPEND(tl, tl->sh_meta, tl->num_sh_meta, sh);
+ return sh;
+}
+
+/* Returns a list of parts, or NULL on parse error.
+ * Syntax (without file header or URI prefix):
+ * url ::= <entry> ( (';' | '\n') <entry> )*
+ * entry ::= <param> ( <param> ',' )*
+ * param ::= [<string> '='] (<string> | '%' <number> '%' <bytes>)
+ */
+static struct tl_root *parse_edl(bstr str, struct mp_log *log)
+{
+ struct tl_root *root = talloc_zero(NULL, struct tl_root);
+ root->tags = talloc_zero(root, struct mp_tags);
+ struct tl_parts *tl = add_part(root);
+ while (str.len) {
+ if (bstr_eatstart0(&str, "#")) {
+ bstr_split_tok(str, "\n", &(bstr){0}, &str);
+ continue;
+ }
+ if (bstr_eatstart0(&str, "\n") || bstr_eatstart0(&str, ";"))
+ continue;
+ bool is_header = bstr_eatstart0(&str, "!");
+ struct parse_ctx ctx = { .log = log };
+ int nparam = 0;
+ while (1) {
+ bstr name, val;
+ // Check if it's of the form "name=..."
+ int next = bstrcspn(str, "=%,;\n");
+ if (next > 0 && next < str.len && str.start[next] == '=') {
+ name = bstr_splice(str, 0, next);
+ str = bstr_cut(str, next + 1);
+ } else if (is_header) {
+ const char *names[] = {"type"}; // implied name
+ name = bstr0(nparam < 1 ? names[nparam] : "-");
+ } else {
+ const char *names[] = {"file", "start", "length"}; // implied name
+ name = bstr0(nparam < 3 ? names[nparam] : "-");
+ }
+ if (bstr_eatstart0(&str, "%")) {
+ int len = bstrtoll(str, &str, 0);
+ if (!bstr_startswith0(str, "%") || (len > str.len - 1))
+ goto error;
+ val = bstr_splice(str, 1, len + 1);
+ str = bstr_cut(str, len + 1);
+ } else {
+ next = bstrcspn(str, ",;\n");
+ val = bstr_splice(str, 0, next);
+ str = bstr_cut(str, next);
+ }
+ if (ctx.num_params >= NUM_MAX_PARAMS) {
+ mp_err(log, "Too many parameters, ignoring '%.*s'.\n",
+ BSTR_P(name));
+ } else {
+ ctx.param_names[ctx.num_params] = name;
+ ctx.param_vals[ctx.num_params] = val;
+ ctx.num_params += 1;
+ }
+ nparam++;
+ if (!bstr_eatstart0(&str, ","))
+ break;
+ }
+ if (is_header) {
+ bstr f_type = get_param(&ctx, "type");
+ if (bstr_equals0(f_type, "mp4_dash")) {
+ tl->dash = true;
+ tl->init_fragment_url = get_param0(&ctx, tl, "init");
+ } else if (bstr_equals0(f_type, "no_clip")) {
+ tl->no_clip = true;
+ } else if (bstr_equals0(f_type, "new_stream")) {
+ // (Special case: ignore "redundant" headers at the start for
+ // general symmetry.)
+ if (root->num_pars > 1 || tl->num_parts)
+ tl = add_part(root);
+ } else if (bstr_equals0(f_type, "no_chapters")) {
+ tl->disable_chapters = true;
+ } else if (bstr_equals0(f_type, "track_meta")) {
+ int index = get_param_int(&ctx, "index", -1);
+ struct sh_stream *sh = index < 0 && tl->num_sh_meta
+ ? tl->sh_meta[tl->num_sh_meta - 1]
+ : get_meta(tl, index);
+ sh->lang = get_param0(&ctx, sh, "lang");
+ sh->title = get_param0(&ctx, sh, "title");
+ sh->hls_bitrate = get_param_int(&ctx, "byterate", 0) * 8;
+ bstr flags = get_param(&ctx, "flags");
+ bstr flag;
+ while (bstr_split_tok(flags, "+", &flag, &flags) || flag.len) {
+ if (bstr_equals0(flag, "default")) {
+ sh->default_track = true;
+ } else if (bstr_equals0(flag, "forced")) {
+ sh->forced_track = true;
+ } else {
+ mp_warn(log, "Unknown flag: '%.*s'\n", BSTR_P(flag));
+ }
+ }
+ } else if (bstr_equals0(f_type, "delay_open")) {
+ struct sh_stream *sh = get_meta(tl, tl->num_sh_meta);
+ bstr mt = get_param(&ctx, "media_type");
+ if (bstr_equals0(mt, "video")) {
+ sh->type = sh->codec->type = STREAM_VIDEO;
+ } else if (bstr_equals0(mt, "audio")) {
+ sh->type = sh->codec->type = STREAM_AUDIO;
+ } else if (bstr_equals0(mt, "sub")) {
+ sh->type = sh->codec->type = STREAM_SUB;
+ } else {
+ mp_err(log, "Invalid or missing !delay_open media type.\n");
+ goto error;
+ }
+ sh->codec->codec = get_param0(&ctx, sh, "codec");
+ if (!sh->codec->codec)
+ sh->codec->codec = "null";
+ sh->codec->disp_w = get_param_int(&ctx, "w", 0);
+ sh->codec->disp_h = get_param_int(&ctx, "h", 0);
+ sh->codec->fps = get_param_int(&ctx, "fps", 0);
+ sh->codec->samplerate = get_param_int(&ctx, "samplerate", 0);
+ tl->delay_open = true;
+ } else if (bstr_equals0(f_type, "global_tags")) {
+ for (int n = 0; n < ctx.num_params; n++) {
+ mp_tags_set_bstr(root->tags, ctx.param_names[n],
+ ctx.param_vals[n]);
+ }
+ ctx.num_params = 0;
+ } else {
+ mp_err(log, "Unknown header: '%.*s'\n", BSTR_P(f_type));
+ goto error;
+ }
+ } else {
+ struct tl_part p = { .length = -1 };
+ p.filename = get_param0(&ctx, tl, "file");
+ p.offset_set = get_param_time(&ctx, "start", &p.offset);
+ get_param_time(&ctx, "length", &p.length);
+ bstr ts = get_param(&ctx, "timestamps");
+ if (bstr_equals0(ts, "chapters")) {
+ p.chapter_ts = true;
+ } else if (ts.start && !bstr_equals0(ts, "seconds")) {
+ mp_warn(log, "Unknown timestamp type: '%.*s'\n", BSTR_P(ts));
+ }
+ p.title = get_param0(&ctx, tl, "title");
+ bstr layout = get_param(&ctx, "layout");
+ if (layout.start) {
+ if (bstr_equals0(layout, "this")) {
+ p.is_layout = true;
+ } else {
+ mp_warn(log, "Unknown layout param: '%.*s'\n", BSTR_P(layout));
+ }
+ }
+ if (!p.filename) {
+ mp_err(log, "Missing filename in segment.'\n");
+ goto error;
+ }
+ MP_TARRAY_APPEND(tl, tl->parts, tl->num_parts, p);
+ }
+ if (ctx.error)
+ goto error;
+ for (int n = 0; n < ctx.num_params; n++) {
+ mp_warn(log, "Unknown or duplicate parameter: '%.*s'\n",
+ BSTR_P(ctx.param_names[n]));
+ }
+ }
+ assert(root->num_pars);
+ for (int n = 0; n < root->num_pars; n++) {
+ if (root->pars[n]->num_parts < 1) {
+ mp_err(log, "EDL specifies no segments.'\n");
+ goto error;
+ }
+ }
+ return root;
+error:
+ mp_err(log, "EDL parsing failed.\n");
+ talloc_free(root);
+ return NULL;
+}
+
+static struct demuxer *open_source(struct timeline *root,
+ struct timeline_par *tl, char *filename)
+{
+ for (int n = 0; n < tl->num_parts; n++) {
+ struct demuxer *d = tl->parts[n].source;
+ if (d && d->filename && strcmp(d->filename, filename) == 0)
+ return d;
+ }
+ struct demuxer_params params = {
+ .init_fragment = tl->init_fragment,
+ .stream_flags = root->stream_origin,
+ };
+ struct demuxer *d = demux_open_url(filename, &params, root->cancel,
+ root->global);
+ if (d) {
+ MP_TARRAY_APPEND(root, root->sources, root->num_sources, d);
+ } else {
+ MP_ERR(root, "EDL: Could not open source file '%s'.\n", filename);
+ }
+ return d;
+}
+
+static double demuxer_chapter_time(struct demuxer *demuxer, int n)
+{
+ if (n < 0 || n >= demuxer->num_chapters)
+ return -1;
+ return demuxer->chapters[n].pts;
+}
+
+// Append all chapters from src to the chapters array.
+// Ignore chapters outside of the given time range.
+static void copy_chapters(struct demux_chapter **chapters, int *num_chapters,
+ struct demuxer *src, double start, double len,
+ double dest_offset)
+{
+ for (int n = 0; n < src->num_chapters; n++) {
+ double time = demuxer_chapter_time(src, n);
+ if (time >= start && time <= start + len) {
+ struct demux_chapter ch = {
+ .pts = dest_offset + time - start,
+ .metadata = mp_tags_dup(*chapters, src->chapters[n].metadata),
+ };
+ MP_TARRAY_APPEND(NULL, *chapters, *num_chapters, ch);
+ }
+ }
+}
+
+static void resolve_timestamps(struct tl_part *part, struct demuxer *demuxer)
+{
+ if (part->chapter_ts) {
+ double start = demuxer_chapter_time(demuxer, part->offset);
+ double length = part->length;
+ double end = length;
+ if (end >= 0)
+ end = demuxer_chapter_time(demuxer, part->offset + part->length);
+ if (end >= 0 && start >= 0)
+ length = end - start;
+ part->offset = start;
+ part->length = length;
+ }
+ if (!part->offset_set)
+ part->offset = demuxer->start_time;
+}
+
+static struct timeline_par *build_timeline(struct timeline *root,
+ struct tl_root *edl_root,
+ struct tl_parts *parts)
+{
+ struct timeline_par *tl = talloc_zero(root, struct timeline_par);
+ MP_TARRAY_APPEND(root, root->pars, root->num_pars, tl);
+
+ tl->track_layout = NULL;
+ tl->dash = parts->dash;
+ tl->no_clip = parts->no_clip;
+ tl->delay_open = parts->delay_open;
+
+ // There is no copy function for sh_stream, so just steal it.
+ for (int n = 0; n < parts->num_sh_meta; n++) {
+ MP_TARRAY_APPEND(tl, tl->sh_meta, tl->num_sh_meta,
+ talloc_steal(tl, parts->sh_meta[n]));
+ parts->sh_meta[n] = NULL;
+ }
+ parts->num_sh_meta = 0;
+
+ if (parts->init_fragment_url && parts->init_fragment_url[0]) {
+ MP_VERBOSE(root, "Opening init fragment...\n");
+ stream_t *s = stream_create(parts->init_fragment_url,
+ STREAM_READ | root->stream_origin,
+ root->cancel, root->global);
+ if (s) {
+ root->is_network |= s->is_network;
+ root->is_streaming |= s->streaming;
+ tl->init_fragment = stream_read_complete(s, tl, 1000000);
+ }
+ free_stream(s);
+ if (!tl->init_fragment.len) {
+ MP_ERR(root, "Could not read init fragment.\n");
+ goto error;
+ }
+ struct demuxer_params params = {
+ .init_fragment = tl->init_fragment,
+ .stream_flags = root->stream_origin,
+ };
+ tl->track_layout = demux_open_url("memory://", &params, root->cancel,
+ root->global);
+ if (!tl->track_layout) {
+ MP_ERR(root, "Could not demux init fragment.\n");
+ goto error;
+ }
+ MP_TARRAY_APPEND(root, root->sources, root->num_sources, tl->track_layout);
+ }
+
+ tl->parts = talloc_array_ptrtype(tl, tl->parts, parts->num_parts);
+ double starttime = 0;
+ for (int n = 0; n < parts->num_parts; n++) {
+ struct tl_part *part = &parts->parts[n];
+ struct demuxer *source = NULL;
+
+ if (tl->dash) {
+ part->offset = starttime;
+ if (part->length <= 0)
+ MP_WARN(root, "Segment %d has unknown duration.\n", n);
+ if (part->offset_set)
+ MP_WARN(root, "Offsets are ignored.\n");
+
+ if (!tl->track_layout)
+ tl->track_layout = open_source(root, tl, part->filename);
+ } else if (tl->delay_open) {
+ if (n == 0 && !part->offset_set) {
+ part->offset = starttime;
+ part->offset_set = true;
+ }
+ if (part->chapter_ts || (part->length < 0 && !tl->no_clip)) {
+ MP_ERR(root, "Invalid specification for delay_open stream.\n");
+ goto error;
+ }
+ } else {
+ MP_VERBOSE(root, "Opening segment %d...\n", n);
+
+ source = open_source(root, tl, part->filename);
+ if (!source)
+ goto error;
+
+ resolve_timestamps(part, source);
+
+ double end_time = source->duration;
+ if (end_time >= 0)
+ end_time += source->start_time;
+
+ // Unknown length => use rest of the file. If duration is unknown, make
+ // something up.
+ if (part->length < 0) {
+ if (end_time < 0) {
+ MP_WARN(root, "EDL: source file '%s' has unknown duration.\n",
+ part->filename);
+ end_time = 1;
+ }
+ part->length = end_time - part->offset;
+ } else if (end_time >= 0) {
+ double end_part = part->offset + part->length;
+ if (end_part > end_time) {
+ MP_WARN(root, "EDL: entry %d uses %f "
+ "seconds, but file has only %f seconds.\n",
+ n, end_part, end_time);
+ }
+ }
+
+ if (!parts->disable_chapters) {
+ // Add a chapter between each file.
+ struct demux_chapter ch = {
+ .pts = starttime,
+ .metadata = talloc_zero(tl, struct mp_tags),
+ };
+ mp_tags_set_str(ch.metadata, "title",
+ part->title ? part->title : part->filename);
+ MP_TARRAY_APPEND(root, root->chapters, root->num_chapters, ch);
+
+ // Also copy the source file's chapters for the relevant parts
+ copy_chapters(&root->chapters, &root->num_chapters, source,
+ part->offset, part->length, starttime);
+ }
+ }
+
+ tl->parts[n] = (struct timeline_part) {
+ .start = starttime,
+ .end = starttime + part->length,
+ .source_start = part->offset,
+ .source = source,
+ .url = talloc_strdup(tl, part->filename),
+ };
+
+ starttime = tl->parts[n].end;
+
+ if (source && !tl->track_layout && part->is_layout)
+ tl->track_layout = source;
+
+ tl->num_parts++;
+ }
+
+ if (tl->no_clip && tl->num_parts > 1)
+ MP_WARN(root, "Multiple parts with no_clip. Undefined behavior ahead.\n");
+
+ if (!tl->track_layout) {
+ // Use a heuristic to select the "broadest" part as layout.
+ for (int n = 0; n < parts->num_parts; n++) {
+ struct demuxer *s = tl->parts[n].source;
+ if (!s)
+ continue;
+ if (!tl->track_layout ||
+ demux_get_num_stream(s) > demux_get_num_stream(tl->track_layout))
+ tl->track_layout = s;
+ }
+ }
+
+ if (!tl->track_layout && !tl->delay_open)
+ goto error;
+ if (!root->meta)
+ root->meta = tl->track_layout;
+
+ // Not very sane, since demuxer fields are supposed to be treated read-only
+ // from outside, but happens to work in this case, so who cares.
+ if (root->meta)
+ mp_tags_merge(root->meta->metadata, edl_root->tags);
+
+ assert(tl->num_parts == parts->num_parts);
+ return tl;
+
+error:
+ root->num_pars = 0;
+ return NULL;
+}
+
+static void fix_filenames(struct tl_parts *parts, char *source_path)
+{
+ if (bstr_equals0(mp_split_proto(bstr0(source_path), NULL), "edl"))
+ return;
+ struct bstr dirname = mp_dirname(source_path);
+ for (int n = 0; n < parts->num_parts; n++) {
+ struct tl_part *part = &parts->parts[n];
+ if (!mp_is_url(bstr0(part->filename))) {
+ part->filename =
+ mp_path_join_bstr(parts, dirname, bstr0(part->filename));
+ }
+ }
+}
+
+static void build_mpv_edl_timeline(struct timeline *tl)
+{
+ struct priv *p = tl->demuxer->priv;
+
+ struct tl_root *root = parse_edl(p->data, tl->log);
+ if (!root) {
+ MP_ERR(tl, "Error in EDL.\n");
+ return;
+ }
+
+ bool all_dash = true;
+ bool all_no_clip = true;
+ bool all_single = true;
+
+ for (int n = 0; n < root->num_pars; n++) {
+ struct tl_parts *parts = root->pars[n];
+ fix_filenames(parts, tl->demuxer->filename);
+ struct timeline_par *par = build_timeline(tl, root, parts);
+ if (!par)
+ break;
+ all_dash &= par->dash;
+ all_no_clip &= par->no_clip;
+ all_single &= par->num_parts == 1;
+ }
+
+ if (all_dash) {
+ tl->format = "dash";
+ } else if (all_no_clip && all_single) {
+ tl->format = "multi";
+ } else {
+ tl->format = "edl";
+ }
+
+ talloc_free(root);
+}
+
+static int try_open_file(struct demuxer *demuxer, enum demux_check check)
+{
+ if (!demuxer->access_references)
+ return -1;
+
+ struct priv *p = talloc_zero(demuxer, struct priv);
+ demuxer->priv = p;
+ demuxer->fully_read = true;
+
+ struct stream *s = demuxer->stream;
+ if (s->info && strcmp(s->info->name, "edl") == 0) {
+ p->data = bstr0(s->path);
+ return 0;
+ }
+ if (check >= DEMUX_CHECK_UNSAFE) {
+ char header[sizeof(HEADER) - 1];
+ int len = stream_read_peek(s, header, sizeof(header));
+ if (len != strlen(HEADER) || memcmp(header, HEADER, len) != 0)
+ return -1;
+ }
+ p->data = stream_read_complete(s, demuxer, 1000000);
+ if (p->data.start == NULL)
+ return -1;
+ bstr_eatstart0(&p->data, HEADER);
+ demux_close_stream(demuxer);
+ return 0;
+}
+
+const struct demuxer_desc demuxer_desc_edl = {
+ .name = "edl",
+ .desc = "Edit decision list",
+ .open = try_open_file,
+ .load_timeline = build_mpv_edl_timeline,
+};
diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c
new file mode 100644
index 0000000..663fab4
--- /dev/null
+++ b/demux/demux_lavf.c
@@ -0,0 +1,1448 @@
+/*
+ * Copyright (C) 2004 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <string.h>
+#include <strings.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "config.h"
+
+#include <libavformat/avformat.h>
+#include <libavformat/avio.h>
+#include <libavutil/avutil.h>
+#include <libavutil/avstring.h>
+#include <libavutil/mathematics.h>
+#include <libavutil/replaygain.h>
+#include <libavutil/display.h>
+#include <libavutil/opt.h>
+
+#include <libavutil/dovi_meta.h>
+
+#include "audio/chmap_avchannel.h"
+
+#include "common/msg.h"
+#include "common/tags.h"
+#include "common/av_common.h"
+#include "misc/bstr.h"
+#include "misc/charset_conv.h"
+#include "misc/thread_tools.h"
+
+#include "stream/stream.h"
+#include "demux.h"
+#include "stheader.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/path.h"
+
+#ifndef AV_DISPOSITION_TIMED_THUMBNAILS
+#define AV_DISPOSITION_TIMED_THUMBNAILS 0
+#endif
+#ifndef AV_DISPOSITION_STILL_IMAGE
+#define AV_DISPOSITION_STILL_IMAGE 0
+#endif
+
+#define INITIAL_PROBE_SIZE STREAM_BUFFER_SIZE
+#define PROBE_BUF_SIZE (10 * 1024 * 1024)
+
+
+// Should correspond to IO_BUFFER_SIZE in libavformat/aviobuf.c (not public)
+// libavformat (almost) always reads data in blocks of this size.
+#define BIO_BUFFER_SIZE 32768
+
+#define OPT_BASE_STRUCT struct demux_lavf_opts
+struct demux_lavf_opts {
+ int probesize;
+ int probeinfo;
+ int probescore;
+ float analyzeduration;
+ int buffersize;
+ bool allow_mimetype;
+ char *format;
+ char **avopts;
+ bool hacks;
+ char *sub_cp;
+ int rtsp_transport;
+ int linearize_ts;
+ bool propagate_opts;
+};
+
+const struct m_sub_options demux_lavf_conf = {
+ .opts = (const m_option_t[]) {
+ {"demuxer-lavf-probesize", OPT_INT(probesize), M_RANGE(32, INT_MAX)},
+ {"demuxer-lavf-probe-info", OPT_CHOICE(probeinfo,
+ {"no", 0}, {"yes", 1}, {"auto", -1}, {"nostreams", -2})},
+ {"demuxer-lavf-format", OPT_STRING(format)},
+ {"demuxer-lavf-analyzeduration", OPT_FLOAT(analyzeduration),
+ M_RANGE(0, 3600)},
+ {"demuxer-lavf-buffersize", OPT_INT(buffersize),
+ M_RANGE(1, 10 * 1024 * 1024), OPTDEF_INT(BIO_BUFFER_SIZE)},
+ {"demuxer-lavf-allow-mimetype", OPT_BOOL(allow_mimetype)},
+ {"demuxer-lavf-probescore", OPT_INT(probescore),
+ M_RANGE(1, AVPROBE_SCORE_MAX)},
+ {"demuxer-lavf-hacks", OPT_BOOL(hacks)},
+ {"demuxer-lavf-o", OPT_KEYVALUELIST(avopts)},
+ {"sub-codepage", OPT_STRING(sub_cp)},
+ {"rtsp-transport", OPT_CHOICE(rtsp_transport,
+ {"lavf", 0},
+ {"udp", 1},
+ {"tcp", 2},
+ {"http", 3},
+ {"udp_multicast", 4})},
+ {"demuxer-lavf-linearize-timestamps", OPT_CHOICE(linearize_ts,
+ {"no", 0}, {"auto", -1}, {"yes", 1})},
+ {"demuxer-lavf-propagate-opts", OPT_BOOL(propagate_opts)},
+ {0}
+ },
+ .size = sizeof(struct demux_lavf_opts),
+ .defaults = &(const struct demux_lavf_opts){
+ .probeinfo = -1,
+ .allow_mimetype = true,
+ .hacks = true,
+ // AVPROBE_SCORE_MAX/4 + 1 is the "recommended" limit. Below that, the
+ // user is supposed to retry with larger probe sizes until a higher
+ // value is reached.
+ .probescore = AVPROBE_SCORE_MAX/4 + 1,
+ .sub_cp = "auto",
+ .rtsp_transport = 2,
+ .linearize_ts = -1,
+ .propagate_opts = true,
+ },
+};
+
+struct format_hack {
+ const char *ff_name;
+ const char *mime_type;
+ int probescore;
+ float analyzeduration;
+ bool skipinfo : 1; // skip avformat_find_stream_info()
+ unsigned int if_flags; // additional AVInputFormat.flags flags
+ bool max_probe : 1; // use probescore only if max. probe size reached
+ bool ignore : 1; // blacklisted
+ bool no_stream : 1; // do not wrap struct stream as AVIOContext
+ bool use_stream_ids : 1; // has a meaningful native stream IDs (export it)
+ bool fully_read : 1; // set demuxer.fully_read flag
+ bool detect_charset : 1; // format is a small text file, possibly not UTF8
+ // Do not confuse player's position estimation (position is into external
+ // segment, with e.g. HLS, player knows about the playlist main file only).
+ bool clear_filepos : 1;
+ bool linearize_audio_ts : 1;// compensate timestamp resets (audio only)
+ bool fix_editlists : 1;
+ bool is_network : 1;
+ bool no_seek : 1;
+ bool no_pcm_seek : 1;
+ bool no_seek_on_no_duration : 1;
+ bool readall_on_no_streamseek : 1;
+};
+
+#define BLACKLIST(fmt) {fmt, .ignore = true}
+#define TEXTSUB(fmt) {fmt, .fully_read = true, .detect_charset = true}
+#define TEXTSUB_UTF8(fmt) {fmt, .fully_read = true}
+
+static const struct format_hack format_hacks[] = {
+ // for webradios
+ {"aac", "audio/aacp", 25, 0.5},
+ {"aac", "audio/aac", 25, 0.5},
+
+ // some mp3 files don't detect correctly (usually id3v2 too large)
+ {"mp3", "audio/mpeg", 24, 0.5},
+ {"mp3", NULL, 24, .max_probe = true},
+
+ {"hls", .no_stream = true, .clear_filepos = true},
+ {"dash", .no_stream = true, .clear_filepos = true},
+ {"sdp", .clear_filepos = true, .is_network = true, .no_seek = true},
+ {"mpeg", .use_stream_ids = true},
+ {"mpegts", .use_stream_ids = true},
+ {"mxf", .use_stream_ids = true},
+ {"avi", .use_stream_ids = true},
+ {"asf", .use_stream_ids = true},
+ {"mp4", .skipinfo = true, .fix_editlists = true, .no_pcm_seek = true,
+ .use_stream_ids = true},
+ {"matroska", .skipinfo = true, .no_pcm_seek = true, .use_stream_ids = true},
+
+ {"v4l2", .no_seek = true},
+ {"rtsp", .no_seek_on_no_duration = true},
+
+ // In theory, such streams might contain timestamps, but virtually none do.
+ {"h264", .if_flags = AVFMT_NOTIMESTAMPS },
+ {"hevc", .if_flags = AVFMT_NOTIMESTAMPS },
+
+ // Some Ogg shoutcast streams are essentially concatenated OGG files. They
+ // reset timestamps, which causes all sorts of problems.
+ {"ogg", .linearize_audio_ts = true, .use_stream_ids = true},
+
+ // At some point, FFmpeg lost the ability to read gif from unseekable
+ // streams.
+ {"gif", .readall_on_no_streamseek = true},
+
+ TEXTSUB("aqtitle"), TEXTSUB("jacosub"), TEXTSUB("microdvd"),
+ TEXTSUB("mpl2"), TEXTSUB("mpsub"), TEXTSUB("pjs"), TEXTSUB("realtext"),
+ TEXTSUB("sami"), TEXTSUB("srt"), TEXTSUB("stl"), TEXTSUB("subviewer"),
+ TEXTSUB("subviewer1"), TEXTSUB("vplayer"), TEXTSUB("ass"),
+
+ TEXTSUB_UTF8("webvtt"),
+
+ // Useless non-sense, sometimes breaks MLP2 subreader.c fallback
+ BLACKLIST("tty"),
+ // Let's open files with extremely generic extensions (.bin) with a
+ // demuxer that doesn't have a probe function! NO.
+ BLACKLIST("bin"),
+ // Useless, does not work with custom streams.
+ BLACKLIST("image2"),
+ {0}
+};
+
+struct nested_stream {
+ AVIOContext *id;
+ int64_t last_bytes;
+};
+
+struct stream_info {
+ struct sh_stream *sh;
+ double last_key_pts;
+ double highest_pts;
+ double ts_offset;
+};
+
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 10, 100)
+ #define HAVE_IO_CLOSE2 1
+#else
+ #define HAVE_IO_CLOSE2 0
+#endif
+
+typedef struct lavf_priv {
+ struct stream *stream;
+ bool own_stream;
+ char *filename;
+ struct format_hack format_hack;
+ const AVInputFormat *avif;
+ int avif_flags;
+ AVFormatContext *avfc;
+ AVIOContext *pb;
+ struct stream_info **streams; // NULL for unknown streams
+ int num_streams;
+ char *mime_type;
+ double seek_delay;
+
+ struct demux_lavf_opts *opts;
+
+ bool pcm_seek_hack_disabled;
+ AVStream *pcm_seek_hack;
+ int pcm_seek_hack_packet_size;
+
+ int linearize_ts;
+ bool any_ts_fixed;
+
+ int retry_counter;
+
+ AVDictionary *av_opts;
+
+ // Proxying nested streams.
+ struct nested_stream *nested;
+ int num_nested;
+ int (*default_io_open)(struct AVFormatContext *s, AVIOContext **pb,
+ const char *url, int flags, AVDictionary **options);
+#if HAVE_IO_CLOSE2
+ int (*default_io_close2)(struct AVFormatContext *s, AVIOContext *pb);
+#else
+ void (*default_io_close)(struct AVFormatContext *s, AVIOContext *pb);
+#endif
+} lavf_priv_t;
+
+static void update_read_stats(struct demuxer *demuxer)
+{
+ lavf_priv_t *priv = demuxer->priv;
+
+ for (int n = 0; n < priv->num_nested; n++) {
+ struct nested_stream *nest = &priv->nested[n];
+
+ int64_t cur = nest->id->bytes_read;
+ int64_t new = cur - nest->last_bytes;
+ nest->last_bytes = cur;
+ demux_report_unbuffered_read_bytes(demuxer, new);
+ }
+}
+
+// At least mp4 has name="mov,mp4,m4a,3gp,3g2,mj2", so we split the name
+// on "," in general.
+static bool matches_avinputformat_name(struct lavf_priv *priv,
+ const char *name)
+{
+ const char *avifname = priv->avif->name;
+ while (1) {
+ const char *next = strchr(avifname, ',');
+ if (!next)
+ return !strcmp(avifname, name);
+ int len = next - avifname;
+ if (len == strlen(name) && !memcmp(avifname, name, len))
+ return true;
+ avifname = next + 1;
+ }
+}
+
+static int mp_read(void *opaque, uint8_t *buf, int size)
+{
+ struct demuxer *demuxer = opaque;
+ lavf_priv_t *priv = demuxer->priv;
+ struct stream *stream = priv->stream;
+ if (!stream)
+ return 0;
+
+ int ret = stream_read_partial(stream, buf, size);
+
+ MP_TRACE(demuxer, "%d=mp_read(%p, %p, %d), pos: %"PRId64", eof:%d\n",
+ ret, stream, buf, size, stream_tell(stream), stream->eof);
+ return ret ? ret : AVERROR_EOF;
+}
+
+static int64_t mp_seek(void *opaque, int64_t pos, int whence)
+{
+ struct demuxer *demuxer = opaque;
+ lavf_priv_t *priv = demuxer->priv;
+ struct stream *stream = priv->stream;
+ if (!stream)
+ return -1;
+
+ MP_TRACE(demuxer, "mp_seek(%p, %"PRId64", %s)\n", stream, pos,
+ whence == SEEK_END ? "end" :
+ whence == SEEK_CUR ? "cur" :
+ whence == SEEK_SET ? "set" : "size");
+ if (whence == SEEK_END || whence == AVSEEK_SIZE) {
+ int64_t end = stream_get_size(stream);
+ if (end < 0)
+ return -1;
+ if (whence == AVSEEK_SIZE)
+ return end;
+ pos += end;
+ } else if (whence == SEEK_CUR) {
+ pos += stream_tell(stream);
+ } else if (whence != SEEK_SET) {
+ return -1;
+ }
+
+ if (pos < 0)
+ return -1;
+
+ int64_t current_pos = stream_tell(stream);
+ if (stream_seek(stream, pos) == 0) {
+ stream_seek(stream, current_pos);
+ return -1;
+ }
+
+ return pos;
+}
+
+static int64_t mp_read_seek(void *opaque, int stream_idx, int64_t ts, int flags)
+{
+ struct demuxer *demuxer = opaque;
+ lavf_priv_t *priv = demuxer->priv;
+ struct stream *stream = priv->stream;
+
+ struct stream_avseek cmd = {
+ .stream_index = stream_idx,
+ .timestamp = ts,
+ .flags = flags,
+ };
+
+ if (stream && stream_control(stream, STREAM_CTRL_AVSEEK, &cmd) == STREAM_OK) {
+ stream_drop_buffers(stream);
+ return 0;
+ }
+ return AVERROR(ENOSYS);
+}
+
+static void list_formats(struct demuxer *demuxer)
+{
+ MP_INFO(demuxer, "Available lavf input formats:\n");
+ const AVInputFormat *fmt;
+ void *iter = NULL;
+ while ((fmt = av_demuxer_iterate(&iter)))
+ MP_INFO(demuxer, "%15s : %s\n", fmt->name, fmt->long_name);
+}
+
+static void convert_charset(struct demuxer *demuxer)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ char *cp = priv->opts->sub_cp;
+ if (!cp || !cp[0] || mp_charset_is_utf8(cp))
+ return;
+ bstr data = stream_read_complete(priv->stream, NULL, 128 * 1024 * 1024);
+ if (!data.start) {
+ MP_WARN(demuxer, "File too big (or error reading) - skip charset probing.\n");
+ return;
+ }
+ void *alloc = data.start;
+ cp = (char *)mp_charset_guess(priv, demuxer->log, data, cp, 0);
+ if (cp && !mp_charset_is_utf8(cp))
+ MP_INFO(demuxer, "Using subtitle charset: %s\n", cp);
+ // libavformat transparently converts UTF-16 to UTF-8
+ if (!mp_charset_is_utf16(cp) && !mp_charset_is_utf8(cp)) {
+ bstr conv = mp_iconv_to_utf8(demuxer->log, data, cp, MP_ICONV_VERBOSE);
+ if (conv.start && conv.start != data.start)
+ talloc_steal(alloc, conv.start);
+ if (conv.start)
+ data = conv;
+ }
+ if (data.start) {
+ priv->stream = stream_memory_open(demuxer->global, data.start, data.len);
+ priv->own_stream = true;
+ }
+ talloc_free(alloc);
+}
+
+static char *remove_prefix(char *s, const char *const *prefixes)
+{
+ for (int n = 0; prefixes[n]; n++) {
+ int len = strlen(prefixes[n]);
+ if (strncmp(s, prefixes[n], len) == 0)
+ return s + len;
+ }
+ return s;
+}
+
+static const char *const prefixes[] =
+ {"ffmpeg://", "lavf://", "avdevice://", "av://", NULL};
+
+static int lavf_check_file(demuxer_t *demuxer, enum demux_check check)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ struct demux_lavf_opts *lavfdopts = priv->opts;
+ struct stream *s = priv->stream;
+
+ priv->filename = remove_prefix(s->url, prefixes);
+
+ char *avdevice_format = NULL;
+ if (s->info && strcmp(s->info->name, "avdevice") == 0) {
+ // always require filename in the form "format:filename"
+ char *sep = strchr(priv->filename, ':');
+ if (!sep) {
+ MP_FATAL(demuxer, "Must specify filename in 'format:filename' form\n");
+ return -1;
+ }
+ avdevice_format = talloc_strndup(priv, priv->filename,
+ sep - priv->filename);
+ priv->filename = sep + 1;
+ }
+
+ char *mime_type = s->mime_type;
+ if (!lavfdopts->allow_mimetype || !mime_type)
+ mime_type = "";
+
+ const AVInputFormat *forced_format = NULL;
+ const char *format = lavfdopts->format;
+ if (!format)
+ format = s->lavf_type;
+ if (!format)
+ format = avdevice_format;
+ if (format) {
+ if (strcmp(format, "help") == 0) {
+ list_formats(demuxer);
+ return -1;
+ }
+ forced_format = av_find_input_format(format);
+ if (!forced_format) {
+ MP_FATAL(demuxer, "Unknown lavf format %s\n", format);
+ return -1;
+ }
+ }
+
+ // HLS streams seems to be not well tagged, so matching mime type is not
+ // enough. Strip URL parameters and match extension.
+ bstr ext = bstr_get_ext(bstr_split(bstr0(priv->filename), "?#", NULL));
+ AVProbeData avpd = {
+ // Disable file-extension matching with normal checks, except for HLS
+ .filename = !bstrcasecmp0(ext, "m3u8") || !bstrcasecmp0(ext, "m3u") ||
+ check <= DEMUX_CHECK_REQUEST ? priv->filename : "",
+ .buf_size = 0,
+ .buf = av_mallocz(PROBE_BUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE),
+ .mime_type = lavfdopts->allow_mimetype ? mime_type : NULL,
+ };
+ if (!avpd.buf)
+ return -1;
+
+ bool final_probe = false;
+ do {
+ int score = 0;
+
+ if (forced_format) {
+ priv->avif = forced_format;
+ score = AVPROBE_SCORE_MAX;
+ } else {
+ int nsize = av_clip(avpd.buf_size * 2, INITIAL_PROBE_SIZE,
+ PROBE_BUF_SIZE);
+ nsize = stream_read_peek(s, avpd.buf, nsize);
+ if (nsize <= avpd.buf_size)
+ final_probe = true;
+ avpd.buf_size = nsize;
+
+ priv->avif = av_probe_input_format2(&avpd, avpd.buf_size > 0, &score);
+ }
+
+ if (priv->avif) {
+ MP_VERBOSE(demuxer, "Found '%s' at score=%d size=%d%s.\n",
+ priv->avif->name, score, avpd.buf_size,
+ forced_format ? " (forced)" : "");
+
+ for (int n = 0; lavfdopts->hacks && format_hacks[n].ff_name; n++) {
+ const struct format_hack *entry = &format_hacks[n];
+ if (!matches_avinputformat_name(priv, entry->ff_name))
+ continue;
+ if (entry->mime_type && strcasecmp(entry->mime_type, mime_type) != 0)
+ continue;
+ priv->format_hack = *entry;
+ break;
+ }
+
+ if (score >= lavfdopts->probescore)
+ break;
+
+ if (priv->format_hack.probescore &&
+ score >= priv->format_hack.probescore &&
+ (!priv->format_hack.max_probe || final_probe))
+ break;
+ }
+
+ priv->avif = NULL;
+ priv->format_hack = (struct format_hack){0};
+ } while (!final_probe);
+
+ av_free(avpd.buf);
+
+ if (priv->avif && !forced_format && priv->format_hack.ignore) {
+ MP_VERBOSE(demuxer, "Format blacklisted.\n");
+ priv->avif = NULL;
+ }
+
+ if (!priv->avif) {
+ MP_VERBOSE(demuxer, "No format found, try lowering probescore or forcing the format.\n");
+ return -1;
+ }
+
+ if (lavfdopts->hacks)
+ priv->avif_flags = priv->avif->flags | priv->format_hack.if_flags;
+
+ priv->linearize_ts = lavfdopts->linearize_ts;
+ if (priv->linearize_ts < 0 && !priv->format_hack.linearize_audio_ts)
+ priv->linearize_ts = 0;
+
+ demuxer->filetype = priv->avif->name;
+
+ if (priv->format_hack.detect_charset)
+ convert_charset(demuxer);
+
+ return 0;
+}
+
+static char *replace_idx_ext(void *ta_ctx, bstr f)
+{
+ if (f.len < 4 || f.start[f.len - 4] != '.')
+ return NULL;
+ char *ext = bstr_endswith0(f, "IDX") ? "SUB" : "sub"; // match case
+ return talloc_asprintf(ta_ctx, "%.*s.%s", BSTR_P(bstr_splice(f, 0, -4)), ext);
+}
+
+static void guess_and_set_vobsub_name(struct demuxer *demuxer, AVDictionary **d)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ if (!matches_avinputformat_name(priv, "vobsub"))
+ return;
+
+ void *tmp = talloc_new(NULL);
+ bstr bfilename = bstr0(priv->filename);
+ char *subname = NULL;
+ if (mp_is_url(bfilename)) {
+ // It might be a http URL, which has additional parameters after the
+ // end of the actual file path.
+ bstr start, end;
+ if (bstr_split_tok(bfilename, "?", &start, &end)) {
+ subname = replace_idx_ext(tmp, start);
+ if (subname)
+ subname = talloc_asprintf(tmp, "%s?%.*s", subname, BSTR_P(end));
+ }
+ }
+ if (!subname)
+ subname = replace_idx_ext(tmp, bfilename);
+ if (!subname)
+ subname = talloc_asprintf(tmp, "%.*s.sub", BSTR_P(bfilename));
+
+ MP_VERBOSE(demuxer, "Assuming associated .sub file: %s\n", subname);
+ av_dict_set(d, "sub_name", subname, 0);
+ talloc_free(tmp);
+}
+
+static void select_tracks(struct demuxer *demuxer, int start)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ for (int n = start; n < priv->num_streams; n++) {
+ struct sh_stream *stream = priv->streams[n]->sh;
+ AVStream *st = priv->avfc->streams[n];
+ bool selected = stream && demux_stream_is_selected(stream) &&
+ !stream->attached_picture;
+ st->discard = selected ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
+ }
+}
+
+static void export_replaygain(demuxer_t *demuxer, struct sh_stream *sh,
+ AVStream *st)
+{
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(60, 15, 100)
+ AVPacketSideData *side_data = st->codecpar->coded_side_data;
+ int nb_side_data = st->codecpar->nb_coded_side_data;
+#else
+ AVPacketSideData *side_data = st->side_data;
+ int nb_side_data = st->nb_side_data;
+#endif
+ for (int i = 0; i < nb_side_data; i++) {
+ AVReplayGain *av_rgain;
+ struct replaygain_data *rgain;
+ AVPacketSideData *src_sd = &side_data[i];
+
+ if (src_sd->type != AV_PKT_DATA_REPLAYGAIN)
+ continue;
+
+ av_rgain = (AVReplayGain*)src_sd->data;
+ rgain = talloc_ptrtype(demuxer, rgain);
+ rgain->track_gain = rgain->album_gain = 0;
+ rgain->track_peak = rgain->album_peak = 1;
+
+ // Set values in *rgain, using track gain as a fallback for album gain
+ // if the latter is not present. This behavior matches that in
+ // demux/demux.c's decode_rgain; if you change this, please make
+ // equivalent changes there too.
+ if (av_rgain->track_gain != INT32_MIN && av_rgain->track_peak != 0.0) {
+ // Track gain is defined.
+ rgain->track_gain = av_rgain->track_gain / 100000.0f;
+ rgain->track_peak = av_rgain->track_peak / 100000.0f;
+
+ if (av_rgain->album_gain != INT32_MIN &&
+ av_rgain->album_peak != 0.0)
+ {
+ // Album gain is also defined.
+ rgain->album_gain = av_rgain->album_gain / 100000.0f;
+ rgain->album_peak = av_rgain->album_peak / 100000.0f;
+ } else {
+ // Album gain is undefined; fall back to track gain.
+ rgain->album_gain = rgain->track_gain;
+ rgain->album_peak = rgain->track_peak;
+ }
+ }
+
+ // This must be run only before the stream was added, otherwise there
+ // will be race conditions with accesses from the user thread.
+ assert(!sh->ds);
+ sh->codec->replaygain_data = rgain;
+ }
+}
+
+// Return a dictionary entry as (decimal) integer.
+static int dict_get_decimal(AVDictionary *dict, const char *entry, int def)
+{
+ AVDictionaryEntry *e = av_dict_get(dict, entry, NULL, 0);
+ if (e && e->value) {
+ char *end = NULL;
+ long int r = strtol(e->value, &end, 10);
+ if (end && !end[0] && r >= INT_MIN && r <= INT_MAX)
+ return r;
+ }
+ return def;
+}
+
+static bool is_image(AVStream *st, bool attached_picture, const AVInputFormat *avif)
+{
+ return st->nb_frames <= 1 && (
+ attached_picture ||
+ bstr_endswith0(bstr0(avif->name), "_pipe") ||
+ strcmp(avif->name, "alias_pix") == 0 ||
+ strcmp(avif->name, "gif") == 0 ||
+ strcmp(avif->name, "image2pipe") == 0 ||
+ (st->codecpar->codec_id == AV_CODEC_ID_AV1 && st->nb_frames == 1)
+ );
+}
+
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(60, 15, 100)
+static inline const uint8_t *mp_av_stream_get_side_data(const AVStream *st,
+ enum AVPacketSideDataType type)
+{
+ const AVPacketSideData *sd;
+ sd = av_packet_side_data_get(st->codecpar->coded_side_data,
+ st->codecpar->nb_coded_side_data,
+ type);
+ return sd ? sd->data : NULL;
+}
+#else
+#define mp_av_stream_get_side_data(st, type) av_stream_get_side_data(st, type, NULL)
+#endif
+
+static void handle_new_stream(demuxer_t *demuxer, int i)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ AVFormatContext *avfc = priv->avfc;
+ AVStream *st = avfc->streams[i];
+ struct sh_stream *sh = NULL;
+ AVCodecParameters *codec = st->codecpar;
+ int lavc_delay = codec->initial_padding;
+
+ switch (codec->codec_type) {
+ case AVMEDIA_TYPE_AUDIO: {
+ sh = demux_alloc_sh_stream(STREAM_AUDIO);
+
+#if !HAVE_AV_CHANNEL_LAYOUT
+ // probably unneeded
+ mp_chmap_set_unknown(&sh->codec->channels, codec->channels);
+ if (codec->channel_layout)
+ mp_chmap_from_lavc(&sh->codec->channels, codec->channel_layout);
+#else
+ if (!mp_chmap_from_av_layout(&sh->codec->channels, &codec->ch_layout)) {
+ char layout[128] = {0};
+ MP_WARN(demuxer,
+ "Failed to convert channel layout %s to mpv one!\n",
+ av_channel_layout_describe(&codec->ch_layout,
+ layout, 128) < 0 ?
+ "undefined" : layout);
+ }
+#endif
+
+ sh->codec->samplerate = codec->sample_rate;
+ sh->codec->bitrate = codec->bit_rate;
+
+ double delay = 0;
+ if (codec->sample_rate > 0)
+ delay = lavc_delay / (double)codec->sample_rate;
+ priv->seek_delay = MPMAX(priv->seek_delay, delay);
+
+ export_replaygain(demuxer, sh, st);
+
+ sh->seek_preroll = delay;
+
+ break;
+ }
+ case AVMEDIA_TYPE_VIDEO: {
+ sh = demux_alloc_sh_stream(STREAM_VIDEO);
+
+ if ((st->disposition & AV_DISPOSITION_ATTACHED_PIC) &&
+ !(st->disposition & AV_DISPOSITION_TIMED_THUMBNAILS))
+ {
+ sh->attached_picture =
+ new_demux_packet_from_avpacket(&st->attached_pic);
+ if (sh->attached_picture) {
+ sh->attached_picture->pts = 0;
+ talloc_steal(sh, sh->attached_picture);
+ sh->attached_picture->keyframe = true;
+ }
+ }
+
+ if (!sh->attached_picture) {
+ // A real video stream probably means it's a packet based format.
+ priv->pcm_seek_hack_disabled = true;
+ priv->pcm_seek_hack = NULL;
+ // Also, we don't want to do this shit for ogv videos.
+ if (priv->linearize_ts < 0)
+ priv->linearize_ts = 0;
+ }
+
+ sh->codec->disp_w = codec->width;
+ sh->codec->disp_h = codec->height;
+ if (st->avg_frame_rate.num)
+ sh->codec->fps = av_q2d(st->avg_frame_rate);
+ if (is_image(st, sh->attached_picture, priv->avif)) {
+ MP_VERBOSE(demuxer, "Assuming this is an image format.\n");
+ sh->image = true;
+ sh->codec->fps = demuxer->opts->mf_fps;
+ }
+ sh->codec->par_w = st->sample_aspect_ratio.num;
+ sh->codec->par_h = st->sample_aspect_ratio.den;
+
+ const uint8_t *sd = mp_av_stream_get_side_data(st, AV_PKT_DATA_DISPLAYMATRIX);
+ if (sd) {
+ double r = av_display_rotation_get((int32_t *)sd);
+ if (!isnan(r))
+ sh->codec->rotate = (((int)(-r) % 360) + 360) % 360;
+ }
+
+ if ((sd = mp_av_stream_get_side_data(st, AV_PKT_DATA_DOVI_CONF))) {
+ const AVDOVIDecoderConfigurationRecord *cfg = (void *) sd;
+ MP_VERBOSE(demuxer, "Found Dolby Vision config record: profile "
+ "%d level %d\n", cfg->dv_profile, cfg->dv_level);
+ av_format_inject_global_side_data(avfc);
+ }
+
+ // This also applies to vfw-muxed mkv, but we can't detect these easily.
+ sh->codec->avi_dts = matches_avinputformat_name(priv, "avi");
+
+ break;
+ }
+ case AVMEDIA_TYPE_SUBTITLE: {
+ sh = demux_alloc_sh_stream(STREAM_SUB);
+
+ if (codec->extradata_size) {
+ sh->codec->extradata = talloc_size(sh, codec->extradata_size);
+ memcpy(sh->codec->extradata, codec->extradata, codec->extradata_size);
+ sh->codec->extradata_size = codec->extradata_size;
+ }
+
+ if (matches_avinputformat_name(priv, "microdvd")) {
+ AVRational r;
+ if (av_opt_get_q(avfc, "subfps", AV_OPT_SEARCH_CHILDREN, &r) >= 0) {
+ // File headers don't have a FPS set.
+ if (r.num < 1 || r.den < 1)
+ sh->codec->frame_based = 23.976; // default timebase
+ }
+ }
+ break;
+ }
+ case AVMEDIA_TYPE_ATTACHMENT: {
+ AVDictionaryEntry *ftag = av_dict_get(st->metadata, "filename", NULL, 0);
+ char *filename = ftag ? ftag->value : NULL;
+ AVDictionaryEntry *mt = av_dict_get(st->metadata, "mimetype", NULL, 0);
+ char *mimetype = mt ? mt->value : NULL;
+ if (mimetype) {
+ demuxer_add_attachment(demuxer, filename, mimetype,
+ codec->extradata, codec->extradata_size);
+ }
+ break;
+ }
+ default: ;
+ }
+
+ struct stream_info *info = talloc_zero(priv, struct stream_info);
+ *info = (struct stream_info){
+ .sh = sh,
+ .last_key_pts = MP_NOPTS_VALUE,
+ .highest_pts = MP_NOPTS_VALUE,
+ };
+ assert(priv->num_streams == i); // directly mapped
+ MP_TARRAY_APPEND(priv, priv->streams, priv->num_streams, info);
+
+ if (sh) {
+ sh->ff_index = st->index;
+ sh->codec->codec = mp_codec_from_av_codec_id(codec->codec_id);
+ sh->codec->codec_tag = codec->codec_tag;
+ sh->codec->lav_codecpar = avcodec_parameters_alloc();
+ if (sh->codec->lav_codecpar)
+ avcodec_parameters_copy(sh->codec->lav_codecpar, codec);
+ sh->codec->native_tb_num = st->time_base.num;
+ sh->codec->native_tb_den = st->time_base.den;
+
+ if (st->disposition & AV_DISPOSITION_DEFAULT)
+ sh->default_track = true;
+ if (st->disposition & AV_DISPOSITION_FORCED)
+ sh->forced_track = true;
+ if (st->disposition & AV_DISPOSITION_DEPENDENT)
+ sh->dependent_track = true;
+ if (st->disposition & AV_DISPOSITION_VISUAL_IMPAIRED)
+ sh->visual_impaired_track = true;
+ if (st->disposition & AV_DISPOSITION_HEARING_IMPAIRED)
+ sh->hearing_impaired_track = true;
+ if (st->disposition & AV_DISPOSITION_STILL_IMAGE)
+ sh->still_image = true;
+ if (priv->format_hack.use_stream_ids)
+ sh->demuxer_id = st->id;
+ AVDictionaryEntry *title = av_dict_get(st->metadata, "title", NULL, 0);
+ if (title && title->value)
+ sh->title = talloc_strdup(sh, title->value);
+ if (!sh->title && st->disposition & AV_DISPOSITION_VISUAL_IMPAIRED)
+ sh->title = talloc_asprintf(sh, "visual impaired");
+ if (!sh->title && st->disposition & AV_DISPOSITION_HEARING_IMPAIRED)
+ sh->title = talloc_asprintf(sh, "hearing impaired");
+ AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL, 0);
+ if (lang && lang->value && strcmp(lang->value, "und") != 0)
+ sh->lang = talloc_strdup(sh, lang->value);
+ sh->hls_bitrate = dict_get_decimal(st->metadata, "variant_bitrate", 0);
+ AVProgram *prog = av_find_program_from_stream(avfc, NULL, i);
+ if (prog)
+ sh->program_id = prog->id;
+ sh->missing_timestamps = !!(priv->avif_flags & AVFMT_NOTIMESTAMPS);
+ mp_tags_move_from_av_dictionary(sh->tags, &st->metadata);
+ demux_add_sh_stream(demuxer, sh);
+
+ // Unfortunately, there is no better way to detect PCM codecs, other
+ // than listing them all manually. (Or other "frameless" codecs. Or
+ // rather, codecs with frames so small libavformat will put multiple of
+ // them into a single packet, but not preserve these artificial packet
+ // boundaries on seeking.)
+ if (sh->codec->codec && strncmp(sh->codec->codec, "pcm_", 4) == 0 &&
+ codec->block_align && !priv->pcm_seek_hack_disabled &&
+ priv->opts->hacks && !priv->format_hack.no_pcm_seek &&
+ st->time_base.num == 1 && st->time_base.den == codec->sample_rate)
+ {
+ if (priv->pcm_seek_hack) {
+ // More than 1 audio stream => usually doesn't apply.
+ priv->pcm_seek_hack_disabled = true;
+ priv->pcm_seek_hack = NULL;
+ } else {
+ priv->pcm_seek_hack = st;
+ }
+ }
+ }
+
+ select_tracks(demuxer, i);
+}
+
+// Add any new streams that might have been added
+static void add_new_streams(demuxer_t *demuxer)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ while (priv->num_streams < priv->avfc->nb_streams)
+ handle_new_stream(demuxer, priv->num_streams);
+}
+
+static void update_metadata(demuxer_t *demuxer)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ if (priv->avfc->event_flags & AVFMT_EVENT_FLAG_METADATA_UPDATED) {
+ mp_tags_move_from_av_dictionary(demuxer->metadata, &priv->avfc->metadata);
+ priv->avfc->event_flags = 0;
+ demux_metadata_changed(demuxer);
+ }
+}
+
+static int interrupt_cb(void *ctx)
+{
+ struct demuxer *demuxer = ctx;
+ return mp_cancel_test(demuxer->cancel);
+}
+
+static int block_io_open(struct AVFormatContext *s, AVIOContext **pb,
+ const char *url, int flags, AVDictionary **options)
+{
+ struct demuxer *demuxer = s->opaque;
+ MP_ERR(demuxer, "Not opening '%s' due to --access-references=no.\n", url);
+ return AVERROR(EACCES);
+}
+
+static int nested_io_open(struct AVFormatContext *s, AVIOContext **pb,
+ const char *url, int flags, AVDictionary **options)
+{
+ struct demuxer *demuxer = s->opaque;
+ lavf_priv_t *priv = demuxer->priv;
+
+ if (priv->opts->propagate_opts) {
+ // Copy av_opts to options, but only entries that are not present in
+ // options. (Hope this will break less by not overwriting important
+ // settings.)
+ AVDictionaryEntry *cur = NULL;
+ while ((cur = av_dict_get(priv->av_opts, "", cur, AV_DICT_IGNORE_SUFFIX)))
+ {
+ if (!*options || !av_dict_get(*options, cur->key, NULL, 0)) {
+ MP_TRACE(demuxer, "Nested option: '%s'='%s'\n",
+ cur->key, cur->value);
+ av_dict_set(options, cur->key, cur->value, 0);
+ } else {
+ MP_TRACE(demuxer, "Skipping nested option: '%s'\n", cur->key);
+ }
+ }
+ }
+
+ int r = priv->default_io_open(s, pb, url, flags, options);
+ if (r >= 0) {
+ if (options)
+ mp_avdict_print_unset(demuxer->log, MSGL_TRACE, *options);
+ struct nested_stream nest = {
+ .id = *pb,
+ };
+ MP_TARRAY_APPEND(priv, priv->nested, priv->num_nested, nest);
+ }
+ return r;
+}
+
+#if HAVE_IO_CLOSE2
+static int nested_io_close2(struct AVFormatContext *s, AVIOContext *pb)
+#else
+static void nested_io_close(struct AVFormatContext *s, AVIOContext *pb)
+#endif
+{
+ struct demuxer *demuxer = s->opaque;
+ lavf_priv_t *priv = demuxer->priv;
+
+ for (int n = 0; n < priv->num_nested; n++) {
+ if (priv->nested[n].id == pb) {
+ MP_TARRAY_REMOVE_AT(priv->nested, priv->num_nested, n);
+ break;
+ }
+ }
+
+#if HAVE_IO_CLOSE2
+ return priv->default_io_close2(s, pb);
+#else
+ priv->default_io_close(s, pb);
+#endif
+}
+
+static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
+{
+ AVFormatContext *avfc = NULL;
+ AVDictionaryEntry *t = NULL;
+ float analyze_duration = 0;
+ lavf_priv_t *priv = talloc_zero(NULL, lavf_priv_t);
+ AVDictionary *dopts = NULL;
+
+ demuxer->priv = priv;
+ priv->stream = demuxer->stream;
+
+ priv->opts = mp_get_config_group(priv, demuxer->global, &demux_lavf_conf);
+ struct demux_lavf_opts *lavfdopts = priv->opts;
+
+ if (lavf_check_file(demuxer, check) < 0)
+ goto fail;
+
+ avfc = avformat_alloc_context();
+ if (!avfc)
+ goto fail;
+
+ if (demuxer->opts->index_mode != 1)
+ avfc->flags |= AVFMT_FLAG_IGNIDX;
+
+ if (lavfdopts->probesize) {
+ if (av_opt_set_int(avfc, "probesize", lavfdopts->probesize, 0) < 0)
+ MP_ERR(demuxer, "couldn't set option probesize to %u\n",
+ lavfdopts->probesize);
+ }
+
+ if (priv->format_hack.analyzeduration)
+ analyze_duration = priv->format_hack.analyzeduration;
+ if (lavfdopts->analyzeduration)
+ analyze_duration = lavfdopts->analyzeduration;
+ if (analyze_duration > 0) {
+ if (av_opt_set_int(avfc, "analyzeduration",
+ analyze_duration * AV_TIME_BASE, 0) < 0)
+ MP_ERR(demuxer, "demux_lavf, couldn't set option "
+ "analyzeduration to %f\n", analyze_duration);
+ }
+
+ if ((priv->avif_flags & AVFMT_NOFILE) || priv->format_hack.no_stream) {
+ mp_setup_av_network_options(&dopts, priv->avif->name,
+ demuxer->global, demuxer->log);
+ // This might be incorrect.
+ demuxer->seekable = true;
+ } else {
+ void *buffer = av_malloc(lavfdopts->buffersize);
+ if (!buffer)
+ goto fail;
+ priv->pb = avio_alloc_context(buffer, lavfdopts->buffersize, 0,
+ demuxer, mp_read, NULL, mp_seek);
+ if (!priv->pb) {
+ av_free(buffer);
+ goto fail;
+ }
+ priv->pb->read_seek = mp_read_seek;
+ priv->pb->seekable = demuxer->seekable ? AVIO_SEEKABLE_NORMAL : 0;
+ avfc->pb = priv->pb;
+ if (stream_control(priv->stream, STREAM_CTRL_HAS_AVSEEK, NULL) > 0)
+ demuxer->seekable = true;
+ demuxer->seekable |= priv->format_hack.fully_read;
+ }
+
+ if (matches_avinputformat_name(priv, "rtsp")) {
+ const char *transport = NULL;
+ switch (lavfdopts->rtsp_transport) {
+ case 1: transport = "udp"; break;
+ case 2: transport = "tcp"; break;
+ case 3: transport = "http"; break;
+ case 4: transport = "udp_multicast"; break;
+ }
+ if (transport)
+ av_dict_set(&dopts, "rtsp_transport", transport, 0);
+ }
+
+ guess_and_set_vobsub_name(demuxer, &dopts);
+
+ if (priv->format_hack.fix_editlists)
+ av_dict_set(&dopts, "advanced_editlist", "0", 0);
+
+ avfc->interrupt_callback = (AVIOInterruptCB){
+ .callback = interrupt_cb,
+ .opaque = demuxer,
+ };
+
+ avfc->opaque = demuxer;
+ if (demuxer->access_references) {
+ priv->default_io_open = avfc->io_open;
+ avfc->io_open = nested_io_open;
+#if HAVE_IO_CLOSE2
+ priv->default_io_close2 = avfc->io_close2;
+ avfc->io_close2 = nested_io_close2;
+#else
+ priv->default_io_close = avfc->io_close;
+ avfc->io_close = nested_io_close;
+#endif
+ } else {
+ avfc->io_open = block_io_open;
+ }
+
+ mp_set_avdict(&dopts, lavfdopts->avopts);
+
+ if (av_dict_copy(&priv->av_opts, dopts, 0) < 0) {
+ MP_ERR(demuxer, "av_dict_copy() failed\n");
+ goto fail;
+ }
+
+ if (priv->format_hack.readall_on_no_streamseek && priv->pb &&
+ !priv->pb->seekable)
+ {
+ MP_VERBOSE(demuxer, "Non-seekable demuxer pre-read hack...\n");
+ // Read incremental to avoid unnecessary large buffer sizes.
+ int r = 0;
+ for (int n = 16; n < 29; n++) {
+ r = stream_peek(priv->stream, 1 << n);
+ if (r < (1 << n))
+ break;
+ }
+ MP_VERBOSE(demuxer, "...did read %d bytes.\n", r);
+ }
+
+ if (avformat_open_input(&avfc, priv->filename, priv->avif, &dopts) < 0) {
+ MP_ERR(demuxer, "avformat_open_input() failed\n");
+ goto fail;
+ }
+
+ mp_avdict_print_unset(demuxer->log, MSGL_V, dopts);
+ av_dict_free(&dopts);
+
+ priv->avfc = avfc;
+
+ bool probeinfo = lavfdopts->probeinfo != 0;
+ switch (lavfdopts->probeinfo) {
+ case -2: probeinfo = priv->avfc->nb_streams == 0; break;
+ case -1: probeinfo = !priv->format_hack.skipinfo; break;
+ }
+ if (demuxer->params && demuxer->params->skip_lavf_probing)
+ probeinfo = false;
+ if (probeinfo) {
+ if (avformat_find_stream_info(avfc, NULL) < 0) {
+ MP_ERR(demuxer, "av_find_stream_info() failed\n");
+ goto fail;
+ }
+
+ MP_VERBOSE(demuxer, "avformat_find_stream_info() finished after %"PRId64
+ " bytes.\n", stream_tell(priv->stream));
+ }
+
+ for (int i = 0; i < avfc->nb_chapters; i++) {
+ AVChapter *c = avfc->chapters[i];
+ t = av_dict_get(c->metadata, "title", NULL, 0);
+ int index = demuxer_add_chapter(demuxer, t ? t->value : "",
+ c->start * av_q2d(c->time_base), i);
+ mp_tags_move_from_av_dictionary(demuxer->chapters[index].metadata, &c->metadata);
+ }
+
+ add_new_streams(demuxer);
+
+ mp_tags_move_from_av_dictionary(demuxer->metadata, &avfc->metadata);
+
+ demuxer->ts_resets_possible =
+ priv->avif_flags & (AVFMT_TS_DISCONT | AVFMT_NOTIMESTAMPS);
+
+ if (avfc->start_time != AV_NOPTS_VALUE)
+ demuxer->start_time = avfc->start_time / (double)AV_TIME_BASE;
+
+ demuxer->fully_read = priv->format_hack.fully_read;
+
+#ifdef AVFMTCTX_UNSEEKABLE
+ if (avfc->ctx_flags & AVFMTCTX_UNSEEKABLE)
+ demuxer->seekable = false;
+#endif
+
+ demuxer->is_network |= priv->format_hack.is_network;
+ demuxer->seekable &= !priv->format_hack.no_seek;
+
+ // We initially prefer track durations over container durations because they
+ // have a higher degree of precision over the container duration which are
+ // only accurate to the 6th decimal place. This is probably a lavf bug.
+ double total_duration = -1;
+ double av_duration = -1;
+ for (int n = 0; n < priv->avfc->nb_streams; n++) {
+ AVStream *st = priv->avfc->streams[n];
+ if (st->duration <= 0)
+ continue;
+ double f_duration = st->duration * av_q2d(st->time_base);
+ total_duration = MPMAX(total_duration, f_duration);
+ if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO ||
+ st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ av_duration = MPMAX(av_duration, f_duration);
+ }
+ double duration = av_duration > 0 ? av_duration : total_duration;
+ if (duration <= 0 && priv->avfc->duration > 0)
+ duration = (double)priv->avfc->duration / AV_TIME_BASE;
+ demuxer->duration = duration;
+
+ if (demuxer->duration < 0 && priv->format_hack.no_seek_on_no_duration)
+ demuxer->seekable = false;
+
+ // In some cases, libavformat will export bogus bullshit timestamps anyway,
+ // such as with mjpeg.
+ if (priv->avif_flags & AVFMT_NOTIMESTAMPS) {
+ MP_WARN(demuxer,
+ "This format is marked by FFmpeg as having no timestamps!\n"
+ "FFmpeg will likely make up its own broken timestamps. For\n"
+ "video streams you can correct this with:\n"
+ " --no-correct-pts --container-fps-override=VALUE\n"
+ "with VALUE being the real framerate of the stream. You can\n"
+ "expect seeking and buffering estimation to be generally\n"
+ "broken as well.\n");
+ }
+
+ if (demuxer->fully_read) {
+ demux_close_stream(demuxer);
+ if (priv->own_stream)
+ free_stream(priv->stream);
+ priv->own_stream = false;
+ priv->stream = NULL;
+ }
+
+ return 0;
+
+fail:
+ if (!priv->avfc)
+ avformat_free_context(avfc);
+ av_dict_free(&dopts);
+
+ return -1;
+}
+
+static bool demux_lavf_read_packet(struct demuxer *demux,
+ struct demux_packet **mp_pkt)
+{
+ lavf_priv_t *priv = demux->priv;
+
+ AVPacket *pkt = &(AVPacket){0};
+ int r = av_read_frame(priv->avfc, pkt);
+ update_read_stats(demux);
+ if (r < 0) {
+ av_packet_unref(pkt);
+ if (r == AVERROR_EOF)
+ return false;
+ MP_WARN(demux, "error reading packet: %s.\n", av_err2str(r));
+ if (priv->retry_counter >= 10) {
+ MP_ERR(demux, "...treating it as fatal error.\n");
+ return false;
+ }
+ priv->retry_counter += 1;
+ return true;
+ }
+ priv->retry_counter = 0;
+
+ add_new_streams(demux);
+ update_metadata(demux);
+
+ assert(pkt->stream_index >= 0 && pkt->stream_index < priv->num_streams);
+ struct stream_info *info = priv->streams[pkt->stream_index];
+ struct sh_stream *stream = info->sh;
+ AVStream *st = priv->avfc->streams[pkt->stream_index];
+
+ if (!demux_stream_is_selected(stream)) {
+ av_packet_unref(pkt);
+ return true; // don't signal EOF if skipping a packet
+ }
+
+ struct demux_packet *dp = new_demux_packet_from_avpacket(pkt);
+ if (!dp) {
+ av_packet_unref(pkt);
+ return true;
+ }
+
+ if (priv->pcm_seek_hack == st && !priv->pcm_seek_hack_packet_size)
+ priv->pcm_seek_hack_packet_size = pkt->size;
+
+ dp->pts = mp_pts_from_av(pkt->pts, &st->time_base);
+ dp->dts = mp_pts_from_av(pkt->dts, &st->time_base);
+ dp->duration = pkt->duration * av_q2d(st->time_base);
+ dp->pos = pkt->pos;
+ dp->keyframe = pkt->flags & AV_PKT_FLAG_KEY;
+ if (pkt->flags & AV_PKT_FLAG_DISCARD)
+ MP_ERR(demux, "Edit lists are not correctly supported (FFmpeg issue).\n");
+ av_packet_unref(pkt);
+
+ if (priv->format_hack.clear_filepos)
+ dp->pos = -1;
+
+ dp->stream = stream->index;
+
+ if (priv->linearize_ts) {
+ dp->pts = MP_ADD_PTS(dp->pts, info->ts_offset);
+ dp->dts = MP_ADD_PTS(dp->dts, info->ts_offset);
+
+ double pts = MP_PTS_OR_DEF(dp->pts, dp->dts);
+ if (pts != MP_NOPTS_VALUE) {
+ if (dp->keyframe) {
+ if (pts < info->highest_pts) {
+ MP_WARN(demux, "Linearizing discontinuity: %f -> %f\n",
+ pts, info->highest_pts);
+ // Note: introduces a small discontinuity by a frame size.
+ double diff = info->highest_pts - pts;
+ dp->pts = MP_ADD_PTS(dp->pts, diff);
+ dp->dts = MP_ADD_PTS(dp->dts, diff);
+ pts += diff;
+ info->ts_offset += diff;
+ priv->any_ts_fixed = true;
+ }
+ info->last_key_pts = pts;
+ }
+ info->highest_pts = MP_PTS_MAX(info->highest_pts, pts);
+ }
+ }
+
+ if (st->event_flags & AVSTREAM_EVENT_FLAG_METADATA_UPDATED) {
+ st->event_flags = 0;
+ struct mp_tags *tags = talloc_zero(NULL, struct mp_tags);
+ mp_tags_move_from_av_dictionary(tags, &st->metadata);
+ double pts = MP_PTS_OR_DEF(dp->pts, dp->dts);
+ demux_stream_tags_changed(demux, stream, tags, pts);
+ }
+
+ *mp_pkt = dp;
+ return true;
+}
+
+static void demux_seek_lavf(demuxer_t *demuxer, double seek_pts, int flags)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ int avsflags = 0;
+ int64_t seek_pts_av = 0;
+ int seek_stream = -1;
+
+ if (priv->any_ts_fixed) {
+ // helpful message to piss of users
+ MP_WARN(demuxer, "Some timestamps returned by the demuxer were linearized. "
+ "A low level seek was requested; this won't work due to "
+ "restrictions in libavformat's API. You may have more "
+ "luck by enabling or enlarging the mpv cache.\n");
+ }
+
+ if (priv->linearize_ts < 0)
+ priv->linearize_ts = 0;
+
+ if (!(flags & SEEK_FORWARD))
+ avsflags = AVSEEK_FLAG_BACKWARD;
+
+ if (flags & SEEK_FACTOR) {
+ struct stream *s = priv->stream;
+ int64_t end = s ? stream_get_size(s) : -1;
+ if (end > 0 && demuxer->ts_resets_possible &&
+ !(priv->avif_flags & AVFMT_NO_BYTE_SEEK))
+ {
+ avsflags |= AVSEEK_FLAG_BYTE;
+ seek_pts_av = end * seek_pts;
+ } else if (priv->avfc->duration != 0 &&
+ priv->avfc->duration != AV_NOPTS_VALUE)
+ {
+ seek_pts_av = seek_pts * priv->avfc->duration;
+ }
+ } else {
+ if (!(flags & SEEK_FORWARD))
+ seek_pts -= priv->seek_delay;
+ seek_pts_av = seek_pts * AV_TIME_BASE;
+ }
+
+ // Hack to make wav seeking "deterministic". Without this, features like
+ // backward playback won't work.
+ if (priv->pcm_seek_hack && !priv->pcm_seek_hack_packet_size) {
+ // This might for example be the initial seek. Fuck it up like the
+ // bullshit it is.
+ AVPacket *pkt = av_packet_alloc();
+ MP_HANDLE_OOM(pkt);
+ if (av_read_frame(priv->avfc, pkt) >= 0)
+ priv->pcm_seek_hack_packet_size = pkt->size;
+ av_packet_free(&pkt);
+ add_new_streams(demuxer);
+ }
+ if (priv->pcm_seek_hack && priv->pcm_seek_hack_packet_size &&
+ !(avsflags & AVSEEK_FLAG_BYTE))
+ {
+ int samples = priv->pcm_seek_hack_packet_size /
+ priv->pcm_seek_hack->codecpar->block_align;
+ if (samples > 0) {
+ MP_VERBOSE(demuxer, "using bullshit libavformat PCM seek hack\n");
+ double pts = seek_pts_av / (double)AV_TIME_BASE;
+ seek_pts_av = pts / av_q2d(priv->pcm_seek_hack->time_base);
+ int64_t align = seek_pts_av % samples;
+ seek_pts_av -= align;
+ seek_stream = priv->pcm_seek_hack->index;
+ }
+ }
+
+ int r = av_seek_frame(priv->avfc, seek_stream, seek_pts_av, avsflags);
+ if (r < 0 && (avsflags & AVSEEK_FLAG_BACKWARD)) {
+ // When seeking before the beginning of the file, and seeking fails,
+ // try again without the backwards flag to make it seek to the
+ // beginning.
+ avsflags &= ~AVSEEK_FLAG_BACKWARD;
+ r = av_seek_frame(priv->avfc, seek_stream, seek_pts_av, avsflags);
+ }
+
+ if (r < 0) {
+ char buf[180];
+ av_strerror(r, buf, sizeof(buf));
+ MP_VERBOSE(demuxer, "Seek failed (%s)\n", buf);
+ }
+
+ update_read_stats(demuxer);
+}
+
+static void demux_lavf_switched_tracks(struct demuxer *demuxer)
+{
+ select_tracks(demuxer, 0);
+}
+
+static void demux_close_lavf(demuxer_t *demuxer)
+{
+ lavf_priv_t *priv = demuxer->priv;
+ if (priv) {
+ // This will be a dangling pointer; but see below.
+ AVIOContext *leaking = priv->avfc ? priv->avfc->pb : NULL;
+ avformat_close_input(&priv->avfc);
+ // The ffmpeg garbage breaks its own API yet again: hls.c will call
+ // io_open on the main playlist, but never calls io_close. This happens
+ // to work out for us (since we don't really use custom I/O), but it's
+ // still weird. Compensate.
+ if (priv->num_nested == 1 && priv->nested[0].id == leaking)
+ priv->num_nested = 0;
+ if (priv->num_nested) {
+ MP_WARN(demuxer, "Leaking %d nested connections (FFmpeg bug).\n",
+ priv->num_nested);
+ }
+ if (priv->pb)
+ av_freep(&priv->pb->buffer);
+ av_freep(&priv->pb);
+ for (int n = 0; n < priv->num_streams; n++) {
+ struct stream_info *info = priv->streams[n];
+ if (info->sh)
+ avcodec_parameters_free(&info->sh->codec->lav_codecpar);
+ }
+ if (priv->own_stream)
+ free_stream(priv->stream);
+ if (priv->av_opts)
+ av_dict_free(&priv->av_opts);
+ talloc_free(priv);
+ demuxer->priv = NULL;
+ }
+}
+
+
+const demuxer_desc_t demuxer_desc_lavf = {
+ .name = "lavf",
+ .desc = "libavformat",
+ .read_packet = demux_lavf_read_packet,
+ .open = demux_open_lavf,
+ .close = demux_close_lavf,
+ .seek = demux_seek_lavf,
+ .switched_tracks = demux_lavf_switched_tracks,
+};
diff --git a/demux/demux_libarchive.c b/demux/demux_libarchive.c
new file mode 100644
index 0000000..ec50498
--- /dev/null
+++ b/demux/demux_libarchive.c
@@ -0,0 +1,120 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <archive.h>
+#include <archive_entry.h>
+
+#include "common/common.h"
+#include "common/playlist.h"
+#include "options/m_config.h"
+#include "stream/stream.h"
+#include "misc/natural_sort.h"
+#include "demux.h"
+
+#include "stream/stream_libarchive.h"
+
+struct demux_libarchive_opts {
+ bool rar_list_all_volumes;
+};
+
+static int cmp_filename(const void *a, const void *b)
+{
+ return mp_natural_sort_cmp(*(char **)a, *(char **)b);
+}
+
+static int open_file(struct demuxer *demuxer, enum demux_check check)
+{
+ if (!demuxer->access_references)
+ return -1;
+
+ int flags = 0;
+ int probe_size = STREAM_BUFFER_SIZE;
+ if (check <= DEMUX_CHECK_REQUEST) {
+ flags |= MP_ARCHIVE_FLAG_UNSAFE;
+ probe_size *= 100;
+ }
+
+ void *probe = ta_alloc_size(NULL, probe_size);
+ if (!probe)
+ return -1;
+ int probe_got = stream_read_peek(demuxer->stream, probe, probe_size);
+ struct stream *probe_stream =
+ stream_memory_open(demuxer->global, probe, probe_got);
+ struct mp_archive *mpa = mp_archive_new(mp_null_log, probe_stream, flags, 0);
+ bool ok = !!mpa;
+ free_stream(probe_stream);
+ mp_archive_free(mpa);
+ ta_free(probe);
+ if (!ok)
+ return -1;
+
+ struct demux_libarchive_opts *opts =
+ mp_get_config_group(demuxer, demuxer->global, demuxer->desc->options);
+
+ if (!opts->rar_list_all_volumes)
+ flags |= MP_ARCHIVE_FLAG_NO_VOLUMES;
+
+ mpa = mp_archive_new(demuxer->log, demuxer->stream, flags, 0);
+ if (!mpa)
+ return -1;
+
+ struct playlist *pl = talloc_zero(demuxer, struct playlist);
+ demuxer->playlist = pl;
+
+ char *prefix = mp_url_escape(mpa, demuxer->stream->url, "~|");
+
+ char **files = NULL;
+ int num_files = 0;
+
+ while (mp_archive_next_entry(mpa)) {
+ // stream_libarchive.c does the real work
+ char *f = talloc_asprintf(mpa, "archive://%s|/%s", prefix,
+ mpa->entry_filename);
+ MP_TARRAY_APPEND(mpa, files, num_files, f);
+ }
+
+ if (files)
+ qsort(files, num_files, sizeof(files[0]), cmp_filename);
+
+ for (int n = 0; n < num_files; n++)
+ playlist_add_file(pl, files[n]);
+
+ playlist_set_stream_flags(pl, demuxer->stream_origin);
+
+ demuxer->filetype = "archive";
+ demuxer->fully_read = true;
+
+ mp_archive_free(mpa);
+ demux_close_stream(demuxer);
+
+ return 0;
+}
+
+#define OPT_BASE_STRUCT struct demux_libarchive_opts
+
+const struct demuxer_desc demuxer_desc_libarchive = {
+ .name = "libarchive",
+ .desc = "libarchive wrapper",
+ .open = open_file,
+ .options = &(const struct m_sub_options){
+ .opts = (const struct m_option[]) {
+ {"rar-list-all-volumes", OPT_BOOL(rar_list_all_volumes)},
+ {0}
+ },
+ .size = sizeof(OPT_BASE_STRUCT),
+ },
+};
diff --git a/demux/demux_mf.c b/demux/demux_mf.c
new file mode 100644
index 0000000..8f7cb70
--- /dev/null
+++ b/demux/demux_mf.c
@@ -0,0 +1,373 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <strings.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "misc/ctype.h"
+
+#include "stream/stream.h"
+#include "demux.h"
+#include "stheader.h"
+#include "codec_tags.h"
+
+#define MF_MAX_FILE_SIZE (1024 * 1024 * 256)
+
+typedef struct mf {
+ struct mp_log *log;
+ struct sh_stream *sh;
+ int curr_frame;
+ int nr_of_files;
+ char **names;
+ // optional
+ struct stream **streams;
+} mf_t;
+
+
+static void mf_add(mf_t *mf, const char *fname)
+{
+ char *entry = talloc_strdup(mf, fname);
+ MP_TARRAY_APPEND(mf, mf->names, mf->nr_of_files, entry);
+}
+
+static mf_t *open_mf_pattern(void *talloc_ctx, struct demuxer *d, char *filename)
+{
+ struct mp_log *log = d->log;
+ int error_count = 0;
+ int count = 0;
+
+ mf_t *mf = talloc_zero(talloc_ctx, mf_t);
+ mf->log = log;
+
+ if (filename[0] == '@') {
+ struct stream *s = stream_create(filename + 1,
+ d->stream_origin | STREAM_READ, d->cancel, d->global);
+ if (s) {
+ while (1) {
+ char buf[512];
+ int len = stream_read_peek(s, buf, sizeof(buf));
+ if (!len)
+ break;
+ bstr data = (bstr){buf, len};
+ int pos = bstrchr(data, '\n');
+ data = bstr_splice(data, 0, pos < 0 ? data.len : pos + 1);
+ bstr fname = bstr_strip(data);
+ if (fname.len) {
+ if (bstrchr(fname, '\0') >= 0) {
+ mp_err(log, "invalid filename\n");
+ break;
+ }
+ char *entry = bstrto0(mf, fname);
+ if (!mp_path_exists(entry)) {
+ mp_verbose(log, "file not found: '%s'\n", entry);
+ } else {
+ MP_TARRAY_APPEND(mf, mf->names, mf->nr_of_files, entry);
+ }
+ }
+ stream_seek_skip(s, stream_tell(s) + data.len);
+ }
+ free_stream(s);
+
+ mp_info(log, "number of files: %d\n", mf->nr_of_files);
+ goto exit_mf;
+ }
+ mp_info(log, "%s is not indirect filelist\n", filename + 1);
+ }
+
+ if (strchr(filename, ',')) {
+ mp_info(log, "filelist: %s\n", filename);
+ bstr bfilename = bstr0(filename);
+
+ while (bfilename.len) {
+ bstr bfname;
+ bstr_split_tok(bfilename, ",", &bfname, &bfilename);
+ char *fname2 = bstrdup0(mf, bfname);
+
+ if (!mp_path_exists(fname2))
+ mp_verbose(log, "file not found: '%s'\n", fname2);
+ else {
+ mf_add(mf, fname2);
+ }
+ talloc_free(fname2);
+ }
+ mp_info(log, "number of files: %d\n", mf->nr_of_files);
+
+ goto exit_mf;
+ }
+
+ size_t fname_avail = strlen(filename) + 32;
+ char *fname = talloc_size(mf, fname_avail);
+
+#if HAVE_GLOB
+ if (!strchr(filename, '%')) {
+ // append * if none present
+ snprintf(fname, fname_avail, "%s%c", filename,
+ strchr(filename, '*') ? 0 : '*');
+ mp_info(log, "search expr: %s\n", fname);
+
+ glob_t gg;
+ if (glob(fname, 0, NULL, &gg)) {
+ talloc_free(mf);
+ return NULL;
+ }
+
+ for (int i = 0; i < gg.gl_pathc; i++) {
+ if (mp_path_isdir(gg.gl_pathv[i]))
+ continue;
+ mf_add(mf, gg.gl_pathv[i]);
+ }
+ mp_info(log, "number of files: %d\n", mf->nr_of_files);
+ globfree(&gg);
+ goto exit_mf;
+ }
+#endif
+
+ // We're using arbitrary user input as printf format with 1 int argument.
+ // Any format which uses exactly 1 int argument would be valid, but for
+ // simplicity we reject all conversion specifiers except %% and simple
+ // integer specifier: %[.][NUM]d where NUM is 1-3 digits (%.d is valid)
+ const char *f = filename;
+ int MAXDIGS = 3, nspec = 0, c;
+ bool bad_spec = false;
+
+ while (nspec < 2 && (c = *f++)) {
+ if (c != '%')
+ continue;
+
+ if (*f == '%') {
+ // '%%', which ends up as an explicit % in the output.
+ // Skipping forwards as it doesn't require further attention.
+ f++;
+ continue;
+ }
+
+ // Now c == '%' and *f != '%', thus we have entered territory of format
+ // specifiers which we are interested in.
+ nspec++;
+
+ if (*f == '.')
+ f++;
+
+ for (int ndig = 0; mp_isdigit(*f) && ndig < MAXDIGS; ndig++, f++)
+ /* no-op */;
+
+ if (*f != 'd') {
+ bad_spec = true; // not int, or beyond our validation capacity
+ break;
+ }
+
+ // *f is 'd'
+ f++;
+ }
+
+ // nspec==0 (zero specifiers) is rejected because fname wouldn't advance.
+ if (bad_spec || nspec != 1) {
+ mp_err(log, "unsupported expr format: '%s'\n", filename);
+ goto exit_mf;
+ }
+
+ mp_info(log, "search expr: %s\n", filename);
+
+ while (error_count < 5) {
+ if (snprintf(fname, fname_avail, filename, count++) >= fname_avail) {
+ mp_err(log, "format result too long: '%s'\n", filename);
+ goto exit_mf;
+ }
+ if (!mp_path_exists(fname)) {
+ error_count++;
+ mp_verbose(log, "file not found: '%s'\n", fname);
+ } else {
+ mf_add(mf, fname);
+ }
+ }
+
+ mp_info(log, "number of files: %d\n", mf->nr_of_files);
+
+exit_mf:
+ return mf;
+}
+
+static mf_t *open_mf_single(void *talloc_ctx, struct mp_log *log, char *filename)
+{
+ mf_t *mf = talloc_zero(talloc_ctx, mf_t);
+ mf->log = log;
+ mf_add(mf, filename);
+ return mf;
+}
+
+static void demux_seek_mf(demuxer_t *demuxer, double seek_pts, int flags)
+{
+ mf_t *mf = demuxer->priv;
+ double newpos = seek_pts * mf->sh->codec->fps;
+ if (flags & SEEK_FACTOR)
+ newpos = seek_pts * (mf->nr_of_files - 1);
+ if (flags & SEEK_FORWARD) {
+ newpos = ceil(newpos);
+ } else {
+ newpos = MPMIN(floor(newpos), mf->nr_of_files - 1);
+ }
+ mf->curr_frame = MPCLAMP((int)newpos, 0, mf->nr_of_files);
+}
+
+static bool demux_mf_read_packet(struct demuxer *demuxer,
+ struct demux_packet **pkt)
+{
+ mf_t *mf = demuxer->priv;
+ if (mf->curr_frame >= mf->nr_of_files)
+ return false;
+ bool ok = false;
+
+ struct stream *entry_stream = NULL;
+ if (mf->streams)
+ entry_stream = mf->streams[mf->curr_frame];
+ struct stream *stream = entry_stream;
+ if (!stream) {
+ char *filename = mf->names[mf->curr_frame];
+ if (filename) {
+ stream = stream_create(filename, demuxer->stream_origin | STREAM_READ,
+ demuxer->cancel, demuxer->global);
+ }
+ }
+
+ if (stream) {
+ stream_seek(stream, 0);
+ bstr data = stream_read_complete(stream, NULL, MF_MAX_FILE_SIZE);
+ if (data.len) {
+ demux_packet_t *dp = new_demux_packet(data.len);
+ if (dp) {
+ memcpy(dp->buffer, data.start, data.len);
+ dp->pts = mf->curr_frame / mf->sh->codec->fps;
+ dp->keyframe = true;
+ dp->stream = mf->sh->index;
+ *pkt = dp;
+ ok = true;
+ }
+ }
+ talloc_free(data.start);
+ }
+
+ if (stream && stream != entry_stream)
+ free_stream(stream);
+
+ mf->curr_frame++;
+
+ if (!ok)
+ MP_ERR(demuxer, "error reading image file\n");
+
+ return true;
+}
+
+static const char *probe_format(mf_t *mf, char *type, enum demux_check check)
+{
+ if (check > DEMUX_CHECK_REQUEST)
+ return NULL;
+ char *org_type = type;
+ if (!type || !type[0]) {
+ char *p = strrchr(mf->names[0], '.');
+ if (p)
+ type = p + 1;
+ }
+ const char *codec = mp_map_type_to_image_codec(type);
+ if (codec)
+ return codec;
+ if (check == DEMUX_CHECK_REQUEST) {
+ if (!org_type) {
+ MP_ERR(mf, "file type was not set! (try --mf-type=ext)\n");
+ } else {
+ MP_ERR(mf, "--mf-type set to an unknown codec!\n");
+ }
+ }
+ return NULL;
+}
+
+static int demux_open_mf(demuxer_t *demuxer, enum demux_check check)
+{
+ mf_t *mf;
+
+ if (strncmp(demuxer->stream->url, "mf://", 5) == 0 &&
+ demuxer->stream->info && strcmp(demuxer->stream->info->name, "mf") == 0)
+ {
+ mf = open_mf_pattern(demuxer, demuxer, demuxer->stream->url + 5);
+ } else {
+ mf = open_mf_single(demuxer, demuxer->log, demuxer->stream->url);
+ int bog = 0;
+ MP_TARRAY_APPEND(mf, mf->streams, bog, demuxer->stream);
+ }
+
+ if (!mf || mf->nr_of_files < 1)
+ goto error;
+
+ const char *codec = mp_map_mimetype_to_video_codec(demuxer->stream->mime_type);
+ if (!codec || (demuxer->opts->mf_type && demuxer->opts->mf_type[0]))
+ codec = probe_format(mf, demuxer->opts->mf_type, check);
+ if (!codec)
+ goto error;
+
+ mf->curr_frame = 0;
+
+ // create a new video stream header
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO);
+ if (mf->nr_of_files == 1) {
+ MP_VERBOSE(demuxer, "Assuming this is an image format.\n");
+ sh->image = true;
+ }
+
+ struct mp_codec_params *c = sh->codec;
+ c->codec = codec;
+ c->disp_w = 0;
+ c->disp_h = 0;
+ c->fps = demuxer->opts->mf_fps;
+ c->reliable_fps = true;
+
+ demux_add_sh_stream(demuxer, sh);
+
+ mf->sh = sh;
+ demuxer->priv = (void *)mf;
+ demuxer->seekable = true;
+ demuxer->duration = mf->nr_of_files / mf->sh->codec->fps;
+
+ return 0;
+
+error:
+ return -1;
+}
+
+static void demux_close_mf(demuxer_t *demuxer)
+{
+}
+
+const demuxer_desc_t demuxer_desc_mf = {
+ .name = "mf",
+ .desc = "image files (mf)",
+ .read_packet = demux_mf_read_packet,
+ .open = demux_open_mf,
+ .close = demux_close_mf,
+ .seek = demux_seek_mf,
+};
diff --git a/demux/demux_mkv.c b/demux/demux_mkv.c
new file mode 100644
index 0000000..41226c5
--- /dev/null
+++ b/demux/demux_mkv.c
@@ -0,0 +1,3392 @@
+/*
+ * Matroska demuxer
+ * Copyright (C) 2004 Aurelien Jacobs <aurel@gnuage.org>
+ * Based on the one written by Ronald Bultje for gstreamer
+ * and on demux_mkv.cpp from Moritz Bunkus.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+#include <libavutil/lzo.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/avstring.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavcodec/version.h>
+
+#include "config.h"
+
+#if HAVE_ZLIB
+#include <zlib.h>
+#endif
+
+#include "mpv_talloc.h"
+#include "common/av_common.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/options.h"
+#include "misc/bstr.h"
+#include "stream/stream.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "demux.h"
+#include "stheader.h"
+#include "ebml.h"
+#include "matroska.h"
+#include "codec_tags.h"
+
+#include "common/msg.h"
+
+static const unsigned char sipr_swaps[38][2] = {
+ {0,63},{1,22},{2,44},{3,90},{5,81},{7,31},{8,86},{9,58},{10,36},{12,68},
+ {13,39},{14,73},{15,53},{16,69},{17,57},{19,88},{20,34},{21,71},{24,46},
+ {25,94},{26,54},{28,75},{29,50},{32,70},{33,92},{35,74},{38,85},{40,56},
+ {42,87},{43,65},{45,59},{48,79},{49,93},{51,89},{55,95},{61,76},{67,83},
+ {77,80}
+};
+
+// Map flavour to bytes per second
+#define SIPR_FLAVORS 4
+#define ATRC_FLAVORS 8
+#define COOK_FLAVORS 34
+static const int sipr_fl2bps[SIPR_FLAVORS] = { 813, 1062, 625, 2000 };
+static const int atrc_fl2bps[ATRC_FLAVORS] = {
+ 8269, 11714, 13092, 16538, 18260, 22050, 33075, 44100 };
+static const int cook_fl2bps[COOK_FLAVORS] = {
+ 1000, 1378, 2024, 2584, 4005, 5513, 8010, 4005, 750, 2498,
+ 4048, 5513, 8010, 11973, 8010, 2584, 4005, 2067, 2584, 2584,
+ 4005, 4005, 5513, 5513, 8010, 12059, 1550, 8010, 12059, 5513,
+ 12016, 16408, 22911, 33506
+};
+
+enum {
+ MAX_NUM_LACES = 256,
+};
+
+typedef struct mkv_content_encoding {
+ uint64_t order, type, scope;
+ uint64_t comp_algo;
+ uint8_t *comp_settings;
+ int comp_settings_len;
+} mkv_content_encoding_t;
+
+typedef struct mkv_track {
+ int tnum;
+ uint64_t uid;
+ char *name;
+ struct sh_stream *stream;
+
+ char *codec_id;
+ char *language;
+
+ int type;
+
+ uint32_t v_width, v_height, v_dwidth, v_dheight;
+ bool v_dwidth_set, v_dheight_set;
+ double v_frate;
+ uint32_t colorspace;
+ int stereo_mode;
+ struct mp_colorspace color;
+ uint32_t v_crop_top, v_crop_left, v_crop_right, v_crop_bottom;
+ float v_projection_pose_roll;
+ bool v_projection_pose_roll_set;
+
+ uint32_t a_channels, a_bps;
+ float a_sfreq;
+ float a_osfreq;
+
+ double default_duration;
+ double codec_delay;
+
+ int default_track;
+ int forced_track;
+
+ unsigned char *private_data;
+ unsigned int private_size;
+
+ bool parse;
+ int64_t parse_timebase;
+ void *parser_tmp;
+ AVCodecParserContext *av_parser;
+ AVCodecContext *av_parser_codec;
+
+ bool require_keyframes;
+
+ /* stuff for realaudio braincancer */
+ double ra_pts; /* previous audio timestamp */
+ uint32_t sub_packet_size; ///< sub packet size, per stream
+ uint32_t sub_packet_h; ///< number of coded frames per block
+ uint32_t coded_framesize; ///< coded frame size, per stream
+ uint32_t audiopk_size; ///< audio packet size
+ unsigned char *audio_buf; ///< place to store reordered audio data
+ double *audio_timestamp; ///< timestamp for each audio packet
+ uint32_t sub_packet_cnt; ///< number of subpacket already received
+
+ /* generic content encoding support */
+ mkv_content_encoding_t *encodings;
+ int num_encodings;
+
+ /* latest added index entry for this track */
+ size_t last_index_entry;
+} mkv_track_t;
+
+typedef struct mkv_index {
+ int tnum;
+ int64_t timecode, duration;
+ uint64_t filepos; // position of the cluster which contains the packet
+} mkv_index_t;
+
+struct block_info {
+ uint64_t duration, discardpadding;
+ bool simple, keyframe, duration_known;
+ int64_t timecode;
+ mkv_track_t *track;
+ // Actual packet data.
+ AVBufferRef *laces[MAX_NUM_LACES];
+ int num_laces;
+ int64_t filepos;
+ struct ebml_block_additions *additions;
+};
+
+typedef struct mkv_demuxer {
+ struct demux_mkv_opts *opts;
+
+ int64_t segment_start, segment_end;
+
+ double duration;
+
+ mkv_track_t **tracks;
+ int num_tracks;
+
+ struct ebml_tags *tags;
+
+ int64_t tc_scale, cluster_tc;
+
+ uint64_t cluster_start;
+ uint64_t cluster_end;
+
+ mkv_index_t *indexes;
+ size_t num_indexes;
+ bool index_complete;
+
+ int edition_id;
+
+ struct header_elem {
+ int32_t id;
+ int64_t pos;
+ bool parsed;
+ } *headers;
+ int num_headers;
+
+ int64_t skip_to_timecode;
+ int v_skip_to_keyframe, a_skip_to_keyframe;
+ int a_skip_preroll;
+ int subtitle_preroll;
+
+ bool index_has_durations;
+
+ bool eof_warning, keyframe_warning;
+
+ // Small queue of read but not yet returned packets. This is mostly
+ // temporary data, and not normally larger than 0 or 1 elements.
+ struct block_info *blocks;
+ int num_blocks;
+
+ // Packets to return.
+ struct demux_packet **packets;
+ int num_packets;
+
+ bool probably_webm_dash_init;
+} mkv_demuxer_t;
+
+#define OPT_BASE_STRUCT struct demux_mkv_opts
+struct demux_mkv_opts {
+ int subtitle_preroll;
+ double subtitle_preroll_secs;
+ double subtitle_preroll_secs_index;
+ int probe_duration;
+ bool probe_start_time;
+};
+
+const struct m_sub_options demux_mkv_conf = {
+ .opts = (const m_option_t[]) {
+ {"subtitle-preroll", OPT_CHOICE(subtitle_preroll,
+ {"no", 0}, {"yes", 1}, {"index", 2})},
+ {"subtitle-preroll-secs", OPT_DOUBLE(subtitle_preroll_secs),
+ M_RANGE(0, DBL_MAX)},
+ {"subtitle-preroll-secs-index", OPT_DOUBLE(subtitle_preroll_secs_index),
+ M_RANGE(0, DBL_MAX)},
+ {"probe-video-duration", OPT_CHOICE(probe_duration,
+ {"no", 0}, {"yes", 1}, {"full", 2})},
+ {"probe-start-time", OPT_BOOL(probe_start_time)},
+ {0}
+ },
+ .size = sizeof(struct demux_mkv_opts),
+ .defaults = &(const struct demux_mkv_opts){
+ .subtitle_preroll = 2,
+ .subtitle_preroll_secs = 1.0,
+ .subtitle_preroll_secs_index = 10.0,
+ .probe_start_time = true,
+ },
+};
+
+#define REALHEADER_SIZE 16
+#define RVPROPERTIES_SIZE 34
+#define RAPROPERTIES4_SIZE 56
+#define RAPROPERTIES5_SIZE 70
+
+// Maximum number of subtitle packets that are accepted for pre-roll.
+// (Subtitle packets added before first A/V keyframe packet is found with seek.)
+#define NUM_SUB_PREROLL_PACKETS 500
+
+static void probe_last_timestamp(struct demuxer *demuxer, int64_t start_pos);
+static void probe_first_timestamp(struct demuxer *demuxer);
+static int read_next_block_into_queue(demuxer_t *demuxer);
+static void free_block(struct block_info *block);
+
+static void add_packet(struct demuxer *demuxer, struct sh_stream *stream,
+ struct demux_packet *pkt)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+ if (!pkt)
+ return;
+
+ pkt->stream = stream->index;
+ MP_TARRAY_APPEND(mkv_d, mkv_d->packets, mkv_d->num_packets, pkt);
+}
+
+#define AAC_SYNC_EXTENSION_TYPE 0x02b7
+static int aac_get_sample_rate_index(uint32_t sample_rate)
+{
+ static const int srates[] = {
+ 92017, 75132, 55426, 46009, 37566, 27713,
+ 23004, 18783, 13856, 11502, 9391, 0
+ };
+ int i = 0;
+ while (sample_rate < srates[i])
+ i++;
+ return i;
+}
+
+static bstr demux_mkv_decode(struct mp_log *log, mkv_track_t *track,
+ bstr data, uint32_t type)
+{
+ uint8_t *src = data.start;
+ uint8_t *orig_src = src;
+ uint8_t *dest = src;
+ uint32_t size = data.len;
+
+ for (int i = 0; i < track->num_encodings; i++) {
+ struct mkv_content_encoding *enc = track->encodings + i;
+ if (!(enc->scope & type))
+ continue;
+
+ if (src != dest && src != orig_src)
+ talloc_free(src);
+ src = dest; // output from last iteration is new source
+
+ if (enc->comp_algo == 0) {
+#if HAVE_ZLIB
+ /* zlib encoded track */
+
+ if (size == 0)
+ continue;
+
+ z_stream zstream;
+
+ zstream.zalloc = (alloc_func) 0;
+ zstream.zfree = (free_func) 0;
+ zstream.opaque = (voidpf) 0;
+ if (inflateInit(&zstream) != Z_OK) {
+ mp_warn(log, "zlib initialization failed.\n");
+ goto error;
+ }
+ zstream.next_in = (Bytef *) src;
+ zstream.avail_in = size;
+
+ dest = NULL;
+ zstream.avail_out = size;
+ int result;
+ do {
+ if (size >= INT_MAX - 4000) {
+ talloc_free(dest);
+ dest = NULL;
+ inflateEnd(&zstream);
+ goto error;
+ }
+ size += 4000;
+ dest = talloc_realloc_size(track->parser_tmp, dest, size);
+ zstream.next_out = (Bytef *) (dest + zstream.total_out);
+ result = inflate(&zstream, Z_NO_FLUSH);
+ if (result != Z_OK && result != Z_STREAM_END) {
+ mp_warn(log, "zlib decompression failed.\n");
+ talloc_free(dest);
+ dest = NULL;
+ inflateEnd(&zstream);
+ goto error;
+ }
+ zstream.avail_out += 4000;
+ } while (zstream.avail_out == 4000 && zstream.avail_in != 0
+ && result != Z_STREAM_END);
+
+ size = zstream.total_out;
+ inflateEnd(&zstream);
+#endif
+ } else if (enc->comp_algo == 2) {
+ /* lzo encoded track */
+ int out_avail;
+ int maxlen = INT_MAX - AV_LZO_OUTPUT_PADDING;
+ if (size >= maxlen / 3)
+ goto error;
+ int dstlen = size * 3;
+
+ dest = NULL;
+ while (1) {
+ int srclen = size;
+ dest = talloc_realloc_size(track->parser_tmp, dest,
+ dstlen + AV_LZO_OUTPUT_PADDING);
+ out_avail = dstlen;
+ int result = av_lzo1x_decode(dest, &out_avail, src, &srclen);
+ if (result == 0)
+ break;
+ if (!(result & AV_LZO_OUTPUT_FULL)) {
+ mp_warn(log, "lzo decompression failed.\n");
+ talloc_free(dest);
+ dest = NULL;
+ goto error;
+ }
+ mp_trace(log, "lzo decompression buffer too small.\n");
+ if (dstlen >= maxlen / 2) {
+ talloc_free(dest);
+ dest = NULL;
+ goto error;
+ }
+ dstlen = MPMAX(1, 2 * dstlen);
+ }
+ size = dstlen - out_avail;
+ } else if (enc->comp_algo == 3) {
+ dest = talloc_size(track->parser_tmp, size + enc->comp_settings_len);
+ memcpy(dest, enc->comp_settings, enc->comp_settings_len);
+ memcpy(dest + enc->comp_settings_len, src, size);
+ size += enc->comp_settings_len;
+ }
+ }
+
+ error:
+ if (src != dest && src != orig_src)
+ talloc_free(src);
+ if (!size)
+ dest = NULL;
+ return (bstr){dest, size};
+}
+
+
+static int demux_mkv_read_info(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+ stream_t *s = demuxer->stream;
+ int res = 0;
+
+ MP_DBG(demuxer, "|+ segment information...\n");
+
+ mkv_d->tc_scale = 1000000;
+ mkv_d->duration = 0;
+
+ struct ebml_info info = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ if (ebml_read_element(s, &parse_ctx, &info, &ebml_info_desc) < 0)
+ return -1;
+ if (info.muxing_app)
+ MP_DBG(demuxer, "| + muxing app: %s\n", info.muxing_app);
+ if (info.writing_app)
+ MP_DBG(demuxer, "| + writing app: %s\n", info.writing_app);
+ if (info.n_timecode_scale) {
+ mkv_d->tc_scale = info.timecode_scale;
+ MP_DBG(demuxer, "| + timecode scale: %"PRId64"\n", mkv_d->tc_scale);
+ if (mkv_d->tc_scale < 1 || mkv_d->tc_scale > INT_MAX) {
+ res = -1;
+ goto out;
+ }
+ }
+ if (info.n_duration) {
+ mkv_d->duration = info.duration * mkv_d->tc_scale / 1e9;
+ MP_DBG(demuxer, "| + duration: %.3fs\n",
+ mkv_d->duration);
+ demuxer->duration = mkv_d->duration;
+ }
+ if (info.title) {
+ mp_tags_set_str(demuxer->metadata, "TITLE", info.title);
+ }
+ if (info.n_segment_uid) {
+ size_t len = info.segment_uid.len;
+ if (len != sizeof(demuxer->matroska_data.uid.segment)) {
+ MP_INFO(demuxer, "segment uid invalid length %zu\n", len);
+ } else {
+ memcpy(demuxer->matroska_data.uid.segment, info.segment_uid.start,
+ len);
+ MP_DBG(demuxer, "| + segment uid");
+ for (size_t i = 0; i < len; i++)
+ MP_DBG(demuxer, " %02x",
+ demuxer->matroska_data.uid.segment[i]);
+ MP_DBG(demuxer, "\n");
+ }
+ }
+ if (demuxer->params && demuxer->params->matroska_wanted_uids) {
+ if (info.n_segment_uid) {
+ for (int i = 0; i < demuxer->params->matroska_num_wanted_uids; i++) {
+ struct matroska_segment_uid *uid = demuxer->params->matroska_wanted_uids + i;
+ if (!memcmp(info.segment_uid.start, uid->segment, 16)) {
+ demuxer->matroska_data.uid.edition = uid->edition;
+ goto out;
+ }
+ }
+ }
+ MP_VERBOSE(demuxer, "This is not one of the wanted files. "
+ "Stopping attempt to open.\n");
+ res = -2;
+ }
+ out:
+ talloc_free(parse_ctx.talloc_ctx);
+ return res;
+}
+
+static void parse_trackencodings(struct demuxer *demuxer,
+ struct mkv_track *track,
+ struct ebml_content_encodings *encodings)
+{
+ // initial allocation to be a non-NULL context before realloc
+ mkv_content_encoding_t *ce = talloc_size(track, 1);
+
+ for (int n_enc = 0; n_enc < encodings->n_content_encoding; n_enc++) {
+ struct ebml_content_encoding *enc = encodings->content_encoding + n_enc;
+ struct mkv_content_encoding e = {0};
+ e.order = enc->content_encoding_order;
+ if (enc->n_content_encoding_scope)
+ e.scope = enc->content_encoding_scope;
+ else
+ e.scope = 1;
+ e.type = enc->content_encoding_type;
+
+ if (enc->n_content_compression) {
+ struct ebml_content_compression *z = &enc->content_compression;
+ e.comp_algo = z->content_comp_algo;
+ if (z->n_content_comp_settings) {
+ int sz = z->content_comp_settings.len;
+ e.comp_settings = talloc_size(ce, sz);
+ memcpy(e.comp_settings, z->content_comp_settings.start, sz);
+ e.comp_settings_len = sz;
+ }
+ }
+
+ if (e.type == 1) {
+ MP_WARN(demuxer, "Track "
+ "number %d has been encrypted and "
+ "decryption has not yet been\n"
+ "implemented. Skipping track.\n",
+ track->tnum);
+ } else if (e.type != 0) {
+ MP_WARN(demuxer, "Unknown content encoding type for "
+ "track %u. Skipping track.\n",
+ track->tnum);
+ } else if (e.comp_algo != 0 && e.comp_algo != 2 && e.comp_algo != 3) {
+ MP_WARN(demuxer, "Track %d has been compressed with "
+ "an unknown/unsupported compression\n"
+ "algorithm (%"PRIu64"). Skipping track.\n",
+ track->tnum, e.comp_algo);
+ }
+#if !HAVE_ZLIB
+ else if (e.comp_algo == 0) {
+ MP_WARN(demuxer, "Track %d was compressed with zlib "
+ "but mpv has not been compiled\n"
+ "with support for zlib compression. "
+ "Skipping track.\n",
+ track->tnum);
+ }
+#endif
+ int i;
+ for (i = 0; i < n_enc; i++) {
+ if (e.order >= ce[i].order)
+ break;
+ }
+ ce = talloc_realloc(track, ce, mkv_content_encoding_t, n_enc + 1);
+ memmove(ce + i + 1, ce + i, (n_enc - i) * sizeof(*ce));
+ memcpy(ce + i, &e, sizeof(e));
+ }
+
+ track->encodings = ce;
+ track->num_encodings = encodings->n_content_encoding;
+}
+
+static void parse_trackaudio(struct demuxer *demuxer, struct mkv_track *track,
+ struct ebml_audio *audio)
+{
+ if (audio->n_sampling_frequency) {
+ track->a_sfreq = audio->sampling_frequency;
+ MP_DBG(demuxer, "| + Sampling frequency: %f\n", track->a_sfreq);
+ } else {
+ track->a_sfreq = 8000;
+ }
+ if (audio->n_output_sampling_frequency) {
+ track->a_osfreq = audio->output_sampling_frequency;
+ MP_DBG(demuxer, "| + Output sampling frequency: %f\n", track->a_osfreq);
+ } else {
+ track->a_osfreq = track->a_sfreq;
+ }
+ if (audio->n_bit_depth) {
+ track->a_bps = audio->bit_depth;
+ MP_DBG(demuxer, "| + Bit depth: %"PRIu32"\n", track->a_bps);
+ }
+ if (audio->n_channels) {
+ track->a_channels = audio->channels;
+ MP_DBG(demuxer, "| + Channels: %"PRIu32"\n", track->a_channels);
+ } else {
+ track->a_channels = 1;
+ }
+}
+
+static void parse_trackcolour(struct demuxer *demuxer, struct mkv_track *track,
+ struct ebml_colour *colour)
+{
+ // Note: As per matroska spec, the order is consistent with ISO/IEC
+ // 23001-8:2013/DCOR1, which is the same order used by libavutil/pixfmt.h,
+ // so we can just re-use our avcol_ conversion functions.
+ if (colour->n_matrix_coefficients) {
+ track->color.space = avcol_spc_to_mp_csp(colour->matrix_coefficients);
+ MP_DBG(demuxer, "| + Matrix: %s\n",
+ m_opt_choice_str(mp_csp_names, track->color.space));
+ }
+ if (colour->n_primaries) {
+ track->color.primaries = avcol_pri_to_mp_csp_prim(colour->primaries);
+ MP_DBG(demuxer, "| + Primaries: %s\n",
+ m_opt_choice_str(mp_csp_prim_names, track->color.primaries));
+ }
+ if (colour->n_transfer_characteristics) {
+ track->color.gamma = avcol_trc_to_mp_csp_trc(colour->transfer_characteristics);
+ MP_DBG(demuxer, "| + Gamma: %s\n",
+ m_opt_choice_str(mp_csp_trc_names, track->color.gamma));
+ }
+ if (colour->n_range) {
+ track->color.levels = avcol_range_to_mp_csp_levels(colour->range);
+ MP_DBG(demuxer, "| + Levels: %s\n",
+ m_opt_choice_str(mp_csp_levels_names, track->color.levels));
+ }
+ if (colour->n_max_cll) {
+ track->color.hdr.max_cll = colour->max_cll;
+ MP_DBG(demuxer, "| + MaxCLL: %"PRIu64"\n", colour->max_cll);
+ }
+ if (colour->n_max_fall) {
+ track->color.hdr.max_fall = colour->max_fall;
+ MP_DBG(demuxer, "| + MaxFALL: %"PRIu64"\n", colour->max_cll);
+ }
+ if (colour->n_mastering_metadata) {
+ struct ebml_mastering_metadata *mastering = &colour->mastering_metadata;
+
+ if (mastering->n_primary_r_chromaticity_x) {
+ track->color.hdr.prim.red.x = mastering->primary_r_chromaticity_x;
+ MP_DBG(demuxer, "| + PrimaryRChromaticityX: %f\n", track->color.hdr.prim.red.x);
+ }
+ if (mastering->n_primary_r_chromaticity_y) {
+ track->color.hdr.prim.red.y = mastering->primary_r_chromaticity_y;
+ MP_DBG(demuxer, "| + PrimaryRChromaticityY: %f\n", track->color.hdr.prim.red.y);
+ }
+ if (mastering->n_primary_g_chromaticity_x) {
+ track->color.hdr.prim.green.x = mastering->primary_g_chromaticity_x;
+ MP_DBG(demuxer, "| + PrimaryGChromaticityX: %f\n", track->color.hdr.prim.green.x);
+ }
+ if (mastering->n_primary_g_chromaticity_y) {
+ track->color.hdr.prim.green.y = mastering->primary_g_chromaticity_y;
+ MP_DBG(demuxer, "| + PrimaryGChromaticityY: %f\n", track->color.hdr.prim.green.y);
+ }
+ if (mastering->n_primary_b_chromaticity_x) {
+ track->color.hdr.prim.blue.x = mastering->primary_b_chromaticity_x;
+ MP_DBG(demuxer, "| + PrimaryBChromaticityX: %f\n", track->color.hdr.prim.blue.x);
+ }
+ if (mastering->n_primary_b_chromaticity_y) {
+ track->color.hdr.prim.blue.y = mastering->primary_b_chromaticity_y;
+ MP_DBG(demuxer, "| + PrimaryBChromaticityY: %f\n", track->color.hdr.prim.blue.y);
+ }
+ if (mastering->n_white_point_chromaticity_x) {
+ track->color.hdr.prim.white.x = mastering->white_point_chromaticity_x;
+ MP_DBG(demuxer, "| + WhitePointChromaticityX: %f\n", track->color.hdr.prim.white.x);
+ }
+ if (mastering->n_white_point_chromaticity_y) {
+ track->color.hdr.prim.white.y = mastering->white_point_chromaticity_y;
+ MP_DBG(demuxer, "| + WhitePointChromaticityY: %f\n", track->color.hdr.prim.white.y);
+ }
+ if (mastering->n_luminance_min) {
+ track->color.hdr.min_luma = mastering->luminance_min;
+ MP_DBG(demuxer, "| + LuminanceMin: %f\n", track->color.hdr.min_luma);
+ }
+ if (mastering->n_luminance_max) {
+ track->color.hdr.max_luma = mastering->luminance_max;
+ MP_DBG(demuxer, "| + LuminanceMax: %f\n", track->color.hdr.max_luma);
+ }
+ }
+}
+
+static void parse_trackprojection(struct demuxer *demuxer, struct mkv_track *track,
+ struct ebml_projection *projection)
+{
+ if (projection->n_projection_pose_yaw || projection->n_projection_pose_pitch)
+ MP_WARN(demuxer, "Projection pose yaw/pitch not supported!\n");
+
+ if (projection->n_projection_pose_roll) {
+ track->v_projection_pose_roll = projection->projection_pose_roll;
+ track->v_projection_pose_roll_set = true;
+ MP_DBG(demuxer, "| + Projection pose roll: %f\n",
+ track->v_projection_pose_roll);
+ }
+}
+
+static void parse_trackvideo(struct demuxer *demuxer, struct mkv_track *track,
+ struct ebml_video *video)
+{
+ if (video->n_frame_rate) {
+ MP_DBG(demuxer, "| + Frame rate: %f (ignored)\n", video->frame_rate);
+ }
+ if (video->n_display_width) {
+ track->v_dwidth = video->display_width;
+ track->v_dwidth_set = true;
+ MP_DBG(demuxer, "| + Display width: %"PRIu32"\n", track->v_dwidth);
+ }
+ if (video->n_display_height) {
+ track->v_dheight = video->display_height;
+ track->v_dheight_set = true;
+ MP_DBG(demuxer, "| + Display height: %"PRIu32"\n", track->v_dheight);
+ }
+ if (video->n_pixel_width) {
+ track->v_width = video->pixel_width;
+ MP_DBG(demuxer, "| + Pixel width: %"PRIu32"\n", track->v_width);
+ }
+ if (video->n_pixel_height) {
+ track->v_height = video->pixel_height;
+ MP_DBG(demuxer, "| + Pixel height: %"PRIu32"\n", track->v_height);
+ }
+ if (video->n_colour_space && video->colour_space.len == 4) {
+ uint8_t *d = (uint8_t *)&video->colour_space.start[0];
+ track->colorspace = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24);
+ MP_DBG(demuxer, "| + Colorspace: %#"PRIx32"\n", track->colorspace);
+ }
+ if (video->n_stereo_mode) {
+ const char *name = MP_STEREO3D_NAME(video->stereo_mode);
+ if (name) {
+ track->stereo_mode = video->stereo_mode;
+ MP_DBG(demuxer, "| + StereoMode: %s\n", name);
+ } else {
+ MP_WARN(demuxer, "Unknown StereoMode: %"PRIu64"\n",
+ video->stereo_mode);
+ }
+ }
+ if (video->n_pixel_crop_top) {
+ track->v_crop_top = video->pixel_crop_top;
+ MP_DBG(demuxer, "| + Crop top: %"PRIu32"\n", track->v_crop_top);
+ }
+ if (video->n_pixel_crop_left) {
+ track->v_crop_left = video->pixel_crop_left;
+ MP_DBG(demuxer, "| + Crop left: %"PRIu32"\n", track->v_crop_left);
+ }
+ if (video->n_pixel_crop_right) {
+ track->v_crop_right = video->pixel_crop_right;
+ MP_DBG(demuxer, "| + Crop right: %"PRIu32"\n", track->v_crop_right);
+ }
+ if (video->n_pixel_crop_bottom) {
+ track->v_crop_bottom = video->pixel_crop_bottom;
+ MP_DBG(demuxer, "| + Crop bottom: %"PRIu32"\n", track->v_crop_bottom);
+ }
+ if (video->n_colour)
+ parse_trackcolour(demuxer, track, &video->colour);
+ if (video->n_projection)
+ parse_trackprojection(demuxer, track, &video->projection);
+}
+
+/**
+ * \brief free any data associated with given track
+ * \param track track of which to free data
+ */
+static void demux_mkv_free_trackentry(mkv_track_t *track)
+{
+ talloc_free(track->parser_tmp);
+ talloc_free(track);
+}
+
+static void parse_trackentry(struct demuxer *demuxer,
+ struct ebml_track_entry *entry)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ struct mkv_track *track = talloc_zero(NULL, struct mkv_track);
+ track->last_index_entry = (size_t)-1;
+ track->parser_tmp = talloc_new(track);
+
+ track->tnum = entry->track_number;
+ if (track->tnum) {
+ MP_DBG(demuxer, "| + Track number: %d\n", track->tnum);
+ } else {
+ MP_ERR(demuxer, "Missing track number!\n");
+ }
+ track->uid = entry->track_uid;
+
+ if (entry->name) {
+ track->name = talloc_strdup(track, entry->name);
+ MP_DBG(demuxer, "| + Name: %s\n", track->name);
+ }
+
+ track->type = entry->track_type;
+ MP_DBG(demuxer, "| + Track type: ");
+ switch (track->type) {
+ case MATROSKA_TRACK_AUDIO:
+ MP_DBG(demuxer, "Audio\n");
+ break;
+ case MATROSKA_TRACK_VIDEO:
+ MP_DBG(demuxer, "Video\n");
+ break;
+ case MATROSKA_TRACK_SUBTITLE:
+ MP_DBG(demuxer, "Subtitle\n");
+ break;
+ default:
+ MP_DBG(demuxer, "unknown\n");
+ break;
+ }
+
+ if (entry->n_audio) {
+ MP_DBG(demuxer, "| + Audio track\n");
+ parse_trackaudio(demuxer, track, &entry->audio);
+ }
+
+ if (entry->n_video) {
+ MP_DBG(demuxer, "| + Video track\n");
+ parse_trackvideo(demuxer, track, &entry->video);
+ }
+
+ if (entry->codec_id) {
+ track->codec_id = talloc_strdup(track, entry->codec_id);
+ MP_DBG(demuxer, "| + Codec ID: %s\n", track->codec_id);
+ } else {
+ MP_ERR(demuxer, "Missing codec ID!\n");
+ track->codec_id = "";
+ }
+
+ if (entry->n_codec_private && entry->codec_private.len <= 0x10000000) {
+ int len = entry->codec_private.len;
+ track->private_data = talloc_size(track, len + AV_LZO_INPUT_PADDING);
+ memcpy(track->private_data, entry->codec_private.start, len);
+ track->private_size = len;
+ MP_DBG(demuxer, "| + CodecPrivate, length %u\n", track->private_size);
+ }
+
+ if (entry->language) {
+ track->language = talloc_strdup(track, entry->language);
+ MP_DBG(demuxer, "| + Language: %s\n", track->language);
+ } else {
+ track->language = talloc_strdup(track, "eng");
+ }
+
+ if (entry->n_flag_default) {
+ track->default_track = entry->flag_default;
+ MP_DBG(demuxer, "| + Default flag: %d\n", track->default_track);
+ } else {
+ track->default_track = 1;
+ }
+
+ if (entry->n_flag_forced) {
+ track->forced_track = entry->flag_forced;
+ MP_DBG(demuxer, "| + Forced flag: %d\n", track->forced_track);
+ }
+
+ if (entry->n_default_duration) {
+ track->default_duration = entry->default_duration / 1e9;
+ if (entry->default_duration == 0) {
+ MP_DBG(demuxer, "| + Default duration: 0");
+ } else {
+ track->v_frate = 1e9 / entry->default_duration;
+ MP_DBG(demuxer, "| + Default duration: %.3fms ( = %.3f fps)\n",
+ entry->default_duration / 1000000.0, track->v_frate);
+ }
+ }
+
+ if (entry->n_content_encodings)
+ parse_trackencodings(demuxer, track, &entry->content_encodings);
+
+ if (entry->n_codec_delay)
+ track->codec_delay = entry->codec_delay / 1e9;
+
+ mkv_d->tracks[mkv_d->num_tracks++] = track;
+}
+
+static int demux_mkv_read_tracks(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ stream_t *s = demuxer->stream;
+
+ MP_DBG(demuxer, "|+ segment tracks...\n");
+
+ struct ebml_tracks tracks = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ if (ebml_read_element(s, &parse_ctx, &tracks, &ebml_tracks_desc) < 0)
+ return -1;
+
+ mkv_d->tracks = talloc_zero_array(mkv_d, struct mkv_track*,
+ tracks.n_track_entry);
+ for (int i = 0; i < tracks.n_track_entry; i++) {
+ MP_DBG(demuxer, "| + a track...\n");
+ parse_trackentry(demuxer, &tracks.track_entry[i]);
+ }
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+}
+
+static void cue_index_add(demuxer_t *demuxer, int track_id, uint64_t filepos,
+ int64_t timecode, int64_t duration)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+
+ MP_TARRAY_GROW(mkv_d, mkv_d->indexes, mkv_d->num_indexes);
+
+ mkv_d->indexes[mkv_d->num_indexes] = (mkv_index_t) {
+ .tnum = track_id,
+ .filepos = filepos,
+ .timecode = timecode,
+ .duration = duration,
+ };
+
+ mkv_d->num_indexes++;
+}
+
+static void add_block_position(demuxer_t *demuxer, struct mkv_track *track,
+ uint64_t filepos,
+ int64_t timecode, int64_t duration)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+
+ if (mkv_d->index_complete || !track)
+ return;
+
+ mkv_d->index_has_durations = true;
+
+ if (track->last_index_entry != (size_t)-1) {
+ mkv_index_t *index = &mkv_d->indexes[track->last_index_entry];
+ // Never add blocks which are already covered by the index.
+ if (index->timecode >= timecode)
+ return;
+ }
+ cue_index_add(demuxer, track->tnum, filepos, timecode, duration);
+ track->last_index_entry = mkv_d->num_indexes - 1;
+}
+
+static int demux_mkv_read_cues(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ stream_t *s = demuxer->stream;
+
+ if (demuxer->opts->index_mode != 1 || mkv_d->index_complete) {
+ ebml_read_skip(demuxer->log, -1, s);
+ return 0;
+ }
+
+ MP_VERBOSE(demuxer, "Parsing cues...\n");
+ struct ebml_cues cues = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ if (ebml_read_element(s, &parse_ctx, &cues, &ebml_cues_desc) < 0)
+ return -1;
+
+ for (int i = 0; i < cues.n_cue_point; i++) {
+ struct ebml_cue_point *cuepoint = &cues.cue_point[i];
+ if (cuepoint->n_cue_time != 1 || !cuepoint->n_cue_track_positions) {
+ MP_WARN(demuxer, "Malformed CuePoint element\n");
+ goto done;
+ }
+ if (cuepoint->cue_time / 1e9 > mkv_d->duration / mkv_d->tc_scale * 10 &&
+ mkv_d->duration != 0)
+ goto done;
+ }
+ if (cues.n_cue_point <= 3) // probably too sparse and will just break seeking
+ goto done;
+
+ // Discard incremental index. (Keep the first entry, which must be the
+ // start of the file - helps with files that miss the first index entry.)
+ mkv_d->num_indexes = MPMIN(1, mkv_d->num_indexes);
+ mkv_d->index_has_durations = false;
+
+ for (int i = 0; i < cues.n_cue_point; i++) {
+ struct ebml_cue_point *cuepoint = &cues.cue_point[i];
+ uint64_t time = cuepoint->cue_time;
+ for (int c = 0; c < cuepoint->n_cue_track_positions; c++) {
+ struct ebml_cue_track_positions *trackpos =
+ &cuepoint->cue_track_positions[c];
+ uint64_t pos = mkv_d->segment_start + trackpos->cue_cluster_position;
+ cue_index_add(demuxer, trackpos->cue_track, pos,
+ time, trackpos->cue_duration);
+ mkv_d->index_has_durations |= trackpos->n_cue_duration > 0;
+ MP_TRACE(demuxer, "|+ found cue point for track %"PRIu64": "
+ "timecode %"PRIu64", filepos: %"PRIu64" "
+ "offset %"PRIu64", duration %"PRIu64"\n",
+ trackpos->cue_track, time, pos,
+ trackpos->cue_relative_position, trackpos->cue_duration);
+ }
+ }
+
+ // Do not attempt to create index on the fly.
+ mkv_d->index_complete = true;
+
+done:
+ if (!mkv_d->index_complete)
+ MP_WARN(demuxer, "Discarding potentially broken or useless index.\n");
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+}
+
+static int demux_mkv_read_chapters(struct demuxer *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+ stream_t *s = demuxer->stream;
+ int wanted_edition = mkv_d->edition_id;
+ uint64_t wanted_edition_uid = demuxer->matroska_data.uid.edition;
+
+ /* A specific edition UID was requested; ignore the user option which is
+ * only applicable to the top-level file. */
+ if (wanted_edition_uid)
+ wanted_edition = -1;
+
+ MP_DBG(demuxer, "Parsing chapters...\n");
+ struct ebml_chapters file_chapters = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ if (ebml_read_element(s, &parse_ctx, &file_chapters,
+ &ebml_chapters_desc) < 0)
+ return -1;
+
+ int selected_edition = -1;
+ int num_editions = file_chapters.n_edition_entry;
+ struct ebml_edition_entry *editions = file_chapters.edition_entry;
+ for (int i = 0; i < num_editions; i++) {
+ struct demux_edition new = {
+ .demuxer_id = editions[i].edition_uid,
+ .default_edition = editions[i].edition_flag_default,
+ .metadata = talloc_zero(demuxer, struct mp_tags),
+ };
+ MP_TARRAY_APPEND(demuxer, demuxer->editions, demuxer->num_editions, new);
+ }
+ if (wanted_edition >= 0 && wanted_edition < num_editions) {
+ selected_edition = wanted_edition;
+ MP_VERBOSE(demuxer, "User-specified edition: %d\n", selected_edition);
+ } else {
+ for (int i = 0; i < num_editions; i++) {
+ if (wanted_edition_uid &&
+ editions[i].edition_uid == wanted_edition_uid) {
+ selected_edition = i;
+ break;
+ } else if (editions[i].edition_flag_default) {
+ selected_edition = i;
+ MP_VERBOSE(demuxer, "Default edition: %d\n", i);
+ break;
+ }
+ }
+ }
+ if (selected_edition < 0) {
+ if (wanted_edition_uid) {
+ MP_ERR(demuxer, "Unable to find expected edition uid: %"PRIu64"\n",
+ wanted_edition_uid);
+ talloc_free(parse_ctx.talloc_ctx);
+ return -1;
+ } else {
+ selected_edition = 0;
+ }
+ }
+
+ for (int idx = 0; idx < num_editions; idx++) {
+ MP_VERBOSE(demuxer, "New edition %d\n", idx);
+ int warn_level = idx == selected_edition ? MSGL_WARN : MSGL_V;
+ if (editions[idx].n_edition_flag_default)
+ MP_VERBOSE(demuxer, "Default edition flag: %"PRIu64"\n",
+ editions[idx].edition_flag_default);
+ if (editions[idx].n_edition_flag_ordered)
+ MP_VERBOSE(demuxer, "Ordered chapter flag: %"PRIu64"\n",
+ editions[idx].edition_flag_ordered);
+
+ int chapter_count = editions[idx].n_chapter_atom;
+
+ struct matroska_chapter *m_chapters = NULL;
+ if (idx == selected_edition && editions[idx].edition_flag_ordered) {
+ m_chapters = talloc_array_ptrtype(demuxer, m_chapters, chapter_count);
+ demuxer->matroska_data.ordered_chapters = m_chapters;
+ demuxer->matroska_data.num_ordered_chapters = chapter_count;
+ }
+
+ for (int i = 0; i < chapter_count; i++) {
+ struct ebml_chapter_atom *ca = editions[idx].chapter_atom + i;
+ struct matroska_chapter chapter = {0};
+ char *name = "(unnamed)";
+
+ chapter.start = ca->chapter_time_start;
+ chapter.end = ca->chapter_time_end;
+
+ if (!ca->n_chapter_time_start)
+ MP_MSG(demuxer, warn_level, "Chapter lacks start time\n");
+ if (!ca->n_chapter_time_start || !ca->n_chapter_time_end) {
+ if (demuxer->matroska_data.ordered_chapters) {
+ MP_MSG(demuxer, warn_level, "Chapter lacks start or end "
+ "time, disabling ordered chapters.\n");
+ demuxer->matroska_data.ordered_chapters = NULL;
+ demuxer->matroska_data.num_ordered_chapters = 0;
+ }
+ }
+
+ if (ca->n_chapter_display) {
+ if (ca->n_chapter_display > 1)
+ MP_MSG(demuxer, warn_level, "Multiple chapter "
+ "names not supported, picking first\n");
+ if (!ca->chapter_display[0].chap_string)
+ MP_MSG(demuxer, warn_level, "Malformed chapter name entry\n");
+ else
+ name = ca->chapter_display[0].chap_string;
+ }
+
+ if (ca->n_chapter_segment_uid) {
+ chapter.has_segment_uid = true;
+ int len = ca->chapter_segment_uid.len;
+ if (len != sizeof(chapter.uid.segment))
+ MP_MSG(demuxer, warn_level,
+ "Chapter segment uid bad length %d\n", len);
+ else {
+ memcpy(chapter.uid.segment, ca->chapter_segment_uid.start,
+ len);
+ if (ca->n_chapter_segment_edition_uid)
+ chapter.uid.edition = ca->chapter_segment_edition_uid;
+ else
+ chapter.uid.edition = 0;
+ MP_DBG(demuxer, "Chapter segment uid ");
+ for (int n = 0; n < len; n++)
+ MP_DBG(demuxer, "%02x ",
+ chapter.uid.segment[n]);
+ MP_DBG(demuxer, "\n");
+ }
+ }
+
+ MP_DBG(demuxer, "Chapter %u from %02d:%02d:%02d.%03d "
+ "to %02d:%02d:%02d.%03d, %s\n", i,
+ (int) (chapter.start / 60 / 60 / 1000000000),
+ (int) ((chapter.start / 60 / 1000000000) % 60),
+ (int) ((chapter.start / 1000000000) % 60),
+ (int) (chapter.start % 1000000000),
+ (int) (chapter.end / 60 / 60 / 1000000000),
+ (int) ((chapter.end / 60 / 1000000000) % 60),
+ (int) ((chapter.end / 1000000000) % 60),
+ (int) (chapter.end % 1000000000),
+ name);
+
+ if (idx == selected_edition) {
+ demuxer_add_chapter(demuxer, name, chapter.start / 1e9,
+ ca->chapter_uid);
+ }
+ if (m_chapters) {
+ chapter.name = talloc_strdup(m_chapters, name);
+ m_chapters[i] = chapter;
+ }
+ }
+ }
+
+ demuxer->num_editions = num_editions;
+ demuxer->edition = selected_edition;
+
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+}
+
+static int demux_mkv_read_tags(demuxer_t *demuxer)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ stream_t *s = demuxer->stream;
+
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ struct ebml_tags tags = {0};
+ if (ebml_read_element(s, &parse_ctx, &tags, &ebml_tags_desc) < 0)
+ return -1;
+
+ mkv_d->tags = talloc_dup(mkv_d, &tags);
+ talloc_steal(mkv_d->tags, parse_ctx.talloc_ctx);
+ return 0;
+}
+
+static void process_tags(demuxer_t *demuxer)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ struct ebml_tags *tags = mkv_d->tags;
+
+ if (!tags)
+ return;
+
+ for (int i = 0; i < tags->n_tag; i++) {
+ struct ebml_tag tag = tags->tag[i];
+ struct mp_tags *dst = NULL;
+
+ if (tag.targets.target_chapter_uid) {
+ for (int n = 0; n < demuxer->num_chapters; n++) {
+ if (demuxer->chapters[n].demuxer_id ==
+ tag.targets.target_chapter_uid)
+ {
+ dst = demuxer->chapters[n].metadata;
+ break;
+ }
+ }
+ } else if (tag.targets.target_edition_uid) {
+ for (int n = 0; n < demuxer->num_editions; n++) {
+ if (demuxer->editions[n].demuxer_id ==
+ tag.targets.target_edition_uid)
+ {
+ dst = demuxer->editions[n].metadata;
+ break;
+ }
+ }
+ } else if (tag.targets.target_track_uid) {
+ for (int n = 0; n < mkv_d->num_tracks; n++) {
+ if (mkv_d->tracks[n]->uid ==
+ tag.targets.target_track_uid)
+ {
+ struct sh_stream *sh = mkv_d->tracks[n]->stream;
+ if (sh)
+ dst = sh->tags;
+ break;
+ }
+ }
+ } else if (tag.targets.target_attachment_uid) {
+ /* ignore */
+ } else {
+ dst = demuxer->metadata;
+ }
+
+ if (dst) {
+ for (int j = 0; j < tag.n_simple_tag; j++) {
+ if (tag.simple_tag[j].tag_name && tag.simple_tag[j].tag_string) {
+ char *name = tag.simple_tag[j].tag_name;
+ char *val = tag.simple_tag[j].tag_string;
+ char *old = mp_tags_get_str(dst, name);
+ if (old)
+ val = talloc_asprintf(NULL, "%s / %s", old, val);
+ mp_tags_set_str(dst, name, val);
+ if (old)
+ talloc_free(val);
+ }
+ }
+ }
+ }
+}
+
+static int demux_mkv_read_attachments(demuxer_t *demuxer)
+{
+ stream_t *s = demuxer->stream;
+
+ MP_DBG(demuxer, "Parsing attachments...\n");
+
+ struct ebml_attachments attachments = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ if (ebml_read_element(s, &parse_ctx, &attachments,
+ &ebml_attachments_desc) < 0)
+ return -1;
+
+ for (int i = 0; i < attachments.n_attached_file; i++) {
+ struct ebml_attached_file *attachment = &attachments.attached_file[i];
+ if (!attachment->file_name || !attachment->file_mime_type
+ || !attachment->n_file_data) {
+ MP_WARN(demuxer, "Malformed attachment\n");
+ continue;
+ }
+ char *name = attachment->file_name;
+ char *mime = attachment->file_mime_type;
+ demuxer_add_attachment(demuxer, name, mime, attachment->file_data.start,
+ attachment->file_data.len);
+ MP_DBG(demuxer, "Attachment: %s, %s, %zu bytes\n",
+ name, mime, attachment->file_data.len);
+ }
+
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+}
+
+static struct header_elem *get_header_element(struct demuxer *demuxer,
+ uint32_t id,
+ int64_t element_filepos)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+
+ // Note that some files in fact contain a SEEKHEAD with a list of all
+ // clusters - we have no use for that.
+ if (!ebml_is_mkv_level1_id(id) || id == MATROSKA_ID_CLUSTER)
+ return NULL;
+
+ for (int n = 0; n < mkv_d->num_headers; n++) {
+ struct header_elem *elem = &mkv_d->headers[n];
+ // SEEKHEAD is the only element that can happen multiple times.
+ // Other elements might be duplicated (or attempted to be read twice,
+ // even if it's only once in the file), but only the first is used.
+ if (elem->id == id && (id != MATROSKA_ID_SEEKHEAD ||
+ elem->pos == element_filepos))
+ return elem;
+ }
+ struct header_elem elem = { .id = id, .pos = element_filepos };
+ MP_TARRAY_APPEND(mkv_d, mkv_d->headers, mkv_d->num_headers, elem);
+ return &mkv_d->headers[mkv_d->num_headers - 1];
+}
+
+// Mark the level 1 element with the given id as read. Return whether it
+// was marked read before (e.g. for checking whether it was already read).
+// element_filepos refers to the file position of the element ID.
+static bool test_header_element(struct demuxer *demuxer, uint32_t id,
+ int64_t element_filepos)
+{
+ struct header_elem *elem = get_header_element(demuxer, id, element_filepos);
+ if (!elem)
+ return false;
+ if (elem->parsed)
+ return true;
+ elem->parsed = true;
+ return false;
+}
+
+static int demux_mkv_read_seekhead(demuxer_t *demuxer)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ struct stream *s = demuxer->stream;
+ int res = 0;
+ struct ebml_seek_head seekhead = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+
+ MP_DBG(demuxer, "Parsing seek head...\n");
+ if (ebml_read_element(s, &parse_ctx, &seekhead, &ebml_seek_head_desc) < 0) {
+ res = -1;
+ goto out;
+ }
+ for (int i = 0; i < seekhead.n_seek; i++) {
+ struct ebml_seek *seek = &seekhead.seek[i];
+ if (seek->n_seek_id != 1 || seek->n_seek_position != 1) {
+ MP_WARN(demuxer, "Invalid SeekHead entry\n");
+ continue;
+ }
+ uint64_t pos = seek->seek_position + mkv_d->segment_start;
+ MP_TRACE(demuxer, "Element 0x%"PRIx32" at %"PRIu64".\n",
+ seek->seek_id, pos);
+ get_header_element(demuxer, seek->seek_id, pos);
+ }
+ out:
+ talloc_free(parse_ctx.talloc_ctx);
+ return res;
+}
+
+static int read_header_element(struct demuxer *demuxer, uint32_t id,
+ int64_t start_filepos)
+{
+ if (id == EBML_ID_INVALID)
+ return 0;
+
+ if (test_header_element(demuxer, id, start_filepos))
+ goto skip;
+
+ switch(id) {
+ case MATROSKA_ID_INFO:
+ return demux_mkv_read_info(demuxer);
+ case MATROSKA_ID_TRACKS:
+ return demux_mkv_read_tracks(demuxer);
+ case MATROSKA_ID_CUES:
+ return demux_mkv_read_cues(demuxer);
+ case MATROSKA_ID_TAGS:
+ return demux_mkv_read_tags(demuxer);
+ case MATROSKA_ID_SEEKHEAD:
+ return demux_mkv_read_seekhead(demuxer);
+ case MATROSKA_ID_CHAPTERS:
+ return demux_mkv_read_chapters(demuxer);
+ case MATROSKA_ID_ATTACHMENTS:
+ return demux_mkv_read_attachments(demuxer);
+ }
+skip:
+ ebml_read_skip(demuxer->log, -1, demuxer->stream);
+ return 0;
+}
+
+static int read_deferred_element(struct demuxer *demuxer,
+ struct header_elem *elem)
+{
+ stream_t *s = demuxer->stream;
+
+ if (elem->parsed)
+ return 0;
+ elem->parsed = true;
+ MP_VERBOSE(demuxer, "Seeking to %"PRIu64" to read header element "
+ "0x%"PRIx32".\n",
+ elem->pos, elem->id);
+ if (!stream_seek(s, elem->pos)) {
+ MP_WARN(demuxer, "Failed to seek when reading header element.\n");
+ return 0;
+ }
+ if (ebml_read_id(s) != elem->id) {
+ MP_ERR(demuxer, "Expected element 0x%"PRIx32" not found\n",
+ elem->id);
+ return 0;
+ }
+ elem->parsed = false; // don't make read_header_element skip it
+ return read_header_element(demuxer, elem->id, elem->pos);
+}
+
+static void read_deferred_cues(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+
+ if (mkv_d->index_complete || demuxer->opts->index_mode != 1)
+ return;
+
+ for (int n = 0; n < mkv_d->num_headers; n++) {
+ struct header_elem *elem = &mkv_d->headers[n];
+
+ if (elem->id == MATROSKA_ID_CUES)
+ read_deferred_element(demuxer, elem);
+ }
+}
+
+static void add_coverart(struct demuxer *demuxer)
+{
+ for (int n = 0; n < demuxer->num_attachments; n++) {
+ struct demux_attachment *att = &demuxer->attachments[n];
+ const char *codec = mp_map_mimetype_to_video_codec(att->type);
+ if (!codec)
+ continue;
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO);
+ sh->codec->codec = codec;
+ sh->attached_picture = new_demux_packet_from(att->data, att->data_size);
+ if (sh->attached_picture) {
+ sh->attached_picture->pts = 0;
+ talloc_steal(sh, sh->attached_picture);
+ sh->attached_picture->keyframe = true;
+ sh->image = true;
+ }
+ sh->title = att->name;
+ demux_add_sh_stream(demuxer, sh);
+ }
+}
+
+static void init_track(demuxer_t *demuxer, mkv_track_t *track,
+ struct sh_stream *sh)
+{
+ track->stream = sh;
+
+ if (track->language && (strcmp(track->language, "und") != 0))
+ sh->lang = track->language;
+
+ sh->demuxer_id = track->tnum;
+ sh->title = track->name;
+ sh->default_track = track->default_track;
+ sh->forced_track = track->forced_track;
+}
+
+static int demux_mkv_open_video(demuxer_t *demuxer, mkv_track_t *track);
+static int demux_mkv_open_audio(demuxer_t *demuxer, mkv_track_t *track);
+static int demux_mkv_open_sub(demuxer_t *demuxer, mkv_track_t *track);
+
+static void display_create_tracks(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+
+ for (int i = 0; i < mkv_d->num_tracks; i++) {
+ switch (mkv_d->tracks[i]->type) {
+ case MATROSKA_TRACK_VIDEO:
+ demux_mkv_open_video(demuxer, mkv_d->tracks[i]);
+ break;
+ case MATROSKA_TRACK_AUDIO:
+ demux_mkv_open_audio(demuxer, mkv_d->tracks[i]);
+ break;
+ case MATROSKA_TRACK_SUBTITLE:
+ demux_mkv_open_sub(demuxer, mkv_d->tracks[i]);
+ break;
+ }
+ }
+}
+
+static const char *const mkv_video_tags[][2] = {
+ {"V_MJPEG", "mjpeg"},
+ {"V_MPEG1", "mpeg1video"},
+ {"V_MPEG2", "mpeg2video"},
+ {"V_MPEG4/ISO/SP", "mpeg4"},
+ {"V_MPEG4/ISO/ASP", "mpeg4"},
+ {"V_MPEG4/ISO/AP", "mpeg4"},
+ {"V_MPEG4/ISO/AVC", "h264"},
+ {"V_MPEG4/MS/V3", "msmpeg4v3"},
+ {"V_THEORA", "theora"},
+ {"V_VP8", "vp8"},
+ {"V_VP9", "vp9"},
+ {"V_DIRAC", "dirac"},
+ {"V_PRORES", "prores"},
+ {"V_MPEGH/ISO/HEVC", "hevc"},
+ {"V_SNOW", "snow"},
+ {"V_AV1", "av1"},
+ {"V_PNG", "png"},
+ {"V_AVS2", "avs2"},
+ {"V_AVS3", "avs3"},
+ {0}
+};
+
+static int demux_mkv_open_video(demuxer_t *demuxer, mkv_track_t *track)
+{
+ unsigned char *extradata = NULL;
+ unsigned int extradata_size = 0;
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO);
+ init_track(demuxer, track, sh);
+ struct mp_codec_params *sh_v = sh->codec;
+
+ sh_v->bits_per_coded_sample = 24;
+
+ if (!strcmp(track->codec_id, "V_MS/VFW/FOURCC")) { /* AVI compatibility mode */
+ // The private_data contains a BITMAPINFOHEADER struct
+ if (track->private_data == NULL || track->private_size < 40)
+ goto done;
+
+ unsigned char *h = track->private_data;
+ if (track->v_width == 0)
+ track->v_width = AV_RL32(h + 4); // biWidth
+ if (track->v_height == 0)
+ track->v_height = AV_RL32(h + 8); // biHeight
+ sh_v->bits_per_coded_sample = AV_RL16(h + 14); // biBitCount
+ sh_v->codec_tag = AV_RL32(h + 16); // biCompression
+
+ extradata = track->private_data + 40;
+ extradata_size = track->private_size - 40;
+ mp_set_codec_from_tag(sh_v);
+ sh_v->avi_dts = true;
+ } else if (track->private_size >= RVPROPERTIES_SIZE
+ && (!strcmp(track->codec_id, "V_REAL/RV10")
+ || !strcmp(track->codec_id, "V_REAL/RV20")
+ || !strcmp(track->codec_id, "V_REAL/RV30")
+ || !strcmp(track->codec_id, "V_REAL/RV40")))
+ {
+ unsigned char *src;
+ unsigned int cnt;
+
+ src = (uint8_t *) track->private_data + RVPROPERTIES_SIZE;
+
+ cnt = track->private_size - RVPROPERTIES_SIZE;
+ uint32_t t2 = AV_RB32(src - 4);
+ switch (t2 == 0x10003000 || t2 == 0x10003001 ? '1' : track->codec_id[9]) {
+ case '1': sh_v->codec = "rv10"; break;
+ case '2': sh_v->codec = "rv20"; break;
+ case '3': sh_v->codec = "rv30"; break;
+ case '4': sh_v->codec = "rv40"; break;
+ }
+ // copy type1 and type2 info from rv properties
+ extradata_size = cnt + 8;
+ extradata = src - 8;
+ track->parse = true;
+ track->parse_timebase = 1e3;
+ } else if (strcmp(track->codec_id, "V_UNCOMPRESSED") == 0) {
+ // raw video, "like AVI" - this is a FourCC
+ sh_v->codec_tag = track->colorspace;
+ sh_v->codec = "rawvideo";
+ } else if (strcmp(track->codec_id, "V_QUICKTIME") == 0) {
+ uint32_t fourcc1 = 0, fourcc2 = 0;
+ if (track->private_size >= 8) {
+ fourcc1 = AV_RL32(track->private_data + 0);
+ fourcc2 = AV_RL32(track->private_data + 4);
+ }
+ if (fourcc1 == MKTAG('S', 'V', 'Q', '3') ||
+ fourcc2 == MKTAG('S', 'V', 'Q', '3'))
+ {
+ sh_v->codec = "svq3";
+ extradata = track->private_data;
+ extradata_size = track->private_size;
+ }
+ } else {
+ for (int i = 0; mkv_video_tags[i][0]; i++) {
+ if (!strcmp(mkv_video_tags[i][0], track->codec_id)) {
+ sh_v->codec = mkv_video_tags[i][1];
+ break;
+ }
+ }
+ if (track->private_data && track->private_size > 0) {
+ extradata = track->private_data;
+ extradata_size = track->private_size;
+ }
+ }
+
+ const char *codec = sh_v->codec ? sh_v->codec : "";
+ if (mp_codec_is_image(codec)) {
+ sh->still_image = true;
+ sh->image = true;
+ }
+ if (!strcmp(codec, "mjpeg")) {
+ sh_v->codec_tag = MKTAG('m', 'j', 'p', 'g');
+ track->require_keyframes = true;
+ }
+
+ if (extradata_size > 0x1000000) {
+ MP_WARN(demuxer, "Invalid CodecPrivate\n");
+ goto done;
+ }
+
+ sh_v->extradata = talloc_memdup(sh_v, extradata, extradata_size);
+ sh_v->extradata_size = extradata_size;
+ if (!sh_v->codec) {
+ MP_WARN(demuxer, "Unknown/unsupported CodecID (%s) or missing/bad "
+ "CodecPrivate data (track %d).\n",
+ track->codec_id, track->tnum);
+ }
+ sh_v->fps = track->v_frate;
+ sh_v->disp_w = track->v_width;
+ sh_v->disp_h = track->v_height;
+
+ // Keep the codec crop rect as 0s if we have no cropping since the
+ // file may have broken width/height tags.
+ if (track->v_crop_left || track->v_crop_top ||
+ track->v_crop_right || track->v_crop_bottom)
+ {
+ sh_v->crop.x0 = track->v_crop_left;
+ sh_v->crop.y0 = track->v_crop_top;
+ sh_v->crop.x1 = track->v_width - track->v_crop_right;
+ sh_v->crop.y1 = track->v_height - track->v_crop_bottom;
+ }
+
+ int dw = track->v_dwidth_set ? track->v_dwidth : track->v_width;
+ int dh = track->v_dheight_set ? track->v_dheight : track->v_height;
+ struct mp_image_params p = {.w = track->v_width, .h = track->v_height};
+ mp_image_params_set_dsize(&p, dw, dh);
+ sh_v->par_w = p.p_w;
+ sh_v->par_h = p.p_h;
+
+ sh_v->stereo_mode = track->stereo_mode;
+ sh_v->color = track->color;
+
+ if (track->v_projection_pose_roll_set) {
+ int rotate = lrintf(fmodf(fmodf(track->v_projection_pose_roll, 360) + 360, 360));
+ sh_v->rotate = rotate;
+ }
+
+done:
+ demux_add_sh_stream(demuxer, sh);
+
+ return 0;
+}
+
+// Parse VorbisComment and look for WAVEFORMATEXTENSIBLE_CHANNEL_MASK.
+// Do not change *channels if nothing found or an error happens.
+static void parse_vorbis_chmap(struct mp_chmap *channels, unsigned char *data,
+ int size)
+{
+ // Skip the useless vendor string.
+ if (size < 4)
+ return;
+ uint32_t vendor_length = AV_RL32(data);
+ if (vendor_length + 4 > size) // also check for the next AV_RB32 below
+ return;
+ size -= vendor_length + 4;
+ data += vendor_length + 4;
+ uint32_t num_headers = AV_RL32(data);
+ size -= 4;
+ data += 4;
+ for (int n = 0; n < num_headers; n++) {
+ if (size < 4)
+ return;
+ uint32_t len = AV_RL32(data);
+ size -= 4;
+ data += 4;
+ if (len > size)
+ return;
+ if (len > 34 && !memcmp(data, "WAVEFORMATEXTENSIBLE_CHANNEL_MASK=", 34)) {
+ char smask[80];
+ snprintf(smask, sizeof(smask), "%.*s", (int)(len - 34), data + 34);
+ char *end = NULL;
+ uint32_t mask = strtol(smask, &end, 0);
+ if (!end || end[0])
+ mask = 0;
+ struct mp_chmap chmask = {0};
+ mp_chmap_from_waveext(&chmask, mask);
+ if (mp_chmap_is_valid(&chmask))
+ *channels = chmask;
+ }
+ size -= len;
+ data += len;
+ }
+}
+
+// Parse VorbisComment-in-FLAC and look for WAVEFORMATEXTENSIBLE_CHANNEL_MASK.
+// Do not change *channels if nothing found or an error happens.
+static void parse_flac_chmap(struct mp_chmap *channels, unsigned char *data,
+ int size)
+{
+ // Skip FLAC header.
+ if (size < 4)
+ return;
+ data += 4;
+ size -= 4;
+ // Parse FLAC blocks...
+ while (size >= 4) {
+ unsigned btype = data[0] & 0x7F;
+ unsigned bsize = AV_RB24(data + 1);
+ data += 4;
+ size -= 4;
+ if (bsize > size)
+ return;
+ if (btype == 4) // VORBIS_COMMENT
+ parse_vorbis_chmap(channels, data, bsize);
+ data += bsize;
+ size -= bsize;
+ }
+}
+
+static const char *const mkv_audio_tags[][2] = {
+ { "A_MPEG/L2", "mp2" },
+ { "A_MPEG/L3", "mp3" },
+ { "A_AC3", "ac3" },
+ { "A_EAC3", "eac3" },
+ { "A_DTS", "dts" },
+ { "A_AAC", "aac" },
+ { "A_VORBIS", "vorbis" },
+ { "A_OPUS", "opus" },
+ { "A_OPUS/EXPERIMENTAL", "opus" },
+ { "A_QUICKTIME/QDMC", "qdmc" },
+ { "A_QUICKTIME/QDM2", "qdm2" },
+ { "A_WAVPACK4", "wavpack" },
+ { "A_TRUEHD", "truehd" },
+ { "A_FLAC", "flac" },
+ { "A_ALAC", "alac" },
+ { "A_TTA1", "tta" },
+ { "A_MLP", "mlp" },
+ { NULL },
+};
+
+static int demux_mkv_open_audio(demuxer_t *demuxer, mkv_track_t *track)
+{
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_AUDIO);
+ init_track(demuxer, track, sh);
+ struct mp_codec_params *sh_a = sh->codec;
+
+ if (track->private_size > 0x1000000)
+ goto error;
+
+ unsigned char *extradata = track->private_data;
+ unsigned int extradata_len = track->private_size;
+
+ if (!track->a_osfreq)
+ track->a_osfreq = track->a_sfreq;
+ sh_a->bits_per_coded_sample = track->a_bps ? track->a_bps : 16;
+ sh_a->samplerate = (uint32_t) track->a_osfreq;
+ mp_chmap_set_unknown(&sh_a->channels, track->a_channels);
+
+ for (int i = 0; mkv_audio_tags[i][0]; i++) {
+ if (!strcmp(mkv_audio_tags[i][0], track->codec_id)) {
+ sh_a->codec = mkv_audio_tags[i][1];
+ break;
+ }
+ }
+
+ if (!strcmp(track->codec_id, "A_MS/ACM")) { /* AVI compatibility mode */
+ // The private_data contains a WAVEFORMATEX struct
+ if (track->private_size < 18)
+ goto error;
+ MP_DBG(demuxer, "track with MS compat audio.\n");
+ unsigned char *h = track->private_data;
+ sh_a->codec_tag = AV_RL16(h + 0); // wFormatTag
+ if (track->a_channels == 0)
+ track->a_channels = AV_RL16(h + 2); // nChannels
+ if (sh_a->samplerate == 0)
+ sh_a->samplerate = AV_RL32(h + 4); // nSamplesPerSec
+ sh_a->bitrate = AV_RL32(h + 8) * 8; // nAvgBytesPerSec
+ sh_a->block_align = AV_RL16(h + 12); // nBlockAlign
+ if (track->a_bps == 0)
+ track->a_bps = AV_RL16(h + 14); // wBitsPerSample
+ extradata = track->private_data + 18;
+ extradata_len = track->private_size - 18;
+ sh_a->bits_per_coded_sample = track->a_bps;
+ sh_a->extradata = extradata;
+ sh_a->extradata_size = extradata_len;
+ mp_set_codec_from_tag(sh_a);
+ extradata = sh_a->extradata;
+ extradata_len = sh_a->extradata_size;
+ } else if (!strcmp(track->codec_id, "A_PCM/INT/LIT")) {
+ bool sign = sh_a->bits_per_coded_sample > 8;
+ mp_set_pcm_codec(sh_a, sign, false, sh_a->bits_per_coded_sample, false);
+ } else if (!strcmp(track->codec_id, "A_PCM/INT/BIG")) {
+ bool sign = sh_a->bits_per_coded_sample > 8;
+ mp_set_pcm_codec(sh_a, sign, false, sh_a->bits_per_coded_sample, true);
+ } else if (!strcmp(track->codec_id, "A_PCM/FLOAT/IEEE")) {
+ sh_a->codec = sh_a->bits_per_coded_sample == 64 ? "pcm_f64le" : "pcm_f32le";
+ } else if (!strncmp(track->codec_id, "A_REAL/", 7)) {
+ if (track->private_size < RAPROPERTIES4_SIZE)
+ goto error;
+ /* Common initialization for all RealAudio codecs */
+ unsigned char *src = track->private_data;
+
+ int version = AV_RB16(src + 4);
+ unsigned int flavor = AV_RB16(src + 22);
+ track->coded_framesize = AV_RB32(src + 24);
+ track->sub_packet_h = AV_RB16(src + 40);
+ sh_a->block_align = track->audiopk_size = AV_RB16(src + 42);
+ track->sub_packet_size = AV_RB16(src + 44);
+ int offset = 0;
+ if (version == 4) {
+ offset += RAPROPERTIES4_SIZE;
+ if (offset + 1 > track->private_size)
+ goto error;
+ offset += (src[offset] + 1) * 2 + 3;
+ } else {
+ offset += RAPROPERTIES5_SIZE + 3 + (version == 5 ? 1 : 0);
+ }
+
+ if (track->audiopk_size == 0 || track->sub_packet_size == 0 ||
+ track->sub_packet_h == 0 || track->coded_framesize == 0)
+ goto error;
+ if (track->coded_framesize > 0x40000000)
+ goto error;
+
+ if (offset + 4 > track->private_size)
+ goto error;
+ uint32_t codecdata_length = AV_RB32(src + offset);
+ offset += 4;
+ if (offset > track->private_size ||
+ codecdata_length > track->private_size - offset)
+ goto error;
+ extradata_len = codecdata_length;
+ extradata = src + offset;
+
+ if (!strcmp(track->codec_id, "A_REAL/ATRC")) {
+ sh_a->codec = "atrac3";
+ if (flavor >= MP_ARRAY_SIZE(atrc_fl2bps))
+ goto error;
+ sh_a->bitrate = atrc_fl2bps[flavor] * 8;
+ sh_a->block_align = track->sub_packet_size;
+ } else if (!strcmp(track->codec_id, "A_REAL/COOK")) {
+ sh_a->codec = "cook";
+ if (flavor >= MP_ARRAY_SIZE(cook_fl2bps))
+ goto error;
+ sh_a->bitrate = cook_fl2bps[flavor] * 8;
+ sh_a->block_align = track->sub_packet_size;
+ } else if (!strcmp(track->codec_id, "A_REAL/SIPR")) {
+ sh_a->codec = "sipr";
+ if (flavor >= MP_ARRAY_SIZE(sipr_fl2bps))
+ goto error;
+ sh_a->bitrate = sipr_fl2bps[flavor] * 8;
+ sh_a->block_align = track->coded_framesize;
+ } else if (!strcmp(track->codec_id, "A_REAL/28_8")) {
+ sh_a->codec = "ra_288";
+ sh_a->bitrate = 3600 * 8;
+ sh_a->block_align = track->coded_framesize;
+ } else if (!strcmp(track->codec_id, "A_REAL/DNET")) {
+ sh_a->codec = "ac3";
+ } else {
+ goto error;
+ }
+
+ track->audio_buf =
+ talloc_array_size(track, track->sub_packet_h, track->audiopk_size);
+ track->audio_timestamp =
+ talloc_array(track, double, track->sub_packet_h);
+ } else if (!strncmp(track->codec_id, "A_AAC/", 6)) {
+ sh_a->codec = "aac";
+
+ /* Recreate the 'private data' (not needed for plain A_AAC) */
+ int srate_idx = aac_get_sample_rate_index(track->a_sfreq);
+ const char *tail = "";
+ if (strlen(track->codec_id) >= 12)
+ tail = &track->codec_id[12];
+ int profile = 3;
+ if (!strncmp(tail, "MAIN", 4))
+ profile = 0;
+ else if (!strncmp(tail, "LC", 2))
+ profile = 1;
+ else if (!strncmp(tail, "SSR", 3))
+ profile = 2;
+ extradata = talloc_size(sh_a, 5);
+ extradata[0] = ((profile + 1) << 3) | ((srate_idx & 0xE) >> 1);
+ extradata[1] = ((srate_idx & 0x1) << 7) | (track->a_channels << 3);
+
+ if (strstr(track->codec_id, "SBR") != NULL) {
+ /* HE-AAC (aka SBR AAC) */
+ extradata_len = 5;
+
+ srate_idx = aac_get_sample_rate_index(sh_a->samplerate);
+ extradata[2] = AAC_SYNC_EXTENSION_TYPE >> 3;
+ extradata[3] = ((AAC_SYNC_EXTENSION_TYPE & 0x07) << 5) | 5;
+ extradata[4] = (1 << 7) | (srate_idx << 3);
+ track->default_duration = 1024.0 / (sh_a->samplerate / 2);
+ } else {
+ extradata_len = 2;
+ track->default_duration = 1024.0 / sh_a->samplerate;
+ }
+ } else if (!strncmp(track->codec_id, "A_AC3/", 6)) {
+ sh_a->codec = "ac3";
+ } else if (!strncmp(track->codec_id, "A_EAC3/", 7)) {
+ sh_a->codec = "eac3";
+ }
+
+ if (!sh_a->codec)
+ goto error;
+
+ const char *codec = sh_a->codec;
+ if (!strcmp(codec, "mp2") || !strcmp(codec, "mp3") ||
+ !strcmp(codec, "truehd") || !strcmp(codec, "eac3"))
+ {
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+ int64_t segment_timebase = (1e9 / mkv_d->tc_scale);
+
+ track->parse = true;
+ track->parse_timebase = MPMAX(sh_a->samplerate, segment_timebase);
+ } else if (!strcmp(codec, "flac")) {
+ unsigned char *ptr = extradata;
+ unsigned int size = extradata_len;
+ if (size < 4 || ptr[0] != 'f' || ptr[1] != 'L' || ptr[2] != 'a'
+ || ptr[3] != 'C') {
+ extradata = talloc_size(sh_a, 4);
+ extradata_len = 4;
+ memcpy(extradata, "fLaC", 4);
+ }
+ parse_flac_chmap(&sh_a->channels, extradata, extradata_len);
+ } else if (!strcmp(codec, "alac")) {
+ if (track->private_size) {
+ extradata_len = track->private_size + 12;
+ extradata = talloc_size(sh_a, extradata_len);
+ char *data = extradata;
+ AV_WB32(data + 0, extradata_len);
+ memcpy(data + 4, "alac", 4);
+ AV_WB32(data + 8, 0);
+ memcpy(data + 12, track->private_data, track->private_size);
+ }
+ } else if (!strcmp(codec, "tta")) {
+ extradata_len = 30;
+ extradata = talloc_zero_size(sh_a, extradata_len);
+ if (!extradata)
+ goto error;
+ char *data = extradata;
+ memcpy(data + 0, "TTA1", 4);
+ AV_WL16(data + 4, 1);
+ AV_WL16(data + 6, sh_a->channels.num);
+ AV_WL16(data + 8, sh_a->bits_per_coded_sample);
+ AV_WL32(data + 10, track->a_osfreq);
+ // Bogus: last frame won't be played.
+ AV_WL32(data + 14, 0);
+ } else if (!strcmp(codec, "opus")) {
+ // Hardcode the rate libavcodec's opus decoder outputs, so that
+ // AV_PKT_DATA_SKIP_SAMPLES actually works. The Matroska header only
+ // has an arbitrary "input" samplerate, while libavcodec is fixed to
+ // output 48000.
+ sh_a->samplerate = 48000;
+ }
+
+ // Some files have broken default DefaultDuration set, which will lead to
+ // audio packets with incorrect timestamps. This follows FFmpeg commit
+ // 6158a3b, sample see FFmpeg ticket 2508.
+ if (sh_a->samplerate == 8000 && strcmp(codec, "ac3") == 0)
+ track->default_duration = 0;
+
+ // Deal with some FFmpeg-produced garbage, and assume all audio codecs can
+ // start decoding from anywhere.
+ if (strcmp(codec, "truehd") != 0)
+ track->require_keyframes = true;
+
+ sh_a->extradata = extradata;
+ sh_a->extradata_size = extradata_len;
+
+ sh->seek_preroll = track->codec_delay;
+
+ demux_add_sh_stream(demuxer, sh);
+
+ return 0;
+
+ error:
+ MP_WARN(demuxer, "Unknown/unsupported audio "
+ "codec ID '%s' for track %u or missing/faulty\n"
+ "private codec data.\n", track->codec_id, track->tnum);
+ demux_add_sh_stream(demuxer, sh); // add it anyway
+ return 1;
+}
+
+static const char *const mkv_sub_tag[][2] = {
+ { "S_VOBSUB", "dvd_subtitle" },
+ { "S_TEXT/SSA", "ass"},
+ { "S_TEXT/ASS", "ass"},
+ { "S_SSA", "ass"},
+ { "S_ASS", "ass"},
+ { "S_TEXT/ASCII", "subrip"},
+ { "S_TEXT/UTF8", "subrip"},
+ { "S_HDMV/PGS", "hdmv_pgs_subtitle"},
+ { "D_WEBVTT/SUBTITLES", "webvtt-webm"},
+ { "D_WEBVTT/CAPTIONS", "webvtt-webm"},
+ { "S_TEXT/WEBVTT", "webvtt"},
+ { "S_DVBSUB", "dvb_subtitle"},
+ { "S_ARIBSUB", "arib_caption"},
+ {0}
+};
+
+static void avcodec_par_destructor(void *p)
+{
+ avcodec_parameters_free(p);
+}
+
+static int demux_mkv_open_sub(demuxer_t *demuxer, mkv_track_t *track)
+{
+ const char *subtitle_type = NULL;
+ for (int n = 0; mkv_sub_tag[n][0]; n++) {
+ if (strcmp(track->codec_id, mkv_sub_tag[n][0]) == 0) {
+ subtitle_type = mkv_sub_tag[n][1];
+ break;
+ }
+ }
+
+ if (track->private_size > 0x10000000)
+ return 1;
+
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_SUB);
+ init_track(demuxer, track, sh);
+
+ sh->codec->codec = subtitle_type;
+ bstr in = (bstr){track->private_data, track->private_size};
+ bstr buffer = demux_mkv_decode(demuxer->log, track, in, 2);
+ if (buffer.start && buffer.start != track->private_data) {
+ talloc_free(track->private_data);
+ talloc_steal(track, buffer.start);
+ track->private_data = buffer.start;
+ track->private_size = buffer.len;
+ }
+ sh->codec->extradata = track->private_data;
+ sh->codec->extradata_size = track->private_size;
+
+ if (!strcmp(sh->codec->codec, "arib_caption") && track->private_size >= 3) {
+ struct AVCodecParameters **lavp = talloc_ptrtype(track, lavp);
+
+ talloc_set_destructor(lavp, avcodec_par_destructor);
+
+ struct AVCodecParameters *lav = *lavp = sh->codec->lav_codecpar = avcodec_parameters_alloc();
+ MP_HANDLE_OOM(lav);
+
+ lav->codec_type = AVMEDIA_TYPE_SUBTITLE;
+ lav->codec_id = AV_CODEC_ID_ARIB_CAPTION;
+
+ int component_tag = track->private_data[0];
+ int data_component_id = AV_RB16(track->private_data + 1);
+ switch (data_component_id) {
+ case 0x0008:
+ // [0x30..0x37] are component tags utilized for
+ // non-mobile captioning service ("profile A").
+ if (component_tag >= 0x30 && component_tag <= 0x37)
+ lav->profile = FF_PROFILE_ARIB_PROFILE_A;
+ break;
+ case 0x0012:
+ // component tag 0x87 signifies a mobile/partial reception
+ // (1seg) captioning service ("profile C").
+ if (component_tag == 0x87)
+ lav->profile = FF_PROFILE_ARIB_PROFILE_C;
+ break;
+ }
+ if (lav->profile == FF_PROFILE_UNKNOWN)
+ MP_WARN(demuxer, "ARIB caption profile %02x / %04x not supported.\n",
+ component_tag, data_component_id);
+ }
+
+ demux_add_sh_stream(demuxer, sh);
+
+ if (!subtitle_type)
+ MP_ERR(demuxer, "Subtitle type '%s' is not supported.\n", track->codec_id);
+
+ return 0;
+}
+
+static void probe_x264_garbage(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+
+ for (int n = 0; n < mkv_d->num_tracks; n++) {
+ mkv_track_t *track = mkv_d->tracks[n];
+ struct sh_stream *sh = track->stream;
+
+ if (!sh || sh->type != STREAM_VIDEO)
+ continue;
+
+ if (sh->codec->codec && strcmp(sh->codec->codec, "h264") != 0)
+ continue;
+
+ struct block_info *block = NULL;
+
+ // Find first block for this track.
+ // Restrict reading number of total packets. (Arbitrary to avoid bloat.)
+ for (int i = 0; i < 100; i++) {
+ if (i >= mkv_d->num_blocks && read_next_block_into_queue(demuxer) < 1)
+ break;
+ if (mkv_d->blocks[i].track == track) {
+ block = &mkv_d->blocks[i];
+ break;
+ }
+ }
+
+ if (!block || block->num_laces < 1)
+ continue;
+
+ bstr sblock = {block->laces[0]->data, block->laces[0]->size};
+ bstr nblock = demux_mkv_decode(demuxer->log, track, sblock, 1);
+
+ sh->codec->first_packet = new_demux_packet_from(nblock.start, nblock.len);
+ talloc_steal(mkv_d, sh->codec->first_packet);
+
+ if (nblock.start != sblock.start)
+ talloc_free(nblock.start);
+ }
+}
+
+static int read_ebml_header(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+ stream_t *s = demuxer->stream;
+
+ if (ebml_read_id(s) != EBML_ID_EBML)
+ return 0;
+ struct ebml_ebml ebml_master = {0};
+ struct ebml_parse_ctx parse_ctx = { demuxer->log, .no_error_messages = true };
+ if (ebml_read_element(s, &parse_ctx, &ebml_master, &ebml_ebml_desc) < 0)
+ return 0;
+ bool is_matroska = false, is_webm = false;
+ if (!ebml_master.doc_type) {
+ MP_VERBOSE(demuxer, "File has EBML header but no doctype. "
+ "Assuming \"matroska\".\n");
+ is_matroska = true;
+ } else if (strcmp(ebml_master.doc_type, "matroska") == 0) {
+ is_matroska = true;
+ } else if (strcmp(ebml_master.doc_type, "webm") == 0) {
+ is_webm = true;
+ }
+ if (!is_matroska && !is_webm) {
+ MP_TRACE(demuxer, "no head found\n");
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+ }
+ mkv_d->probably_webm_dash_init &= is_webm;
+ if (ebml_master.doc_type_read_version > 2) {
+ MP_WARN(demuxer, "This looks like a Matroska file, "
+ "but we don't support format version %"PRIu64"\n",
+ ebml_master.doc_type_read_version);
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+ }
+ if ((ebml_master.n_ebml_read_version
+ && ebml_master.ebml_read_version != EBML_VERSION)
+ || (ebml_master.n_ebml_max_size_length
+ && ebml_master.ebml_max_size_length > 8)
+ || (ebml_master.n_ebml_max_id_length
+ && ebml_master.ebml_max_id_length != 4))
+ {
+ MP_WARN(demuxer, "This looks like a Matroska file, "
+ "but the header has bad parameters\n");
+ talloc_free(parse_ctx.talloc_ctx);
+ return 0;
+ }
+ talloc_free(parse_ctx.talloc_ctx);
+
+ return 1;
+}
+
+static int read_mkv_segment_header(demuxer_t *demuxer, int64_t *segment_end)
+{
+ stream_t *s = demuxer->stream;
+ int num_skip = 0;
+ if (demuxer->params)
+ num_skip = demuxer->params->matroska_wanted_segment;
+
+ while (stream_read_peek(s, &(char){0}, 1)) {
+ if (ebml_read_id(s) != MATROSKA_ID_SEGMENT) {
+ MP_VERBOSE(demuxer, "segment not found\n");
+ return 0;
+ }
+ MP_DBG(demuxer, "+ a segment...\n");
+ uint64_t len = ebml_read_length(s);
+ *segment_end = (len == EBML_UINT_INVALID) ? 0 : stream_tell(s) + len;
+ if (num_skip <= 0)
+ return 1;
+ num_skip--;
+ MP_DBG(demuxer, " (skipping)\n");
+ if (*segment_end <= 0)
+ break;
+ if (*segment_end >= stream_get_size(s))
+ return 0;
+ if (!stream_seek(s, *segment_end)) {
+ MP_WARN(demuxer, "Failed to seek in file\n");
+ return 0;
+ }
+ // Segments are like concatenated Matroska files
+ if (!read_ebml_header(demuxer))
+ return 0;
+ }
+
+ MP_VERBOSE(demuxer, "End of file, no further segments.\n");
+ return 0;
+}
+
+static int demux_mkv_open(demuxer_t *demuxer, enum demux_check check)
+{
+ stream_t *s = demuxer->stream;
+ mkv_demuxer_t *mkv_d;
+ int64_t start_pos;
+ int64_t end_pos;
+
+ mkv_d = talloc_zero(demuxer, struct mkv_demuxer);
+ demuxer->priv = mkv_d;
+ mkv_d->tc_scale = 1000000;
+ mkv_d->a_skip_preroll = 1;
+ mkv_d->skip_to_timecode = INT64_MIN;
+
+ if (demuxer->params)
+ mkv_d->probably_webm_dash_init = demuxer->params->init_fragment.len > 0;
+
+ // Make sure you can seek back after read_ebml_header() if no EBML ID.
+ if (stream_read_peek(s, &(char[4]){0}, 4) != 4)
+ return -1;
+ if (!read_ebml_header(demuxer))
+ return -1;
+ MP_DBG(demuxer, "Found the head...\n");
+
+ if (!read_mkv_segment_header(demuxer, &end_pos))
+ return -1;
+
+ mkv_d->segment_start = stream_tell(s);
+ mkv_d->segment_end = end_pos;
+
+ struct MPOpts *mp_opts = mp_get_config_group(mkv_d, demuxer->global, &mp_opt_root);
+ mkv_d->edition_id = mp_opts->edition_id;
+ talloc_free(mp_opts);
+
+ mkv_d->opts = mp_get_config_group(mkv_d, demuxer->global, &demux_mkv_conf);
+
+ if (demuxer->params && demuxer->params->matroska_was_valid)
+ *demuxer->params->matroska_was_valid = true;
+
+ while (1) {
+ start_pos = stream_tell(s);
+ uint32_t id = ebml_read_id(s);
+ if (s->eof) {
+ if (!mkv_d->probably_webm_dash_init)
+ MP_WARN(demuxer, "Unexpected end of file (no clusters found)\n");
+ break;
+ }
+ if (id == MATROSKA_ID_CLUSTER) {
+ MP_DBG(demuxer, "|+ found cluster\n");
+ mkv_d->cluster_start = start_pos;
+ break;
+ }
+ int res = read_header_element(demuxer, id, start_pos);
+ if (res < 0)
+ return -1;
+ }
+
+ int64_t end = stream_get_size(s);
+
+ // Read headers that come after the first cluster (i.e. require seeking).
+ // Note: reading might increase ->num_headers.
+ // Likewise, ->headers might be reallocated.
+ int only_cue = -1;
+ for (int n = 0; n < mkv_d->num_headers; n++) {
+ struct header_elem *elem = &mkv_d->headers[n];
+ if (elem->parsed)
+ continue;
+ // Warn against incomplete files and skip headers outside of range.
+ if (elem->pos >= end || !s->seekable) {
+ elem->parsed = true; // don't bother if file is incomplete
+ if (end < 0 || !s->seekable) {
+ MP_WARN(demuxer, "Stream is not seekable or unknown size, "
+ "not reading mkv metadata at end of file.\n");
+ } else if (!mkv_d->eof_warning &&
+ !(mkv_d->probably_webm_dash_init && elem->pos == end))
+ {
+ MP_WARN(demuxer, "mkv metadata beyond end of file - incomplete "
+ "file?\n");
+ mkv_d->eof_warning = true;
+ }
+ continue;
+ }
+ only_cue = only_cue < 0 && elem->id == MATROSKA_ID_CUES;
+ }
+
+ // If there's only 1 needed element, and it's the cues, defer reading.
+ if (only_cue == 1) {
+ // Read cues when they are needed, to avoid seeking on opening.
+ MP_VERBOSE(demuxer, "Deferring reading cues.\n");
+ } else {
+ // Read them by ascending position to reduce unneeded seeks.
+ // O(n^2) because the number of elements is very low.
+ while (1) {
+ struct header_elem *lowest = NULL;
+ for (int n = 0; n < mkv_d->num_headers; n++) {
+ struct header_elem *elem = &mkv_d->headers[n];
+ if (elem->parsed)
+ continue;
+ if (!lowest || elem->pos < lowest->pos)
+ lowest = elem;
+ }
+
+ if (!lowest)
+ break;
+
+ if (read_deferred_element(demuxer, lowest) < 0)
+ return -1;
+ }
+ }
+
+ if (!stream_seek(s, start_pos)) {
+ MP_ERR(demuxer, "Couldn't seek back after reading headers?\n");
+ return -1;
+ }
+
+ MP_VERBOSE(demuxer, "All headers are parsed!\n");
+
+ display_create_tracks(demuxer);
+ add_coverart(demuxer);
+ process_tags(demuxer);
+
+ probe_first_timestamp(demuxer);
+ if (mkv_d->opts->probe_duration)
+ probe_last_timestamp(demuxer, start_pos);
+ probe_x264_garbage(demuxer);
+
+ return 0;
+}
+
+// Read the laced block data at the current stream position (until endpos as
+// indicated by the block length field) into individual buffers.
+static int demux_mkv_read_block_lacing(struct block_info *block, int type,
+ struct stream *s, uint64_t endpos)
+{
+ int laces;
+ uint32_t lace_size[MAX_NUM_LACES];
+
+
+ if (type == 0) { /* no lacing */
+ laces = 1;
+ lace_size[0] = endpos - stream_tell(s);
+ } else {
+ laces = stream_read_char(s);
+ if (laces < 0 || stream_tell(s) > endpos)
+ goto error;
+ laces += 1;
+
+ switch (type) {
+ case 1: { /* xiph lacing */
+ uint32_t total = 0;
+ for (int i = 0; i < laces - 1; i++) {
+ lace_size[i] = 0;
+ uint8_t t;
+ do {
+ t = stream_read_char(s);
+ if (s->eof || stream_tell(s) >= endpos)
+ goto error;
+ lace_size[i] += t;
+ } while (t == 0xFF);
+ total += lace_size[i];
+ }
+ uint32_t rest_length = endpos - stream_tell(s);
+ lace_size[laces - 1] = rest_length - total;
+ break;
+ }
+
+ case 2: { /* fixed-size lacing */
+ uint32_t full_length = endpos - stream_tell(s);
+ for (int i = 0; i < laces; i++)
+ lace_size[i] = full_length / laces;
+ break;
+ }
+
+ case 3: { /* EBML lacing */
+ uint64_t num = ebml_read_length(s);
+ if (num == EBML_UINT_INVALID || stream_tell(s) >= endpos)
+ goto error;
+
+ uint32_t total = lace_size[0] = num;
+ for (int i = 1; i < laces - 1; i++) {
+ int64_t snum = ebml_read_signed_length(s);
+ if (snum == EBML_INT_INVALID || stream_tell(s) >= endpos)
+ goto error;
+ lace_size[i] = lace_size[i - 1] + snum;
+ total += lace_size[i];
+ }
+ uint32_t rest_length = endpos - stream_tell(s);
+ lace_size[laces - 1] = rest_length - total;
+ break;
+ }
+
+ default:
+ goto error;
+ }
+ }
+
+ for (int i = 0; i < laces; i++) {
+ uint32_t size = lace_size[i];
+ if (stream_tell(s) + size > endpos || size > (1 << 30))
+ goto error;
+ int pad = MPMAX(AV_INPUT_BUFFER_PADDING_SIZE, AV_LZO_INPUT_PADDING);
+ AVBufferRef *buf = av_buffer_alloc(size + pad);
+ if (!buf)
+ goto error;
+ buf->size = size;
+ if (stream_read(s, buf->data, buf->size) != buf->size) {
+ av_buffer_unref(&buf);
+ goto error;
+ }
+ memset(buf->data + buf->size, 0, pad);
+ block->laces[block->num_laces++] = buf;
+ }
+
+ if (stream_tell(s) != endpos)
+ goto error;
+
+ return 0;
+
+ error:
+ return 1;
+}
+
+// Return whether the packet was handled & freed.
+static bool handle_realaudio(demuxer_t *demuxer, mkv_track_t *track,
+ struct demux_packet *orig)
+{
+ uint32_t sps = track->sub_packet_size;
+ uint32_t sph = track->sub_packet_h;
+ uint32_t cfs = track->coded_framesize; // restricted to [1,0x40000000]
+ uint32_t w = track->audiopk_size;
+ uint32_t spc = track->sub_packet_cnt;
+ uint8_t *buffer = orig->buffer;
+ uint32_t size = orig->len;
+ demux_packet_t *dp;
+ // track->audio_buf allocation size
+ size_t audiobuf_size = sph * w;
+
+ if (!track->audio_buf || !track->audio_timestamp || !track->stream)
+ return false;
+
+ const char *codec = track->stream->codec->codec ? track->stream->codec->codec : "";
+ if (!strcmp(codec, "ra_288")) {
+ for (int x = 0; x < sph / 2; x++) {
+ uint64_t dst_offset = x * 2 * w + spc * (uint64_t)cfs;
+ if (dst_offset + cfs > audiobuf_size)
+ goto error;
+ uint64_t src_offset = x * (uint64_t)cfs;
+ if (src_offset + cfs > size)
+ goto error;
+ memcpy(track->audio_buf + dst_offset, buffer + src_offset, cfs);
+ }
+ } else if (!strcmp(codec, "cook") || !strcmp(codec, "atrac3")) {
+ for (int x = 0; x < w / sps; x++) {
+ uint32_t dst_offset =
+ sps * (sph * x + ((sph + 1) / 2) * (spc & 1) + (spc >> 1));
+ if (dst_offset + sps > audiobuf_size)
+ goto error;
+ uint32_t src_offset = sps * x;
+ if (src_offset + sps > size)
+ goto error;
+ memcpy(track->audio_buf + dst_offset, buffer + src_offset, sps);
+ }
+ } else if (!strcmp(codec, "sipr")) {
+ if (spc * w + w > audiobuf_size || w > size)
+ goto error;
+ memcpy(track->audio_buf + spc * w, buffer, w);
+ if (spc == sph - 1) {
+ int n;
+ int bs = sph * w * 2 / 96; // nibbles per subpacket
+ // Perform reordering
+ for (n = 0; n < 38; n++) {
+ unsigned int i = bs * sipr_swaps[n][0]; // 77 max
+ unsigned int o = bs * sipr_swaps[n][1]; // 95 max
+ // swap nibbles of block 'i' with 'o'
+ for (int j = 0; j < bs; j++) {
+ if (i / 2 >= audiobuf_size || o / 2 >= audiobuf_size)
+ goto error;
+ uint8_t iv = track->audio_buf[i / 2];
+ uint8_t ov = track->audio_buf[o / 2];
+ int x = (i & 1) ? iv >> 4 : iv & 0x0F;
+ int y = (o & 1) ? ov >> 4 : ov & 0x0F;
+ track->audio_buf[o / 2] = (ov & 0x0F) | (o & 1 ? x << 4 : x);
+ track->audio_buf[i / 2] = (iv & 0x0F) | (i & 1 ? y << 4 : y);
+ i++;
+ o++;
+ }
+ }
+ }
+ } else {
+ // Not a codec that requires reordering
+ return false;
+ }
+
+ track->audio_timestamp[track->sub_packet_cnt] =
+ track->ra_pts == orig->pts ? 0 : orig->pts;
+ track->ra_pts = orig->pts;
+
+ if (++(track->sub_packet_cnt) == sph) {
+ track->sub_packet_cnt = 0;
+ // apk_usize has same range as coded_framesize in worst case
+ uint32_t apk_usize = track->stream->codec->block_align;
+ if (apk_usize > audiobuf_size)
+ goto error;
+ // Release all the audio packets
+ for (int x = 0; x < sph * w / apk_usize; x++) {
+ dp = new_demux_packet_from(track->audio_buf + x * apk_usize,
+ apk_usize);
+ if (!dp)
+ goto error;
+ /* Put timestamp only on packets that correspond to original
+ * audio packets in file */
+ dp->pts = (x * apk_usize % w) ? MP_NOPTS_VALUE :
+ track->audio_timestamp[x * apk_usize / w];
+ dp->pos = orig->pos + x;
+ dp->keyframe = !x; // Mark first packet as keyframe
+ add_packet(demuxer, track->stream, dp);
+ }
+ }
+
+error:
+ talloc_free(orig);
+ return true;
+}
+
+static void mkv_seek_reset(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+
+ for (int i = 0; i < mkv_d->num_tracks; i++) {
+ mkv_track_t *track = mkv_d->tracks[i];
+ if (track->av_parser)
+ av_parser_close(track->av_parser);
+ track->av_parser = NULL;
+ avcodec_free_context(&track->av_parser_codec);
+ }
+
+ for (int n = 0; n < mkv_d->num_blocks; n++)
+ free_block(&mkv_d->blocks[n]);
+ mkv_d->num_blocks = 0;
+
+ for (int n = 0; n < mkv_d->num_packets; n++)
+ talloc_free(mkv_d->packets[n]);
+ mkv_d->num_packets = 0;
+
+ mkv_d->skip_to_timecode = INT64_MIN;
+}
+
+// Copied from libavformat/matroskadec.c (FFmpeg 310f9dd / 2013-05-30)
+// Originally added with Libav commit 9b6f47c
+// License: LGPL v2.1 or later
+// Author header: The FFmpeg Project (this function still came from Libav)
+// Modified to use talloc, removed ffmpeg/libav specific error codes.
+static int libav_parse_wavpack(mkv_track_t *track, uint8_t *src,
+ uint8_t **pdst, int *size)
+{
+ uint8_t *dst = NULL;
+ int dstlen = 0;
+ int srclen = *size;
+ uint32_t samples;
+ uint16_t ver;
+ int offset = 0;
+
+ if (srclen < 12 || track->private_size < 2)
+ return -1;
+
+ ver = AV_RL16(track->private_data);
+
+ samples = AV_RL32(src);
+ src += 4;
+ srclen -= 4;
+
+ while (srclen >= 8) {
+ int multiblock;
+ uint32_t blocksize;
+ uint8_t *tmp;
+
+ uint32_t flags = AV_RL32(src);
+ uint32_t crc = AV_RL32(src + 4);
+ src += 8;
+ srclen -= 8;
+
+ multiblock = (flags & 0x1800) != 0x1800;
+ if (multiblock) {
+ if (srclen < 4)
+ goto fail;
+ blocksize = AV_RL32(src);
+ src += 4;
+ srclen -= 4;
+ } else {
+ blocksize = srclen;
+ }
+
+ if (blocksize > srclen)
+ goto fail;
+
+ if (dstlen > 0x10000000 || blocksize > 0x10000000)
+ goto fail;
+
+ tmp = talloc_realloc(track->parser_tmp, dst, uint8_t,
+ dstlen + blocksize + 32);
+ if (!tmp)
+ goto fail;
+ dst = tmp;
+ dstlen += blocksize + 32;
+
+ AV_WL32(dst + offset, MKTAG('w', 'v', 'p', 'k')); // tag
+ AV_WL32(dst + offset + 4, blocksize + 24); // blocksize - 8
+ AV_WL16(dst + offset + 8, ver); // version
+ AV_WL16(dst + offset + 10, 0); // track/index_no
+ AV_WL32(dst + offset + 12, 0); // total samples
+ AV_WL32(dst + offset + 16, 0); // block index
+ AV_WL32(dst + offset + 20, samples); // number of samples
+ AV_WL32(dst + offset + 24, flags); // flags
+ AV_WL32(dst + offset + 28, crc); // crc
+ memcpy (dst + offset + 32, src, blocksize); // block data
+
+ src += blocksize;
+ srclen -= blocksize;
+ offset += blocksize + 32;
+ }
+
+ *pdst = dst;
+ *size = dstlen;
+
+ return 0;
+
+fail:
+ talloc_free(dst);
+ return -1;
+}
+
+static void mkv_parse_and_add_packet(demuxer_t *demuxer, mkv_track_t *track,
+ struct demux_packet *dp)
+{
+ struct sh_stream *stream = track->stream;
+
+ if (stream->type == STREAM_AUDIO && handle_realaudio(demuxer, track, dp))
+ return;
+
+ if (strcmp(stream->codec->codec, "wavpack") == 0) {
+ int size = dp->len;
+ uint8_t *parsed;
+ if (libav_parse_wavpack(track, dp->buffer, &parsed, &size) >= 0) {
+ struct demux_packet *new = new_demux_packet_from(parsed, size);
+ if (new) {
+ demux_packet_copy_attribs(new, dp);
+ talloc_free(dp);
+ add_packet(demuxer, stream, new);
+ return;
+ }
+ }
+ }
+
+ if (strcmp(stream->codec->codec, "prores") == 0) {
+ size_t newlen = dp->len + 8;
+ struct demux_packet *new = new_demux_packet(newlen);
+ if (new) {
+ AV_WB32(new->buffer + 0, newlen);
+ AV_WB32(new->buffer + 4, MKBETAG('i', 'c', 'p', 'f'));
+ memcpy(new->buffer + 8, dp->buffer, dp->len);
+ demux_packet_copy_attribs(new, dp);
+ talloc_free(dp);
+ add_packet(demuxer, stream, new);
+ return;
+ }
+ }
+
+ if (track->parse && !track->av_parser) {
+ int id = mp_codec_to_av_codec_id(track->stream->codec->codec);
+ const AVCodec *codec = avcodec_find_decoder(id);
+ track->av_parser = av_parser_init(id);
+ if (codec)
+ track->av_parser_codec = avcodec_alloc_context3(codec);
+ }
+
+ if (!track->parse || !track->av_parser || !track->av_parser_codec) {
+ add_packet(demuxer, stream, dp);
+ return;
+ }
+
+ double tb = track->parse_timebase;
+ int64_t pts = dp->pts == MP_NOPTS_VALUE ? AV_NOPTS_VALUE : dp->pts * tb;
+ int64_t dts = dp->dts == MP_NOPTS_VALUE ? AV_NOPTS_VALUE : dp->dts * tb;
+ bool copy_sidedata = true;
+
+ while (dp->len) {
+ uint8_t *data = NULL;
+ int size = 0;
+ int len = av_parser_parse2(track->av_parser, track->av_parser_codec,
+ &data, &size, dp->buffer, dp->len,
+ pts, dts, 0);
+ if (len < 0 || len > dp->len)
+ break;
+ dp->buffer += len;
+ dp->len -= len;
+ dp->pos += len;
+ if (size) {
+ struct demux_packet *new = new_demux_packet_from(data, size);
+ if (!new)
+ break;
+ if (copy_sidedata)
+ av_packet_copy_props(new->avpacket, dp->avpacket);
+ copy_sidedata = false;
+ demux_packet_copy_attribs(new, dp);
+ if (track->parse_timebase) {
+ new->pts = track->av_parser->pts == AV_NOPTS_VALUE
+ ? MP_NOPTS_VALUE : track->av_parser->pts / tb;
+ new->dts = track->av_parser->dts == AV_NOPTS_VALUE
+ ? MP_NOPTS_VALUE : track->av_parser->dts / tb;
+ }
+ add_packet(demuxer, stream, new);
+ }
+ pts = dts = AV_NOPTS_VALUE;
+ }
+
+ if (dp->len) {
+ add_packet(demuxer, stream, dp);
+ } else {
+ talloc_free(dp);
+ }
+}
+
+static void free_block(struct block_info *block)
+{
+ for (int n = 0; n < block->num_laces; n++)
+ av_buffer_unref(&block->laces[n]);
+ block->num_laces = 0;
+ TA_FREEP(&block->additions);
+}
+
+static void index_block(demuxer_t *demuxer, struct block_info *block)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ if (block->keyframe) {
+ add_block_position(demuxer, block->track, mkv_d->cluster_start,
+ block->timecode / mkv_d->tc_scale,
+ block->duration / mkv_d->tc_scale);
+ }
+}
+
+static int read_block(demuxer_t *demuxer, int64_t end, struct block_info *block)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ stream_t *s = demuxer->stream;
+ uint64_t num;
+ int16_t time;
+ uint64_t length;
+
+ free_block(block);
+ length = ebml_read_length(s);
+ if (!length || length > 500000000 || stream_tell(s) + length > (uint64_t)end)
+ return -1;
+
+ uint64_t endpos = stream_tell(s) + length;
+ int res = -1;
+
+ // Parse header of the Block element
+ /* first byte(s): track num */
+ num = ebml_read_length(s);
+ if (num == EBML_UINT_INVALID || stream_tell(s) >= endpos)
+ goto exit;
+
+ /* time (relative to cluster time) */
+ if (stream_tell(s) + 3 > endpos)
+ goto exit;
+ uint8_t c1 = stream_read_char(s);
+ uint8_t c2 = stream_read_char(s);
+ time = c1 << 8 | c2;
+
+ uint8_t header_flags = stream_read_char(s);
+
+ block->filepos = stream_tell(s);
+
+ int lace_type = (header_flags >> 1) & 0x03;
+ if (demux_mkv_read_block_lacing(block, lace_type, s, endpos))
+ goto exit;
+
+ if (block->simple)
+ block->keyframe = header_flags & 0x80;
+ block->timecode = time * mkv_d->tc_scale + mkv_d->cluster_tc;
+ for (int i = 0; i < mkv_d->num_tracks; i++) {
+ if (mkv_d->tracks[i]->tnum == num) {
+ block->track = mkv_d->tracks[i];
+ break;
+ }
+ }
+ if (!block->track) {
+ res = 0;
+ goto exit;
+ }
+
+ if (stream_tell(s) != endpos)
+ goto exit;
+
+ res = 1;
+exit:
+ if (res <= 0)
+ free_block(block);
+ stream_seek_skip(s, endpos);
+ return res;
+}
+
+static int handle_block(demuxer_t *demuxer, struct block_info *block_info)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ double current_pts;
+ bool keyframe = block_info->keyframe;
+ uint64_t block_duration = block_info->duration;
+ int64_t tc = block_info->timecode;
+ mkv_track_t *track = block_info->track;
+ struct sh_stream *stream = track->stream;
+ bool use_this_block = tc >= mkv_d->skip_to_timecode;
+
+ if (!demux_stream_is_selected(stream))
+ return 0;
+
+ current_pts = tc / 1e9 - track->codec_delay;
+
+ if (track->require_keyframes && !keyframe) {
+ keyframe = true;
+ if (!mkv_d->keyframe_warning) {
+ MP_WARN(demuxer, "This is a broken file! Packets with incorrect "
+ "keyframe flag found. Enabling workaround.\n");
+ mkv_d->keyframe_warning = true;
+ }
+ }
+
+ if (track->type == MATROSKA_TRACK_AUDIO) {
+ if (mkv_d->a_skip_to_keyframe)
+ use_this_block &= keyframe;
+ } else if (track->type == MATROSKA_TRACK_SUBTITLE) {
+ if (!use_this_block && mkv_d->subtitle_preroll) {
+ int64_t end_time = block_info->timecode + block_info->duration;
+ if (!block_info->duration)
+ end_time = INT64_MAX;
+ use_this_block = end_time > mkv_d->skip_to_timecode;
+ if (use_this_block) {
+ if (mkv_d->subtitle_preroll) {
+ mkv_d->subtitle_preroll--;
+ } else {
+ // This could overflow the demuxer queue.
+ use_this_block = 0;
+ }
+ }
+ }
+ if (use_this_block) {
+ if (block_info->num_laces > 1) {
+ MP_WARN(demuxer, "Subtitles use Matroska "
+ "lacing. This is abnormal and not supported.\n");
+ use_this_block = 0;
+ }
+ }
+ } else if (track->type == MATROSKA_TRACK_VIDEO) {
+ if (mkv_d->v_skip_to_keyframe)
+ use_this_block &= keyframe;
+ }
+
+ if (use_this_block) {
+ uint64_t filepos = block_info->filepos;
+
+ for (int i = 0; i < block_info->num_laces; i++) {
+ AVBufferRef *data = block_info->laces[i];
+ demux_packet_t *dp = NULL;
+
+ bstr block = {data->data, data->size};
+ bstr nblock = demux_mkv_decode(demuxer->log, track, block, 1);
+
+ if (block.start != nblock.start || block.len != nblock.len) {
+ // (avoidable copy of the entire data)
+ dp = new_demux_packet_from(nblock.start, nblock.len);
+ } else {
+ dp = new_demux_packet_from_buf(data);
+ }
+ if (!dp)
+ break;
+
+ dp->pos = filepos;
+ /* If default_duration is 0, assume no pts value is known
+ * for packets after the first one (rather than all pts
+ * values being the same). Also, don't use it for extra
+ * packets resulting from parsing. */
+ if (i == 0 || track->default_duration) {
+ dp->pts = current_pts + i * track->default_duration;
+ dp->keyframe = keyframe;
+ }
+ if (stream->codec->avi_dts)
+ MPSWAP(double, dp->pts, dp->dts);
+ if (i == 0 && block_info->duration_known)
+ dp->duration = block_duration / 1e9;
+ if (stream->type == STREAM_AUDIO) {
+ unsigned int srate = stream->codec->samplerate;
+ demux_packet_set_padding(dp, 0,
+ block_info->discardpadding / 1e9 * srate);
+ mkv_d->a_skip_preroll = 0;
+ }
+ if (block_info->additions) {
+ for (int n = 0; n < block_info->additions->n_block_more; n++) {
+ struct ebml_block_more *add =
+ &block_info->additions->block_more[n];
+ int64_t id = add->n_block_add_id ? add->block_add_id : 1;
+ demux_packet_add_blockadditional(dp, id,
+ add->block_additional.start, add->block_additional.len);
+ }
+ }
+
+ mkv_parse_and_add_packet(demuxer, track, dp);
+ talloc_free_children(track->parser_tmp);
+ filepos += data->size;
+ }
+
+ if (stream->type == STREAM_VIDEO) {
+ mkv_d->v_skip_to_keyframe = 0;
+ mkv_d->skip_to_timecode = INT64_MIN;
+ mkv_d->subtitle_preroll = 0;
+ } else if (stream->type == STREAM_AUDIO) {
+ mkv_d->a_skip_to_keyframe = 0;
+ }
+
+ return 1;
+ }
+
+ return 0;
+}
+
+static int read_block_group(demuxer_t *demuxer, int64_t end,
+ struct block_info *block)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ stream_t *s = demuxer->stream;
+ *block = (struct block_info){ .keyframe = true };
+
+ while (stream_tell(s) < end) {
+ switch (ebml_read_id(s)) {
+ case MATROSKA_ID_BLOCKDURATION:
+ block->duration = ebml_read_uint(s);
+ if (block->duration == EBML_UINT_INVALID)
+ goto error;
+ block->duration *= mkv_d->tc_scale;
+ block->duration_known = true;
+ break;
+
+ case MATROSKA_ID_DISCARDPADDING:
+ block->discardpadding = ebml_read_uint(s);
+ if (block->discardpadding == EBML_UINT_INVALID)
+ goto error;
+ break;
+
+ case MATROSKA_ID_BLOCK:
+ if (read_block(demuxer, end, block) < 0)
+ goto error;
+ break;
+
+ case MATROSKA_ID_REFERENCEBLOCK:;
+ int64_t num = ebml_read_int(s);
+ if (num == EBML_INT_INVALID)
+ goto error;
+ block->keyframe = false;
+ break;
+
+ case MATROSKA_ID_BLOCKADDITIONS:;
+ struct ebml_block_additions additions = {0};
+ struct ebml_parse_ctx parse_ctx = {demuxer->log};
+ if (ebml_read_element(s, &parse_ctx, &additions,
+ &ebml_block_additions_desc) < 0)
+ return -1;
+ if (additions.n_block_more > 0) {
+ block->additions = talloc_dup(NULL, &additions);
+ talloc_steal(block->additions, parse_ctx.talloc_ctx);
+ parse_ctx.talloc_ctx = NULL;
+ }
+ talloc_free(parse_ctx.talloc_ctx);
+ break;
+
+ case MATROSKA_ID_CLUSTER:
+ case EBML_ID_INVALID:
+ goto error;
+
+ default:
+ if (ebml_read_skip(demuxer->log, end, s) != 0)
+ goto error;
+ break;
+ }
+ }
+
+ return block->num_laces ? 1 : 0;
+
+error:
+ free_block(block);
+ return -1;
+}
+
+static int read_next_block_into_queue(demuxer_t *demuxer)
+{
+ mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
+ stream_t *s = demuxer->stream;
+ struct block_info block = {0};
+
+ while (1) {
+ while (stream_tell(s) < mkv_d->cluster_end) {
+ int64_t start_filepos = stream_tell(s);
+ switch (ebml_read_id(s)) {
+ case MATROSKA_ID_TIMECODE: {
+ uint64_t num = ebml_read_uint(s);
+ if (num == EBML_UINT_INVALID)
+ goto find_next_cluster;
+ mkv_d->cluster_tc = num * mkv_d->tc_scale;
+ break;
+ }
+
+ case MATROSKA_ID_BLOCKGROUP: {
+ int64_t end = ebml_read_length(s);
+ end += stream_tell(s);
+ if (end > mkv_d->cluster_end)
+ goto find_next_cluster;
+ int res = read_block_group(demuxer, end, &block);
+ if (res < 0)
+ goto find_next_cluster;
+ if (res > 0)
+ goto add_block;
+ break;
+ }
+
+ case MATROSKA_ID_SIMPLEBLOCK: {
+ block = (struct block_info){ .simple = true };
+ int res = read_block(demuxer, mkv_d->cluster_end, &block);
+ if (res < 0)
+ goto find_next_cluster;
+ if (res > 0)
+ goto add_block;
+ break;
+ }
+
+ case MATROSKA_ID_CLUSTER:
+ mkv_d->cluster_start = start_filepos;
+ goto next_cluster;
+
+ case EBML_ID_INVALID:
+ goto find_next_cluster;
+
+ default: ;
+ if (ebml_read_skip(demuxer->log, mkv_d->cluster_end, s) != 0)
+ goto find_next_cluster;
+ break;
+ }
+ }
+
+ find_next_cluster:
+ mkv_d->cluster_end = 0;
+ for (;;) {
+ mkv_d->cluster_start = stream_tell(s);
+ uint32_t id = ebml_read_id(s);
+ if (id == MATROSKA_ID_CLUSTER)
+ break;
+ if (s->eof)
+ return -1;
+ if (demux_cancel_test(demuxer))
+ return -1;
+ if (id == EBML_ID_EBML && stream_tell(s) >= mkv_d->segment_end) {
+ // Appended segment - don't use its clusters, consider this EOF.
+ stream_seek(s, stream_tell(s) - 4);
+ return -1;
+ }
+ // For the sake of robustness, consider even unknown level 1
+ // elements the same as unknown/broken IDs.
+ if ((!ebml_is_mkv_level1_id(id) && id != EBML_ID_VOID) ||
+ ebml_read_skip(demuxer->log, -1, s) != 0)
+ {
+ stream_seek(s, mkv_d->cluster_start);
+ ebml_resync_cluster(demuxer->log, s);
+ }
+ }
+ next_cluster:
+ mkv_d->cluster_end = ebml_read_length(s);
+ // mkv files for "streaming" can have this legally
+ if (mkv_d->cluster_end != EBML_UINT_INVALID)
+ mkv_d->cluster_end += stream_tell(s);
+ }
+ MP_ASSERT_UNREACHABLE();
+
+add_block:
+ index_block(demuxer, &block);
+ MP_TARRAY_APPEND(mkv_d, mkv_d->blocks, mkv_d->num_blocks, block);
+ return 1;
+}
+
+static int read_next_block(demuxer_t *demuxer, struct block_info *block)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+
+ if (!mkv_d->num_blocks) {
+ int res = read_next_block_into_queue(demuxer);
+ if (res < 1)
+ return res;
+
+ assert(mkv_d->num_blocks);
+ }
+
+ *block = mkv_d->blocks[0];
+ MP_TARRAY_REMOVE_AT(mkv_d->blocks, mkv_d->num_blocks, 0);
+ return 1;
+}
+
+static bool demux_mkv_read_packet(struct demuxer *demuxer,
+ struct demux_packet **pkt)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+
+ for (;;) {
+ if (mkv_d->num_packets) {
+ *pkt = mkv_d->packets[0];
+ MP_TARRAY_REMOVE_AT(mkv_d->packets, mkv_d->num_packets, 0);
+ return true;
+ }
+
+ int res;
+ struct block_info block;
+ res = read_next_block(demuxer, &block);
+ if (res < 0)
+ return false;
+ if (res > 0) {
+ handle_block(demuxer, &block);
+ free_block(&block);
+ }
+ }
+}
+
+static mkv_index_t *get_highest_index_entry(struct demuxer *demuxer)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ assert(!mkv_d->index_complete); // would require separate code
+
+ mkv_index_t *index = NULL;
+ for (int n = 0; n < mkv_d->num_tracks; n++) {
+ int n_index = mkv_d->tracks[n]->last_index_entry;
+ if (n_index >= 0) {
+ mkv_index_t *index2 = &mkv_d->indexes[n_index];
+ if (!index || index2->filepos > index->filepos)
+ index = index2;
+ }
+ }
+ return index;
+}
+
+static int create_index_until(struct demuxer *demuxer, int64_t timecode)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ struct stream *s = demuxer->stream;
+
+ read_deferred_cues(demuxer);
+
+ if (mkv_d->index_complete)
+ return 0;
+
+ mkv_index_t *index = get_highest_index_entry(demuxer);
+
+ if (!index || index->timecode * mkv_d->tc_scale < timecode) {
+ stream_seek(s, index ? index->filepos : mkv_d->cluster_start);
+ MP_VERBOSE(demuxer, "creating index until TC %"PRId64"\n", timecode);
+ for (;;) {
+ int res;
+ struct block_info block;
+ res = read_next_block(demuxer, &block);
+ if (res < 0)
+ break;
+ if (res > 0) {
+ free_block(&block);
+ }
+ index = get_highest_index_entry(demuxer);
+ if (index && index->timecode * mkv_d->tc_scale >= timecode)
+ break;
+ }
+ }
+ if (!mkv_d->indexes) {
+ MP_WARN(demuxer, "no target for seek found\n");
+ return -1;
+ }
+ return 0;
+}
+
+static struct mkv_index *seek_with_cues(struct demuxer *demuxer, int seek_id,
+ int64_t target_timecode, int flags)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ struct mkv_index *index = NULL;
+
+ int64_t min_diff = INT64_MIN;
+ for (size_t i = 0; i < mkv_d->num_indexes; i++) {
+ if (seek_id < 0 || mkv_d->indexes[i].tnum == seek_id) {
+ int64_t diff =
+ mkv_d->indexes[i].timecode * mkv_d->tc_scale - target_timecode;
+ if (flags & SEEK_FORWARD)
+ diff = -diff;
+ if (min_diff != INT64_MIN) {
+ if (diff <= 0) {
+ if (min_diff <= 0 && diff <= min_diff)
+ continue;
+ } else if (diff >= min_diff)
+ continue;
+ }
+ min_diff = diff;
+ index = mkv_d->indexes + i;
+ }
+ }
+
+ if (index) { /* We've found an entry. */
+ uint64_t seek_pos = index->filepos;
+ if (flags & SEEK_HR) {
+ // Find the cluster with the highest filepos, that has a timestamp
+ // still lower than min_tc.
+ double secs = mkv_d->opts->subtitle_preroll_secs;
+ if (mkv_d->index_has_durations)
+ secs = MPMAX(secs, mkv_d->opts->subtitle_preroll_secs_index);
+ double pre_f = secs * 1e9 / mkv_d->tc_scale;
+ int64_t pre = pre_f >= (double)INT64_MAX ? INT64_MAX : (int64_t)pre_f;
+ int64_t min_tc = pre < index->timecode ? index->timecode - pre : 0;
+ uint64_t prev_target = 0;
+ int64_t prev_tc = 0;
+ for (size_t i = 0; i < mkv_d->num_indexes; i++) {
+ if (seek_id < 0 || mkv_d->indexes[i].tnum == seek_id) {
+ struct mkv_index *cur = &mkv_d->indexes[i];
+ if (cur->timecode <= min_tc && cur->timecode >= prev_tc) {
+ prev_tc = cur->timecode;
+ prev_target = cur->filepos;
+ }
+ }
+ }
+ if (mkv_d->index_has_durations) {
+ // Find the earliest cluster that is not before prev_target,
+ // but contains subtitle packets overlapping with the cluster
+ // at seek_pos.
+ uint64_t target = seek_pos;
+ for (size_t i = 0; i < mkv_d->num_indexes; i++) {
+ struct mkv_index *cur = &mkv_d->indexes[i];
+ if (cur->timecode <= index->timecode &&
+ cur->timecode + cur->duration > index->timecode &&
+ cur->filepos >= prev_target &&
+ cur->filepos < target)
+ {
+ target = cur->filepos;
+ }
+ }
+ prev_target = target;
+ }
+ if (prev_target)
+ seek_pos = prev_target;
+ }
+
+ mkv_d->cluster_end = 0;
+ stream_seek(demuxer->stream, seek_pos);
+ }
+ return index;
+}
+
+static void demux_mkv_seek(demuxer_t *demuxer, double seek_pts, int flags)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+ int64_t old_pos = stream_tell(demuxer->stream);
+ uint64_t v_tnum = -1;
+ uint64_t a_tnum = -1;
+ bool st_active[STREAM_TYPE_COUNT] = {0};
+ mkv_seek_reset(demuxer);
+ for (int i = 0; i < mkv_d->num_tracks; i++) {
+ mkv_track_t *track = mkv_d->tracks[i];
+ if (demux_stream_is_selected(track->stream)) {
+ st_active[track->stream->type] = true;
+ if (track->type == MATROSKA_TRACK_VIDEO)
+ v_tnum = track->tnum;
+ if (track->type == MATROSKA_TRACK_AUDIO)
+ a_tnum = track->tnum;
+ }
+ }
+
+ mkv_d->subtitle_preroll = NUM_SUB_PREROLL_PACKETS;
+ int preroll_opt = mkv_d->opts->subtitle_preroll;
+ if (preroll_opt == 1 || (preroll_opt == 2 && mkv_d->index_has_durations))
+ flags |= SEEK_HR;
+ if (!st_active[STREAM_SUB])
+ flags &= ~SEEK_HR;
+
+ // Adjust the target a little bit to catch cases where the target position
+ // specifies a keyframe with high, but not perfect, precision.
+ seek_pts += flags & SEEK_FORWARD ? -0.005 : 0.005;
+
+ if (!(flags & SEEK_FACTOR)) { /* time in secs */
+ mkv_index_t *index = NULL;
+
+ seek_pts = MPMAX(seek_pts, 0);
+ int64_t target_timecode = seek_pts * 1e9 + 0.5;
+
+ if (create_index_until(demuxer, target_timecode) >= 0) {
+ int seek_id = st_active[STREAM_VIDEO] ? v_tnum : a_tnum;
+ index = seek_with_cues(demuxer, seek_id, target_timecode, flags);
+ if (!index)
+ index = seek_with_cues(demuxer, -1, target_timecode, flags);
+ }
+
+ if (!index)
+ stream_seek(demuxer->stream, old_pos);
+
+ if (flags & SEEK_FORWARD) {
+ mkv_d->skip_to_timecode = target_timecode;
+ } else {
+ mkv_d->skip_to_timecode = index ? index->timecode * mkv_d->tc_scale
+ : INT64_MIN;
+ }
+ } else {
+ stream_t *s = demuxer->stream;
+
+ read_deferred_cues(demuxer);
+
+ int64_t size = stream_get_size(s);
+ int64_t target_filepos = size * MPCLAMP(seek_pts, 0, 1);
+
+ mkv_index_t *index = NULL;
+ if (mkv_d->index_complete) {
+ for (size_t i = 0; i < mkv_d->num_indexes; i++) {
+ if (mkv_d->indexes[i].tnum == v_tnum) {
+ if ((index == NULL)
+ || ((mkv_d->indexes[i].filepos >= target_filepos)
+ && ((index->filepos < target_filepos)
+ || (mkv_d->indexes[i].filepos < index->filepos))))
+ index = &mkv_d->indexes[i];
+ }
+ }
+ }
+
+ mkv_d->cluster_end = 0;
+
+ if (index) {
+ stream_seek(s, index->filepos);
+ mkv_d->skip_to_timecode = index->timecode * mkv_d->tc_scale;
+ } else {
+ stream_seek(s, MPMAX(target_filepos, 0));
+ if (ebml_resync_cluster(mp_null_log, s) < 0) {
+ // Assume EOF
+ mkv_d->cluster_end = size;
+ }
+ }
+ }
+
+ mkv_d->v_skip_to_keyframe = st_active[STREAM_VIDEO];
+ mkv_d->a_skip_to_keyframe = st_active[STREAM_AUDIO];
+ mkv_d->a_skip_preroll = mkv_d->a_skip_to_keyframe;
+}
+
+static void probe_last_timestamp(struct demuxer *demuxer, int64_t start_pos)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+
+ if (!demuxer->seekable)
+ return;
+
+ // Pick some arbitrary video track
+ int v_tnum = -1;
+ for (int n = 0; n < mkv_d->num_tracks; n++) {
+ if (mkv_d->tracks[n]->type == MATROSKA_TRACK_VIDEO) {
+ v_tnum = mkv_d->tracks[n]->tnum;
+ break;
+ }
+ }
+ if (v_tnum < 0)
+ return;
+
+ // In full mode, we start reading data from the current file position,
+ // which works because this function is called after headers are parsed.
+ if (mkv_d->opts->probe_duration != 2) {
+ read_deferred_cues(demuxer);
+ if (mkv_d->index_complete) {
+ // Find last cluster that still has video packets
+ int64_t target = 0;
+ for (size_t i = 0; i < mkv_d->num_indexes; i++) {
+ struct mkv_index *cur = &mkv_d->indexes[i];
+ if (cur->tnum == v_tnum)
+ target = MPMAX(target, cur->filepos);
+ }
+ if (!target)
+ return;
+
+ if (!stream_seek(demuxer->stream, target))
+ return;
+ } else {
+ // No index -> just try to find a random cluster towards file end.
+ int64_t size = stream_get_size(demuxer->stream);
+ stream_seek(demuxer->stream, MPMAX(size - 10 * 1024 * 1024, 0));
+ if (ebml_resync_cluster(mp_null_log, demuxer->stream) < 0)
+ stream_seek(demuxer->stream, start_pos); // full scan otherwise
+ }
+ }
+
+ mkv_seek_reset(demuxer);
+
+ int64_t last_ts[STREAM_TYPE_COUNT] = {0};
+ while (1) {
+ struct block_info block;
+ int res = read_next_block(demuxer, &block);
+ if (res < 0)
+ break;
+ if (res > 0) {
+ if (block.track && block.track->stream) {
+ enum stream_type type = block.track->stream->type;
+ uint64_t endtime = block.timecode + block.duration;
+ if (last_ts[type] < endtime)
+ last_ts[type] = endtime;
+ }
+ free_block(&block);
+ }
+ }
+
+ if (!last_ts[STREAM_VIDEO])
+ last_ts[STREAM_VIDEO] = mkv_d->cluster_tc;
+
+ if (last_ts[STREAM_VIDEO]) {
+ mkv_d->duration = last_ts[STREAM_VIDEO] / 1e9 - demuxer->start_time;
+ demuxer->duration = mkv_d->duration;
+ }
+
+ stream_seek(demuxer->stream, start_pos);
+ mkv_d->cluster_start = mkv_d->cluster_end = 0;
+}
+
+static void probe_first_timestamp(struct demuxer *demuxer)
+{
+ mkv_demuxer_t *mkv_d = demuxer->priv;
+
+ if (!mkv_d->opts->probe_start_time)
+ return;
+
+ read_next_block_into_queue(demuxer);
+
+ demuxer->start_time = mkv_d->cluster_tc / 1e9;
+
+ if (demuxer->start_time)
+ MP_VERBOSE(demuxer, "Start PTS: %f\n", demuxer->start_time);
+}
+
+static void mkv_free(struct demuxer *demuxer)
+{
+ struct mkv_demuxer *mkv_d = demuxer->priv;
+ if (!mkv_d)
+ return;
+ mkv_seek_reset(demuxer);
+ for (int i = 0; i < mkv_d->num_tracks; i++)
+ demux_mkv_free_trackentry(mkv_d->tracks[i]);
+}
+
+const demuxer_desc_t demuxer_desc_matroska = {
+ .name = "mkv",
+ .desc = "Matroska",
+ .open = demux_mkv_open,
+ .read_packet = demux_mkv_read_packet,
+ .close = mkv_free,
+ .seek = demux_mkv_seek,
+ .load_timeline = build_ordered_chapter_timeline,
+};
+
+bool demux_matroska_uid_cmp(struct matroska_segment_uid *a,
+ struct matroska_segment_uid *b)
+{
+ return (!memcmp(a->segment, b->segment, 16) &&
+ a->edition == b->edition);
+}
diff --git a/demux/demux_mkv_timeline.c b/demux/demux_mkv_timeline.c
new file mode 100644
index 0000000..0c23e27
--- /dev/null
+++ b/demux/demux_mkv_timeline.c
@@ -0,0 +1,642 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <dirent.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <libavutil/common.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "demux/demux.h"
+#include "demux/timeline.h"
+#include "demux/matroska.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "misc/thread_tools.h"
+#include "common/common.h"
+#include "common/playlist.h"
+#include "stream/stream.h"
+
+struct tl_ctx {
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct MPOpts *opts;
+ struct timeline *tl;
+
+ struct demuxer *demuxer;
+
+ struct demuxer **sources;
+ int num_sources;
+
+ struct timeline_part *timeline;
+ int num_parts;
+
+ struct matroska_segment_uid *uids;
+ uint64_t start_time; // When the next part should start on the complete timeline.
+ uint64_t missing_time; // Total missing time so far.
+ uint64_t last_end_time; // When the last part ended on the complete timeline.
+ int num_chapters; // Total number of expected chapters.
+};
+
+struct find_entry {
+ char *name;
+ int matchlen;
+ off_t size;
+};
+
+static int cmp_entry(const void *pa, const void *pb)
+{
+ const struct find_entry *a = pa, *b = pb;
+ // check "similar" filenames first
+ int matchdiff = b->matchlen - a->matchlen;
+ if (matchdiff)
+ return FFSIGN(matchdiff);
+ // check small files first
+ off_t sizediff = a->size - b->size;
+ if (sizediff)
+ return FFSIGN(sizediff);
+ return 0;
+}
+
+static bool test_matroska_ext(const char *filename)
+{
+ static const char *const exts[] = {".mkv", ".mka", ".mks", ".mk3d", NULL};
+ for (int n = 0; exts[n]; n++) {
+ const char *suffix = exts[n];
+ int offset = strlen(filename) - strlen(suffix);
+ // name must end with suffix
+ if (offset > 0 && strcasecmp(filename + offset, suffix) == 0)
+ return true;
+ }
+ return false;
+}
+
+static char **find_files(const char *original_file)
+{
+ void *tmpmem = talloc_new(NULL);
+ char *basename = mp_basename(original_file);
+ struct bstr directory = mp_dirname(original_file);
+ char **results = talloc_size(NULL, 0);
+ char *dir_zero = bstrdup0(tmpmem, directory);
+ DIR *dp = opendir(dir_zero);
+ if (!dp) {
+ talloc_free(tmpmem);
+ return results;
+ }
+ struct find_entry *entries = NULL;
+ struct dirent *ep;
+ int num_results = 0;
+ while ((ep = readdir(dp))) {
+ if (!test_matroska_ext(ep->d_name))
+ continue;
+ // don't list the original name
+ if (!strcmp(ep->d_name, basename))
+ continue;
+
+ char *name = mp_path_join_bstr(results, directory, bstr0(ep->d_name));
+ char *s1 = ep->d_name;
+ char *s2 = basename;
+ int matchlen = 0;
+ while (*s1 && *s1++ == *s2++)
+ matchlen++;
+ // be a bit more fuzzy about matching the filename
+ matchlen = (matchlen + 3) / 5;
+
+ struct stat statbuf;
+ if (stat(name, &statbuf) != 0)
+ continue;
+ off_t size = statbuf.st_size;
+
+ entries = talloc_realloc(tmpmem, entries, struct find_entry,
+ num_results + 1);
+ entries[num_results] = (struct find_entry) { name, matchlen, size };
+ num_results++;
+ }
+ closedir(dp);
+ // NOTE: maybe should make it compare pointers instead
+ if (entries)
+ qsort(entries, num_results, sizeof(struct find_entry), cmp_entry);
+ results = talloc_realloc(NULL, results, char *, num_results);
+ for (int i = 0; i < num_results; i++) {
+ results[i] = entries[i].name;
+ }
+ talloc_free(tmpmem);
+ return results;
+}
+
+static bool has_source_request(struct tl_ctx *ctx,
+ struct matroska_segment_uid *new_uid)
+{
+ for (int i = 0; i < ctx->num_sources; ++i) {
+ if (demux_matroska_uid_cmp(&ctx->uids[i], new_uid))
+ return true;
+ }
+ return false;
+}
+
+// segment = get Nth segment of a multi-segment file
+static bool check_file_seg(struct tl_ctx *ctx, char *filename, int segment)
+{
+ bool was_valid = false;
+ struct demuxer_params params = {
+ .force_format = "mkv",
+ .matroska_num_wanted_uids = ctx->num_sources,
+ .matroska_wanted_uids = ctx->uids,
+ .matroska_wanted_segment = segment,
+ .matroska_was_valid = &was_valid,
+ .disable_timeline = true,
+ .stream_flags = ctx->tl->stream_origin,
+ };
+ struct mp_cancel *cancel = ctx->tl->cancel;
+ if (mp_cancel_test(cancel))
+ return false;
+
+ struct demuxer *d = demux_open_url(filename, &params, cancel, ctx->global);
+ if (!d)
+ return false;
+
+ struct matroska_data *m = &d->matroska_data;
+
+ for (int i = 1; i < ctx->num_sources; i++) {
+ struct matroska_segment_uid *uid = &ctx->uids[i];
+ if (ctx->sources[i])
+ continue;
+ /* Accept the source if the segment uid matches and the edition
+ * either matches or isn't specified. */
+ if (!memcmp(uid->segment, m->uid.segment, 16) &&
+ (!uid->edition || uid->edition == m->uid.edition))
+ {
+ MP_INFO(ctx, "Match for source %d: %s\n", i, d->filename);
+
+ if (!uid->edition) {
+ m->uid.edition = 0;
+ } else {
+ for (int j = 0; j < m->num_ordered_chapters; j++) {
+ struct matroska_chapter *c = m->ordered_chapters + j;
+
+ if (!c->has_segment_uid)
+ continue;
+
+ if (has_source_request(ctx, &c->uid))
+ continue;
+
+ /* Set the requested segment. */
+ MP_TARRAY_GROW(NULL, ctx->uids, ctx->num_sources);
+ ctx->uids[ctx->num_sources] = c->uid;
+
+ /* Add a new source slot. */
+ MP_TARRAY_APPEND(NULL, ctx->sources, ctx->num_sources, NULL);
+ }
+ }
+
+ ctx->sources[i] = d;
+ return true;
+ }
+ }
+
+ demux_free(d);
+ return was_valid;
+}
+
+static void check_file(struct tl_ctx *ctx, char *filename, int first)
+{
+ for (int segment = first; ; segment++) {
+ if (!check_file_seg(ctx, filename, segment))
+ break;
+ }
+}
+
+static bool missing(struct tl_ctx *ctx)
+{
+ for (int i = 0; i < ctx->num_sources; i++) {
+ if (!ctx->sources[i])
+ return true;
+ }
+ return false;
+}
+
+static void find_ordered_chapter_sources(struct tl_ctx *ctx)
+{
+ struct MPOpts *opts = ctx->opts;
+ void *tmp = talloc_new(NULL);
+ int num_filenames = 0;
+ char **filenames = NULL;
+ if (ctx->num_sources > 1) {
+ char *main_filename = ctx->demuxer->filename;
+ MP_INFO(ctx, "This file references data from other sources.\n");
+ if (opts->ordered_chapters_files && opts->ordered_chapters_files[0]) {
+ MP_INFO(ctx, "Loading references from '%s'.\n",
+ opts->ordered_chapters_files);
+ struct playlist *pl =
+ playlist_parse_file(opts->ordered_chapters_files,
+ ctx->tl->cancel, ctx->global);
+ talloc_steal(tmp, pl);
+ for (int n = 0; n < pl->num_entries; n++) {
+ MP_TARRAY_APPEND(tmp, filenames, num_filenames,
+ pl->entries[n]->filename);
+ }
+ } else if (!ctx->demuxer->stream->is_local_file) {
+ MP_WARN(ctx, "Playback source is not a "
+ "normal disk file. Will not search for related files.\n");
+ } else {
+ MP_INFO(ctx, "Will scan other files in the "
+ "same directory to find referenced sources.\n");
+ filenames = find_files(main_filename);
+ num_filenames = MP_TALLOC_AVAIL(filenames);
+ talloc_steal(tmp, filenames);
+ }
+ // Possibly get further segments appended to the first segment
+ check_file(ctx, main_filename, 1);
+ }
+
+ int old_source_count;
+ do {
+ old_source_count = ctx->num_sources;
+ for (int i = 0; i < num_filenames; i++) {
+ if (!missing(ctx))
+ break;
+ MP_VERBOSE(ctx, "Checking file %s\n", filenames[i]);
+ check_file(ctx, filenames[i], 0);
+ }
+ } while (old_source_count != ctx->num_sources);
+
+ if (missing(ctx)) {
+ MP_ERR(ctx, "Failed to find ordered chapter part!\n");
+ int j = 1;
+ for (int i = 1; i < ctx->num_sources; i++) {
+ if (ctx->sources[i]) {
+ ctx->sources[j] = ctx->sources[i];
+ ctx->uids[j] = ctx->uids[i];
+ j++;
+ }
+ }
+ ctx->num_sources = j;
+ }
+
+ // Copy attachments from referenced sources so fonts are loaded for sub
+ // rendering.
+ for (int i = 1; i < ctx->num_sources; i++) {
+ for (int j = 0; j < ctx->sources[i]->num_attachments; j++) {
+ struct demux_attachment *att = &ctx->sources[i]->attachments[j];
+ demuxer_add_attachment(ctx->demuxer, att->name, att->type,
+ att->data, att->data_size);
+ }
+ }
+
+ talloc_free(tmp);
+}
+
+struct inner_timeline_info {
+ uint64_t skip; // Amount of time to skip.
+ uint64_t limit; // How much time is expected for the parent chapter.
+};
+
+static int64_t add_timeline_part(struct tl_ctx *ctx,
+ struct demuxer *source,
+ uint64_t start)
+{
+ /* Merge directly adjacent parts. We allow for a configurable fudge factor
+ * because of files which specify chapter end times that are one frame too
+ * early; we don't want to try seeking over a one frame gap. */
+ int64_t join_diff = start - ctx->last_end_time;
+ if (ctx->num_parts == 0
+ || FFABS(join_diff) > ctx->opts->chapter_merge_threshold * 1e6
+ || source != ctx->timeline[ctx->num_parts - 1].source)
+ {
+ struct timeline_part new = {
+ .start = ctx->start_time / 1e9,
+ .source_start = start / 1e9,
+ .source = source,
+ };
+ MP_TARRAY_APPEND(NULL, ctx->timeline, ctx->num_parts, new);
+ } else if (ctx->num_parts > 0 && join_diff) {
+ // Chapter was merged at an inexact boundary; adjust timestamps to match.
+ MP_VERBOSE(ctx, "Merging timeline part %d with offset %g ms.\n",
+ ctx->num_parts, join_diff / 1e6);
+ ctx->start_time += join_diff;
+ return join_diff;
+ }
+
+ return 0;
+}
+
+static void build_timeline_loop(struct tl_ctx *ctx,
+ struct demux_chapter *chapters,
+ struct inner_timeline_info *info,
+ int current_source)
+{
+ uint64_t local_starttime = 0;
+ struct demuxer *source = ctx->sources[current_source];
+ struct matroska_data *m = &source->matroska_data;
+
+ for (int i = 0; i < m->num_ordered_chapters; i++) {
+ struct matroska_chapter *c = m->ordered_chapters + i;
+ uint64_t chapter_length = c->end - c->start;
+
+ if (!c->has_segment_uid)
+ c->uid = m->uid;
+
+ local_starttime += chapter_length;
+
+ // If we're before the start time for the chapter, skip to the next one.
+ if (local_starttime <= info->skip)
+ continue;
+
+ /* Look for the source for this chapter. */
+ for (int j = 0; j < ctx->num_sources; j++) {
+ struct demuxer *linked_source = ctx->sources[j];
+ struct matroska_data *linked_m = &linked_source->matroska_data;
+
+ if (!demux_matroska_uid_cmp(&c->uid, &linked_m->uid))
+ continue;
+
+ if (!info->limit) {
+ if (i >= ctx->num_chapters)
+ break; // malformed files can cause this to happen.
+
+ chapters[i].pts = ctx->start_time / 1e9;
+ chapters[i].metadata = talloc_zero(chapters, struct mp_tags);
+ mp_tags_set_str(chapters[i].metadata, "title", c->name);
+ }
+
+ /* If we're the source or it's a non-ordered edition reference,
+ * just add a timeline part from the source. */
+ if (current_source == j || !linked_m->uid.edition) {
+ uint64_t source_full_length = linked_source->duration * 1e9;
+ uint64_t source_length = source_full_length - c->start;
+ int64_t join_diff = 0;
+
+ /* If the chapter starts after the end of a source, there's
+ * nothing we can get from it. Instead, mark the entire chapter
+ * as missing and make the chapter length 0. */
+ if (source_full_length <= c->start) {
+ ctx->missing_time += chapter_length;
+ chapter_length = 0;
+ goto found;
+ }
+
+ /* If the source length starting at the chapter start is
+ * shorter than the chapter it is supposed to fill, add the gap
+ * to missing_time. Also, modify the chapter length to be what
+ * we actually have to avoid playing off the end of the file
+ * and not switching to the next source. */
+ if (source_length < chapter_length) {
+ ctx->missing_time += chapter_length - source_length;
+ chapter_length = source_length;
+ }
+
+ join_diff = add_timeline_part(ctx, linked_source, c->start);
+
+ /* If we merged two chapters into a single part due to them
+ * being off by a few frames, we need to change the limit to
+ * avoid chopping the end of the intended chapter (the adding
+ * frames case) or showing extra content (the removing frames
+ * case). Also update chapter_length to incorporate the extra
+ * time. */
+ if (info->limit) {
+ info->limit += join_diff;
+ chapter_length += join_diff;
+ }
+ } else {
+ /* We have an ordered edition as the source. Since this
+ * can jump around all over the place, we need to build up the
+ * timeline parts for each of its chapters, but not add them as
+ * chapters. */
+ struct inner_timeline_info new_info = {
+ .skip = c->start,
+ .limit = c->end
+ };
+ build_timeline_loop(ctx, chapters, &new_info, j);
+ // Already handled by the loop call.
+ chapter_length = 0;
+ }
+ ctx->last_end_time = c->end;
+ goto found;
+ }
+
+ ctx->missing_time += chapter_length;
+ chapter_length = 0;
+ found:;
+ ctx->start_time += chapter_length;
+ /* If we're after the limit on this chapter, stop here. */
+ if (info->limit && local_starttime >= info->limit) {
+ /* Back up the global start time by the overflow. */
+ ctx->start_time -= local_starttime - info->limit;
+ break;
+ }
+ }
+
+ /* If we stopped before the limit, add up the missing time. */
+ if (local_starttime < info->limit)
+ ctx->missing_time += info->limit - local_starttime;
+}
+
+static void check_track_compatibility(struct tl_ctx *tl, struct demuxer *mainsrc)
+{
+ for (int n = 0; n < tl->num_parts; n++) {
+ struct timeline_part *p = &tl->timeline[n];
+ if (p->source == mainsrc)
+ continue;
+
+ int num_source_streams = demux_get_num_stream(p->source);
+ for (int i = 0; i < num_source_streams; i++) {
+ struct sh_stream *s = demux_get_stream(p->source, i);
+ if (s->attached_picture)
+ continue;
+
+ if (!demuxer_stream_by_demuxer_id(mainsrc, s->type, s->demuxer_id)) {
+ MP_WARN(tl, "Source %s has %s stream with TID=%d, which "
+ "is not present in the ordered chapters main "
+ "file. This is a broken file. "
+ "The additional stream is ignored.\n",
+ p->source->filename, stream_type_name(s->type),
+ s->demuxer_id);
+ }
+ }
+
+ int num_main_streams = demux_get_num_stream(mainsrc);
+ for (int i = 0; i < num_main_streams; i++) {
+ struct sh_stream *m = demux_get_stream(mainsrc, i);
+ if (m->attached_picture)
+ continue;
+
+ struct sh_stream *s =
+ demuxer_stream_by_demuxer_id(p->source, m->type, m->demuxer_id);
+ if (s) {
+ // There are actually many more things that in theory have to
+ // match (though mpv's implementation doesn't care).
+ if (strcmp(s->codec->codec, m->codec->codec) != 0)
+ MP_WARN(tl, "Timeline segments have mismatching codec.\n");
+ if (s->codec->extradata_size != m->codec->extradata_size ||
+ (s->codec->extradata_size &&
+ memcmp(s->codec->extradata, m->codec->extradata,
+ s->codec->extradata_size) != 0))
+ MP_WARN(tl, "Timeline segments have mismatching codec info.\n");
+ } else {
+ MP_WARN(tl, "Source %s lacks %s stream with TID=%d, which "
+ "is present in the ordered chapters main "
+ "file. This is a broken file.\n",
+ p->source->filename, stream_type_name(m->type),
+ m->demuxer_id);
+ }
+ }
+ }
+}
+
+void build_ordered_chapter_timeline(struct timeline *tl)
+{
+ struct demuxer *demuxer = tl->demuxer;
+
+ if (!demuxer->matroska_data.ordered_chapters)
+ return;
+
+ struct tl_ctx *ctx = talloc_ptrtype(tl, ctx);
+ *ctx = (struct tl_ctx){
+ .log = tl->log,
+ .global = tl->global,
+ .tl = tl,
+ .demuxer = demuxer,
+ .opts = mp_get_config_group(ctx, tl->global, &mp_opt_root),
+ };
+
+ if (!ctx->opts->ordered_chapters || !demuxer->access_references) {
+ MP_INFO(demuxer, "File uses ordered chapters, but "
+ "you have disabled support for them. Ignoring.\n");
+ talloc_free(ctx);
+ return;
+ }
+
+ MP_INFO(ctx, "File uses ordered chapters, will build edit timeline.\n");
+
+ struct matroska_data *m = &demuxer->matroska_data;
+
+ // +1 because sources/uid_map[0] is original file even if all chapters
+ // actually use other sources and need separate entries
+ ctx->sources = talloc_zero_array(tl, struct demuxer *,
+ m->num_ordered_chapters + 1);
+ ctx->sources[0] = demuxer;
+ ctx->num_sources = 1;
+
+ ctx->uids = talloc_zero_array(NULL, struct matroska_segment_uid,
+ m->num_ordered_chapters + 1);
+ ctx->uids[0] = m->uid;
+ ctx->uids[0].edition = 0;
+
+ for (int i = 0; i < m->num_ordered_chapters; i++) {
+ struct matroska_chapter *c = m->ordered_chapters + i;
+ /* If there isn't a segment uid, we are the source. If the segment uid
+ * is our segment uid and the edition matches. We can't accept the
+ * "don't care" edition value of 0 since the user may have requested a
+ * non-default edition. */
+ if (!c->has_segment_uid || demux_matroska_uid_cmp(&c->uid, &m->uid))
+ continue;
+
+ if (has_source_request(ctx, &c->uid))
+ continue;
+
+ ctx->uids[ctx->num_sources] = c->uid;
+ ctx->sources[ctx->num_sources] = NULL;
+ ctx->num_sources++;
+ }
+
+ find_ordered_chapter_sources(ctx);
+
+ talloc_free(ctx->uids);
+ ctx->uids = NULL;
+
+ struct demux_chapter *chapters =
+ talloc_zero_array(tl, struct demux_chapter, m->num_ordered_chapters);
+
+ ctx->timeline = talloc_array_ptrtype(tl, ctx->timeline, 0);
+ ctx->num_chapters = m->num_ordered_chapters;
+
+ struct inner_timeline_info info = {
+ .skip = 0,
+ .limit = 0
+ };
+ build_timeline_loop(ctx, chapters, &info, 0);
+
+ // Fuck everything: filter out all "unset" chapters.
+ for (int n = m->num_ordered_chapters - 1; n >= 0; n--) {
+ if (!chapters[n].metadata)
+ MP_TARRAY_REMOVE_AT(chapters, m->num_ordered_chapters, n);
+ }
+
+ if (!ctx->num_parts) {
+ // None of the parts come from the file itself???
+ // Broken file, but we need at least 1 valid timeline part - add a dummy.
+ MP_WARN(ctx, "Ordered chapters file with no parts?\n");
+ struct timeline_part new = {
+ .source = demuxer,
+ };
+ MP_TARRAY_APPEND(NULL, ctx->timeline, ctx->num_parts, new);
+ }
+
+ for (int n = 0; n < ctx->num_parts; n++) {
+ ctx->timeline[n].end = n == ctx->num_parts - 1
+ ? ctx->start_time / 1e9
+ : ctx->timeline[n + 1].start;
+ };
+
+ /* Ignore anything less than a millisecond when reporting missing time. If
+ * users really notice less than a millisecond missing, maybe this can be
+ * revisited. */
+ if (ctx->missing_time >= 1e6) {
+ MP_ERR(ctx, "There are %.3f seconds missing from the timeline!\n",
+ ctx->missing_time / 1e9);
+ }
+
+ // With Matroska, the "master" file usually dictates track layout etc.,
+ // except maybe with playlist-like files.
+ struct demuxer *track_layout = ctx->timeline[0].source;
+ for (int n = 0; n < ctx->num_parts; n++) {
+ if (ctx->timeline[n].source == ctx->demuxer) {
+ track_layout = ctx->demuxer;
+ break;
+ }
+ }
+
+ check_track_compatibility(ctx, track_layout);
+
+ tl->sources = ctx->sources;
+ tl->num_sources = ctx->num_sources;
+
+ struct timeline_par *par = talloc_ptrtype(tl, par);
+ *par = (struct timeline_par){
+ .parts = ctx->timeline,
+ .num_parts = ctx->num_parts,
+ .track_layout = track_layout,
+ };
+ MP_TARRAY_APPEND(tl, tl->pars, tl->num_pars, par);
+ tl->chapters = chapters;
+ tl->num_chapters = m->num_ordered_chapters;
+ tl->meta = track_layout;
+ tl->format = "mkv_oc";
+}
diff --git a/demux/demux_null.c b/demux/demux_null.c
new file mode 100644
index 0000000..0ce3ac4
--- /dev/null
+++ b/demux/demux_null.c
@@ -0,0 +1,35 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc/bstr.h"
+#include "stream/stream.h"
+#include "demux.h"
+
+static int try_open_file(struct demuxer *demux, enum demux_check check)
+{
+ if (!bstr_startswith0(bstr0(demux->filename), "null://") &&
+ check != DEMUX_CHECK_REQUEST)
+ return -1;
+ demux->seekable = true;
+ return 0;
+}
+
+const struct demuxer_desc demuxer_desc_null = {
+ .name = "null",
+ .desc = "null demuxer",
+ .open = try_open_file,
+};
diff --git a/demux/demux_playlist.c b/demux/demux_playlist.c
new file mode 100644
index 0000000..63355be
--- /dev/null
+++ b/demux/demux_playlist.c
@@ -0,0 +1,584 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <dirent.h>
+
+#include <libavutil/common.h>
+
+#include "common/common.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "common/msg.h"
+#include "common/playlist.h"
+#include "misc/charset_conv.h"
+#include "misc/thread_tools.h"
+#include "options/path.h"
+#include "stream/stream.h"
+#include "osdep/io.h"
+#include "misc/natural_sort.h"
+#include "demux.h"
+
+#define PROBE_SIZE (8 * 1024)
+
+enum dir_mode {
+ DIR_AUTO,
+ DIR_LAZY,
+ DIR_RECURSIVE,
+ DIR_IGNORE,
+};
+
+#define OPT_BASE_STRUCT struct demux_playlist_opts
+struct demux_playlist_opts {
+ int dir_mode;
+};
+
+struct m_sub_options demux_playlist_conf = {
+ .opts = (const struct m_option[]) {
+ {"directory-mode", OPT_CHOICE(dir_mode,
+ {"auto", DIR_AUTO},
+ {"lazy", DIR_LAZY},
+ {"recursive", DIR_RECURSIVE},
+ {"ignore", DIR_IGNORE})},
+ {0}
+ },
+ .size = sizeof(struct demux_playlist_opts),
+ .defaults = &(const struct demux_playlist_opts){
+ .dir_mode = DIR_AUTO,
+ },
+};
+
+static bool check_mimetype(struct stream *s, const char *const *list)
+{
+ if (s->mime_type) {
+ for (int n = 0; list && list[n]; n++) {
+ if (strcasecmp(s->mime_type, list[n]) == 0)
+ return true;
+ }
+ }
+ return false;
+}
+
+struct pl_parser {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct stream *s;
+ char buffer[2 * 1024 * 1024];
+ int utf16;
+ struct playlist *pl;
+ bool error;
+ bool probing;
+ bool force;
+ bool add_base;
+ bool line_allocated;
+ enum demux_check check_level;
+ struct stream *real_stream;
+ char *format;
+ char *codepage;
+ struct demux_playlist_opts *opts;
+};
+
+
+static uint16_t stream_read_word_endian(stream_t *s, bool big_endian)
+{
+ unsigned int y = stream_read_char(s);
+ y = (y << 8) | stream_read_char(s);
+ if (!big_endian)
+ y = ((y >> 8) & 0xFF) | (y << 8);
+ return y;
+}
+
+// Read characters until the next '\n' (including), or until the buffer in s is
+// exhausted.
+static int read_characters(stream_t *s, uint8_t *dst, int dstsize, int utf16)
+{
+ if (utf16 == 1 || utf16 == 2) {
+ uint8_t *cur = dst;
+ while (1) {
+ if ((cur - dst) + 8 >= dstsize) // PUT_UTF8 writes max. 8 bytes
+ return -1; // line too long
+ uint32_t c;
+ uint8_t tmp;
+ GET_UTF16(c, stream_read_word_endian(s, utf16 == 2), return -1;)
+ if (s->eof)
+ break; // legitimate EOF; ignore the case of partial reads
+ PUT_UTF8(c, tmp, *cur++ = tmp;)
+ if (c == '\n')
+ break;
+ }
+ return cur - dst;
+ } else {
+ uint8_t buf[1024];
+ int buf_len = stream_read_peek(s, buf, sizeof(buf));
+ uint8_t *end = memchr(buf, '\n', buf_len);
+ int len = end ? end - buf + 1 : buf_len;
+ if (len > dstsize)
+ return -1; // line too long
+ memcpy(dst, buf, len);
+ stream_seek_skip(s, stream_tell(s) + len);
+ return len;
+ }
+}
+
+// On error, or if the line is larger than max-1, return NULL and unset s->eof.
+// On EOF, return NULL, and s->eof will be set.
+// Otherwise, return the line (including \n or \r\n at the end of the line).
+// If the return value is non-NULL, it's always the same as mem.
+// utf16: 0: UTF8 or 8 bit legacy, 1: UTF16-LE, 2: UTF16-BE
+static char *read_line(stream_t *s, char *mem, int max, int utf16)
+{
+ if (max < 1)
+ return NULL;
+ int read = 0;
+ while (1) {
+ // Reserve 1 byte of ptr for terminating \0.
+ int l = read_characters(s, &mem[read], max - read - 1, utf16);
+ if (l < 0 || memchr(&mem[read], '\0', l)) {
+ MP_WARN(s, "error reading line\n");
+ return NULL;
+ }
+ read += l;
+ if (l == 0 || (read > 0 && mem[read - 1] == '\n'))
+ break;
+ }
+ mem[read] = '\0';
+ if (!stream_read_peek(s, &(char){0}, 1) && read == 0) // legitimate EOF
+ return NULL;
+ return mem;
+}
+
+static char *pl_get_line0(struct pl_parser *p)
+{
+ char *res = read_line(p->s, p->buffer, sizeof(p->buffer), p->utf16);
+ if (res) {
+ int len = strlen(res);
+ if (len > 0 && res[len - 1] == '\n')
+ res[len - 1] = '\0';
+ } else {
+ p->error |= !p->s->eof;
+ }
+ return res;
+}
+
+static bstr pl_get_line(struct pl_parser *p)
+{
+ bstr line = bstr_strip(bstr0(pl_get_line0(p)));
+ const char *charset = mp_charset_guess(p, p->log, line, p->codepage, 0);
+ if (charset && !mp_charset_is_utf8(charset)) {
+ bstr utf8 = mp_iconv_to_utf8(p->log, line, charset, 0);
+ if (utf8.start && utf8.start != line.start) {
+ line = utf8;
+ p->line_allocated = true;
+ }
+ }
+ return line;
+}
+
+// Helper in case mp_iconv_to_utf8 allocates memory
+static void pl_free_line(struct pl_parser *p, bstr line)
+{
+ if (p->line_allocated) {
+ talloc_free(line.start);
+ p->line_allocated = false;
+ }
+}
+
+static void pl_add(struct pl_parser *p, bstr entry)
+{
+ char *s = bstrto0(NULL, entry);
+ playlist_add_file(p->pl, s);
+ talloc_free(s);
+}
+
+static bool pl_eof(struct pl_parser *p)
+{
+ return p->error || p->s->eof;
+}
+
+static bool maybe_text(bstr d)
+{
+ for (int n = 0; n < d.len; n++) {
+ unsigned char c = d.start[n];
+ if (c < 32 && c != '\n' && c != '\r' && c != '\t')
+ return false;
+ }
+ return true;
+}
+
+static int parse_m3u(struct pl_parser *p)
+{
+ bstr line = pl_get_line(p);
+ if (p->probing && !bstr_equals0(line, "#EXTM3U")) {
+ // Last resort: if the file extension is m3u, it might be headerless.
+ if (p->check_level == DEMUX_CHECK_UNSAFE) {
+ char *ext = mp_splitext(p->real_stream->url, NULL);
+ char probe[PROBE_SIZE];
+ int len = stream_read_peek(p->real_stream, probe, sizeof(probe));
+ bstr data = {probe, len};
+ if (ext && data.len >= 2 && maybe_text(data)) {
+ const char *exts[] = {"m3u", "m3u8", NULL};
+ for (int n = 0; exts[n]; n++) {
+ if (strcasecmp(ext, exts[n]) == 0)
+ goto ok;
+ }
+ }
+ }
+ pl_free_line(p, line);
+ return -1;
+ }
+
+ok:
+ if (p->probing) {
+ pl_free_line(p, line);
+ return 0;
+ }
+
+ char *title = NULL;
+ while (line.len || !pl_eof(p)) {
+ bstr line_dup = line;
+ if (bstr_eatstart0(&line_dup, "#EXTINF:")) {
+ bstr duration, btitle;
+ if (bstr_split_tok(line_dup, ",", &duration, &btitle) && btitle.len) {
+ talloc_free(title);
+ title = bstrto0(NULL, btitle);
+ }
+ } else if (bstr_startswith0(line_dup, "#EXT-X-")) {
+ p->format = "hls";
+ } else if (line_dup.len > 0 && !bstr_startswith0(line_dup, "#")) {
+ char *fn = bstrto0(NULL, line_dup);
+ struct playlist_entry *e = playlist_entry_new(fn);
+ talloc_free(fn);
+ e->title = talloc_steal(e, title);
+ title = NULL;
+ playlist_add(p->pl, e);
+ }
+ pl_free_line(p, line);
+ line = pl_get_line(p);
+ }
+ pl_free_line(p, line);
+ talloc_free(title);
+ return 0;
+}
+
+static int parse_ref_init(struct pl_parser *p)
+{
+ bstr line = pl_get_line(p);
+ if (!bstr_equals0(line, "[Reference]")) {
+ pl_free_line(p, line);
+ return -1;
+ }
+ pl_free_line(p, line);
+
+ // ASF http streaming redirection - this is needed because ffmpeg http://
+ // and mmsh:// can not automatically switch automatically between each
+ // others. Both protocols use http - MMSH requires special http headers
+ // to "activate" it, and will in other cases return this playlist.
+ static const char *const mmsh_types[] = {"audio/x-ms-wax",
+ "audio/x-ms-wma", "video/x-ms-asf", "video/x-ms-afs", "video/x-ms-wmv",
+ "video/x-ms-wma", "application/x-mms-framed",
+ "application/vnd.ms.wms-hdr.asfv1", NULL};
+ bstr burl = bstr0(p->s->url);
+ if (bstr_eatstart0(&burl, "http://") && check_mimetype(p->s, mmsh_types)) {
+ MP_INFO(p, "Redirecting to mmsh://\n");
+ playlist_add_file(p->pl, talloc_asprintf(p, "mmsh://%.*s", BSTR_P(burl)));
+ return 0;
+ }
+
+ while (!pl_eof(p)) {
+ line = pl_get_line(p);
+ bstr value;
+ if (bstr_case_startswith(line, bstr0("Ref"))) {
+ bstr_split_tok(line, "=", &(bstr){0}, &value);
+ if (value.len)
+ pl_add(p, value);
+ }
+ pl_free_line(p, line);
+ }
+ return 0;
+}
+
+static int parse_ini_thing(struct pl_parser *p, const char *header,
+ const char *entry)
+{
+ bstr line = {0};
+ while (!line.len && !pl_eof(p))
+ line = pl_get_line(p);
+ if (bstrcasecmp0(line, header) != 0) {
+ pl_free_line(p, line);
+ return -1;
+ }
+ if (p->probing) {
+ pl_free_line(p, line);
+ return 0;
+ }
+ pl_free_line(p, line);
+ while (!pl_eof(p)) {
+ line = pl_get_line(p);
+ bstr key, value;
+ if (bstr_split_tok(line, "=", &key, &value) &&
+ bstr_case_startswith(key, bstr0(entry)))
+ {
+ value = bstr_strip(value);
+ if (bstr_startswith0(value, "\"") && bstr_endswith0(value, "\""))
+ value = bstr_splice(value, 1, -1);
+ pl_add(p, value);
+ }
+ pl_free_line(p, line);
+ }
+ return 0;
+}
+
+static int parse_pls(struct pl_parser *p)
+{
+ return parse_ini_thing(p, "[playlist]", "File");
+}
+
+static int parse_url(struct pl_parser *p)
+{
+ return parse_ini_thing(p, "[InternetShortcut]", "URL");
+}
+
+static int parse_txt(struct pl_parser *p)
+{
+ if (!p->force)
+ return -1;
+ if (p->probing)
+ return 0;
+ MP_WARN(p, "Reading plaintext playlist.\n");
+ while (!pl_eof(p)) {
+ bstr line = pl_get_line(p);
+ if (line.len == 0)
+ continue;
+ pl_add(p, line);
+ pl_free_line(p, line);
+ }
+ return 0;
+}
+
+#define MAX_DIR_STACK 20
+
+static bool same_st(struct stat *st1, struct stat *st2)
+{
+ return st1->st_dev == st2->st_dev && st1->st_ino == st2->st_ino;
+}
+
+struct pl_dir_entry {
+ char *path;
+ char *name;
+ struct stat st;
+ bool is_dir;
+};
+
+static int cmp_dir_entry(const void *a, const void *b)
+{
+ struct pl_dir_entry *a_entry = (struct pl_dir_entry*) a;
+ struct pl_dir_entry *b_entry = (struct pl_dir_entry*) b;
+ if (a_entry->is_dir == b_entry->is_dir) {
+ return mp_natural_sort_cmp(a_entry->name, b_entry->name);
+ } else {
+ return a_entry->is_dir ? 1 : -1;
+ }
+}
+
+// Return true if this was a readable directory.
+static bool scan_dir(struct pl_parser *p, char *path,
+ struct stat *dir_stack, int num_dir_stack)
+{
+ if (strlen(path) >= 8192 || num_dir_stack == MAX_DIR_STACK)
+ return false; // things like mount bind loops
+
+ DIR *dp = opendir(path);
+ if (!dp) {
+ MP_ERR(p, "Could not read directory.\n");
+ return false;
+ }
+
+ struct pl_dir_entry *dir_entries = NULL;
+ int num_dir_entries = 0;
+ int path_len = strlen(path);
+ int dir_mode = p->opts->dir_mode;
+
+ struct dirent *ep;
+ while ((ep = readdir(dp))) {
+ if (ep->d_name[0] == '.')
+ continue;
+
+ if (mp_cancel_test(p->s->cancel))
+ break;
+
+ char *file = mp_path_join(p, path, ep->d_name);
+
+ struct stat st;
+ if (stat(file, &st) == 0 && S_ISDIR(st.st_mode)) {
+ if (dir_mode != DIR_IGNORE) {
+ for (int n = 0; n < num_dir_stack; n++) {
+ if (same_st(&dir_stack[n], &st)) {
+ MP_VERBOSE(p, "Skip recursive entry: %s\n", file);
+ goto skip;
+ }
+ }
+
+ struct pl_dir_entry d = {file, &file[path_len], st, true};
+ MP_TARRAY_APPEND(p, dir_entries, num_dir_entries, d);
+ }
+ } else {
+ struct pl_dir_entry f = {file, &file[path_len], .is_dir = false};
+ MP_TARRAY_APPEND(p, dir_entries, num_dir_entries, f);
+ }
+
+ skip: ;
+ }
+ closedir(dp);
+
+ if (dir_entries)
+ qsort(dir_entries, num_dir_entries, sizeof(dir_entries[0]), cmp_dir_entry);
+
+ for (int n = 0; n < num_dir_entries; n++) {
+ if (dir_mode == DIR_RECURSIVE && dir_entries[n].is_dir) {
+ dir_stack[num_dir_stack] = dir_entries[n].st;
+ char *file = dir_entries[n].path;
+ scan_dir(p, file, dir_stack, num_dir_stack + 1);
+ }
+ else {
+ playlist_add_file(p->pl, dir_entries[n].path);
+ }
+ }
+
+ return true;
+}
+
+static int parse_dir(struct pl_parser *p)
+{
+ if (!p->real_stream->is_directory)
+ return -1;
+ if (p->probing)
+ return 0;
+
+ char *path = mp_file_get_path(p, bstr0(p->real_stream->url));
+ if (!path)
+ return -1;
+
+ struct stat dir_stack[MAX_DIR_STACK];
+
+ if (p->opts->dir_mode == DIR_AUTO) {
+ struct MPOpts *opts = mp_get_config_group(NULL, p->global, &mp_opt_root);
+ p->opts->dir_mode = opts->shuffle ? DIR_RECURSIVE : DIR_LAZY;
+ talloc_free(opts);
+ }
+
+ scan_dir(p, path, dir_stack, 0);
+
+ p->add_base = false;
+
+ return p->pl->num_entries > 0 ? 0 : -1;
+}
+
+#define MIME_TYPES(...) \
+ .mime_types = (const char*const[]){__VA_ARGS__, NULL}
+
+struct pl_format {
+ const char *name;
+ int (*parse)(struct pl_parser *p);
+ const char *const *mime_types;
+};
+
+static const struct pl_format formats[] = {
+ {"directory", parse_dir},
+ {"m3u", parse_m3u,
+ MIME_TYPES("audio/mpegurl", "audio/x-mpegurl", "application/x-mpegurl")},
+ {"ini", parse_ref_init},
+ {"pls", parse_pls,
+ MIME_TYPES("audio/x-scpls")},
+ {"url", parse_url},
+ {"txt", parse_txt},
+};
+
+static const struct pl_format *probe_pl(struct pl_parser *p)
+{
+ int64_t start = stream_tell(p->s);
+ for (int n = 0; n < MP_ARRAY_SIZE(formats); n++) {
+ const struct pl_format *fmt = &formats[n];
+ stream_seek(p->s, start);
+ if (check_mimetype(p->s, fmt->mime_types)) {
+ MP_VERBOSE(p, "forcing format by mime-type.\n");
+ p->force = true;
+ return fmt;
+ }
+ if (fmt->parse(p) >= 0)
+ return fmt;
+ }
+ return NULL;
+}
+
+static int open_file(struct demuxer *demuxer, enum demux_check check)
+{
+ if (!demuxer->access_references)
+ return -1;
+
+ bool force = check < DEMUX_CHECK_UNSAFE || check == DEMUX_CHECK_REQUEST;
+
+ struct pl_parser *p = talloc_zero(NULL, struct pl_parser);
+ p->global = demuxer->global;
+ p->log = demuxer->log;
+ p->pl = talloc_zero(p, struct playlist);
+ p->real_stream = demuxer->stream;
+ p->add_base = true;
+
+ struct demux_opts *opts = mp_get_config_group(p, p->global, &demux_conf);
+ p->codepage = opts->meta_cp;
+
+ char probe[PROBE_SIZE];
+ int probe_len = stream_read_peek(p->real_stream, probe, sizeof(probe));
+ p->s = stream_memory_open(demuxer->global, probe, probe_len);
+ p->s->mime_type = demuxer->stream->mime_type;
+ p->utf16 = stream_skip_bom(p->s);
+ p->force = force;
+ p->check_level = check;
+ p->probing = true;
+ const struct pl_format *fmt = probe_pl(p);
+ free_stream(p->s);
+ playlist_clear(p->pl);
+ if (!fmt) {
+ talloc_free(p);
+ return -1;
+ }
+
+ p->probing = false;
+ p->error = false;
+ p->s = demuxer->stream;
+ p->utf16 = stream_skip_bom(p->s);
+ p->opts = mp_get_config_group(demuxer, demuxer->global, &demux_playlist_conf);
+ bool ok = fmt->parse(p) >= 0 && !p->error;
+ if (p->add_base)
+ playlist_add_base_path(p->pl, mp_dirname(demuxer->filename));
+ playlist_set_stream_flags(p->pl, demuxer->stream_origin);
+ demuxer->playlist = talloc_steal(demuxer, p->pl);
+ demuxer->filetype = p->format ? p->format : fmt->name;
+ demuxer->fully_read = true;
+ talloc_free(p);
+ if (ok)
+ demux_close_stream(demuxer);
+ return ok ? 0 : -1;
+}
+
+const demuxer_desc_t demuxer_desc_playlist = {
+ .name = "playlist",
+ .desc = "Playlist file",
+ .open = open_file,
+};
diff --git a/demux/demux_raw.c b/demux/demux_raw.c
new file mode 100644
index 0000000..86b0368
--- /dev/null
+++ b/demux/demux_raw.c
@@ -0,0 +1,326 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/common.h>
+
+#include "common/av_common.h"
+
+#include "options/m_config.h"
+#include "options/m_option.h"
+
+#include "stream/stream.h"
+#include "demux.h"
+#include "stheader.h"
+#include "codec_tags.h"
+
+#include "video/fmt-conversion.h"
+#include "video/img_format.h"
+
+#include "osdep/endian.h"
+
+struct demux_rawaudio_opts {
+ struct m_channels channels;
+ int samplerate;
+ int aformat;
+};
+
+// Ad-hoc schema to systematically encode the format as int
+#define PCM(sign, is_float, bits, is_be) \
+ ((sign) | ((is_float) << 1) | ((is_be) << 2) | ((bits) << 3))
+#define NE (BYTE_ORDER == BIG_ENDIAN)
+
+#define OPT_BASE_STRUCT struct demux_rawaudio_opts
+const struct m_sub_options demux_rawaudio_conf = {
+ .opts = (const m_option_t[]) {
+ {"channels", OPT_CHANNELS(channels), .flags = M_OPT_CHANNELS_LIMITED},
+ {"rate", OPT_INT(samplerate), M_RANGE(1000, 8 * 48000)},
+ {"format", OPT_CHOICE(aformat,
+ {"u8", PCM(0, 0, 8, 0)},
+ {"s8", PCM(1, 0, 8, 0)},
+ {"u16le", PCM(0, 0, 16, 0)}, {"u16be", PCM(0, 0, 16, 1)},
+ {"s16le", PCM(1, 0, 16, 0)}, {"s16be", PCM(1, 0, 16, 1)},
+ {"u24le", PCM(0, 0, 24, 0)}, {"u24be", PCM(0, 0, 24, 1)},
+ {"s24le", PCM(1, 0, 24, 0)}, {"s24be", PCM(1, 0, 24, 1)},
+ {"u32le", PCM(0, 0, 32, 0)}, {"u32be", PCM(0, 0, 32, 1)},
+ {"s32le", PCM(1, 0, 32, 0)}, {"s32be", PCM(1, 0, 32, 1)},
+ {"floatle", PCM(0, 1, 32, 0)}, {"floatbe", PCM(0, 1, 32, 1)},
+ {"doublele",PCM(0, 1, 64, 0)}, {"doublebe", PCM(0, 1, 64, 1)},
+ {"u16", PCM(0, 0, 16, NE)},
+ {"s16", PCM(1, 0, 16, NE)},
+ {"u24", PCM(0, 0, 24, NE)},
+ {"s24", PCM(1, 0, 24, NE)},
+ {"u32", PCM(0, 0, 32, NE)},
+ {"s32", PCM(1, 0, 32, NE)},
+ {"float", PCM(0, 1, 32, NE)},
+ {"double", PCM(0, 1, 64, NE)})},
+ {0}
+ },
+ .size = sizeof(struct demux_rawaudio_opts),
+ .defaults = &(const struct demux_rawaudio_opts){
+ // Note that currently, stream_cdda expects exactly these parameters!
+ .channels = {
+ .set = 1,
+ .chmaps = (struct mp_chmap[]){ MP_CHMAP_INIT_STEREO, },
+ .num_chmaps = 1,
+ },
+ .samplerate = 44100,
+ .aformat = PCM(1, 0, 16, 0), // s16le
+ },
+};
+
+#undef PCM
+#undef NE
+
+struct demux_rawvideo_opts {
+ int vformat;
+ int mp_format;
+ char *codec;
+ int width;
+ int height;
+ float fps;
+ int imgsize;
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct demux_rawvideo_opts
+const struct m_sub_options demux_rawvideo_conf = {
+ .opts = (const m_option_t[]) {
+ {"w", OPT_INT(width), M_RANGE(1, 8192)},
+ {"h", OPT_INT(height), M_RANGE(1, 8192)},
+ {"format", OPT_FOURCC(vformat)},
+ {"mp-format", OPT_IMAGEFORMAT(mp_format)},
+ {"codec", OPT_STRING(codec)},
+ {"fps", OPT_FLOAT(fps), M_RANGE(0.001, 1000)},
+ {"size", OPT_INT(imgsize), M_RANGE(1, 8192 * 8192 * 4)},
+ {0}
+ },
+ .size = sizeof(struct demux_rawvideo_opts),
+ .defaults = &(const struct demux_rawvideo_opts){
+ .vformat = MKTAG('I', '4', '2', '0'),
+ .width = 1280,
+ .height = 720,
+ .fps = 25,
+ },
+};
+
+struct priv {
+ struct sh_stream *sh;
+ int frame_size;
+ int read_frames;
+ double frame_rate;
+};
+
+static int generic_open(struct demuxer *demuxer)
+{
+ struct stream *s = demuxer->stream;
+ struct priv *p = demuxer->priv;
+
+ int64_t end = stream_get_size(s);
+ if (end >= 0)
+ demuxer->duration = (end / p->frame_size) / p->frame_rate;
+
+ return 0;
+}
+
+static int demux_rawaudio_open(demuxer_t *demuxer, enum demux_check check)
+{
+ struct demux_rawaudio_opts *opts =
+ mp_get_config_group(demuxer, demuxer->global, &demux_rawaudio_conf);
+
+ if (check != DEMUX_CHECK_REQUEST && check != DEMUX_CHECK_FORCE)
+ return -1;
+
+ if (opts->channels.num_chmaps != 1) {
+ MP_ERR(demuxer, "Invalid channels option given.\n");
+ return -1;
+ }
+
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_AUDIO);
+ struct mp_codec_params *c = sh->codec;
+ c->channels = opts->channels.chmaps[0];
+ c->force_channels = true;
+ c->samplerate = opts->samplerate;
+
+ c->native_tb_num = 1;
+ c->native_tb_den = c->samplerate;
+
+ int f = opts->aformat;
+ // See PCM(): sign float bits endian
+ mp_set_pcm_codec(sh->codec, f & 1, f & 2, f >> 3, f & 4);
+ int samplesize = ((f >> 3) + 7) / 8;
+
+ demux_add_sh_stream(demuxer, sh);
+
+ struct priv *p = talloc_ptrtype(demuxer, p);
+ demuxer->priv = p;
+ *p = (struct priv) {
+ .sh = sh,
+ .frame_size = samplesize * c->channels.num,
+ .frame_rate = c->samplerate,
+ .read_frames = c->samplerate / 8,
+ };
+
+ return generic_open(demuxer);
+}
+
+static int demux_rawvideo_open(demuxer_t *demuxer, enum demux_check check)
+{
+ struct demux_rawvideo_opts *opts =
+ mp_get_config_group(demuxer, demuxer->global, &demux_rawvideo_conf);
+
+ if (check != DEMUX_CHECK_REQUEST && check != DEMUX_CHECK_FORCE)
+ return -1;
+
+ int width = opts->width;
+ int height = opts->height;
+
+ if (!width || !height) {
+ MP_ERR(demuxer, "rawvideo: width or height not specified!\n");
+ return -1;
+ }
+
+ const char *decoder = "rawvideo";
+ int imgfmt = opts->vformat;
+ int imgsize = opts->imgsize;
+ int mp_imgfmt = 0;
+ if (opts->mp_format && !IMGFMT_IS_HWACCEL(opts->mp_format)) {
+ mp_imgfmt = opts->mp_format;
+ if (!imgsize) {
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(opts->mp_format);
+ for (int p = 0; p < desc.num_planes; p++) {
+ imgsize += ((width >> desc.xs[p]) * (height >> desc.ys[p]) *
+ desc.bpp[p] + 7) / 8;
+ }
+ }
+ } else if (opts->codec && opts->codec[0])
+ decoder = talloc_strdup(demuxer, opts->codec);
+
+ if (!imgsize) {
+ int bpp = 0;
+ switch (imgfmt) {
+ case MKTAG('Y', 'V', '1', '2'):
+ case MKTAG('I', '4', '2', '0'):
+ case MKTAG('I', 'Y', 'U', 'V'):
+ bpp = 12;
+ break;
+ case MKTAG('U', 'Y', 'V', 'Y'):
+ case MKTAG('Y', 'U', 'Y', '2'):
+ bpp = 16;
+ break;
+ }
+ if (!bpp) {
+ MP_ERR(demuxer, "rawvideo: img size not specified and unknown format!\n");
+ return -1;
+ }
+ imgsize = width * height * bpp / 8;
+ }
+
+ struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO);
+ struct mp_codec_params *c = sh->codec;
+ c->codec = decoder;
+ c->codec_tag = imgfmt;
+ c->fps = opts->fps;
+ c->reliable_fps = true;
+ c->disp_w = width;
+ c->disp_h = height;
+ if (mp_imgfmt) {
+ c->lav_codecpar = avcodec_parameters_alloc();
+ MP_HANDLE_OOM(c->lav_codecpar);
+ c->lav_codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+ c->lav_codecpar->codec_id = mp_codec_to_av_codec_id(decoder);
+ c->lav_codecpar->format = imgfmt2pixfmt(mp_imgfmt);
+ c->lav_codecpar->width = width;
+ c->lav_codecpar->height = height;
+ }
+ demux_add_sh_stream(demuxer, sh);
+
+ struct priv *p = talloc_ptrtype(demuxer, p);
+ demuxer->priv = p;
+ *p = (struct priv) {
+ .sh = sh,
+ .frame_size = imgsize,
+ .frame_rate = c->fps,
+ .read_frames = 1,
+ };
+
+ return generic_open(demuxer);
+}
+
+static bool raw_read_packet(struct demuxer *demuxer, struct demux_packet **pkt)
+{
+ struct priv *p = demuxer->priv;
+
+ if (demuxer->stream->eof)
+ return false;
+
+ struct demux_packet *dp = new_demux_packet(p->frame_size * p->read_frames);
+ if (!dp) {
+ MP_ERR(demuxer, "Can't read packet.\n");
+ return true;
+ }
+
+ dp->keyframe = true;
+ dp->pos = stream_tell(demuxer->stream);
+ dp->pts = (dp->pos / p->frame_size) / p->frame_rate;
+
+ int len = stream_read(demuxer->stream, dp->buffer, dp->len);
+ demux_packet_shorten(dp, len);
+
+ dp->stream = p->sh->index;
+ *pkt = dp;
+
+ return true;
+}
+
+static void raw_seek(demuxer_t *demuxer, double seek_pts, int flags)
+{
+ struct priv *p = demuxer->priv;
+ stream_t *s = demuxer->stream;
+ int64_t end = stream_get_size(s);
+ int64_t frame_nr = seek_pts * p->frame_rate;
+ frame_nr = frame_nr - (frame_nr % p->read_frames);
+ int64_t pos = frame_nr * p->frame_size;
+ if (flags & SEEK_FACTOR)
+ pos = end * seek_pts;
+ if (pos < 0)
+ pos = 0;
+ if (end > 0 && pos > end)
+ pos = end;
+ stream_seek(s, (pos / p->frame_size) * p->frame_size);
+}
+
+const demuxer_desc_t demuxer_desc_rawaudio = {
+ .name = "rawaudio",
+ .desc = "Uncompressed audio",
+ .open = demux_rawaudio_open,
+ .read_packet = raw_read_packet,
+ .seek = raw_seek,
+};
+
+const demuxer_desc_t demuxer_desc_rawvideo = {
+ .name = "rawvideo",
+ .desc = "Uncompressed video",
+ .open = demux_rawvideo_open,
+ .read_packet = raw_read_packet,
+ .seek = raw_seek,
+};
diff --git a/demux/demux_timeline.c b/demux/demux_timeline.c
new file mode 100644
index 0000000..5572fb5
--- /dev/null
+++ b/demux/demux_timeline.c
@@ -0,0 +1,719 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <limits.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+
+#include "demux.h"
+#include "timeline.h"
+#include "stheader.h"
+#include "stream/stream.h"
+
+struct segment {
+ int index; // index into virtual_source.segments[] (and timeline.parts[])
+ double start, end;
+ double d_start;
+ char *url;
+ bool lazy;
+ struct demuxer *d;
+ // stream_map[sh_stream.index] = virtual_stream, where sh_stream is a stream
+ // from the source d, and virtual_stream is a streamexported by the
+ // timeline demuxer (virtual_stream.sh). It's used to map the streams of the
+ // source onto the set of streams of the virtual timeline.
+ // Uses NULL for streams that do not appear in the virtual timeline.
+ struct virtual_stream **stream_map;
+ int num_stream_map;
+};
+
+// Information for each stream on the virtual timeline. (Mirrors streams
+// exposed by demux_timeline.)
+struct virtual_stream {
+ struct sh_stream *sh; // stream exported by demux_timeline
+ bool selected; // ==demux_stream_is_selected(sh)
+ int eos_packets; // deal with b-frame delay
+ struct virtual_source *src; // group this stream is part of
+};
+
+// This represents a single timeline source. (See timeline.pars[]. For each
+// timeline_par struct there is a virtual_source.)
+struct virtual_source {
+ struct timeline_par *tl;
+
+ bool dash, no_clip, delay_open;
+
+ struct segment **segments;
+ int num_segments;
+ struct segment *current;
+
+ struct virtual_stream **streams;
+ int num_streams;
+
+ // Total number of packets received past end of segment. Used
+ // to be clever about determining when to switch segments.
+ int eos_packets;
+
+ bool eof_reached;
+ double dts; // highest read DTS (or PTS if no DTS available)
+ bool any_selected; // at least one stream is actually selected
+
+ struct demux_packet *next;
+};
+
+struct priv {
+ struct timeline *tl;
+ bool owns_tl;
+
+ double duration;
+
+ // As the demuxer user sees it.
+ struct virtual_stream **streams;
+ int num_streams;
+
+ struct virtual_source **sources;
+ int num_sources;
+};
+
+static void update_slave_stats(struct demuxer *demuxer, struct demuxer *slave)
+{
+ demux_report_unbuffered_read_bytes(demuxer, demux_get_bytes_read_hack(slave));
+}
+
+static bool target_stream_used(struct segment *seg, struct virtual_stream *vs)
+{
+ for (int n = 0; n < seg->num_stream_map; n++) {
+ if (seg->stream_map[n] == vs)
+ return true;
+ }
+ return false;
+}
+
+// Create mapping from segment streams to virtual timeline streams.
+static void associate_streams(struct demuxer *demuxer,
+ struct virtual_source *src,
+ struct segment *seg)
+{
+ if (!seg->d || seg->stream_map)
+ return;
+
+ int num_streams = demux_get_num_stream(seg->d);
+ for (int n = 0; n < num_streams; n++) {
+ struct sh_stream *sh = demux_get_stream(seg->d, n);
+ struct virtual_stream *other = NULL;
+
+ for (int i = 0; i < src->num_streams; i++) {
+ struct virtual_stream *vs = src->streams[i];
+
+ // The stream must always have the same media type. Also, a stream
+ // can't be assigned multiple times.
+ if (sh->type != vs->sh->type || target_stream_used(seg, vs))
+ continue;
+
+ // By default pick the first matching stream.
+ if (!other)
+ other = vs;
+
+ // Matching by demuxer ID is supposedly useful and preferable for
+ // ordered chapters.
+ if (sh->demuxer_id >= 0 && sh->demuxer_id == vs->sh->demuxer_id)
+ other = vs;
+ }
+
+ if (!other) {
+ MP_WARN(demuxer, "Source stream %d (%s) unused and hidden.\n",
+ n, stream_type_name(sh->type));
+ }
+
+ MP_TARRAY_APPEND(seg, seg->stream_map, seg->num_stream_map, other);
+ }
+}
+
+static void reselect_streams(struct demuxer *demuxer)
+{
+ struct priv *p = demuxer->priv;
+
+ for (int n = 0; n < p->num_streams; n++) {
+ struct virtual_stream *vs = p->streams[n];
+ vs->selected = demux_stream_is_selected(vs->sh);
+ }
+
+ for (int x = 0; x < p->num_sources; x++) {
+ struct virtual_source *src = p->sources[x];
+
+ for (int n = 0; n < src->num_segments; n++) {
+ struct segment *seg = src->segments[n];
+
+ if (!seg->d)
+ continue;
+
+ for (int i = 0; i < seg->num_stream_map; i++) {
+ bool selected =
+ seg->stream_map[i] && seg->stream_map[i]->selected;
+
+ // This stops demuxer readahead for inactive segments.
+ if (!src->current || seg->d != src->current->d)
+ selected = false;
+ struct sh_stream *sh = demux_get_stream(seg->d, i);
+ demuxer_select_track(seg->d, sh, MP_NOPTS_VALUE, selected);
+
+ update_slave_stats(demuxer, seg->d);
+ }
+ }
+
+ bool was_selected = src->any_selected;
+ src->any_selected = false;
+
+ for (int n = 0; n < src->num_streams; n++)
+ src->any_selected |= src->streams[n]->selected;
+
+ if (!was_selected && src->any_selected) {
+ src->eof_reached = false;
+ src->dts = MP_NOPTS_VALUE;
+ TA_FREEP(&src->next);
+ }
+ }
+}
+
+static void close_lazy_segments(struct demuxer *demuxer,
+ struct virtual_source *src)
+{
+ // unload previous segment
+ for (int n = 0; n < src->num_segments; n++) {
+ struct segment *seg = src->segments[n];
+ if (seg != src->current && seg->d && seg->lazy) {
+ TA_FREEP(&src->next); // might depend on one of the sub-demuxers
+ demux_free(seg->d);
+ seg->d = NULL;
+ }
+ }
+}
+
+static void reopen_lazy_segments(struct demuxer *demuxer,
+ struct virtual_source *src)
+{
+ if (src->current->d)
+ return;
+
+ // Note: in delay_open mode, we must _not_ close segments during demuxing,
+ // because demuxed packets have demux_packet.codec set to objects owned
+ // by the segments. Closing them would create dangling pointers.
+ if (!src->delay_open)
+ close_lazy_segments(demuxer, src);
+
+ struct demuxer_params params = {
+ .init_fragment = src->tl->init_fragment,
+ .skip_lavf_probing = src->tl->dash,
+ .stream_flags = demuxer->stream_origin,
+ };
+ src->current->d = demux_open_url(src->current->url, &params,
+ demuxer->cancel, demuxer->global);
+ if (!src->current->d && !demux_cancel_test(demuxer))
+ MP_ERR(demuxer, "failed to load segment\n");
+ if (src->current->d)
+ update_slave_stats(demuxer, src->current->d);
+ associate_streams(demuxer, src, src->current);
+}
+
+static void switch_segment(struct demuxer *demuxer, struct virtual_source *src,
+ struct segment *new, double start_pts, int flags,
+ bool init)
+{
+ if (!(flags & SEEK_FORWARD))
+ flags |= SEEK_HR;
+
+ MP_VERBOSE(demuxer, "switch to segment %d\n", new->index);
+
+ if (src->current && src->current->d)
+ update_slave_stats(demuxer, src->current->d);
+
+ src->current = new;
+ reopen_lazy_segments(demuxer, src);
+ if (!new->d)
+ return;
+ reselect_streams(demuxer);
+ if (!src->no_clip)
+ demux_set_ts_offset(new->d, new->start - new->d_start);
+ if (!src->no_clip || !init)
+ demux_seek(new->d, start_pts, flags);
+
+ for (int n = 0; n < src->num_streams; n++) {
+ struct virtual_stream *vs = src->streams[n];
+ vs->eos_packets = 0;
+ }
+
+ src->eof_reached = false;
+ src->eos_packets = 0;
+}
+
+static void do_read_next_packet(struct demuxer *demuxer,
+ struct virtual_source *src)
+{
+ if (src->next)
+ return;
+
+ struct segment *seg = src->current;
+ if (!seg || !seg->d) {
+ src->eof_reached = true;
+ return;
+ }
+
+ struct demux_packet *pkt = demux_read_any_packet(seg->d);
+ if (!pkt || (!src->no_clip && pkt->pts >= seg->end))
+ src->eos_packets += 1;
+
+ update_slave_stats(demuxer, seg->d);
+
+ // Test for EOF. Do this here to properly run into EOF even if other
+ // streams are disabled etc. If it somehow doesn't manage to reach the end
+ // after demuxing a high (bit arbitrary) number of packets, assume one of
+ // the streams went EOF early.
+ bool eos_reached = src->eos_packets > 0;
+ if (eos_reached && src->eos_packets < 100) {
+ for (int n = 0; n < src->num_streams; n++) {
+ struct virtual_stream *vs = src->streams[n];
+ if (vs->selected) {
+ int max_packets = 0;
+ if (vs->sh->type == STREAM_AUDIO)
+ max_packets = 1;
+ if (vs->sh->type == STREAM_VIDEO)
+ max_packets = 16;
+ eos_reached &= vs->eos_packets >= max_packets;
+ }
+ }
+ }
+
+ src->eof_reached = false;
+
+ if (eos_reached || !pkt) {
+ talloc_free(pkt);
+
+ struct segment *next = NULL;
+ for (int n = 0; n < src->num_segments - 1; n++) {
+ if (src->segments[n] == seg) {
+ next = src->segments[n + 1];
+ break;
+ }
+ }
+ if (!next) {
+ src->eof_reached = true;
+ return;
+ }
+ switch_segment(demuxer, src, next, next->start, 0, true);
+ return; // reader will retry
+ }
+
+ if (pkt->stream < 0 || pkt->stream >= seg->num_stream_map)
+ goto drop;
+
+ if (!src->no_clip || src->delay_open) {
+ pkt->segmented = true;
+ if (!pkt->codec)
+ pkt->codec = demux_get_stream(seg->d, pkt->stream)->codec;
+ }
+ if (!src->no_clip) {
+ if (pkt->start == MP_NOPTS_VALUE || pkt->start < seg->start)
+ pkt->start = seg->start;
+ if (pkt->end == MP_NOPTS_VALUE || pkt->end > seg->end)
+ pkt->end = seg->end;
+ }
+
+ struct virtual_stream *vs = seg->stream_map[pkt->stream];
+ if (!vs)
+ goto drop;
+
+ // for refresh seeks, demux.c prefers monotonically increasing packet pos
+ // since the packet pos is meaningless anyway for timeline, use it
+ if (pkt->pos >= 0)
+ pkt->pos |= (seg->index & 0x7FFFULL) << 48;
+
+ if (pkt->pts != MP_NOPTS_VALUE && !src->no_clip && pkt->pts >= seg->end) {
+ // Trust the keyframe flag. Might not always be a good idea, but will
+ // be sufficient at least with mkv. The problem is that this flag is
+ // not well-defined in libavformat and is container-dependent.
+ if (pkt->keyframe || vs->eos_packets == INT_MAX) {
+ vs->eos_packets = INT_MAX;
+ goto drop;
+ } else {
+ vs->eos_packets += 1;
+ }
+ }
+
+ double dts = pkt->dts != MP_NOPTS_VALUE ? pkt->dts : pkt->pts;
+ if (src->dts == MP_NOPTS_VALUE || (dts != MP_NOPTS_VALUE && dts > src->dts))
+ src->dts = dts;
+
+ pkt->stream = vs->sh->index;
+ src->next = pkt;
+ return;
+
+drop:
+ talloc_free(pkt);
+}
+
+static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt)
+{
+ struct priv *p = demuxer->priv;
+ struct virtual_source *src = NULL;
+
+ for (int x = 0; x < p->num_sources; x++) {
+ struct virtual_source *cur = p->sources[x];
+
+ if (!cur->any_selected || cur->eof_reached)
+ continue;
+
+ if (!cur->current)
+ switch_segment(demuxer, cur, cur->segments[0], 0, 0, true);
+
+ if (!cur->any_selected || !cur->current || !cur->current->d)
+ continue;
+
+ if (!src || cur->dts == MP_NOPTS_VALUE ||
+ (src->dts != MP_NOPTS_VALUE && cur->dts < src->dts))
+ src = cur;
+ }
+
+ if (!src)
+ return false;
+
+ do_read_next_packet(demuxer, src);
+ *out_pkt = src->next;
+ src->next = NULL;
+ return true;
+}
+
+static void seek_source(struct demuxer *demuxer, struct virtual_source *src,
+ double pts, int flags)
+{
+ struct segment *new = src->segments[src->num_segments - 1];
+ for (int n = 0; n < src->num_segments; n++) {
+ if (pts < src->segments[n]->end) {
+ new = src->segments[n];
+ break;
+ }
+ }
+
+ switch_segment(demuxer, src, new, pts, flags, false);
+
+ src->dts = MP_NOPTS_VALUE;
+ TA_FREEP(&src->next);
+}
+
+static void d_seek(struct demuxer *demuxer, double seek_pts, int flags)
+{
+ struct priv *p = demuxer->priv;
+
+ seek_pts = seek_pts * ((flags & SEEK_FACTOR) ? p->duration : 1);
+ flags &= SEEK_FORWARD | SEEK_HR;
+
+ // The intention is to seek audio streams to the same target as video
+ // streams if they are separate streams. Video streams usually have more
+ // coarse keyframe snapping, which could leave video without audio.
+ struct virtual_source *master = NULL;
+ bool has_slaves = false;
+ for (int x = 0; x < p->num_sources; x++) {
+ struct virtual_source *src = p->sources[x];
+
+ bool any_audio = false, any_video = false;
+ for (int i = 0; i < src->num_streams; i++) {
+ struct virtual_stream *str = src->streams[i];
+ if (str->selected) {
+ if (str->sh->type == STREAM_VIDEO)
+ any_video = true;
+ if (str->sh->type == STREAM_AUDIO)
+ any_audio = true;
+ }
+ }
+
+ if (any_video)
+ master = src;
+ // A true slave stream is audio-only; this also prevents that the master
+ // stream is considered a slave stream.
+ if (any_audio && !any_video)
+ has_slaves = true;
+ }
+
+ if (!has_slaves)
+ master = NULL;
+
+ if (master) {
+ seek_source(demuxer, master, seek_pts, flags);
+ do_read_next_packet(demuxer, master);
+ if (master->next && master->next->pts != MP_NOPTS_VALUE) {
+ // Assume we got a seek target. Actually apply the heuristic.
+ MP_VERBOSE(demuxer, "adjust seek target from %f to %f\n", seek_pts,
+ master->next->pts);
+ seek_pts = master->next->pts;
+ flags &= ~(unsigned)SEEK_FORWARD;
+ }
+ }
+
+ for (int x = 0; x < p->num_sources; x++) {
+ struct virtual_source *src = p->sources[x];
+ if (src != master && src->any_selected)
+ seek_source(demuxer, src, seek_pts, flags);
+ }
+}
+
+static void print_timeline(struct demuxer *demuxer)
+{
+ struct priv *p = demuxer->priv;
+
+ MP_VERBOSE(demuxer, "Timeline segments:\n");
+ for (int x = 0; x < p->num_sources; x++) {
+ struct virtual_source *src = p->sources[x];
+
+ if (x >= 1)
+ MP_VERBOSE(demuxer, " --- new parallel stream ---\n");
+
+ for (int n = 0; n < src->num_segments; n++) {
+ struct segment *seg = src->segments[n];
+ int src_num = n;
+ for (int i = 0; i < n; i++) {
+ if (seg->d && src->segments[i]->d == seg->d) {
+ src_num = i;
+ break;
+ }
+ }
+ MP_VERBOSE(demuxer, " %2d: %12f - %12f [%12f] (",
+ n, seg->start, seg->end, seg->d_start);
+ for (int i = 0; i < seg->num_stream_map; i++) {
+ struct virtual_stream *vs = seg->stream_map[i];
+ MP_VERBOSE(demuxer, "%s%d", i ? " " : "",
+ vs ? vs->sh->index : -1);
+ }
+ MP_VERBOSE(demuxer, ")\n source %d:'%s'\n", src_num, seg->url);
+ }
+
+ if (src->dash)
+ MP_VERBOSE(demuxer, " (Using pseudo-DASH mode.)\n");
+ }
+ MP_VERBOSE(demuxer, "Total duration: %f\n", p->duration);
+}
+
+// Copy various (not all) metadata fields from src to dst, but try not to
+// overwrite fields in dst that are unset in src.
+// May keep data from src by reference.
+// Imperfect and arbitrary, only suited for EDL stuff.
+static void apply_meta(struct sh_stream *dst, struct sh_stream *src)
+{
+ if (src->demuxer_id >= 0)
+ dst->demuxer_id = src->demuxer_id;
+ if (src->title)
+ dst->title = src->title;
+ if (src->lang)
+ dst->lang = src->lang;
+ dst->default_track = src->default_track;
+ dst->forced_track = src->forced_track;
+ if (src->hls_bitrate)
+ dst->hls_bitrate = src->hls_bitrate;
+ dst->missing_timestamps = src->missing_timestamps;
+ if (src->attached_picture)
+ dst->attached_picture = src->attached_picture;
+ dst->image = src->image;
+}
+
+// This is mostly for EDL user-defined metadata.
+static struct sh_stream *find_matching_meta(struct timeline_par *tl, int index)
+{
+ for (int n = 0; n < tl->num_sh_meta; n++) {
+ struct sh_stream *sh = tl->sh_meta[n];
+ if (sh->index == index || sh->index < 0)
+ return sh;
+ }
+ return NULL;
+}
+
+static bool add_tl(struct demuxer *demuxer, struct timeline_par *tl)
+{
+ struct priv *p = demuxer->priv;
+
+ struct virtual_source *src = talloc_ptrtype(p, src);
+ *src = (struct virtual_source){
+ .tl = tl,
+ .dash = tl->dash,
+ .delay_open = tl->delay_open,
+ .no_clip = tl->no_clip || tl->dash,
+ .dts = MP_NOPTS_VALUE,
+ };
+
+ if (!tl->num_parts)
+ return false;
+
+ MP_TARRAY_APPEND(p, p->sources, p->num_sources, src);
+
+ p->duration = MPMAX(p->duration, tl->parts[tl->num_parts - 1].end);
+
+ struct demuxer *meta = tl->track_layout;
+
+ // delay_open streams normally have meta==NULL, and 1 virtual stream
+ int num_streams = 0;
+ if (tl->delay_open) {
+ num_streams = tl->num_sh_meta;
+ } else if (meta) {
+ num_streams = demux_get_num_stream(meta);
+ }
+ for (int n = 0; n < num_streams; n++) {
+ struct sh_stream *new = NULL;
+
+ if (tl->delay_open) {
+ struct sh_stream *tsh = tl->sh_meta[n];
+ new = demux_alloc_sh_stream(tsh->type);
+ new->codec = tsh->codec;
+ apply_meta(new, tsh);
+ demuxer->is_network = true;
+ demuxer->is_streaming = true;
+ } else {
+ struct sh_stream *sh = demux_get_stream(meta, n);
+ new = demux_alloc_sh_stream(sh->type);
+ apply_meta(new, sh);
+ new->codec = sh->codec;
+ struct sh_stream *tsh = find_matching_meta(tl, n);
+ if (tsh)
+ apply_meta(new, tsh);
+ }
+
+ demux_add_sh_stream(demuxer, new);
+ struct virtual_stream *vs = talloc_ptrtype(p, vs);
+ *vs = (struct virtual_stream){
+ .src = src,
+ .sh = new,
+ };
+ MP_TARRAY_APPEND(p, p->streams, p->num_streams, vs);
+ assert(demux_get_stream(demuxer, p->num_streams - 1) == new);
+ MP_TARRAY_APPEND(src, src->streams, src->num_streams, vs);
+ }
+
+ for (int n = 0; n < tl->num_parts; n++) {
+ struct timeline_part *part = &tl->parts[n];
+
+ // demux_timeline already does caching, doing it for the sub-demuxers
+ // would be pointless and wasteful.
+ if (part->source) {
+ demuxer->is_network |= part->source->is_network;
+ demuxer->is_streaming |= part->source->is_streaming;
+ }
+
+ if (!part->source)
+ assert(tl->dash || tl->delay_open);
+
+ struct segment *seg = talloc_ptrtype(src, seg);
+ *seg = (struct segment){
+ .d = part->source,
+ .url = part->source ? part->source->filename : part->url,
+ .lazy = !part->source,
+ .d_start = part->source_start,
+ .start = part->start,
+ .end = part->end,
+ };
+
+ associate_streams(demuxer, src, seg);
+
+ seg->index = n;
+ MP_TARRAY_APPEND(src, src->segments, src->num_segments, seg);
+ }
+
+ if (tl->track_layout) {
+ demuxer->is_network |= tl->track_layout->is_network;
+ demuxer->is_streaming |= tl->track_layout->is_streaming;
+ }
+ return true;
+}
+
+static int d_open(struct demuxer *demuxer, enum demux_check check)
+{
+ struct priv *p = demuxer->priv = talloc_zero(demuxer, struct priv);
+ p->tl = demuxer->params ? demuxer->params->timeline : NULL;
+ if (!p->tl || p->tl->num_pars < 1)
+ return -1;
+
+ demuxer->chapters = p->tl->chapters;
+ demuxer->num_chapters = p->tl->num_chapters;
+
+ struct demuxer *meta = p->tl->meta;
+ if (meta) {
+ demuxer->metadata = meta->metadata;
+ demuxer->attachments = meta->attachments;
+ demuxer->num_attachments = meta->num_attachments;
+ demuxer->editions = meta->editions;
+ demuxer->num_editions = meta->num_editions;
+ demuxer->edition = meta->edition;
+ }
+
+ for (int n = 0; n < p->tl->num_pars; n++) {
+ if (!add_tl(demuxer, p->tl->pars[n]))
+ return -1;
+ }
+
+ if (!p->num_sources)
+ return -1;
+
+ demuxer->is_network |= p->tl->is_network;
+ demuxer->is_streaming |= p->tl->is_streaming;
+
+ demuxer->duration = p->duration;
+
+ print_timeline(demuxer);
+
+ demuxer->seekable = true;
+ demuxer->partially_seekable = false;
+
+ const char *format_name = "unknown";
+ if (meta)
+ format_name = meta->filetype ? meta->filetype : meta->desc->name;
+ demuxer->filetype = talloc_asprintf(p, "%s/%s", p->tl->format, format_name);
+
+ reselect_streams(demuxer);
+
+ p->owns_tl = true;
+ return 0;
+}
+
+static void d_close(struct demuxer *demuxer)
+{
+ struct priv *p = demuxer->priv;
+
+ for (int x = 0; x < p->num_sources; x++) {
+ struct virtual_source *src = p->sources[x];
+
+ src->current = NULL;
+ TA_FREEP(&src->next);
+ close_lazy_segments(demuxer, src);
+ }
+
+ if (p->owns_tl) {
+ struct demuxer *master = p->tl->demuxer;
+ timeline_destroy(p->tl);
+ demux_free(master);
+ }
+}
+
+static void d_switched_tracks(struct demuxer *demuxer)
+{
+ reselect_streams(demuxer);
+}
+
+const demuxer_desc_t demuxer_desc_timeline = {
+ .name = "timeline",
+ .desc = "timeline segments",
+ .read_packet = d_read_packet,
+ .open = d_open,
+ .close = d_close,
+ .seek = d_seek,
+ .switched_tracks = d_switched_tracks,
+};
diff --git a/demux/ebml.c b/demux/ebml.c
new file mode 100644
index 0000000..7f62f1f
--- /dev/null
+++ b/demux/ebml.c
@@ -0,0 +1,619 @@
+/*
+ * native ebml reader for the Matroska demuxer
+ * new parser copyright (c) 2010 Uoti Urpala
+ * copyright (c) 2004 Aurelien Jacobs <aurel@gnuage.org>
+ * based on the one written by Ronald Bultje for gstreamer
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <stddef.h>
+#include <assert.h>
+
+#include <libavutil/intfloat.h>
+#include <libavutil/common.h>
+#include "mpv_talloc.h"
+#include "ebml.h"
+#include "stream/stream.h"
+#include "common/msg.h"
+
+// Whether the id is a known Matroska level 1 element (allowed as element on
+// global file level, after the level 0 MATROSKA_ID_SEGMENT).
+// This (intentionally) doesn't include "global" elements.
+bool ebml_is_mkv_level1_id(uint32_t id)
+{
+ switch (id) {
+ case MATROSKA_ID_SEEKHEAD:
+ case MATROSKA_ID_INFO:
+ case MATROSKA_ID_CLUSTER:
+ case MATROSKA_ID_TRACKS:
+ case MATROSKA_ID_CUES:
+ case MATROSKA_ID_ATTACHMENTS:
+ case MATROSKA_ID_CHAPTERS:
+ case MATROSKA_ID_TAGS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/*
+ * Read: the element content data ID.
+ * Return: the ID.
+ */
+uint32_t ebml_read_id(stream_t *s)
+{
+ int i, len_mask = 0x80;
+ uint32_t id;
+
+ for (i = 0, id = stream_read_char(s); i < 4 && !(id & len_mask); i++)
+ len_mask >>= 1;
+ if (i >= 4)
+ return EBML_ID_INVALID;
+ while (i--)
+ id = (id << 8) | stream_read_char(s);
+ return id;
+}
+
+/*
+ * Read: element content length.
+ */
+uint64_t ebml_read_length(stream_t *s)
+{
+ int i, j, num_ffs = 0, len_mask = 0x80;
+ uint64_t len;
+
+ for (i = 0, len = stream_read_char(s); i < 8 && !(len & len_mask); i++)
+ len_mask >>= 1;
+ if (i >= 8)
+ return EBML_UINT_INVALID;
+ j = i + 1;
+ if ((int) (len &= (len_mask - 1)) == len_mask - 1)
+ num_ffs++;
+ while (i--) {
+ len = (len << 8) | stream_read_char(s);
+ if ((len & 0xFF) == 0xFF)
+ num_ffs++;
+ }
+ if (j == num_ffs)
+ return EBML_UINT_INVALID;
+ if (len >= 1ULL<<63) // Can happen if stream_read_char returns EOF
+ return EBML_UINT_INVALID;
+ return len;
+}
+
+
+/*
+ * Read a variable length signed int.
+ */
+int64_t ebml_read_signed_length(stream_t *s)
+{
+ uint64_t unum;
+ int l;
+
+ /* read as unsigned number first */
+ uint64_t offset = stream_tell(s);
+ unum = ebml_read_length(s);
+ if (unum == EBML_UINT_INVALID)
+ return EBML_INT_INVALID;
+ l = stream_tell(s) - offset;
+
+ return unum - ((1LL << ((7 * l) - 1)) - 1);
+}
+
+/*
+ * Read the next element as an unsigned int.
+ */
+uint64_t ebml_read_uint(stream_t *s)
+{
+ uint64_t len, value = 0;
+
+ len = ebml_read_length(s);
+ if (len == EBML_UINT_INVALID || len > 8)
+ return EBML_UINT_INVALID;
+
+ while (len--)
+ value = (value << 8) | stream_read_char(s);
+
+ return value;
+}
+
+/*
+ * Read the next element as a signed int.
+ */
+int64_t ebml_read_int(stream_t *s)
+{
+ uint64_t value = 0;
+ uint64_t len;
+ int l;
+
+ len = ebml_read_length(s);
+ if (len == EBML_UINT_INVALID || len > 8)
+ return EBML_INT_INVALID;
+ if (!len)
+ return 0;
+
+ len--;
+ l = stream_read_char(s);
+ if (l & 0x80)
+ value = -1;
+ value = (value << 8) | l;
+ while (len--)
+ value = (value << 8) | stream_read_char(s);
+
+ return (int64_t)value; // assume complement of 2
+}
+
+/*
+ * Skip the current element.
+ * end: the end of the parent element or -1 (for robust error handling)
+ */
+int ebml_read_skip(struct mp_log *log, int64_t end, stream_t *s)
+{
+ uint64_t len;
+
+ int64_t pos = stream_tell(s);
+
+ len = ebml_read_length(s);
+ if (len == EBML_UINT_INVALID)
+ goto invalid;
+
+ int64_t pos2 = stream_tell(s);
+ if (len >= INT64_MAX - pos2 || (end > 0 && pos2 + len > end))
+ goto invalid;
+
+ if (!stream_seek_skip(s, pos2 + len))
+ goto invalid;
+
+ return 0;
+
+invalid:
+ mp_err(log, "Invalid EBML length at position %"PRId64"\n", pos);
+ stream_seek_skip(s, pos);
+ return 1;
+}
+
+/*
+ * Skip to (probable) next cluster (MATROSKA_ID_CLUSTER) element start position.
+ */
+int ebml_resync_cluster(struct mp_log *log, stream_t *s)
+{
+ int64_t pos = stream_tell(s);
+ uint32_t last_4_bytes = 0;
+ stream_read_peek(s, &(char){0}, 1);
+ if (!s->eof) {
+ mp_err(log, "Corrupt file detected. "
+ "Trying to resync starting from position %"PRId64"...\n", pos);
+ }
+ while (!s->eof) {
+ // Assumes MATROSKA_ID_CLUSTER is 4 bytes, with no 0 bytes.
+ if (last_4_bytes == MATROSKA_ID_CLUSTER) {
+ mp_err(log, "Cluster found at %"PRId64".\n", pos - 4);
+ stream_seek(s, pos - 4);
+ return 0;
+ }
+ last_4_bytes = (last_4_bytes << 8) | stream_read_char(s);
+ pos++;
+ }
+ return -1;
+}
+
+
+
+#define EVALARGS(F, ...) F(__VA_ARGS__)
+#define E(str, N, type) const struct ebml_elem_desc ebml_ ## N ## _desc = { str, type };
+#define E_SN(str, count, N) const struct ebml_elem_desc ebml_ ## N ## _desc = { str, EBML_TYPE_SUBELEMENTS, sizeof(struct ebml_ ## N), count, (const struct ebml_field_desc[]){
+#define E_S(str, count) EVALARGS(E_SN, str, count, N)
+#define FN(id, name, multiple, N) { id, multiple, offsetof(struct ebml_ ## N, name), offsetof(struct ebml_ ## N, n_ ## name), &ebml_##name##_desc},
+#define F(id, name, multiple) EVALARGS(FN, id, name, multiple, N)
+#include "ebml_defs.inc"
+#undef EVALARGS
+#undef SN
+#undef S
+#undef FN
+#undef F
+
+// Used to read/write pointers to different struct types
+struct generic;
+#define generic_struct struct generic
+
+static uint32_t ebml_parse_id(uint8_t *data, size_t data_len, int *length)
+{
+ *length = -1;
+ uint8_t *end = data + data_len;
+ if (data == end)
+ return EBML_ID_INVALID;
+ int len = 1;
+ uint32_t id = *data++;
+ for (int len_mask = 0x80; !(id & len_mask); len_mask >>= 1) {
+ len++;
+ if (len > 4)
+ return EBML_ID_INVALID;
+ }
+ *length = len;
+ while (--len && data < end)
+ id = (id << 8) | *data++;
+ return id;
+}
+
+static uint64_t ebml_parse_length(uint8_t *data, size_t data_len, int *length)
+{
+ *length = -1;
+ uint8_t *end = data + data_len;
+ if (data == end)
+ return -1;
+ uint64_t r = *data++;
+ int len = 1;
+ int len_mask;
+ for (len_mask = 0x80; !(r & len_mask); len_mask >>= 1) {
+ len++;
+ if (len > 8)
+ return -1;
+ }
+ r &= len_mask - 1;
+
+ int num_allones = 0;
+ if (r == len_mask - 1)
+ num_allones++;
+ for (int i = 1; i < len; i++) {
+ if (data == end)
+ return -1;
+ if (*data == 255)
+ num_allones++;
+ r = (r << 8) | *data++;
+ }
+ // According to Matroska specs this means "unknown length"
+ // Could be supported if there are any actual files using it
+ if (num_allones == len)
+ return -1;
+ *length = len;
+ return r;
+}
+
+static uint64_t ebml_parse_uint(uint8_t *data, int length)
+{
+ assert(length >= 0 && length <= 8);
+ uint64_t r = 0;
+ while (length--)
+ r = (r << 8) + *data++;
+ return r;
+}
+
+static int64_t ebml_parse_sint(uint8_t *data, int length)
+{
+ assert(length >= 0 && length <= 8);
+ if (!length)
+ return 0;
+ uint64_t r = 0;
+ if (*data & 0x80)
+ r = -1;
+ while (length--)
+ r = (r << 8) | *data++;
+ return (int64_t)r; // assume complement of 2
+}
+
+static double ebml_parse_float(uint8_t *data, int length)
+{
+ assert(length == 0 || length == 4 || length == 8);
+ uint64_t i = ebml_parse_uint(data, length);
+ if (length == 4)
+ return av_int2float(i);
+ else
+ return av_int2double(i);
+}
+
+
+// target must be initialized to zero
+static void ebml_parse_element(struct ebml_parse_ctx *ctx, void *target,
+ uint8_t *data, int size,
+ const struct ebml_elem_desc *type, int level)
+{
+ assert(type->type == EBML_TYPE_SUBELEMENTS);
+ assert(level < 8);
+ MP_TRACE(ctx, "%.*sParsing element %s\n", level, " ", type->name);
+
+ char *s = target;
+ uint8_t *end = data + size;
+ uint8_t *p = data;
+ int num_elems[MAX_EBML_SUBELEMENTS] = {0};
+ while (p < end) {
+ uint8_t *startp = p;
+ int len;
+ uint32_t id = ebml_parse_id(p, end - p, &len);
+ if (len > end - p)
+ goto past_end_error;
+ if (len < 0) {
+ MP_ERR(ctx, "Error parsing subelement id\n");
+ goto other_error;
+ }
+ p += len;
+ uint64_t length = ebml_parse_length(p, end - p, &len);
+ if (len > end - p)
+ goto past_end_error;
+ if (len < 0) {
+ MP_ERR(ctx, "Error parsing subelement length\n");
+ goto other_error;
+ }
+ p += len;
+
+ int field_idx = -1;
+ for (int i = 0; i < type->field_count; i++)
+ if (type->fields[i].id == id) {
+ field_idx = i;
+ num_elems[i]++;
+ if (num_elems[i] >= 0x70000000) {
+ MP_ERR(ctx, "Too many EBML subelements.\n");
+ goto other_error;
+ }
+ break;
+ }
+
+ if (length > end - p) {
+ if (field_idx >= 0 && type->fields[field_idx].desc->type
+ != EBML_TYPE_SUBELEMENTS) {
+ MP_ERR(ctx, "Subelement content goes "
+ "past end of containing element\n");
+ goto other_error;
+ }
+ // Try to parse what is possible from inside this partial element
+ ctx->has_errors = true;
+ length = end - p;
+ }
+ p += length;
+
+ continue;
+
+ past_end_error:
+ MP_ERR(ctx, "Subelement headers go past end of containing element\n");
+ other_error:
+ ctx->has_errors = true;
+ end = startp;
+ break;
+ }
+
+ for (int i = 0; i < type->field_count; i++) {
+ if (num_elems[i] && type->fields[i].multiple) {
+ char *ptr = s + type->fields[i].offset;
+ switch (type->fields[i].desc->type) {
+ case EBML_TYPE_SUBELEMENTS: {
+ size_t max = 1000000000 / type->fields[i].desc->size;
+ if (num_elems[i] > max) {
+ MP_ERR(ctx, "Too many subelements.\n");
+ num_elems[i] = max;
+ }
+ int sz = num_elems[i] * type->fields[i].desc->size;
+ *(generic_struct **) ptr = talloc_zero_size(ctx->talloc_ctx, sz);
+ break;
+ }
+ case EBML_TYPE_UINT:
+ *(uint64_t **) ptr = talloc_zero_array(ctx->talloc_ctx,
+ uint64_t, num_elems[i]);
+ break;
+ case EBML_TYPE_SINT:
+ *(int64_t **) ptr = talloc_zero_array(ctx->talloc_ctx,
+ int64_t, num_elems[i]);
+ break;
+ case EBML_TYPE_FLOAT:
+ *(double **) ptr = talloc_zero_array(ctx->talloc_ctx,
+ double, num_elems[i]);
+ break;
+ case EBML_TYPE_STR:
+ *(char ***) ptr = talloc_zero_array(ctx->talloc_ctx,
+ char *, num_elems[i]);
+ break;
+ case EBML_TYPE_BINARY:
+ *(struct bstr **) ptr = talloc_zero_array(ctx->talloc_ctx,
+ struct bstr,
+ num_elems[i]);
+ break;
+ case EBML_TYPE_EBML_ID:
+ *(int32_t **) ptr = talloc_zero_array(ctx->talloc_ctx,
+ uint32_t, num_elems[i]);
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+ }
+ }
+
+ while (data < end) {
+ int len;
+ uint32_t id = ebml_parse_id(data, end - data, &len);
+ if (len < 0 || len > end - data) {
+ MP_ERR(ctx, "Error parsing subelement\n");
+ break;
+ }
+ data += len;
+ uint64_t length = ebml_parse_length(data, end - data, &len);
+ if (len < 0 || len > end - data) {
+ MP_ERR(ctx, "Error parsing subelement length\n");
+ break;
+ }
+ data += len;
+ if (length > end - data) {
+ // Try to parse what is possible from inside this partial element
+ length = end - data;
+ MP_ERR(ctx, "Next subelement content goes "
+ "past end of containing element, will be truncated\n");
+ }
+ int field_idx = -1;
+ for (int i = 0; i < type->field_count; i++)
+ if (type->fields[i].id == id) {
+ field_idx = i;
+ break;
+ }
+ if (field_idx < 0) {
+ if (id == 0xec) {
+ MP_TRACE(ctx, "%.*sIgnoring Void element "
+ "size: %"PRIu64"\n", level+1, " ", length);
+ } else if (id == 0xbf) {
+ MP_TRACE(ctx, "%.*sIgnoring CRC-32 "
+ "element size: %"PRIu64"\n", level+1, " ",
+ length);
+ } else {
+ MP_DBG(ctx, "Ignoring unrecognized "
+ "subelement. ID: %x size: %"PRIu64"\n", id, length);
+ }
+ data += length;
+ continue;
+ }
+ const struct ebml_field_desc *fd = &type->fields[field_idx];
+ const struct ebml_elem_desc *ed = fd->desc;
+ bool multiple = fd->multiple;
+ int *countptr = (int *) (s + fd->count_offset);
+ if (*countptr >= num_elems[field_idx]) {
+ // Shouldn't happen on any sane file without bugs
+ MP_ERR(ctx, "Too many subelements.\n");
+ ctx->has_errors = true;
+ data += length;
+ continue;
+ }
+ if (*countptr > 0 && !multiple) {
+ MP_WARN(ctx, "Another subelement of type "
+ "%x %s (size: %"PRIu64"). Only one allowed. Ignoring.\n",
+ id, ed->name, length);
+ ctx->has_errors = true;
+ data += length;
+ continue;
+ }
+ MP_TRACE(ctx, "%.*sParsing %x %s size: %"PRIu64
+ " value: ", level+1, " ", id, ed->name, length);
+
+ char *fieldptr = s + fd->offset;
+ switch (ed->type) {
+ case EBML_TYPE_SUBELEMENTS:
+ MP_TRACE(ctx, "subelements\n");
+ char *subelptr;
+ if (multiple) {
+ char *array_start = (char *) *(generic_struct **) fieldptr;
+ subelptr = array_start + *countptr * ed->size;
+ } else
+ subelptr = fieldptr;
+ ebml_parse_element(ctx, subelptr, data, length, ed, level + 1);
+ break;
+
+ case EBML_TYPE_UINT:;
+ uint64_t *uintptr;
+#define GETPTR(subelptr, fieldtype) \
+ if (multiple) \
+ subelptr = *(fieldtype **) fieldptr + *countptr; \
+ else \
+ subelptr = (fieldtype *) fieldptr
+ GETPTR(uintptr, uint64_t);
+ if (length < 1 || length > 8) {
+ MP_ERR(ctx, "uint invalid length %"PRIu64"\n", length);
+ goto error;
+ }
+ *uintptr = ebml_parse_uint(data, length);
+ MP_TRACE(ctx, "uint %"PRIu64"\n", *uintptr);
+ break;
+
+ case EBML_TYPE_SINT:;
+ int64_t *sintptr;
+ GETPTR(sintptr, int64_t);
+ if (length > 8) {
+ MP_ERR(ctx, "sint invalid length %"PRIu64"\n", length);
+ goto error;
+ }
+ *sintptr = ebml_parse_sint(data, length);
+ MP_TRACE(ctx, "sint %"PRId64"\n", *sintptr);
+ break;
+
+ case EBML_TYPE_FLOAT:;
+ double *floatptr;
+ GETPTR(floatptr, double);
+ if (length != 0 && length != 4 && length != 8) {
+ MP_ERR(ctx, "float invalid length %"PRIu64"\n", length);
+ goto error;
+ }
+ *floatptr = ebml_parse_float(data, length);
+ MP_DBG(ctx, "float %f\n", *floatptr);
+ break;
+
+ case EBML_TYPE_STR:
+ if (length > 1024 * 1024) {
+ MP_ERR(ctx, "Not reading overly long string element.\n");
+ break;
+ }
+ char **strptr;
+ GETPTR(strptr, char *);
+ *strptr = talloc_strndup(ctx->talloc_ctx, data, length);
+ MP_TRACE(ctx, "string \"%s\"\n", *strptr);
+ break;
+
+ case EBML_TYPE_BINARY:;
+ if (length > 0x80000000) {
+ MP_ERR(ctx, "Not reading overly long EBML element.\n");
+ break;
+ }
+ struct bstr *binptr;
+ GETPTR(binptr, struct bstr);
+ binptr->start = data;
+ binptr->len = length;
+ MP_TRACE(ctx, "binary %zd bytes\n", binptr->len);
+ break;
+
+ case EBML_TYPE_EBML_ID:;
+ uint32_t *idptr;
+ GETPTR(idptr, uint32_t);
+ *idptr = ebml_parse_id(data, end - data, &len);
+ if (len != length) {
+ MP_ERR(ctx, "ebml_id broken value\n");
+ goto error;
+ }
+ MP_TRACE(ctx, "ebml_id %x\n", (unsigned)*idptr);
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+ *countptr += 1;
+ error:
+ data += length;
+ }
+}
+
+// target must be initialized to zero
+int ebml_read_element(struct stream *s, struct ebml_parse_ctx *ctx,
+ void *target, const struct ebml_elem_desc *desc)
+{
+ ctx->has_errors = false;
+ int msglevel = ctx->no_error_messages ? MSGL_DEBUG : MSGL_WARN;
+ uint64_t length = ebml_read_length(s);
+ if (s->eof) {
+ MP_MSG(ctx, msglevel, "Unexpected end of file "
+ "- partial or corrupt file?\n");
+ return -1;
+ }
+ if (length == EBML_UINT_INVALID) {
+ MP_MSG(ctx, msglevel, "EBML element with unknown length - unsupported\n");
+ return -1;
+ }
+ if (length > 1000000000) {
+ MP_MSG(ctx, msglevel, "Refusing to read element over 100 MB in size\n");
+ return -1;
+ }
+ ctx->talloc_ctx = talloc_size(NULL, length);
+ int read_len = stream_read(s, ctx->talloc_ctx, length);
+ if (read_len < length)
+ MP_MSG(ctx, msglevel, "Unexpected end of file - partial or corrupt file?\n");
+ ebml_parse_element(ctx, target, ctx->talloc_ctx, read_len, desc, 0);
+ if (ctx->has_errors)
+ MP_MSG(ctx, msglevel, "Error parsing element %s\n", desc->name);
+ return 0;
+}
diff --git a/demux/ebml.h b/demux/ebml.h
new file mode 100644
index 0000000..86a4009
--- /dev/null
+++ b/demux/ebml.h
@@ -0,0 +1,93 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_EBML_H
+#define MPLAYER_EBML_H
+
+#include <inttypes.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "stream/stream.h"
+#include "misc/bstr.h"
+
+struct mp_log;
+
+/* EBML version supported */
+#define EBML_VERSION 1
+
+enum ebml_elemtype {
+ EBML_TYPE_SUBELEMENTS,
+ EBML_TYPE_UINT,
+ EBML_TYPE_SINT,
+ EBML_TYPE_FLOAT,
+ EBML_TYPE_STR,
+ EBML_TYPE_BINARY,
+ EBML_TYPE_EBML_ID,
+};
+
+struct ebml_field_desc {
+ uint32_t id;
+ bool multiple;
+ int offset;
+ int count_offset;
+ const struct ebml_elem_desc *desc;
+};
+
+struct ebml_elem_desc {
+ char *name;
+ enum ebml_elemtype type;
+ int size;
+ int field_count;
+ const struct ebml_field_desc *fields;
+};
+
+struct ebml_parse_ctx {
+ struct mp_log *log;
+ void *talloc_ctx;
+ bool has_errors;
+ bool no_error_messages;
+};
+
+#include "ebml_types.h"
+
+#define EBML_ID_INVALID 0xffffffff
+
+/* matroska track types */
+#define MATROSKA_TRACK_VIDEO 0x01 /* rectangle-shaped pictures aka video */
+#define MATROSKA_TRACK_AUDIO 0x02 /* anything you can hear */
+#define MATROSKA_TRACK_COMPLEX 0x03 /* audio+video in same track used by DV */
+#define MATROSKA_TRACK_LOGO 0x10 /* overlay-pictures displayed over video*/
+#define MATROSKA_TRACK_SUBTITLE 0x11 /* text-subtitles */
+#define MATROSKA_TRACK_CONTROL 0x20 /* control-codes for menu or other stuff*/
+
+#define EBML_UINT_INVALID UINT64_MAX
+#define EBML_INT_INVALID INT64_MAX
+
+bool ebml_is_mkv_level1_id(uint32_t id);
+uint32_t ebml_read_id (stream_t *s);
+uint64_t ebml_read_length (stream_t *s);
+int64_t ebml_read_signed_length(stream_t *s);
+uint64_t ebml_read_uint (stream_t *s);
+int64_t ebml_read_int (stream_t *s);
+int ebml_read_skip(struct mp_log *log, int64_t end, stream_t *s);
+int ebml_resync_cluster(struct mp_log *log, stream_t *s);
+
+int ebml_read_element(struct stream *s, struct ebml_parse_ctx *ctx,
+ void *target, const struct ebml_elem_desc *desc);
+
+#endif /* MPLAYER_EBML_H */
diff --git a/demux/matroska.h b/demux/matroska.h
new file mode 100644
index 0000000..939792c
--- /dev/null
+++ b/demux/matroska.h
@@ -0,0 +1,24 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_MATROSKA_H
+#define MPLAYER_MATROSKA_H
+
+struct timeline;
+void build_ordered_chapter_timeline(struct timeline *tl);
+
+#endif /* MPLAYER_MATROSKA_H */
diff --git a/demux/packet.c b/demux/packet.c
new file mode 100644
index 0000000..ed43729
--- /dev/null
+++ b/demux/packet.c
@@ -0,0 +1,244 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/intreadwrite.h>
+
+#include "common/av_common.h"
+#include "common/common.h"
+#include "demux.h"
+
+#include "packet.h"
+
+// Free any refcounted data dp holds (but don't free dp itself). This does not
+// care about pointers that are _not_ refcounted (like demux_packet.codec).
+// Normally, a user should use talloc_free(dp). This function is only for
+// annoyingly specific obscure use cases.
+void demux_packet_unref_contents(struct demux_packet *dp)
+{
+ if (dp->avpacket) {
+ assert(!dp->is_cached);
+ av_packet_free(&dp->avpacket);
+ dp->buffer = NULL;
+ dp->len = 0;
+ }
+}
+
+static void packet_destroy(void *ptr)
+{
+ struct demux_packet *dp = ptr;
+ demux_packet_unref_contents(dp);
+}
+
+static struct demux_packet *packet_create(void)
+{
+ struct demux_packet *dp = talloc(NULL, struct demux_packet);
+ talloc_set_destructor(dp, packet_destroy);
+ *dp = (struct demux_packet) {
+ .pts = MP_NOPTS_VALUE,
+ .dts = MP_NOPTS_VALUE,
+ .duration = -1,
+ .pos = -1,
+ .start = MP_NOPTS_VALUE,
+ .end = MP_NOPTS_VALUE,
+ .stream = -1,
+ .avpacket = av_packet_alloc(),
+ };
+ MP_HANDLE_OOM(dp->avpacket);
+ return dp;
+}
+
+// This actually preserves only data and side data, not PTS/DTS/pos/etc.
+// It also allows avpkt->data==NULL with avpkt->size!=0 - the libavcodec API
+// does not allow it, but we do it to simplify new_demux_packet().
+struct demux_packet *new_demux_packet_from_avpacket(struct AVPacket *avpkt)
+{
+ if (avpkt->size > 1000000000)
+ return NULL;
+ struct demux_packet *dp = packet_create();
+ int r = -1;
+ if (avpkt->data) {
+ // We hope that this function won't need/access AVPacket input padding,
+ // because otherwise new_demux_packet_from() wouldn't work.
+ r = av_packet_ref(dp->avpacket, avpkt);
+ } else {
+ r = av_new_packet(dp->avpacket, avpkt->size);
+ }
+ if (r < 0) {
+ talloc_free(dp);
+ return NULL;
+ }
+ dp->buffer = dp->avpacket->data;
+ dp->len = dp->avpacket->size;
+ return dp;
+}
+
+// (buf must include proper padding)
+struct demux_packet *new_demux_packet_from_buf(struct AVBufferRef *buf)
+{
+ if (!buf)
+ return NULL;
+ if (buf->size > 1000000000)
+ return NULL;
+
+ struct demux_packet *dp = packet_create();
+ dp->avpacket->buf = av_buffer_ref(buf);
+ if (!dp->avpacket->buf) {
+ talloc_free(dp);
+ return NULL;
+ }
+ dp->avpacket->data = dp->buffer = buf->data;
+ dp->avpacket->size = dp->len = buf->size;
+ return dp;
+}
+
+// Input data doesn't need to be padded.
+struct demux_packet *new_demux_packet_from(void *data, size_t len)
+{
+ struct demux_packet *dp = new_demux_packet(len);
+ if (!dp)
+ return NULL;
+ memcpy(dp->avpacket->data, data, len);
+ return dp;
+}
+
+struct demux_packet *new_demux_packet(size_t len)
+{
+ if (len > INT_MAX)
+ return NULL;
+
+ struct demux_packet *dp = packet_create();
+ int r = av_new_packet(dp->avpacket, len);
+ if (r < 0) {
+ talloc_free(dp);
+ return NULL;
+ }
+ dp->buffer = dp->avpacket->data;
+ dp->len = len;
+ return dp;
+}
+
+void demux_packet_shorten(struct demux_packet *dp, size_t len)
+{
+ assert(len <= dp->len);
+ if (dp->len) {
+ dp->len = len;
+ memset(dp->buffer + dp->len, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+ }
+}
+
+void free_demux_packet(struct demux_packet *dp)
+{
+ talloc_free(dp);
+}
+
+void demux_packet_copy_attribs(struct demux_packet *dst, struct demux_packet *src)
+{
+ dst->pts = src->pts;
+ dst->dts = src->dts;
+ dst->duration = src->duration;
+ dst->pos = src->pos;
+ dst->segmented = src->segmented;
+ dst->start = src->start;
+ dst->end = src->end;
+ dst->codec = src->codec;
+ dst->back_restart = src->back_restart;
+ dst->back_preroll = src->back_preroll;
+ dst->keyframe = src->keyframe;
+ dst->stream = src->stream;
+}
+
+struct demux_packet *demux_copy_packet(struct demux_packet *dp)
+{
+ struct demux_packet *new = NULL;
+ if (dp->avpacket) {
+ new = new_demux_packet_from_avpacket(dp->avpacket);
+ } else {
+ // Some packets might be not created by new_demux_packet*().
+ new = new_demux_packet_from(dp->buffer, dp->len);
+ }
+ if (!new)
+ return NULL;
+ demux_packet_copy_attribs(new, dp);
+ return new;
+}
+
+#define ROUND_ALLOC(s) MP_ALIGN_UP((s), 16)
+
+// Attempt to estimate the total memory consumption of the given packet.
+// This is important if we store thousands of packets and not to exceed
+// user-provided limits. Of course we can't know how much memory internal
+// fragmentation of the libc memory allocator will waste.
+// Note that this should return a "stable" value - e.g. if a new packet ref
+// is created, this should return the same value with the new ref. (This
+// implies the value is not exact and does not return the actual size of
+// memory wasted due to internal fragmentation.)
+size_t demux_packet_estimate_total_size(struct demux_packet *dp)
+{
+ size_t size = ROUND_ALLOC(sizeof(struct demux_packet));
+ size += 8 * sizeof(void *); // ta overhead
+ size += 10 * sizeof(void *); // additional estimate for ta_ext_header
+ if (dp->avpacket) {
+ assert(!dp->is_cached);
+ size += ROUND_ALLOC(dp->len);
+ size += ROUND_ALLOC(sizeof(AVPacket));
+ size += 8 * sizeof(void *); // ta overhead
+ size += ROUND_ALLOC(sizeof(AVBufferRef));
+ size += ROUND_ALLOC(64); // upper bound estimate on sizeof(AVBuffer)
+ size += ROUND_ALLOC(dp->avpacket->side_data_elems *
+ sizeof(dp->avpacket->side_data[0]));
+ for (int n = 0; n < dp->avpacket->side_data_elems; n++)
+ size += ROUND_ALLOC(dp->avpacket->side_data[n].size);
+ }
+ return size;
+}
+
+int demux_packet_set_padding(struct demux_packet *dp, int start, int end)
+{
+ if (!start && !end)
+ return 0;
+ if (!dp->avpacket)
+ return -1;
+ uint8_t *p = av_packet_new_side_data(dp->avpacket, AV_PKT_DATA_SKIP_SAMPLES, 10);
+ if (!p)
+ return -1;
+
+ AV_WL32(p + 0, start);
+ AV_WL32(p + 4, end);
+ return 0;
+}
+
+int demux_packet_add_blockadditional(struct demux_packet *dp, uint64_t id,
+ void *data, size_t size)
+{
+ if (!dp->avpacket)
+ return -1;
+ uint8_t *sd = av_packet_new_side_data(dp->avpacket,
+ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
+ 8 + size);
+ if (!sd)
+ return -1;
+ AV_WB64(sd, id);
+ if (size > 0)
+ memcpy(sd + 8, data, size);
+ return 0;
+}
diff --git a/demux/packet.h b/demux/packet.h
new file mode 100644
index 0000000..cd1183d
--- /dev/null
+++ b/demux/packet.h
@@ -0,0 +1,86 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_DEMUX_PACKET_H
+#define MPLAYER_DEMUX_PACKET_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <inttypes.h>
+
+// Holds one packet/frame/whatever
+typedef struct demux_packet {
+ double pts;
+ double dts;
+ double duration;
+ int64_t pos; // position in source file byte stream
+
+ union {
+ // Normally valid for packets.
+ struct {
+ unsigned char *buffer;
+ size_t len;
+ };
+
+ // Used if is_cached==true, special uses only.
+ struct {
+ uint64_t pos;
+ } cached_data;
+ };
+
+ int stream; // source stream index (typically sh_stream.index)
+
+ bool keyframe;
+
+ // backward playback
+ bool back_restart : 1; // restart point (reverse and return previous frames)
+ bool back_preroll : 1; // initial discarded frame for smooth decoder reinit
+
+ // If true, cached_data is valid, while buffer/len are not.
+ bool is_cached : 1;
+
+ // segmentation (ordered chapters, EDL)
+ bool segmented;
+ struct mp_codec_params *codec; // set to non-NULL iff segmented is set
+ double start, end; // set to non-NOPTS iff segmented is set
+
+ // private
+ struct demux_packet *next;
+ struct AVPacket *avpacket; // keep the buffer allocation and sidedata
+ uint64_t cum_pos; // demux.c internal: cumulative size until _start_ of pkt
+} demux_packet_t;
+
+struct AVBufferRef;
+
+struct demux_packet *new_demux_packet(size_t len);
+struct demux_packet *new_demux_packet_from_avpacket(struct AVPacket *avpkt);
+struct demux_packet *new_demux_packet_from(void *data, size_t len);
+struct demux_packet *new_demux_packet_from_buf(struct AVBufferRef *buf);
+void demux_packet_shorten(struct demux_packet *dp, size_t len);
+void free_demux_packet(struct demux_packet *dp);
+struct demux_packet *demux_copy_packet(struct demux_packet *dp);
+size_t demux_packet_estimate_total_size(struct demux_packet *dp);
+
+void demux_packet_copy_attribs(struct demux_packet *dst, struct demux_packet *src);
+
+int demux_packet_set_padding(struct demux_packet *dp, int start, int end);
+int demux_packet_add_blockadditional(struct demux_packet *dp, uint64_t id,
+ void *data, size_t size);
+
+void demux_packet_unref_contents(struct demux_packet *dp);
+
+#endif /* MPLAYER_DEMUX_PACKET_H */
diff --git a/demux/stheader.h b/demux/stheader.h
new file mode 100644
index 0000000..1bc036d
--- /dev/null
+++ b/demux/stheader.h
@@ -0,0 +1,119 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_STHEADER_H
+#define MPLAYER_STHEADER_H
+
+#include <stdbool.h>
+
+#include "common/common.h"
+#include "audio/chmap.h"
+#include "video/mp_image.h"
+
+struct MPOpts;
+struct demuxer;
+
+// Stream headers:
+
+struct sh_stream {
+ enum stream_type type;
+ // Index into demuxer->streams.
+ int index;
+ // Demuxer/format specific ID. Corresponds to the stream IDs as encoded in
+ // some file formats (e.g. MPEG), or an index chosen by demux.c.
+ int demuxer_id;
+ // FFmpeg stream index (AVFormatContext.streams[index]), or equivalent.
+ int ff_index;
+
+ struct mp_codec_params *codec;
+
+ char *title;
+ char *lang; // language code
+ bool default_track; // container default track flag
+ bool forced_track; // container forced track flag
+ bool dependent_track; // container dependent track flag
+ bool visual_impaired_track; // container flag
+ bool hearing_impaired_track;// container flag
+ bool image; // video stream is an image
+ bool still_image; // video stream contains still images
+ int hls_bitrate;
+ int program_id;
+
+ struct mp_tags *tags;
+
+ bool missing_timestamps;
+
+ double seek_preroll;
+
+ // stream is a picture (such as album art)
+ struct demux_packet *attached_picture;
+
+ // Internal to demux.c
+ struct demux_stream *ds;
+};
+
+struct mp_codec_params {
+ enum stream_type type;
+
+ // E.g. "h264" (usually corresponds to AVCodecDescriptor.name)
+ const char *codec;
+
+ // Usually a FourCC, exact meaning depends on codec.
+ unsigned int codec_tag;
+
+ unsigned char *extradata; // codec specific per-stream header
+ int extradata_size;
+
+ // Codec specific header data (set by demux_lavf.c only)
+ struct AVCodecParameters *lav_codecpar;
+
+ // Timestamp granularity for converting double<->rational timestamps.
+ int native_tb_num, native_tb_den;
+
+ // Used by an obscure bug workaround mechanism. As an exception to the usual
+ // rules, demuxers are allowed to set this after adding the sh_stream, but
+ // only before the demuxer open call returns.
+ struct demux_packet *first_packet;
+
+ // STREAM_AUDIO
+ int samplerate;
+ struct mp_chmap channels;
+ bool force_channels;
+ int bitrate; // compressed bits/sec
+ int block_align;
+ struct replaygain_data *replaygain_data;
+
+ // STREAM_VIDEO
+ bool avi_dts; // use DTS timing; first frame and DTS is 0
+ double fps; // frames per second (set only if constant fps)
+ bool reliable_fps; // the fps field is definitely not broken
+ int par_w, par_h; // pixel aspect ratio (0 if unknown/square)
+ int disp_w, disp_h; // display size
+ int rotate; // intended display rotation, in degrees, [0, 359]
+ int stereo_mode; // mp_stereo3d_mode (0 if none/unknown)
+ struct mp_colorspace color; // colorspace info where available
+ struct mp_rect crop; // crop to be applied
+
+ // STREAM_VIDEO + STREAM_AUDIO
+ int bits_per_coded_sample;
+
+ // STREAM_SUB
+ double frame_based; // timestamps are frame-based (and this is the
+ // fallback framerate used for timestamps)
+};
+
+#endif /* MPLAYER_STHEADER_H */
diff --git a/demux/timeline.c b/demux/timeline.c
new file mode 100644
index 0000000..0e5ff75
--- /dev/null
+++ b/demux/timeline.c
@@ -0,0 +1,41 @@
+#include "common/common.h"
+#include "stream/stream.h"
+#include "demux.h"
+
+#include "timeline.h"
+
+struct timeline *timeline_load(struct mpv_global *global, struct mp_log *log,
+ struct demuxer *demuxer)
+{
+ if (!demuxer->desc->load_timeline)
+ return NULL;
+
+ struct timeline *tl = talloc_ptrtype(NULL, tl);
+ *tl = (struct timeline){
+ .global = global,
+ .log = log,
+ .cancel = demuxer->cancel,
+ .demuxer = demuxer,
+ .format = "unknown",
+ .stream_origin = demuxer->stream_origin,
+ };
+
+ demuxer->desc->load_timeline(tl);
+
+ if (tl->num_pars)
+ return tl;
+ timeline_destroy(tl);
+ return NULL;
+}
+
+void timeline_destroy(struct timeline *tl)
+{
+ if (!tl)
+ return;
+ for (int n = 0; n < tl->num_sources; n++) {
+ struct demuxer *d = tl->sources[n];
+ if (d != tl->demuxer)
+ demux_free(d);
+ }
+ talloc_free(tl);
+}
diff --git a/demux/timeline.h b/demux/timeline.h
new file mode 100644
index 0000000..7bc7e9e
--- /dev/null
+++ b/demux/timeline.h
@@ -0,0 +1,72 @@
+#ifndef MP_TIMELINE_H_
+#define MP_TIMELINE_H_
+
+#include "common/common.h"
+#include "misc/bstr.h"
+
+// Single segment in a timeline.
+struct timeline_part {
+ // (end time must match with start time of the next part)
+ double start, end;
+ double source_start;
+ char *url;
+ struct demuxer *source;
+};
+
+// Timeline formed by a single demuxer. Multiple pars are used to get tracks
+// that require a separate opened demuxer, such as separate audio tracks. (For
+// example, for ordered chapters there is only a single par, because all streams
+// demux from the same file at a given time, while for DASH-style video+audio,
+// each track would have its own timeline.)
+// Note that demuxer instances must not be shared across timeline_pars. This
+// would conflict in demux_timeline.c.
+// "par" is short for parallel stream.
+struct timeline_par {
+ bstr init_fragment;
+ bool dash, no_clip, delay_open;
+
+ // Of any of these, _some_ fields are used. If delay_open==true, this
+ // describes each sub-track, and the codec info is used.
+ // In both cases, the metadata is mapped to actual tracks in specific ways.
+ struct sh_stream **sh_meta;
+ int num_sh_meta;
+
+ // Segments to play, ordered by time.
+ struct timeline_part *parts;
+ int num_parts;
+
+ // Which source defines the overall track list (over the full timeline).
+ struct demuxer *track_layout;
+};
+
+struct timeline {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct mp_cancel *cancel;
+
+ bool is_network, is_streaming;
+ int stream_origin;
+ const char *format;
+
+ // main source, and all other sources (this usually only has special meaning
+ // for memory management; mostly compensates for the lack of refcounting)
+ struct demuxer *demuxer;
+ struct demuxer **sources;
+ int num_sources;
+
+ // Description of timeline ranges, possibly multiple parallel ones.
+ struct timeline_par **pars;
+ int num_pars;
+
+ struct demux_chapter *chapters;
+ int num_chapters;
+
+ // global tags, attachments, editions
+ struct demuxer *meta;
+};
+
+struct timeline *timeline_load(struct mpv_global *global, struct mp_log *log,
+ struct demuxer *demuxer);
+void timeline_destroy(struct timeline *tl);
+
+#endif
diff --git a/etc/_mpv.zsh b/etc/_mpv.zsh
new file mode 100644
index 0000000..c34a381
--- /dev/null
+++ b/etc/_mpv.zsh
@@ -0,0 +1,265 @@
+#compdef mpv
+
+# ZSH completion for mpv
+#
+# For customization, see:
+# https://github.com/mpv-player/mpv/wiki/Zsh-completion-customization
+
+#
+# This file is part of mpv.
+#
+# mpv is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# mpv is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+#
+
+local curcontext="$curcontext" state state_descr line
+typeset -A opt_args
+
+local -a match mbegin mend
+local MATCH MBEGIN MEND
+
+# By default, don't complete URLs unless no files match
+local -a tag_order
+zstyle -a ":completion:*:*:$service:*" tag-order tag_order ||
+ zstyle ":completion:*:*:$service:*" tag-order '!urls'
+
+typeset -ga _mpv_completion_arguments _mpv_completion_protocols
+
+function _mpv_generate_arguments {
+
+ _mpv_completion_arguments=()
+
+ local -a option_aliases=()
+
+ local list_options_line
+ for list_options_line in "${(@f)$($~words[1] --no-config --list-options)}"; do
+
+ [[ $list_options_line =~ $'^[ \t]+--([^ \t]+)[ \t]*(.*)' ]] || continue
+
+ local name=$match[1] desc=$match[2]
+
+ if [[ $desc == Flag* ]]; then
+
+ _mpv_completion_arguments+="$name"
+ if [[ $name != (\{|\}|v|list-options) ]]; then
+ # Negated version
+ _mpv_completion_arguments+="no-$name"
+ fi
+
+ elif [[ -z $desc ]]; then
+
+ # Sub-option for list option
+
+ if [[ $name == *-(clr|help) ]]; then
+ # Like a flag
+ _mpv_completion_arguments+="$name"
+ else
+ # Find the parent option and use that with this option's name
+ _mpv_completion_arguments+="${_mpv_completion_arguments[(R)${name%-*}=*]/*=/$name=}"
+ fi
+
+ elif [[ $desc == Print* ]]; then
+
+ _mpv_completion_arguments+="$name"
+
+ elif [[ $desc =~ $'^alias for (--)?([^ \t]+)' ]]; then
+
+ # Save this for later; we might not have parsed the target option yet
+ option_aliases+="$name $match[2]"
+
+ elif [[ $desc =~ $'^removed ' ]]; then
+
+ # skip
+
+ else
+
+ # Option takes argument
+
+ local entry="$name=-:${desc//:/\\:}:"
+
+ if [[ $desc =~ '^Choices: ([^(]*)' ]]; then
+
+ local -a choices=(${(s: :)match[1]})
+ entry+="($choices)"
+ # If "no" is one of the choices, it can also be negated like a flag
+ # (--no-whatever is equivalent to --whatever=no).
+ if (( ${+choices[(r)no]} )); then
+ _mpv_completion_arguments+="no-$name"
+ fi
+
+ elif [[ $desc == *'[file]'* ]]; then
+
+ entry+='->files'
+
+ elif [[ $name == (ao|vo|af|vf|profile|audio-device|vulkan-device) ]]; then
+
+ entry+="->parse-help-$name"
+
+ elif [[ $name == show-profile ]]; then
+
+ entry+="->parse-help-profile"
+
+ elif [[ $name == h(|elp) ]]; then
+
+ entry+="->help-options"
+
+ fi
+
+ _mpv_completion_arguments+="$entry"
+
+ fi
+
+ done
+
+ # Process aliases
+ local to_from real_name arg_spec
+ for to_from in $option_aliases; do
+ # to_from='alias-name real-name'
+ real_name=${to_from##* }
+ for arg_spec in "$real_name" "$real_name=*" "no-$real_name"; do
+ arg_spec=${_mpv_completion_arguments[(r)$arg_spec]}
+ [[ -n $arg_spec ]] &&
+ _mpv_completion_arguments+="${arg_spec/$real_name/${to_from%% *}}"
+ done
+ done
+
+ # Older versions of zsh have a bug where they won't complete an option listed
+ # after one that's a prefix of it. To work around this, we can sort the
+ # options by length, longest first, so that any prefix of an option will be
+ # listed after it. On newer versions of zsh where the bug is fixed, we skip
+ # this to avoid slowing down the first tab press any more than we have to.
+ autoload -Uz is-at-least
+ if ! is-at-least 5.2; then
+ # If this were a real language, we wouldn't have to sort by prepending the
+ # length, sorting the whole thing numerically, and then removing it again.
+ local -a sort_tmp=()
+ for arg_spec in $_mpv_completion_arguments; do
+ sort_tmp+=${#arg_spec%%=*}_$arg_spec
+ done
+ _mpv_completion_arguments=(${${(On)sort_tmp}/#*_})
+ fi
+
+}
+
+function _mpv_generate_protocols {
+ _mpv_completion_protocols=()
+ local list_protos_line
+ for list_protos_line in "${(@f)$($~words[1] --no-config --list-protocols)}"; do
+ if [[ $list_protos_line =~ $'^[ \t]+(.*)' ]]; then
+ _mpv_completion_protocols+="$match[1]"
+ fi
+ done
+}
+
+function _mpv_generate_if_changed {
+ # Called with $1 = 'arguments' or 'protocols'. Generates the respective list
+ # on the first run and re-generates it if the executable being completed for
+ # is different than the one we used to generate the cached list.
+ typeset -gA _mpv_completion_binary
+ local current_binary=${~words[1]:c}
+ zmodload -F zsh/stat b:zstat
+ current_binary+=T$(zstat +mtime $current_binary)
+ if [[ $_mpv_completion_binary[$1] != $current_binary ]]; then
+ # Use PCRE for regular expression matching if possible. This approximately
+ # halves the execution time of generate_arguments compared to the default
+ # POSIX regex, which translates to a more responsive first tab press.
+ # However, we can't rely on PCRE being available, so we keep all our
+ # patterns POSIX-compatible.
+ zmodload -s -F zsh/pcre C:pcre-match && setopt re_match_pcre
+ _mpv_generate_$1
+ _mpv_completion_binary[$1]=$current_binary
+ fi
+}
+
+# Only consider generating arguments if the argument being completed looks like
+# an option. This way, the user should never see a delay when just completing a
+# filename.
+if [[ $words[$CURRENT] == -* ]]; then
+ _mpv_generate_if_changed arguments
+fi
+
+local rc=1
+
+_arguments -C -S \*--$_mpv_completion_arguments '*:files:->mfiles' && rc=0
+
+case $state in
+
+ parse-help-*)
+ local option_name=${state#parse-help-}
+ local no_config="--no-config"
+ # Can't do non-capturing groups without pcre, so we index the ones we want
+ local pattern name_group=1 desc_group=2
+ case $option_name in
+ audio-device|vulkan-device)
+ pattern=$'^[ \t]+'\''([^'\'']*)'\'$'[ \t]+''\((.*)\)'
+ ;;
+ profile)
+ # The generic pattern would actually work in most cases for --profile,
+ # but would break if a profile name contained spaces. This stricter one
+ # only breaks if a profile name contains tabs.
+ pattern=$'^\t([^\t]*)\t(.*)'
+ # We actually want config so we can autocomplete the user's profiles
+ no_config=""
+ ;;
+ *)
+ pattern=$'^[ \t]+(--'${option_name}$'=)?([^ \t]+)[ \t]*[-:]?[ \t]*(.*)'
+ name_group=2 desc_group=3
+ ;;
+ esac
+ local -a values
+ local current
+ for current in "${(@f)$($~words[1] ${no_config} --${option_name}=help)}"; do
+ [[ $current =~ $pattern ]] || continue;
+ local name=${match[name_group]//:/\\:} desc=${match[desc_group]}
+ if [[ -n $desc ]]; then
+ values+="${name}:${desc}"
+ else
+ values+="${name}"
+ fi
+ done
+ (( $#values )) && {
+ compset -P '*,'
+ compset -S ',*'
+ _describe "$state_descr" values -r ',=: \t\n\-' && rc=0
+ }
+ ;;
+
+ files)
+ compset -P '*,'
+ compset -S ',*'
+ _files -r ',/ \t\n\-' && rc=0
+ ;;
+
+ mfiles)
+ local expl
+ _tags files urls
+ while _tags; do
+ _requested files expl 'media file' _files && rc=0
+ if _requested urls; then
+ while _next_label urls expl URL; do
+ _urls "$expl[@]" && rc=0
+ _mpv_generate_if_changed protocols
+ compadd -S '' "$expl[@]" $_mpv_completion_protocols && rc=0
+ done
+ fi
+ (( rc )) || return 0
+ done
+ ;;
+
+ help-options)
+ compadd ${${${_mpv_completion_arguments%%=*}:#no-*}:#*-(add|append|clr|pre|set|remove|toggle)}
+ ;;
+
+esac
+
+return rc
diff --git a/etc/builtin.conf b/etc/builtin.conf
new file mode 100644
index 0000000..7bfbace
--- /dev/null
+++ b/etc/builtin.conf
@@ -0,0 +1,80 @@
+# This file is baked into the mpv binary at compile time, and automatically
+# loaded at early initialization time. Some of the profiles are automatically
+# applied at later stages during loading.
+
+# Note: this contains profiles only. The option defaults for normal options
+# (i.e. the default profile) are defined in C code. Do NOT set any
+# options in the default profile here. It won't work correctly in subtle
+# ways.
+#
+# To see the normal option defaults, run: mpv --list-options
+
+[pseudo-gui]
+player-operation-mode=pseudo-gui
+
+[builtin-pseudo-gui]
+terminal=no
+force-window=yes
+idle=once
+screenshot-dir=~~desktop/
+
+[libmpv]
+config=no
+idle=yes
+terminal=no
+input-terminal=no
+osc=no
+input-default-bindings=no
+input-vo-keyboard=no
+# OSX/Cocoa global input hooks
+input-media-keys=no
+
+[encoding]
+vo=lavc
+ao=lavc
+keep-open=no
+force-window=no
+gapless-audio=yes
+resume-playback=no
+load-scripts=no
+osc=no
+framedrop=no
+
+[fast]
+scale=bilinear
+dscale=bilinear
+dither=no
+correct-downscaling=no
+linear-downscaling=no
+sigmoid-upscaling=no
+hdr-compute-peak=no
+allow-delayed-peak-detect=yes
+
+[high-quality]
+scale=ewa_lanczossharp
+hdr-peak-percentile=99.995
+hdr-contrast-recovery=0.30
+deband=yes
+
+# Deprecated alias
+[gpu-hq]
+profile=high-quality
+
+[low-latency]
+audio-buffer=0 # minimize extra audio buffer (can lead to dropouts)
+vd-lavc-threads=1 # multithreaded decoding buffers extra frames
+cache-pause=no # do not pause on underruns
+demuxer-lavf-o-add=fflags=+nobuffer # can help for weird reasons
+demuxer-lavf-probe-info=nostreams # avoid probing unless absolutely needed
+demuxer-lavf-analyzeduration=0.1 # if it probes, reduce it
+video-sync=audio # DS currently requires reading ahead a frame
+interpolation=no # requires reference frames (more buffering)
+video-latency-hacks=yes # typically 1 or 2 video frame less latency
+stream-buffer-size=4k # minimal buffer size; normally not needed
+
+[sw-fast]
+# For VOs which use software scalers, also affects screenshots and others.
+sws-scaler=bilinear
+sws-fast=yes
+zimg-scaler=bilinear
+zimg-dither=no
diff --git a/etc/encoding-profiles.conf b/etc/encoding-profiles.conf
new file mode 100644
index 0000000..77a0d91
--- /dev/null
+++ b/etc/encoding-profiles.conf
@@ -0,0 +1,224 @@
+#
+# mpv configuration file
+#
+
+#########################
+# encoding profile file #
+#########################
+#
+# Note: by default, this file is installed to /etc/mpv/encoding-profiles.conf
+# (or a different location, depending on --prefix). mpv will load it by
+# default on program start. If ~/.mpv/encoding-profiles.conf exists, this file
+# will be loaded instead.
+#
+# Then, list all profiles by
+# mpv --profile=help | grep enc-
+#
+# The following kinds of encoding profiles exist:
+# enc-a-*: initialize an audio codec including good defaults
+# enc-v-*: initialize a video codec including good defaults
+# enc-f-*: initialize a file format including good defaults, including
+# selecting and initializing a good audio and video codec
+# enc-to-*: load known good settings for a target device; this typically
+# includes selecting an enc-f-* profile, then adjusting some
+# settings like frame rate, resolution and codec parameters
+#
+# AFTER including a profile of these, you can of course still change
+# options, or even switch to another codec.
+#
+# You can view the exact options a profile sets by
+# mpv --show-profile=enc-to-hp-slate-7
+#
+# Examples:
+# mpv --profile=enc-to-dvdpal --o=outfile.mpg infile.mkv
+# mpv --profile=enc-f-avi --vf=fps=30 --o=outfile.avi infile.mkv
+# mpv --profile=enc-v-mpeg4 --ovcopts-add=qscale=7 --profile=enc-a-mp3 --oacopts-add=b=320k --o=outfile.avi infile.mkv
+
+################
+# audio codecs #
+################
+[enc-a-aac]
+profile-desc = "AAC (libfdk-aac or FFmpeg)"
+oac = libfdk_aac,aac
+oacopts = b=96k
+
+[enc-a-ac3]
+profile-desc = "AC3 (FFmpeg)"
+oac = ac3
+oacopts = b=448k
+
+[enc-a-mp3]
+profile-desc = "MP3 (LAME)"
+oac = libmp3lame
+oacopts = b=128k
+
+[enc-a-vorbis]
+profile-desc = "Vorbis (libvorbis or FFmpeg)"
+oac = libvorbis,vorbis
+oacopts = qscale=3
+
+[enc-a-opus]
+profile-desc = "Opus (libopus or FFmpeg)"
+oac = libopus,opus
+audio-samplerate = 48000
+oacopts = b=96k
+
+################
+# video codecs #
+################
+[enc-v-h263]
+profile-desc = "H.263 (FFmpeg)"
+ovc = h263
+ovcopts = qscale=4
+
+[enc-v-h264]
+profile-desc = "H.264 (x264)"
+ovc = libx264
+ovcopts = preset=medium,crf=23,threads=0
+# If you want to restrict the output video to something compatible with most
+# hardware and basic decoders, add "profile=high,level=41" to ovcopts above and
+# uncomment the following line to avoid errors when source video isn't yuv420p:
+#vf-add = format=yuv420p
+
+[enc-v-mpeg2]
+profile-desc = "MPEG-2 Video (FFmpeg)"
+ovc = mpeg2video
+
+[enc-v-mpeg4]
+profile-desc = "MPEG-4 Part 2 (FFmpeg)"
+ovc = mpeg4
+ovcopts = qscale=4
+
+[enc-v-vp8]
+profile-desc = "VP8 (libvpx)"
+ovc = libvpx
+ovcopts = speed=0,lag-in-frames=8,slices=2,threads=0,b=2M,crf=10,qmin=0,qmax=36
+
+[enc-v-vp9]
+profile-desc = "VP9 (libvpx)"
+ovc = libvpx-vp9
+ovcopts = speed=6,lag-in-frames=8,slices=2,threads=0,crf=18,qmin=0,qmax=36
+
+###########
+# formats #
+###########
+[enc-f-3gp]
+profile-desc = "H.263 + AAC (for 3GP)"
+of = 3gp
+profile = enc-v-h263
+profile = enc-a-aac
+ofopts = ""
+
+[enc-f-avi]
+profile-desc = "MPEG-4 + MP3 (for AVI)"
+of = avi
+profile = enc-v-mpeg4
+profile = enc-a-mp3
+ofopts = ""
+
+[enc-f-mp4]
+profile-desc = "H.264 + AAC (for MP4)"
+of = mp4
+profile = enc-v-h264
+profile = enc-a-aac
+## equivalent to using qt-faststart tool
+## can be used to speed up seeking when streaming
+# ofopts = movflags=+faststart
+
+[enc-f-webm]
+profile-desc = "VP9 + Opus (for WebM)"
+of = webm
+profile = enc-v-vp9
+profile = enc-a-opus
+ofopts = ""
+
+##################
+# target devices #
+##################
+[enc-to-dvdpal]
+profile-desc = "DVD-Video PAL, use dvdauthor -v pal -a ac3+en (MUST be used with 4:3 or 16:9 aspect, and 720x576, 704x576, 352x576 or 352x288 resolution)"
+profile = enc-v-mpeg2
+profile = enc-a-ac3
+of = dvd
+ofopts-append = packetsize=2048
+ofopts-append = muxrate=10080000
+vf-add = fps=25
+audio-samplerate = 48000
+ovcopts = g=15,b=6000000,maxrate=9000000,minrate=0,bufsize=1835008
+
+[enc-to-dvdntsc]
+profile-desc = "DVD-Video NTSC, use dvdauthor -v ntsc -a ac3+en (MUST be used with 4:3 or 16:9 aspect, and 720x480, 704x480, 352x480 or 352x240 resolution)"
+profile = enc-v-mpeg2
+profile = enc-a-ac3
+of = dvd
+ofopts-append = packetsize=2048
+ofopts-append = muxrate=10080000
+vf-add = fps="24000/1001"
+audio-samplerate = 48000
+ovcopts = g=18,b=6000000,maxrate=9000000,minrate=0,bufsize=1835008
+
+[enc-to-nok-n900]
+profile-desc = "MP4 for Nokia N900"
+profile = enc-f-mp4
+# DW = 800, DH = 480, SAR = 1
+vf-add = lavfi=graph="scale=floor(min(min(800\,dar*480)\,in_w*max(1\,sar))/2+0.5)*2:floor(min(min(800/dar\,480)\,in_h*max(1/sar\,1))/2+0.5)*2,setsar=sar=1"
+ovcopts-append = profile=baseline
+ovcopts-append = level=30
+ovcopts-append = maxrate=10000k
+ovcopts-append = bufsize=10000k
+ovcopts-append = rc_init_occupancy=9000k
+ovcopts-append = refs=5
+
+[enc-to-nok-6300]
+profile-desc = "3GP for Nokia 6300"
+profile = enc-f-3gp
+vf-add = fps=25
+vf-add = lavfi=graph="scale=176:144"
+audio-samplerate = 16000
+audio-channels = 1
+oacopts-add = b=32k
+
+[enc-to-hp-slate-7]
+profile-desc = "MP4 for HP Slate 7 (1024x600, crazy aspect)"
+profile = enc-f-mp4
+ovcopts-add = profile=high
+# DW = 1024, DH = 600, DAR = 97:54 (=> SAR = 2425:2304)
+vf-add = lavfi=graph="scale=floor(min(1024*min(1\,dar/(97/54))\,in_w)/2+0.5)*2:floor(min(600*min((97/54)/dar\,1)\,in_h)/2+0.5)*2,setsar=sar=sar/(2425/2304)"
+
+# Advanced scaling for specific output devices - how it works:
+# DW = display width (px) (1024)
+# DH = display height (px) (600)
+# SAR = display sample aspect ratio, i.e. DAR * DH / DW (2425:2304)
+# DAR = display aspect ratio, i.e. SAR * DW / DH (97:54)
+# Variant: zoomed out
+# vf-add = lavfi=graph="scale=floor(min(DW*min(1\,dar/DAR)\,in_w*max(1\,sar/SAR))/2+0.5)*2:floor(min(DH*min(DAR/dar\,1)\,in_h*max(SAR/sar\,1))/2+0.5)*2,setsar=sar=1"
+# Variant: zoomed in
+# vf-add = lavfi=graph="scale=floor(min(DW*max(1\,dar/DAR)\,in_w*max(1\,sar/SAR))/2+0.5)*2:floor(min(DH*max(DAR/dar\,1)\,in_h*max(SAR/sar\,1))/2+0.5)*2,setsar=sar=1"
+# How it works:
+# 1a: DW, DH*dar/DAR - fit to display width
+# 1b: DH*DAR/dar, DH - fit to display height
+# 1: the min of 1a and 1b these (i.e. fit inside both width and height); for zoomed in view, use the max
+# 2a: in_w, in_h*SAR/sar - fit to original width
+# 2b: in_w*sar/SAR, in_h - fit to original height
+# 2: the max of 2a and 2b (i.e. avoid enlarging both dimensions - let HW scaling handle this)
+# output: the min of 1 and 2 (i.e. fulfill both constraints)
+# setsar=sar=1 to prevent scaling on the device (skip this if the device actually wants the proper SAR to be specified for not performing needless scaling)
+#
+# Simplified special case for SAR == 1, DAR == DW/DH:
+# Variant: zoomed out
+# vf-add = lavfi=graph="scale=floor(min(min(DW\,dar*DH)\,in_w*max(1\,sar))/2+0.5)*2:floor(min(min(DW/dar\,DH)\,in_h*max(1/sar\,1))/2+0.5)*2,setsar=sar=1"
+# Variant: zoomed in
+# vf-add = lavfi=graph="scale=floor(min(max(DW\,dar*DH)\,in_w*max(1\,sar))/2+0.5)*2:floor(min(max(DW/dar\,DH)\,in_h*max(1/sar\,1))/2+0.5)*2,setsar=sar=1"
+# setsar=sar=1 to prevent nasty almost-1 SAR to be passed to the codec due to the rounding which can fail
+#
+# If the device supports file SAR properly, we can make use of it to avoid
+# upscaling. The setsar=sar=sar/SAR at the end serves to fake the SAR for devices that don't know their own display's SAR.
+# Variant: zoomed out
+# vf-add = lavfi=graph="scale=floor(min(DW*min(1\,dar/DAR)\,in_w)/2+0.5)*2:floor(min(DH*min(DAR/dar\,1)\,in_h)/2+0.5)*2,setsar=sar=sar/SAR"
+# Variant: zoomed in
+# vf-add = lavfi=graph="scale=floor(min(DW*max(1\,dar/DAR)\,in_w)/2+0.5)*2:floor(min(DH*max(DAR/dar\,1)\,in_h)/2+0.5)*2,setsar=sar=sar/SAR"
+# Simplified special case for SAR == 1, DAR == DW/DH:
+# Variant: zoomed out
+# vf-add = lavfi=graph="scale=floor(min(min(DW\,dar*DH)\,in_w)/2+0.5)*2:floor(min(min(DW/dar\,DH)\,in_h)/2+0.5)*2"
+# Variant: zoomed in
+# vf-add = lavfi=graph="scale=floor(min(max(DW\,dar*DH)\,in_w)/2+0.5)*2:floor(min(max(DW/dar\,DH)\,in_h)/2+0.5)*2"
diff --git a/etc/input.conf b/etc/input.conf
new file mode 100644
index 0000000..0b0e6da
--- /dev/null
+++ b/etc/input.conf
@@ -0,0 +1,181 @@
+# mpv keybindings
+#
+# Location of user-defined bindings: ~/.config/mpv/input.conf
+#
+# Lines starting with # are comments. Use SHARP to assign the # key.
+# Copy this file and uncomment and edit the bindings you want to change.
+#
+# List of commands and further details: DOCS/man/input.rst
+# List of special keys: --input-keylist
+# Keybindings testing mode: mpv --input-test --force-window --idle
+#
+# Use 'ignore' to unbind a key fully (e.g. 'ctrl+a ignore').
+#
+# Strings need to be quoted and escaped:
+# KEY show-text "This is a single backslash: \\ and a quote: \" !"
+#
+# You can use modifier-key combinations like Shift+Left or Ctrl+Alt+x with
+# the modifiers Shift, Ctrl, Alt and Meta (may not work on the terminal).
+#
+# The default keybindings are hardcoded into the mpv binary.
+# You can disable them completely with: --no-input-default-bindings
+
+# Developer note:
+# On compilation, this file is baked into the mpv binary, and all lines are
+# uncommented (unless '#' is followed by a space) - thus this file defines the
+# default key bindings.
+
+# If this is enabled, treat all the following bindings as default.
+#default-bindings start
+
+#MBTN_LEFT ignore # don't do anything
+#MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen
+#MBTN_RIGHT cycle pause # toggle pause/playback mode
+#MBTN_BACK playlist-prev # skip to the previous file
+#MBTN_FORWARD playlist-next # skip to the next file
+
+# Mouse wheels, touchpad or other input devices that have axes
+# if the input devices supports precise scrolling it will also scale the
+# numeric value accordingly
+#WHEEL_UP add volume 2
+#WHEEL_DOWN add volume -2
+#WHEEL_LEFT seek -10 # seek 10 seconds backward
+#WHEEL_RIGHT seek 10 # seek 10 seconds forward
+
+## Seek units are in seconds, but note that these are limited by keyframes
+#RIGHT seek 5 # seek 5 seconds forward
+#LEFT seek -5 # seek 5 seconds backward
+#UP seek 60 # seek 1 minute forward
+#DOWN seek -60 # seek 1 minute backward
+# Do smaller, always exact (non-keyframe-limited), seeks with shift.
+# Don't show them on the OSD (no-osd).
+#Shift+RIGHT no-osd seek 1 exact # seek exactly 1 second forward
+#Shift+LEFT no-osd seek -1 exact # seek exactly 1 second backward
+#Shift+UP no-osd seek 5 exact # seek exactly 5 seconds forward
+#Shift+DOWN no-osd seek -5 exact # seek exactly 5 seconds backward
+#Ctrl+LEFT no-osd sub-seek -1 # seek to the previous subtitle
+#Ctrl+RIGHT no-osd sub-seek 1 # seek to the next subtitle
+#Ctrl+Shift+LEFT sub-step -1 # change subtitle timing such that the previous subtitle is displayed
+#Ctrl+Shift+RIGHT sub-step 1 # change subtitle timing such that the next subtitle is displayed
+#Alt+left add video-pan-x 0.1 # move the video right
+#Alt+right add video-pan-x -0.1 # move the video left
+#Alt+up add video-pan-y 0.1 # move the video down
+#Alt+down add video-pan-y -0.1 # move the video up
+#Alt++ add video-zoom 0.1 # zoom in
+#ZOOMIN add video-zoom 0.1 # zoom in
+#Alt+- add video-zoom -0.1 # zoom out
+#ZOOMOUT add video-zoom -0.1 # zoom out
+#Alt+BS set video-zoom 0 ; set video-pan-x 0 ; set video-pan-y 0 # reset zoom and pan settings
+#PGUP add chapter 1 # seek to the next chapter
+#PGDWN add chapter -1 # seek to the previous chapter
+#Shift+PGUP seek 600 # seek 10 minutes forward
+#Shift+PGDWN seek -600 # seek 10 minutes backward
+#[ multiply speed 1/1.1 # decrease the playback speed
+#] multiply speed 1.1 # increase the playback speed
+#{ multiply speed 0.5 # halve the playback speed
+#} multiply speed 2.0 # double the playback speed
+#BS set speed 1.0 # reset the speed to normal
+#Shift+BS revert-seek # undo the previous (or marked) seek
+#Shift+Ctrl+BS revert-seek mark # mark the position for revert-seek
+#q quit
+#Q quit-watch-later # exit and remember the playback position
+#q {encode} quit 4
+#ESC set fullscreen no # leave fullscreen
+#ESC {encode} quit 4
+#p cycle pause # toggle pause/playback mode
+#. frame-step # advance one frame and pause
+#, frame-back-step # go back by one frame and pause
+#SPACE cycle pause # toggle pause/playback mode
+#> playlist-next # skip to the next file
+#ENTER playlist-next # skip to the next file
+#< playlist-prev # skip to the previous file
+#O no-osd cycle-values osd-level 3 1 # toggle displaying the OSD on user interaction or always
+#o show-progress # show playback progress
+#P show-progress # show playback progress
+#i script-binding stats/display-stats # display information and statistics
+#I script-binding stats/display-stats-toggle # toggle displaying information and statistics
+#` script-binding console/enable # open the console
+#z add sub-delay -0.1 # shift subtitles 100 ms earlier
+#Z add sub-delay +0.1 # delay subtitles by 100 ms
+#x add sub-delay +0.1 # delay subtitles by 100 ms
+#ctrl++ add audio-delay 0.100 # change audio/video sync by delaying the audio
+#ctrl+- add audio-delay -0.100 # change audio/video sync by shifting the audio earlier
+#Shift+g add sub-scale +0.1 # increase the subtitle font size
+#Shift+f add sub-scale -0.1 # decrease the subtitle font size
+#9 add volume -2
+#/ add volume -2
+#0 add volume 2
+#* add volume 2
+#m cycle mute # toggle mute
+#1 add contrast -1
+#2 add contrast 1
+#3 add brightness -1
+#4 add brightness 1
+#5 add gamma -1
+#6 add gamma 1
+#7 add saturation -1
+#8 add saturation 1
+#Alt+0 set current-window-scale 0.5 # halve the window size
+#Alt+1 set current-window-scale 1.0 # reset the window size
+#Alt+2 set current-window-scale 2.0 # double the window size
+#d cycle deinterlace # toggle the deinterlacing filter
+#r add sub-pos -1 # move subtitles up
+#R add sub-pos +1 # move subtitles down
+#t add sub-pos +1 # move subtitles down
+#v cycle sub-visibility # hide or show the subtitles
+#Alt+v cycle secondary-sub-visibility # hide or show the secondary subtitles
+#V cycle sub-ass-vsfilter-aspect-compat # toggle stretching SSA/ASS subtitles with anamorphic videos to match the historical renderer
+#u cycle-values sub-ass-override "force" "yes" # toggle overriding SSA/ASS subtitle styles with the normal styles
+#j cycle sub # switch subtitle track
+#J cycle sub down # switch subtitle track backwards
+#SHARP cycle audio # switch audio track
+#_ cycle video # switch video track
+#T cycle ontop # toggle placing the video on top of other windows
+#f cycle fullscreen # toggle fullscreen
+#s screenshot # take a screenshot of the video in its original resolution with subtitles
+#S screenshot video # take a screenshot of the video in its original resolution without subtitles
+#Ctrl+s screenshot window # take a screenshot of the window with OSD and subtitles
+#Alt+s screenshot each-frame # automatically screenshot every frame; issue this command again to stop taking screenshots
+#w add panscan -0.1 # decrease panscan
+#W add panscan +0.1 # shrink black bars by cropping the video
+#e add panscan +0.1 # shrink black bars by cropping the video
+#A cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" # cycle the video aspect ratio ("-1" is the container aspect)
+#POWER quit
+#PLAY cycle pause # toggle pause/playback mode
+#PAUSE cycle pause # toggle pause/playback mode
+#PLAYPAUSE cycle pause # toggle pause/playback mode
+#PLAYONLY set pause no # unpause
+#PAUSEONLY set pause yes # pause
+#STOP quit
+#FORWARD seek 60 # seek 1 minute forward
+#REWIND seek -60 # seek 1 minute backward
+#NEXT playlist-next # skip to the next file
+#PREV playlist-prev # skip to the previous file
+#VOLUME_UP add volume 2
+#VOLUME_DOWN add volume -2
+#MUTE cycle mute # toggle mute
+#CLOSE_WIN quit
+#CLOSE_WIN {encode} quit 4
+#ctrl+w quit
+#E cycle edition # switch edition
+#l ab-loop # set/clear A-B loop points
+#L cycle-values loop-file "inf" "no" # toggle infinite looping
+#ctrl+c quit 4
+#DEL script-binding osc/visibility # cycle OSC visibility between never, auto (mouse-move) and always
+#ctrl+h cycle-values hwdec "auto-safe" "no" # toggle hardware decoding
+#F8 show-text ${playlist} # show the playlist
+#F9 show-text ${track-list} # show the list of video, audio and sub tracks
+
+#
+# Legacy bindings (may or may not be removed in the future)
+#
+#! add chapter -1 # seek to the previous chapter
+#@ add chapter 1 # seek to the next chapter
+
+#
+# Not assigned by default
+# (not an exhaustive list of unbound commands)
+#
+
+# ? cycle sub-forced-events-only # display only DVD/PGS forced subtitle events
+# ? stop # stop playback (quit or enter idle mode)
diff --git a/etc/meson.build b/etc/meson.build
new file mode 100644
index 0000000..12fe732
--- /dev/null
+++ b/etc/meson.build
@@ -0,0 +1,20 @@
+icons = ['16', '32', '64', '128']
+foreach size: icons
+ name = 'mpv-icon-8bit-'+size+'x'+size+'.png'
+ icon = custom_target(name,
+ input: join_paths(source_root, 'etc', name),
+ output: name + '.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+ )
+ sources += icon
+endforeach
+
+etc_files = ['input.conf', 'builtin.conf']
+foreach file: etc_files
+ etc_file = custom_target(file,
+ input: join_paths(source_root, 'etc', file),
+ output: file + '.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+ )
+ sources += etc_file
+endforeach
diff --git a/etc/mplayer-input.conf b/etc/mplayer-input.conf
new file mode 100644
index 0000000..2d23e47
--- /dev/null
+++ b/etc/mplayer-input.conf
@@ -0,0 +1,93 @@
+##
+## MPlayer-style key bindings
+##
+## Save it as ~/.config/mpv/input.conf to use it.
+##
+## Generally, it's recommended to use this as reference-only.
+##
+
+RIGHT seek +10
+LEFT seek -10
+DOWN seek -60
+UP seek +60
+PGUP seek 600
+PGDWN seek -600
+m cycle mute
+SHARP cycle audio # switch audio streams
++ add audio-delay 0.100
+= add audio-delay 0.100
+- add audio-delay -0.100
+[ multiply speed 0.9091 # scale playback speed
+] multiply speed 1.1
+{ multiply speed 0.5
+} multiply speed 2.0
+BS set speed 1.0 # reset speed to normal
+q quit
+ESC quit
+ENTER playlist-next force # skip to next file
+p cycle pause
+. frame-step # advance one frame and pause
+SPACE cycle pause
+HOME set playlist-pos 0 # not the same as MPlayer
+#END pt_up_step -1
+> playlist-next # skip to next file
+< playlist-prev # previous
+#INS alt_src_step 1
+#DEL alt_src_step -1
+o osd
+I show-text "${filename}" # display filename in osd
+P show-progress
+z add sub-delay -0.1 # subtract 100 ms delay from subs
+x add sub-delay +0.1 # add
+9 add volume -1
+/ add volume -1
+0 add volume 1
+* add volume 1
+1 add contrast -1
+2 add contrast 1
+3 add brightness -1
+4 add brightness 1
+5 add hue -1
+6 add hue 1
+7 add saturation -1
+8 add saturation 1
+( add balance -0.1 # adjust audio balance in favor of left
+) add balance +0.1 # right
+d cycle framedrop
+D cycle deinterlace # toggle deinterlacer (auto-inserted filter)
+r add sub-pos -1 # move subtitles up
+t add sub-pos +1 # down
+#? sub-step +1 # immediately display next subtitle
+#? sub-step -1 # previous
+#? add sub-scale +0.1 # increase subtitle font size
+#? add sub-scale -0.1 # decrease subtitle font size
+f cycle fullscreen
+T cycle ontop # toggle video window ontop of other windows
+w add panscan -0.1 # zoom out with -panscan 0 -fs
+e add panscan +0.1 # in
+c cycle stream-capture # save (and append) file/stream to stream.dump with -capture
+s screenshot # take a screenshot (if you want PNG, use "--screenshot-format=png")
+S screenshot - each-frame # S will take a png screenshot of every frame
+
+h cycle tv-channel 1
+l cycle tv-channel -1
+n cycle tv-norm
+#b tv_step_chanlist
+
+#? add chapter -1 # skip to previous dvd chapter
+#? add chapter +1 # next
+
+##
+## Advanced seek
+## Uncomment the following lines to be able to seek to n% of the media with
+## the Fx keys.
+##
+#F1 seek 10 absolute-percent
+#F2 seek 20 absolute-percent
+#F3 seek 30 absolute-percent
+#F4 seek 40 absolute-percent
+#F5 seek 50 absolute-percent
+#F6 seek 60 absolute-percent
+#F7 seek 70 absolute-percent
+#F8 seek 80 absolute-percent
+#F9 seek 90 absolute-percent
diff --git a/etc/mpv-gradient.svg b/etc/mpv-gradient.svg
new file mode 100644
index 0000000..c40fe71
--- /dev/null
+++ b/etc/mpv-gradient.svg
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 63.999999 63.999999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="mpv-gradient.svg">
+ <defs
+ id="defs4">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4430">
+ <stop
+ style="stop-color:#65376e;stop-opacity:1"
+ offset="0"
+ id="stop4432" />
+ <stop
+ style="stop-color:#4c2354;stop-opacity:1"
+ offset="1"
+ id="stop4434" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4418"
+ inkscape:collect="always">
+ <stop
+ id="stop4420"
+ offset="0"
+ style="stop-color:#e6e2e8;stop-opacity:1" />
+ <stop
+ id="stop4422"
+ offset="1"
+ style="stop-color:#aaa3ad;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4402">
+ <stop
+ style="stop-color:#320f38;stop-opacity:1"
+ offset="0"
+ id="stop4404" />
+ <stop
+ style="stop-color:#5a2963;stop-opacity:1"
+ offset="1"
+ id="stop4406" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4392">
+ <stop
+ style="stop-color:#6b3c74;stop-opacity:1"
+ offset="0"
+ id="stop4394" />
+ <stop
+ style="stop-color:#461b4d;stop-opacity:1"
+ offset="1"
+ id="stop4396" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4382">
+ <stop
+ style="stop-color:#fafafa;stop-opacity:1"
+ offset="0"
+ id="stop4384" />
+ <stop
+ style="stop-color:#bababa;stop-opacity:1"
+ offset="1"
+ id="stop4386" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4382"
+ id="linearGradient5278"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-86.873198,699.81912)"
+ x1="97.187729"
+ y1="305.67371"
+ x2="140.38228"
+ y2="335.64926" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4392"
+ id="linearGradient5280"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-32.973108,701.2155)"
+ x1="50.828064"
+ y1="298.31949"
+ x2="78.197021"
+ y2="339.65219" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4402"
+ id="linearGradient5282"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-31.48364,700.09839)"
+ x1="53.620815"
+ y1="305.76682"
+ x2="77.824654"
+ y2="331.73938" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4418"
+ id="linearGradient5284"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-87.152474,700.0053)"
+ x1="110.31366"
+ y1="312.28323"
+ x2="126.88398"
+ y2="329.41211" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4430"
+ id="linearGradient5286"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-87.152474,700.0053)"
+ x1="115.97468"
+ y1="317.41763"
+ x2="119.37984"
+ y2="322.43457" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.3710484"
+ inkscape:cx="-19.490294"
+ inkscape:cy="18.643164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="1920"
+ inkscape:window-height="1016"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.3622)">
+ <circle
+ style="opacity:1;fill:url(#linearGradient5278);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10161044;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ id="path4380-0"
+ cx="32"
+ cy="1020.3622"
+ r="27.949194" />
+ <circle
+ style="opacity:1;fill:url(#linearGradient5280);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0988237;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ id="path4390-3"
+ cx="32.727058"
+ cy="1019.5079"
+ r="25.950588" />
+ <circle
+ style="opacity:1;fill:url(#linearGradient5282);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ id="path4400-3"
+ cx="34.224396"
+ cy="1017.7957"
+ r="20" />
+ <path
+ style="fill:url(#linearGradient5284);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 44.481446,1020.4807 a 12.848894,12.848894 0 0 1 -12.84889,12.8489 12.848894,12.848894 0 0 1 -12.8489,-12.8489 12.848894,12.848894 0 0 1 12.8489,-12.8489 12.848894,12.848894 0 0 1 12.84889,12.8489 z"
+ id="path4412-5"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:url(#linearGradient5286);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 28.374316,1014.709 0,11.4502 9.21608,-5.8647 z"
+ id="path4426-2"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/etc/mpv-icon-8bit-128x128.png b/etc/mpv-icon-8bit-128x128.png
new file mode 100644
index 0000000..1840269
--- /dev/null
+++ b/etc/mpv-icon-8bit-128x128.png
Binary files differ
diff --git a/etc/mpv-icon-8bit-16x16.png b/etc/mpv-icon-8bit-16x16.png
new file mode 100644
index 0000000..ac2cb81
--- /dev/null
+++ b/etc/mpv-icon-8bit-16x16.png
Binary files differ
diff --git a/etc/mpv-icon-8bit-32x32.png b/etc/mpv-icon-8bit-32x32.png
new file mode 100644
index 0000000..bfb5f9c
--- /dev/null
+++ b/etc/mpv-icon-8bit-32x32.png
Binary files differ
diff --git a/etc/mpv-icon-8bit-64x64.png b/etc/mpv-icon-8bit-64x64.png
new file mode 100644
index 0000000..46bb33d
--- /dev/null
+++ b/etc/mpv-icon-8bit-64x64.png
Binary files differ
diff --git a/etc/mpv-icon.ico b/etc/mpv-icon.ico
new file mode 100644
index 0000000..5467e4e
--- /dev/null
+++ b/etc/mpv-icon.ico
Binary files differ
diff --git a/etc/mpv-symbolic.svg b/etc/mpv-symbolic.svg
new file mode 100644
index 0000000..a4f9263
--- /dev/null
+++ b/etc/mpv-symbolic.svg
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 63.999999 63.999999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="mpv-symbolic.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.3710484"
+ inkscape:cx="19.094402"
+ inkscape:cy="44.778512"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="1920"
+ inkscape:window-height="1016"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.3622)">
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10161044;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ d="M 32.582031 2.9101562 A 27.949194 27.949194 0 0 0 4.6328125 30.859375 A 27.949194 27.949194 0 0 0 32.582031 58.808594 A 27.949194 27.949194 0 0 0 60.53125 30.859375 A 27.949194 27.949194 0 0 0 32.582031 2.9101562 z M 33.308594 4.0546875 A 25.950588 25.950588 0 0 1 59.259766 30.005859 A 25.950588 25.950588 0 0 1 33.308594 55.955078 A 25.950588 25.950588 0 0 1 7.359375 30.005859 A 25.950588 25.950588 0 0 1 33.308594 4.0546875 z "
+ transform="translate(0,988.3622)"
+ id="path4380" />
+ <path
+ id="path4412"
+ transform="translate(0,988.3622)"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 32.214844,18.128906 a 12.848894,12.848894 0 0 0 -12.84961,12.84961 12.848894,12.848894 0 0 0 12.84961,12.847656 12.848894,12.848894 0 0 0 12.849609,-12.847656 12.848894,12.848894 0 0 0 -12.849609,-12.84961 z m -3.257813,7.078125 9.214844,5.583985 -9.214844,5.865234 0,-11.449219 z M 54.806488,28.2928 a 20,20 0 0 1 -20,20 20,20 0 0 1 -20,-20 20,20 0 0 1 20,-19.99997 20,20 0 0 1 20,19.99997 z" />
+ </g>
+</svg>
diff --git a/etc/mpv.bash-completion b/etc/mpv.bash-completion
new file mode 100644
index 0000000..d5d504a
--- /dev/null
+++ b/etc/mpv.bash-completion
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+#
+# This file is part of mpv.
+#
+# mpv is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# mpv is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Cache all the mpv options
+_mpv_options=$(mpv --no-config --list-options)
+
+_mpv_get_args()
+{
+ local doc=$(echo "$_mpv_options" | grep -E "^\\s*$1\\s")
+ local partial="$2"
+ local type=$(echo "$doc" | awk '{print $2;}')
+
+ # We special-case profiles to ensure we read the config
+ if [ "$1" = "--show-profile" ]; then
+ type="ShowProfile"
+ elif [ "$1" = "--profile" ]; then
+ type="Profile"
+ fi
+
+ declare -a candidates
+ case $type in
+ String)
+ if echo "$doc" | grep -q '\[file\]' ; then
+ if [ "$cur" = '=' ]; then
+ # Without this, _filedir will try and complete files starting with '='
+ cur=""
+ fi
+ _filedir 2>/dev/null || COMPREPLY=($(compgen -f))
+ return 0
+ else
+ candidates=($(mpv --no-config $1=help | grep -v ':' | awk '{print $1;}'))
+ candidates+=("help")
+ fi
+ ;;
+ Flag)
+ candidates=("yes" "no" "help")
+ ;;
+ Choices:|Object)
+ candidates=($(mpv --no-config $1=help | grep -v ':' | awk '{print $1;}'))
+ candidates+=("help")
+ ;;
+ Image)
+ candidates=($(mpv --no-config $1=help))
+ candidates=("${candidates[@]:2}")
+ candidates+=("help")
+ ;;
+ Profile)
+ candidates=($(mpv $1=help | grep -v ':' | awk '{print $1;}'))
+ candidates+=("help")
+ ;;
+ ShowProfile)
+ candidates=($(mpv $1= | grep -v ':' | awk '{print $1;}'))
+ ;;
+ *)
+ # There are other categories; some of which we could do something smarter
+ # about, with enough work.
+ ;;
+ esac
+ COMPREPLY=($(compgen -W "${candidates[*]}" -- "${partial}"))
+ if [ ${#COMPREPLY[@]} -gt 1 ]; then
+ compopt -o nospace mpv
+ fi
+}
+
+# This regex detects special options where we don't want an '=' appended
+_mpv_special_regex='\s(Flag.*\[not in config files\]|Print)'
+_mpv_skip_regex='\sremoved \[deprecated\]'
+_mpv_regular_options=($(echo "$_mpv_options" | grep -vE "$_mpv_skip_regex" | \
+ grep -vE "$_mpv_special_regex" | awk '{print "\\"$1;}' | grep '\--'))
+_mpv_special_options=($(echo "$_mpv_options" | grep -vE "$_mpv_skip_regex" | \
+ grep -E "$_mpv_special_regex" | awk '{print "\\"$1;}' | grep '\--'))
+
+_mpv()
+{
+ compopt +o nospace mpv
+
+ # _filedir requires the current candidate be in $cur
+ local cur=${COMP_WORDS[COMP_CWORD]}
+ local prev=${COMP_WORDS[((COMP_CWORD - 1))]}
+
+ if [ "$cur" = '=' ]; then
+ # If the current word is '=' then we are looking for an argument for the
+ # option specified by the previous word.
+ _mpv_get_args "$prev"
+ elif [ "$prev" = '=' ]; then
+ # If the previous word is '=' then we are completing an argument for the
+ # option specified by the word before the '='.
+ local prevprev=${COMP_WORDS[((COMP_CWORD - 2))]}
+ _mpv_get_args "$prevprev" "$cur"
+ else
+ case $cur in
+ -*)
+ COMPREPLY=($(compgen -W "${_mpv_regular_options[*]}" -S '=' -- "${cur}"))
+ local normal_count=${#COMPREPLY[@]}
+ COMPREPLY+=($(compgen -W "${_mpv_special_options[*]}" -- "${cur}"))
+ if [ $normal_count -gt 0 -o ${#COMPREPLY[@]} -gt 1 ]; then
+ compopt -o nospace mpv
+ fi
+ ;;
+ *)
+ _filedir 2>/dev/null || COMPREPLY=($(compgen -f))
+ ;;
+ esac
+ fi
+}
+
+complete -F _mpv mpv
diff --git a/etc/mpv.conf b/etc/mpv.conf
new file mode 100644
index 0000000..d873c06
--- /dev/null
+++ b/etc/mpv.conf
@@ -0,0 +1,143 @@
+#
+# Example mpv configuration file
+#
+# Warning:
+#
+# The commented example options usually do _not_ set the default values. Call
+# mpv with --list-options to see the default values for most options. There is
+# no builtin or example mpv.conf with all the defaults.
+#
+#
+# Configuration files are read system-wide from /usr/local/etc/mpv.conf
+# and per-user from ~/.config/mpv/mpv.conf, where per-user settings override
+# system-wide settings, all of which are overridden by the command line.
+#
+# Configuration file settings and the command line options use the same
+# underlying mechanisms. Most options can be put into the configuration file
+# by dropping the preceding '--'. See the man page for a complete list of
+# options.
+#
+# Lines starting with '#' are comments and are ignored.
+#
+# See the CONFIGURATION FILES section in the man page
+# for a detailed description of the syntax.
+#
+# Profiles should be placed at the bottom of the configuration file to ensure
+# that settings wanted as defaults are not restricted to specific profiles.
+
+##################
+# video settings #
+##################
+
+# Start in fullscreen mode by default.
+#fs=yes
+
+# force starting with centered window
+#geometry=50%:50%
+
+# don't allow a new window to have a size larger than 90% of the screen size
+#autofit-larger=90%x90%
+
+# Do not close the window on exit.
+#keep-open=yes
+
+# Do not wait with showing the video window until it has loaded. (This will
+# resize the window once video is loaded. Also always shows a window with
+# audio.)
+#force-window=immediate
+
+# Disable the On Screen Controller (OSC).
+#osc=no
+
+# Keep the player window on top of all other windows.
+#ontop=yes
+
+# Specify fast video rendering preset (for --vo=<gpu|gpu-next> only)
+# Recommended for mobile devices or older hardware with limited processing power
+#profile=fast
+
+# Specify high quality video rendering preset (for --vo=<gpu|gpu-next> only)
+# Offers superior image fidelity and visual quality for an enhanced viewing
+# experience on capable hardware
+#profile=high-quality
+
+# Force video to lock on the display's refresh rate, and change video and audio
+# speed to some degree to ensure synchronous playback - can cause problems
+# with some drivers and desktop environments.
+#video-sync=display-resample
+
+# Enable hardware decoding if available. Often, this does not work with all
+# video outputs, but should work well with default settings on most systems.
+# If performance or energy usage is an issue, forcing the vdpau or vaapi VOs
+# may or may not help.
+#hwdec=auto
+
+##################
+# audio settings #
+##################
+
+# Specify default audio device. You can list devices with: --audio-device=help
+# The option takes the device string (the stuff between the '...').
+#audio-device=alsa/default
+
+# Do not filter audio to keep pitch when changing playback speed.
+#audio-pitch-correction=no
+
+# Output 5.1 audio natively, and upmix/downmix audio with a different format.
+#audio-channels=5.1
+# Disable any automatic remix, _if_ the audio output accepts the audio format.
+# of the currently played file. See caveats mentioned in the manpage.
+# (The default is "auto-safe", see manpage.)
+#audio-channels=auto
+
+##################
+# other settings #
+##################
+
+# Pretend to be a web browser. Might fix playback with some streaming sites,
+# but also will break with shoutcast streams.
+#user-agent="Mozilla/5.0"
+
+# cache settings
+#
+# Use a large seekable RAM cache even for local input.
+#cache=yes
+#
+# Use extra large RAM cache (needs cache=yes to make it useful).
+#demuxer-max-bytes=500M
+#demuxer-max-back-bytes=100M
+#
+# Disable the behavior that the player will pause if the cache goes below a
+# certain fill size.
+#cache-pause=no
+#
+# Store cache payload on the hard disk instead of in RAM. (This may negatively
+# impact performance unless used for slow input such as network.)
+#cache-dir=~/.cache/
+#cache-on-disk=yes
+
+# Display English subtitles if available.
+#slang=en
+
+# Play Finnish audio if available, fall back to English otherwise.
+#alang=fi,en
+
+# Change subtitle encoding. For Arabic subtitles use 'cp1256'.
+# If the file seems to be valid UTF-8, prefer UTF-8.
+# (You can add '+' in front of the codepage to force it.)
+#sub-codepage=cp1256
+
+# You can also include other configuration files.
+#include=/path/to/the/file/you/want/to/include
+
+############
+# Profiles #
+############
+
+# The options declared as part of profiles override global default settings,
+# but only take effect when the profile is active.
+
+# The following profile can be enabled on the command line with: --profile=eye-cancer
+
+#[eye-cancer]
+#sharpen=5
diff --git a/etc/mpv.desktop b/etc/mpv.desktop
new file mode 100644
index 0000000..db71520
--- /dev/null
+++ b/etc/mpv.desktop
@@ -0,0 +1,44 @@
+[Desktop Entry]
+Type=Application
+Name=mpv Media Player
+Name[ca]=Reproductor multimèdia mpv
+Name[cs]=mpv přehrávač
+Name[da]=mpv-medieafspiller
+Name[fr]=Lecteur multimédia mpv
+Name[ja]=mpv メディアプレイヤー
+Name[pl]=Odtwarzacz mpv
+Name[ru]=Проигрыватель mpv
+Name[tr]=mpv Ortam Oynatıcı
+Name[zh_CN]=mpv 媒体播放器
+Name[zh_TW]=mpv 媒體播放器
+GenericName=Multimedia player
+GenericName[cs]=Multimediální přehrávač
+GenericName[da]=Multimedieafspiller
+GenericName[fr]=Lecteur multimédia
+GenericName[ja]=マルチメディアプレイヤー
+GenericName[ru]=Мультимедийный проигрыватель
+GenericName[tr]=Çoklu ortam oynatıcı
+GenericName[zh_CN]=多媒体播放器
+GenericName[zh_TW]=多媒體播放器
+Comment=Play movies and songs
+Comment[ca]=Reproduïu vídeos i cançons
+Comment[cs]=Přehrává filmy a hudbu
+Comment[da]=Afspil film og sange
+Comment[de]=Filme und Musik abspielen
+Comment[es]=Reproduzca vídeos y canciones
+Comment[fr]=Lire des vidéos et des musiques
+Comment[ja]=映画や音楽を再生する
+Comment[it]=Lettore multimediale
+Comment[pl]=Odtwarzaj filmy i muzykę
+Comment[ru]=Воспроизведение фильмов и музыки
+Comment[tr]=Filmleri ve şarkıları oynatın
+Comment[zh_CN]=播放电影和歌曲
+Comment[zh_TW]=播放電影和歌曲
+Icon=mpv
+TryExec=mpv
+Exec=mpv --player-operation-mode=pseudo-gui -- %U
+Terminal=false
+Categories=AudioVideo;Audio;Video;Player;TV;
+MimeType=application/ogg;application/x-ogg;application/mxf;application/sdp;application/smil;application/x-smil;application/streamingmedia;application/x-streamingmedia;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/aac;audio/x-aac;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/aiff;audio/x-aiff;audio/m4a;audio/x-m4a;application/x-extension-m4a;audio/mp1;audio/x-mp1;audio/mp2;audio/x-mp2;audio/mp3;audio/x-mp3;audio/mpeg;audio/mpeg2;audio/mpeg3;audio/mpegurl;audio/x-mpegurl;audio/mpg;audio/x-mpg;audio/rn-mpeg;audio/musepack;audio/x-musepack;audio/ogg;audio/scpls;audio/x-scpls;audio/vnd.rn-realaudio;audio/wav;audio/x-pn-wav;audio/x-pn-windows-pcm;audio/x-realaudio;audio/x-pn-realaudio;audio/x-ms-wma;audio/x-pls;audio/x-wav;video/mpeg;video/x-mpeg2;video/x-mpeg3;video/mp4v-es;video/x-m4v;video/mp4;application/x-extension-mp4;video/divx;video/vnd.divx;video/msvideo;video/x-msvideo;video/ogg;video/quicktime;video/vnd.rn-realvideo;video/x-ms-afs;video/x-ms-asf;audio/x-ms-asf;application/vnd.ms-asf;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvxvideo;video/x-avi;video/avi;video/x-flic;video/fli;video/x-flc;video/flv;video/x-flv;video/x-theora;video/x-theora+ogg;video/x-matroska;video/mkv;audio/x-matroska;application/x-matroska;video/webm;audio/webm;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/x-ogm;video/x-ogm+ogg;application/x-ogm;application/x-ogm-audio;application/x-ogm-video;application/x-shorten;audio/x-shorten;audio/x-ape;audio/x-wavpack;audio/x-tta;audio/AMR;audio/ac3;audio/eac3;audio/amr-wb;video/mp2t;audio/flac;audio/mp4;application/x-mpegurl;video/vnd.mpegurl;application/vnd.apple.mpegurl;audio/x-pn-au;video/3gp;video/3gpp;video/3gpp2;audio/3gpp;audio/3gpp2;video/dv;audio/dv;audio/opus;audio/vnd.dts;audio/vnd.dts.hd;audio/x-adpcm;application/x-cue;audio/m3u;
+X-KDE-Protocols=ftp,http,https,mms,rtmp,rtsp,sftp,smb,srt,rist,webdav,webdavs
+StartupWMClass=mpv
diff --git a/etc/mpv.metainfo.xml b/etc/mpv.metainfo.xml
new file mode 100644
index 0000000..618abd3
--- /dev/null
+++ b/etc/mpv.metainfo.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop-application">
+ <id>io.mpv.mpv</id>
+ <name>mpv</name>
+ <summary>A free, open source, and cross-platform media player</summary>
+ <description>
+ <p>mpv is a free (as in freedom) media player for the command line. It supports a wide variety of media file formats, audio and video codecs, and subtitle types.</p>
+ <p>mpv has an OpenGL, Vulkan, and D3D11 based video output that is capable of many features loved by videophiles, such as video scaling with popular high quality algorithms, color management, frame timing, interpolation, HDR, and more.</p>
+ <p>While mpv strives for minimalism and provides no real GUI, it has a small controller on top of the video for basic control.</p>
+ <p>mpv can leverage most hardware decoding APIs on all platforms. Hardware decoding can be enabled at runtime on demand.</p>
+ <p>Powerful scripting capabilities can make the player do almost anything. There is a large selection of user scripts on the wiki.</p>
+ <p>A straightforward C API was designed from the ground up to make mpv usable as a library and facilitate easy integration into other applications.</p>
+ </description>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0-or-later AND LGPL-2.0-or-later</project_license>
+ <developer_name>mpv.io</developer_name>
+ <launchable type="desktop-id">mpv.desktop</launchable>
+ <content_rating type="oars-1.1" />
+ <screenshots>
+ <screenshot type="default">
+ <image>https://mpv.io/images/mpv-screenshot-34cd36ae.jpg</image>
+ <caption>Main window</caption>
+ </screenshot>
+ </screenshots>
+ <url type="homepage">https://mpv.io/</url>
+ <url type="bugtracker">https://github.com/mpv-player/mpv/issues</url>
+ <url type="faq">https://github.com/mpv-player/mpv/wiki/FAQ</url>
+ <url type="help">https://mpv.io/manual/stable</url>
+</component>
diff --git a/etc/mpv.svg b/etc/mpv.svg
new file mode 100644
index 0000000..5e7355e
--- /dev/null
+++ b/etc/mpv.svg
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 63.999999 63.999999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="mpv.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.3710484"
+ inkscape:cx="10.112865"
+ inkscape:cy="18.643164"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="1920"
+ inkscape:window-height="1016"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.3622)">
+ <circle
+ style="opacity:1;fill:#e5e5e5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10161044;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ id="path4380"
+ cx="32"
+ cy="1020.3622"
+ r="27.949194" />
+ <circle
+ style="opacity:1;fill:#672168;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0988237;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ id="path4390"
+ cx="32.727058"
+ cy="1019.5079"
+ r="25.950588" />
+ <circle
+ style="opacity:1;fill:#420143;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686"
+ id="path4400"
+ cx="34.224396"
+ cy="1017.7957"
+ r="20" />
+ <path
+ style="fill:#dddbdd;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 44.481446,1020.4807 a 12.848894,12.848894 0 0 1 -12.84889,12.8489 12.848894,12.848894 0 0 1 -12.8489,-12.8489 12.848894,12.848894 0 0 1 12.8489,-12.8489 12.848894,12.848894 0 0 1 12.84889,12.8489 z"
+ id="path4412"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#691f69;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 28.374316,1014.709 0,11.4502 9.21608,-5.8647 z"
+ id="path4426"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/etc/restore-old-bindings.conf b/etc/restore-old-bindings.conf
new file mode 100644
index 0000000..54b1524
--- /dev/null
+++ b/etc/restore-old-bindings.conf
@@ -0,0 +1,59 @@
+
+# This file contains all bindings that were removed after a certain release.
+# If you want MPlayer bindings, use mplayer-input.conf
+
+# Pick the bindings you want back and add them to your own input.conf. Append
+# this file to your input.conf if you want them all back:
+#
+# cat restore-old-bindings.conf >> ~/.config/mpv/input.conf
+#
+# Older installations use ~/.mpv/input.conf instead.
+
+# changed in mpv 0.37.0
+
+WHEEL_UP seek 10 # seek 10 seconds forward
+WHEEL_DOWN seek -10 # seek 10 seconds backward
+WHEEL_LEFT add volume -2
+WHEEL_RIGHT add volume 2
+
+# changed in mpv 0.27.0 (macOS and Wayland only)
+
+# WHEEL_LEFT seek 5
+# WHEEL_RIGHT seek -5
+
+# changed in mpv 0.26.0
+
+H cycle dvbin-channel-switch-offset up
+K cycle dvbin-channel-switch-offset down
+
+I show-text "${filename}" # display filename in osd
+
+# changed in mpv 0.24.0
+
+L cycle-values loop-playlist "inf" "no"
+
+# changed in mpv 0.10.0
+
+O osd
+D cycle deinterlace
+d cycle framedrop
+
+# changed in mpv 0.7.0
+
+ENTER playlist-next force
+
+# changed in mpv 0.6.0
+
+ESC quit
+
+# changed in mpv 0.5.0
+
+PGUP seek 600
+PGDWN seek -600
+RIGHT seek 10
+LEFT seek -10
++ add audio-delay 0.100
+- add audio-delay -0.100
+F cycle sub-forced-events-only
+U stop
+o cycle-values osd-level
diff --git a/filters/f_async_queue.c b/filters/f_async_queue.c
new file mode 100644
index 0000000..95db385
--- /dev/null
+++ b/filters/f_async_queue.c
@@ -0,0 +1,375 @@
+#include <limits.h>
+#include <stdatomic.h>
+
+#include "audio/aframe.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "osdep/threads.h"
+
+#include "f_async_queue.h"
+#include "filter_internal.h"
+
+struct mp_async_queue {
+ // This is just a wrapper, so the API user can talloc_free() it, instead of
+ // having to call a special unref function.
+ struct async_queue *q;
+};
+
+struct async_queue {
+ _Atomic uint64_t refcount;
+
+ mp_mutex lock;
+
+ // -- protected by lock
+ struct mp_async_queue_config cfg;
+ bool active; // queue was resumed; consumer may request frames
+ bool reading; // data flow: reading => consumer has requested frames
+ int64_t samples_size; // queue size in the cfg.sample_unit
+ size_t byte_size; // queue size in bytes (using approx. frame sizes)
+ int num_frames;
+ struct mp_frame *frames;
+ int eof_count; // number of MP_FRAME_EOF in frames[], for draining
+ struct mp_filter *conn[2]; // filters: in (0), out (1)
+};
+
+static void reset_queue(struct async_queue *q)
+{
+ mp_mutex_lock(&q->lock);
+ q->active = q->reading = false;
+ for (int n = 0; n < q->num_frames; n++)
+ mp_frame_unref(&q->frames[n]);
+ q->num_frames = 0;
+ q->eof_count = 0;
+ q->samples_size = 0;
+ q->byte_size = 0;
+ for (int n = 0; n < 2; n++) {
+ if (q->conn[n])
+ mp_filter_wakeup(q->conn[n]);
+ }
+ mp_mutex_unlock(&q->lock);
+}
+
+static void unref_queue(struct async_queue *q)
+{
+ if (!q)
+ return;
+ int count = atomic_fetch_add(&q->refcount, -1) - 1;
+ assert(count >= 0);
+ if (count == 0) {
+ reset_queue(q);
+ mp_mutex_destroy(&q->lock);
+ talloc_free(q);
+ }
+}
+
+static void on_free_queue(void *p)
+{
+ struct mp_async_queue *q = p;
+ unref_queue(q->q);
+}
+
+struct mp_async_queue *mp_async_queue_create(void)
+{
+ struct mp_async_queue *r = talloc_zero(NULL, struct mp_async_queue);
+ r->q = talloc_zero(NULL, struct async_queue);
+ *r->q = (struct async_queue){
+ .refcount = 1,
+ };
+ mp_mutex_init(&r->q->lock);
+ talloc_set_destructor(r, on_free_queue);
+ mp_async_queue_set_config(r, (struct mp_async_queue_config){0});
+ return r;
+}
+
+static int64_t frame_get_samples(struct async_queue *q, struct mp_frame frame)
+{
+ int64_t res = 1;
+ if (frame.type == MP_FRAME_AUDIO && q->cfg.sample_unit == AQUEUE_UNIT_SAMPLES) {
+ struct mp_aframe *aframe = frame.data;
+ res = mp_aframe_get_size(aframe);
+ }
+ if (mp_frame_is_signaling(frame))
+ return 0;
+ return res;
+}
+
+static bool is_full(struct async_queue *q)
+{
+ if (q->samples_size >= q->cfg.max_samples || q->byte_size >= q->cfg.max_bytes)
+ return true;
+ if (q->num_frames >= 2 && q->cfg.max_duration > 0) {
+ double pts1 = mp_frame_get_pts(q->frames[q->num_frames - 1]);
+ double pts2 = mp_frame_get_pts(q->frames[0]);
+ if (pts1 != MP_NOPTS_VALUE && pts2 != MP_NOPTS_VALUE &&
+ pts2 - pts1 >= q->cfg.max_duration)
+ return true;
+ }
+ return false;
+}
+
+// Add or remove a frame from the accounted queue size.
+// dir==1: add, dir==-1: remove
+static void account_frame(struct async_queue *q, struct mp_frame frame,
+ int dir)
+{
+ assert(dir == 1 || dir == -1);
+
+ q->samples_size += dir * frame_get_samples(q, frame);
+ q->byte_size += dir * mp_frame_approx_size(frame);
+
+ if (frame.type == MP_FRAME_EOF)
+ q->eof_count += dir;
+}
+
+static void recompute_sizes(struct async_queue *q)
+{
+ q->eof_count = 0;
+ q->samples_size = 0;
+ q->byte_size = 0;
+ for (int n = 0; n < q->num_frames; n++)
+ account_frame(q, q->frames[n], 1);
+}
+
+void mp_async_queue_set_config(struct mp_async_queue *queue,
+ struct mp_async_queue_config cfg)
+{
+ struct async_queue *q = queue->q;
+
+ cfg.max_bytes = MPCLAMP(cfg.max_bytes, 1, (size_t)-1 / 2);
+
+ assert(cfg.sample_unit == AQUEUE_UNIT_FRAME ||
+ cfg.sample_unit == AQUEUE_UNIT_SAMPLES);
+
+ cfg.max_samples = MPMAX(cfg.max_samples, 1);
+
+ mp_mutex_lock(&q->lock);
+ bool recompute = q->cfg.sample_unit != cfg.sample_unit;
+ q->cfg = cfg;
+ if (recompute)
+ recompute_sizes(q);
+ mp_mutex_unlock(&q->lock);
+}
+
+void mp_async_queue_reset(struct mp_async_queue *queue)
+{
+ reset_queue(queue->q);
+}
+
+bool mp_async_queue_is_active(struct mp_async_queue *queue)
+{
+ struct async_queue *q = queue->q;
+ mp_mutex_lock(&q->lock);
+ bool res = q->active;
+ mp_mutex_unlock(&q->lock);
+ return res;
+}
+
+bool mp_async_queue_is_full(struct mp_async_queue *queue)
+{
+ struct async_queue *q = queue->q;
+ mp_mutex_lock(&q->lock);
+ bool res = is_full(q);
+ mp_mutex_unlock(&q->lock);
+ return res;
+}
+
+void mp_async_queue_resume(struct mp_async_queue *queue)
+{
+ struct async_queue *q = queue->q;
+
+ mp_mutex_lock(&q->lock);
+ if (!q->active) {
+ q->active = true;
+ // Possibly make the consumer request new frames.
+ if (q->conn[1])
+ mp_filter_wakeup(q->conn[1]);
+ }
+ mp_mutex_unlock(&q->lock);
+}
+
+void mp_async_queue_resume_reading(struct mp_async_queue *queue)
+{
+ struct async_queue *q = queue->q;
+
+ mp_mutex_lock(&q->lock);
+ if (!q->active || !q->reading) {
+ q->active = true;
+ q->reading = true;
+ // Possibly start producer/consumer.
+ for (int n = 0; n < 2; n++) {
+ if (q->conn[n])
+ mp_filter_wakeup(q->conn[n]);
+ }
+ }
+ mp_mutex_unlock(&q->lock);
+}
+
+int64_t mp_async_queue_get_samples(struct mp_async_queue *queue)
+{
+ struct async_queue *q = queue->q;
+ mp_mutex_lock(&q->lock);
+ int64_t res = q->samples_size;
+ mp_mutex_unlock(&q->lock);
+ return res;
+}
+
+int mp_async_queue_get_frames(struct mp_async_queue *queue)
+{
+ struct async_queue *q = queue->q;
+ mp_mutex_lock(&q->lock);
+ int res = q->num_frames;
+ mp_mutex_unlock(&q->lock);
+ return res;
+}
+
+struct priv {
+ struct async_queue *q;
+ struct mp_filter *notify;
+};
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ struct async_queue *q = p->q;
+
+ mp_mutex_lock(&q->lock);
+ for (int n = 0; n < 2; n++) {
+ if (q->conn[n] == f)
+ q->conn[n] = NULL;
+ }
+ mp_mutex_unlock(&q->lock);
+
+ unref_queue(q);
+}
+
+static void process_in(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ struct async_queue *q = p->q;
+ assert(q->conn[0] == f);
+
+ mp_mutex_lock(&q->lock);
+ if (!q->reading) {
+ // mp_async_queue_reset()/reset_queue() is usually called asynchronously,
+ // so we might have requested a frame earlier, and now can't use it.
+ // Discard it; the expectation is that this is a benign logical race
+ // condition, and the filter graph will be reset anyway.
+ if (mp_pin_out_has_data(f->ppins[0])) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ mp_frame_unref(&frame);
+ MP_DBG(f, "discarding frame due to async reset\n");
+ }
+ } else if (!is_full(q) && mp_pin_out_request_data(f->ppins[0])) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ account_frame(q, frame, 1);
+ MP_TARRAY_INSERT_AT(q, q->frames, q->num_frames, 0, frame);
+ // Notify reader that we have new frames.
+ if (q->conn[1])
+ mp_filter_wakeup(q->conn[1]);
+ bool full = is_full(q);
+ if (!full)
+ mp_pin_out_request_data_next(f->ppins[0]);
+ if (p->notify && full)
+ mp_filter_wakeup(p->notify);
+ }
+ if (p->notify && !q->num_frames)
+ mp_filter_wakeup(p->notify);
+ mp_mutex_unlock(&q->lock);
+}
+
+static void process_out(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ struct async_queue *q = p->q;
+ assert(q->conn[1] == f);
+
+ if (!mp_pin_in_needs_data(f->ppins[0]))
+ return;
+
+ mp_mutex_lock(&q->lock);
+ if (q->active && !q->reading) {
+ q->reading = true;
+ mp_filter_wakeup(q->conn[0]);
+ }
+ if (q->active && q->num_frames) {
+ struct mp_frame frame = q->frames[q->num_frames - 1];
+ q->num_frames -= 1;
+ account_frame(q, frame, -1);
+ assert(q->samples_size >= 0);
+ mp_pin_in_write(f->ppins[0], frame);
+ // Notify writer that we need new frames.
+ if (q->conn[0])
+ mp_filter_wakeup(q->conn[0]);
+ }
+ mp_mutex_unlock(&q->lock);
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ struct async_queue *q = p->q;
+
+ mp_mutex_lock(&q->lock);
+ // If the queue is in reading state, it is logical that it should request
+ // input immediately.
+ if (mp_pin_get_dir(f->pins[0]) == MP_PIN_IN && q->reading)
+ mp_filter_wakeup(f);
+ mp_mutex_unlock(&q->lock);
+}
+
+// producer
+static const struct mp_filter_info info_in = {
+ .name = "async_queue_in",
+ .priv_size = sizeof(struct priv),
+ .destroy = destroy,
+ .process = process_in,
+ .reset = reset,
+};
+
+// consumer
+static const struct mp_filter_info info_out = {
+ .name = "async_queue_out",
+ .priv_size = sizeof(struct priv),
+ .destroy = destroy,
+ .process = process_out,
+};
+
+void mp_async_queue_set_notifier(struct mp_filter *f, struct mp_filter *notify)
+{
+ assert(mp_filter_get_info(f) == &info_in);
+ struct priv *p = f->priv;
+ if (p->notify != notify) {
+ p->notify = notify;
+ if (notify)
+ mp_filter_wakeup(notify);
+ }
+}
+
+struct mp_filter *mp_async_queue_create_filter(struct mp_filter *parent,
+ enum mp_pin_dir dir,
+ struct mp_async_queue *queue)
+{
+ bool is_in = dir == MP_PIN_IN;
+ assert(queue);
+
+ struct mp_filter *f = mp_filter_create(parent, is_in ? &info_in : &info_out);
+ if (!f)
+ return NULL;
+
+ struct priv *p = f->priv;
+
+ struct async_queue *q = queue->q;
+
+ mp_filter_add_pin(f, dir, is_in ? "in" : "out");
+
+ atomic_fetch_add(&q->refcount, 1);
+ p->q = q;
+
+ mp_mutex_lock(&q->lock);
+ int slot = is_in ? 0 : 1;
+ assert(!q->conn[slot]); // fails if already connected on this end
+ q->conn[slot] = f;
+ mp_mutex_unlock(&q->lock);
+
+ return f;
+}
diff --git a/filters/f_async_queue.h b/filters/f_async_queue.h
new file mode 100644
index 0000000..46dafcd
--- /dev/null
+++ b/filters/f_async_queue.h
@@ -0,0 +1,135 @@
+#pragma once
+
+#include <stdint.h>
+#include "filter.h"
+
+// A thread safe queue, which buffers a configurable number of frames like a
+// FIFO. It's part of the filter framework, and intended to provide such a
+// queue between filters. Since a filter graph can't be used from multiple
+// threads without synchronization, this provides 2 filters, which are
+// implicitly connected. (This seemed much saner than having special thread
+// safe mp_pins or such in the filter framework.)
+struct mp_async_queue;
+
+// Create a blank queue. Can be freed with talloc_free(). To use it, you need
+// to create input and output filters with mp_async_queue_create_filter().
+// Note that freeing it will only unref it. (E.g. you can free it once you've
+// created the input and output filters.)
+struct mp_async_queue *mp_async_queue_create(void);
+
+// Clear all queued data and make the queue "inactive". The latter prevents any
+// further communication until mp_async_queue_resume() is called.
+// For correct operation, you also need to call reset on the access filters
+void mp_async_queue_reset(struct mp_async_queue *queue);
+
+// Put the queue into "active" mode. If it wasn't, then the consumer is woken
+// up (and if there is no data in the queue, this will in turn wake up the
+// producer, i.e. start transfers automatically).
+// If there is a writer end but no reader end, this will simply make the queue
+// fill up.
+void mp_async_queue_resume(struct mp_async_queue *queue);
+
+// Like mp_async_queue_resume(), but also allows the producer writing to the
+// queue, even if the consumer will request any data yet.
+void mp_async_queue_resume_reading(struct mp_async_queue *queue);
+
+// Returns true if out of mp_async_queue_reset()/mp_async_queue_resume(), the
+// latter was most recently called.
+bool mp_async_queue_is_active(struct mp_async_queue *queue);
+
+// Returns true if the queue reached its configured size, the input filter
+// accepts no further frames. Always returns false if not active (then it does
+// not accept any input at all).
+bool mp_async_queue_is_full(struct mp_async_queue *queue);
+
+// Get the total of samples buffered within the queue itself. This doesn't count
+// samples buffered in the access filters. mp_async_queue_config.sample_unit is
+// used to define what "1 sample" means.
+int64_t mp_async_queue_get_samples(struct mp_async_queue *queue);
+
+// Get the total number of frames buffered within the queue itself. Frames
+// buffered in the access filters are not included.
+int mp_async_queue_get_frames(struct mp_async_queue *queue);
+
+// Create a filter to access the queue, and connect it. It's not allowed to
+// connect an already connected end of the queue. The filter can be freed at
+// any time.
+//
+// The queue starts out in "inactive" mode, where the queue does not allow
+// the producer to write any data. You need to call mp_async_queue_resume() to
+// start communication. Actual transfers happen only once the consumer filter
+// has read requests on its mp_pin.
+// If the producer filter requested a new frame from its filter graph, and the
+// queue is asynchronously set to "inactive", then the requested frame will be
+// silently discarded once it reaches the producer filter.
+//
+// Resetting a queue filter does not affect the queue at all. Managing the
+// queue state is the API user's responsibility. Note that resetting an input
+// filter (dir==MP_PIN_IN) while the queue is active and in "reading" state
+// (the output filter requested data at any point before the last
+// mp_async_queue_reset(), or mp_async_queue_resume_reading() was called), the
+// filter will immediately request data after the reset.
+//
+// For proper global reset, this order should be preferred:
+// - mp_async_queue_reset()
+// - reset producer and consumer filters on their respective threads (in any
+// order)
+// - do whatever other reset work is required
+// - mp_async_queue_resume()
+//
+// parent: filter graph the filter should be part of (or for standalone use,
+// create one with mp_filter_create_root())
+// dir: MP_PIN_IN for a filter that writes to the queue, MP_PIN_OUT to read
+// queue: queue to attach to (which end of it depends on dir)
+// The returned filter will have exactly 1 pin with the requested dir.
+struct mp_filter *mp_async_queue_create_filter(struct mp_filter *parent,
+ enum mp_pin_dir dir,
+ struct mp_async_queue *queue);
+
+// Set a filter that should be woken up with mp_filter_wakeup() in the following
+// situations:
+// - mp_async_queue_is_full() changes to true (at least for a short moment)
+// - mp_async_queue_get_frames() changes to 0 (at least until new data is fed)
+// This is a workaround for the filter design, which does not allow you to write
+// to the queue in a "sequential" way (write, then check condition).
+// Calling this again on the same filter removes the previous notify filter.
+// f: must be a filter returned by mp_async_queue_create_filter(, MP_PIN_IN,)
+// notify: filter to be woken up
+void mp_async_queue_set_notifier(struct mp_filter *f, struct mp_filter *notify);
+
+enum mp_async_queue_sample_unit {
+ AQUEUE_UNIT_FRAME = 0, // a frame counts as 1 sample
+ AQUEUE_UNIT_SAMPLES, // number of audio samples (1 for other media types,
+ // 0 for signaling)
+};
+
+// Setting this struct to all-0 is equivalent to defaults.
+struct mp_async_queue_config {
+ // Maximum size of frames buffered. mp_frame_approx_size() is used. May be
+ // overshot by up to 1 full frame. Clamped to [1, SIZE_MAX/2].
+ int64_t max_bytes;
+
+ // Defines what a "sample" is; affects the fields below.
+ enum mp_async_queue_sample_unit sample_unit;
+
+ // Maximum number of frames allowed to be buffered at a time (if
+ // unit!=AQUEUE_UNIT_FRAME, can be overshot by the contents of 1 mp_frame).
+ // 0 is treated as 1.
+ int64_t max_samples;
+
+ // Maximum allowed timestamp difference between 2 frames. This still allows
+ // at least 2 samples. Behavior is unclear on timestamp resets (even if EOF
+ // frames are between them). A value of 0 disables this completely.
+ double max_duration;
+};
+
+// Configure the queue size. By default, the queue size is 1 frame.
+// The wakeup_threshold_* fields can be used to avoid too frequent wakeups by
+// delaying wakeups, and then making the producer to filter multiple frames at
+// once.
+// In all cases, the filters can still read/write if the producer/consumer got
+// woken up by something else.
+// If the current queue contains more frames than the new config allows, the
+// queue will remain over-allocated until these frames have been read.
+void mp_async_queue_set_config(struct mp_async_queue *queue,
+ struct mp_async_queue_config cfg);
diff --git a/filters/f_auto_filters.c b/filters/f_auto_filters.c
new file mode 100644
index 0000000..c8b31f6
--- /dev/null
+++ b/filters/f_auto_filters.c
@@ -0,0 +1,431 @@
+#include <math.h>
+
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+
+#include "f_auto_filters.h"
+#include "f_autoconvert.h"
+#include "f_hwtransfer.h"
+#include "f_swscale.h"
+#include "f_utils.h"
+#include "filter.h"
+#include "filter_internal.h"
+#include "user_filters.h"
+
+struct deint_priv {
+ struct mp_subfilter sub;
+ int prev_imgfmt;
+ int prev_setting;
+ struct m_config_cache *opts;
+};
+
+static void deint_process(struct mp_filter *f)
+{
+ struct deint_priv *p = f->priv;
+
+ if (!mp_subfilter_read(&p->sub))
+ return;
+
+ struct mp_frame frame = p->sub.frame;
+
+ if (mp_frame_is_signaling(frame)) {
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ if (frame.type != MP_FRAME_VIDEO) {
+ MP_ERR(f, "video input required!\n");
+ mp_filter_internal_mark_failed(f);
+ return;
+ }
+
+ m_config_cache_update(p->opts);
+ struct filter_opts *opts = p->opts->opts;
+
+ if (!opts->deinterlace)
+ mp_subfilter_destroy(&p->sub);
+
+ struct mp_image *img = frame.data;
+
+ if (img->imgfmt == p->prev_imgfmt && p->prev_setting == opts->deinterlace) {
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ if (!mp_subfilter_drain_destroy(&p->sub))
+ return;
+
+ assert(!p->sub.filter);
+
+ p->prev_imgfmt = img->imgfmt;
+ p->prev_setting = opts->deinterlace;
+ if (!p->prev_setting) {
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ bool has_filter = true;
+ if (img->imgfmt == IMGFMT_VDPAU) {
+ char *args[] = {"deint", "yes", NULL};
+ p->sub.filter =
+ mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "vdpaupp", args);
+ } else if (img->imgfmt == IMGFMT_D3D11) {
+ p->sub.filter =
+ mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "d3d11vpp", NULL);
+ } else if (img->imgfmt == IMGFMT_CUDA) {
+ char *args[] = {"mode", "send_field", NULL};
+ p->sub.filter =
+ mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "yadif_cuda", args);
+#if HAVE_VULKAN_INTEROP
+ } else if (img->imgfmt == IMGFMT_VULKAN) {
+ char *args[] = {"mode", "send_field", NULL};
+ p->sub.filter =
+ mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "bwdif_vulkan", args);
+#endif
+ } else if (img->imgfmt == IMGFMT_VAAPI) {
+ char *args[] = {"deint", "motion-adaptive",
+ "interlaced-only", "yes", NULL};
+ p->sub.filter =
+ mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "vavpp", args);
+ } else {
+ has_filter = false;
+ }
+
+ if (!p->sub.filter) {
+ if (has_filter)
+ MP_ERR(f, "creating deinterlacer failed\n");
+
+ struct mp_filter *subf = mp_bidir_dummy_filter_create(f);
+ struct mp_filter *filters[2] = {0};
+
+ struct mp_autoconvert *ac = mp_autoconvert_create(subf);
+ if (ac) {
+ filters[0] = ac->f;
+ // We know vf_yadif does not support hw inputs.
+ mp_autoconvert_add_all_sw_imgfmts(ac);
+
+ if (!mp_autoconvert_probe_input_video(ac, img)) {
+ MP_ERR(f, "no deinterlace filter available for format %s\n",
+ mp_imgfmt_to_name(img->imgfmt));
+ talloc_free(subf);
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+ }
+
+ char *args[] = {"mode", "send_field", NULL};
+ filters[1] =
+ mp_create_user_filter(subf, MP_OUTPUT_CHAIN_VIDEO, "yadif", args);
+
+ mp_chain_filters(subf->ppins[0], subf->ppins[1], filters, 2);
+ p->sub.filter = subf;
+ }
+
+ mp_subfilter_continue(&p->sub);
+}
+
+static void deint_reset(struct mp_filter *f)
+{
+ struct deint_priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+}
+
+static void deint_destroy(struct mp_filter *f)
+{
+ struct deint_priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+ TA_FREEP(&p->sub.filter);
+}
+
+static bool deint_command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct deint_priv *p = f->priv;
+
+ if (cmd->type == MP_FILTER_COMMAND_IS_ACTIVE) {
+ cmd->is_active = !!p->sub.filter;
+ return true;
+ }
+ return false;
+}
+
+static const struct mp_filter_info deint_filter = {
+ .name = "deint",
+ .priv_size = sizeof(struct deint_priv),
+ .command = deint_command,
+ .process = deint_process,
+ .reset = deint_reset,
+ .destroy = deint_destroy,
+};
+
+struct mp_filter *mp_deint_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &deint_filter);
+ if (!f)
+ return NULL;
+
+ struct deint_priv *p = f->priv;
+
+ p->sub.in = mp_filter_add_pin(f, MP_PIN_IN, "in");
+ p->sub.out = mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ p->opts = m_config_cache_alloc(f, f->global, &filter_conf);
+
+ return f;
+}
+
+struct rotate_priv {
+ struct mp_subfilter sub;
+ int prev_rotate;
+ int prev_imgfmt;
+ int target_rotate;
+};
+
+static void rotate_process(struct mp_filter *f)
+{
+ struct rotate_priv *p = f->priv;
+
+ if (!mp_subfilter_read(&p->sub))
+ return;
+
+ struct mp_frame frame = p->sub.frame;
+
+ if (mp_frame_is_signaling(frame)) {
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ if (frame.type != MP_FRAME_VIDEO) {
+ MP_ERR(f, "video input required!\n");
+ return;
+ }
+
+ struct mp_image *img = frame.data;
+
+ if (img->params.rotate == p->prev_rotate &&
+ img->imgfmt == p->prev_imgfmt)
+ {
+ img->params.rotate = p->target_rotate;
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ if (!mp_subfilter_drain_destroy(&p->sub))
+ return;
+
+ assert(!p->sub.filter);
+
+ int rotate = p->prev_rotate = img->params.rotate;
+ p->target_rotate = rotate;
+ p->prev_imgfmt = img->imgfmt;
+
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ if (rotate == 0 || (info && info->rotate90 && !(rotate % 90))) {
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ if (!mp_sws_supports_input(img->imgfmt)) {
+ MP_ERR(f, "Video rotation with this format not supported\n");
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ double angle = rotate / 360.0 * M_PI * 2;
+ char *args[] = {"angle", mp_tprintf(30, "%f", angle),
+ "ow", mp_tprintf(30, "rotw(%f)", angle),
+ "oh", mp_tprintf(30, "roth(%f)", angle),
+ NULL};
+ p->sub.filter =
+ mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "rotate", args);
+
+ if (p->sub.filter) {
+ MP_INFO(f, "Inserting rotation filter.\n");
+ p->target_rotate = 0;
+ } else {
+ MP_ERR(f, "could not create rotation filter\n");
+ }
+
+ mp_subfilter_continue(&p->sub);
+}
+
+static void rotate_reset(struct mp_filter *f)
+{
+ struct rotate_priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+}
+
+static void rotate_destroy(struct mp_filter *f)
+{
+ struct rotate_priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+ TA_FREEP(&p->sub.filter);
+}
+
+static bool rotate_command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct rotate_priv *p = f->priv;
+
+ if (cmd->type == MP_FILTER_COMMAND_IS_ACTIVE) {
+ cmd->is_active = !!p->sub.filter;
+ return true;
+ }
+ return false;
+}
+
+static const struct mp_filter_info rotate_filter = {
+ .name = "autorotate",
+ .priv_size = sizeof(struct rotate_priv),
+ .command = rotate_command,
+ .process = rotate_process,
+ .reset = rotate_reset,
+ .destroy = rotate_destroy,
+};
+
+struct mp_filter *mp_autorotate_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &rotate_filter);
+ if (!f)
+ return NULL;
+
+ struct rotate_priv *p = f->priv;
+ p->prev_rotate = -1;
+
+ p->sub.in = mp_filter_add_pin(f, MP_PIN_IN, "in");
+ p->sub.out = mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ return f;
+}
+
+struct aspeed_priv {
+ struct mp_subfilter sub;
+ double cur_speed, cur_speed_drop;
+ int current_filter;
+};
+
+static void aspeed_process(struct mp_filter *f)
+{
+ struct aspeed_priv *p = f->priv;
+
+ if (!mp_subfilter_read(&p->sub))
+ return;
+
+ if (!p->sub.filter)
+ p->current_filter = 0;
+
+ double speed = p->cur_speed * p->cur_speed_drop;
+
+ int req_filter = 0;
+ if (fabs(speed - 1.0) >= 1e-8) {
+ req_filter = p->cur_speed_drop == 1.0 ? 1 : 2;
+ if (p->sub.frame.type == MP_FRAME_AUDIO &&
+ !af_fmt_is_pcm(mp_aframe_get_format(p->sub.frame.data)))
+ req_filter = 2;
+ }
+
+ if (req_filter != p->current_filter) {
+ if (p->sub.filter)
+ MP_VERBOSE(f, "removing audio speed filter\n");
+ if (!mp_subfilter_drain_destroy(&p->sub))
+ return;
+
+ if (req_filter) {
+ if (req_filter == 1) {
+ MP_VERBOSE(f, "adding scaletempo2\n");
+ p->sub.filter = mp_create_user_filter(f, MP_OUTPUT_CHAIN_AUDIO,
+ "scaletempo2", NULL);
+ } else if (req_filter == 2) {
+ MP_VERBOSE(f, "adding drop\n");
+ p->sub.filter = mp_create_user_filter(f, MP_OUTPUT_CHAIN_AUDIO,
+ "drop", NULL);
+ }
+ if (!p->sub.filter) {
+ MP_ERR(f, "could not create filter\n");
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+ p->current_filter = req_filter;
+ }
+ }
+
+ if (p->sub.filter) {
+ struct mp_filter_command cmd = {
+ .type = MP_FILTER_COMMAND_SET_SPEED,
+ .speed = speed,
+ };
+ mp_filter_command(p->sub.filter, &cmd);
+ }
+
+ mp_subfilter_continue(&p->sub);
+}
+
+static bool aspeed_command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct aspeed_priv *p = f->priv;
+
+ if (cmd->type == MP_FILTER_COMMAND_SET_SPEED) {
+ p->cur_speed = cmd->speed;
+ return true;
+ }
+
+ if (cmd->type == MP_FILTER_COMMAND_SET_SPEED_DROP) {
+ p->cur_speed_drop = cmd->speed;
+ return true;
+ }
+
+ if (cmd->type == MP_FILTER_COMMAND_IS_ACTIVE) {
+ cmd->is_active = !!p->sub.filter;
+ return true;
+ }
+
+ return false;
+}
+
+static void aspeed_reset(struct mp_filter *f)
+{
+ struct aspeed_priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+}
+
+static void aspeed_destroy(struct mp_filter *f)
+{
+ struct aspeed_priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+ TA_FREEP(&p->sub.filter);
+}
+
+static const struct mp_filter_info aspeed_filter = {
+ .name = "autoaspeed",
+ .priv_size = sizeof(struct aspeed_priv),
+ .command = aspeed_command,
+ .process = aspeed_process,
+ .reset = aspeed_reset,
+ .destroy = aspeed_destroy,
+};
+
+struct mp_filter *mp_autoaspeed_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &aspeed_filter);
+ if (!f)
+ return NULL;
+
+ struct aspeed_priv *p = f->priv;
+ p->cur_speed = 1.0;
+ p->cur_speed_drop = 1.0;
+
+ p->sub.in = mp_filter_add_pin(f, MP_PIN_IN, "in");
+ p->sub.out = mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ return f;
+}
diff --git a/filters/f_auto_filters.h b/filters/f_auto_filters.h
new file mode 100644
index 0000000..f315084
--- /dev/null
+++ b/filters/f_auto_filters.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "filter.h"
+
+// A filter which inserts the required deinterlacing filter based on the
+// hardware decode mode and the deinterlace user option.
+struct mp_filter *mp_deint_create(struct mp_filter *parent);
+
+// Rotate according to mp_image.rotate and VO capabilities.
+struct mp_filter *mp_autorotate_create(struct mp_filter *parent);
+
+// Insert a filter that inserts scaletempo2 depending on speed settings.
+struct mp_filter *mp_autoaspeed_create(struct mp_filter *parent);
diff --git a/filters/f_autoconvert.c b/filters/f_autoconvert.c
new file mode 100644
index 0000000..dcd5ea2
--- /dev/null
+++ b/filters/f_autoconvert.c
@@ -0,0 +1,576 @@
+#include "audio/aframe.h"
+#include "audio/chmap_sel.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/hwdec.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+
+#include "f_autoconvert.h"
+#include "f_hwtransfer.h"
+#include "f_swresample.h"
+#include "f_swscale.h"
+#include "f_utils.h"
+#include "filter.h"
+#include "filter_internal.h"
+
+struct priv {
+ struct mp_log *log;
+
+ struct mp_subfilter sub;
+
+ bool force_update;
+
+ int *imgfmts;
+ int *subfmts;
+ int num_imgfmts;
+ struct mp_image_params imgparams;
+ bool imgparams_set;
+
+ // Enable special conversion for the final stage before the VO.
+ bool vo_convert;
+
+ // sws state
+ int in_imgfmt, in_subfmt;
+
+ int *afmts;
+ int num_afmts;
+ int *srates;
+ int num_srates;
+ struct mp_chmap_sel chmaps;
+
+ int in_afmt, in_srate;
+ struct mp_chmap in_chmap;
+
+ double audio_speed;
+ bool resampling_forced;
+
+ bool format_change_blocked;
+ bool format_change_cont;
+
+ struct mp_autoconvert public;
+};
+
+// Dummy filter for bundling sub-conversion filters.
+static const struct mp_filter_info convert_filter = {
+ .name = "convert",
+};
+
+void mp_autoconvert_clear(struct mp_autoconvert *c)
+{
+ struct priv *p = c->f->priv;
+
+ p->num_imgfmts = 0;
+ p->imgparams_set = false;
+ p->num_afmts = 0;
+ p->num_srates = 0;
+ p->chmaps = (struct mp_chmap_sel){0};
+ p->force_update = true;
+}
+
+void mp_autoconvert_add_imgfmt(struct mp_autoconvert *c, int imgfmt, int subfmt)
+{
+ struct priv *p = c->f->priv;
+
+ MP_TARRAY_GROW(p, p->imgfmts, p->num_imgfmts);
+ MP_TARRAY_GROW(p, p->subfmts, p->num_imgfmts);
+
+ p->imgfmts[p->num_imgfmts] = imgfmt;
+ p->subfmts[p->num_imgfmts] = subfmt;
+
+ p->num_imgfmts += 1;
+ p->force_update = true;
+}
+
+void mp_autoconvert_set_target_image_params(struct mp_autoconvert *c,
+ struct mp_image_params *par)
+{
+ struct priv *p = c->f->priv;
+
+ if (p->imgparams_set && mp_image_params_equal(&p->imgparams, par) &&
+ p->num_imgfmts == 1 && p->imgfmts[0] == par->imgfmt &&
+ p->subfmts[0] == par->hw_subfmt)
+ return;
+
+ p->imgparams = *par;
+ p->imgparams_set = true;
+
+ p->num_imgfmts = 0;
+ mp_autoconvert_add_imgfmt(c, par->imgfmt, par->hw_subfmt);
+}
+
+void mp_autoconvert_add_all_sw_imgfmts(struct mp_autoconvert *c)
+{
+ for (int n = IMGFMT_START; n < IMGFMT_END; n++) {
+ if (!IMGFMT_IS_HWACCEL(n))
+ mp_autoconvert_add_imgfmt(c, n, 0);
+ }
+}
+
+void mp_autoconvert_add_afmt(struct mp_autoconvert *c, int afmt)
+{
+ struct priv *p = c->f->priv;
+
+ MP_TARRAY_APPEND(p, p->afmts, p->num_afmts, afmt);
+ p->force_update = true;
+}
+
+void mp_autoconvert_add_chmap(struct mp_autoconvert *c, struct mp_chmap *chmap)
+{
+ struct priv *p = c->f->priv;
+
+ mp_chmap_sel_add_map(&p->chmaps, chmap);
+ p->force_update = true;
+}
+
+void mp_autoconvert_add_srate(struct mp_autoconvert *c, int rate)
+{
+ struct priv *p = c->f->priv;
+
+ MP_TARRAY_APPEND(p, p->srates, p->num_srates, rate);
+ // Some other API we call expects a 0-terminated sample rates array.
+ MP_TARRAY_GROW(p, p->srates, p->num_srates);
+ p->srates[p->num_srates] = 0;
+ p->force_update = true;
+}
+
+// If this returns true, and *out==NULL, no conversion is necessary.
+static bool build_image_converter(struct mp_autoconvert *c, struct mp_log *log,
+ struct mp_image *img, struct mp_filter **f_out)
+{
+ struct mp_filter *f = c->f;
+ struct priv *p = f->priv;
+
+ *f_out = NULL;
+
+ if (!p->num_imgfmts)
+ return true;
+
+ for (int n = 0; n < p->num_imgfmts; n++) {
+ bool samefmt = img->params.imgfmt == p->imgfmts[n];
+ bool samesubffmt = img->params.hw_subfmt == p->subfmts[n];
+ /*
+ * In practice, `p->subfmts` is not usually populated today, in which
+ * case we must actively probe formats below to establish if the VO can
+ * accept the subfmt being used by the hwdec.
+ */
+ if (samefmt && samesubffmt) {
+ if (p->imgparams_set) {
+ if (!mp_image_params_equal(&p->imgparams, &img->params))
+ break;
+ }
+ return true;
+ }
+ }
+
+ struct mp_filter *conv = mp_filter_create(f, &convert_filter);
+ if (!conv)
+ return false;
+
+ mp_filter_add_pin(conv, MP_PIN_IN, "in");
+ mp_filter_add_pin(conv, MP_PIN_OUT, "out");
+
+ // 0: hw->sw download
+ // 1: swscale
+ // 2: sw->hw upload
+ struct mp_filter *filters[3] = {0};
+ bool need_sws = true;
+ bool force_sws_params = false;
+ struct mp_image_params imgpar = img->params;
+
+ int *fmts = p->imgfmts;
+ int num_fmts = p->num_imgfmts;
+ int hwupload_fmt = 0;
+
+ bool imgfmt_is_sw = !IMGFMT_IS_HWACCEL(img->imgfmt);
+
+ // This should not happen. But not enough guarantee to make it an assert().
+ if (imgfmt_is_sw != !img->hwctx)
+ mp_warn(log, "Unexpected AVFrame/imgfmt hardware context mismatch.\n");
+
+ bool dst_all_hw = true;
+ bool dst_have_sw = false;
+ bool has_src_hw_fmt = false;
+ for (int n = 0; n < num_fmts; n++) {
+ bool is_hw = IMGFMT_IS_HWACCEL(fmts[n]);
+ dst_all_hw &= is_hw;
+ dst_have_sw |= !is_hw;
+ has_src_hw_fmt |= is_hw && fmts[n] == imgpar.imgfmt;
+ }
+
+ // Source is hw, some targets are sw -> try to download.
+ bool hw_to_sw = !imgfmt_is_sw && dst_have_sw;
+
+ if (has_src_hw_fmt) {
+ int src_fmt = img->params.hw_subfmt;
+ /*
+ * If the source format is a hardware format, and our output supports
+ * that hardware format, we prioritize preserving the use of that
+ * hardware format. In most cases, the sub format will also be supported
+ * and no conversion will be required, but in some cases, the hwdec
+ * may be able to output formats that the VO cannot display, and
+ * hardware format conversion becomes necessary.
+ */
+ struct mp_hwupload upload = mp_hwupload_create(conv, imgpar.imgfmt,
+ src_fmt,
+ true);
+ if (upload.successful_init) {
+ if (upload.f) {
+ mp_info(log, "Converting %s[%s] -> %s[%s]\n",
+ mp_imgfmt_to_name(imgpar.imgfmt),
+ mp_imgfmt_to_name(src_fmt),
+ mp_imgfmt_to_name(imgpar.imgfmt),
+ mp_imgfmt_to_name(upload.selected_sw_imgfmt));
+ filters[2] = upload.f;
+ }
+ hw_to_sw = false;
+ need_sws = false;
+ } else {
+ mp_err(log, "Failed to create HW uploader for format %s\n",
+ mp_imgfmt_to_name(src_fmt));
+ }
+ } else if (dst_all_hw && num_fmts > 0) {
+ bool upload_created = false;
+ int sw_fmt = imgfmt_is_sw ? img->imgfmt : img->params.hw_subfmt;
+
+ for (int i = 0; i < num_fmts; i++) {
+ // We can probably use this! Very lazy and very approximate.
+ struct mp_hwupload upload = mp_hwupload_create(conv, fmts[i],
+ sw_fmt, false);
+ if (upload.successful_init) {
+ mp_info(log, "HW-uploading to %s\n", mp_imgfmt_to_name(fmts[i]));
+ filters[2] = upload.f;
+ hwupload_fmt = upload.selected_sw_imgfmt;
+ fmts = &hwupload_fmt;
+ num_fmts = hwupload_fmt ? 1 : 0;
+ hw_to_sw = false;
+
+ // We cannot do format conversions when transferring between
+ // two hardware devices, so reject this format if that would be
+ // required.
+ if (!imgfmt_is_sw && hwupload_fmt != sw_fmt) {
+ mp_err(log, "Format %s is not supported by %s\n",
+ mp_imgfmt_to_name(sw_fmt),
+ mp_imgfmt_to_name(p->imgfmts[i]));
+ continue;
+ }
+ upload_created = true;
+ break;
+ }
+ }
+ if (!upload_created) {
+ mp_err(log, "Failed to create HW uploader for format %s\n",
+ mp_imgfmt_to_name(sw_fmt));
+ }
+ }
+
+ int src_fmt = img->imgfmt;
+ if (hw_to_sw) {
+ mp_info(log, "HW-downloading from %s\n", mp_imgfmt_to_name(img->imgfmt));
+ int res_fmt = mp_image_hw_download_get_sw_format(img);
+ if (!res_fmt) {
+ mp_err(log, "cannot copy surface of this format to CPU memory\n");
+ goto fail;
+ }
+ struct mp_hwdownload *hwd = mp_hwdownload_create(conv);
+ if (hwd) {
+ filters[0] = hwd->f;
+ src_fmt = res_fmt;
+ // Downloading from hw will obviously change the parameters. We
+ // stupidly don't know the result parameters, but if it's
+ // sufficiently sane, it will only do the following.
+ imgpar.imgfmt = src_fmt;
+ imgpar.hw_subfmt = 0;
+ // Try to compensate for in-sane cases.
+ mp_image_params_guess_csp(&imgpar);
+ }
+ }
+
+ if (p->imgparams_set) {
+ force_sws_params |= !mp_image_params_equal(&imgpar, &p->imgparams);
+ need_sws |= force_sws_params;
+ }
+ if (!imgfmt_is_sw && dst_all_hw) {
+ // This is a hw -> hw upload, so the sw format must already be
+ // mutually understood. No conversion can be done.
+ need_sws = false;
+ }
+
+ if (need_sws) {
+ // Create a new conversion filter.
+ struct mp_sws_filter *sws = mp_sws_filter_create(conv);
+ if (!sws) {
+ mp_err(log, "error creating conversion filter\n");
+ goto fail;
+ }
+
+ sws->force_scaler = c->force_scaler;
+
+ int out = mp_sws_find_best_out_format(sws, src_fmt, fmts, num_fmts);
+ if (!out) {
+ mp_err(log, "can't find video conversion for %s\n",
+ mp_imgfmt_to_name(src_fmt));
+ goto fail;
+ }
+
+ if (out == src_fmt && !force_sws_params) {
+ // Can happen if hwupload goes to same format.
+ talloc_free(sws->f);
+ } else {
+ sws->out_format = out;
+ sws->out_params = p->imgparams;
+ sws->use_out_params = force_sws_params;
+ mp_info(log, "Converting %s -> %s\n", mp_imgfmt_to_name(src_fmt),
+ mp_imgfmt_to_name(sws->out_format));
+ filters[1] = sws->f;
+ }
+ }
+
+ mp_chain_filters(conv->ppins[0], conv->ppins[1], filters, 3);
+
+ *f_out = conv;
+ return true;
+
+fail:
+ talloc_free(conv);
+ return false;
+}
+
+bool mp_autoconvert_probe_input_video(struct mp_autoconvert *c,
+ struct mp_image *img)
+{
+ struct mp_filter *conv = NULL;
+ bool res = build_image_converter(c, mp_null_log, img, &conv);
+ talloc_free(conv);
+ return res;
+}
+
+static void handle_video_frame(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ struct mp_image *img = p->sub.frame.data;
+
+ if (p->force_update)
+ p->in_imgfmt = p->in_subfmt = 0;
+
+ if (img->imgfmt == p->in_imgfmt && img->params.hw_subfmt == p->in_subfmt) {
+ mp_subfilter_continue(&p->sub);
+ return;
+ }
+
+ if (!mp_subfilter_drain_destroy(&p->sub)) {
+ MP_VERBOSE(f, "Sub-filter requires draining but we must destroy it now.\n");
+ mp_subfilter_destroy(&p->sub);
+ }
+
+ p->in_imgfmt = img->params.imgfmt;
+ p->in_subfmt = img->params.hw_subfmt;
+ p->force_update = false;
+
+ struct mp_filter *conv = NULL;
+ if (build_image_converter(&p->public, p->log, img, &conv)) {
+ p->sub.filter = conv;
+ mp_subfilter_continue(&p->sub);
+ } else {
+ mp_filter_internal_mark_failed(f);
+ }
+}
+
+static void handle_audio_frame(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ struct mp_aframe *aframe = p->sub.frame.data;
+
+ int afmt = mp_aframe_get_format(aframe);
+ int srate = mp_aframe_get_rate(aframe);
+ struct mp_chmap chmap = {0};
+ mp_aframe_get_chmap(aframe, &chmap);
+
+ if (p->resampling_forced && !af_fmt_is_pcm(afmt)) {
+ MP_WARN(p, "ignoring request to resample non-PCM audio for speed change\n");
+ p->resampling_forced = false;
+ }
+
+ bool format_change = afmt != p->in_afmt ||
+ srate != p->in_srate ||
+ !mp_chmap_equals(&chmap, &p->in_chmap) ||
+ p->force_update;
+
+ if (!format_change && (!p->resampling_forced || p->sub.filter))
+ goto cont;
+
+ if (!mp_subfilter_drain_destroy(&p->sub))
+ return;
+
+ if (format_change && p->public.on_audio_format_change) {
+ if (p->format_change_blocked)
+ return;
+
+ if (!p->format_change_cont) {
+ p->format_change_blocked = true;
+ p->public.
+ on_audio_format_change(p->public.on_audio_format_change_opaque);
+ return;
+ }
+ p->format_change_cont = false;
+ }
+
+ p->in_afmt = afmt;
+ p->in_srate = srate;
+ p->in_chmap = chmap;
+ p->force_update = false;
+
+ int out_afmt = 0;
+ int best_score = 0;
+ for (int n = 0; n < p->num_afmts; n++) {
+ int score = af_format_conversion_score(p->afmts[n], afmt);
+ if (!out_afmt || score > best_score) {
+ best_score = score;
+ out_afmt = p->afmts[n];
+ }
+ }
+ if (!out_afmt)
+ out_afmt = afmt;
+
+ // (The p->srates array is 0-terminated already.)
+ int out_srate = af_select_best_samplerate(srate, p->srates);
+ if (out_srate <= 0)
+ out_srate = p->num_srates ? p->srates[0] : srate;
+
+ struct mp_chmap out_chmap = chmap;
+ if (p->chmaps.num_chmaps) {
+ if (!mp_chmap_sel_adjust(&p->chmaps, &out_chmap))
+ out_chmap = p->chmaps.chmaps[0]; // violently force fallback
+ }
+
+ if (out_afmt == p->in_afmt && out_srate == p->in_srate &&
+ mp_chmap_equals(&out_chmap, &p->in_chmap) && !p->resampling_forced)
+ {
+ goto cont;
+ }
+
+ MP_VERBOSE(p, "inserting resampler\n");
+
+ struct mp_swresample *s = mp_swresample_create(f, NULL);
+ if (!s)
+ abort();
+
+ s->out_format = out_afmt;
+ s->out_rate = out_srate;
+ s->out_channels = out_chmap;
+
+ p->sub.filter = s->f;
+
+cont:
+
+ if (p->sub.filter) {
+ struct mp_filter_command cmd = {
+ .type = MP_FILTER_COMMAND_SET_SPEED_RESAMPLE,
+ .speed = p->audio_speed,
+ };
+ mp_filter_command(p->sub.filter, &cmd);
+ }
+
+ mp_subfilter_continue(&p->sub);
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_subfilter_read(&p->sub))
+ return;
+
+ if (p->sub.frame.type == MP_FRAME_VIDEO) {
+ handle_video_frame(f);
+ return;
+ }
+
+ if (p->sub.frame.type == MP_FRAME_AUDIO) {
+ handle_audio_frame(f);
+ return;
+ }
+
+ mp_subfilter_continue(&p->sub);
+}
+
+void mp_autoconvert_format_change_continue(struct mp_autoconvert *c)
+{
+ struct priv *p = c->f->priv;
+
+ if (p->format_change_blocked) {
+ p->format_change_cont = true;
+ p->format_change_blocked = false;
+ mp_filter_wakeup(c->f);
+ }
+}
+
+static bool command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *p = f->priv;
+
+ if (cmd->type == MP_FILTER_COMMAND_SET_SPEED_RESAMPLE) {
+ p->audio_speed = cmd->speed;
+ // If we needed resampling once, keep forcing resampling, as it might be
+ // quickly changing between 1.0 and other values for A/V compensation.
+ if (p->audio_speed != 1.0)
+ p->resampling_forced = true;
+ return true;
+ }
+
+ if (cmd->type == MP_FILTER_COMMAND_IS_ACTIVE) {
+ cmd->is_active = !!p->sub.filter;
+ return true;
+ }
+
+ return false;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+
+ p->format_change_cont = false;
+ p->format_change_blocked = false;
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ mp_subfilter_reset(&p->sub);
+ TA_FREEP(&p->sub.filter);
+}
+
+static const struct mp_filter_info autoconvert_filter = {
+ .name = "autoconvert",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .command = command,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+struct mp_autoconvert *mp_autoconvert_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &autoconvert_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->public.f = f;
+ p->log = f->log;
+ p->audio_speed = 1.0;
+ p->sub.in = f->ppins[0];
+ p->sub.out = f->ppins[1];
+
+ return &p->public;
+}
diff --git a/filters/f_autoconvert.h b/filters/f_autoconvert.h
new file mode 100644
index 0000000..6d6660c
--- /dev/null
+++ b/filters/f_autoconvert.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "filter.h"
+#include "video/sws_utils.h"
+
+struct mp_image;
+struct mp_image_params;
+
+// A filter which automatically creates and uses a conversion filter based on
+// the filter settings, or passes through data unchanged if no conversion is
+// required.
+struct mp_autoconvert {
+ // f->pins[0] is input, f->pins[1] is output
+ struct mp_filter *f;
+
+ enum mp_sws_scaler force_scaler;
+
+ // If this is set, the callback is invoked (from the process function), and
+ // further data flow is blocked until mp_autoconvert_format_change_continue()
+ // is called. The idea is that you can reselect the output parameters on
+ // format changes and continue filtering when ready.
+ void (*on_audio_format_change)(void *opaque);
+ void *on_audio_format_change_opaque;
+};
+
+// (to free this, free the filter itself, mp_autoconvert.f)
+struct mp_autoconvert *mp_autoconvert_create(struct mp_filter *parent);
+
+// Require that output frames have the following params set.
+// This implicitly clears the image format list, and calls
+// mp_autoconvert_add_imgfmt() with the values in *p.
+// Idempotent on subsequent calls (no reinit forced if parameters don't change).
+// Mixing this with other format-altering calls has undefined effects.
+void mp_autoconvert_set_target_image_params(struct mp_autoconvert *c,
+ struct mp_image_params *p);
+
+// Add the imgfmt as allowed video image format, and error on non-video frames.
+// Each call adds to the list of allowed formats. Before the first call, all
+// formats are allowed (even non-video).
+// subfmt can be used to specify underlying surface formats for hardware formats,
+// otherwise must be 0. (Mismatches lead to conversion errors.)
+void mp_autoconvert_add_imgfmt(struct mp_autoconvert *c, int imgfmt, int subfmt);
+
+// Add all sw image formats. The effect is that hardware video image formats are
+// disallowed. The semantics are the same as calling mp_autoconvert_add_imgfmt()
+// for each sw format that exists.
+// No need to do this if you add sw formats with mp_autoconvert_add_imgfmt(),
+// as the normal semantics will exclude other formats (including hw ones).
+void mp_autoconvert_add_all_sw_imgfmts(struct mp_autoconvert *c);
+
+// Approximate test for whether the input would be accepted for conversion
+// according to the current settings. If false is returned, conversion will
+// definitely fail; if true is returned, it might succeed, but with no hard
+// guarantee. This is mainly intended for better error reporting to the user.
+// The result is "approximate" because it could still fail at runtime.
+// The mp_image is not mutated.
+// This function is relatively slow.
+// Accepting mp_image instead of any mp_frame is the result of laziness.
+bool mp_autoconvert_probe_input_video(struct mp_autoconvert *c,
+ struct mp_image *img);
+
+// This is pointless.
+struct mp_hwdec_devices;
+void mp_autoconvert_add_vo_hwdec_subfmts(struct mp_autoconvert *c,
+ struct mp_hwdec_devices *devs);
+
+// Add afmt (an AF_FORMAT_* value) as allowed audio format.
+// See mp_autoconvert_add_imgfmt() for other remarks.
+void mp_autoconvert_add_afmt(struct mp_autoconvert *c, int afmt);
+
+// Add allowed audio channel configuration.
+struct mp_chmap;
+void mp_autoconvert_add_chmap(struct mp_autoconvert *c, struct mp_chmap *chmap);
+
+// Add allowed audio sample rate.
+void mp_autoconvert_add_srate(struct mp_autoconvert *c, int rate);
+
+// Reset set of allowed formats back to initial state. (This does not flush
+// any frames or remove currently active filters, although to get reasonable
+// behavior, you need to readd all previously allowed formats, or reset the
+// filter.)
+void mp_autoconvert_clear(struct mp_autoconvert *c);
+
+// See mp_autoconvert.on_audio_format_change.
+void mp_autoconvert_format_change_continue(struct mp_autoconvert *c);
diff --git a/filters/f_decoder_wrapper.c b/filters/f_decoder_wrapper.c
new file mode 100644
index 0000000..76b9707
--- /dev/null
+++ b/filters/f_decoder_wrapper.c
@@ -0,0 +1,1326 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+
+#include <libavutil/buffer.h>
+#include <libavutil/common.h>
+#include <libavutil/rational.h>
+
+#include "options/options.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+
+#include "demux/demux.h"
+#include "demux/packet.h"
+
+#include "common/codecs.h"
+#include "common/global.h"
+#include "common/recorder.h"
+#include "misc/dispatch.h"
+
+#include "audio/aframe.h"
+#include "video/out/vo.h"
+#include "video/csputils.h"
+
+#include "demux/stheader.h"
+
+#include "f_async_queue.h"
+#include "f_decoder_wrapper.h"
+#include "f_demux_in.h"
+#include "filter_internal.h"
+
+struct dec_queue_opts {
+ bool use_queue;
+ int64_t max_bytes;
+ int64_t max_samples;
+ double max_duration;
+};
+
+#define OPT_BASE_STRUCT struct dec_queue_opts
+
+static const struct m_option dec_queue_opts_list[] = {
+ {"enable", OPT_BOOL(use_queue)},
+ {"max-secs", OPT_DOUBLE(max_duration), M_RANGE(0, DBL_MAX)},
+ {"max-bytes", OPT_BYTE_SIZE(max_bytes), M_RANGE(0, M_MAX_MEM_BYTES)},
+ {"max-samples", OPT_INT64(max_samples), M_RANGE(0, DBL_MAX)},
+ {0}
+};
+
+static const struct m_sub_options vdec_queue_conf = {
+ .opts = dec_queue_opts_list,
+ .size = sizeof(struct dec_queue_opts),
+ .defaults = &(const struct dec_queue_opts){
+ .max_bytes = 512 * 1024 * 1024,
+ .max_samples = 50,
+ .max_duration = 2,
+ },
+};
+
+static const struct m_sub_options adec_queue_conf = {
+ .opts = dec_queue_opts_list,
+ .size = sizeof(struct dec_queue_opts),
+ .defaults = &(const struct dec_queue_opts){
+ .max_bytes = 1 * 1024 * 1024,
+ .max_samples = 48000,
+ .max_duration = 1,
+ },
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct dec_wrapper_opts
+
+struct dec_wrapper_opts {
+ double movie_aspect;
+ int aspect_method;
+ double fps_override;
+ bool correct_pts;
+ int video_rotate;
+ char *audio_decoders;
+ char *video_decoders;
+ char *audio_spdif;
+ struct dec_queue_opts *vdec_queue_opts;
+ struct dec_queue_opts *adec_queue_opts;
+ int64_t video_reverse_size;
+ int64_t audio_reverse_size;
+};
+
+static int decoder_list_help(struct mp_log *log, const m_option_t *opt,
+ struct bstr name);
+
+const struct m_sub_options dec_wrapper_conf = {
+ .opts = (const struct m_option[]){
+ {"correct-pts", OPT_BOOL(correct_pts)},
+ {"container-fps-override", OPT_DOUBLE(fps_override), M_RANGE(0, DBL_MAX)},
+ {"ad", OPT_STRING(audio_decoders),
+ .help = decoder_list_help},
+ {"vd", OPT_STRING(video_decoders),
+ .help = decoder_list_help},
+ {"audio-spdif", OPT_STRING(audio_spdif),
+ .help = decoder_list_help},
+ {"video-rotate", OPT_CHOICE(video_rotate, {"no", -1}),
+ .flags = UPDATE_IMGPAR, M_RANGE(0, 359)},
+ {"video-aspect-override", OPT_ASPECT(movie_aspect),
+ .flags = UPDATE_IMGPAR, M_RANGE(-1, 10)},
+ {"video-aspect-method", OPT_CHOICE(aspect_method,
+ {"bitstream", 1}, {"container", 2}),
+ .flags = UPDATE_IMGPAR},
+ {"vd-queue", OPT_SUBSTRUCT(vdec_queue_opts, vdec_queue_conf)},
+ {"ad-queue", OPT_SUBSTRUCT(adec_queue_opts, adec_queue_conf)},
+ {"video-reversal-buffer", OPT_BYTE_SIZE(video_reverse_size),
+ M_RANGE(0, M_MAX_MEM_BYTES)},
+ {"audio-reversal-buffer", OPT_BYTE_SIZE(audio_reverse_size),
+ M_RANGE(0, M_MAX_MEM_BYTES)},
+ {"fps", OPT_REPLACED("container-fps-override")},
+ {0}
+ },
+ .size = sizeof(struct dec_wrapper_opts),
+ .defaults = &(const struct dec_wrapper_opts){
+ .correct_pts = true,
+ .movie_aspect = -1.,
+ .aspect_method = 2,
+ .video_reverse_size = 1 * 1024 * 1024 * 1024,
+ .audio_reverse_size = 64 * 1024 * 1024,
+ },
+};
+
+struct priv {
+ struct mp_log *log;
+ struct sh_stream *header;
+
+ // --- The following fields are to be accessed by dec_dispatch (or if that
+ // field is NULL, by the mp_decoder_wrapper user thread).
+ // Use thread_lock() for access outside of the decoder thread.
+
+ bool request_terminate_dec_thread;
+ struct mp_filter *dec_root_filter; // thread root filter; no thread => NULL
+ struct mp_filter *decf; // wrapper filter which drives the decoder
+ struct m_config_cache *opt_cache;
+ struct dec_wrapper_opts *opts;
+ struct dec_queue_opts *queue_opts;
+ struct mp_stream_info stream_info;
+
+ struct mp_codec_params *codec;
+ struct mp_decoder *decoder;
+
+ // Demuxer output.
+ struct mp_pin *demux;
+
+ // Last PTS from decoder (set with each vd_driver->decode() call)
+ double codec_pts;
+ int num_codec_pts_problems;
+
+ // Last packet DTS from decoder (passed through from source packets)
+ double codec_dts;
+ int num_codec_dts_problems;
+
+ // PTS or DTS of packet first read
+ double first_packet_pdts;
+
+ // There was at least one packet with nonsense timestamps.
+ // Intentionally not reset on seeks; its whole purpose is to enable faster
+ // future seeks.
+ int has_broken_packet_pts; // <0: uninitialized, 0: no problems, 1: broken
+
+ int has_broken_decoded_pts;
+
+ int packets_without_output; // number packets sent without frame received
+
+ // Final PTS of previously decoded frame
+ double pts;
+
+ struct mp_image_params dec_format, last_format, fixed_format;
+
+ double fps;
+
+ double start_pts;
+ double start, end;
+ struct demux_packet *new_segment;
+ struct mp_frame packet;
+ bool packet_fed, preroll_discard;
+
+ size_t reverse_queue_byte_size;
+ struct mp_frame *reverse_queue;
+ int num_reverse_queue;
+ bool reverse_queue_complete;
+
+ struct mp_frame decoded_coverart;
+ int coverart_returned; // 0: no, 1: coverart frame itself, 2: EOF returned
+
+ int play_dir;
+
+ // --- The following fields can be accessed only from the mp_decoder_wrapper
+ // user thread.
+ struct mp_decoder_wrapper public;
+
+ // --- Specific access depending on threading stuff.
+ struct mp_async_queue *queue; // decoded frame output queue
+ struct mp_dispatch_queue *dec_dispatch; // non-NULL if decoding thread used
+ bool dec_thread_lock; // debugging (esp. for no-thread case)
+ mp_thread dec_thread;
+ bool dec_thread_valid;
+ mp_mutex cache_lock;
+
+ // --- Protected by cache_lock.
+ char *cur_hwdec;
+ char *decoder_desc;
+ bool try_spdif;
+ bool attached_picture;
+ bool pts_reset;
+ int attempt_framedrops; // try dropping this many frames
+ int dropped_frames; // total frames _probably_ dropped
+};
+
+static int decoder_list_help(struct mp_log *log, const m_option_t *opt,
+ struct bstr name)
+{
+ if (strcmp(opt->name, "ad") == 0) {
+ struct mp_decoder_list *list = audio_decoder_list();
+ mp_print_decoders(log, MSGL_INFO, "Audio decoders:", list);
+ talloc_free(list);
+ return M_OPT_EXIT;
+ }
+ if (strcmp(opt->name, "vd") == 0) {
+ struct mp_decoder_list *list = video_decoder_list();
+ mp_print_decoders(log, MSGL_INFO, "Video decoders:", list);
+ talloc_free(list);
+ return M_OPT_EXIT;
+ }
+ if (strcmp(opt->name, "audio-spdif") == 0) {
+ mp_info(log, "Choices: ac3,dts-hd,dts (and possibly more)\n");
+ return M_OPT_EXIT;
+ }
+ return 1;
+}
+
+// Update cached values for main thread which require access to the decoder
+// thread state. Must run on/locked with decoder thread.
+static void update_cached_values(struct priv *p)
+{
+ mp_mutex_lock(&p->cache_lock);
+
+ p->cur_hwdec = NULL;
+ if (p->decoder && p->decoder->control)
+ p->decoder->control(p->decoder->f, VDCTRL_GET_HWDEC, &p->cur_hwdec);
+
+ mp_mutex_unlock(&p->cache_lock);
+}
+
+// Lock the decoder thread. This may synchronously wait until the decoder thread
+// is done with its current work item (such as waiting for a frame), and thus
+// may block for a while. (I.e. avoid during normal playback.)
+// If no decoder thread is running, this is a no-op, except for some debug stuff.
+static void thread_lock(struct priv *p)
+{
+ if (p->dec_dispatch)
+ mp_dispatch_lock(p->dec_dispatch);
+
+ assert(!p->dec_thread_lock);
+ p->dec_thread_lock = true;
+}
+
+// Undo thread_lock().
+static void thread_unlock(struct priv *p)
+{
+ assert(p->dec_thread_lock);
+ p->dec_thread_lock = false;
+
+ if (p->dec_dispatch)
+ mp_dispatch_unlock(p->dec_dispatch);
+}
+
+// This resets only the decoder. Unlike a full reset(), this doesn't imply a
+// seek reset. This distinction exists only when using timeline stuff (EDL and
+// ordered chapters). timeline stuff needs to reset the decoder state, but keep
+// some of the user-relevant state.
+static void reset_decoder(struct priv *p)
+{
+ p->first_packet_pdts = MP_NOPTS_VALUE;
+ p->start_pts = MP_NOPTS_VALUE;
+ p->codec_pts = MP_NOPTS_VALUE;
+ p->codec_dts = MP_NOPTS_VALUE;
+ p->num_codec_pts_problems = 0;
+ p->num_codec_dts_problems = 0;
+ p->has_broken_decoded_pts = 0;
+ p->packets_without_output = 0;
+ mp_frame_unref(&p->packet);
+ p->packet_fed = false;
+ p->preroll_discard = false;
+ talloc_free(p->new_segment);
+ p->new_segment = NULL;
+ p->start = p->end = MP_NOPTS_VALUE;
+
+ if (p->decoder)
+ mp_filter_reset(p->decoder->f);
+}
+
+static void decf_reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ assert(p->decf == f);
+
+ p->pts = MP_NOPTS_VALUE;
+ p->last_format = p->fixed_format = (struct mp_image_params){0};
+
+ mp_mutex_lock(&p->cache_lock);
+ p->pts_reset = false;
+ p->attempt_framedrops = 0;
+ p->dropped_frames = 0;
+ mp_mutex_unlock(&p->cache_lock);
+
+ p->coverart_returned = 0;
+
+ for (int n = 0; n < p->num_reverse_queue; n++)
+ mp_frame_unref(&p->reverse_queue[n]);
+ p->num_reverse_queue = 0;
+ p->reverse_queue_byte_size = 0;
+ p->reverse_queue_complete = false;
+
+ reset_decoder(p);
+}
+
+int mp_decoder_wrapper_control(struct mp_decoder_wrapper *d,
+ enum dec_ctrl cmd, void *arg)
+{
+ struct priv *p = d->f->priv;
+ int res = CONTROL_UNKNOWN;
+ if (cmd == VDCTRL_GET_HWDEC) {
+ mp_mutex_lock(&p->cache_lock);
+ *(char **)arg = p->cur_hwdec;
+ mp_mutex_unlock(&p->cache_lock);
+ } else {
+ thread_lock(p);
+ if (p->decoder && p->decoder->control)
+ res = p->decoder->control(p->decoder->f, cmd, arg);
+ update_cached_values(p);
+ thread_unlock(p);
+ }
+ return res;
+}
+
+static void decf_destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ assert(p->decf == f);
+
+ if (p->decoder) {
+ MP_DBG(f, "Uninit decoder.\n");
+ talloc_free(p->decoder->f);
+ p->decoder = NULL;
+ }
+
+ decf_reset(f);
+ mp_frame_unref(&p->decoded_coverart);
+}
+
+struct mp_decoder_list *video_decoder_list(void)
+{
+ struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list);
+ vd_lavc.add_decoders(list);
+ return list;
+}
+
+struct mp_decoder_list *audio_decoder_list(void)
+{
+ struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list);
+ ad_lavc.add_decoders(list);
+ return list;
+}
+
+static bool reinit_decoder(struct priv *p)
+{
+ if (p->decoder)
+ talloc_free(p->decoder->f);
+ p->decoder = NULL;
+
+ reset_decoder(p);
+ p->has_broken_packet_pts = -10; // needs 10 packets to reach decision
+
+ talloc_free(p->decoder_desc);
+ p->decoder_desc = NULL;
+
+ const struct mp_decoder_fns *driver = NULL;
+ struct mp_decoder_list *list = NULL;
+ char *user_list = NULL;
+ char *fallback = NULL;
+
+ if (p->codec->type == STREAM_VIDEO) {
+ driver = &vd_lavc;
+ user_list = p->opts->video_decoders;
+ fallback = "h264";
+ } else if (p->codec->type == STREAM_AUDIO) {
+ driver = &ad_lavc;
+ user_list = p->opts->audio_decoders;
+ fallback = "aac";
+
+ mp_mutex_lock(&p->cache_lock);
+ bool try_spdif = p->try_spdif;
+ mp_mutex_unlock(&p->cache_lock);
+
+ if (try_spdif && p->codec->codec) {
+ struct mp_decoder_list *spdif =
+ select_spdif_codec(p->codec->codec, p->opts->audio_spdif);
+ if (spdif->num_entries) {
+ driver = &ad_spdif;
+ list = spdif;
+ } else {
+ talloc_free(spdif);
+ }
+ }
+ }
+
+ if (!driver)
+ return false;
+
+ if (!list) {
+ struct mp_decoder_list *full = talloc_zero(NULL, struct mp_decoder_list);
+ driver->add_decoders(full);
+ const char *codec = p->codec->codec;
+ if (codec && strcmp(codec, "null") == 0)
+ codec = fallback;
+ list = mp_select_decoders(p->log, full, codec, user_list);
+ talloc_free(full);
+ }
+
+ mp_print_decoders(p->log, MSGL_V, "Codec list:", list);
+
+ for (int n = 0; n < list->num_entries; n++) {
+ struct mp_decoder_entry *sel = &list->entries[n];
+ MP_VERBOSE(p, "Opening decoder %s\n", sel->decoder);
+
+ p->decoder = driver->create(p->decf, p->codec, sel->decoder);
+ if (p->decoder) {
+ mp_mutex_lock(&p->cache_lock);
+ const char *d = sel->desc && sel->desc[0] ? sel->desc : sel->decoder;
+ p->decoder_desc = talloc_strdup(p, d);
+ MP_VERBOSE(p, "Selected codec: %s\n", p->decoder_desc);
+ mp_mutex_unlock(&p->cache_lock);
+ break;
+ }
+
+ MP_WARN(p, "Decoder init failed for %s\n", sel->decoder);
+ }
+
+ if (!p->decoder) {
+ MP_ERR(p, "Failed to initialize a decoder for codec '%s'.\n",
+ p->codec->codec ? p->codec->codec : "<?>");
+ }
+
+ update_cached_values(p);
+
+ talloc_free(list);
+ return !!p->decoder;
+}
+
+bool mp_decoder_wrapper_reinit(struct mp_decoder_wrapper *d)
+{
+ struct priv *p = d->f->priv;
+ thread_lock(p);
+ bool res = reinit_decoder(p);
+ thread_unlock(p);
+ return res;
+}
+
+void mp_decoder_wrapper_get_desc(struct mp_decoder_wrapper *d,
+ char *buf, size_t buf_size)
+{
+ struct priv *p = d->f->priv;
+ mp_mutex_lock(&p->cache_lock);
+ snprintf(buf, buf_size, "%s", p->decoder_desc ? p->decoder_desc : "");
+ mp_mutex_unlock(&p->cache_lock);
+}
+
+void mp_decoder_wrapper_set_frame_drops(struct mp_decoder_wrapper *d, int num)
+{
+ struct priv *p = d->f->priv;
+ mp_mutex_lock(&p->cache_lock);
+ p->attempt_framedrops = num;
+ mp_mutex_unlock(&p->cache_lock);
+}
+
+int mp_decoder_wrapper_get_frames_dropped(struct mp_decoder_wrapper *d)
+{
+ struct priv *p = d->f->priv;
+ mp_mutex_lock(&p->cache_lock);
+ int res = p->dropped_frames;
+ mp_mutex_unlock(&p->cache_lock);
+ return res;
+}
+
+double mp_decoder_wrapper_get_container_fps(struct mp_decoder_wrapper *d)
+{
+ struct priv *p = d->f->priv;
+ thread_lock(p);
+ double res = p->fps;
+ thread_unlock(p);
+ return res;
+}
+
+void mp_decoder_wrapper_set_spdif_flag(struct mp_decoder_wrapper *d, bool spdif)
+{
+ struct priv *p = d->f->priv;
+ mp_mutex_lock(&p->cache_lock);
+ p->try_spdif = spdif;
+ mp_mutex_unlock(&p->cache_lock);
+}
+
+void mp_decoder_wrapper_set_coverart_flag(struct mp_decoder_wrapper *d, bool c)
+{
+ struct priv *p = d->f->priv;
+ mp_mutex_lock(&p->cache_lock);
+ p->attached_picture = c;
+ mp_mutex_unlock(&p->cache_lock);
+}
+
+bool mp_decoder_wrapper_get_pts_reset(struct mp_decoder_wrapper *d)
+{
+ struct priv *p = d->f->priv;
+ mp_mutex_lock(&p->cache_lock);
+ bool res = p->pts_reset;
+ mp_mutex_unlock(&p->cache_lock);
+ return res;
+}
+
+void mp_decoder_wrapper_set_play_dir(struct mp_decoder_wrapper *d, int dir)
+{
+ struct priv *p = d->f->priv;
+ thread_lock(p);
+ p->play_dir = dir;
+ thread_unlock(p);
+}
+
+static void fix_image_params(struct priv *p,
+ struct mp_image_params *params)
+{
+ struct mp_image_params m = *params;
+ struct mp_codec_params *c = p->codec;
+ struct dec_wrapper_opts *opts = p->opts;
+
+ MP_VERBOSE(p, "Decoder format: %s\n", mp_image_params_to_str(params));
+ p->dec_format = *params;
+
+ // While mp_image_params normally always have to have d_w/d_h set, the
+ // decoder signals unknown bitstream aspect ratio with both set to 0.
+ bool use_container = true;
+ if (opts->aspect_method == 1 && m.p_w > 0 && m.p_h > 0) {
+ MP_VERBOSE(p, "Using bitstream aspect ratio.\n");
+ use_container = false;
+ }
+
+ if (use_container && c->par_w > 0 && c->par_h) {
+ MP_VERBOSE(p, "Using container aspect ratio.\n");
+ m.p_w = c->par_w;
+ m.p_h = c->par_h;
+ }
+
+ if (opts->movie_aspect >= 0) {
+ MP_VERBOSE(p, "Forcing user-set aspect ratio.\n");
+ if (opts->movie_aspect == 0) {
+ m.p_w = m.p_h = 1;
+ } else {
+ AVRational a = av_d2q(opts->movie_aspect, INT_MAX);
+ mp_image_params_set_dsize(&m, a.num, a.den);
+ }
+ }
+
+ // Assume square pixels if no aspect ratio is set at all.
+ if (m.p_w <= 0 || m.p_h <= 0)
+ m.p_w = m.p_h = 1;
+
+ m.stereo3d = p->codec->stereo_mode;
+
+ if (!mp_rect_equals(&p->codec->crop, &(struct mp_rect){0})) {
+ struct mp_rect crop = p->codec->crop;
+ // Offset to respect existing decoder crop.
+ crop.x0 += m.crop.x0;
+ crop.x1 += m.crop.x0;
+ crop.y0 += m.crop.y0;
+ crop.y1 += m.crop.y0;
+ // Crop has to be inside existing image bounds.
+ if (mp_image_crop_valid(&(struct mp_image_params) {
+ .w = mp_rect_w(m.crop), .h = mp_rect_h(m.crop), .crop = crop }))
+ {
+ m.crop = crop;
+ } else {
+ MP_WARN(p, "Invalid container crop %dx%d+%d+%d for %dx%d image\n",
+ mp_rect_w(crop), mp_rect_h(crop), crop.x0, crop.y0,
+ mp_rect_w(m.crop), mp_rect_h(m.crop));
+ }
+ }
+
+ if (opts->video_rotate < 0) {
+ m.rotate = 0;
+ } else {
+ // ffmpeg commit 535a835e51 says that frame rotate takes priority
+ if (!m.rotate)
+ m.rotate = p->codec->rotate;
+ m.rotate = (m.rotate + opts->video_rotate) % 360;
+ }
+
+ mp_colorspace_merge(&m.color, &c->color);
+
+ // Guess missing colorspace fields from metadata. This guarantees all
+ // fields are at least set to legal values afterwards.
+ mp_image_params_guess_csp(&m);
+
+ p->last_format = *params;
+ p->fixed_format = m;
+}
+
+void mp_decoder_wrapper_reset_params(struct mp_decoder_wrapper *d)
+{
+ struct priv *p = d->f->priv;
+ p->last_format = (struct mp_image_params){0};
+}
+
+void mp_decoder_wrapper_get_video_dec_params(struct mp_decoder_wrapper *d,
+ struct mp_image_params *m)
+{
+ struct priv *p = d->f->priv;
+ *m = p->dec_format;
+}
+
+// This code exists only because multimedia is so god damn crazy. In a sane
+// world, the video decoder would always output a video frame with a valid PTS;
+// this deals with cases where it doesn't.
+static void crazy_video_pts_stuff(struct priv *p, struct mp_image *mpi)
+{
+ // Note: the PTS is reordered, but the DTS is not. Both must be monotonic.
+
+ if (mpi->pts != MP_NOPTS_VALUE) {
+ if (mpi->pts < p->codec_pts)
+ p->num_codec_pts_problems++;
+ p->codec_pts = mpi->pts;
+ }
+
+ if (mpi->dts != MP_NOPTS_VALUE) {
+ if (mpi->dts <= p->codec_dts)
+ p->num_codec_dts_problems++;
+ p->codec_dts = mpi->dts;
+ }
+
+ if (p->has_broken_packet_pts < 0)
+ p->has_broken_packet_pts++;
+ if (p->num_codec_pts_problems)
+ p->has_broken_packet_pts = 1;
+
+ // If PTS is unset, or non-monotonic, fall back to DTS.
+ if ((p->num_codec_pts_problems > p->num_codec_dts_problems ||
+ mpi->pts == MP_NOPTS_VALUE) && mpi->dts != MP_NOPTS_VALUE)
+ mpi->pts = mpi->dts;
+
+ // Compensate for incorrectly using mpeg-style DTS for avi timestamps.
+ if (p->decoder && p->decoder->control && p->codec->avi_dts &&
+ mpi->pts != MP_NOPTS_VALUE && p->fps > 0)
+ {
+ int delay = -1;
+ p->decoder->control(p->decoder->f, VDCTRL_GET_BFRAMES, &delay);
+ mpi->pts -= MPMAX(delay, 0) / p->fps;
+ }
+}
+
+// Return true if the current frame is outside segment range.
+static bool process_decoded_frame(struct priv *p, struct mp_frame *frame)
+{
+ if (frame->type == MP_FRAME_EOF) {
+ // if we were just draining current segment, don't propagate EOF
+ if (p->new_segment)
+ mp_frame_unref(frame);
+ return true;
+ }
+
+ bool segment_ended = false;
+
+ if (frame->type == MP_FRAME_VIDEO) {
+ struct mp_image *mpi = frame->data;
+
+ crazy_video_pts_stuff(p, mpi);
+
+ struct demux_packet *ccpkt = new_demux_packet_from_buf(mpi->a53_cc);
+ if (ccpkt) {
+ av_buffer_unref(&mpi->a53_cc);
+ ccpkt->pts = mpi->pts;
+ ccpkt->dts = mpi->dts;
+ demuxer_feed_caption(p->header, ccpkt);
+ }
+
+ // Stop hr-seek logic.
+ if (mpi->pts == MP_NOPTS_VALUE || mpi->pts >= p->start_pts)
+ p->start_pts = MP_NOPTS_VALUE;
+
+ if (mpi->pts != MP_NOPTS_VALUE) {
+ segment_ended = p->end != MP_NOPTS_VALUE && mpi->pts >= p->end;
+ if ((p->start != MP_NOPTS_VALUE && mpi->pts < p->start) ||
+ segment_ended)
+ {
+ mp_frame_unref(frame);
+ goto done;
+ }
+ }
+ } else if (frame->type == MP_FRAME_AUDIO) {
+ struct mp_aframe *aframe = frame->data;
+
+ mp_aframe_clip_timestamps(aframe, p->start, p->end);
+ double pts = mp_aframe_get_pts(aframe);
+ if (pts != MP_NOPTS_VALUE && p->start != MP_NOPTS_VALUE)
+ segment_ended = pts >= p->end;
+
+ if (mp_aframe_get_size(aframe) == 0) {
+ mp_frame_unref(frame);
+ goto done;
+ }
+ } else {
+ MP_ERR(p, "unknown frame type from decoder\n");
+ }
+
+done:
+ return segment_ended;
+}
+
+static void correct_video_pts(struct priv *p, struct mp_image *mpi)
+{
+ mpi->pts *= p->play_dir;
+
+ if (!p->opts->correct_pts || mpi->pts == MP_NOPTS_VALUE) {
+ double fps = p->fps > 0 ? p->fps : 25;
+
+ if (p->opts->correct_pts) {
+ if (p->has_broken_decoded_pts <= 1) {
+ MP_WARN(p, "No video PTS! Making something up. Using "
+ "%f FPS.\n", fps);
+ if (p->has_broken_decoded_pts == 1)
+ MP_WARN(p, "Ignoring further missing PTS warnings.\n");
+ p->has_broken_decoded_pts++;
+ }
+ }
+
+ double frame_time = 1.0f / fps;
+ double base = p->first_packet_pdts;
+ mpi->pts = p->pts;
+ if (mpi->pts == MP_NOPTS_VALUE) {
+ mpi->pts = base == MP_NOPTS_VALUE ? 0 : base;
+ } else {
+ mpi->pts += frame_time;
+ }
+ }
+
+ p->pts = mpi->pts;
+}
+
+static void correct_audio_pts(struct priv *p, struct mp_aframe *aframe)
+{
+ double dir = p->play_dir;
+
+ double frame_pts = mp_aframe_get_pts(aframe);
+ double frame_len = mp_aframe_duration(aframe);
+
+ if (frame_pts != MP_NOPTS_VALUE) {
+ if (dir < 0)
+ frame_pts = -(frame_pts + frame_len);
+
+ if (p->pts != MP_NOPTS_VALUE)
+ MP_STATS(p, "value %f audio-pts-err", p->pts - frame_pts);
+
+ double diff = fabs(p->pts - frame_pts);
+
+ // Attempt to detect jumps in PTS. Even for the lowest sample rates and
+ // with worst container rounded timestamp, this should be a margin more
+ // than enough.
+ if (p->pts != MP_NOPTS_VALUE && diff > 0.1) {
+ MP_WARN(p, "Invalid audio PTS: %f -> %f\n", p->pts, frame_pts);
+ if (diff >= 5) {
+ mp_mutex_lock(&p->cache_lock);
+ p->pts_reset = true;
+ mp_mutex_unlock(&p->cache_lock);
+ }
+ }
+
+ // Keep the interpolated timestamp if it doesn't deviate more
+ // than 1 ms from the real one. (MKV rounded timestamps.)
+ if (p->pts == MP_NOPTS_VALUE || diff > 0.001)
+ p->pts = frame_pts;
+ }
+
+ if (p->pts == MP_NOPTS_VALUE && p->header->missing_timestamps)
+ p->pts = 0;
+
+ mp_aframe_set_pts(aframe, p->pts);
+
+ if (p->pts != MP_NOPTS_VALUE)
+ p->pts += frame_len;
+}
+
+static void process_output_frame(struct priv *p, struct mp_frame frame)
+{
+ if (frame.type == MP_FRAME_VIDEO) {
+ struct mp_image *mpi = frame.data;
+
+ correct_video_pts(p, mpi);
+
+ if (!mp_image_params_equal(&p->last_format, &mpi->params))
+ fix_image_params(p, &mpi->params);
+
+ mpi->params = p->fixed_format;
+ mpi->nominal_fps = p->fps;
+ } else if (frame.type == MP_FRAME_AUDIO) {
+ struct mp_aframe *aframe = frame.data;
+
+ if (p->play_dir < 0 && !mp_aframe_reverse(aframe))
+ MP_ERR(p, "Couldn't reverse audio frame.\n");
+
+ correct_audio_pts(p, aframe);
+ }
+}
+
+void mp_decoder_wrapper_set_start_pts(struct mp_decoder_wrapper *d, double pts)
+{
+ struct priv *p = d->f->priv;
+ p->start_pts = pts;
+}
+
+static bool is_new_segment(struct priv *p, struct mp_frame frame)
+{
+ if (frame.type != MP_FRAME_PACKET)
+ return false;
+ struct demux_packet *pkt = frame.data;
+ return (pkt->segmented && (pkt->start != p->start || pkt->end != p->end ||
+ pkt->codec != p->codec)) ||
+ (p->play_dir < 0 && pkt->back_restart && p->packet_fed);
+}
+
+static void feed_packet(struct priv *p)
+{
+ if (!p->decoder || !mp_pin_in_needs_data(p->decoder->f->pins[0]))
+ return;
+
+ if (p->decoded_coverart.type)
+ return;
+
+ if (!p->packet.type && !p->new_segment) {
+ p->packet = mp_pin_out_read(p->demux);
+ if (!p->packet.type)
+ return;
+ if (p->packet.type != MP_FRAME_EOF && p->packet.type != MP_FRAME_PACKET) {
+ MP_ERR(p, "invalid frame type from demuxer\n");
+ mp_frame_unref(&p->packet);
+ mp_filter_internal_mark_failed(p->decf);
+ return;
+ }
+ }
+
+ if (!p->packet.type)
+ return;
+
+ // Flush current data if the packet is a new segment.
+ if (is_new_segment(p, p->packet)) {
+ assert(!p->new_segment);
+ p->new_segment = p->packet.data;
+ p->packet = MP_EOF_FRAME;
+ }
+
+ assert(p->packet.type == MP_FRAME_PACKET || p->packet.type == MP_FRAME_EOF);
+ struct demux_packet *packet =
+ p->packet.type == MP_FRAME_PACKET ? p->packet.data : NULL;
+
+ // For video framedropping, including parts of the hr-seek logic.
+ if (p->decoder->control) {
+ double start_pts = p->start_pts;
+ if (p->start != MP_NOPTS_VALUE && (start_pts == MP_NOPTS_VALUE ||
+ p->start > start_pts))
+ start_pts = p->start;
+
+ int framedrop_type = 0;
+
+ mp_mutex_lock(&p->cache_lock);
+ if (p->attempt_framedrops)
+ framedrop_type = 1;
+ mp_mutex_unlock(&p->cache_lock);
+
+ if (start_pts != MP_NOPTS_VALUE && packet && p->play_dir > 0 &&
+ packet->pts < start_pts - .005 && !p->has_broken_packet_pts)
+ framedrop_type = 2;
+
+ p->decoder->control(p->decoder->f, VDCTRL_SET_FRAMEDROP, &framedrop_type);
+ }
+
+ if (!p->dec_dispatch && p->public.recorder_sink)
+ mp_recorder_feed_packet(p->public.recorder_sink, packet);
+
+ double pkt_pts = packet ? packet->pts : MP_NOPTS_VALUE;
+ double pkt_dts = packet ? packet->dts : MP_NOPTS_VALUE;
+
+ if (pkt_pts == MP_NOPTS_VALUE)
+ p->has_broken_packet_pts = 1;
+
+ if (packet && packet->dts == MP_NOPTS_VALUE && !p->codec->avi_dts)
+ packet->dts = packet->pts;
+
+ double pkt_pdts = pkt_pts == MP_NOPTS_VALUE ? pkt_dts : pkt_pts;
+ if (p->first_packet_pdts == MP_NOPTS_VALUE)
+ p->first_packet_pdts = pkt_pdts;
+
+ if (packet && packet->back_preroll) {
+ p->preroll_discard = true;
+ packet->pts = packet->dts = MP_NOPTS_VALUE;
+ }
+
+ mp_pin_in_write(p->decoder->f->pins[0], p->packet);
+ p->packet_fed = true;
+ p->packet = MP_NO_FRAME;
+
+ p->packets_without_output += 1;
+}
+
+static void enqueue_backward_frame(struct priv *p, struct mp_frame frame)
+{
+ bool eof = frame.type == MP_FRAME_EOF;
+
+ if (!eof) {
+ struct dec_wrapper_opts *opts = p->opts;
+
+ uint64_t queue_size = 0;
+ switch (p->header->type) {
+ case STREAM_VIDEO: queue_size = opts->video_reverse_size; break;
+ case STREAM_AUDIO: queue_size = opts->audio_reverse_size; break;
+ }
+
+ if (p->reverse_queue_byte_size >= queue_size) {
+ MP_ERR(p, "Reversal queue overflow, discarding frame.\n");
+ mp_frame_unref(&frame);
+ return;
+ }
+
+ p->reverse_queue_byte_size += mp_frame_approx_size(frame);
+ }
+
+ // Note: EOF (really BOF) is propagated, but not reversed.
+ MP_TARRAY_INSERT_AT(p, p->reverse_queue, p->num_reverse_queue,
+ eof ? 0 : p->num_reverse_queue, frame);
+
+ p->reverse_queue_complete = eof;
+}
+
+static void read_frame(struct priv *p)
+{
+ struct mp_pin *pin = p->decf->ppins[0];
+ struct mp_frame frame = {0};
+
+ if (!p->decoder || !mp_pin_in_needs_data(pin))
+ return;
+
+ if (p->decoded_coverart.type) {
+ if (p->coverart_returned == 0) {
+ frame = mp_frame_ref(p->decoded_coverart);
+ p->coverart_returned = 1;
+ goto output_frame;
+ } else if (p->coverart_returned == 1) {
+ frame = MP_EOF_FRAME;
+ p->coverart_returned = 2;
+ goto output_frame;
+ }
+ return;
+ }
+
+ if (p->reverse_queue_complete && p->num_reverse_queue) {
+ frame = p->reverse_queue[p->num_reverse_queue - 1];
+ p->num_reverse_queue -= 1;
+ goto output_frame;
+ }
+ p->reverse_queue_complete = false;
+
+ frame = mp_pin_out_read(p->decoder->f->pins[1]);
+ if (!frame.type)
+ return;
+
+ mp_mutex_lock(&p->cache_lock);
+ if (p->attached_picture && frame.type == MP_FRAME_VIDEO)
+ p->decoded_coverart = frame;
+ if (p->attempt_framedrops) {
+ int dropped = MPMAX(0, p->packets_without_output - 1);
+ p->attempt_framedrops = MPMAX(0, p->attempt_framedrops - dropped);
+ p->dropped_frames += dropped;
+ }
+ mp_mutex_unlock(&p->cache_lock);
+
+ if (p->decoded_coverart.type) {
+ mp_filter_internal_mark_progress(p->decf);
+ return;
+ }
+
+ p->packets_without_output = 0;
+
+ if (p->preroll_discard && frame.type != MP_FRAME_EOF) {
+ double ts = mp_frame_get_pts(frame);
+ if (ts == MP_NOPTS_VALUE) {
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_progress(p->decf);
+ return;
+ }
+ p->preroll_discard = false;
+ }
+
+ bool segment_ended = process_decoded_frame(p, &frame);
+
+ if (p->play_dir < 0 && frame.type) {
+ enqueue_backward_frame(p, frame);
+ frame = MP_NO_FRAME;
+ }
+
+ // If there's a new segment, start it as soon as we're drained/finished.
+ if (segment_ended && p->new_segment) {
+ struct demux_packet *new_segment = p->new_segment;
+ p->new_segment = NULL;
+
+ reset_decoder(p);
+
+ if (new_segment->segmented) {
+ if (p->codec != new_segment->codec) {
+ p->codec = new_segment->codec;
+ if (!mp_decoder_wrapper_reinit(&p->public))
+ mp_filter_internal_mark_failed(p->decf);
+ }
+
+ p->start = new_segment->start;
+ p->end = new_segment->end;
+ }
+
+ p->reverse_queue_byte_size = 0;
+ p->reverse_queue_complete = p->num_reverse_queue > 0;
+
+ p->packet = MAKE_FRAME(MP_FRAME_PACKET, new_segment);
+ mp_filter_internal_mark_progress(p->decf);
+ }
+
+ if (!frame.type) {
+ mp_filter_internal_mark_progress(p->decf); // make it retry
+ return;
+ }
+
+output_frame:
+ process_output_frame(p, frame);
+ mp_pin_in_write(pin, frame);
+}
+
+static void update_queue_config(struct priv *p)
+{
+ if (!p->queue)
+ return;
+
+ struct mp_async_queue_config cfg = {
+ .max_bytes = p->queue_opts->max_bytes,
+ .sample_unit = AQUEUE_UNIT_SAMPLES,
+ .max_samples = p->queue_opts->max_samples,
+ .max_duration = p->queue_opts->max_duration,
+ };
+ mp_async_queue_set_config(p->queue, cfg);
+}
+
+static void decf_process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ assert(p->decf == f);
+
+ if (m_config_cache_update(p->opt_cache))
+ update_queue_config(p);
+
+ feed_packet(p);
+ read_frame(p);
+}
+
+static MP_THREAD_VOID dec_thread(void *ptr)
+{
+ struct priv *p = ptr;
+
+ char *t_name = "dec/?";
+ switch (p->header->type) {
+ case STREAM_VIDEO: t_name = "dec/video"; break;
+ case STREAM_AUDIO: t_name = "dec/audio"; break;
+ }
+ mp_thread_set_name(t_name);
+
+ while (!p->request_terminate_dec_thread) {
+ mp_filter_graph_run(p->dec_root_filter);
+ update_cached_values(p);
+ mp_dispatch_queue_process(p->dec_dispatch, INFINITY);
+ }
+
+ MP_THREAD_RETURN();
+}
+
+static void public_f_reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ assert(p->public.f == f);
+
+ if (p->queue) {
+ mp_async_queue_reset(p->queue);
+ thread_lock(p);
+ if (p->dec_root_filter)
+ mp_filter_reset(p->dec_root_filter);
+ mp_dispatch_interrupt(p->dec_dispatch);
+ thread_unlock(p);
+ mp_async_queue_resume(p->queue);
+ }
+}
+
+static void public_f_destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ assert(p->public.f == f);
+
+ if (p->dec_thread_valid) {
+ assert(p->dec_dispatch);
+ thread_lock(p);
+ p->request_terminate_dec_thread = 1;
+ mp_dispatch_interrupt(p->dec_dispatch);
+ thread_unlock(p);
+ mp_thread_join(p->dec_thread);
+ p->dec_thread_valid = false;
+ }
+
+ mp_filter_free_children(f);
+
+ talloc_free(p->dec_root_filter);
+ talloc_free(p->queue);
+ mp_mutex_destroy(&p->cache_lock);
+}
+
+static const struct mp_filter_info decf_filter = {
+ .name = "decode",
+ .process = decf_process,
+ .reset = decf_reset,
+ .destroy = decf_destroy,
+};
+
+static const struct mp_filter_info decode_wrapper_filter = {
+ .name = "decode_wrapper",
+ .priv_size = sizeof(struct priv),
+ .reset = public_f_reset,
+ .destroy = public_f_destroy,
+};
+
+static void wakeup_dec_thread(void *ptr)
+{
+ struct priv *p = ptr;
+
+ mp_dispatch_interrupt(p->dec_dispatch);
+}
+
+static void onlock_dec_thread(void *ptr)
+{
+ struct priv *p = ptr;
+
+ mp_filter_graph_interrupt(p->dec_root_filter);
+}
+
+struct mp_decoder_wrapper *mp_decoder_wrapper_create(struct mp_filter *parent,
+ struct sh_stream *src)
+{
+ struct mp_filter *public_f = mp_filter_create(parent, &decode_wrapper_filter);
+ if (!public_f)
+ return NULL;
+
+ struct priv *p = public_f->priv;
+ p->public.f = public_f;
+
+ mp_mutex_init(&p->cache_lock);
+ p->opt_cache = m_config_cache_alloc(p, public_f->global, &dec_wrapper_conf);
+ p->opts = p->opt_cache->opts;
+ p->header = src;
+ p->codec = p->header->codec;
+ p->play_dir = 1;
+ mp_filter_add_pin(public_f, MP_PIN_OUT, "out");
+
+ if (p->header->type == STREAM_VIDEO) {
+ p->log = mp_log_new(p, parent->global->log, "!vd");
+
+ p->fps = src->codec->fps;
+
+ MP_VERBOSE(p, "Container reported FPS: %f\n", p->fps);
+
+ if (p->opts->fps_override) {
+ p->fps = p->opts->fps_override;
+ MP_INFO(p, "Container FPS forced to %5.3f.\n", p->fps);
+ MP_INFO(p, "Use --no-correct-pts to force FPS based timing.\n");
+ }
+
+ p->queue_opts = p->opts->vdec_queue_opts;
+ } else if (p->header->type == STREAM_AUDIO) {
+ p->log = mp_log_new(p, parent->global->log, "!ad");
+ p->queue_opts = p->opts->adec_queue_opts;
+ } else {
+ goto error;
+ }
+
+ if (p->queue_opts && p->queue_opts->use_queue) {
+ p->queue = mp_async_queue_create();
+ p->dec_dispatch = mp_dispatch_create(p);
+ p->dec_root_filter = mp_filter_create_root(public_f->global);
+ mp_filter_graph_set_wakeup_cb(p->dec_root_filter, wakeup_dec_thread, p);
+ mp_dispatch_set_onlock_fn(p->dec_dispatch, onlock_dec_thread, p);
+
+ struct mp_stream_info *sinfo = mp_filter_find_stream_info(parent);
+ if (sinfo) {
+ p->dec_root_filter->stream_info = &p->stream_info;
+ p->stream_info = (struct mp_stream_info){
+ .dr_vo = sinfo->dr_vo,
+ .hwdec_devs = sinfo->hwdec_devs,
+ };
+ }
+
+ update_queue_config(p);
+ }
+
+ p->decf = mp_filter_create(p->dec_root_filter ? p->dec_root_filter : public_f,
+ &decf_filter);
+ if (!p->decf)
+ goto error;
+ p->decf->priv = p;
+ p->decf->log = public_f->log = p->log;
+ mp_filter_add_pin(p->decf, MP_PIN_OUT, "out");
+
+ struct mp_filter *demux = mp_demux_in_create(p->decf, p->header);
+ if (!demux)
+ goto error;
+ p->demux = demux->pins[0];
+
+ decf_reset(p->decf);
+
+ if (p->queue) {
+ struct mp_filter *f_in =
+ mp_async_queue_create_filter(public_f, MP_PIN_OUT, p->queue);
+ struct mp_filter *f_out =
+ mp_async_queue_create_filter(p->decf, MP_PIN_IN, p->queue);
+ mp_pin_connect(public_f->ppins[0], f_in->pins[0]);
+ mp_pin_connect(f_out->pins[0], p->decf->pins[0]);
+
+ p->dec_thread_valid = true;
+ if (mp_thread_create(&p->dec_thread, dec_thread, p)) {
+ p->dec_thread_valid = false;
+ goto error;
+ }
+ } else {
+ mp_pin_connect(public_f->ppins[0], p->decf->pins[0]);
+ }
+
+ public_f_reset(public_f);
+
+ return &p->public;
+error:
+ talloc_free(public_f);
+ return NULL;
+}
+
+void lavc_process(struct mp_filter *f, struct lavc_state *state,
+ int (*send)(struct mp_filter *f, struct demux_packet *pkt),
+ int (*receive)(struct mp_filter *f, struct mp_frame *res))
+{
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ struct mp_frame frame = {0};
+ int ret_recv = receive(f, &frame);
+ if (frame.type) {
+ state->eof_returned = false;
+ mp_pin_in_write(f->ppins[1], frame);
+ } else if (ret_recv == AVERROR_EOF) {
+ if (!state->eof_returned)
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ state->eof_returned = true;
+ state->packets_sent = false;
+ } else if (ret_recv == AVERROR(EAGAIN)) {
+ // Need to feed a packet.
+ frame = mp_pin_out_read(f->ppins[0]);
+ struct demux_packet *pkt = NULL;
+ if (frame.type == MP_FRAME_PACKET) {
+ pkt = frame.data;
+ } else if (frame.type != MP_FRAME_EOF) {
+ if (frame.type) {
+ MP_ERR(f, "unexpected frame type\n");
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+ }
+ return;
+ } else if (!state->packets_sent) {
+ // EOF only; just return it, without requiring send/receive to
+ // pass it through properly.
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ return;
+ }
+ int ret_send = send(f, pkt);
+ if (ret_send == AVERROR(EAGAIN)) {
+ // Should never happen, but can happen with broken decoders.
+ MP_WARN(f, "could not consume packet\n");
+ mp_pin_out_unread(f->ppins[0], frame);
+ mp_filter_wakeup(f);
+ return;
+ }
+ state->packets_sent = true;
+ talloc_free(pkt);
+ mp_filter_internal_mark_progress(f);
+ } else {
+ // Decoding error, or hwdec fallback recovery. Just try again.
+ mp_filter_internal_mark_progress(f);
+ }
+}
diff --git a/filters/f_decoder_wrapper.h b/filters/f_decoder_wrapper.h
new file mode 100644
index 0000000..9f1a8b5
--- /dev/null
+++ b/filters/f_decoder_wrapper.h
@@ -0,0 +1,125 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "filter.h"
+
+struct sh_stream;
+struct mp_codec_params;
+struct mp_image_params;
+struct mp_decoder_list;
+struct demux_packet;
+
+// (free with talloc_free(mp_decoder_wrapper.f)
+struct mp_decoder_wrapper {
+ // Filter with no input and 1 output, which returns the decoded data.
+ struct mp_filter *f;
+
+ // Can be set by user.
+ struct mp_recorder_sink *recorder_sink;
+};
+
+// Create the decoder wrapper for the given stream, plus underlying decoder.
+// The src stream must be selected, and remain valid and selected until the
+// wrapper is destroyed.
+struct mp_decoder_wrapper *mp_decoder_wrapper_create(struct mp_filter *parent,
+ struct sh_stream *src);
+
+// For informational purposes.
+void mp_decoder_wrapper_get_desc(struct mp_decoder_wrapper *d,
+ char *buf, size_t buf_size);
+
+// Legacy decoder framedrop control.
+void mp_decoder_wrapper_set_frame_drops(struct mp_decoder_wrapper *d, int num);
+int mp_decoder_wrapper_get_frames_dropped(struct mp_decoder_wrapper *d);
+
+double mp_decoder_wrapper_get_container_fps(struct mp_decoder_wrapper *d);
+
+// Whether to prefer spdif wrapper over real decoders on next reinit.
+void mp_decoder_wrapper_set_spdif_flag(struct mp_decoder_wrapper *d, bool spdif);
+
+// Whether to decode only 1 frame and then stop, and cache the frame across resets.
+void mp_decoder_wrapper_set_coverart_flag(struct mp_decoder_wrapper *d, bool c);
+
+// True if a pts reset was observed (audio only, heuristic).
+bool mp_decoder_wrapper_get_pts_reset(struct mp_decoder_wrapper *d);
+
+void mp_decoder_wrapper_set_play_dir(struct mp_decoder_wrapper *d, int dir);
+
+struct mp_decoder_list *video_decoder_list(void);
+struct mp_decoder_list *audio_decoder_list(void);
+
+// For precise seeking: if possible, try to drop frames up until the given PTS.
+// This is automatically unset if the target is reached, or on reset.
+void mp_decoder_wrapper_set_start_pts(struct mp_decoder_wrapper *d, double pts);
+
+enum dec_ctrl {
+ VDCTRL_FORCE_HWDEC_FALLBACK, // force software decoding fallback
+ VDCTRL_GET_HWDEC,
+ VDCTRL_REINIT,
+ VDCTRL_GET_BFRAMES,
+ // framedrop mode: 0=none, 1=standard, 2=hrseek
+ VDCTRL_SET_FRAMEDROP,
+ VDCTRL_CHECK_FORCED_EOF,
+};
+
+int mp_decoder_wrapper_control(struct mp_decoder_wrapper *d,
+ enum dec_ctrl cmd, void *arg);
+
+// Force it to reevaluate output parameters (for overrides like aspect).
+void mp_decoder_wrapper_reset_params(struct mp_decoder_wrapper *d);
+
+void mp_decoder_wrapper_get_video_dec_params(struct mp_decoder_wrapper *d,
+ struct mp_image_params *p);
+
+bool mp_decoder_wrapper_reinit(struct mp_decoder_wrapper *d);
+
+struct mp_decoder {
+ // Bidirectional filter; takes MP_FRAME_PACKET for input.
+ struct mp_filter *f;
+
+ // Can be set by decoder impl. on init for "special" functionality.
+ int (*control)(struct mp_filter *f, enum dec_ctrl cmd, void *arg);
+};
+
+struct mp_decoder_fns {
+ struct mp_decoder *(*create)(struct mp_filter *parent,
+ struct mp_codec_params *codec,
+ const char *decoder);
+ void (*add_decoders)(struct mp_decoder_list *list);
+};
+
+extern const struct mp_decoder_fns vd_lavc;
+extern const struct mp_decoder_fns ad_lavc;
+extern const struct mp_decoder_fns ad_spdif;
+
+// Convenience wrapper for lavc based decoders. Treat lavc_state as private;
+// init to all-0 on init and resets.
+struct lavc_state {
+ bool eof_returned;
+ bool packets_sent;
+};
+void lavc_process(struct mp_filter *f, struct lavc_state *state,
+ int (*send)(struct mp_filter *f, struct demux_packet *pkt),
+ int (*receive)(struct mp_filter *f, struct mp_frame *res));
+
+// ad_spdif.c
+struct mp_decoder_list *select_spdif_codec(const char *codec, const char *pref);
diff --git a/filters/f_demux_in.c b/filters/f_demux_in.c
new file mode 100644
index 0000000..995bb00
--- /dev/null
+++ b/filters/f_demux_in.c
@@ -0,0 +1,85 @@
+#include "common/common.h"
+#include "demux/demux.h"
+#include "demux/packet.h"
+
+#include "f_demux_in.h"
+#include "filter_internal.h"
+
+struct priv {
+ struct sh_stream *src;
+ bool eof_returned;
+};
+
+static void wakeup(void *ctx)
+{
+ struct mp_filter *f = ctx;
+
+ mp_filter_wakeup(f);
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[0]))
+ return;
+
+ struct demux_packet *pkt = NULL;
+ if (demux_read_packet_async(p->src, &pkt) == 0)
+ return; // wait
+
+ struct mp_frame frame = {MP_FRAME_PACKET, pkt};
+ if (pkt) {
+ if (p->eof_returned)
+ MP_VERBOSE(f, "unset EOF on stream %d\n", p->src->index);
+ p->eof_returned = false;
+ } else {
+ frame.type = MP_FRAME_EOF;
+
+ // While the demuxer will repeat EOFs, filters never do that.
+ if (p->eof_returned)
+ return;
+ p->eof_returned = true;
+ }
+
+ mp_pin_in_write(f->ppins[0], frame);
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ p->eof_returned = false;
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ demux_set_stream_wakeup_cb(p->src, NULL, NULL);
+}
+
+static const struct mp_filter_info demux_filter = {
+ .name = "demux_in",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+struct mp_filter *mp_demux_in_create(struct mp_filter *parent,
+ struct sh_stream *src)
+{
+ struct mp_filter *f = mp_filter_create(parent, &demux_filter);
+ if (!f)
+ return NULL;
+
+ struct priv *p = f->priv;
+ p->src = src;
+
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ demux_set_stream_wakeup_cb(p->src, wakeup, f);
+
+ return f;
+}
diff --git a/filters/f_demux_in.h b/filters/f_demux_in.h
new file mode 100644
index 0000000..eebd428
--- /dev/null
+++ b/filters/f_demux_in.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "filter.h"
+
+struct sh_stream;
+
+// Create a filter with a single output for the given stream. The stream must
+// be selected, and remain so until the filter is destroyed. The filter will
+// set/unset the stream's wakeup callback.
+struct mp_filter *mp_demux_in_create(struct mp_filter *parent,
+ struct sh_stream *src);
diff --git a/filters/f_hwtransfer.c b/filters/f_hwtransfer.c
new file mode 100644
index 0000000..94359bf
--- /dev/null
+++ b/filters/f_hwtransfer.c
@@ -0,0 +1,667 @@
+#include <libavutil/buffer.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/mem.h>
+#include <libavutil/pixdesc.h>
+
+#include "video/fmt-conversion.h"
+#include "video/hwdec.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+
+#include "f_hwtransfer.h"
+#include "f_output_chain.h"
+#include "f_utils.h"
+#include "filter_internal.h"
+#include "user_filters.h"
+
+struct priv {
+ AVBufferRef *av_device_ctx;
+
+ AVBufferRef *hw_pool;
+
+ int last_source_fmt;
+ int last_hw_output_fmt;
+ int last_hw_input_fmt;
+
+ // Hardware wrapper format, e.g. IMGFMT_VAAPI.
+ int hw_imgfmt;
+
+ // List of supported underlying surface formats.
+ int *fmts;
+ int num_fmts;
+ // List of supported upload image formats. May contain duplicate entries
+ // (which should be ignored).
+ int *upload_fmts;
+ int num_upload_fmts;
+ // For fmts[n], fmt_upload_index[n] gives the index of the first supported
+ // upload format in upload_fmts[], and fmt_upload_num[n] gives the number
+ // of formats at this position.
+ int *fmt_upload_index;
+ int *fmt_upload_num;
+
+ // List of source formats that require hwmap instead of hwupload.
+ int *map_fmts;
+ int num_map_fmts;
+
+ // If the selected hwdec has a conversion filter available for converting
+ // between sw formats in hardware, the name will be set. NULL otherwise.
+ const char *conversion_filter_name;
+};
+
+struct hwmap_pairs {
+ int first_fmt;
+ int second_fmt;
+};
+
+// We cannot discover which pairs of hardware formats need to use hwmap to
+// convert between the formats, so we need a lookup table.
+static const struct hwmap_pairs hwmap_pairs[] = {
+#if HAVE_VULKAN_INTEROP
+ {
+ .first_fmt = IMGFMT_VAAPI,
+ .second_fmt = IMGFMT_VULKAN,
+ },
+#endif
+ {
+ .first_fmt = IMGFMT_DRMPRIME,
+ .second_fmt = IMGFMT_VAAPI,
+ },
+ {0}
+};
+
+/**
+ * @brief Find the closest supported format when hw uploading
+ *
+ * Return the best format suited for upload that is supported for a given input
+ * imgfmt. This returns the same as imgfmt if the format is natively supported,
+ * and otherwise a format that likely results in the least loss.
+ * Returns 0 if completely unsupported.
+ *
+ * Some hardware types support implicit format conversion on upload. For these
+ * types, it is possible for the set of formats that are accepts as inputs to
+ * the upload process to differ from the set of formats that can be outputs of
+ * the upload.
+ *
+ * hw_input_format -> hwupload -> hw_output_format
+ *
+ * Awareness of this is important because we can avoid doing software conversion
+ * if our input_fmt is accepted as a hw_input_format even if it cannot be the
+ * hw_output_format.
+ */
+static bool select_format(struct priv *p, int input_fmt,
+ int *out_hw_input_fmt, int *out_hw_output_fmt)
+{
+ if (!input_fmt)
+ return false;
+
+ // If the input format is a hw format, then we shouldn't be doing this
+ // format selection here at all.
+ if (IMGFMT_IS_HWACCEL(input_fmt)) {
+ return false;
+ }
+
+ // If there is no capability to do uploads or conversions during uploads,
+ // assume that directly displaying the input format works. Maybe it does,
+ // maybe it doesn't but at this point, it's clear that we simply don't know
+ // and should assume it works, rather than blocking unnecessarily.
+ if (p->num_fmts == 0 && p->num_upload_fmts == 0) {
+ *out_hw_input_fmt = input_fmt;
+ *out_hw_output_fmt = input_fmt;
+ return true;
+ }
+
+ // First find the closest hw input fmt. Some hwdec APIs return crazy lists of
+ // "supported" formats, which then are not supported or crash (???), so
+ // the this is a good way to avoid problems.
+ // (Actually we should just have hardcoded everything instead of relying on
+ // this fragile bullshit FFmpeg API and the fragile bullshit hwdec drivers.)
+ int hw_input_fmt = mp_imgfmt_select_best_list(p->fmts, p->num_fmts, input_fmt);
+ if (!hw_input_fmt)
+ return false;
+
+ // Dumb, but find index for p->fmts[index]==hw_input_fmt.
+ int index = -1;
+ for (int n = 0; n < p->num_fmts; n++) {
+ if (p->fmts[n] == hw_input_fmt)
+ index = n;
+ }
+ if (index < 0)
+ return false;
+
+ // Now check the available output formats. This is the format our sw frame
+ // will be in after the upload (probably).
+ int *upload_fmts = &p->upload_fmts[p->fmt_upload_index[index]];
+ int num_upload_fmts = p->fmt_upload_num[index];
+
+ int hw_output_fmt = mp_imgfmt_select_best_list(upload_fmts, num_upload_fmts,
+ input_fmt);
+ if (!hw_output_fmt)
+ return false;
+
+ *out_hw_input_fmt = hw_input_fmt;
+ *out_hw_output_fmt = hw_output_fmt;
+ return true;
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (mp_frame_is_signaling(frame)) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+ if (frame.type != MP_FRAME_VIDEO) {
+ MP_ERR(f, "unsupported frame type\n");
+ goto error;
+ }
+ struct mp_image *src = frame.data;
+
+ /*
+ * Just pass though HW frames in the same format. This shouldn't normally
+ * occur as the upload filter will not be inserted when the formats already
+ * match.
+ *
+ * Technically, we could have frames from different device contexts,
+ * which would require an explicit transfer, but mpv doesn't let you
+ * create that configuration.
+ */
+ if (src->imgfmt == p->hw_imgfmt) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+
+ if (src->imgfmt != p->last_source_fmt) {
+ if (IMGFMT_IS_HWACCEL(src->imgfmt)) {
+ // Because there cannot be any conversion of the sw format when the
+ // input is a hw format, just pick the source sw format.
+ p->last_hw_input_fmt = p->last_hw_output_fmt = src->params.hw_subfmt;
+ } else {
+ if (!select_format(p, src->imgfmt,
+ &p->last_hw_input_fmt, &p->last_hw_output_fmt))
+ {
+ MP_ERR(f, "no hw upload format found\n");
+ goto error;
+ }
+ if (src->imgfmt != p->last_hw_input_fmt) {
+ // Should not fail; if it does, mp_hwupload_find_upload_format()
+ // does not return the src->imgfmt format.
+ MP_ERR(f, "input format is not an upload format\n");
+ goto error;
+ }
+ }
+ p->last_source_fmt = src->imgfmt;
+ MP_INFO(f, "upload %s -> %s[%s]\n",
+ mp_imgfmt_to_name(p->last_source_fmt),
+ mp_imgfmt_to_name(p->hw_imgfmt),
+ mp_imgfmt_to_name(p->last_hw_output_fmt));
+ }
+
+ if (!mp_update_av_hw_frames_pool(&p->hw_pool, p->av_device_ctx, p->hw_imgfmt,
+ p->last_hw_output_fmt, src->w, src->h,
+ src->imgfmt == IMGFMT_CUDA))
+ {
+ MP_ERR(f, "failed to create frame pool\n");
+ goto error;
+ }
+
+ struct mp_image *dst;
+ bool map_images = false;
+ for (int n = 0; n < p->num_map_fmts; n++) {
+ if (src->imgfmt == p->map_fmts[n]) {
+ map_images = true;
+ break;
+ }
+ }
+
+ if (map_images)
+ dst = mp_av_pool_image_hw_map(p->hw_pool, src);
+ else
+ dst = mp_av_pool_image_hw_upload(p->hw_pool, src);
+ if (!dst)
+ goto error;
+
+ mp_frame_unref(&frame);
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_VIDEO, dst));
+
+ return;
+
+error:
+ mp_frame_unref(&frame);
+ MP_ERR(f, "failed to upload frame\n");
+ mp_filter_internal_mark_failed(f);
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ av_buffer_unref(&p->hw_pool);
+ av_buffer_unref(&p->av_device_ctx);
+}
+
+static const struct mp_filter_info hwupload_filter = {
+ .name = "hwupload",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .destroy = destroy,
+};
+
+// The VO layer might have restricted format support. It might actually
+// work if this is input to a conversion filter anyway, but our format
+// negotiation is too stupid and non-existent to detect this.
+// So filter out all not explicitly supported formats.
+static bool vo_supports(struct mp_hwdec_ctx *ctx, int hw_fmt, int sw_fmt)
+{
+ if (ctx->hw_imgfmt != hw_fmt)
+ return false;
+ if (!ctx->supported_formats)
+ return true; // if unset, all formats are allowed
+
+ for (int i = 0; ctx->supported_formats && ctx->supported_formats[i]; i++) {
+ if (ctx->supported_formats[i] == sw_fmt)
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Some hwcontexts do not implement constraints, and so cannot
+ * report supported formats, so cobble something together from our
+ * static metadata.
+ */
+static AVHWFramesConstraints *build_static_constraints(struct mp_hwdec_ctx *ctx)
+{
+ AVHWFramesConstraints *cstr = NULL;
+ cstr = av_malloc(sizeof(AVHWFramesConstraints));
+ if (!cstr)
+ return NULL;
+
+ cstr->valid_hw_formats =
+ av_malloc_array(2, sizeof(*cstr->valid_hw_formats));
+ if (!cstr->valid_hw_formats)
+ goto fail;
+ cstr->valid_hw_formats[0] = imgfmt2pixfmt(ctx->hw_imgfmt);
+ cstr->valid_hw_formats[1] = AV_PIX_FMT_NONE;
+
+ int num_sw_formats;
+ for (num_sw_formats = 0;
+ ctx->supported_formats && ctx->supported_formats[num_sw_formats] != 0;
+ num_sw_formats++);
+
+ cstr->valid_sw_formats =
+ av_malloc_array(num_sw_formats + 1,
+ sizeof(*cstr->valid_sw_formats));
+ if (!cstr->valid_sw_formats)
+ goto fail;
+ for (int i = 0; i < num_sw_formats; i++) {
+ cstr->valid_sw_formats[i] = imgfmt2pixfmt(ctx->supported_formats[i]);
+ }
+ cstr->valid_sw_formats[num_sw_formats] = AV_PIX_FMT_NONE;
+
+ return cstr;
+
+ fail:
+ av_freep(&cstr->valid_hw_formats);
+ av_freep(&cstr->valid_sw_formats);
+ return NULL;
+}
+
+static bool probe_formats(struct mp_filter *f, int hw_imgfmt, bool use_conversion_filter)
+{
+ struct priv *p = f->priv;
+
+ p->hw_imgfmt = hw_imgfmt;
+ p->num_fmts = 0;
+ p->num_upload_fmts = 0;
+
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ if (!info || !info->hwdec_devs) {
+ MP_ERR(f, "no hw context\n");
+ return false;
+ }
+
+ struct mp_hwdec_ctx *ctx = NULL;
+ AVHWFramesConstraints *cstr = NULL;
+ AVHWFramesConstraints *conversion_cstr = NULL;
+
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = hw_imgfmt,
+ .probing = true,
+ };
+ hwdec_devices_request_for_img_fmt(info->hwdec_devs, &params);
+
+ for (int n = 0; ; n++) {
+ struct mp_hwdec_ctx *cur = hwdec_devices_get_n(info->hwdec_devs, n);
+ if (!cur)
+ break;
+ if (!cur->av_device_ref)
+ continue;
+ cstr = av_hwdevice_get_hwframe_constraints(cur->av_device_ref, NULL);
+ if (!cstr) {
+ MP_VERBOSE(f, "hwdec '%s' does not report hwframe constraints. "
+ "Using static metadata.\n", cur->driver_name);
+ cstr = build_static_constraints(cur);
+ }
+ bool found = false;
+ for (int i = 0; cstr->valid_hw_formats &&
+ cstr->valid_hw_formats[i] != AV_PIX_FMT_NONE; i++)
+ {
+ found |= cstr->valid_hw_formats[i] == imgfmt2pixfmt(hw_imgfmt);
+ }
+ if (found && (!cur->hw_imgfmt || cur->hw_imgfmt == hw_imgfmt)) {
+ ctx = cur;
+ break;
+ }
+ av_hwframe_constraints_free(&cstr);
+ }
+
+ if (!ctx) {
+ MP_INFO(f, "no support for this hw format\n");
+ return false;
+ }
+
+ // Probe for supported formats. This is very roundabout, because the
+ // hwcontext API does not give us this information directly. We resort to
+ // creating temporary AVHWFramesContexts in order to retrieve the list of
+ // supported formats. This should be relatively cheap as we don't create
+ // any real frames (although some backends do for probing info).
+
+ for (int n = 0; hwmap_pairs[n].first_fmt; n++) {
+ if (hwmap_pairs[n].first_fmt == hw_imgfmt) {
+ MP_TARRAY_APPEND(p, p->map_fmts, p->num_map_fmts,
+ hwmap_pairs[n].second_fmt);
+ } else if (hwmap_pairs[n].second_fmt == hw_imgfmt) {
+ MP_TARRAY_APPEND(p, p->map_fmts, p->num_map_fmts,
+ hwmap_pairs[n].first_fmt);
+ }
+ }
+
+ if (use_conversion_filter) {
+ // We will not be doing a transfer, so do not probe for transfer
+ // formats. This can produce incorrect results. Instead, we need to
+ // obtain the constraints for a conversion configuration.
+
+ conversion_cstr =
+ av_hwdevice_get_hwframe_constraints(ctx->av_device_ref,
+ ctx->conversion_config);
+ }
+
+ for (int n = 0; cstr->valid_sw_formats &&
+ cstr->valid_sw_formats[n] != AV_PIX_FMT_NONE; n++)
+ {
+ int *not_supported_by_vo = NULL;
+ int num_not_supported = 0;
+ int imgfmt = pixfmt2imgfmt(cstr->valid_sw_formats[n]);
+ if (!imgfmt)
+ continue;
+
+ MP_DBG(f, "looking at format %s/%s\n",
+ mp_imgfmt_to_name(hw_imgfmt),
+ mp_imgfmt_to_name(imgfmt));
+
+ if (IMGFMT_IS_HWACCEL(imgfmt)) {
+ // If the enumerated format is a hardware format, we don't need to
+ // do any further probing. It will be supported.
+ MP_DBG(f, " supports %s (a hardware format)\n",
+ mp_imgfmt_to_name(imgfmt));
+ continue;
+ }
+
+ if (use_conversion_filter) {
+ // The conversion constraints are universal, and do not vary with
+ // source format, so we will associate the same set of target formats
+ // with all source formats.
+ int index = p->num_fmts;
+ MP_TARRAY_APPEND(p, p->fmts, p->num_fmts, imgfmt);
+ MP_TARRAY_GROW(p, p->fmt_upload_index, index);
+ MP_TARRAY_GROW(p, p->fmt_upload_num, index);
+
+ p->fmt_upload_index[index] = p->num_upload_fmts;
+
+ /*
+ * First check if the VO supports the source format. If it does,
+ * ensure it is in the target list, so that we never do an
+ * unnecessary conversion. This explicit step is required because
+ * there can be situations where the conversion filter cannot output
+ * the source format, but the VO can accept it, so just looking at
+ * the supported conversion targets can make it seem as if a
+ * conversion is required.
+ */
+ if (!ctx->supported_formats) {
+ /*
+ * If supported_formats is unset, that means we should assume
+ * the VO can accept all source formats, so append the source
+ * format.
+ */
+ MP_TARRAY_APPEND(p, p->upload_fmts, p->num_upload_fmts, imgfmt);
+ } else {
+ for (int i = 0; ctx->supported_formats[i]; i++) {
+ int fmt = ctx->supported_formats[i];
+ if (fmt == imgfmt) {
+ MP_DBG(f, " vo accepts %s\n", mp_imgfmt_to_name(fmt));
+ MP_TARRAY_APPEND(p, p->upload_fmts, p->num_upload_fmts, fmt);
+ }
+ }
+ }
+
+ enum AVPixelFormat *fmts = conversion_cstr ?
+ conversion_cstr->valid_sw_formats : NULL;
+ MP_DBG(f, " supports:");
+ for (int i = 0; fmts && fmts[i] != AV_PIX_FMT_NONE; i++) {
+ int fmt = pixfmt2imgfmt(fmts[i]);
+ if (!fmt)
+ continue;
+ if (!vo_supports(ctx, hw_imgfmt, fmt)) {
+ MP_TARRAY_APPEND(p, not_supported_by_vo, num_not_supported, fmt);
+ continue;
+ }
+ MP_DBG(f, " %s", mp_imgfmt_to_name(fmt));
+ MP_TARRAY_APPEND(p, p->upload_fmts, p->num_upload_fmts, fmt);
+ }
+ if (num_not_supported) {
+ MP_DBG(f, "\n not supported by VO:");
+ for (int i = 0; i < num_not_supported; i++) {
+ MP_DBG(f, " %s", mp_imgfmt_to_name(not_supported_by_vo[i]));
+ }
+ }
+ MP_DBG(f, "\n");
+
+ p->fmt_upload_num[index] =
+ p->num_upload_fmts - p->fmt_upload_index[index];
+ } else {
+ // Creates an AVHWFramesContexts with the given parameters.
+ AVBufferRef *frames = NULL;
+ if (!mp_update_av_hw_frames_pool(&frames, ctx->av_device_ref,
+ hw_imgfmt, imgfmt, 128, 128, false))
+ {
+ MP_WARN(f, "failed to allocate pool\n");
+ continue;
+ }
+
+ enum AVPixelFormat *fmts;
+ if (av_hwframe_transfer_get_formats(frames,
+ AV_HWFRAME_TRANSFER_DIRECTION_TO, &fmts, 0) >= 0)
+ {
+ int index = p->num_fmts;
+ MP_TARRAY_APPEND(p, p->fmts, p->num_fmts, imgfmt);
+ MP_TARRAY_GROW(p, p->fmt_upload_index, index);
+ MP_TARRAY_GROW(p, p->fmt_upload_num, index);
+
+ p->fmt_upload_index[index] = p->num_upload_fmts;
+
+ MP_DBG(f, " supports:");
+ for (int i = 0; fmts[i] != AV_PIX_FMT_NONE; i++) {
+ int fmt = pixfmt2imgfmt(fmts[i]);
+ if (!fmt)
+ continue;
+ if (!vo_supports(ctx, hw_imgfmt, fmt)) {
+ MP_TARRAY_APPEND(p, not_supported_by_vo, num_not_supported, fmt);
+ continue;
+ }
+ MP_DBG(f, " %s", mp_imgfmt_to_name(fmt));
+ MP_TARRAY_APPEND(p, p->upload_fmts, p->num_upload_fmts, fmt);
+ }
+ if (num_not_supported) {
+ MP_DBG(f, "\n not supported by VO:");
+ for (int i = 0; i < num_not_supported; i++) {
+ MP_DBG(f, " %s", mp_imgfmt_to_name(not_supported_by_vo[i]));
+ }
+ }
+ MP_DBG(f, "\n");
+
+ p->fmt_upload_num[index] =
+ p->num_upload_fmts - p->fmt_upload_index[index];
+
+ av_free(fmts);
+ }
+
+ av_buffer_unref(&frames);
+ }
+ talloc_free(not_supported_by_vo);
+ }
+
+ av_hwframe_constraints_free(&cstr);
+ av_hwframe_constraints_free(&conversion_cstr);
+ p->av_device_ctx = av_buffer_ref(ctx->av_device_ref);
+ if (!p->av_device_ctx)
+ return false;
+ p->conversion_filter_name = ctx->conversion_filter_name;
+
+ /*
+ * In the case of needing to do hardware conversion vs uploading, we will
+ * still consider ourselves to be successful if we see no available upload
+ * formats for a conversion and there is no conversion filter. This means
+ * that we cannot do conversions at all, and should just assume we can pass
+ * through whatever format we are given.
+ */
+ return p->num_upload_fmts > 0 ||
+ (use_conversion_filter && !p->conversion_filter_name);
+}
+
+struct mp_hwupload mp_hwupload_create(struct mp_filter *parent, int hw_imgfmt,
+ int sw_imgfmt, bool src_is_same_hw)
+{
+ struct mp_hwupload u = {0,};
+ struct mp_filter *f = mp_filter_create(parent, &hwupload_filter);
+ if (!f)
+ return u;
+
+ struct priv *p = f->priv;
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ if (!probe_formats(f, hw_imgfmt, src_is_same_hw)) {
+ MP_INFO(f, "hardware format not supported\n");
+ goto fail;
+ }
+
+ int hw_input_fmt = 0, hw_output_fmt = 0;
+ if (!select_format(p, sw_imgfmt, &hw_input_fmt, &hw_output_fmt)) {
+ MP_ERR(f, "Unable to find a compatible upload format for %s\n",
+ mp_imgfmt_to_name(sw_imgfmt));
+ goto fail;
+ }
+
+ if (src_is_same_hw) {
+ if (p->conversion_filter_name) {
+ /*
+ * If we are converting from one sw format to another within the same
+ * hw format, we will use that hw format's conversion filter rather
+ * than the actual hwupload filter.
+ */
+ u.selected_sw_imgfmt = hw_output_fmt;
+ if (sw_imgfmt != u.selected_sw_imgfmt) {
+ enum AVPixelFormat pixfmt = imgfmt2pixfmt(u.selected_sw_imgfmt);
+ const char *avfmt_name = av_get_pix_fmt_name(pixfmt);
+ char *args[] = {"format", (char *)avfmt_name, NULL};
+ MP_VERBOSE(f, "Hardware conversion: %s -> %s\n",
+ p->conversion_filter_name, avfmt_name);
+ struct mp_filter *sv =
+ mp_create_user_filter(parent, MP_OUTPUT_CHAIN_VIDEO,
+ p->conversion_filter_name, args);
+ u.f = sv;
+ talloc_free(f);
+ }
+ }
+ } else {
+ u.f = f;
+ /*
+ * In the case where the imgfmt is not natively supported, it must be
+ * converted, either before or during upload. If the imgfmt is supported
+ * as a hw input format, then prefer that, and if the upload has to do
+ * implicit conversion, that's fine. On the other hand, if the imgfmt is
+ * not a supported input format, then pick the output format as the
+ * conversion target to avoid doing two conversions (one before upload,
+ * and one during upload). Note that for most hardware types, there is
+ * no ability to convert during upload, and the two formats will always
+ * be the same.
+ */
+ u.selected_sw_imgfmt =
+ sw_imgfmt == hw_input_fmt ? hw_input_fmt : hw_output_fmt;
+ }
+
+ u.successful_init = true;
+ return u;
+fail:
+ talloc_free(f);
+ return u;
+}
+
+static void hwdownload_process(struct mp_filter *f)
+{
+ struct mp_hwdownload *d = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (frame.type != MP_FRAME_VIDEO)
+ goto passthrough;
+
+ struct mp_image *src = frame.data;
+ if (!src->hwctx)
+ goto passthrough;
+
+ struct mp_image *dst = mp_image_hw_download(src, d->pool);
+ if (!dst) {
+ MP_ERR(f, "Could not copy hardware frame to CPU memory.\n");
+ goto passthrough;
+ }
+
+ mp_frame_unref(&frame);
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_VIDEO, dst));
+ return;
+
+passthrough:
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+}
+
+static const struct mp_filter_info hwdownload_filter = {
+ .name = "hwdownload",
+ .priv_size = sizeof(struct mp_hwdownload),
+ .process = hwdownload_process,
+};
+
+struct mp_hwdownload *mp_hwdownload_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &hwdownload_filter);
+ if (!f)
+ return NULL;
+
+ struct mp_hwdownload *d = f->priv;
+
+ d->f = f;
+ d->pool = mp_image_pool_new(d);
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ return d;
+}
diff --git a/filters/f_hwtransfer.h b/filters/f_hwtransfer.h
new file mode 100644
index 0000000..dde9cf7
--- /dev/null
+++ b/filters/f_hwtransfer.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "filter.h"
+
+// A filter which uploads sw frames to hw. Ignores hw frames.
+struct mp_hwupload {
+ // Indicates if the filter was successfully initialised, or not.
+ // If not, the state of other members is undefined.
+ bool successful_init;
+
+ // The filter to use for uploads. NULL if none is required.
+ struct mp_filter *f;
+
+ // The underlying format of uploaded frames
+ int selected_sw_imgfmt;
+};
+
+struct mp_hwupload mp_hwupload_create(struct mp_filter *parent, int hw_imgfmt,
+ int sw_imgfmt, bool src_is_same_hw);
+
+// A filter which downloads sw frames from hw. Ignores sw frames.
+struct mp_hwdownload {
+ struct mp_filter *f;
+
+ struct mp_image_pool *pool;
+};
+
+struct mp_hwdownload *mp_hwdownload_create(struct mp_filter *parent);
diff --git a/filters/f_lavfi.c b/filters/f_lavfi.c
new file mode 100644
index 0000000..fe7d3e4
--- /dev/null
+++ b/filters/f_lavfi.c
@@ -0,0 +1,1208 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <inttypes.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include <libavutil/avstring.h>
+#include <libavutil/mem.h>
+#include <libavutil/mathematics.h>
+#include <libavutil/rational.h>
+#include <libavutil/error.h>
+#include <libavutil/opt.h>
+#include <libavfilter/avfilter.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/av_common.h"
+#include "common/tags.h"
+#include "common/msg.h"
+
+#include "audio/format.h"
+#include "audio/aframe.h"
+#include "audio/chmap_avchannel.h"
+#include "video/mp_image.h"
+#include "audio/fmt-conversion.h"
+#include "video/fmt-conversion.h"
+#include "video/hwdec.h"
+#include "video/out/gpu/hwdec.h"
+
+#include "f_lavfi.h"
+#include "filter.h"
+#include "filter_internal.h"
+#include "user_filters.h"
+
+struct lavfi {
+ struct mp_log *log;
+ struct mp_filter *f;
+
+ char *graph_string;
+ char **graph_opts;
+ bool force_bidir;
+ enum mp_frame_type force_type;
+ bool direct_filter;
+ char **direct_filter_opts;
+
+ AVFilterGraph *graph;
+ // Set to true once all inputs have been initialized, and the graph is
+ // linked.
+ bool initialized;
+
+ bool warned_nospeed;
+
+ // Graph is draining to either handle format changes (if input format
+ // changes for one pad, recreate the graph after draining all buffered
+ // frames), or undo previously sent EOF (libavfilter does not accept
+ // input anymore after sending EOF, so recreate the graph to "unstuck" it).
+ bool draining_recover;
+
+ // Filter can't be put into a working state.
+ bool failed;
+
+ struct lavfi_pad **in_pads;
+ int num_in_pads;
+
+ struct lavfi_pad **out_pads;
+ int num_out_pads;
+
+ struct lavfi_pad **all_pads;
+ int num_all_pads;
+
+ AVFrame *tmp_frame;
+
+ // Audio timestamp emulation.
+ bool emulate_audio_pts;
+ double in_pts; // last input timestamps
+ int64_t in_samples; // samples ever sent to the filter
+ double delay; // seconds of audio apparently buffered by filter
+
+ struct mp_lavfi public;
+
+ // Identify a specific hwdec_interop to use
+ char *hwdec_interop;
+};
+
+struct lavfi_pad {
+ struct lavfi *main;
+ enum mp_frame_type type;
+ enum mp_pin_dir dir;
+ char *name; // user-given pad name
+
+ struct mp_pin *pin; // internal pin for this (never NULL once initialized)
+ int pin_index;
+
+ AVFilterContext *filter;
+ int filter_pad;
+ // buffersrc or buffersink connected to filter/filter_pad
+ AVFilterContext *buffer;
+ AVRational timebase;
+ bool buffer_is_eof; // received/sent EOF to the buffer
+ bool got_eagain;
+
+ struct mp_tags *metadata;
+
+ // 1-frame queue for input.
+ struct mp_frame pending;
+
+ // Used to check input format changes.
+ struct mp_frame in_fmt;
+};
+
+// Free the libavfilter graph (not c), reset all state.
+// Does not free pending data intentionally.
+static void free_graph(struct lavfi *c)
+{
+ avfilter_graph_free(&c->graph);
+ for (int n = 0; n < c->num_all_pads; n++) {
+ struct lavfi_pad *pad = c->all_pads[n];
+
+ pad->filter = NULL;
+ pad->filter_pad = -1;
+ pad->buffer = NULL;
+ mp_frame_unref(&pad->in_fmt);
+ pad->buffer_is_eof = false;
+ pad->got_eagain = false;
+ }
+ c->initialized = false;
+ c->draining_recover = false;
+ c->in_pts = MP_NOPTS_VALUE;
+ c->in_samples = 0;
+ c->delay = 0;
+}
+
+static void add_pad(struct lavfi *c, int dir, int index, AVFilterContext *filter,
+ int filter_pad, const char *name, bool first_init)
+{
+ if (c->failed)
+ return;
+
+ enum AVMediaType avmt;
+ if (dir == MP_PIN_IN) {
+ avmt = avfilter_pad_get_type(filter->input_pads, filter_pad);
+ } else {
+ avmt = avfilter_pad_get_type(filter->output_pads, filter_pad);
+ }
+ int type;
+ switch (avmt) {
+ case AVMEDIA_TYPE_VIDEO: type = MP_FRAME_VIDEO; break;
+ case AVMEDIA_TYPE_AUDIO: type = MP_FRAME_AUDIO; break;
+ default:
+ MP_FATAL(c, "unknown media type\n");
+ c->failed = true;
+ return;
+ }
+
+ // For anonymous pads, just make something up. libavfilter allows duplicate
+ // pad names (while we don't), so we check for collisions along with normal
+ // duplicate pads below.
+ char tmp[80];
+ const char *dir_string = dir == MP_PIN_IN ? "in" : "out";
+ if (name) {
+ if (c->direct_filter) {
+ // libavfilter has this very unpleasant thing that filter labels
+ // don't have to be unique - in particular, both input and output
+ // are usually named "default". With direct filters, the user has
+ // no chance to provide better names, so do something to resolve it.
+ snprintf(tmp, sizeof(tmp), "%s_%s", name, dir_string);
+ name = tmp;
+ }
+ } else {
+ snprintf(tmp, sizeof(tmp), "%s%d", dir_string, index);
+ name = tmp;
+ }
+
+ struct lavfi_pad *p = NULL;
+ for (int n = 0; n < c->num_all_pads; n++) {
+ if (strcmp(c->all_pads[n]->name, name) == 0) {
+ p = c->all_pads[n];
+ break;
+ }
+ }
+
+ if (p) {
+ // Graph recreation case: reassociate an existing pad.
+ if (p->filter) {
+ // Collision due to duplicate names.
+ MP_FATAL(c, "more than one pad with label '%s'\n", name);
+ c->failed = true;
+ return;
+ }
+ if (p->dir != dir || p->type != type) {
+ // libavfilter graph parser behavior not deterministic.
+ MP_FATAL(c, "pad '%s' changed type or direction\n", name);
+ c->failed = true;
+ return;
+ }
+ } else {
+ if (!first_init) {
+ MP_FATAL(c, "filter pad '%s' got added later?\n", name);
+ c->failed = true;
+ return;
+ }
+ p = talloc_zero(c, struct lavfi_pad);
+ p->main = c;
+ p->dir = dir;
+ p->name = talloc_strdup(p, name);
+ p->type = type;
+ p->pin_index = -1;
+ p->metadata = talloc_zero(p, struct mp_tags);
+ if (p->dir == MP_PIN_IN)
+ MP_TARRAY_APPEND(c, c->in_pads, c->num_in_pads, p);
+ if (p->dir == MP_PIN_OUT)
+ MP_TARRAY_APPEND(c, c->out_pads, c->num_out_pads, p);
+ MP_TARRAY_APPEND(c, c->all_pads, c->num_all_pads, p);
+ }
+ p->filter = filter;
+ p->filter_pad = filter_pad;
+}
+
+static void add_pads(struct lavfi *c, int dir, AVFilterInOut *l, bool first_init)
+{
+ int index = 0;
+ for (; l; l = l->next)
+ add_pad(c, dir, index++, l->filter_ctx, l->pad_idx, l->name, first_init);
+}
+
+static void add_pads_direct(struct lavfi *c, int dir, AVFilterContext *f,
+ AVFilterPad *pads, int num_pads, bool first_init)
+{
+ for (int n = 0; n < num_pads; n++)
+ add_pad(c, dir, n, f, n, avfilter_pad_get_name(pads, n), first_init);
+}
+
+// Parse the user-provided filter graph, and populate the unlinked filter pads.
+static void precreate_graph(struct lavfi *c, bool first_init)
+{
+ assert(!c->graph);
+
+ c->failed = false;
+
+ c->graph = avfilter_graph_alloc();
+ MP_HANDLE_OOM(c->graph);
+
+ if (mp_set_avopts(c->log, c->graph, c->graph_opts) < 0)
+ goto error;
+
+ if (c->direct_filter) {
+ AVFilterContext *filter = avfilter_graph_alloc_filter(c->graph,
+ avfilter_get_by_name(c->graph_string), "filter");
+ if (!filter) {
+ MP_FATAL(c, "filter '%s' not found or failed to allocate\n",
+ c->graph_string);
+ goto error;
+ }
+
+ if (mp_set_avopts_pos(c->log, filter, filter->priv,
+ c->direct_filter_opts) < 0)
+ goto error;
+
+ if (avfilter_init_str(filter, NULL) < 0) {
+ MP_FATAL(c, "filter failed to initialize\n");
+ goto error;
+ }
+
+ add_pads_direct(c, MP_PIN_IN, filter, filter->input_pads,
+ filter->nb_inputs, first_init);
+ add_pads_direct(c, MP_PIN_OUT, filter, filter->output_pads,
+ filter->nb_outputs, first_init);
+ } else {
+ AVFilterInOut *in = NULL, *out = NULL;
+ if (avfilter_graph_parse2(c->graph, c->graph_string, &in, &out) < 0) {
+ MP_FATAL(c, "parsing the filter graph failed\n");
+ goto error;
+ }
+ add_pads(c, MP_PIN_IN, in, first_init);
+ add_pads(c, MP_PIN_OUT, out, first_init);
+ avfilter_inout_free(&in);
+ avfilter_inout_free(&out);
+ }
+
+ for (int n = 0; n < c->num_all_pads; n++)
+ c->failed |= !c->all_pads[n]->filter;
+
+ if (c->failed)
+ goto error;
+
+ return;
+
+error:
+ free_graph(c);
+ c->failed = true;
+ return;
+}
+
+// Ensure to send EOF to each input pad, so the graph can be drained properly.
+static void send_global_eof(struct lavfi *c)
+{
+ for (int n = 0; n < c->num_in_pads; n++) {
+ struct lavfi_pad *pad = c->in_pads[n];
+ if (!pad->buffer || pad->buffer_is_eof)
+ continue;
+
+ if (av_buffersrc_add_frame(pad->buffer, NULL) < 0)
+ MP_FATAL(c, "could not send EOF to filter\n");
+
+ pad->buffer_is_eof = true;
+ }
+}
+
+// libavfilter allows changing some parameters on the fly, but not
+// others.
+static bool is_aformat_ok(struct mp_aframe *a, struct mp_aframe *b)
+{
+ struct mp_chmap ca = {0}, cb = {0};
+ mp_aframe_get_chmap(a, &ca);
+ mp_aframe_get_chmap(b, &cb);
+ return mp_chmap_equals(&ca, &cb) &&
+ mp_aframe_get_rate(a) == mp_aframe_get_rate(b) &&
+ mp_aframe_get_format(a) == mp_aframe_get_format(b);
+}
+static bool is_vformat_ok(struct mp_image *a, struct mp_image *b)
+{
+ return a->imgfmt == b->imgfmt &&
+ a->w == b->w && a->h && b->h &&
+ a->params.p_w == b->params.p_w && a->params.p_h == b->params.p_h &&
+ a->nominal_fps == b->nominal_fps;
+}
+static bool is_format_ok(struct mp_frame a, struct mp_frame b)
+{
+ if (a.type == b.type && a.type == MP_FRAME_VIDEO)
+ return is_vformat_ok(a.data, b.data);
+ if (a.type == b.type && a.type == MP_FRAME_AUDIO)
+ return is_aformat_ok(a.data, b.data);
+ return false;
+}
+
+static void read_pad_input(struct lavfi *c, struct lavfi_pad *pad)
+{
+ assert(pad->dir == MP_PIN_IN);
+
+ if (pad->pending.type || c->draining_recover)
+ return;
+
+ pad->pending = mp_pin_out_read(pad->pin);
+
+ if (pad->pending.type && pad->pending.type != MP_FRAME_EOF &&
+ pad->pending.type != pad->type)
+ {
+ MP_FATAL(c, "unknown frame %s\n", mp_frame_type_str(pad->pending.type));
+ mp_frame_unref(&pad->pending);
+ }
+
+ if (mp_frame_is_data(pad->pending) && pad->in_fmt.type &&
+ !is_format_ok(pad->pending, pad->in_fmt))
+ {
+ if (!c->draining_recover)
+ MP_VERBOSE(c, "format change on %s\n", pad->name);
+ c->draining_recover = true;
+ if (c->initialized)
+ send_global_eof(c);
+ }
+}
+
+// Attempt to initialize all pads. Return true if all are initialized, or
+// false if more data is needed (or on error).
+static bool init_pads(struct lavfi *c)
+{
+ if (!c->graph)
+ goto error;
+
+ for (int n = 0; n < c->num_out_pads; n++) {
+ struct lavfi_pad *pad = c->out_pads[n];
+ if (pad->buffer)
+ continue;
+
+ const AVFilter *dst_filter = NULL;
+ if (pad->type == MP_FRAME_AUDIO) {
+ dst_filter = avfilter_get_by_name("abuffersink");
+ } else if (pad->type == MP_FRAME_VIDEO) {
+ dst_filter = avfilter_get_by_name("buffersink");
+ } else {
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ if (!dst_filter)
+ goto error;
+
+ char name[256];
+ snprintf(name, sizeof(name), "mpv_sink_%s", pad->name);
+
+ if (avfilter_graph_create_filter(&pad->buffer, dst_filter,
+ name, NULL, NULL, c->graph) < 0)
+ goto error;
+
+ if (avfilter_link(pad->filter, pad->filter_pad, pad->buffer, 0) < 0)
+ goto error;
+ }
+
+ for (int n = 0; n < c->num_in_pads; n++) {
+ struct lavfi_pad *pad = c->in_pads[n];
+ if (pad->buffer)
+ continue;
+
+ mp_frame_unref(&pad->in_fmt);
+
+ read_pad_input(c, pad);
+ // no input data, format unknown, can't init, wait longer.
+ if (!pad->pending.type)
+ return false;
+
+ if (mp_frame_is_data(pad->pending)) {
+ assert(pad->pending.type == pad->type);
+
+ pad->in_fmt = mp_frame_ref(pad->pending);
+ if (!pad->in_fmt.type)
+ goto error;
+
+ if (pad->in_fmt.type == MP_FRAME_VIDEO)
+ mp_image_unref_data(pad->in_fmt.data);
+ if (pad->in_fmt.type == MP_FRAME_AUDIO)
+ mp_aframe_unref_data(pad->in_fmt.data);
+ }
+
+ if (pad->pending.type == MP_FRAME_EOF && !pad->in_fmt.type) {
+ // libavfilter makes this painful. Init it with a dummy config,
+ // just so we can tell it the stream is EOF.
+ if (pad->type == MP_FRAME_AUDIO) {
+ struct mp_aframe *fmt = mp_aframe_create();
+ mp_aframe_set_format(fmt, AF_FORMAT_FLOAT);
+ mp_aframe_set_chmap(fmt, &(struct mp_chmap)MP_CHMAP_INIT_STEREO);
+ mp_aframe_set_rate(fmt, 48000);
+ pad->in_fmt = (struct mp_frame){MP_FRAME_AUDIO, fmt};
+ }
+ if (pad->type == MP_FRAME_VIDEO) {
+ struct mp_image *fmt = talloc_zero(NULL, struct mp_image);
+ mp_image_setfmt(fmt, IMGFMT_420P);
+ mp_image_set_size(fmt, 64, 64);
+ pad->in_fmt = (struct mp_frame){MP_FRAME_VIDEO, fmt};
+ }
+ }
+
+ if (pad->in_fmt.type != pad->type)
+ goto error;
+
+ AVBufferSrcParameters *params = av_buffersrc_parameters_alloc();
+ if (!params)
+ goto error;
+
+ pad->timebase = AV_TIME_BASE_Q;
+
+ char *filter_name = NULL;
+ if (pad->type == MP_FRAME_AUDIO) {
+ struct mp_aframe *fmt = pad->in_fmt.data;
+ params->format = af_to_avformat(mp_aframe_get_format(fmt));
+ params->sample_rate = mp_aframe_get_rate(fmt);
+ struct mp_chmap chmap = {0};
+ mp_aframe_get_chmap(fmt, &chmap);
+#if !HAVE_AV_CHANNEL_LAYOUT
+ params->channel_layout = mp_chmap_to_lavc(&chmap);
+#else
+ mp_chmap_to_av_layout(&params->ch_layout, &chmap);
+#endif
+ pad->timebase = (AVRational){1, mp_aframe_get_rate(fmt)};
+ filter_name = "abuffer";
+ } else if (pad->type == MP_FRAME_VIDEO) {
+ struct mp_image *fmt = pad->in_fmt.data;
+ params->format = imgfmt2pixfmt(fmt->imgfmt);
+ params->width = fmt->w;
+ params->height = fmt->h;
+ params->sample_aspect_ratio.num = fmt->params.p_w;
+ params->sample_aspect_ratio.den = fmt->params.p_h;
+ params->hw_frames_ctx = fmt->hwctx;
+ params->frame_rate = av_d2q(fmt->nominal_fps, 1000000);
+ filter_name = "buffer";
+ } else {
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ params->time_base = pad->timebase;
+
+ const AVFilter *filter = avfilter_get_by_name(filter_name);
+ if (filter) {
+ char name[256];
+ snprintf(name, sizeof(name), "mpv_src_%s", pad->name);
+
+ pad->buffer = avfilter_graph_alloc_filter(c->graph, filter, name);
+ }
+ if (!pad->buffer) {
+ av_free(params);
+ goto error;
+ }
+
+ int ret = av_buffersrc_parameters_set(pad->buffer, params);
+ av_free(params);
+ if (ret < 0)
+ goto error;
+
+ if (avfilter_init_str(pad->buffer, NULL) < 0)
+ goto error;
+
+ if (avfilter_link(pad->buffer, 0, pad->filter, pad->filter_pad) < 0)
+ goto error;
+ }
+
+ return true;
+error:
+ MP_FATAL(c, "could not initialize filter pads\n");
+ c->failed = true;
+ return false;
+}
+
+static void dump_graph(struct lavfi *c)
+{
+ MP_DBG(c, "Filter graph:\n");
+ char *s = avfilter_graph_dump(c->graph, NULL);
+ if (s)
+ MP_DBG(c, "%s\n", s);
+ av_free(s);
+}
+
+// Initialize the graph if all inputs have formats set. If it's already
+// initialized, or can't be initialized yet, do nothing.
+static void init_graph(struct lavfi *c)
+{
+ assert(!c->initialized);
+
+ if (!c->graph)
+ precreate_graph(c, false);
+
+ if (init_pads(c)) {
+ struct mp_stream_info *info = mp_filter_find_stream_info(c->f);
+ if (info && info->hwdec_devs) {
+ struct mp_hwdec_ctx *hwdec_ctx = NULL;
+ if (c->hwdec_interop) {
+ int imgfmt =
+ ra_hwdec_driver_get_imgfmt_for_name(c->hwdec_interop);
+ hwdec_ctx = mp_filter_load_hwdec_device(c->f, imgfmt);
+ } else {
+ hwdec_ctx = hwdec_devices_get_first(info->hwdec_devs);
+ }
+ if (hwdec_ctx && hwdec_ctx->av_device_ref) {
+ MP_VERBOSE(c, "Configuring hwdec_interop=%s for filter graph: %s\n",
+ hwdec_ctx->driver_name, c->graph_string);
+ for (int n = 0; n < c->graph->nb_filters; n++) {
+ AVFilterContext *filter = c->graph->filters[n];
+ filter->hw_device_ctx =
+ av_buffer_ref(hwdec_ctx->av_device_ref);
+ }
+ }
+ }
+
+ // And here the actual libavfilter initialization happens.
+ if (avfilter_graph_config(c->graph, NULL) < 0) {
+ MP_FATAL(c, "failed to configure the filter graph\n");
+ free_graph(c);
+ c->failed = true;
+ return;
+ }
+
+ // The timebase is available after configuring.
+ for (int n = 0; n < c->num_out_pads; n++) {
+ struct lavfi_pad *pad = c->out_pads[n];
+
+ pad->timebase = pad->buffer->inputs[0]->time_base;
+ }
+
+ c->initialized = true;
+
+ if (!c->direct_filter) // (output uninteresting for direct filters)
+ dump_graph(c);
+ }
+}
+
+static bool feed_input_pads(struct lavfi *c)
+{
+ bool progress = false;
+ bool was_draining = c->draining_recover;
+
+ assert(c->initialized);
+
+ for (int n = 0; n < c->num_in_pads; n++) {
+ struct lavfi_pad *pad = c->in_pads[n];
+
+ bool requested = av_buffersrc_get_nb_failed_requests(pad->buffer) > 0;
+
+ // Always request a frame after EOF so that we can know if the EOF state
+ // changes (e.g. for sparse streams with midstream EOF).
+ requested |= pad->buffer_is_eof;
+
+ if (requested)
+ read_pad_input(c, pad);
+
+ if (!pad->pending.type || c->draining_recover)
+ continue;
+
+ if (pad->buffer_is_eof) {
+ MP_WARN(c, "eof state changed on %s\n", pad->name);
+ c->draining_recover = true;
+ send_global_eof(c);
+ continue;
+ }
+
+ if (pad->pending.type == MP_FRAME_AUDIO && !c->warned_nospeed) {
+ struct mp_aframe *aframe = pad->pending.data;
+ if (mp_aframe_get_speed(aframe) != 1.0) {
+ MP_ERR(c, "speed changing filters before libavfilter are not "
+ "supported and can cause desyncs\n");
+ c->warned_nospeed = true;
+ }
+ }
+
+ AVFrame *frame = mp_frame_to_av(pad->pending, &pad->timebase);
+ bool eof = pad->pending.type == MP_FRAME_EOF;
+
+ if (c->emulate_audio_pts && pad->pending.type == MP_FRAME_AUDIO) {
+ struct mp_aframe *aframe = pad->pending.data;
+ c->in_pts = mp_aframe_end_pts(aframe);
+ frame->pts = c->in_samples; // timebase is 1/sample_rate
+ c->in_samples += frame->nb_samples;
+ }
+
+ mp_frame_unref(&pad->pending);
+
+ if (!frame && !eof) {
+ MP_FATAL(c, "out of memory or unsupported format\n");
+ continue;
+ }
+
+ pad->buffer_is_eof = !frame;
+
+ if (av_buffersrc_add_frame(pad->buffer, frame) < 0)
+ MP_FATAL(c, "could not pass frame to filter\n");
+ av_frame_free(&frame);
+
+ for (int i = 0; i < c->num_out_pads; i++)
+ c->out_pads[i]->got_eagain = false;
+
+ progress = true;
+ }
+
+ if (!was_draining && c->draining_recover)
+ progress = true;
+
+ return progress;
+}
+
+// Some filters get stuck and return EAGAIN forever if they did not get any
+// input (i.e. we send only EOF as input). "dynaudnorm" is known to be affected.
+static bool check_stuck_eagain_on_eof_bug(struct lavfi *c)
+{
+ for (int n = 0; n < c->num_in_pads; n++) {
+ if (!c->in_pads[n]->buffer_is_eof)
+ return false;
+ }
+
+ for (int n = 0; n < c->num_out_pads; n++) {
+ struct lavfi_pad *pad = c->out_pads[n];
+
+ if (!pad->buffer_is_eof && !pad->got_eagain)
+ return false;
+ }
+
+ MP_WARN(c, "Filter is stuck. This is a FFmpeg bug. Treating as EOF.\n");
+ return true;
+}
+
+static bool read_output_pads(struct lavfi *c)
+{
+ bool progress = false;
+
+ assert(c->initialized);
+
+ for (int n = 0; n < c->num_out_pads; n++) {
+ struct lavfi_pad *pad = c->out_pads[n];
+
+ if (!mp_pin_in_needs_data(pad->pin))
+ continue;
+
+ assert(pad->buffer);
+
+ int r = AVERROR_EOF;
+ if (!pad->buffer_is_eof)
+ r = av_buffersink_get_frame_flags(pad->buffer, c->tmp_frame, 0);
+
+ pad->got_eagain = r == AVERROR(EAGAIN);
+ if (pad->got_eagain) {
+ if (check_stuck_eagain_on_eof_bug(c))
+ r = AVERROR_EOF;
+ } else {
+ for (int i = 0; i < c->num_out_pads; i++)
+ c->out_pads[i]->got_eagain = false;
+ }
+
+ if (r >= 0) {
+ mp_tags_copy_from_av_dictionary(pad->metadata, c->tmp_frame->metadata);
+ struct mp_frame frame =
+ mp_frame_from_av(pad->type, c->tmp_frame, &pad->timebase);
+ if (c->emulate_audio_pts && frame.type == MP_FRAME_AUDIO) {
+ AVFrame *avframe = c->tmp_frame;
+ struct mp_aframe *aframe = frame.data;
+ double in_time = c->in_samples * av_q2d(c->in_pads[0]->timebase);
+ double out_time = avframe->pts * av_q2d(pad->timebase);
+ mp_aframe_set_pts(aframe, c->in_pts +
+ (c->in_pts != MP_NOPTS_VALUE ? (out_time - in_time) : 0));
+ }
+ if (frame.type == MP_FRAME_VIDEO) {
+ struct mp_image *vframe = frame.data;
+ vframe->nominal_fps =
+ av_q2d(av_buffersink_get_frame_rate(pad->buffer));
+ }
+ av_frame_unref(c->tmp_frame);
+ if (frame.type) {
+ mp_pin_in_write(pad->pin, frame);
+ } else {
+ MP_ERR(c, "could not use filter output\n");
+ mp_frame_unref(&frame);
+ }
+ progress = true;
+ } else if (r == AVERROR(EAGAIN)) {
+ // We expect that libavfilter will request input on one of the
+ // input pads (via av_buffersrc_get_nb_failed_requests()).
+ } else if (r == AVERROR_EOF) {
+ if (!c->draining_recover && !pad->buffer_is_eof)
+ mp_pin_in_write(pad->pin, MP_EOF_FRAME);
+ if (!pad->buffer_is_eof)
+ progress = true;
+ pad->buffer_is_eof = true;
+ } else {
+ // Real error - ignore it.
+ MP_ERR(c, "error on filtering (%d)\n", r);
+ }
+ }
+
+ return progress;
+}
+
+static void lavfi_process(struct mp_filter *f)
+{
+ struct lavfi *c = f->priv;
+
+ if (!c->initialized)
+ init_graph(c);
+
+ while (c->initialized) {
+ bool a = read_output_pads(c);
+ bool b = feed_input_pads(c);
+ if (!a && !b)
+ break;
+ }
+
+ // Start over on format changes or EOF draining.
+ if (c->draining_recover) {
+ // Wait until all outputs got EOF.
+ bool all_eof = true;
+ for (int n = 0; n < c->num_out_pads; n++)
+ all_eof &= c->out_pads[n]->buffer_is_eof;
+
+ if (all_eof) {
+ MP_VERBOSE(c, "recovering all eof\n");
+ free_graph(c);
+ mp_filter_internal_mark_progress(c->f);
+ }
+ }
+
+ if (c->failed)
+ mp_filter_internal_mark_failed(c->f);
+}
+
+static void lavfi_reset(struct mp_filter *f)
+{
+ struct lavfi *c = f->priv;
+
+ free_graph(c);
+
+ for (int n = 0; n < c->num_in_pads; n++)
+ mp_frame_unref(&c->in_pads[n]->pending);
+}
+
+static void lavfi_destroy(struct mp_filter *f)
+{
+ struct lavfi *c = f->priv;
+
+ lavfi_reset(f);
+ av_frame_free(&c->tmp_frame);
+}
+
+static bool lavfi_command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct lavfi *c = f->priv;
+
+ if (!c->initialized)
+ return false;
+
+ switch (cmd->type) {
+ case MP_FILTER_COMMAND_TEXT: {
+ return avfilter_graph_send_command(c->graph, cmd->target,
+ cmd->cmd, cmd->arg,
+ &(char){0}, 0, 0) >= 0;
+ }
+ case MP_FILTER_COMMAND_GET_META: {
+ // We can worry later about what it should do to multi output filters.
+ if (c->num_out_pads < 1)
+ return false;
+ struct mp_tags **ptags = cmd->res;
+ *ptags = mp_tags_dup(NULL, c->out_pads[0]->metadata);
+ return true;
+ }
+ default:
+ return false;
+ }
+}
+
+static const struct mp_filter_info lavfi_filter = {
+ .name = "lavfi",
+ .priv_size = sizeof(struct lavfi),
+ .process = lavfi_process,
+ .reset = lavfi_reset,
+ .destroy = lavfi_destroy,
+ .command = lavfi_command,
+};
+
+static struct lavfi *lavfi_alloc(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &lavfi_filter);
+ if (!f)
+ return NULL;
+
+ struct lavfi *c = f->priv;
+
+ c->f = f;
+ c->log = f->log;
+ c->public.f = f;
+ c->tmp_frame = av_frame_alloc();
+ MP_HANDLE_OOM(c->tmp_frame);
+
+ return c;
+}
+
+static struct mp_lavfi *do_init(struct lavfi *c)
+{
+ precreate_graph(c, true);
+
+ if (c->failed)
+ goto error;
+
+ for (int n = 0; n < c->num_in_pads + c->num_out_pads; n++) {
+ // First add input pins to satisfy order for bidir graph types.
+ struct lavfi_pad *pad =
+ n < c->num_in_pads ? c->in_pads[n] : c->out_pads[n - c->num_in_pads];
+
+ pad->pin_index = c->f->num_pins;
+ pad->pin = mp_filter_add_pin(c->f, pad->dir, pad->name);
+
+ if (c->force_type && c->force_type != pad->type) {
+ MP_FATAL(c, "mismatching media type\n");
+ goto error;
+ }
+ }
+
+ if (c->force_bidir) {
+ if (c->f->num_pins != 2) {
+ MP_FATAL(c, "exactly 2 pads required\n");
+ goto error;
+ }
+ if (mp_pin_get_dir(c->f->ppins[0]) != MP_PIN_OUT ||
+ mp_pin_get_dir(c->f->ppins[1]) != MP_PIN_IN)
+ {
+ MP_FATAL(c, "1 input and 1 output pad required\n");
+ goto error;
+ }
+ }
+
+ return &c->public;
+
+error:
+ talloc_free(c->f);
+ return NULL;
+}
+
+struct mp_lavfi *mp_lavfi_create_graph(struct mp_filter *parent,
+ enum mp_frame_type type, bool bidir,
+ char *hwdec_interop,
+ char **graph_opts, const char *graph)
+{
+ struct lavfi *c = lavfi_alloc(parent);
+ if (!c)
+ return NULL;
+
+ c->force_type = type;
+ c->force_bidir = bidir;
+ c->graph_opts = mp_dup_str_array(c, graph_opts);
+ c->graph_string = talloc_strdup(c, graph);
+ c->hwdec_interop = talloc_strdup(c, hwdec_interop);
+
+ return do_init(c);
+}
+
+struct mp_lavfi *mp_lavfi_create_filter(struct mp_filter *parent,
+ enum mp_frame_type type, bool bidir,
+ char *hwdec_interop,
+ char **graph_opts,
+ const char *filter, char **filter_opts)
+{
+ struct lavfi *c = lavfi_alloc(parent);
+ if (!c)
+ return NULL;
+
+ c->force_type = type;
+ c->force_bidir = bidir;
+ c->hwdec_interop = talloc_strdup(c, hwdec_interop);
+ c->graph_opts = mp_dup_str_array(c, graph_opts);
+ c->graph_string = talloc_strdup(c, filter);
+ c->direct_filter_opts = mp_dup_str_array(c, filter_opts);
+ c->direct_filter = true;
+
+ return do_init(c);
+}
+
+struct lavfi_user_opts {
+ bool is_bridge;
+ enum mp_frame_type type;
+
+ char *graph;
+ char **avopts;
+
+ char *filter_name;
+ char **filter_opts;
+
+ bool fix_pts;
+
+ char *hwdec_interop;
+};
+
+static struct mp_filter *lavfi_create(struct mp_filter *parent, void *options)
+{
+ struct lavfi_user_opts *opts = options;
+ struct mp_lavfi *l;
+ if (opts->is_bridge) {
+ l = mp_lavfi_create_filter(parent, opts->type, true,
+ opts->hwdec_interop, opts->avopts,
+ opts->filter_name, opts->filter_opts);
+ } else {
+ l = mp_lavfi_create_graph(parent, opts->type, true, opts->hwdec_interop,
+ opts->avopts, opts->graph);
+ }
+ if (l) {
+ struct lavfi *c = l->f->priv;
+ c->emulate_audio_pts = opts->fix_pts;
+ }
+ talloc_free(opts);
+ return l ? l->f : NULL;
+}
+
+// Does it have exactly one video input and one video output?
+static bool is_usable(const AVFilter *filter, int media_type)
+{
+#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(8, 3, 0)
+ int nb_inputs = avfilter_filter_pad_count(filter, 0),
+ nb_outputs = avfilter_filter_pad_count(filter, 1);
+#else
+ int nb_inputs = avfilter_pad_count(filter->inputs),
+ nb_outputs = avfilter_pad_count(filter->outputs);
+#endif
+ bool input_ok = filter->flags & AVFILTER_FLAG_DYNAMIC_INPUTS;
+ bool output_ok = filter->flags & AVFILTER_FLAG_DYNAMIC_OUTPUTS;
+ if (nb_inputs == 1)
+ input_ok = avfilter_pad_get_type(filter->inputs, 0) == media_type;
+ if (nb_outputs == 1)
+ output_ok = avfilter_pad_get_type(filter->outputs, 0) == media_type;
+ return input_ok && output_ok;
+}
+
+bool mp_lavfi_is_usable(const char *name, int media_type)
+{
+ const AVFilter *f = avfilter_get_by_name(name);
+ return f && is_usable(f, media_type);
+}
+
+static void dump_list(struct mp_log *log, int media_type)
+{
+ mp_info(log, "Available libavfilter filters:\n");
+ void *iter = NULL;
+ for (;;) {
+ const AVFilter *filter = av_filter_iterate(&iter);
+ if (!filter)
+ break;
+ if (is_usable(filter, media_type))
+ mp_info(log, " %-16s %s\n", filter->name, filter->description);
+ }
+}
+
+static const char *get_avopt_type_name(enum AVOptionType type)
+{
+ switch (type) {
+ case AV_OPT_TYPE_FLAGS: return "flags";
+ case AV_OPT_TYPE_INT: return "int";
+ case AV_OPT_TYPE_INT64: return "int64";
+ case AV_OPT_TYPE_DOUBLE: return "double";
+ case AV_OPT_TYPE_FLOAT: return "float";
+ case AV_OPT_TYPE_STRING: return "string";
+ case AV_OPT_TYPE_RATIONAL: return "rational";
+ case AV_OPT_TYPE_BINARY: return "binary";
+ case AV_OPT_TYPE_DICT: return "dict";
+ case AV_OPT_TYPE_UINT64: return "uint64";
+ case AV_OPT_TYPE_IMAGE_SIZE: return "imagesize";
+ case AV_OPT_TYPE_PIXEL_FMT: return "pixfmt";
+ case AV_OPT_TYPE_SAMPLE_FMT: return "samplefmt";
+ case AV_OPT_TYPE_VIDEO_RATE: return "fps";
+ case AV_OPT_TYPE_DURATION: return "duration";
+ case AV_OPT_TYPE_COLOR: return "color";
+ case AV_OPT_TYPE_CHANNEL_LAYOUT: return "channellayout";
+ case AV_OPT_TYPE_BOOL: return "bool";
+ case AV_OPT_TYPE_CONST: // fallthrough
+ default:
+ return NULL;
+ }
+}
+
+#define NSTR(s) ((s) ? (s) : "")
+
+void print_lavfi_help(struct mp_log *log, const char *name, int media_type)
+{
+ const AVFilter *f = avfilter_get_by_name(name);
+ if (!f) {
+ mp_err(log, "Filter '%s' not found.\n", name);
+ return;
+ }
+ if (!is_usable(f, media_type)) {
+ mp_err(log, "Filter '%s' is not usable in this context (wrong media \n"
+ "types or wrong number of inputs/outputs).\n", name);
+ }
+ mp_info(log, "Options:\n\n");
+ const AVClass *class = f->priv_class;
+ // av_opt_next() requires this for some retarded incomprehensible reason.
+ const AVClass **c = &class;
+ int offset= -1;
+ int count = 0;
+ for (const AVOption *o = av_opt_next(c, 0); o; o = av_opt_next(c, o)) {
+ // This is how libavfilter (at the time) decided to assign positional
+ // options (called "shorthand" in the libavfilter code). So we
+ // duplicate it exactly.
+ if (o->type == AV_OPT_TYPE_CONST || o->offset == offset)
+ continue;
+ offset = o->offset;
+
+ const char *t = get_avopt_type_name(o->type);
+ char *tstr = t ? mp_tprintf(30, "<%s>", t) : "?";
+ mp_info(log, " %-10s %-12s %s\n", o->name, tstr, NSTR(o->help));
+
+ const AVOption *sub = o;
+ while (1) {
+ sub = av_opt_next(c, sub);
+ if (!sub || sub->type != AV_OPT_TYPE_CONST)
+ break;
+ mp_info(log, " %3s%-23s %s\n", "", sub->name, NSTR(sub->help));
+ }
+
+ count++;
+ }
+ mp_info(log, "\nTotal: %d options\n", count);
+}
+
+void print_lavfi_help_list(struct mp_log *log, int media_type)
+{
+ dump_list(log, media_type);
+ mp_info(log, "\nIf libavfilter filters clash with builtin mpv filters,\n"
+ "prefix them with lavfi- to select the libavfilter one.\n\n");
+}
+
+static void print_help(struct mp_log *log, int mediatype, char *name, char *ex)
+{
+ dump_list(log, mediatype);
+ mp_info(log, "\n"
+ "This lists %s->%s filters only. Refer to\n"
+ "\n"
+ " https://ffmpeg.org/ffmpeg-filters.html\n"
+ "\n"
+ "to see how to use each filter and what arguments each filter takes.\n"
+ "Also, be sure to quote the FFmpeg filter string properly, e.g.:\n"
+ "\n"
+ " \"%s\"\n"
+ "\n"
+ "Otherwise, mpv and libavfilter syntax will conflict.\n"
+ "\n", name, name, ex);
+}
+
+static void print_help_v(struct mp_log *log)
+{
+ print_help(log, AVMEDIA_TYPE_VIDEO, "video", "--vf=lavfi=[gradfun=20:30]");
+}
+
+static void print_help_a(struct mp_log *log)
+{
+ print_help(log, AVMEDIA_TYPE_AUDIO, "audio", "--af=lavfi=[volume=0.5]");
+}
+
+#define OPT_BASE_STRUCT struct lavfi_user_opts
+
+const struct mp_user_filter_entry af_lavfi = {
+ .desc = {
+ .description = "libavfilter bridge",
+ .name = "lavfi",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = (const m_option_t[]){
+ {"graph", OPT_STRING(graph)},
+ {"fix-pts", OPT_BOOL(fix_pts)},
+ {"o", OPT_KEYVALUELIST(avopts)},
+ {"hwdec_interop",
+ OPT_STRING_VALIDATE(hwdec_interop,
+ ra_hwdec_validate_drivers_only_opt)},
+ {0}
+ },
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .type = MP_FRAME_AUDIO,
+ },
+ .print_help = print_help_a,
+ },
+ .create = lavfi_create,
+};
+
+const struct mp_user_filter_entry af_lavfi_bridge = {
+ .desc = {
+ .description = "libavfilter bridge (explicit options)",
+ .name = "lavfi-bridge",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = (const m_option_t[]){
+ {"name", OPT_STRING(filter_name)},
+ {"opts", OPT_KEYVALUELIST(filter_opts)},
+ {"o", OPT_KEYVALUELIST(avopts)},
+ {"hwdec_interop",
+ OPT_STRING_VALIDATE(hwdec_interop,
+ ra_hwdec_validate_drivers_only_opt)},
+ {0}
+ },
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .is_bridge = true,
+ .type = MP_FRAME_AUDIO,
+ },
+ .print_help = print_help_a,
+ },
+ .create = lavfi_create,
+};
+
+const struct mp_user_filter_entry vf_lavfi = {
+ .desc = {
+ .description = "libavfilter bridge",
+ .name = "lavfi",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = (const m_option_t[]){
+ {"graph", OPT_STRING(graph)},
+ {"o", OPT_KEYVALUELIST(avopts)},
+ {"hwdec_interop",
+ OPT_STRING_VALIDATE(hwdec_interop,
+ ra_hwdec_validate_drivers_only_opt)},
+ {0}
+ },
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .type = MP_FRAME_VIDEO,
+ },
+ .print_help = print_help_v,
+ },
+ .create = lavfi_create,
+};
+
+const struct mp_user_filter_entry vf_lavfi_bridge = {
+ .desc = {
+ .description = "libavfilter bridge (explicit options)",
+ .name = "lavfi-bridge",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = (const m_option_t[]){
+ {"name", OPT_STRING(filter_name)},
+ {"opts", OPT_KEYVALUELIST(filter_opts)},
+ {"o", OPT_KEYVALUELIST(avopts)},
+ {"hwdec_interop",
+ OPT_STRING_VALIDATE(hwdec_interop,
+ ra_hwdec_validate_drivers_only_opt)},
+
+ {0}
+ },
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .is_bridge = true,
+ .type = MP_FRAME_VIDEO,
+ },
+ .print_help = print_help_v,
+ },
+ .create = lavfi_create,
+};
diff --git a/filters/f_lavfi.h b/filters/f_lavfi.h
new file mode 100644
index 0000000..b86ed90
--- /dev/null
+++ b/filters/f_lavfi.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "frame.h"
+
+// A wrapped libavfilter filter or filter graph.
+// (to free this, free the filter itself, mp_lavfi.f)
+struct mp_lavfi {
+ // This mirrors the libavfilter pads according to the user specification.
+ struct mp_filter *f;
+};
+
+// Create a filter with the given libavfilter graph string. The graph must
+// have labels on all unconnected pads; these are exposed as pins.
+// type: if not 0, require all pads to have a compatible media type (or error)
+// bidir: if true, require exactly 2 pads, 1 input, 1 output (mp_lavfi.f will
+// have the input as pin 0, and the output as pin 1)
+// graph_opts: options for the filter graph, see mp_set_avopts() (NULL is OK)
+// graph: a libavfilter graph specification
+struct mp_lavfi *mp_lavfi_create_graph(struct mp_filter *parent,
+ enum mp_frame_type type, bool bidir,
+ char *hwdec_interop,
+ char **graph_opts, const char *graph);
+
+// Unlike mp_lavfi_create_graph(), this creates a single filter, using explicit
+// options, and without involving the libavfilter graph parser. Instead of
+// a graph, it takes a filter name, and a key-value list of filter options
+// (which are applied with mp_set_avopts()).
+struct mp_lavfi *mp_lavfi_create_filter(struct mp_filter *parent,
+ enum mp_frame_type type, bool bidir,
+ char *hwdec_interop,
+ char **graph_opts,
+ const char *filter, char **filter_opts);
+
+struct mp_log;
+// Print libavfilter list for --vf/--af
+void print_lavfi_help_list(struct mp_log *log, int media_type);
+
+// Print libavfilter help for the given filter
+void print_lavfi_help(struct mp_log *log, const char *name, int media_type);
+
+// Return whether the given filter exists and has the required media_type in/outs.
+bool mp_lavfi_is_usable(const char *name, int media_type);
diff --git a/filters/f_output_chain.c b/filters/f_output_chain.c
new file mode 100644
index 0000000..2d4dcba
--- /dev/null
+++ b/filters/f_output_chain.c
@@ -0,0 +1,729 @@
+#include "audio/aframe.h"
+#include "audio/out/ao.h"
+#include "common/global.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "video/out/vo.h"
+
+#include "filter_internal.h"
+
+#include "f_autoconvert.h"
+#include "f_auto_filters.h"
+#include "f_lavfi.h"
+#include "f_output_chain.h"
+#include "f_utils.h"
+#include "user_filters.h"
+
+struct chain {
+ struct mp_filter *f;
+ struct mp_log *log;
+
+ enum mp_output_chain_type type;
+
+ // Expected media type.
+ enum mp_frame_type frame_type;
+
+ struct mp_stream_info stream_info;
+
+ struct mp_user_filter **pre_filters;
+ int num_pre_filters;
+ struct mp_user_filter **post_filters;
+ int num_post_filters;
+
+ struct mp_user_filter **user_filters;
+ int num_user_filters;
+
+ // Concatenated list of pre+user+post filters.
+ struct mp_user_filter **all_filters;
+ int num_all_filters;
+ // First input/last output of all_filters[].
+ struct mp_pin *filters_in, *filters_out;
+
+ struct mp_user_filter *input, *output, *convert_wrapper;
+ struct mp_autoconvert *convert;
+
+ struct vo *vo;
+ struct ao *ao;
+
+ struct mp_output_chain public;
+};
+
+// This wraps each individual "actual" filter for:
+// - isolating against its failure (logging it and disabling the filter)
+// - tracking its output format (mostly for logging)
+// - store extra per-filter information like the filter label
+struct mp_user_filter {
+ struct chain *p;
+
+ struct mp_filter *wrapper; // parent filter for f
+ struct mp_filter *f; // the actual user filter
+ struct m_obj_settings *args; // NULL, or list of 1 item with creation args
+ char *label;
+ bool generated_label;
+ char *name;
+
+ struct mp_image_params last_in_vformat;
+ struct mp_aframe *last_in_aformat;
+
+ bool last_is_active;
+
+ int64_t last_in_pts, last_out_pts;
+
+ bool failed;
+ bool error_eof_sent;
+};
+
+static void update_output_caps(struct chain *p)
+{
+ if (p->type != MP_OUTPUT_CHAIN_VIDEO)
+ return;
+
+ mp_autoconvert_clear(p->convert);
+
+ if (p->vo) {
+ uint8_t allowed_output_formats[IMGFMT_END - IMGFMT_START] = {0};
+ vo_query_formats(p->vo, allowed_output_formats);
+
+ for (int n = 0; n < MP_ARRAY_SIZE(allowed_output_formats); n++) {
+ if (allowed_output_formats[n])
+ mp_autoconvert_add_imgfmt(p->convert, IMGFMT_START + n, 0);
+ }
+ }
+}
+
+static void check_in_format_change(struct mp_user_filter *u,
+ struct mp_frame frame)
+{
+ struct chain *p = u->p;
+
+ if (frame.type == MP_FRAME_VIDEO) {
+ struct mp_image *img = frame.data;
+
+ if (!mp_image_params_equal(&img->params, &u->last_in_vformat)) {
+ MP_VERBOSE(p, "[%s] %s\n", u->name,
+ mp_image_params_to_str(&img->params));
+ u->last_in_vformat = img->params;
+
+ if (u == p->input) {
+ p->public.input_params = img->params;
+ } else if (u == p->output) {
+ p->public.output_params = img->params;
+ }
+
+ // Unfortunately there's no good place to update these.
+ // But a common case is enabling HW decoding, which
+ // might init some support of them in the VO, and update
+ // the VO's format list.
+ //
+ // But as this is only relevant to the "convert" filter, don't
+ // do this for the other filters as it is wasted work.
+ if (strcmp(u->name, "convert") == 0)
+ update_output_caps(p);
+
+ p->public.reconfig_happened = true;
+ }
+ }
+
+ if (frame.type == MP_FRAME_AUDIO) {
+ struct mp_aframe *aframe = frame.data;
+
+ if (!mp_aframe_config_equals(aframe, u->last_in_aformat)) {
+ MP_VERBOSE(p, "[%s] %s\n", u->name,
+ mp_aframe_format_str(aframe));
+ mp_aframe_config_copy(u->last_in_aformat, aframe);
+
+ if (u == p->input) {
+ mp_aframe_config_copy(p->public.input_aformat, aframe);
+ } else if (u == p->output) {
+ mp_aframe_config_copy(p->public.output_aformat, aframe);
+ }
+
+ p->public.reconfig_happened = true;
+ }
+ }
+}
+
+static void process_user(struct mp_filter *f)
+{
+ struct mp_user_filter *u = f->priv;
+ struct chain *p = u->p;
+
+ mp_filter_set_error_handler(u->f, f);
+ const char *name = u->label ? u->label : u->name;
+ assert(u->name);
+
+ if (!u->failed && mp_filter_has_failed(u->f)) {
+ if (u == p->convert_wrapper) {
+ // This is a fuckup we can't ignore.
+ MP_FATAL(p, "Cannot convert decoder/filter output to any format "
+ "supported by the output.\n");
+ p->public.failed_output_conversion = true;
+ mp_filter_wakeup(p->f);
+ } else {
+ MP_ERR(p, "Disabling filter %s because it has failed.\n", name);
+ mp_filter_reset(u->f); // clear out staled buffered data
+ }
+ u->failed = true;
+ }
+
+ if (u->failed) {
+ if (u == p->convert_wrapper) {
+ if (mp_pin_in_needs_data(f->ppins[1])) {
+ if (!u->error_eof_sent)
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ u->error_eof_sent = true;
+ }
+ return;
+ }
+
+ mp_pin_transfer_data(f->ppins[1], f->ppins[0]);
+ return;
+ }
+
+ if (mp_pin_can_transfer_data(u->f->pins[0], f->ppins[0])) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ check_in_format_change(u, frame);
+
+ double pts = mp_frame_get_pts(frame);
+ if (pts != MP_NOPTS_VALUE)
+ u->last_in_pts = pts;
+
+ mp_pin_in_write(u->f->pins[0], frame);
+ }
+
+ if (mp_pin_can_transfer_data(f->ppins[1], u->f->pins[1])) {
+ struct mp_frame frame = mp_pin_out_read(u->f->pins[1]);
+
+ double pts = mp_frame_get_pts(frame);
+ if (pts != MP_NOPTS_VALUE)
+ u->last_out_pts = pts;
+
+ mp_pin_in_write(f->ppins[1], frame);
+
+ struct mp_filter_command cmd = {.type = MP_FILTER_COMMAND_IS_ACTIVE};
+ if (mp_filter_command(u->f, &cmd) && u->last_is_active != cmd.is_active) {
+ u->last_is_active = cmd.is_active;
+ MP_VERBOSE(p, "[%s] (%sabled)\n", u->name,
+ u->last_is_active ? "en" : "dis");
+ }
+ }
+}
+
+static void reset_user(struct mp_filter *f)
+{
+ struct mp_user_filter *u = f->priv;
+
+ u->error_eof_sent = false;
+ u->last_in_pts = u->last_out_pts = MP_NOPTS_VALUE;
+}
+
+static void destroy_user(struct mp_filter *f)
+{
+ struct mp_user_filter *u = f->priv;
+
+ struct m_option dummy = {.type = &m_option_type_obj_settings_list};
+ m_option_free(&dummy, &u->args);
+
+ mp_filter_free_children(f);
+}
+
+static const struct mp_filter_info user_wrapper_filter = {
+ .name = "user_filter_wrapper",
+ .priv_size = sizeof(struct mp_user_filter),
+ .process = process_user,
+ .reset = reset_user,
+ .destroy = destroy_user,
+};
+
+static struct mp_user_filter *create_wrapper_filter(struct chain *p)
+{
+ struct mp_filter *f = mp_filter_create(p->f, &user_wrapper_filter);
+ if (!f)
+ abort();
+ struct mp_user_filter *wrapper = f->priv;
+ wrapper->wrapper = f;
+ wrapper->p = p;
+ wrapper->last_in_aformat = talloc_steal(wrapper, mp_aframe_create());
+ wrapper->last_is_active = true;
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+ return wrapper;
+}
+
+// Rebuild p->all_filters and relink the filters. Non-destructive if no change.
+static void relink_filter_list(struct chain *p)
+{
+ struct mp_user_filter **all_filters[3] =
+ {p->pre_filters, p->user_filters, p->post_filters};
+ int all_filters_num[3] =
+ {p->num_pre_filters, p->num_user_filters, p->num_post_filters};
+ p->num_all_filters = 0;
+ for (int n = 0; n < 3; n++) {
+ struct mp_user_filter **filters = all_filters[n];
+ int filters_num = all_filters_num[n];
+ for (int i = 0; i < filters_num; i++)
+ MP_TARRAY_APPEND(p, p->all_filters, p->num_all_filters, filters[i]);
+ }
+
+ assert(p->num_all_filters > 0);
+
+ p->filters_in = NULL;
+ p->filters_out = NULL;
+ for (int n = 0; n < p->num_all_filters; n++) {
+ struct mp_filter *f = p->all_filters[n]->wrapper;
+ if (n == 0)
+ p->filters_in = f->pins[0];
+ if (p->filters_out)
+ mp_pin_connect(f->pins[0], p->filters_out);
+ p->filters_out = f->pins[1];
+ }
+}
+
+static void process(struct mp_filter *f)
+{
+ struct chain *p = f->priv;
+
+ if (mp_pin_can_transfer_data(p->filters_in, f->ppins[0])) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (frame.type == MP_FRAME_EOF)
+ MP_VERBOSE(p, "filter input EOF\n");
+
+ if (frame.type == MP_FRAME_VIDEO && p->public.update_subtitles) {
+ p->public.update_subtitles(p->public.update_subtitles_ctx,
+ mp_frame_get_pts(frame));
+ }
+
+ mp_pin_in_write(p->filters_in, frame);
+ }
+
+ if (mp_pin_can_transfer_data(f->ppins[1], p->filters_out)) {
+ struct mp_frame frame = mp_pin_out_read(p->filters_out);
+
+ p->public.got_output_eof = frame.type == MP_FRAME_EOF;
+ if (p->public.got_output_eof)
+ MP_VERBOSE(p, "filter output EOF\n");
+
+ mp_pin_in_write(f->ppins[1], frame);
+ }
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct chain *p = f->priv;
+
+ p->public.ao_needs_update = false;
+
+ p->public.got_output_eof = false;
+}
+
+void mp_output_chain_reset_harder(struct mp_output_chain *c)
+{
+ struct chain *p = c->f->priv;
+
+ mp_filter_reset(p->f);
+
+ p->public.failed_output_conversion = false;
+ for (int n = 0; n < p->num_all_filters; n++) {
+ struct mp_user_filter *u = p->all_filters[n];
+
+ u->failed = false;
+ u->last_in_vformat = (struct mp_image_params){0};
+ mp_aframe_reset(u->last_in_aformat);
+ }
+
+ if (p->type == MP_OUTPUT_CHAIN_AUDIO) {
+ p->ao = NULL;
+ mp_autoconvert_clear(p->convert);
+ }
+}
+
+static void destroy(struct mp_filter *f)
+{
+ reset(f);
+}
+
+static const struct mp_filter_info output_chain_filter = {
+ .name = "output_chain",
+ .priv_size = sizeof(struct chain),
+ .process = process,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static double get_display_fps(struct mp_stream_info *i)
+{
+ struct chain *p = i->priv;
+ double res = 0;
+ if (p->vo)
+ vo_control(p->vo, VOCTRL_GET_DISPLAY_FPS, &res);
+ return res;
+}
+
+static void get_display_res(struct mp_stream_info *i, int *res)
+{
+ struct chain *p = i->priv;
+ if (p->vo)
+ vo_control(p->vo, VOCTRL_GET_DISPLAY_RES, res);
+}
+
+void mp_output_chain_set_vo(struct mp_output_chain *c, struct vo *vo)
+{
+ struct chain *p = c->f->priv;
+
+ p->stream_info.hwdec_devs = vo ? vo->hwdec_devs : NULL;
+ p->stream_info.osd = vo ? vo->osd : NULL;
+ p->stream_info.rotate90 = vo ? vo->driver->caps & VO_CAP_ROTATE90 : false;
+ p->stream_info.dr_vo = vo;
+ p->vo = vo;
+ update_output_caps(p);
+}
+
+void mp_output_chain_set_ao(struct mp_output_chain *c, struct ao *ao)
+{
+ struct chain *p = c->f->priv;
+
+ assert(p->public.ao_needs_update); // can't just call it any time
+ assert(!p->ao);
+
+ p->public.ao_needs_update = false;
+
+ p->ao = ao;
+
+ int out_format = 0;
+ int out_rate = 0;
+ struct mp_chmap out_channels = {0};
+ ao_get_format(p->ao, &out_rate, &out_format, &out_channels);
+
+ mp_autoconvert_clear(p->convert);
+ mp_autoconvert_add_afmt(p->convert, out_format);
+ mp_autoconvert_add_srate(p->convert, out_rate);
+ mp_autoconvert_add_chmap(p->convert, &out_channels);
+
+ mp_autoconvert_format_change_continue(p->convert);
+
+ // Just to get the format change logged again.
+ mp_aframe_reset(p->public.output_aformat);
+}
+
+static void on_audio_format_change(void *opaque)
+{
+ struct chain *p = opaque;
+
+ // Let the f_output_chain user know what format to use. (Not quite proper,
+ // since we overwrite what some other code normally automatically sets.
+ // The main issue is that this callback is called before output_aformat can
+ // be set, because we "block" the converter until the AO is reconfigured,
+ // and mp_autoconvert_format_change_continue() is called.)
+ mp_aframe_config_copy(p->public.output_aformat,
+ p->convert_wrapper->last_in_aformat);
+
+ // Ask for calling mp_output_chain_set_ao().
+ p->public.ao_needs_update = true;
+ p->ao = NULL;
+
+ // Do something silly to notify the f_output_chain user. (Not quite proper,
+ // it's merely that this will cause the core to run again, and check the
+ // flag set above.)
+ mp_filter_wakeup(p->f);
+}
+
+static struct mp_user_filter *find_by_label(struct chain *p, const char *label)
+{
+ for (int n = 0; n < p->num_user_filters; n++) {
+ struct mp_user_filter *u = p->user_filters[n];
+ if (label && u->label && strcmp(label, u->label) == 0)
+ return u;
+ }
+ return NULL;
+}
+
+bool mp_output_chain_command(struct mp_output_chain *c, const char *target,
+ struct mp_filter_command *cmd)
+{
+ struct chain *p = c->f->priv;
+
+ if (!target || !target[0])
+ return false;
+
+ if (strcmp(target, "all") == 0 && cmd->type == MP_FILTER_COMMAND_TEXT) {
+ // (Following old semantics.)
+ for (int n = 0; n < p->num_user_filters; n++)
+ mp_filter_command(p->user_filters[n]->f, cmd);
+ return true;
+ }
+
+ struct mp_user_filter *f = find_by_label(p, target);
+ if (!f)
+ return false;
+
+ return mp_filter_command(f->f, cmd);
+}
+
+// Set the speed on the last filter in the chain that supports it. If a filter
+// supports it, reset *speed, then keep setting the speed on the other filters.
+// The purpose of this is to make sure only 1 filter changes speed.
+static void set_speed_any(struct mp_user_filter **filters, int num_filters,
+ int command, double *speed)
+{
+ for (int n = num_filters - 1; n >= 0; n--) {
+ assert(*speed);
+ struct mp_filter_command cmd = {
+ .type = command,
+ .speed = *speed,
+ };
+ if (mp_filter_command(filters[n]->f, &cmd))
+ *speed = 1.0;
+ }
+}
+
+void mp_output_chain_set_audio_speed(struct mp_output_chain *c,
+ double speed, double resample, double drop)
+{
+ struct chain *p = c->f->priv;
+
+ // We always resample with the final libavresample instance.
+ set_speed_any(p->post_filters, p->num_post_filters,
+ MP_FILTER_COMMAND_SET_SPEED_RESAMPLE, &resample);
+
+ // If users have filters like "scaletempo" insert anywhere, use that,
+ // otherwise use the builtin ones.
+ set_speed_any(p->user_filters, p->num_user_filters,
+ MP_FILTER_COMMAND_SET_SPEED, &speed);
+ set_speed_any(p->post_filters, p->num_post_filters,
+ MP_FILTER_COMMAND_SET_SPEED, &speed);
+ set_speed_any(p->user_filters, p->num_user_filters,
+ MP_FILTER_COMMAND_SET_SPEED_DROP, &drop);
+ set_speed_any(p->post_filters, p->num_post_filters,
+ MP_FILTER_COMMAND_SET_SPEED_DROP, &drop);
+}
+
+double mp_output_get_measured_total_delay(struct mp_output_chain *c)
+{
+ struct chain *p = c->f->priv;
+
+ double delay = 0;
+
+ for (int n = 0; n < p->num_all_filters; n++) {
+ struct mp_user_filter *u = p->all_filters[n];
+
+ if (u->last_in_pts != MP_NOPTS_VALUE &&
+ u->last_out_pts != MP_NOPTS_VALUE)
+ {
+ delay += u->last_in_pts - u->last_out_pts;
+ }
+ }
+
+ return delay;
+}
+
+bool mp_output_chain_update_filters(struct mp_output_chain *c,
+ struct m_obj_settings *list)
+{
+ struct chain *p = c->f->priv;
+
+ struct mp_user_filter **add = NULL; // new filters
+ int num_add = 0;
+ struct mp_user_filter **res = NULL; // new final list
+ int num_res = 0;
+ bool *used = talloc_zero_array(NULL, bool, p->num_user_filters);
+
+ for (int n = 0; list && list[n].name; n++) {
+ struct m_obj_settings *entry = &list[n];
+
+ if (!entry->enabled)
+ continue;
+
+ struct mp_user_filter *u = NULL;
+
+ for (int i = 0; i < p->num_user_filters; i++) {
+ if (!used[i] && m_obj_settings_equal(entry, p->user_filters[i]->args))
+ {
+ u = p->user_filters[i];
+ used[i] = true;
+ break;
+ }
+ }
+
+ if (!u) {
+ u = create_wrapper_filter(p);
+ u->name = talloc_strdup(u, entry->name);
+ u->label = talloc_strdup(u, entry->label);
+ u->f = mp_create_user_filter(u->wrapper, p->type, entry->name,
+ entry->attribs);
+ if (!u->f) {
+ talloc_free(u->wrapper);
+ goto error;
+ }
+
+ struct m_obj_settings *args = (struct m_obj_settings[2]){*entry, {0}};
+
+ struct m_option dummy = {.type = &m_option_type_obj_settings_list};
+ m_option_copy(&dummy, &u->args, &args);
+
+ MP_TARRAY_APPEND(NULL, add, num_add, u);
+ }
+
+ MP_TARRAY_APPEND(p, res, num_res, u);
+ }
+
+ // At this point we definitely know we'll use the new list, so clean up.
+
+ for (int n = 0; n < p->num_user_filters; n++) {
+ if (!used[n])
+ talloc_free(p->user_filters[n]->wrapper);
+ }
+
+ talloc_free(p->user_filters);
+ p->user_filters = res;
+ p->num_user_filters = num_res;
+
+ relink_filter_list(p);
+
+ for (int n = 0; n < p->num_user_filters; n++) {
+ struct mp_user_filter *u = p->user_filters[n];
+ if (u->generated_label)
+ TA_FREEP(&u->label);
+ if (!u->label) {
+ for (int i = 0; i < 100; i++) {
+ char *label = mp_tprintf(80, "%s.%02d", u->name, i);
+ if (!find_by_label(p, label)) {
+ u->label = talloc_strdup(u, label);
+ u->generated_label = true;
+ break;
+ }
+ }
+ }
+ }
+
+ MP_VERBOSE(p, "User filter list:\n");
+ for (int n = 0; n < p->num_user_filters; n++) {
+ struct mp_user_filter *u = p->user_filters[n];
+ MP_VERBOSE(p, " %s (%s)\n", u->name, u->label ? u->label : "-");
+ }
+ if (!p->num_user_filters)
+ MP_VERBOSE(p, " (empty)\n");
+
+ // Filters can load hwdec interops, which might add new formats.
+ update_output_caps(p);
+
+ mp_filter_wakeup(p->f);
+
+ talloc_free(add);
+ talloc_free(used);
+ return true;
+
+error:
+ for (int n = 0; n < num_add; n++)
+ talloc_free(add[n]);
+ talloc_free(add);
+ talloc_free(used);
+ return false;
+}
+
+static void create_video_things(struct chain *p)
+{
+ p->frame_type = MP_FRAME_VIDEO;
+
+ p->stream_info.priv = p;
+ p->stream_info.get_display_fps = get_display_fps;
+ p->stream_info.get_display_res = get_display_res;
+
+ p->f->stream_info = &p->stream_info;
+
+ struct mp_user_filter *f = create_wrapper_filter(p);
+ f->name = "userdeint";
+ f->f = mp_deint_create(f->wrapper);
+ if (!f->f)
+ abort();
+ MP_TARRAY_APPEND(p, p->pre_filters, p->num_pre_filters, f);
+
+ f = create_wrapper_filter(p);
+ f->name = "autorotate";
+ f->f = mp_autorotate_create(f->wrapper);
+ if (!f->f)
+ abort();
+ MP_TARRAY_APPEND(p, p->post_filters, p->num_post_filters, f);
+}
+
+static void create_audio_things(struct chain *p)
+{
+ p->frame_type = MP_FRAME_AUDIO;
+
+ struct mp_user_filter *f = create_wrapper_filter(p);
+ f->name = "userspeed";
+ f->f = mp_autoaspeed_create(f->wrapper);
+ if (!f->f)
+ abort();
+ MP_TARRAY_APPEND(p, p->post_filters, p->num_post_filters, f);
+}
+
+struct mp_output_chain *mp_output_chain_create(struct mp_filter *parent,
+ enum mp_output_chain_type type)
+{
+ struct mp_filter *f = mp_filter_create(parent, &output_chain_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ const char *log_name = NULL;
+ switch (type) {
+ case MP_OUTPUT_CHAIN_VIDEO: log_name = "!vf"; break;
+ case MP_OUTPUT_CHAIN_AUDIO: log_name = "!af"; break;
+ }
+ if (log_name)
+ f->log = mp_log_new(f, parent->global->log, log_name);
+
+ struct chain *p = f->priv;
+ p->f = f;
+ p->log = f->log;
+ p->type = type;
+
+ struct mp_output_chain *c = &p->public;
+ c->f = f;
+ c->input_aformat = talloc_steal(p, mp_aframe_create());
+ c->output_aformat = talloc_steal(p, mp_aframe_create());
+
+ // Dummy filter for reporting and logging the input format.
+ p->input = create_wrapper_filter(p);
+ p->input->f = mp_bidir_nop_filter_create(p->input->wrapper);
+ if (!p->input->f)
+ abort();
+ p->input->name = "in";
+ MP_TARRAY_APPEND(p, p->pre_filters, p->num_pre_filters, p->input);
+
+ switch (type) {
+ case MP_OUTPUT_CHAIN_VIDEO: create_video_things(p); break;
+ case MP_OUTPUT_CHAIN_AUDIO: create_audio_things(p); break;
+ }
+
+ p->convert_wrapper = create_wrapper_filter(p);
+ p->convert = mp_autoconvert_create(p->convert_wrapper->wrapper);
+ if (!p->convert)
+ abort();
+ p->convert_wrapper->name = "convert";
+ p->convert_wrapper->f = p->convert->f;
+ MP_TARRAY_APPEND(p, p->post_filters, p->num_post_filters, p->convert_wrapper);
+
+ if (type == MP_OUTPUT_CHAIN_AUDIO) {
+ p->convert->on_audio_format_change = on_audio_format_change;
+ p->convert->on_audio_format_change_opaque = p;
+ }
+
+ // Dummy filter for reporting and logging the output format.
+ p->output = create_wrapper_filter(p);
+ p->output->f = mp_bidir_nop_filter_create(p->output->wrapper);
+ if (!p->output->f)
+ abort();
+ p->output->name = "out";
+ MP_TARRAY_APPEND(p, p->post_filters, p->num_post_filters, p->output);
+
+ relink_filter_list(p);
+
+ reset(f);
+
+ return c;
+}
diff --git a/filters/f_output_chain.h b/filters/f_output_chain.h
new file mode 100644
index 0000000..f06769c
--- /dev/null
+++ b/filters/f_output_chain.h
@@ -0,0 +1,87 @@
+#pragma once
+
+#include "options/m_option.h"
+#include "video/mp_image.h"
+
+#include "filter.h"
+
+enum mp_output_chain_type {
+ MP_OUTPUT_CHAIN_VIDEO = 1, // --vf
+ MP_OUTPUT_CHAIN_AUDIO, // --af
+};
+
+// A classic single-media filter chain, which reflects --vf and --af.
+// It manages the user-specified filter chain, and VO/AO output conversions.
+// Also handles some automatic filtering (auto rotation and such).
+struct mp_output_chain {
+ // This filter will have 1 input (from decoder) and 1 output (to VO/AO).
+ struct mp_filter *f;
+
+ bool got_output_eof;
+
+ // The filter chain output could not be converted to any format the output
+ // supports.
+ bool failed_output_conversion;
+
+ // Set if any formats in the chain changed. The user can reset the flag.
+ // For implementing change notifications out input/output_params.
+ bool reconfig_happened;
+
+ // --- for type==MP_OUTPUT_CHAIN_VIDEO
+ struct mp_image_params input_params;
+ struct mp_image_params output_params;
+ double container_fps;
+ void (*update_subtitles)(void *ctx, double pts);
+ void *update_subtitles_ctx;
+
+ // --- for type==MP_OUTPUT_CHAIN_AUDIO
+ struct mp_aframe *input_aformat;
+ struct mp_aframe *output_aformat;
+ // If true, there was a format change. output_aformat might have changed,
+ // and the implementation drained the filter chain and unset the internal ao
+ // reference. The API user needs to call mp_output_chain_set_ao() again.
+ // Until this is done, the filter chain will not output new data.
+ bool ao_needs_update;
+};
+
+// (free by freeing mp_output_chain.f)
+struct mp_output_chain *mp_output_chain_create(struct mp_filter *parent,
+ enum mp_output_chain_type type);
+
+// Set the VO, which will be used to determine basic capabilities like format
+// and rotation support, and to init hardware filtering things.
+// For type==MP_OUTPUT_CHAIN_VIDEO only.
+struct vo;
+void mp_output_chain_set_vo(struct mp_output_chain *p, struct vo *vo);
+
+// Set the AO. The AO format will be used to determine the filter chain output.
+// The API user may be asked to update the AO midstream if ao_needs_update is
+// set.
+// For type==MP_OUTPUT_CHAIN_AUDIO only.
+struct ao;
+void mp_output_chain_set_ao(struct mp_output_chain *p, struct ao *ao);
+
+// Send a command to the filter with the target label.
+bool mp_output_chain_command(struct mp_output_chain *p, const char *target,
+ struct mp_filter_command *cmd);
+
+// Perform a seek reset _and_ reset all filter failure states, so that future
+// filtering continues normally.
+void mp_output_chain_reset_harder(struct mp_output_chain *p);
+
+// Try to exchange the filter list. If creation of any filter fails, roll
+// back the changes, and return false.
+struct m_obj_settings;
+bool mp_output_chain_update_filters(struct mp_output_chain *p,
+ struct m_obj_settings *list);
+
+// Desired audio speed, with resample being strict resampling.
+void mp_output_chain_set_audio_speed(struct mp_output_chain *p,
+ double speed, double resample, double drop);
+
+// Total delay incurred by the filter chain, as measured by the recent filtered
+// frames. The intention is that this sums the measured delays for each filter,
+// so if a filter is removed, the caller can estimate how much audio is missing
+// due to the change.
+// Makes sense for audio only.
+double mp_output_get_measured_total_delay(struct mp_output_chain *p);
diff --git a/filters/f_swresample.c b/filters/f_swresample.c
new file mode 100644
index 0000000..8cb687d
--- /dev/null
+++ b/filters/f_swresample.c
@@ -0,0 +1,677 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/opt.h>
+#include <libavutil/common.h>
+#include <libavutil/samplefmt.h>
+#include <libavutil/channel_layout.h>
+#include <libavutil/mathematics.h>
+#include <libswresample/swresample.h>
+
+#include "audio/aframe.h"
+#include "audio/fmt-conversion.h"
+#include "audio/format.h"
+#include "common/common.h"
+#include "common/av_common.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+
+#include "f_swresample.h"
+#include "filter_internal.h"
+
+struct priv {
+ struct mp_log *log;
+ bool is_resampling;
+ struct SwrContext *avrctx;
+ struct mp_aframe *avrctx_fmt; // output format of avrctx
+ struct mp_aframe *pool_fmt; // format used to allocate frames for avrctx output
+ struct mp_aframe *pre_out_fmt; // format before final conversion
+ struct SwrContext *avrctx_out; // for output channel reordering
+ struct mp_resample_opts *opts; // opts requested by the user
+ // At least libswresample keeps a pointer around for this:
+ int reorder_in[MP_NUM_CHANNELS];
+ int reorder_out[MP_NUM_CHANNELS];
+ struct mp_aframe_pool *reorder_buffer;
+ struct mp_aframe_pool *out_pool;
+
+ int in_rate_user; // user input sample rate
+ int in_rate; // actual rate (used by lavr), adjusted for playback speed
+ int in_format;
+ struct mp_chmap in_channels;
+ int out_rate;
+ int out_format;
+ struct mp_chmap out_channels;
+
+ double current_pts;
+ struct mp_aframe *input;
+
+ double cmd_speed;
+ double speed;
+
+ struct mp_swresample public;
+};
+
+#define OPT_BASE_STRUCT struct mp_resample_opts
+const struct m_sub_options resample_conf = {
+ .opts = (const m_option_t[]) {
+ {"audio-resample-filter-size", OPT_INT(filter_size), M_RANGE(0, 32)},
+ {"audio-resample-phase-shift", OPT_INT(phase_shift), M_RANGE(0, 30)},
+ {"audio-resample-linear", OPT_BOOL(linear)},
+ {"audio-resample-cutoff", OPT_DOUBLE(cutoff), M_RANGE(0, 1)},
+ {"audio-normalize-downmix", OPT_BOOL(normalize)},
+ {"audio-resample-max-output-size", OPT_DOUBLE(max_output_frame_size)},
+ {"audio-swresample-o", OPT_KEYVALUELIST(avopts)},
+ {0}
+ },
+ .size = sizeof(struct mp_resample_opts),
+ .defaults = &(const struct mp_resample_opts)MP_RESAMPLE_OPTS_DEF,
+ .change_flags = UPDATE_AUDIO,
+};
+
+static double get_delay(struct priv *p)
+{
+ int64_t base = p->in_rate * (int64_t)p->out_rate;
+ return swr_get_delay(p->avrctx, base) / (double)base;
+}
+static int get_out_samples(struct priv *p, int in_samples)
+{
+ return swr_get_out_samples(p->avrctx, in_samples);
+}
+
+static void close_lavrr(struct priv *p)
+{
+ swr_free(&p->avrctx);
+ swr_free(&p->avrctx_out);
+
+ TA_FREEP(&p->pre_out_fmt);
+ TA_FREEP(&p->avrctx_fmt);
+ TA_FREEP(&p->pool_fmt);
+}
+
+static int rate_from_speed(int rate, double speed)
+{
+ return lrint(rate * speed);
+}
+
+static struct mp_chmap fudge_pairs[][2] = {
+ {MP_CHMAP2(BL, BR), MP_CHMAP2(SL, SR)},
+ {MP_CHMAP2(SL, SR), MP_CHMAP2(BL, BR)},
+ {MP_CHMAP2(SDL, SDR), MP_CHMAP2(SL, SR)},
+ {MP_CHMAP2(SL, SR), MP_CHMAP2(SDL, SDR)},
+};
+
+// Modify out_layout and return the new value. The intention is reducing the
+// loss libswresample's rematrixing will cause by exchanging similar, but
+// strictly speaking incompatible channel pairs. For example, 7.1 should be
+// changed to 7.1(wide) without dropping the SL/SR channels. (We still leave
+// it to libswresample to create the remix matrix.)
+static uint64_t fudge_layout_conversion(struct priv *p,
+ uint64_t in, uint64_t out)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(fudge_pairs); n++) {
+ uint64_t a = mp_chmap_to_lavc(&fudge_pairs[n][0]);
+ uint64_t b = mp_chmap_to_lavc(&fudge_pairs[n][1]);
+ if ((in & a) == a && (in & b) == 0 &&
+ (out & a) == 0 && (out & b) == b)
+ {
+ out = (out & ~b) | a;
+
+ MP_VERBOSE(p, "Fudge: %s -> %s\n",
+ mp_chmap_to_str(&fudge_pairs[n][0]),
+ mp_chmap_to_str(&fudge_pairs[n][1]));
+ }
+ }
+ return out;
+}
+
+// mp_chmap_get_reorder() performs:
+// to->speaker[n] = from->speaker[src[n]]
+// but libavresample does:
+// to->speaker[dst[n]] = from->speaker[n]
+static void transpose_order(int *map, int num)
+{
+ int nmap[MP_NUM_CHANNELS] = {0};
+ for (int n = 0; n < num; n++) {
+ for (int i = 0; i < num; i++) {
+ if (map[n] == i)
+ nmap[i] = n;
+ }
+ }
+ memcpy(map, nmap, sizeof(nmap));
+}
+
+static bool configure_lavrr(struct priv *p, bool verbose)
+{
+ close_lavrr(p);
+
+ p->in_rate = rate_from_speed(p->in_rate_user, p->speed);
+
+ MP_VERBOSE(p, "%dHz %s %s -> %dHz %s %s\n",
+ p->in_rate, mp_chmap_to_str(&p->in_channels),
+ af_fmt_to_str(p->in_format),
+ p->out_rate, mp_chmap_to_str(&p->out_channels),
+ af_fmt_to_str(p->out_format));
+
+ p->avrctx = swr_alloc();
+ p->avrctx_out = swr_alloc();
+ if (!p->avrctx || !p->avrctx_out)
+ goto error;
+
+ enum AVSampleFormat in_samplefmt = af_to_avformat(p->in_format);
+ enum AVSampleFormat out_samplefmt = af_to_avformat(p->out_format);
+ enum AVSampleFormat out_samplefmtp = av_get_planar_sample_fmt(out_samplefmt);
+
+ if (in_samplefmt == AV_SAMPLE_FMT_NONE ||
+ out_samplefmt == AV_SAMPLE_FMT_NONE ||
+ out_samplefmtp == AV_SAMPLE_FMT_NONE)
+ {
+ MP_ERR(p, "unsupported conversion: %s -> %s\n",
+ af_fmt_to_str(p->in_format), af_fmt_to_str(p->out_format));
+ goto error;
+ }
+
+ av_opt_set_int(p->avrctx, "filter_size", p->opts->filter_size, 0);
+ av_opt_set_int(p->avrctx, "phase_shift", p->opts->phase_shift, 0);
+ av_opt_set_int(p->avrctx, "linear_interp", p->opts->linear, 0);
+
+ double cutoff = p->opts->cutoff;
+ if (cutoff <= 0.0)
+ cutoff = MPMAX(1.0 - 6.5 / (p->opts->filter_size + 8), 0.80);
+ av_opt_set_double(p->avrctx, "cutoff", cutoff, 0);
+
+ int normalize = p->opts->normalize;
+ av_opt_set_double(p->avrctx, "rematrix_maxval", normalize ? 1 : 1000, 0);
+
+ if (mp_set_avopts(p->log, p->avrctx, p->opts->avopts) < 0)
+ goto error;
+
+ struct mp_chmap map_in = p->in_channels;
+ struct mp_chmap map_out = p->out_channels;
+
+ // Try not to do any remixing if at least one is "unknown". Some corner
+ // cases also benefit from disabling all channel handling logic if the
+ // src/dst layouts are the same (like fl-fr-na -> fl-fr-na).
+ if (mp_chmap_is_unknown(&map_in) || mp_chmap_is_unknown(&map_out) ||
+ mp_chmap_equals(&map_in, &map_out))
+ {
+ mp_chmap_set_unknown(&map_in, map_in.num);
+ mp_chmap_set_unknown(&map_out, map_out.num);
+ }
+
+ // unchecked: don't take any channel reordering into account
+ uint64_t in_ch_layout = mp_chmap_to_lavc_unchecked(&map_in);
+ uint64_t out_ch_layout = mp_chmap_to_lavc_unchecked(&map_out);
+
+ struct mp_chmap in_lavc, out_lavc;
+ mp_chmap_from_lavc(&in_lavc, in_ch_layout);
+ mp_chmap_from_lavc(&out_lavc, out_ch_layout);
+
+ if (verbose && !mp_chmap_equals(&in_lavc, &out_lavc)) {
+ MP_VERBOSE(p, "Remix: %s -> %s\n", mp_chmap_to_str(&in_lavc),
+ mp_chmap_to_str(&out_lavc));
+ }
+
+ if (in_lavc.num != map_in.num) {
+ // For handling NA channels, we would have to add a planarization step.
+ MP_FATAL(p, "Unsupported input channel layout %s.\n",
+ mp_chmap_to_str(&map_in));
+ goto error;
+ }
+
+ mp_chmap_get_reorder(p->reorder_in, &map_in, &in_lavc);
+ transpose_order(p->reorder_in, map_in.num);
+
+ if (mp_chmap_equals(&out_lavc, &map_out)) {
+ // No intermediate step required - output new format directly.
+ out_samplefmtp = out_samplefmt;
+ } else {
+ // Verify that we really just reorder and/or insert NA channels.
+ struct mp_chmap withna = out_lavc;
+ mp_chmap_fill_na(&withna, map_out.num);
+ if (withna.num != map_out.num)
+ goto error;
+ }
+ mp_chmap_get_reorder(p->reorder_out, &out_lavc, &map_out);
+
+ p->pre_out_fmt = mp_aframe_create();
+ mp_aframe_set_rate(p->pre_out_fmt, p->out_rate);
+ mp_aframe_set_chmap(p->pre_out_fmt, &p->out_channels);
+ mp_aframe_set_format(p->pre_out_fmt, p->out_format);
+
+ p->avrctx_fmt = mp_aframe_create();
+ mp_aframe_config_copy(p->avrctx_fmt, p->pre_out_fmt);
+ mp_aframe_set_chmap(p->avrctx_fmt, &out_lavc);
+ mp_aframe_set_format(p->avrctx_fmt, af_from_avformat(out_samplefmtp));
+
+ // If there are NA channels, the final output will have more channels than
+ // the avrctx output. Also, avrctx will output planar (out_samplefmtp was
+ // not overwritten). Allocate the output frame with more channels, so the
+ // NA channels can be trivially added.
+ p->pool_fmt = mp_aframe_create();
+ mp_aframe_config_copy(p->pool_fmt, p->avrctx_fmt);
+ if (map_out.num > out_lavc.num)
+ mp_aframe_set_chmap(p->pool_fmt, &map_out);
+
+ out_ch_layout = fudge_layout_conversion(p, in_ch_layout, out_ch_layout);
+
+ // Real conversion; output is input to avrctx_out.
+ av_opt_set_int(p->avrctx, "in_channel_layout", in_ch_layout, 0);
+ av_opt_set_int(p->avrctx, "out_channel_layout", out_ch_layout, 0);
+ av_opt_set_int(p->avrctx, "in_sample_rate", p->in_rate, 0);
+ av_opt_set_int(p->avrctx, "out_sample_rate", p->out_rate, 0);
+ av_opt_set_int(p->avrctx, "in_sample_fmt", in_samplefmt, 0);
+ av_opt_set_int(p->avrctx, "out_sample_fmt", out_samplefmtp, 0);
+
+ // Just needs the correct number of channels for deplanarization.
+ struct mp_chmap fake_chmap;
+ mp_chmap_set_unknown(&fake_chmap, map_out.num);
+ uint64_t fake_out_ch_layout = mp_chmap_to_lavc_unchecked(&fake_chmap);
+ if (!fake_out_ch_layout)
+ goto error;
+ av_opt_set_int(p->avrctx_out, "in_channel_layout", fake_out_ch_layout, 0);
+ av_opt_set_int(p->avrctx_out, "out_channel_layout", fake_out_ch_layout, 0);
+
+ av_opt_set_int(p->avrctx_out, "in_sample_fmt", out_samplefmtp, 0);
+ av_opt_set_int(p->avrctx_out, "out_sample_fmt", out_samplefmt, 0);
+ av_opt_set_int(p->avrctx_out, "in_sample_rate", p->out_rate, 0);
+ av_opt_set_int(p->avrctx_out, "out_sample_rate", p->out_rate, 0);
+
+ // API has weird requirements, quoting avresample.h:
+ // * This function can only be called when the allocated context is not open.
+ // * Also, the input channel layout must have already been set.
+ swr_set_channel_mapping(p->avrctx, p->reorder_in);
+
+ p->is_resampling = false;
+
+ if (swr_init(p->avrctx) < 0 || swr_init(p->avrctx_out) < 0) {
+ MP_ERR(p, "Cannot open Libavresample context.\n");
+ goto error;
+ }
+ return true;
+
+error:
+ close_lavrr(p);
+ mp_filter_internal_mark_failed(p->public.f);
+ MP_FATAL(p, "libswresample failed to initialize.\n");
+ return false;
+}
+
+static void reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ p->current_pts = MP_NOPTS_VALUE;
+ TA_FREEP(&p->input);
+
+ if (!p->avrctx)
+ return;
+ swr_close(p->avrctx);
+ if (swr_init(p->avrctx) < 0)
+ close_lavrr(p);
+}
+
+// This relies on the tricky way mpa was allocated.
+static bool reorder_planes(struct mp_aframe *mpa, int *reorder,
+ struct mp_chmap *newmap)
+{
+ if (!mp_aframe_set_chmap(mpa, newmap))
+ return false;
+
+ int num_planes = mp_aframe_get_planes(mpa);
+ uint8_t **planes = mp_aframe_get_data_rw(mpa);
+ if (num_planes && !planes)
+ return false;
+ uint8_t *old_planes[MP_NUM_CHANNELS];
+ assert(num_planes <= MP_NUM_CHANNELS);
+ for (int n = 0; n < num_planes; n++)
+ old_planes[n] = planes[n];
+
+ int next_na = 0;
+ for (int n = 0; n < num_planes; n++)
+ next_na += newmap->speaker[n] != MP_SPEAKER_ID_NA;
+
+ for (int n = 0; n < num_planes; n++) {
+ int src = reorder[n];
+ assert(src >= -1 && src < num_planes);
+ if (src >= 0) {
+ planes[n] = old_planes[src];
+ } else {
+ assert(next_na < num_planes);
+ planes[n] = old_planes[next_na++];
+ // The NA planes were never written by avrctx, so clear them.
+ af_fill_silence(planes[n],
+ mp_aframe_get_sstride(mpa) * mp_aframe_get_size(mpa),
+ mp_aframe_get_format(mpa));
+ }
+ }
+
+ return true;
+}
+
+static int resample_frame(struct SwrContext *r,
+ struct mp_aframe *out, struct mp_aframe *in,
+ int consume_in)
+{
+ // Be aware that the channel layout and count can be different for in and
+ // out frames. In some situations the caller will fix up the frames before
+ // or after conversion. The sample rates can also be different.
+ AVFrame *av_i = in ? mp_aframe_get_raw_avframe(in) : NULL;
+ AVFrame *av_o = out ? mp_aframe_get_raw_avframe(out) : NULL;
+ return swr_convert(r,
+ av_o ? av_o->extended_data : NULL,
+ av_o ? av_o->nb_samples : 0,
+ (const uint8_t **)(av_i ? av_i->extended_data : NULL),
+ av_i ? MPMIN(av_i->nb_samples, consume_in) : 0);
+}
+
+static struct mp_frame filter_resample_output(struct priv *p,
+ struct mp_aframe *in)
+{
+ struct mp_aframe *out = NULL;
+
+ if (!p->avrctx)
+ goto error;
+
+ // Limit the filtered data size for better latency when changing speed.
+ // Avoid buffering data within the resampler => restrict input size.
+ // p->in_rate already includes the speed factor.
+ double s = p->opts->max_output_frame_size / 1000 * p->in_rate;
+ int max_in = lrint(MPCLAMP(s, 128, INT_MAX));
+ int consume_in = in ? mp_aframe_get_size(in) : 0;
+ consume_in = MPMIN(consume_in, max_in);
+
+ int samples = get_out_samples(p, consume_in);
+ out = mp_aframe_create();
+ mp_aframe_config_copy(out, p->pool_fmt);
+ if (mp_aframe_pool_allocate(p->out_pool, out, samples) < 0)
+ goto error;
+
+ int out_samples = 0;
+ if (samples) {
+ out_samples = resample_frame(p->avrctx, out, in, consume_in);
+ if (out_samples < 0 || out_samples > samples)
+ goto error;
+ mp_aframe_set_size(out, out_samples);
+ }
+
+ struct mp_chmap out_chmap;
+ if (!mp_aframe_get_chmap(p->pool_fmt, &out_chmap))
+ goto error;
+ if (!reorder_planes(out, p->reorder_out, &out_chmap))
+ goto error;
+
+ if (!mp_aframe_config_equals(out, p->pre_out_fmt)) {
+ struct mp_aframe *new = mp_aframe_create();
+ mp_aframe_config_copy(new, p->pre_out_fmt);
+ if (mp_aframe_pool_allocate(p->reorder_buffer, new, out_samples) < 0) {
+ talloc_free(new);
+ goto error;
+ }
+ int got = 0;
+ if (out_samples)
+ got = resample_frame(p->avrctx_out, new, out, out_samples);
+ talloc_free(out);
+ out = new;
+ if (got != out_samples)
+ goto error;
+ }
+
+ if (in) {
+ mp_aframe_copy_attributes(out, in);
+ p->current_pts = mp_aframe_end_pts(in);
+ mp_aframe_skip_samples(in, consume_in);
+ }
+
+ if (out_samples) {
+ if (p->current_pts != MP_NOPTS_VALUE) {
+ double delay = get_delay(p) * mp_aframe_get_speed(out) +
+ mp_aframe_duration(out) +
+ (p->input ? mp_aframe_duration(p->input) : 0);
+ mp_aframe_set_pts(out, p->current_pts - delay);
+ mp_aframe_mul_speed(out, p->speed);
+ }
+ } else {
+ TA_FREEP(&out);
+ }
+
+ return out ? MAKE_FRAME(MP_FRAME_AUDIO, out) : MP_NO_FRAME;
+error:
+ talloc_free(out);
+ MP_ERR(p, "Error on resampling.\n");
+ mp_filter_internal_mark_failed(p->public.f);
+ return MP_NO_FRAME;
+}
+
+static void process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ p->speed = p->cmd_speed * p->public.speed;
+
+ struct mp_aframe *input = NULL;
+ if (!p->input) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (frame.type == MP_FRAME_AUDIO) {
+ input = frame.data;
+ } else if (!frame.type) {
+ return; // no new data
+ } else if (frame.type != MP_FRAME_EOF) {
+ MP_ERR(p, "Unsupported frame type.\n");
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+ return;
+ }
+
+ if (!input && !p->avrctx) {
+ // Obviously no draining needed.
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ return;
+ }
+ }
+
+ if (input) {
+ assert(!p->input);
+
+ struct mp_swresample *s = &p->public;
+
+ int in_rate = mp_aframe_get_rate(input);
+ int in_format = mp_aframe_get_format(input);
+ struct mp_chmap in_channels = {0};
+ mp_aframe_get_chmap(input, &in_channels);
+
+ if (!in_rate || !in_format || !in_channels.num) {
+ MP_ERR(p, "Frame with invalid format unsupported\n");
+ talloc_free(input);
+ mp_filter_internal_mark_failed(f);
+ return;
+ }
+
+ int out_rate = s->out_rate ? s->out_rate : in_rate;
+ int out_format = s->out_format ? s->out_format : in_format;
+ struct mp_chmap out_channels =
+ s->out_channels.num ? s->out_channels : in_channels;
+
+ if (p->in_rate_user != in_rate ||
+ p->in_format != in_format ||
+ !mp_chmap_equals(&p->in_channels, &in_channels) ||
+ p->out_rate != out_rate ||
+ p->out_format != out_format ||
+ !mp_chmap_equals(&p->out_channels, &out_channels) ||
+ !p->avrctx)
+ {
+ if (p->avrctx) {
+ // drain remaining audio
+ struct mp_frame out = filter_resample_output(p, NULL);
+ if (out.type) {
+ mp_pin_in_write(f->ppins[1], out);
+ // continue filtering next time.
+ mp_pin_out_unread(f->ppins[0],
+ MAKE_FRAME(MP_FRAME_AUDIO, input));
+ input = NULL;
+ }
+ }
+
+ MP_VERBOSE(p, "format change, reinitializing resampler\n");
+
+ p->in_rate_user = in_rate;
+ p->in_format = in_format;
+ p->in_channels = in_channels;
+ p->out_rate = out_rate;
+ p->out_format = out_format;
+ p->out_channels = out_channels;
+
+ if (!configure_lavrr(p, true)) {
+ talloc_free(input);
+ return;
+ }
+
+ if (!input) {
+ // continue filtering next time
+ mp_filter_internal_mark_progress(f);
+ return;
+ }
+ }
+
+ p->input = input;
+ }
+
+ int new_rate = rate_from_speed(p->in_rate_user, p->speed);
+ bool exact_rate = new_rate == p->in_rate;
+ bool use_comp = fabs(new_rate / (double)p->in_rate - 1) <= 0.01;
+ // If we've never used compensation, avoid setting it - even if it's in
+ // theory a NOP, libswresample will enable resampling. _If_ we're
+ // resampling, we might have to disable previously enabled compensation.
+ if (exact_rate && !p->is_resampling)
+ use_comp = false;
+ if (p->avrctx && use_comp) {
+ AVRational r =
+ av_d2q(p->speed * p->in_rate_user / p->in_rate, INT_MAX / 2);
+ // Essentially, swr_set_compensation() does 2 things:
+ // - adjust output sample rate by sample_delta/compensation_distance
+ // - reset the adjustment after compensation_distance output samples
+ // Increase the compensation_distance to avoid undesired reset
+ // semantics - we want to keep the ratio for the whole frame we're
+ // feeding it, until the next filter() call.
+ int mult = INT_MAX / 2 / MPMAX(MPMAX(abs(r.num), abs(r.den)), 1);
+ r = (AVRational){ r.num * mult, r.den * mult };
+ if (r.den == r.num)
+ r = (AVRational){0}; // fully disable
+ if (swr_set_compensation(p->avrctx, r.den - r.num, r.den) >= 0) {
+ exact_rate = true;
+ p->is_resampling = true; // libswresample can auto-enable it
+ }
+ }
+
+ if (!exact_rate) {
+ // Before reconfiguring, drain the audio that is still buffered
+ // in the resampler.
+ struct mp_frame out = filter_resample_output(p, NULL);
+ bool need_drain = !!out.type;
+ if (need_drain)
+ mp_pin_in_write(f->ppins[1], out);
+ // Reinitialize resampler.
+ configure_lavrr(p, false);
+ // If we've written output, we must continue filtering next time.
+ if (need_drain)
+ return;
+ }
+
+ struct mp_frame out = filter_resample_output(p, p->input);
+
+ if (out.type) {
+ mp_pin_in_write(f->ppins[1], out);
+ if (!p->input)
+ mp_pin_out_repeat_eof(f->ppins[0]);
+ } else if (p->input) {
+ mp_filter_internal_mark_progress(f); // try to consume more input
+ } else {
+ mp_pin_in_write(f->ppins[1], MP_EOF_FRAME);
+ }
+
+ if (p->input && !mp_aframe_get_size(p->input))
+ TA_FREEP(&p->input);
+}
+
+double mp_swresample_get_delay(struct mp_swresample *s)
+{
+ struct priv *p = s->f->priv;
+
+ return get_delay(p);
+}
+
+static bool command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *p = f->priv;
+
+ if (cmd->type == MP_FILTER_COMMAND_SET_SPEED_RESAMPLE) {
+ p->cmd_speed = cmd->speed;
+ return true;
+ }
+
+ return false;
+}
+
+static void destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ close_lavrr(p);
+ TA_FREEP(&p->input);
+}
+
+static const struct mp_filter_info swresample_filter = {
+ .name = "swresample",
+ .priv_size = sizeof(struct priv),
+ .process = process,
+ .command = command,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+struct mp_swresample *mp_swresample_create(struct mp_filter *parent,
+ struct mp_resample_opts *opts)
+{
+ struct mp_filter *f = mp_filter_create(parent, &swresample_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->public.f = f;
+ p->public.speed = 1.0;
+ p->cmd_speed = 1.0;
+ p->log = f->log;
+
+ if (opts) {
+ p->opts = talloc_dup(p, opts);
+ p->opts->avopts = mp_dup_str_array(p, p->opts->avopts);
+ } else {
+ p->opts = mp_get_config_group(p, f->global, &resample_conf);
+ }
+
+ p->reorder_buffer = mp_aframe_pool_create(p);
+ p->out_pool = mp_aframe_pool_create(p);
+
+ return &p->public;
+}
diff --git a/filters/f_swresample.h b/filters/f_swresample.h
new file mode 100644
index 0000000..8ef3335
--- /dev/null
+++ b/filters/f_swresample.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <stdbool.h>
+
+#include "audio/chmap.h"
+#include "filter.h"
+
+// Resampler filter, wrapping libswresample or libavresample.
+struct mp_swresample {
+ struct mp_filter *f;
+ // Desired output parameters. For unset parameters, passes through the
+ // format.
+ int out_rate;
+ int out_format;
+ struct mp_chmap out_channels;
+ double speed;
+};
+
+struct mp_resample_opts {
+ int filter_size;
+ int phase_shift;
+ bool linear;
+ double cutoff;
+ bool normalize;
+ int allow_passthrough;
+ double max_output_frame_size;
+ char **avopts;
+};
+
+#define MP_RESAMPLE_OPTS_DEF { \
+ .filter_size = 16, \
+ .cutoff = 0.0, \
+ .phase_shift = 10, \
+ .normalize = 0, \
+ .max_output_frame_size = 40,\
+ }
+
+// Create the filter. If opts==NULL, use the global options as defaults.
+// Free with talloc_free(mp_swresample.f).
+struct mp_swresample *mp_swresample_create(struct mp_filter *parent,
+ struct mp_resample_opts *opts);
+
+// Internal resampler delay. Does not include data buffered in mp_pins and such.
+double mp_swresample_get_delay(struct mp_swresample *s);
diff --git a/filters/f_swscale.c b/filters/f_swscale.c
new file mode 100644
index 0000000..4aca609
--- /dev/null
+++ b/filters/f_swscale.c
@@ -0,0 +1,153 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <inttypes.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include <libswscale/swscale.h>
+
+#include "common/av_common.h"
+#include "common/msg.h"
+
+#include "options/options.h"
+
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+#include "video/sws_utils.h"
+#include "video/fmt-conversion.h"
+
+#include "f_swscale.h"
+#include "filter.h"
+#include "filter_internal.h"
+
+int mp_sws_find_best_out_format(struct mp_sws_filter *sws, int in_format,
+ int *out_formats, int num_out_formats)
+{
+ sws->sws->force_scaler = sws->force_scaler;
+
+ int best = 0;
+ for (int n = 0; n < num_out_formats; n++) {
+ int out_format = out_formats[n];
+
+ if (!mp_sws_supports_formats(sws->sws, out_format, in_format))
+ continue;
+
+ if (best) {
+ int candidate = mp_imgfmt_select_best(best, out_format, in_format);
+ if (candidate)
+ best = candidate;
+ } else {
+ best = out_format;
+ }
+ }
+ return best;
+}
+
+bool mp_sws_supports_input(int imgfmt)
+{
+ return sws_isSupportedInput(imgfmt2pixfmt(imgfmt));
+}
+
+static void process(struct mp_filter *f)
+{
+ struct mp_sws_filter *s = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ s->sws->force_scaler = s->force_scaler;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (mp_frame_is_signaling(frame)) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+
+ if (frame.type != MP_FRAME_VIDEO) {
+ MP_ERR(f, "video frame expected\n");
+ goto error;
+ }
+
+ struct mp_image *src = frame.data;
+
+ int dstfmt = s->out_format ? s->out_format : src->imgfmt;
+ int w = src->w;
+ int h = src->h;
+
+ if (s->use_out_params) {
+ w = s->out_params.w;
+ h = s->out_params.h;
+ dstfmt = s->out_params.imgfmt;
+ }
+
+ struct mp_image *dst = mp_image_pool_get(s->pool, dstfmt, w, h);
+ if (!dst)
+ goto error;
+
+ mp_image_copy_attributes(dst, src);
+
+ if (s->use_out_params)
+ dst->params = s->out_params;
+ mp_image_params_guess_csp(&dst->params);
+
+ bool ok = mp_sws_scale(s->sws, dst, src) >= 0;
+
+ mp_frame_unref(&frame);
+ frame = (struct mp_frame){MP_FRAME_VIDEO, dst};
+
+ if (!ok)
+ goto error;
+
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+
+error:
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+ return;
+}
+
+static const struct mp_filter_info sws_filter = {
+ .name = "swscale",
+ .priv_size = sizeof(struct mp_sws_filter),
+ .process = process,
+};
+
+struct mp_sws_filter *mp_sws_filter_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &sws_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct mp_sws_filter *s = f->priv;
+ s->f = f;
+ s->sws = mp_sws_alloc(s);
+ s->sws->log = f->log;
+ mp_sws_enable_cmdline_opts(s->sws, f->global);
+ s->pool = mp_image_pool_new(s);
+
+ return s;
+}
diff --git a/filters/f_swscale.h b/filters/f_swscale.h
new file mode 100644
index 0000000..3ee7455
--- /dev/null
+++ b/filters/f_swscale.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <stdbool.h>
+
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+
+struct mp_sws_filter {
+ struct mp_filter *f;
+ // Desired output imgfmt. If 0, uses the input format.
+ int out_format;
+ // If set, force all image params; ignores out_format.
+ bool use_out_params;
+ struct mp_image_params out_params;
+ // Other options.
+ enum mp_sws_scaler force_scaler;
+ // private state
+ struct mp_sws_context *sws;
+ struct mp_image_pool *pool;
+};
+
+// Create the filter. Free it with talloc_free(mp_sws_filter.f).
+struct mp_sws_filter *mp_sws_filter_create(struct mp_filter *parent);
+
+// Return the best format based on the input format and a list of allowed output
+// formats. This tries to set the output format to the one that will result in
+// the least loss. Returns a format from out_formats[], or 0 if no format could
+// be chosen (or it's not supported by libswscale).
+int mp_sws_find_best_out_format(struct mp_sws_filter *sws,
+ int in_format, int *out_formats,
+ int num_out_formats);
+
+// Whether the given format is supported as input format.
+bool mp_sws_supports_input(int imgfmt);
diff --git a/filters/f_utils.c b/filters/f_utils.c
new file mode 100644
index 0000000..86ef106
--- /dev/null
+++ b/filters/f_utils.c
@@ -0,0 +1,311 @@
+#include "audio/aframe.h"
+#include "video/mp_image.h"
+
+#include "f_utils.h"
+#include "filter_internal.h"
+
+struct frame_duration_priv {
+ struct mp_image *buffered;
+};
+
+static void frame_duration_process(struct mp_filter *f)
+{
+ struct frame_duration_priv *p = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (frame.type == MP_FRAME_EOF && p->buffered) {
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_VIDEO, p->buffered));
+ p->buffered = NULL;
+ // Pass through the actual EOF in the next iteration.
+ mp_pin_out_repeat_eof(f->ppins[0]);
+ } else if (frame.type == MP_FRAME_VIDEO) {
+ struct mp_image *next = frame.data;
+ if (p->buffered) {
+ if (p->buffered->pts != MP_NOPTS_VALUE &&
+ next->pts != MP_NOPTS_VALUE &&
+ next->pts >= p->buffered->pts)
+ {
+ p->buffered->pkt_duration = next->pts - p->buffered->pts;
+ }
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_VIDEO, p->buffered));
+ } else {
+ mp_pin_out_request_data(f->ppins[0]);
+ }
+ p->buffered = next;
+ } else {
+ mp_pin_in_write(f->ppins[1], frame);
+ }
+}
+
+static void frame_duration_reset(struct mp_filter *f)
+{
+ struct frame_duration_priv *p = f->priv;
+
+ mp_image_unrefp(&p->buffered);
+}
+
+static const struct mp_filter_info frame_duration_filter = {
+ .name = "frame_duration",
+ .priv_size = sizeof(struct frame_duration_priv),
+ .process = frame_duration_process,
+ .reset = frame_duration_reset,
+ .destroy = frame_duration_reset,
+};
+
+struct mp_filter *mp_compute_frame_duration_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &frame_duration_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ return f;
+}
+
+void mp_chain_filters(struct mp_pin *in, struct mp_pin *out,
+ struct mp_filter **filters, int num_filters)
+{
+ for (int n = 0; n < num_filters; n++) {
+ if (!filters[n])
+ continue;
+ assert(filters[n]->num_pins == 2);
+ mp_pin_connect(filters[n]->pins[0], in);
+ in = filters[n]->pins[1];
+ }
+ mp_pin_connect(out, in);
+}
+
+// Make it repeat process().
+static void mark_progress(struct mp_subfilter *sub)
+{
+ // f == NULL is not really allowed, but at least don't crash.
+ struct mp_filter *f = mp_pin_get_manual_connection(sub->in);
+ if (f)
+ mp_filter_internal_mark_progress(f);
+}
+
+bool mp_subfilter_read(struct mp_subfilter *sub)
+{
+ if (sub->filter) {
+ if (mp_pin_can_transfer_data(sub->out, sub->filter->pins[1])) {
+ struct mp_frame frame = mp_pin_out_read(sub->filter->pins[1]);
+ if (sub->draining && frame.type == MP_FRAME_EOF) {
+ sub->draining = false;
+ TA_FREEP(&sub->filter);
+ mark_progress(sub);
+ return false;
+ }
+ mp_pin_in_write(sub->out, frame);
+ return false;
+ }
+ if (sub->draining)
+ return false;
+ }
+
+ struct mp_pin *out = sub->filter ? sub->filter->pins[0] : sub->out;
+
+ if (sub->frame.type)
+ return mp_pin_in_needs_data(out);
+
+ if (!mp_pin_can_transfer_data(out, sub->in))
+ return false;
+
+ sub->frame = mp_pin_out_read(sub->in);
+ return true;
+}
+
+void mp_subfilter_reset(struct mp_subfilter *sub)
+{
+ if (sub->filter && sub->draining)
+ TA_FREEP(&sub->filter);
+ sub->draining = false;
+ mp_frame_unref(&sub->frame);
+}
+
+void mp_subfilter_continue(struct mp_subfilter *sub)
+{
+ struct mp_pin *out = sub->filter ? sub->filter->pins[0] : sub->out;
+ // It was made sure earlier that the pin is writable, unless the filter
+ // was newly created, or a previously existing filter (which was going to
+ // accept input) was destroyed. In those cases, essentially restart
+ // data flow.
+ if (!mp_pin_in_needs_data(out)) {
+ mark_progress(sub);
+ return;
+ }
+ mp_pin_in_write(out, sub->frame);
+ sub->frame = MP_NO_FRAME;
+}
+
+void mp_subfilter_destroy(struct mp_subfilter *sub)
+{
+ TA_FREEP(&sub->filter);
+ sub->draining = false;
+}
+
+bool mp_subfilter_drain_destroy(struct mp_subfilter *sub)
+{
+ if (!sub->draining && sub->filter) {
+ // We know the filter is writable (unless the user created a new filter
+ // and immediately called this function, which is invalid).
+ mp_pin_in_write(sub->filter->pins[0], MP_EOF_FRAME);
+ sub->draining = true;
+ }
+ return !sub->filter;
+}
+
+static const struct mp_filter_info bidir_nop_filter = {
+ .name = "nop",
+};
+
+struct mp_filter *mp_bidir_nop_filter_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &bidir_nop_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ mp_pin_connect(f->ppins[1], f->ppins[0]);
+
+ return f;
+}
+
+static const struct mp_filter_info bidir_dummy_filter = {
+ .name = "dummy",
+};
+
+struct mp_filter *mp_bidir_dummy_filter_create(struct mp_filter *parent)
+{
+ struct mp_filter *f = mp_filter_create(parent, &bidir_dummy_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ return f;
+}
+
+struct fixed_aframe_size_priv {
+ int samples;
+ bool pad_silence;
+ struct mp_aframe *in;
+ struct mp_aframe *out;
+ int out_written; // valid samples in out
+ struct mp_aframe_pool *pool;
+};
+
+static void fixed_aframe_size_process(struct mp_filter *f)
+{
+ struct fixed_aframe_size_priv *p = f->priv;
+
+ if (!mp_pin_in_needs_data(f->ppins[1]))
+ return;
+
+ if (p->in && !mp_aframe_get_size(p->in))
+ TA_FREEP(&p->in);
+
+ if (!p->in) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (frame.type == MP_FRAME_EOF) {
+ if (!p->out) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+ mp_pin_out_repeat_eof(f->ppins[0]);
+ } else if (frame.type == MP_FRAME_AUDIO) {
+ p->in = frame.data;
+ if (p->out && !mp_aframe_config_equals(p->out, p->in)) {
+ mp_pin_out_unread(f->ppins[0], frame);
+ p->in = NULL;
+ }
+ } else if (frame.type) {
+ MP_ERR(f, "unsupported frame type\n");
+ mp_filter_internal_mark_failed(f);
+ return;
+ } else {
+ return; // no new data yet
+ }
+ }
+
+ if (p->in) {
+ if (!p->out) {
+ p->out = mp_aframe_create();
+ mp_aframe_config_copy(p->out, p->in);
+ mp_aframe_copy_attributes(p->out, p->in);
+ if (mp_aframe_pool_allocate(p->pool, p->out, p->samples) < 0) {
+ mp_filter_internal_mark_failed(f);
+ return;
+ }
+ p->out_written = 0;
+ }
+ int in_samples = mp_aframe_get_size(p->in);
+ int copy = MPMIN(in_samples, p->samples - p->out_written);
+ if (!mp_aframe_copy_samples(p->out, p->out_written, p->in, 0, copy))
+ MP_ASSERT_UNREACHABLE();
+ mp_aframe_skip_samples(p->in, copy);
+ p->out_written += copy;
+ }
+
+ // p->in not set means draining for EOF or format change
+ if ((!p->in && p->out_written) || p->out_written == p->samples) {
+ int missing = p->samples - p->out_written;
+ assert(missing >= 0);
+ if (missing) {
+ mp_aframe_set_silence(p->out, p->out_written, missing);
+ if (!p->pad_silence)
+ mp_aframe_set_size(p->out, p->out_written);
+ }
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, p->out));
+ p->out = NULL;
+ p->out_written = 0;
+ } else {
+ mp_pin_out_request_data_next(f->ppins[0]);
+ }
+}
+
+static void fixed_aframe_size_reset(struct mp_filter *f)
+{
+ struct fixed_aframe_size_priv *p = f->priv;
+
+ TA_FREEP(&p->in);
+ TA_FREEP(&p->out);
+ p->out_written = 0;
+}
+
+static const struct mp_filter_info fixed_aframe_size_filter = {
+ .name = "fixed_aframe_size",
+ .priv_size = sizeof(struct fixed_aframe_size_priv),
+ .process = fixed_aframe_size_process,
+ .reset = fixed_aframe_size_reset,
+ .destroy = fixed_aframe_size_reset,
+};
+
+struct mp_filter *mp_fixed_aframe_size_create(struct mp_filter *parent,
+ int samples, bool pad_silence)
+{
+ if (samples < 1)
+ return NULL;
+
+ struct mp_filter *f = mp_filter_create(parent, &fixed_aframe_size_filter);
+ if (!f)
+ return NULL;
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct fixed_aframe_size_priv *p = f->priv;
+ p->samples = samples;
+ p->pad_silence = pad_silence;
+ p->pool = mp_aframe_pool_create(p);
+
+ return f;
+}
diff --git a/filters/f_utils.h b/filters/f_utils.h
new file mode 100644
index 0000000..05c5c8e
--- /dev/null
+++ b/filters/f_utils.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "filter.h"
+
+// Filter that computes the exact duration of video frames by buffering 1 frame,
+// and taking the PTS difference. This supports video frames only, and stores
+// the duration in mp_image.pkt_duration. All other frame types are passed
+// through.
+struct mp_filter *mp_compute_frame_duration_create(struct mp_filter *parent);
+
+// Given the filters[0..num_filters] array, connect in with the input of the
+// first filter, connect the output of the first filter to the input to the
+// second filter, etc., until out. All filters are assumed to be bidirectional,
+// with input on pin 0 and output on pin 1. NULL entries are skipped.
+void mp_chain_filters(struct mp_pin *in, struct mp_pin *out,
+ struct mp_filter **filters, int num_filters);
+
+// Helper for maintaining a sub-filter that is created or destroyed on demand,
+// because it might depend on frame input formats or is otherwise dynamically
+// changing. (This is overkill for more static sub filters, or entirely manual
+// filtering.)
+// To initialize this, zero-init all fields, and set the in/out fields.
+struct mp_subfilter {
+ // These two fields must be set on init. The pins must have a manual
+ // connection to the filter whose process() function calls the
+ // mp_subfilter_*() functions.
+ struct mp_pin *in, *out;
+ // Temporary buffered frame, as triggered by mp_subfilter_read(). You can
+ // not mutate this (unless you didn't create or destroy sub->filter).
+ struct mp_frame frame;
+ // The sub-filter, set by the user. Can be NULL if disabled. If set, this
+ // must be a bidirectional filter, with manual connections same as
+ // mp_sub_filter.in/out (to get the correct process() function called).
+ // Set this only if it's NULL. You should not overwrite this if it's set.
+ // Use either mp_subfilter_drain_destroy(), mp_subfilter_destroy(), or
+ // mp_subfilter_reset() to unset and destroy the filter gracefully.
+ struct mp_filter *filter;
+ // Internal state.
+ bool draining;
+};
+
+// Make requests for a new frame.
+// Returns whether sub->frame is set to anything. If true is returned, you
+// must either call mp_subfilter_continue() or mp_subfilter_drain_destroy()
+// once to continue data flow normally (otherwise it will stall). If you call
+// mp_subfilter_drain_destroy(), and it returns true, or you call
+// mp_subfilter_destroy(), you can call mp_subfilter_continue() once after it.
+// If this returns true, sub->frame is never unset (MP_FRAME_NONE).
+bool mp_subfilter_read(struct mp_subfilter *sub);
+
+// Clear internal state (usually to be called by parent filter's reset(), or
+// destroy()). This usually does not free sub->filter.
+void mp_subfilter_reset(struct mp_subfilter *sub);
+
+// Continue filtering sub->frame. This can happen after setting a new filter
+// too.
+void mp_subfilter_continue(struct mp_subfilter *sub);
+
+// Destroy the filter immediately (if it's set). You must call
+// mp_subfilter_continue() after this to propagate sub->frame.
+void mp_subfilter_destroy(struct mp_subfilter *sub);
+
+// Make sure the filter is destroyed. Returns true if the filter was destroyed.
+// If this returns false, exit your process() function, so dataflow can
+// continue normally. (process() is repeated until this function returns true,
+// which can take a while if sub->filter has many frames buffered).
+// If this returns true, call mp_subfilter_continue() to propagate sub->frame.
+// The filter is destroyed with talloc_free(sub->filter).
+bool mp_subfilter_drain_destroy(struct mp_subfilter *sub);
+
+// A bidirectional filter which passes through all data.
+struct mp_filter *mp_bidir_nop_filter_create(struct mp_filter *parent);
+
+// A bidirectional filter which does not connect its pins. Instead, the user is,
+// by convention, allowed to access the filter's private pins, and use them
+// freely. (This is sometimes convenient, such as when you need to pass a single
+// filter instance to other code, and you don't need a full "proper" filter.)
+struct mp_filter *mp_bidir_dummy_filter_create(struct mp_filter *parent);
+
+// A filter which repacks audio frame to fixed frame sizes with the given
+// number of samples. On hard format changes (sample format/channels/srate),
+// the frame can be shorter, unless pad_silence is true. Fails on non-aframes.
+struct mp_filter *mp_fixed_aframe_size_create(struct mp_filter *parent,
+ int samples, bool pad_silence);
diff --git a/filters/filter.c b/filters/filter.c
new file mode 100644
index 0000000..1d13393
--- /dev/null
+++ b/filters/filter.c
@@ -0,0 +1,909 @@
+#include <math.h>
+#include <stdatomic.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "video/hwdec.h"
+#include "video/img_format.h"
+
+#include "filter.h"
+#include "filter_internal.h"
+
+// Note about connections:
+// They can be confusing, because pins come in pairs, and multiple pins can be
+// transitively connected via mp_pin_connect(). To avoid dealing with this,
+// mp_pin.conn is used to skip redundant connected pins.
+// Consider <1a|1b> a symbol for mp_pin pair #1 and f1 as filter #1. Then:
+// f1 <-> <1a|1b> <-> <2a|2b> <-> <3a|3b> <-> f2
+// would be a connection from 1a to 3b. 1a could be a private pin of f1 (e.g.
+// mp_filter.ppin[0]), and 1b would be the public pin (e.g. mp_filter.pin[0]).
+// A user could have called mp_pin_connect(2a, 1b) mp_pin_connect(3a, 2b)
+// (assuming 1b has dir==MP_PIN_OUT). The end result are the following values:
+// pin user_conn conn manual_connection within_conn (uses mp_pin.data)
+// 1a NULL 3b f1 false no
+// 1b 2a NULL NULL true no
+// 2a 1b NULL NULL true no
+// 2b 3a NULL NULL true no
+// 3a 2b NULL NULL true no
+// 3b NULL 1a f2 false yes
+// The minimal case of f1 <-> <1a|1b> <-> f2 (1b dir=out) would be:
+// 1a NULL 1b f1 false no
+// 1b NULL 1a f2 false yes
+// In both cases, only the final output pin uses mp_pin.data/data_requested.
+struct mp_pin {
+ const char *name;
+ enum mp_pin_dir dir;
+ struct mp_pin *other; // paired mp_pin representing other end
+ struct mp_filter *owner;
+
+ struct mp_pin *user_conn; // as set by mp_pin_connect()
+ struct mp_pin *conn; // transitive, actual end of the connection
+
+ // Set if the pin is considered connected, but has no user_conn. pin
+ // state changes are handled by the given filter. (Defaults to the root
+ // filter if the pin is for the user of a filter graph.)
+ // As an invariant, conn and manual_connection are both either set or unset.
+ struct mp_filter *manual_connection;
+
+ // Set if the pin is indirect part of a connection chain, but not one of
+ // the end pins. Basically it's a redundant in-between pin. You never access
+ // these with the pin data flow functions, because only the end pins matter.
+ // This flag is for checking and enforcing this.
+ bool within_conn;
+
+ // This is used for the final output mp_pin in connections only.
+ bool data_requested; // true if out wants new data
+ struct mp_frame data; // possibly buffered frame (MP_FRAME_NONE if
+ // empty, usually only temporary)
+};
+
+// Root filters create this, all other filters reference it.
+struct filter_runner {
+ struct mpv_global *global;
+
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_ctx;
+
+ struct mp_filter *root_filter;
+
+ double max_run_time;
+ atomic_bool interrupt_flag;
+
+ // If we're currently running the filter graph (for avoiding recursion).
+ bool filtering;
+
+ // If set, recursive filtering was initiated through this pin.
+ struct mp_pin *recursive;
+
+ // Set of filters which need process() to be called. A filter is in this
+ // array iff mp_filter_internal.pending==true.
+ struct mp_filter **pending;
+ int num_pending;
+
+ // Any outside pins have changed state.
+ bool external_pending;
+
+ // For async notifications only. We don't bother making this fine grained
+ // across filters.
+ mp_mutex async_lock;
+
+ // Wakeup is pending. Protected by async_lock.
+ bool async_wakeup_sent;
+
+ // Similar to pending[]. Uses mp_filter_internal.async_pending. Protected
+ // by async_lock.
+ struct mp_filter **async_pending;
+ int num_async_pending;
+};
+
+struct mp_filter_internal {
+ const struct mp_filter_info *info;
+
+ struct mp_filter *parent;
+ struct filter_runner *runner;
+
+ struct mp_filter **children;
+ int num_children;
+
+ struct mp_filter *error_handler;
+
+ char *name;
+ bool high_priority;
+
+ bool pending;
+ bool async_pending;
+ bool failed;
+};
+
+// Called when new work needs to be done on a pin belonging to the filter:
+// - new data was requested
+// - new data has been queued
+// - or just an connect/disconnect/async notification happened
+// This means the process function for this filter has to be called at some
+// point in the future to continue filtering.
+static void add_pending(struct mp_filter *f)
+{
+ struct filter_runner *r = f->in->runner;
+
+ if (f->in->pending)
+ return;
+
+ // This should probably really be some sort of priority queue, but for now
+ // something naive and dumb does the job too.
+ f->in->pending = true;
+ if (f->in->high_priority) {
+ MP_TARRAY_INSERT_AT(r, r->pending, r->num_pending, 0, f);
+ } else {
+ MP_TARRAY_APPEND(r, r->pending, r->num_pending, f);
+ }
+}
+
+static void add_pending_pin(struct mp_pin *p)
+{
+ struct mp_filter *f = p->manual_connection;
+ assert(f);
+
+ if (f->in->pending)
+ return;
+
+ add_pending(f);
+
+ // Need to tell user that something changed.
+ if (f == f->in->runner->root_filter && p != f->in->runner->recursive)
+ f->in->runner->external_pending = true;
+}
+
+// Possibly enter recursive filtering. This is done as convenience for
+// "external" filter users only. (Normal filtering does this iteratively via
+// mp_filter_graph_run() to avoid filter reentrancy issues and deep call
+// stacks.) If the API users uses an external manually connected pin, do
+// recursive filtering as a not strictly necessary feature which makes outside
+// I/O with filters easier.
+static void filter_recursive(struct mp_pin *p)
+{
+ struct mp_filter *f = p->conn->manual_connection;
+ assert(f);
+ struct filter_runner *r = f->in->runner;
+
+ // Never do internal filtering recursively.
+ if (r->filtering)
+ return;
+
+ assert(!r->recursive);
+ r->recursive = p;
+
+ // Also don't lose the pending state, which the user may or may not
+ // care about.
+ r->external_pending |= mp_filter_graph_run(r->root_filter);
+
+ assert(r->recursive == p);
+ r->recursive = NULL;
+}
+
+void mp_filter_internal_mark_progress(struct mp_filter *f)
+{
+ struct filter_runner *r = f->in->runner;
+ assert(r->filtering); // only call from f's process()
+ add_pending(f);
+}
+
+// Basically copy the async notifications to the sync ones. Done so that the
+// sync notifications don't need any locking.
+static void flush_async_notifications(struct filter_runner *r)
+{
+ mp_mutex_lock(&r->async_lock);
+ for (int n = 0; n < r->num_async_pending; n++) {
+ struct mp_filter *f = r->async_pending[n];
+ add_pending(f);
+ f->in->async_pending = false;
+ }
+ r->num_async_pending = 0;
+ r->async_wakeup_sent = false;
+ mp_mutex_unlock(&r->async_lock);
+}
+
+bool mp_filter_graph_run(struct mp_filter *filter)
+{
+ struct filter_runner *r = filter->in->runner;
+ assert(filter == r->root_filter); // user is supposed to call this on root only
+
+ int64_t end_time = 0;
+ if (isfinite(r->max_run_time))
+ end_time = mp_time_ns_add(mp_time_ns(), MPMAX(r->max_run_time, 0));
+
+ // (could happen with separate filter graphs calling each other, for now
+ // ignore this issue as we don't use such a setup anywhere)
+ assert(!r->filtering);
+
+ r->filtering = true;
+
+ flush_async_notifications(r);
+
+ bool exit_req = false;
+
+ while (1) {
+ if (atomic_exchange_explicit(&r->interrupt_flag, false,
+ memory_order_acq_rel))
+ {
+ mp_mutex_lock(&r->async_lock);
+ if (!r->async_wakeup_sent && r->wakeup_cb)
+ r->wakeup_cb(r->wakeup_ctx);
+ r->async_wakeup_sent = true;
+ mp_mutex_unlock(&r->async_lock);
+ exit_req = true;
+ }
+
+ if (!r->num_pending) {
+ flush_async_notifications(r);
+ if (!r->num_pending)
+ break;
+ }
+
+ struct mp_filter *next = NULL;
+
+ if (r->pending[0]->in->high_priority) {
+ next = r->pending[0];
+ MP_TARRAY_REMOVE_AT(r->pending, r->num_pending, 0);
+ } else if (!exit_req) {
+ next = r->pending[r->num_pending - 1];
+ r->num_pending -= 1;
+ }
+
+ if (!next)
+ break;
+
+ next->in->pending = false;
+ if (next->in->info->process)
+ next->in->info->process(next);
+
+ if (end_time && mp_time_ns() >= end_time)
+ mp_filter_graph_interrupt(r->root_filter);
+ }
+
+ r->filtering = false;
+
+ bool externals = r->external_pending;
+ r->external_pending = false;
+ return externals;
+}
+
+bool mp_pin_can_transfer_data(struct mp_pin *dst, struct mp_pin *src)
+{
+ return mp_pin_in_needs_data(dst) && mp_pin_out_request_data(src);
+}
+
+bool mp_pin_transfer_data(struct mp_pin *dst, struct mp_pin *src)
+{
+ if (!mp_pin_can_transfer_data(dst, src))
+ return false;
+ mp_pin_in_write(dst, mp_pin_out_read(src));
+ return true;
+}
+
+bool mp_pin_in_needs_data(struct mp_pin *p)
+{
+ assert(p->dir == MP_PIN_IN);
+ assert(!p->within_conn);
+ return p->conn && p->conn->manual_connection && p->conn->data_requested;
+}
+
+bool mp_pin_in_write(struct mp_pin *p, struct mp_frame frame)
+{
+ if (!mp_pin_in_needs_data(p) || frame.type == MP_FRAME_NONE) {
+ if (frame.type)
+ MP_ERR(p->owner, "losing frame on %s\n", p->name);
+ mp_frame_unref(&frame);
+ return false;
+ }
+ assert(p->conn->data.type == MP_FRAME_NONE);
+ p->conn->data = frame;
+ p->conn->data_requested = false;
+ add_pending_pin(p->conn);
+ filter_recursive(p);
+ return true;
+}
+
+bool mp_pin_out_has_data(struct mp_pin *p)
+{
+ assert(p->dir == MP_PIN_OUT);
+ assert(!p->within_conn);
+ return p->conn && p->conn->manual_connection && p->data.type != MP_FRAME_NONE;
+}
+
+bool mp_pin_out_request_data(struct mp_pin *p)
+{
+ if (mp_pin_out_has_data(p))
+ return true;
+ if (p->conn && p->conn->manual_connection) {
+ if (!p->data_requested) {
+ p->data_requested = true;
+ add_pending_pin(p->conn);
+ }
+ filter_recursive(p);
+ }
+ return mp_pin_out_has_data(p);
+}
+
+void mp_pin_out_request_data_next(struct mp_pin *p)
+{
+ if (mp_pin_out_request_data(p))
+ add_pending_pin(p->conn);
+}
+
+struct mp_frame mp_pin_out_read(struct mp_pin *p)
+{
+ if (!mp_pin_out_request_data(p))
+ return MP_NO_FRAME;
+ struct mp_frame res = p->data;
+ p->data = MP_NO_FRAME;
+ return res;
+}
+
+void mp_pin_out_unread(struct mp_pin *p, struct mp_frame frame)
+{
+ assert(p->dir == MP_PIN_OUT);
+ assert(!p->within_conn);
+ assert(p->conn && p->conn->manual_connection);
+ // Unread is allowed strictly only if you didn't do anything else with
+ // the pin since the time you read it.
+ assert(!mp_pin_out_has_data(p));
+ assert(!p->data_requested);
+ p->data = frame;
+}
+
+void mp_pin_out_repeat_eof(struct mp_pin *p)
+{
+ mp_pin_out_unread(p, MP_EOF_FRAME);
+}
+
+// Follow mp_pin pairs/connection into the "other" direction of the pin, until
+// the last pin is found. (In the simplest case, this is just p->other.) E.g.:
+// <1a|1b> <-> <2a|2b> <-> <3a|3b>
+// find_connected_end(2b)==1a
+// find_connected_end(1b)==1a
+// find_connected_end(1a)==3b
+static struct mp_pin *find_connected_end(struct mp_pin *p)
+{
+ while (1) {
+ struct mp_pin *other = p->other;
+ if (!other->user_conn)
+ return other;
+ p = other->user_conn;
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+// With p being part of a connection, create the pin_connection and set all
+// state flags.
+static void init_connection(struct mp_pin *p)
+{
+ struct filter_runner *runner = p->owner->in->runner;
+
+ if (p->dir == MP_PIN_IN)
+ p = p->other;
+
+ struct mp_pin *in = find_connected_end(p);
+ struct mp_pin *out = find_connected_end(p->other);
+
+ // These are the "outer" pins by definition, they have no user connections.
+ assert(!in->user_conn);
+ assert(!out->user_conn);
+
+ // This and similar checks enforce the same root filter requirement.
+ if (in->manual_connection)
+ assert(in->manual_connection->in->runner == runner);
+ if (out->manual_connection)
+ assert(out->manual_connection->in->runner == runner);
+
+ // Logically, the ends are always manual connections. A pin chain without
+ // manual connections at the ends is still disconnected (or if this
+ // attempted to extend an existing connection, becomes dangling and gets
+ // disconnected).
+ if (!in->manual_connection || !out->manual_connection)
+ return;
+
+ assert(in->dir == MP_PIN_IN);
+ assert(out->dir == MP_PIN_OUT);
+
+ struct mp_pin *cur = in;
+ while (cur) {
+ assert(!cur->within_conn && !cur->other->within_conn);
+ assert(!cur->conn && !cur->other->conn);
+ assert(!cur->data_requested); // unused for in pins
+ assert(!cur->data.type); // unused for in pins
+ assert(!cur->other->data_requested); // unset for unconnected out pins
+ assert(!cur->other->data.type); // unset for unconnected out pins
+ assert(cur->owner->in->runner == runner);
+ cur->within_conn = cur->other->within_conn = true;
+ cur = cur->other->user_conn;
+ }
+
+ in->conn = out;
+ in->within_conn = false;
+ out->conn = in;
+ out->within_conn = false;
+
+ // Scheduling so far will be messed up.
+ add_pending(in->manual_connection);
+ add_pending(out->manual_connection);
+}
+
+void mp_pin_connect(struct mp_pin *dst, struct mp_pin *src)
+{
+ assert(src->dir == MP_PIN_OUT);
+ assert(dst->dir == MP_PIN_IN);
+
+ if (dst->user_conn == src) {
+ assert(src->user_conn == dst);
+ return;
+ }
+
+ mp_pin_disconnect(src);
+ mp_pin_disconnect(dst);
+
+ src->user_conn = dst;
+ dst->user_conn = src;
+
+ init_connection(src);
+}
+
+void mp_pin_set_manual_connection(struct mp_pin *p, bool connected)
+{
+ mp_pin_set_manual_connection_for(p, connected ? p->owner->in->parent : NULL);
+}
+
+void mp_pin_set_manual_connection_for(struct mp_pin *p, struct mp_filter *f)
+{
+ if (p->manual_connection == f)
+ return;
+ if (p->within_conn)
+ mp_pin_disconnect(p);
+ p->manual_connection = f;
+ init_connection(p);
+}
+
+struct mp_filter *mp_pin_get_manual_connection(struct mp_pin *p)
+{
+ return p->manual_connection;
+}
+
+static void deinit_connection(struct mp_pin *p)
+{
+ if (p->dir == MP_PIN_OUT)
+ p = p->other;
+
+ p = find_connected_end(p);
+
+ while (p) {
+ p->conn = p->other->conn = NULL;
+ p->within_conn = p->other->within_conn = false;
+ assert(!p->other->data_requested); // unused for in pins
+ assert(!p->other->data.type); // unused for in pins
+ p->data_requested = false;
+ if (p->data.type)
+ MP_VERBOSE(p->owner, "dropping frame due to pin disconnect\n");
+ if (p->data_requested)
+ MP_VERBOSE(p->owner, "dropping request due to pin disconnect\n");
+ mp_frame_unref(&p->data);
+ p = p->other->user_conn;
+ }
+}
+
+void mp_pin_disconnect(struct mp_pin *p)
+{
+ if (!mp_pin_is_connected(p))
+ return;
+
+ p->manual_connection = NULL;
+
+ struct mp_pin *conn = p->user_conn;
+ if (conn) {
+ p->user_conn = NULL;
+ conn->user_conn = NULL;
+ deinit_connection(conn);
+ }
+
+ deinit_connection(p);
+}
+
+bool mp_pin_is_connected(struct mp_pin *p)
+{
+ return p->user_conn || p->manual_connection;
+}
+
+const char *mp_pin_get_name(struct mp_pin *p)
+{
+ return p->name;
+}
+
+enum mp_pin_dir mp_pin_get_dir(struct mp_pin *p)
+{
+ return p->dir;
+}
+
+const char *mp_filter_get_name(struct mp_filter *f)
+{
+ return f->in->name;
+}
+
+const struct mp_filter_info *mp_filter_get_info(struct mp_filter *f)
+{
+ return f->in->info;
+}
+
+void mp_filter_set_high_priority(struct mp_filter *f, bool pri)
+{
+ f->in->high_priority = pri;
+}
+
+void mp_filter_set_name(struct mp_filter *f, const char *name)
+{
+ talloc_free(f->in->name);
+ f->in->name = talloc_strdup(f, name);
+}
+
+struct mp_pin *mp_filter_get_named_pin(struct mp_filter *f, const char *name)
+{
+ for (int n = 0; n < f->num_pins; n++) {
+ if (name && strcmp(f->pins[n]->name, name) == 0)
+ return f->pins[n];
+ }
+ return NULL;
+}
+
+void mp_filter_set_error_handler(struct mp_filter *f, struct mp_filter *handler)
+{
+ f->in->error_handler = handler;
+}
+
+void mp_filter_internal_mark_failed(struct mp_filter *f)
+{
+ while (f) {
+ f->in->failed = true;
+ if (f->in->error_handler) {
+ add_pending(f->in->error_handler);
+ break;
+ }
+ f = f->in->parent;
+ }
+}
+
+bool mp_filter_has_failed(struct mp_filter *filter)
+{
+ bool failed = filter->in->failed;
+ filter->in->failed = false;
+ return failed;
+}
+
+static void reset_pin(struct mp_pin *p)
+{
+ if (!p->conn || p->dir != MP_PIN_OUT) {
+ assert(!p->data.type);
+ assert(!p->data_requested);
+ }
+ mp_frame_unref(&p->data);
+ p->data_requested = false;
+}
+
+void mp_filter_reset(struct mp_filter *filter)
+{
+ for (int n = 0; n < filter->in->num_children; n++)
+ mp_filter_reset(filter->in->children[n]);
+
+ for (int n = 0; n < filter->num_pins; n++) {
+ struct mp_pin *p = filter->ppins[n];
+ reset_pin(p);
+ reset_pin(p->other);
+ }
+
+ if (filter->in->info->reset)
+ filter->in->info->reset(filter);
+}
+
+struct mp_pin *mp_filter_add_pin(struct mp_filter *f, enum mp_pin_dir dir,
+ const char *name)
+{
+ assert(dir == MP_PIN_IN || dir == MP_PIN_OUT);
+ assert(name && name[0]);
+ assert(!mp_filter_get_named_pin(f, name));
+
+ // "Public" pin
+ struct mp_pin *p = talloc_ptrtype(NULL, p);
+ *p = (struct mp_pin){
+ .name = talloc_strdup(p, name),
+ .dir = dir,
+ .owner = f,
+ .manual_connection = f->in->parent,
+ };
+
+ // "Private" paired pin
+ p->other = talloc_ptrtype(NULL, p);
+ *p->other = (struct mp_pin){
+ .name = p->name,
+ .dir = p->dir == MP_PIN_IN ? MP_PIN_OUT : MP_PIN_IN,
+ .owner = f,
+ .other = p,
+ .manual_connection = f,
+ };
+
+ MP_TARRAY_GROW(f, f->pins, f->num_pins);
+ MP_TARRAY_GROW(f, f->ppins, f->num_pins);
+ f->pins[f->num_pins] = p;
+ f->ppins[f->num_pins] = p->other;
+ f->num_pins += 1;
+
+ init_connection(p);
+
+ return p->other;
+}
+
+void mp_filter_remove_pin(struct mp_filter *f, struct mp_pin *p)
+{
+ if (!p)
+ return;
+
+ assert(p->owner == f);
+ mp_pin_disconnect(p);
+ mp_pin_disconnect(p->other);
+
+ int index = -1;
+ for (int n = 0; n < f->num_pins; n++) {
+ if (f->ppins[n] == p) {
+ index = n;
+ break;
+ }
+ }
+ assert(index >= 0);
+
+ talloc_free(f->pins[index]);
+ talloc_free(f->ppins[index]);
+
+ int count = f->num_pins;
+ MP_TARRAY_REMOVE_AT(f->pins, count, index);
+ count = f->num_pins;
+ MP_TARRAY_REMOVE_AT(f->ppins, count, index);
+ f->num_pins -= 1;
+}
+
+bool mp_filter_command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ return f->in->info->command ? f->in->info->command(f, cmd) : false;
+}
+
+struct mp_stream_info *mp_filter_find_stream_info(struct mp_filter *f)
+{
+ while (f) {
+ if (f->stream_info)
+ return f->stream_info;
+ f = f->in->parent;
+ }
+ return NULL;
+}
+
+struct mp_hwdec_ctx *mp_filter_load_hwdec_device(struct mp_filter *f, int imgfmt)
+{
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ if (!info || !info->hwdec_devs)
+ return NULL;
+
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = imgfmt,
+ .probing = false,
+ };
+ hwdec_devices_request_for_img_fmt(info->hwdec_devs, &params);
+
+ return hwdec_devices_get_by_imgfmt(info->hwdec_devs, imgfmt);
+}
+
+static void filter_wakeup(struct mp_filter *f, bool mark_only)
+{
+ struct filter_runner *r = f->in->runner;
+ mp_mutex_lock(&r->async_lock);
+ if (!f->in->async_pending) {
+ f->in->async_pending = true;
+ // (not using a talloc parent for thread safety reasons)
+ MP_TARRAY_APPEND(NULL, r->async_pending, r->num_async_pending, f);
+ }
+ if (!mark_only && !r->async_wakeup_sent) {
+ if (r->wakeup_cb)
+ r->wakeup_cb(r->wakeup_ctx);
+ r->async_wakeup_sent = true;
+ }
+ mp_mutex_unlock(&r->async_lock);
+}
+
+void mp_filter_wakeup(struct mp_filter *f)
+{
+ filter_wakeup(f, false);
+}
+
+void mp_filter_mark_async_progress(struct mp_filter *f)
+{
+ filter_wakeup(f, true);
+}
+
+void mp_filter_graph_set_max_run_time(struct mp_filter *f, double seconds)
+{
+ struct filter_runner *r = f->in->runner;
+ assert(f == r->root_filter); // user is supposed to call this on root only
+ r->max_run_time = seconds;
+}
+
+void mp_filter_graph_interrupt(struct mp_filter *f)
+{
+ struct filter_runner *r = f->in->runner;
+ assert(f == r->root_filter); // user is supposed to call this on root only
+ atomic_store(&r->interrupt_flag, true);
+}
+
+void mp_filter_free_children(struct mp_filter *f)
+{
+ while(f->in->num_children)
+ talloc_free(f->in->children[0]);
+}
+
+static void filter_destructor(void *p)
+{
+ struct mp_filter *f = p;
+ struct filter_runner *r = f->in->runner;
+
+ if (f->in->info->destroy)
+ f->in->info->destroy(f);
+
+ // For convenience, free child filters.
+ mp_filter_free_children(f);
+
+ while (f->num_pins)
+ mp_filter_remove_pin(f, f->ppins[0]);
+
+ // Just make sure the filter is not still in the async notifications set.
+ // There will be no more new notifications at this point (due to destroy()).
+ flush_async_notifications(r);
+
+ for (int n = 0; n < r->num_pending; n++) {
+ if (r->pending[n] == f) {
+ MP_TARRAY_REMOVE_AT(r->pending, r->num_pending, n);
+ break;
+ }
+ }
+
+ if (f->in->parent) {
+ struct mp_filter_internal *p_in = f->in->parent->in;
+ for (int n = 0; n < p_in->num_children; n++) {
+ if (p_in->children[n] == f) {
+ MP_TARRAY_REMOVE_AT(p_in->children, p_in->num_children, n);
+ break;
+ }
+ }
+ }
+
+ if (r->root_filter == f) {
+ assert(!f->in->parent);
+ mp_mutex_destroy(&r->async_lock);
+ talloc_free(r->async_pending);
+ talloc_free(r);
+ }
+}
+
+
+struct mp_filter *mp_filter_create_with_params(struct mp_filter_params *params)
+{
+ struct mp_filter *f = talloc(NULL, struct mp_filter);
+ talloc_set_destructor(f, filter_destructor);
+ *f = (struct mp_filter){
+ .priv = params->info->priv_size ?
+ talloc_zero_size(f, params->info->priv_size) : NULL,
+ .global = params->global,
+ .in = talloc(f, struct mp_filter_internal),
+ };
+ *f->in = (struct mp_filter_internal){
+ .info = params->info,
+ .parent = params->parent,
+ .runner = params->parent ? params->parent->in->runner : NULL,
+ };
+
+ if (!f->in->runner) {
+ assert(params->global);
+
+ f->in->runner = talloc(NULL, struct filter_runner);
+ *f->in->runner = (struct filter_runner){
+ .global = params->global,
+ .root_filter = f,
+ .max_run_time = INFINITY,
+ };
+ mp_mutex_init(&f->in->runner->async_lock);
+ }
+
+ if (!f->global)
+ f->global = f->in->runner->global;
+
+ if (f->in->parent) {
+ struct mp_filter_internal *parent = f->in->parent->in;
+ MP_TARRAY_APPEND(parent, parent->children, parent->num_children, f);
+ f->log = mp_log_new(f, f->global->log, params->info->name);
+ } else {
+ f->log = mp_log_new(f, f->global->log, "!root");
+ }
+
+ if (f->in->info->init) {
+ if (!f->in->info->init(f, params)) {
+ talloc_free(f);
+ return NULL;
+ }
+ }
+
+ return f;
+}
+
+struct mp_filter *mp_filter_create(struct mp_filter *parent,
+ const struct mp_filter_info *info)
+{
+ assert(parent);
+ assert(info);
+ struct mp_filter_params params = {
+ .info = info,
+ .parent = parent,
+ };
+ return mp_filter_create_with_params(&params);
+}
+
+// (the root filter is just a dummy filter - nothing special about it, except
+// that it has no parent, and serves as manual connection for "external" pins)
+static const struct mp_filter_info filter_root = {
+ .name = "root",
+};
+
+struct mp_filter *mp_filter_create_root(struct mpv_global *global)
+{
+ struct mp_filter_params params = {
+ .info = &filter_root,
+ .global = global,
+ };
+ return mp_filter_create_with_params(&params);
+}
+
+void mp_filter_graph_set_wakeup_cb(struct mp_filter *root,
+ void (*wakeup_cb)(void *ctx), void *ctx)
+{
+ struct filter_runner *r = root->in->runner;
+ assert(root == r->root_filter); // user is supposed to call this on root only
+ mp_mutex_lock(&r->async_lock);
+ r->wakeup_cb = wakeup_cb;
+ r->wakeup_ctx = ctx;
+ mp_mutex_unlock(&r->async_lock);
+}
+
+static const char *filt_name(struct mp_filter *f)
+{
+ return f ? f->in->info->name : "-";
+}
+
+static void dump_pin_state(struct mp_filter *f, struct mp_pin *pin)
+{
+ MP_WARN(f, " [%p] %s %s c=%s[%p] f=%s[%p] m=%s[%p] %s %s %s\n",
+ pin, pin->name, pin->dir == MP_PIN_IN ? "->" : "<-",
+ pin->user_conn ? filt_name(pin->user_conn->owner) : "-", pin->user_conn,
+ pin->conn ? filt_name(pin->conn->owner) : "-", pin->conn,
+ filt_name(pin->manual_connection), pin->manual_connection,
+ pin->within_conn ? "(within)" : "",
+ pin->data_requested ? "(request)" : "",
+ mp_frame_type_str(pin->data.type));
+}
+
+void mp_filter_dump_states(struct mp_filter *f)
+{
+ MP_WARN(f, "%s[%p] (%s[%p])\n", filt_name(f), f,
+ filt_name(f->in->parent), f->in->parent);
+ for (int n = 0; n < f->num_pins; n++) {
+ dump_pin_state(f, f->pins[n]);
+ dump_pin_state(f, f->ppins[n]);
+ }
+
+ for (int n = 0; n < f->in->num_children; n++)
+ mp_filter_dump_states(f->in->children[n]);
+}
diff --git a/filters/filter.h b/filters/filter.h
new file mode 100644
index 0000000..44d5f59
--- /dev/null
+++ b/filters/filter.h
@@ -0,0 +1,470 @@
+#pragma once
+
+#include <stdbool.h>
+
+#include "frame.h"
+
+struct mpv_global;
+struct mp_filter;
+
+// A filter input or output. These always come in pairs: one mp_pin is for
+// input, the other is for output. (The separation is mostly for checking
+// their API use, and for the connection functions.)
+// Effectively, this is a 1-frame queue. The data flow rules have the goal to
+// reduce the number of buffered frames and the amount of time they are
+// buffered.
+// A mp_pin must be connected to be usable. The default state of a mp_pin is
+// a manual connection, which means you use the mp_pin_*() functions to
+// manually read or write data.
+struct mp_pin;
+
+enum mp_pin_dir {
+ MP_PIN_INVALID = 0, // used as a placeholder value
+ MP_PIN_IN, // you write data to the pin
+ MP_PIN_OUT, // you read data from the pin
+};
+
+// The established direction for this pin. The direction of a pin is immutable.
+// You must use the mp_pin_in_*() and mp_pin_out_*() functions on the correct
+// pin type - mismatching it is an API violation.
+enum mp_pin_dir mp_pin_get_dir(struct mp_pin *p);
+
+// True if a new frame should be written to the pin.
+bool mp_pin_in_needs_data(struct mp_pin *p);
+
+// Write a frame to the pin. If the input was not accepted, false is returned
+// (does not normally happen, as long as mp_pin_in_needs_data() returned true).
+// The callee owns the reference to the frame data, even on failure.
+// Writing a MP_FRAME_NONE has no effect (and returns false).
+// If you did not call mp_pin_in_needs_data() before this, it's likely a bug.
+bool mp_pin_in_write(struct mp_pin *p, struct mp_frame frame);
+
+// True if a frame is actually available for reading right now, and
+// mp_pin_out_read() will return success. If this returns false, the pin is
+// flagged for needing data (the filter might either produce output the next
+// time it's run, or request new input).
+// You should call this only if you can immediately consume the data. The goal
+// is to have no redundant buffering in the filter graph, and leaving frames
+// buffered in mp_pins goes against this.
+bool mp_pin_out_request_data(struct mp_pin *p);
+
+// Same as mp_pin_out_request_data(), but call the filter's process() function
+// next time even if there is new data. the intention is that the filter reads
+// the data in the next iteration, without checking for the data now.
+void mp_pin_out_request_data_next(struct mp_pin *p);
+
+// Same as mp_pin_out_request_data(), but does not attempt to procure new frames
+// if the return value is false.
+bool mp_pin_out_has_data(struct mp_pin *p);
+
+// Read a frame. Returns MP_FRAME_NONE if currently no frame is available.
+// You need to call mp_pin_out_request_data() and wait until the frame is ready
+// to be sure this returns a frame. (This call implicitly calls _request if no
+// frame is available, but to get proper data flow in filters, you should
+// probably follow the preferred conventions.)
+// If no frame is returned, a frame is automatically requested via
+// mp_pin_out_request_data() (so it might be returned in the future).
+// If a frame is returned, no new frame is automatically requested (this is
+// usually not wanted, because it could lead to additional buffering).
+// This is guaranteed to return a non-NONE frame if mp_pin_out_has_data()
+// returned true and no other filter functions were called.
+// The caller owns the reference to the returned data.
+struct mp_frame mp_pin_out_read(struct mp_pin *p);
+
+// Undo mp_pin_out_read(). This should be only used in special cases. Normally,
+// you should make an effort to reduce buffering, which means you signal that
+// you need a frame only once you know that you can use it (meaning you'll
+// really use it and have no need to "undo" the read). But in special cases,
+// especially if the behavior depends on the exact frame data, using this might
+// be justified.
+// If this is called, the next mp_pin_out_read() call will return the same frame
+// again. You must not have called mp_pin_out_request_data() on this pin and
+// you must not have disconnected or changed the pin in any way.
+// This does not mark the filter for progress, i.e. the filter's process()
+// function won't be repeated (unless other pins change). If you really need
+// that, call mp_filter_internal_mark_progress() manually in addition.
+void mp_pin_out_unread(struct mp_pin *p, struct mp_frame frame);
+
+// A helper to make draining on MP_FRAME_EOF frames easier. For filters which
+// buffer data, but have no easy way to buffer MP_FRAME_EOF frames natively.
+// This is to be used as follows:
+// 1. caller receives MP_FRAME_EOF
+// 2. initiates draining (or continues, see step 4.)
+// 2b. if there are no more buffered frames, just propagates the EOF frame and
+// exits
+// 3. calls mp_pin_out_repeat_eof(pin)
+// 4. returns a buffered frame normally, and continues normally
+// 4b. pin returns "repeated" MP_FRAME_EOF, jump to 1.
+// 5. if there's nothing more to do, stop
+// 5b. there might be a sporadic wakeup, and an unwanted wait for output (in
+// a typical filter implementation)
+// You must not have requested data before calling this. (Usually you'd call
+// this after mp_pin_out_read(). Requesting data after queuing the repeat EOF
+// is OK and idempotent.)
+// This is equivalent to mp_pin_out_unread(p, MP_EOF_FRAME). See that function
+// for further remarks.
+void mp_pin_out_repeat_eof(struct mp_pin *p);
+
+// Trivial helper to determine whether src is readable and dst is writable right
+// now. Defers or requests new data if not ready. This means it has the side
+// effect of telling the filters that you want to transfer data.
+// You use this in a filter process() function. If the result is false, it will
+// have requested new output from src, and your process() function will be
+// called again once src has output and dst is accepts input (the latest).
+bool mp_pin_can_transfer_data(struct mp_pin *dst, struct mp_pin *src);
+
+// Trivial helper to copy data between two manual pins. This uses filter data
+// flow - so if data can't be copied, it requests the pins to make it possible
+// on the next filter run. This implies you call this either from a filter
+// process() function, or call it manually when needed. Also see
+// mp_pin_can_transfer_data(). Returns whether a transfer happened.
+bool mp_pin_transfer_data(struct mp_pin *dst, struct mp_pin *src);
+
+// Connect src and dst, for automatic data flow. Pin src will reflect the request
+// state of pin dst, and accept and pass down frames to dst when appropriate.
+// src must be MP_PIN_OUT, dst must be MP_PIN_IN.
+// Previous connections are always removed. If the pins were already connected,
+// no action is taken.
+// Creating circular connections will just cause infinite recursion or such.
+// Both API user and filter implementations can use this, but always only on
+// the pins they're allowed to access.
+void mp_pin_connect(struct mp_pin *dst, struct mp_pin *src);
+
+// Enable manual filter access. This means you want to directly use the
+// mp_pin_in*() and mp_pin_out_*() functions for data flow.
+// Always severs previous connections.
+void mp_pin_set_manual_connection(struct mp_pin *p, bool connected);
+
+// Enable manual filter access, like mp_pin_set_manual_connection(). In
+// addition, this specifies which filter's process function should be invoked
+// on pin state changes. Using mp_pin_set_manual_connection() will default to
+// the parent filter for this.
+// Passing f=NULL disconnects.
+void mp_pin_set_manual_connection_for(struct mp_pin *p, struct mp_filter *f);
+
+// Return the manual connection for this pin, or NULL if none.
+struct mp_filter *mp_pin_get_manual_connection(struct mp_pin *p);
+
+// Disconnect the pin, possibly breaking connections.
+void mp_pin_disconnect(struct mp_pin *p);
+
+// Return whether a connection was set on this pin. Note that this is not
+// transitive (if the pin is connected to an pin with no further connections,
+// there is no active connection, but this still returns true).
+bool mp_pin_is_connected(struct mp_pin *p);
+
+// Return a symbolic name of the pin. Usually it will be something redundant
+// (like "in" or "out"), or something the user set.
+// The returned pointer is valid as long as the mp_pin is allocated.
+const char *mp_pin_get_name(struct mp_pin *p);
+
+/**
+ * A filter converts input frames to output frames (mp_frame, usually audio or
+ * video data). It can support multiple inputs and outputs. Data always flows
+ * through mp_pin instances.
+ *
+ * --- General rules for data flow:
+ *
+ * All data goes through mp_pin (present in the mp_filter inputs/outputs list).
+ * Actual work is done in the filter's process() function. This function
+ * queries whether input mp_pins have data and output mp_pins require data. If
+ * both is the case, a frame is read, filtered, and written to the output.
+ * Depending on the filter type, the filter might internally buffer data (e.g.
+ * things that require readahead). But in general, a filter should not request
+ * input before output is needed.
+ *
+ * The general goal is to reduce the amount of data buffered. This is why
+ * mp_pins buffer at most 1 frame, and the API is designed such that queued
+ * data in pins will be immediately passed to the next filter. If buffering is
+ * actually desired, explicit filters for buffering have to be introduced into
+ * the filter chain.
+ *
+ * Typically a filter will do something like this:
+ *
+ * process(struct mp_filter *f) {
+ * if (!mp_pin_in_needs_data(f->ppins[1]))
+ * return; // reader needs no output yet, so stop filtering
+ * if (!have_enough_data_for_output) {
+ * // Could check mp_pin_out_request_data(), but often just trying to
+ * // read is enough, as a failed read will request more data.
+ * struct mp_frame fr = mp_pin_out_read_data(f->ppins[0]);
+ * if (!fr.type)
+ * return; // no frame was returned - data was requested, and will
+ * // be queued when available, and invoke process() again
+ * ... do something with fr here ...
+ * }
+ * ... produce output frame (i.e. actual filtering) ...
+ * mp_pin_in_write(f->ppins[1], output_frame);
+ * }
+ *
+ * Simpler filters can use utility functions like mp_pin_can_transfer_data(),
+ * which reduce the boilerplate. Such filters also may not need to buffer data
+ * as internal state.
+ *
+ * --- Driving filters:
+ *
+ * The filter root (created by mp_filter_create_root()) will internally create
+ * a graph runner, that can be entered with mp_filter_graph_run(). This will
+ * check if any filter/pin has unhandled requests, and call filter process()
+ * functions accordingly. Outside of the filter, this can be triggered
+ * implicitly via the mp_pin_* functions.
+ *
+ * Multiple filters are driven by letting mp_pin flag filters which need
+ * process() to be called. The process starts by requesting output from the
+ * last filter. The requests will "bubble up" by iteratively calling process()
+ * on each filter, which will request further input, until input on the first
+ * filter's input pin is requested. The API user feeds it a frame, which will
+ * call the first filter's process() function, which will filter and output
+ * the frame, and the frame is iteratively filtered until it reaches the output.
+ *
+ * --- General rules for thread safety:
+ *
+ * Filters are by default not thread safe. However, some filters can be
+ * partially thread safe and allow certain functions to be accessed from
+ * foreign threads. The common filter code itself is not thread safe, except
+ * for some utility functions explicitly marked as such, and which are meant
+ * to make implementing threaded filters easier.
+ *
+ * (Semi-)automatic filter communication such as pins must always be within the
+ * same root filter. This is meant to help with ensuring thread-safety. Every
+ * thread that wants to run filters "on its own" should use a different filter
+ * graph, and disallowing different root filters ensures these graphs are not
+ * accidentally connected using non-thread safe mechanisms. Actual threaded
+ * filter graphs would use several independent graphs connected by asynchronous
+ * helpers (such as mp_async_queue instead of mp_pin connections).
+ *
+ * --- Rules for manual connections:
+ *
+ * A pin can be marked for manual connection via mp_pin_set_manual_connection().
+ * It's also the default. These have two uses:
+ *
+ * 1. filter internal (the filter actually does something with a frame)
+ * 2. filter user manually feeding/retrieving frames
+ *
+ * Basically, a manual connection means someone uses the mp_pin_in_*() or
+ * mp_pin_out_*() functions on a pin. The alternative is an automatic connection
+ * made via mp_pin_connect(). Manual connections need special considerations
+ * for wakeups:
+ *
+ * Internal manual pins (within a filter) will invoke the filter's process()
+ * function, and the filter polls the state of all pins to see if anything
+ * needs to be filtered or requested.
+ *
+ * External manual pins (filter user) require the user to poll all manual pins
+ * that are part of the graph. In addition, the filter's wakeup callback must be
+ * set, and trigger repolling all pins. This is needed in case any filters do
+ * async filtering internally.
+ *
+ * --- Rules for filters with multiple inputs or outputs:
+ *
+ * The generic filter code does not do any kind of scheduling. It's the filter's
+ * responsibility to request frames from input when needed, and to avoid
+ * internal excessive buffering if outputs aren't read.
+ *
+ * --- Rules for async filters:
+ *
+ * Async filters will have a synchronous interface with asynchronous waiting.
+ * They change mp_pin data flow to being poll based, with a wakeup mechanism to
+ * avoid active waiting. Once polling results in no change, the API user can go
+ * to sleep, and wait until the wakeup callback set via mp_filter_create_root()
+ * is invoked. Then it can poll the filters again. Internally, filters use
+ * mp_filter_wakeup() to get their process() function invoked on the user
+ * thread, and update the mp_pin states.
+ *
+ * For running parts of a filter graph on a different thread, f_async_queue.h
+ * can be used.
+ *
+ * With different filter graphs working asynchronously, reset handling and start
+ * of filtering becomes more difficult. Since filtering is always triggered by
+ * requesting output from a filter, a simple way to solve this is to trigger
+ * resets from the consumer, and to synchronously reset the producer.
+ *
+ * --- Format conversions and mid-stream format changes:
+ *
+ * Generally, all filters must support all formats, as well as mid-stream
+ * format changes. If they don't, they will have to error out. There are some
+ * helpers for dealing with these two things.
+ *
+ * mp_pin_out_unread() can temporarily put back an input frame. If the input
+ * format changed, and you have to drain buffered data, you can put back the
+ * frame every time you output a buffered frame. Once all buffered data is
+ * drained this way, you can actually change the internal filter state to the
+ * new format, and actually consume the input frame.
+ *
+ * There is an f_autoconvert filter, which lets you transparently convert to
+ * a set of target formats (and which passes through the data if no conversion
+ * is needed).
+ *
+ * --- Rules for format negotiation:
+ *
+ * Since libavfilter does not provide _any_ kind of format negotiation to the
+ * user, and most filters use the libavfilter wrapper anyway, this is pretty
+ * broken and rudimentary. (The only thing libavfilter provides is that you
+ * can try to create a filter with a specific input format. Then you get
+ * either failure, or an output format. It involves actually initializing all
+ * filters, so a try run is not cheap or even side effect free.)
+ */
+struct mp_filter {
+ // Private state for the filter implementation. API users must not access
+ // this.
+ void *priv;
+
+ struct mpv_global *global;
+ struct mp_log *log;
+
+ // Array of public pins. API users can read this, but are not allowed to
+ // modify the array. Filter implementations use mp_filter_add_pin() to add
+ // pins to the array. The array is in order of the add calls.
+ // Most filters will use pins[0] for input (MP_PIN_IN), and pins[1] for
+ // output (MP_PIN_OUT). This is the default convention for filters. Some
+ // filters may have more complex usage, and assign pin entries with
+ // different meanings.
+ // The filter implementation must not use this. It must access ppins[]
+ // instead.
+ struct mp_pin **pins;
+ int num_pins;
+
+ // Internal pins, for access by the filter implementation. The meaning of
+ // in/out is swapped from the public interface: inputs use MP_PIN_OUT,
+ // because the filter reads from the inputs, and outputs use MP_PIN_IN,
+ // because the filter writes to them. ppins[n] always corresponds to pin[n],
+ // with swapped direction, and implicit data flow between the two.
+ // Outside API users must not access this.
+ struct mp_pin **ppins;
+
+ // Dumb garbage.
+ struct mp_stream_info *stream_info;
+
+ // Private state for the generic filter code.
+ struct mp_filter_internal *in;
+};
+
+// Return a symbolic name, which is set at init time. NULL if no name.
+// Valid until filter is destroyed or next mp_filter_set_name() call.
+const char *mp_filter_get_name(struct mp_filter *f);
+
+// Change mp_filter_get_name() return value.
+void mp_filter_set_name(struct mp_filter *f, const char *name);
+
+// Set filter priority. A higher priority gets processed first. Also, high
+// priority filters disable "interrupting" the filter graph.
+void mp_filter_set_high_priority(struct mp_filter *filter, bool pri);
+
+// Get a pin from f->pins[] for which mp_pin_get_name() returns the same name.
+// If name is NULL, always return NULL.
+struct mp_pin *mp_filter_get_named_pin(struct mp_filter *f, const char *name);
+
+// Return true if the filter has failed in some fatal way that does not allow
+// it to continue. This resets the error state (but does not reset the child
+// failed status on any parent filter).
+bool mp_filter_has_failed(struct mp_filter *filter);
+
+// Invoke mp_filter_info.reset on this filter and all children (but not
+// other filters connected via pins).
+void mp_filter_reset(struct mp_filter *filter);
+
+enum mp_filter_command_type {
+ MP_FILTER_COMMAND_TEXT = 1,
+ MP_FILTER_COMMAND_GET_META,
+ MP_FILTER_COMMAND_SET_SPEED,
+ MP_FILTER_COMMAND_SET_SPEED_RESAMPLE,
+ MP_FILTER_COMMAND_SET_SPEED_DROP,
+ MP_FILTER_COMMAND_IS_ACTIVE,
+};
+
+struct mp_filter_command {
+ enum mp_filter_command_type type;
+
+ // For MP_FILTER_COMMAND_TEXT
+ const char *target;
+ const char *cmd;
+ const char *arg;
+
+ // For MP_FILTER_COMMAND_GET_META
+ void *res; // must point to struct mp_tags*, will be set to new instance
+
+ // For MP_FILTER_COMMAND_SET_SPEED and MP_FILTER_COMMAND_SET_SPEED_RESAMPLE
+ double speed;
+
+ // For MP_FILTER_COMMAND_IS_ACTIVE
+ bool is_active;
+};
+
+// Run a command on the filter. Returns success. For libavfilter.
+bool mp_filter_command(struct mp_filter *f, struct mp_filter_command *cmd);
+
+// Specific information about a sub-tree in a filter graph. Currently, this is
+// mostly used to give filters access to VO mechanisms and capabilities.
+struct mp_stream_info {
+ void *priv; // for use by whoever implements the callbacks
+
+ double (*get_display_fps)(struct mp_stream_info *i);
+ void (*get_display_res)(struct mp_stream_info *i, int *res);
+
+ struct mp_hwdec_devices *hwdec_devs;
+ struct osd_state *osd;
+ bool rotate90;
+ struct vo *dr_vo; // for calling vo_get_image()
+};
+
+// Search for a parent filter (including f) that has this set, and return it.
+struct mp_stream_info *mp_filter_find_stream_info(struct mp_filter *f);
+
+struct mp_hwdec_ctx *mp_filter_load_hwdec_device(struct mp_filter *f, int imgfmt);
+
+// Perform filtering. This runs until the filter graph is blocked (due to
+// missing external input or unread output). It returns whether any outside
+// pins have changed state.
+// Can be called on the root filter only.
+bool mp_filter_graph_run(struct mp_filter *root);
+
+// Set the maximum time mp_filter_graph_run() should block. If the maximum time
+// expires, the effect is the same as calling mp_filter_graph_interrupt() while
+// the function is running. See that function for further details.
+// The default is seconds==INFINITY. Values <=0 make it return after 1 iteration.
+// Can be called on the root filter only.
+void mp_filter_graph_set_max_run_time(struct mp_filter *root, double seconds);
+
+// Interrupt mp_filter_graph_run() asynchronously. This does not stop filtering
+// in a destructive way, but merely suspends it. In practice, this will make
+// mp_filter_graph_run() return after the current filter's process() function has
+// finished. Filtering can be resumed with subsequent mp_filter_graph_run() calls.
+// When mp_filter_graph_run() is interrupted, it will trigger the filter graph
+// wakeup callback, which in turn ensures that the user will call
+// mp_filter_graph_run() again.
+// If it is called if not in mp_filter_graph_run(), the next mp_filter_graph_run()
+// call is interrupted and no filtering is done for that call.
+// Calling this too often will starve filtering.
+// This does not call the graph wakeup callback directly, which will avoid
+// potential reentrancy issues. (But mp_filter_graph_run() will call it in
+// reaction to it, as described above.)
+// Explicitly thread-safe.
+// Can be called on the root filter only.
+void mp_filter_graph_interrupt(struct mp_filter *root);
+
+// Create a root dummy filter with no inputs or outputs. This fulfills the
+// following functions:
+// - creating a new filter graph (attached to the root filter)
+// - passing it as parent filter to top-level filters
+// - driving the filter loop between the shared filters
+// - setting the wakeup callback for async filtering
+// - implicitly passing down global data like mpv_global and keeping filter
+// constructor functions simple
+// Note that you can still connect pins of filters with different parents or
+// root filters, but then you may have to manually invoke mp_filter_graph_run()
+// on the root filters of the connected filters to drive data flow.
+struct mp_filter *mp_filter_create_root(struct mpv_global *global);
+
+// Asynchronous filters may need to wakeup the user thread if the status of any
+// mp_pin has changed. If this is called, the callback provider should get the
+// user's thread to call mp_filter_graph_run() again.
+// The wakeup callback must not recursively call into any filter APIs, or do
+// blocking waits on the filter API (deadlocks will happen).
+// A wakeup callback should always set a "wakeup" flag, that is reset only when
+// mp_filter_graph_run() is going to be called again with no wait time.
+// Can be called on the root filter only.
+void mp_filter_graph_set_wakeup_cb(struct mp_filter *root,
+ void (*wakeup_cb)(void *ctx), void *ctx);
+
+// Debugging internal stuff.
+void mp_filter_dump_states(struct mp_filter *f);
diff --git a/filters/filter_internal.h b/filters/filter_internal.h
new file mode 100644
index 0000000..e2e9db9
--- /dev/null
+++ b/filters/filter_internal.h
@@ -0,0 +1,145 @@
+#pragma once
+
+#include <stddef.h>
+
+#include "filter.h"
+
+// Flag the thread as needing mp_filter_process() to be called. Useful for
+// (some) async filters only. Idempotent.
+// Explicitly thread-safe.
+void mp_filter_wakeup(struct mp_filter *f);
+
+// Same as mp_filter_wakeup(), but skip the wakeup, and only mark the filter
+// as requiring processing to possibly update pin states changed due to async
+// processing.
+// Explicitly thread-safe.
+void mp_filter_mark_async_progress(struct mp_filter *f);
+
+// Flag the thread as needing mp_filter_process() to be called. Unlike
+// mp_filter_wakeup(), not thread-safe, and must be called from the process()
+// function of f (in exchange this is very light-weight).
+// In practice, this means process() is repeated.
+void mp_filter_internal_mark_progress(struct mp_filter *f);
+
+// Flag the filter as having failed, and propagate the error to the parent
+// filter. The error propagation stops either at the root filter, or if a filter
+// has an error handler set.
+// Must be called from f's process function.
+void mp_filter_internal_mark_failed(struct mp_filter *f);
+
+// If handler is not NULL, then if filter f errors, don't propagate the error
+// flag to its parent. Also invoke the handler's process() function, which is
+// supposed to use mp_filter_has_failed(f) to check any filters for which it has
+// set itself as error handler.
+// A filter must manually unset itself as error handler if it gets destroyed
+// before the filter f, otherwise dangling pointers will occur.
+void mp_filter_set_error_handler(struct mp_filter *f, struct mp_filter *handler);
+
+// Add a pin. Returns the private handle (same as f->ppins[f->num_pins-1]).
+// The name must be unique across all filter pins (you must verify this
+// yourself if filter names are from user input). name=="" is not allowed.
+// Never returns NULL. dir should be the external filter direction (a filter
+// input will use dir==MP_PIN_IN, and the returned pin will use MP_PIN_OUT,
+// because the internal pin is the opposite end of the external one).
+struct mp_pin *mp_filter_add_pin(struct mp_filter *f, enum mp_pin_dir dir,
+ const char *name);
+
+// Remove and deallocate a pin. The caller must be sure that nothing else
+// references the pin anymore. You must pass the private pin (from
+// mp_filter.ppin). This removes/deallocates the public paired pin as well.
+void mp_filter_remove_pin(struct mp_filter *f, struct mp_pin *p);
+
+// Free all filters which have f as parent. (This has nothing to do with
+// talloc.)
+void mp_filter_free_children(struct mp_filter *f);
+
+struct mp_filter_params;
+
+struct mp_filter_info {
+ // Informational name, in some cases might be used for filter discovery.
+ const char *name;
+
+ // mp_filter.priv is set to memory allocated with this size (if > 0)
+ size_t priv_size;
+
+ // Called during mp_filter_create(). Optional, can be NULL if use of a
+ // constructor function is required, which sets up the real filter after
+ // creation. Actually turns out nothing uses this.
+ bool (*init)(struct mp_filter *f, struct mp_filter_params *params);
+
+ // Free internal resources. Optional.
+ void (*destroy)(struct mp_filter *f);
+
+ // Called if any mp_pin was signalled (i.e. likely new data to process), or
+ // an async wakeup was received some time earlier.
+ // Generally, the implementation would consist of 2 stages:
+ // 1. check for the pin states, possibly request/probe for input/output
+ // 2. if data flow can happen, read a frame, perform actual work, write
+ // result
+ // The process function will usually run very often, when pin states are
+ // updated, so the generic code can determine where data flow can happen.
+ // The common case will be that process() is called running stage 1 a bunch
+ // of times, until it finally can run stage 2 too.
+ // Optional.
+ void (*process)(struct mp_filter *f);
+
+ // Clear internal state and buffers (e.g. on seeks). Filtering can restart
+ // after this, and all settings are preserved. It makes sense to preserve
+ // internal resources for further filtering as well if you can.
+ // Input/output pins are always cleared by the common code before invoking
+ // this callback.
+ // Optional, can be NULL for filters without state.
+ // Don't create or destroy filters in this function, don't reconnect pins,
+ // don't access pins.
+ void (*reset)(struct mp_filter *f);
+
+ // Send a command to the filter. Highly implementation specific, usually
+ // user-initiated. Optional.
+ bool (*command)(struct mp_filter *f, struct mp_filter_command *cmd);
+};
+
+// Return the mp_filter_info this filter was created with.
+const struct mp_filter_info *mp_filter_get_info(struct mp_filter *f);
+
+// Create a filter instance. Returns NULL on failure.
+// Destroy/free with talloc_free().
+// This is for filter implementers only. Filters are created with their own
+// constructor functions (instead of a generic one), which call this function
+// to create the filter itself.
+// parent is never NULL; use mp_filter_create_root() to create a top most
+// filter.
+// The parent does not imply anything about the position of the filter in
+// the dataflow (only the mp_pin connections matter). The parent exists for
+// convenience, which includes:
+// - passing down implicit and explicit parameters (such as the filter driver
+// loop)
+// - auto freeing child filters if the parent is free'd
+// - propagating errors
+// - setting the parent as default manual connection for new external filter
+// pins
+// The parent filter stays valid for the lifetime of any filters having it
+// directly or indirectly as parent. If the parent is free'd, all children are
+// automatically free'd.
+// All filters in the same parent tree must be driven in the same thread (or be
+// explicitly synchronized otherwise).
+struct mp_filter *mp_filter_create(struct mp_filter *parent,
+ const struct mp_filter_info *info);
+
+struct mp_filter_params {
+ // Identifies the filter and its implementation. The pointer must stay
+ // valid for the life time of the created filter instance.
+ const struct mp_filter_info *info;
+
+ // Must be set if global==NULL. See mp_filter_create() for remarks.
+ struct mp_filter *parent;
+
+ // Must be set if parent==NULL, can otherwise be NULL.
+ struct mpv_global *global;
+
+ // Filter specific parameters. Most filters will have a constructor
+ // function, and pass in something internal.
+ void *params;
+};
+
+// Same as mp_filter_create(), but technically more flexible.
+struct mp_filter *mp_filter_create_with_params(struct mp_filter_params *params);
diff --git a/filters/frame.c b/filters/frame.c
new file mode 100644
index 0000000..200e900
--- /dev/null
+++ b/filters/frame.c
@@ -0,0 +1,210 @@
+#include <libavutil/frame.h>
+
+#include "audio/aframe.h"
+#include "common/av_common.h"
+#include "demux/packet.h"
+#include "video/mp_image.h"
+
+#include "frame.h"
+
+struct frame_handler {
+ const char *name;
+ bool is_data;
+ bool is_signaling;
+ void *(*new_ref)(void *data);
+ double (*get_pts)(void *data);
+ void (*set_pts)(void *data, double pts);
+ int (*approx_size)(void *data);
+ AVFrame *(*new_av_ref)(void *data);
+ void *(*from_av_ref)(AVFrame *data);
+ void (*free)(void *data);
+};
+
+static void *video_ref(void *data)
+{
+ return mp_image_new_ref(data);
+}
+
+static double video_get_pts(void *data)
+{
+ return ((struct mp_image *)data)->pts;
+}
+
+static void video_set_pts(void *data, double pts)
+{
+ ((struct mp_image *)data)->pts = pts;
+}
+
+static int video_approx_size(void *data)
+{
+ return mp_image_approx_byte_size(data);
+}
+
+static AVFrame *video_new_av_ref(void *data)
+{
+ return mp_image_to_av_frame(data);
+}
+
+static void *video_from_av_ref(AVFrame *data)
+{
+ return mp_image_from_av_frame(data);
+}
+
+static void *audio_ref(void *data)
+{
+ return mp_aframe_new_ref(data);
+}
+
+static double audio_get_pts(void *data)
+{
+ return mp_aframe_get_pts(data);
+}
+
+static void audio_set_pts(void *data, double pts)
+{
+ mp_aframe_set_pts(data, pts);
+}
+
+static int audio_approx_size(void *data)
+{
+ return mp_aframe_approx_byte_size(data);
+}
+
+static AVFrame *audio_new_av_ref(void *data)
+{
+ return mp_aframe_to_avframe(data);
+}
+
+static void *audio_from_av_ref(AVFrame *data)
+{
+ return mp_aframe_from_avframe(data);
+}
+
+static void *packet_ref(void *data)
+{
+ return demux_copy_packet(data);
+}
+
+static const struct frame_handler frame_handlers[] = {
+ [MP_FRAME_NONE] = {
+ .name = "none",
+ },
+ [MP_FRAME_EOF] = {
+ .name = "eof",
+ .is_signaling = true,
+ },
+ [MP_FRAME_VIDEO] = {
+ .name = "video",
+ .is_data = true,
+ .new_ref = video_ref,
+ .get_pts = video_get_pts,
+ .set_pts = video_set_pts,
+ .approx_size = video_approx_size,
+ .new_av_ref = video_new_av_ref,
+ .from_av_ref = video_from_av_ref,
+ .free = talloc_free,
+ },
+ [MP_FRAME_AUDIO] = {
+ .name = "audio",
+ .is_data = true,
+ .new_ref = audio_ref,
+ .get_pts = audio_get_pts,
+ .set_pts = audio_set_pts,
+ .approx_size = audio_approx_size,
+ .new_av_ref = audio_new_av_ref,
+ .from_av_ref = audio_from_av_ref,
+ .free = talloc_free,
+ },
+ [MP_FRAME_PACKET] = {
+ .name = "packet",
+ .is_data = true,
+ .new_ref = packet_ref,
+ .free = talloc_free,
+ },
+};
+
+const char *mp_frame_type_str(enum mp_frame_type t)
+{
+ return frame_handlers[t].name;
+}
+
+bool mp_frame_is_data(struct mp_frame frame)
+{
+ return frame_handlers[frame.type].is_data;
+}
+
+bool mp_frame_is_signaling(struct mp_frame frame)
+{
+ return frame_handlers[frame.type].is_signaling;
+}
+
+void mp_frame_unref(struct mp_frame *frame)
+{
+ if (!frame)
+ return;
+
+ if (frame_handlers[frame->type].free)
+ frame_handlers[frame->type].free(frame->data);
+
+ *frame = (struct mp_frame){0};
+}
+
+struct mp_frame mp_frame_ref(struct mp_frame frame)
+{
+ if (frame_handlers[frame.type].new_ref) {
+ assert(frame.data);
+ frame.data = frame_handlers[frame.type].new_ref(frame.data);
+ if (!frame.data)
+ frame.type = MP_FRAME_NONE;
+ }
+ return frame;
+}
+
+double mp_frame_get_pts(struct mp_frame frame)
+{
+ if (frame_handlers[frame.type].get_pts)
+ return frame_handlers[frame.type].get_pts(frame.data);
+ return MP_NOPTS_VALUE;
+}
+
+void mp_frame_set_pts(struct mp_frame frame, double pts)
+{
+ if (frame_handlers[frame.type].get_pts)
+ frame_handlers[frame.type].set_pts(frame.data, pts);
+}
+
+int mp_frame_approx_size(struct mp_frame frame)
+{
+ if (frame_handlers[frame.type].approx_size)
+ return frame_handlers[frame.type].approx_size(frame.data);
+ return 0;
+}
+
+AVFrame *mp_frame_to_av(struct mp_frame frame, struct AVRational *tb)
+{
+ if (!frame_handlers[frame.type].new_av_ref)
+ return NULL;
+
+ AVFrame *res = frame_handlers[frame.type].new_av_ref(frame.data);
+ if (!res)
+ return NULL;
+
+ res->pts = mp_pts_to_av(mp_frame_get_pts(frame), tb);
+ return res;
+}
+
+struct mp_frame mp_frame_from_av(enum mp_frame_type type, struct AVFrame *frame,
+ struct AVRational *tb)
+{
+ struct mp_frame res = {type};
+
+ if (!frame_handlers[res.type].from_av_ref)
+ return MP_NO_FRAME;
+
+ res.data = frame_handlers[res.type].from_av_ref(frame);
+ if (!res.data)
+ return MP_NO_FRAME;
+
+ mp_frame_set_pts(res, mp_pts_from_av(frame->pts, tb));
+ return res;
+}
diff --git a/filters/frame.h b/filters/frame.h
new file mode 100644
index 0000000..4c6c4ef
--- /dev/null
+++ b/filters/frame.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <stdbool.h>
+
+enum mp_frame_type {
+ MP_FRAME_NONE = 0, // NULL, placeholder, no frame available (_not_ EOF)
+ MP_FRAME_VIDEO, // struct mp_image*
+ MP_FRAME_AUDIO, // struct mp_aframe*
+ MP_FRAME_PACKET, // struct demux_packet*
+ MP_FRAME_EOF, // NULL, signals end of stream (but frames after it can
+ // resume filtering!)
+};
+
+const char *mp_frame_type_str(enum mp_frame_type t);
+
+// Generic container for a piece of data, such as a video frame, or a collection
+// of audio samples. Wraps an actual media-specific frame data types in a
+// generic way. Also can be an empty frame for signaling (MP_FRAME_EOF and
+// possibly others).
+// This struct is usually allocated on the stack and can be copied by value.
+// You need to consider that the underlying pointer is ref-counted, and that
+// the _unref/_ref functions must be used accordingly.
+struct mp_frame {
+ enum mp_frame_type type;
+ void *data;
+};
+
+// Return whether the frame contains actual data (audio, video, ...). If false,
+// it's either signaling, or MP_FRAME_NONE.
+bool mp_frame_is_data(struct mp_frame frame);
+
+// Return whether the frame is for signaling (data flow commands like
+// MP_FRAME_EOF). If false, it's either data (mp_frame_is_data()), or
+// MP_FRAME_NONE.
+bool mp_frame_is_signaling(struct mp_frame frame);
+
+// Unreferences any frame data, and sets *frame to MP_FRAME_NONE. (It does
+// _not_ deallocate the memory block the parameter points to, only frame->data.)
+void mp_frame_unref(struct mp_frame *frame);
+
+// Return a new reference to the given frame. The caller owns the returned
+// frame. On failure returns a MP_FRAME_NONE.
+struct mp_frame mp_frame_ref(struct mp_frame frame);
+
+double mp_frame_get_pts(struct mp_frame frame);
+void mp_frame_set_pts(struct mp_frame frame, double pts);
+
+// Estimation of total size in bytes. This is for buffering purposes.
+int mp_frame_approx_size(struct mp_frame frame);
+
+struct AVFrame;
+struct AVRational;
+struct AVFrame *mp_frame_to_av(struct mp_frame frame, struct AVRational *tb);
+struct mp_frame mp_frame_from_av(enum mp_frame_type type, struct AVFrame *frame,
+ struct AVRational *tb);
+
+#define MAKE_FRAME(type, frame) ((struct mp_frame){(type), (frame)})
+#define MP_NO_FRAME MAKE_FRAME(0, 0)
+#define MP_EOF_FRAME MAKE_FRAME(MP_FRAME_EOF, 0)
diff --git a/filters/user_filters.c b/filters/user_filters.c
new file mode 100644
index 0000000..c879535
--- /dev/null
+++ b/filters/user_filters.c
@@ -0,0 +1,185 @@
+#include <libavutil/avutil.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_config_frontend.h"
+
+#include "f_lavfi.h"
+#include "user_filters.h"
+
+static bool get_desc_from(const struct mp_user_filter_entry **list, int num,
+ struct m_obj_desc *dst, int index)
+{
+ if (index >= num)
+ return false;
+ const struct mp_user_filter_entry *entry = list[index];
+ *dst = entry->desc;
+ dst->p = entry;
+ return true;
+}
+
+static bool check_unknown_entry(const char *name, int media_type)
+{
+ // Generic lavfi bridge: skip the lavfi- prefix, if present.
+ if (strncmp(name, "lavfi-", 6) == 0)
+ name += 6;
+ return mp_lavfi_is_usable(name, media_type);
+}
+
+// --af option
+
+const struct mp_user_filter_entry *af_list[] = {
+ &af_lavfi,
+ &af_lavfi_bridge,
+ &af_scaletempo,
+ &af_scaletempo2,
+ &af_format,
+#if HAVE_RUBBERBAND
+ &af_rubberband,
+#endif
+ &af_lavcac3enc,
+ &af_drop,
+};
+
+static bool get_af_desc(struct m_obj_desc *dst, int index)
+{
+ return get_desc_from(af_list, MP_ARRAY_SIZE(af_list), dst, index);
+}
+
+static void print_af_help_list(struct mp_log *log)
+{
+ print_lavfi_help_list(log, AVMEDIA_TYPE_AUDIO);
+}
+
+static void print_af_lavfi_help(struct mp_log *log, const char *name)
+{
+ print_lavfi_help(log, name, AVMEDIA_TYPE_AUDIO);
+}
+
+static bool check_af_lavfi(const char *name)
+{
+ return check_unknown_entry(name, AVMEDIA_TYPE_AUDIO);
+}
+
+const struct m_obj_list af_obj_list = {
+ .get_desc = get_af_desc,
+ .description = "audio filters",
+ .allow_disable_entries = true,
+ .check_unknown_entry = check_af_lavfi,
+ .print_help_list = print_af_help_list,
+ .print_unknown_entry_help = print_af_lavfi_help,
+};
+
+// --vf option
+
+const struct mp_user_filter_entry *vf_list[] = {
+ &vf_format,
+ &vf_lavfi,
+ &vf_lavfi_bridge,
+ &vf_sub,
+#if HAVE_ZIMG
+ &vf_fingerprint,
+#endif
+#if HAVE_VAPOURSYNTH
+ &vf_vapoursynth,
+#endif
+#if HAVE_VDPAU
+ &vf_vdpaupp,
+#endif
+#if HAVE_VAAPI
+ &vf_vavpp,
+#endif
+#if HAVE_D3D_HWACCEL
+ &vf_d3d11vpp,
+#endif
+#if HAVE_EGL_HELPERS && HAVE_GL && HAVE_EGL
+ &vf_gpu,
+#endif
+};
+
+static bool get_vf_desc(struct m_obj_desc *dst, int index)
+{
+ return get_desc_from(vf_list, MP_ARRAY_SIZE(vf_list), dst, index);
+}
+
+static void print_vf_help_list(struct mp_log *log)
+{
+ print_lavfi_help_list(log, AVMEDIA_TYPE_VIDEO);
+}
+
+static void print_vf_lavfi_help(struct mp_log *log, const char *name)
+{
+ print_lavfi_help(log, name, AVMEDIA_TYPE_VIDEO);
+}
+
+static bool check_vf_lavfi(const char *name)
+{
+ return check_unknown_entry(name, AVMEDIA_TYPE_VIDEO);
+}
+
+const struct m_obj_list vf_obj_list = {
+ .get_desc = get_vf_desc,
+ .description = "video filters",
+ .allow_disable_entries = true,
+ .check_unknown_entry = check_vf_lavfi,
+ .print_help_list = print_vf_help_list,
+ .print_unknown_entry_help = print_vf_lavfi_help,
+};
+
+// Create a bidir, single-media filter from command line arguments.
+struct mp_filter *mp_create_user_filter(struct mp_filter *parent,
+ enum mp_output_chain_type type,
+ const char *name, char **args)
+{
+ const struct m_obj_list *obj_list = NULL;
+ enum mp_frame_type frame_type = 0;
+ if (type == MP_OUTPUT_CHAIN_VIDEO) {
+ frame_type = MP_FRAME_VIDEO;
+ obj_list = &vf_obj_list;
+ } else if (type == MP_OUTPUT_CHAIN_AUDIO) {
+ frame_type = MP_FRAME_AUDIO;
+ obj_list = &af_obj_list;
+ }
+ assert(frame_type && obj_list);
+
+ struct mp_filter *f = NULL;
+
+ struct m_obj_desc desc;
+ if (!m_obj_list_find(&desc, obj_list, bstr0(name))) {
+ // Generic lavfi bridge.
+ if (strncmp(name, "lavfi-", 6) == 0)
+ name += 6;
+ struct mp_lavfi *l =
+ mp_lavfi_create_filter(parent, frame_type, true, NULL, NULL, name, args);
+ if (l)
+ f = l->f;
+ goto done;
+ }
+
+ void *options = NULL;
+ if (desc.options) {
+ struct m_config *config =
+ m_config_from_obj_desc_and_args(NULL, parent->log, parent->global,
+ &desc, args);
+
+ if (!config)
+ goto done;
+
+ options = config->optstruct;
+ // Free config when options is freed.
+ ta_set_parent(options, NULL);
+ ta_set_parent(config, options);
+ }
+
+ const struct mp_user_filter_entry *entry = desc.p;
+ f = entry->create(parent, options);
+
+done:
+ if (!f) {
+ MP_ERR(parent, "Creating filter '%s' failed.\n", name);
+ return NULL;
+ }
+ return f;
+}
diff --git a/filters/user_filters.h b/filters/user_filters.h
new file mode 100644
index 0000000..639ffe7
--- /dev/null
+++ b/filters/user_filters.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "options/m_option.h"
+
+#include "f_output_chain.h"
+
+// For creating filters from command line. Strictly for --vf/--af.
+struct mp_user_filter_entry {
+ // Name and sub-option description.
+ struct m_obj_desc desc;
+ // Create a filter. The option pointer is non-NULL if desc implies a priv
+ // struct to be allocated; then options are parsed into it. The callee
+ // must always free options (but can reparent it with talloc to keep it).
+ struct mp_filter *(*create)(struct mp_filter *parent, void *options);
+};
+
+struct mp_filter *mp_create_user_filter(struct mp_filter *parent,
+ enum mp_output_chain_type type,
+ const char *name, char **args);
+
+extern const struct mp_user_filter_entry af_lavfi;
+extern const struct mp_user_filter_entry af_lavfi_bridge;
+extern const struct mp_user_filter_entry af_scaletempo;
+extern const struct mp_user_filter_entry af_scaletempo2;
+extern const struct mp_user_filter_entry af_format;
+extern const struct mp_user_filter_entry af_rubberband;
+extern const struct mp_user_filter_entry af_lavcac3enc;
+extern const struct mp_user_filter_entry af_drop;
+
+extern const struct mp_user_filter_entry vf_lavfi;
+extern const struct mp_user_filter_entry vf_lavfi_bridge;
+extern const struct mp_user_filter_entry vf_sub;
+extern const struct mp_user_filter_entry vf_vapoursynth;
+extern const struct mp_user_filter_entry vf_format;
+extern const struct mp_user_filter_entry vf_vdpaupp;
+extern const struct mp_user_filter_entry vf_vavpp;
+extern const struct mp_user_filter_entry vf_d3d11vpp;
+extern const struct mp_user_filter_entry vf_fingerprint;
+extern const struct mp_user_filter_entry vf_gpu;
diff --git a/input/cmd.c b/input/cmd.c
new file mode 100644
index 0000000..6423214
--- /dev/null
+++ b/input/cmd.c
@@ -0,0 +1,671 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+
+#include "misc/bstr.h"
+#include "misc/node.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_option.h"
+
+#include "cmd.h"
+#include "input.h"
+#include "misc/json.h"
+
+#include "libmpv/client.h"
+
+static void destroy_cmd(void *ptr)
+{
+ struct mp_cmd *cmd = ptr;
+ for (int n = 0; n < cmd->nargs; n++) {
+ if (cmd->args[n].type)
+ m_option_free(cmd->args[n].type, &cmd->args[n].v);
+ }
+}
+
+struct flag {
+ const char *name;
+ unsigned int remove, add;
+};
+
+static const struct flag cmd_flags[] = {
+ {"no-osd", MP_ON_OSD_FLAGS, MP_ON_OSD_NO},
+ {"osd-bar", MP_ON_OSD_FLAGS, MP_ON_OSD_BAR},
+ {"osd-msg", MP_ON_OSD_FLAGS, MP_ON_OSD_MSG},
+ {"osd-msg-bar", MP_ON_OSD_FLAGS, MP_ON_OSD_MSG | MP_ON_OSD_BAR},
+ {"osd-auto", MP_ON_OSD_FLAGS, MP_ON_OSD_AUTO},
+ {"expand-properties", 0, MP_EXPAND_PROPERTIES},
+ {"raw", MP_EXPAND_PROPERTIES, 0},
+ {"repeatable", 0, MP_ALLOW_REPEAT},
+ {"async", MP_SYNC_CMD, MP_ASYNC_CMD},
+ {"sync", MP_ASYNC_CMD, MP_SYNC_CMD},
+ {0}
+};
+
+static bool apply_flag(struct mp_cmd *cmd, bstr str)
+{
+ for (int n = 0; cmd_flags[n].name; n++) {
+ if (bstr_equals0(str, cmd_flags[n].name)) {
+ cmd->flags = (cmd->flags & ~cmd_flags[n].remove) | cmd_flags[n].add;
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool find_cmd(struct mp_log *log, struct mp_cmd *cmd, bstr name)
+{
+ if (name.len == 0) {
+ mp_err(log, "Command name missing.\n");
+ return false;
+ }
+
+ char nname[80];
+ snprintf(nname, sizeof(nname), "%.*s", BSTR_P(name));
+ for (int n = 0; nname[n]; n++) {
+ if (nname[n] == '_')
+ nname[n] = '-';
+ }
+
+ for (int n = 0; mp_cmds[n].name; n++) {
+ if (strcmp(nname, mp_cmds[n].name) == 0) {
+ cmd->def = &mp_cmds[n];
+ cmd->name = (char *)cmd->def->name;
+ return true;
+ }
+ }
+ mp_err(log, "Command '%.*s' not found.\n", BSTR_P(name));
+ return false;
+}
+
+static bool is_vararg(const struct mp_cmd_def *m, int i)
+{
+ return m->vararg && (i + 1 >= MP_CMD_DEF_MAX_ARGS || !m->args[i + 1].type);
+}
+
+static const struct m_option *get_arg_type(const struct mp_cmd_def *cmd, int i)
+{
+ const struct m_option *opt = NULL;
+ if (is_vararg(cmd, i)) {
+ // The last arg in a vararg command sets all vararg types.
+ for (int n = MPMIN(i, MP_CMD_DEF_MAX_ARGS - 1); n >= 0; n--) {
+ if (cmd->args[n].type) {
+ opt = &cmd->args[n];
+ break;
+ }
+ }
+ } else if (i < MP_CMD_DEF_MAX_ARGS) {
+ opt = &cmd->args[i];
+ }
+ return opt && opt->type ? opt : NULL;
+}
+
+// Return the name of the argument, possibly as stack allocated string (which is
+// why this is a macro, and out of laziness). Otherwise as get_arg_type().
+#define get_arg_name(cmd, i) \
+ ((i) < MP_CMD_DEF_MAX_ARGS && (cmd)->args[(i)].name && \
+ (cmd)->args[(i)].name[0] \
+ ? (cmd)->args[(i)].name : mp_tprintf(10, "%d", (i) + 1))
+
+// Verify that there are no missing args, fill in missing optional args.
+static bool finish_cmd(struct mp_log *log, struct mp_cmd *cmd)
+{
+ for (int i = 0; i < MP_CMD_DEF_MAX_ARGS; i++) {
+ // (type==NULL is used for yet unset arguments)
+ if (i < cmd->nargs && cmd->args[i].type)
+ continue;
+ const struct m_option *opt = get_arg_type(cmd->def, i);
+ if (i >= cmd->nargs && (!opt || is_vararg(cmd->def, i)))
+ break;
+ if (!opt->defval && !(opt->flags & MP_CMD_OPT_ARG)) {
+ mp_err(log, "Command %s: required argument %s not set.\n",
+ cmd->name, get_arg_name(cmd->def, i));
+ return false;
+ }
+ struct mp_cmd_arg arg = {.type = opt};
+ if (opt->defval)
+ m_option_copy(opt, &arg.v, opt->defval);
+ assert(i <= cmd->nargs);
+ if (i == cmd->nargs) {
+ MP_TARRAY_APPEND(cmd, cmd->args, cmd->nargs, arg);
+ } else {
+ cmd->args[i] = arg;
+ }
+ }
+
+ if (!(cmd->flags & (MP_ASYNC_CMD | MP_SYNC_CMD)))
+ cmd->flags |= cmd->def->default_async ? MP_ASYNC_CMD : MP_SYNC_CMD;
+
+ return true;
+}
+
+static bool set_node_arg(struct mp_log *log, struct mp_cmd *cmd, int i,
+ mpv_node *val)
+{
+ const char *name = get_arg_name(cmd->def, i);
+
+ const struct m_option *opt = get_arg_type(cmd->def, i);
+ if (!opt) {
+ mp_err(log, "Command %s: has only %d arguments.\n", cmd->name, i);
+ return false;
+ }
+
+ if (i < cmd->nargs && cmd->args[i].type) {
+ mp_err(log, "Command %s: argument %s was already set.\n", cmd->name, name);
+ return false;
+ }
+
+ struct mp_cmd_arg arg = {.type = opt};
+ void *dst = &arg.v;
+ if (val->format == MPV_FORMAT_STRING) {
+ int r = m_option_parse(log, opt, bstr0(cmd->name),
+ bstr0(val->u.string), dst);
+ if (r < 0) {
+ mp_err(log, "Command %s: argument %s can't be parsed: %s.\n",
+ cmd->name, name, m_option_strerror(r));
+ return false;
+ }
+ } else {
+ int r = m_option_set_node(opt, dst, val);
+ if (r < 0) {
+ mp_err(log, "Command %s: argument %s has incompatible type.\n",
+ cmd->name, name);
+ return false;
+ }
+ }
+
+ // (leave unset arguments blank, to be set later or checked by finish_cmd())
+ while (i >= cmd->nargs) {
+ struct mp_cmd_arg t = {0};
+ MP_TARRAY_APPEND(cmd, cmd->args, cmd->nargs, t);
+ }
+
+ cmd->args[i] = arg;
+ return true;
+}
+
+static bool cmd_node_array(struct mp_log *log, struct mp_cmd *cmd, mpv_node *node)
+{
+ assert(node->format == MPV_FORMAT_NODE_ARRAY);
+ mpv_node_list *args = node->u.list;
+ int cur = 0;
+
+ while (cur < args->num) {
+ if (args->values[cur].format != MPV_FORMAT_STRING)
+ break;
+ if (!apply_flag(cmd, bstr0(args->values[cur].u.string)))
+ break;
+ cur++;
+ }
+
+ bstr cmd_name = {0};
+ if (cur < args->num && args->values[cur].format == MPV_FORMAT_STRING)
+ cmd_name = bstr0(args->values[cur++].u.string);
+ if (!find_cmd(log, cmd, cmd_name))
+ return false;
+
+ int first = cur;
+ for (int i = 0; i < args->num - first; i++) {
+ if (!set_node_arg(log, cmd, cmd->nargs, &args->values[cur++]))
+ return false;
+ }
+
+ return true;
+}
+
+static bool cmd_node_map(struct mp_log *log, struct mp_cmd *cmd, mpv_node *node)
+{
+ assert(node->format == MPV_FORMAT_NODE_MAP);
+ mpv_node_list *args = node->u.list;
+
+ mpv_node *name = node_map_get(node, "name");
+ if (!name || name->format != MPV_FORMAT_STRING)
+ return false;
+
+ if (!find_cmd(log, cmd, bstr0(name->u.string)))
+ return false;
+
+ if (cmd->def->vararg) {
+ mp_err(log, "Command %s: this command uses a variable number of "
+ "arguments, which does not work with named arguments.\n",
+ cmd->name);
+ return false;
+ }
+
+ for (int n = 0; n < args->num; n++) {
+ const char *key = args->keys[n];
+ mpv_node *val = &args->values[n];
+
+ if (strcmp(key, "name") == 0) {
+ // already handled above
+ } else if (strcmp(key, "_flags") == 0) {
+ if (val->format != MPV_FORMAT_NODE_ARRAY)
+ return false;
+ mpv_node_list *flags = val->u.list;
+ for (int i = 0; i < flags->num; i++) {
+ if (flags->values[i].format != MPV_FORMAT_STRING)
+ return false;
+ if (!apply_flag(cmd, bstr0(flags->values[i].u.string)))
+ return false;
+ }
+ } else {
+ int arg = -1;
+
+ for (int i = 0; i < MP_CMD_DEF_MAX_ARGS; i++) {
+ const char *arg_name = cmd->def->args[i].name;
+ if (arg_name && arg_name[0] && strcmp(key, arg_name) == 0) {
+ arg = i;
+ break;
+ }
+ }
+
+ if (arg < 0) {
+ mp_err(log, "Command %s: no argument %s.\n", cmd->name, key);
+ return false;
+ }
+
+ if (!set_node_arg(log, cmd, arg, val))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+struct mp_cmd *mp_input_parse_cmd_node(struct mp_log *log, mpv_node *node)
+{
+ struct mp_cmd *cmd = talloc_ptrtype(NULL, cmd);
+ talloc_set_destructor(cmd, destroy_cmd);
+ *cmd = (struct mp_cmd) { .scale = 1, .scale_units = 1 };
+
+ bool res = false;
+ if (node->format == MPV_FORMAT_NODE_ARRAY) {
+ res = cmd_node_array(log, cmd, node);
+ } else if (node->format == MPV_FORMAT_NODE_MAP) {
+ res = cmd_node_map(log, cmd, node);
+ }
+
+ res = res && finish_cmd(log, cmd);
+
+ if (!res)
+ TA_FREEP(&cmd);
+
+ return cmd;
+}
+
+static bool read_token(bstr str, bstr *out_rest, bstr *out_token)
+{
+ bstr t = bstr_lstrip(str);
+ int next = bstrcspn(t, WHITESPACE "#;");
+ if (!next)
+ return false;
+ *out_token = bstr_splice(t, 0, next);
+ *out_rest = bstr_cut(t, next);
+ return true;
+}
+
+struct parse_ctx {
+ struct mp_log *log;
+ void *tmp;
+ bstr start, str;
+};
+
+static int pctx_read_token(struct parse_ctx *ctx, bstr *out)
+{
+ *out = (bstr){0};
+ ctx->str = bstr_lstrip(ctx->str);
+ bstr start = ctx->str;
+ if (bstr_eatstart0(&ctx->str, "\"")) {
+ if (!mp_append_escaped_string_noalloc(ctx->tmp, out, &ctx->str)) {
+ MP_ERR(ctx, "Broken string escapes: ...>%.*s<.\n", BSTR_P(start));
+ return -1;
+ }
+ if (!bstr_eatstart0(&ctx->str, "\"")) {
+ MP_ERR(ctx, "Unterminated double quote: ...>%.*s<.\n", BSTR_P(start));
+ return -1;
+ }
+ return 1;
+ }
+ if (bstr_eatstart0(&ctx->str, "'")) {
+ int next = bstrchr(ctx->str, '\'');
+ if (next < 0) {
+ MP_ERR(ctx, "Unterminated single quote: ...>%.*s<.\n", BSTR_P(start));
+ return -1;
+ }
+ *out = bstr_splice(ctx->str, 0, next);
+ ctx->str = bstr_cut(ctx->str, next+1);
+ return 1;
+ }
+ if (ctx->start.len > 1 && bstr_eatstart0(&ctx->str, "`")) {
+ char endquote[2] = {ctx->str.start[0], '`'};
+ ctx->str = bstr_cut(ctx->str, 1);
+ int next = bstr_find(ctx->str, (bstr){endquote, 2});
+ if (next < 0) {
+ MP_ERR(ctx, "Unterminated custom quote: ...>%.*s<.\n", BSTR_P(start));
+ return -1;
+ }
+ *out = bstr_splice(ctx->str, 0, next);
+ ctx->str = bstr_cut(ctx->str, next+2);
+ return 1;
+ }
+
+ return read_token(ctx->str, &ctx->str, out) ? 1 : 0;
+}
+
+static struct mp_cmd *parse_cmd_str(struct mp_log *log, void *tmp,
+ bstr *str, const char *loc)
+{
+ struct parse_ctx *ctx = &(struct parse_ctx){
+ .log = log,
+ .tmp = tmp,
+ .str = *str,
+ .start = *str,
+ };
+
+ struct mp_cmd *cmd = talloc_ptrtype(NULL, cmd);
+ talloc_set_destructor(cmd, destroy_cmd);
+ *cmd = (struct mp_cmd) {
+ .flags = MP_ON_OSD_AUTO | MP_EXPAND_PROPERTIES,
+ .scale = 1,
+ .scale_units = 1,
+ };
+
+ ctx->str = bstr_lstrip(ctx->str);
+
+ bstr cur_token;
+ if (pctx_read_token(ctx, &cur_token) < 0)
+ goto error;
+
+ while (1) {
+ if (!apply_flag(cmd, cur_token))
+ break;
+ if (pctx_read_token(ctx, &cur_token) < 0)
+ goto error;
+ }
+
+ if (!find_cmd(ctx->log, cmd, cur_token))
+ goto error;
+
+ for (int i = 0; i < MP_CMD_MAX_ARGS; i++) {
+ const struct m_option *opt = get_arg_type(cmd->def, i);
+ if (!opt)
+ break;
+
+ int r = pctx_read_token(ctx, &cur_token);
+ if (r < 0) {
+ MP_ERR(ctx, "Command %s: error in argument %d.\n", cmd->name, i + 1);
+ goto error;
+ }
+ if (r < 1)
+ break;
+
+ struct mp_cmd_arg arg = {.type = opt};
+ r = m_option_parse(ctx->log, opt, bstr0(cmd->name), cur_token, &arg.v);
+ if (r < 0) {
+ MP_ERR(ctx, "Command %s: argument %d can't be parsed: %s.\n",
+ cmd->name, i + 1, m_option_strerror(r));
+ goto error;
+ }
+
+ MP_TARRAY_APPEND(cmd, cmd->args, cmd->nargs, arg);
+ }
+
+ if (!finish_cmd(ctx->log, cmd))
+ goto error;
+
+ bstr dummy;
+ if (read_token(ctx->str, &dummy, &dummy) && ctx->str.len) {
+ MP_ERR(ctx, "Command %s has trailing unused arguments: '%.*s'.\n",
+ cmd->name, BSTR_P(ctx->str));
+ // Better make it fatal to make it clear something is wrong.
+ goto error;
+ }
+
+ bstr orig = {ctx->start.start, ctx->str.start - ctx->start.start};
+ cmd->original = bstrto0(cmd, bstr_strip(orig));
+
+ *str = ctx->str;
+ return cmd;
+
+error:
+ MP_ERR(ctx, "Command was defined at %s.\n", loc);
+ talloc_free(cmd);
+ *str = ctx->str;
+ return NULL;
+}
+
+mp_cmd_t *mp_input_parse_cmd_str(struct mp_log *log, bstr str, const char *loc)
+{
+ void *tmp = talloc_new(NULL);
+ bstr original = str;
+ struct mp_cmd *cmd = parse_cmd_str(log, tmp, &str, loc);
+ if (!cmd)
+ goto done;
+
+ // Handle "multi" commands
+ struct mp_cmd **p_prev = NULL;
+ while (1) {
+ str = bstr_lstrip(str);
+ // read_token just to check whether it's trailing whitespace only
+ bstr u1, u2;
+ if (!bstr_eatstart0(&str, ";") || !read_token(str, &u1, &u2))
+ break;
+ // Multi-command. Since other input.c code uses queue_next for its
+ // own purposes, a pseudo-command is used to wrap the command list.
+ if (!p_prev) {
+ struct mp_cmd *list = talloc_ptrtype(NULL, list);
+ talloc_set_destructor(list, destroy_cmd);
+ *list = (struct mp_cmd) {
+ .name = (char *)mp_cmd_list.name,
+ .def = &mp_cmd_list,
+ };
+ talloc_steal(list, cmd);
+ struct mp_cmd_arg arg = {0};
+ arg.v.p = cmd;
+ list->args = talloc_dup(list, &arg);
+ p_prev = &cmd->queue_next;
+ cmd = list;
+ }
+ struct mp_cmd *sub = parse_cmd_str(log, tmp, &str, loc);
+ if (!sub) {
+ talloc_free(cmd);
+ cmd = NULL;
+ goto done;
+ }
+ talloc_steal(cmd, sub);
+ *p_prev = sub;
+ p_prev = &sub->queue_next;
+ }
+
+ cmd->original = bstrto0(cmd, bstr_strip(
+ bstr_splice(original, 0, str.start - original.start)));
+
+ str = bstr_strip(str);
+ if (bstr_eatstart0(&str, "#") && !bstr_startswith0(str, "#")) {
+ str = bstr_strip(str);
+ if (str.len)
+ cmd->desc = bstrto0(cmd, str);
+ }
+
+done:
+ talloc_free(tmp);
+ return cmd;
+}
+
+struct mp_cmd *mp_input_parse_cmd_strv(struct mp_log *log, const char **argv)
+{
+ int count = 0;
+ while (argv[count])
+ count++;
+ mpv_node *items = talloc_zero_array(NULL, mpv_node, count);
+ mpv_node_list list = {.values = items, .num = count};
+ mpv_node node = {.format = MPV_FORMAT_NODE_ARRAY, .u = {.list = &list}};
+ for (int n = 0; n < count; n++) {
+ items[n] = (mpv_node){.format = MPV_FORMAT_STRING,
+ .u = {.string = (char *)argv[n]}};
+ }
+ struct mp_cmd *res = mp_input_parse_cmd_node(log, &node);
+ talloc_free(items);
+ return res;
+}
+
+void mp_cmd_free(mp_cmd_t *cmd)
+{
+ talloc_free(cmd);
+}
+
+mp_cmd_t *mp_cmd_clone(mp_cmd_t *cmd)
+{
+ if (!cmd)
+ return NULL;
+
+ mp_cmd_t *ret = talloc_dup(NULL, cmd);
+ talloc_set_destructor(ret, destroy_cmd);
+ ret->name = talloc_strdup(ret, cmd->name);
+ ret->args = talloc_zero_array(ret, struct mp_cmd_arg, ret->nargs);
+ for (int i = 0; i < ret->nargs; i++) {
+ ret->args[i].type = cmd->args[i].type;
+ m_option_copy(ret->args[i].type, &ret->args[i].v, &cmd->args[i].v);
+ }
+ ret->original = talloc_strdup(ret, cmd->original);
+ ret->desc = talloc_strdup(ret, cmd->desc);
+ ret->sender = NULL;
+ ret->key_name = talloc_strdup(ret, ret->key_name);
+ ret->key_text = talloc_strdup(ret, ret->key_text);
+
+ if (cmd->def == &mp_cmd_list) {
+ struct mp_cmd *prev = NULL;
+ for (struct mp_cmd *sub = cmd->args[0].v.p; sub; sub = sub->queue_next) {
+ sub = mp_cmd_clone(sub);
+ talloc_steal(ret, sub);
+ if (prev) {
+ prev->queue_next = sub;
+ } else {
+ struct mp_cmd_arg arg = {0};
+ arg.v.p = sub;
+ ret->args = talloc_dup(ret, &arg);
+ }
+ prev = sub;
+ }
+ }
+
+ return ret;
+}
+
+static int get_arg_count(const struct mp_cmd_def *cmd)
+{
+ for (int i = MP_CMD_DEF_MAX_ARGS - 1; i >= 0; i--) {
+ if (cmd->args[i].type)
+ return i + 1;
+ }
+ return 0;
+}
+
+void mp_cmd_dump(struct mp_log *log, int msgl, char *header, struct mp_cmd *cmd)
+{
+ if (!mp_msg_test(log, msgl))
+ return;
+ if (header)
+ mp_msg(log, msgl, "%s ", header);
+ if (!cmd) {
+ mp_msg(log, msgl, "(NULL)\n");
+ return;
+ }
+ mp_msg(log, msgl, "%s, flags=%d, args=[", cmd->name, cmd->flags);
+ int argc = get_arg_count(cmd->def);
+ for (int n = 0; n < cmd->nargs; n++) {
+ const char *argname = cmd->def->args[MPMIN(n, argc - 1)].name;
+ char *s = m_option_print(cmd->args[n].type, &cmd->args[n].v);
+ if (n)
+ mp_msg(log, msgl, ", ");
+ struct mpv_node node = {
+ .format = MPV_FORMAT_STRING,
+ .u.string = s ? s : "(NULL)",
+ };
+ char *esc = NULL;
+ json_write(&esc, &node);
+ mp_msg(log, msgl, "%s=%s", argname, esc ? esc : "<error>");
+ talloc_free(esc);
+ talloc_free(s);
+ }
+ mp_msg(log, msgl, "]\n");
+}
+
+bool mp_input_is_repeatable_cmd(struct mp_cmd *cmd)
+{
+ if (cmd->def == &mp_cmd_list && cmd->args[0].v.p)
+ cmd = cmd->args[0].v.p; // list - only 1st cmd is considered
+
+ return (cmd->def->allow_auto_repeat) || (cmd->flags & MP_ALLOW_REPEAT);
+}
+
+bool mp_input_is_scalable_cmd(struct mp_cmd *cmd)
+{
+ return cmd->def->scalable;
+}
+
+void mp_print_cmd_list(struct mp_log *out)
+{
+ for (int i = 0; mp_cmds[i].name; i++) {
+ const struct mp_cmd_def *def = &mp_cmds[i];
+ mp_info(out, "%-20.20s", def->name);
+ for (int j = 0; j < MP_CMD_DEF_MAX_ARGS && def->args[j].type; j++) {
+ const struct m_option *arg = &def->args[j];
+ bool is_opt = arg->defval || (arg->flags & MP_CMD_OPT_ARG);
+ mp_info(out, " %s%s=%s%s", is_opt ? "[" : "", arg->name,
+ arg->type->name, is_opt ? "]" : "");
+ }
+ if (def->vararg)
+ mp_info(out, "..."); // essentially append to last argument
+ mp_info(out, "\n");
+ }
+}
+
+static int parse_cycle_dir(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ double val;
+ if (bstrcmp0(param, "up") == 0) {
+ val = +1;
+ } else if (bstrcmp0(param, "down") == 0) {
+ val = -1;
+ } else {
+ return m_option_type_double.parse(log, opt, name, param, dst);
+ }
+ *(double *)dst = val;
+ return 1;
+}
+
+static char *print_cycle_dir(const m_option_t *opt, const void *val)
+{
+ return talloc_asprintf(NULL, "%f", *(double *)val);
+}
+
+static void copy_opt(const m_option_t *opt, void *dst, const void *src)
+{
+ if (dst && src)
+ memcpy(dst, src, opt->type->size);
+}
+
+const struct m_option_type m_option_type_cycle_dir = {
+ .name = "up|down",
+ .parse = parse_cycle_dir,
+ .print = print_cycle_dir,
+ .copy = copy_opt,
+ .size = sizeof(double),
+};
diff --git a/input/cmd.h b/input/cmd.h
new file mode 100644
index 0000000..1c9fb3e
--- /dev/null
+++ b/input/cmd.h
@@ -0,0 +1,156 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_PARSE_COMMAND_H
+#define MP_PARSE_COMMAND_H
+
+#include <stdbool.h>
+
+#include "misc/bstr.h"
+#include "options/m_option.h"
+
+#define MP_CMD_DEF_MAX_ARGS 9
+#define MP_CMD_OPT_ARG M_OPT_OPTIONAL_PARAM
+
+struct mp_log;
+struct mp_cmd;
+struct mpv_node;
+
+struct mp_cmd_def {
+ const char *name; // user-visible name (as used in input.conf)
+ void (*handler)(void *ctx);
+ const struct m_option args[MP_CMD_DEF_MAX_ARGS];
+ const void *priv; // for free use by handler()
+ bool allow_auto_repeat; // react to repeated key events
+ bool on_updown; // always emit it on both up and down key events
+ bool vararg; // last argument can be given 0 to multiple times
+ bool scalable;
+ bool is_ignore;
+ bool is_noisy; // reduce log level
+ bool default_async; // default to MP_ASYNC flag if none set by user
+ // If you set this, handler() must ensure mp_cmd_ctx_complete() is called
+ // at some point (can be after handler() returns). If you don't set it, the
+ // common code will call mp_cmd_ctx_complete() when handler() returns.
+ // You must make sure that the core cannot disappear while you do work. The
+ // common code keeps the core referenced only until handler() returns.
+ bool exec_async;
+ // If set, handler() is run on a separate worker thread. This means you can
+ // use mp_core_[un]lock() to temporarily unlock and re-lock the core (while
+ // unlocked, you have no synchronized access to mpctx, but you can do long
+ // running operations without blocking playback or input handling).
+ bool spawn_thread;
+ // If this is set, mp_cmd_ctx.abort is set. Set this if handler() can do
+ // asynchronous abort of the command, and explicitly uses mp_cmd_ctx.abort.
+ // (Not setting it when it's not needed can save resources.)
+ bool can_abort;
+ // If playback ends, and the command is still running, an abort is
+ // automatically triggered.
+ bool abort_on_playback_end;
+};
+
+enum mp_cmd_flags {
+ MP_ON_OSD_NO = 0, // prefer not using OSD
+ MP_ON_OSD_AUTO = 1, // use default behavior of the specific command
+ MP_ON_OSD_BAR = 2, // force a bar, if applicable
+ MP_ON_OSD_MSG = 4, // force a message, if applicable
+ MP_EXPAND_PROPERTIES = 8, // expand strings as properties
+ MP_ALLOW_REPEAT = 16, // if used as keybinding, allow key repeat
+
+ // Exactly one of the following 2 bits is set. Which one is used depends on
+ // the command parser (prefixes and mp_cmd_def.default_async).
+ MP_ASYNC_CMD = 32, // do not wait for command to complete
+ MP_SYNC_CMD = 64, // block on command completion
+
+ MP_ON_OSD_FLAGS = MP_ON_OSD_NO | MP_ON_OSD_AUTO |
+ MP_ON_OSD_BAR | MP_ON_OSD_MSG,
+};
+
+// Arbitrary upper bound for sanity.
+#define MP_CMD_MAX_ARGS 100
+
+struct mp_cmd_arg {
+ const struct m_option *type;
+ union {
+ bool b;
+ int i;
+ int64_t i64;
+ float f;
+ double d;
+ char *s;
+ char **str_list;
+ void *p;
+ } v;
+};
+
+typedef struct mp_cmd {
+ char *name;
+ struct mp_cmd_arg *args;
+ int nargs;
+ int flags; // mp_cmd_flags bitfield
+ char *original;
+ char *desc; // (usually NULL since stripped away later)
+ char *input_section;
+ bool is_up_down : 1;
+ bool is_up : 1;
+ bool emit_on_up : 1;
+ bool is_mouse_button : 1;
+ bool repeated : 1;
+ bool mouse_move : 1;
+ int mouse_x, mouse_y;
+ struct mp_cmd *queue_next;
+ double scale; // for scaling numeric arguments
+ int scale_units;
+ const struct mp_cmd_def *def;
+ char *sender; // name of the client API user which sent this
+ char *key_name; // string representation of the key binding
+ char *key_text; // text if key is a text key
+} mp_cmd_t;
+
+extern const struct mp_cmd_def mp_cmds[];
+extern const struct mp_cmd_def mp_cmd_list;
+
+bool mp_input_is_repeatable_cmd(struct mp_cmd *cmd);
+
+bool mp_input_is_scalable_cmd(struct mp_cmd *cmd);
+
+void mp_print_cmd_list(struct mp_log *out);
+
+// Parse text and return corresponding struct mp_cmd.
+// The location parameter is for error messages.
+struct mp_cmd *mp_input_parse_cmd_str(struct mp_log *log, bstr str,
+ const char *loc);
+
+// Similar to mp_input_parse_cmd(), but takes a list of strings instead.
+// Also, MP_ON_OSD_AUTO | MP_EXPAND_PROPERTIES are not set by default.
+// Keep in mind that these functions (naturally) don't take multiple commands,
+// i.e. a ";" argument does not start a new command.
+struct mp_cmd *mp_input_parse_cmd_strv(struct mp_log *log, const char **argv);
+
+struct mp_cmd *mp_input_parse_cmd_node(struct mp_log *log, struct mpv_node *node);
+
+// After getting a command from mp_input_get_cmd you need to free it using this
+// function
+void mp_cmd_free(struct mp_cmd *cmd);
+
+void mp_cmd_dump(struct mp_log *log, int msgl, char *header, struct mp_cmd *cmd);
+
+// This creates a copy of a command (used by the auto repeat stuff).
+struct mp_cmd *mp_cmd_clone(struct mp_cmd *cmd);
+
+extern const struct m_option_type m_option_type_cycle_dir;
+
+#endif
diff --git a/input/event.c b/input/event.c
new file mode 100644
index 0000000..266e029
--- /dev/null
+++ b/input/event.c
@@ -0,0 +1,93 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "event.h"
+#include "input.h"
+#include "common/msg.h"
+#include "player/external_files.h"
+
+void mp_event_drop_files(struct input_ctx *ictx, int num_files, char **files,
+ enum mp_dnd_action action)
+{
+ bool all_sub = true;
+ for (int i = 0; i < num_files; i++)
+ all_sub &= mp_might_be_subtitle_file(files[i]);
+
+ if (all_sub) {
+ for (int i = 0; i < num_files; i++) {
+ const char *cmd[] = {
+ "osd-auto",
+ "sub-add",
+ files[i],
+ NULL
+ };
+ mp_input_run_cmd(ictx, cmd);
+ }
+ } else {
+ for (int i = 0; i < num_files; i++) {
+ const char *cmd[] = {
+ "osd-auto",
+ "loadfile",
+ files[i],
+ /* Either start playing the dropped files right away
+ or add them to the end of the current playlist */
+ (i == 0 && action == DND_REPLACE) ? "replace" : "append-play",
+ NULL
+ };
+ mp_input_run_cmd(ictx, cmd);
+ }
+ }
+}
+
+int mp_event_drop_mime_data(struct input_ctx *ictx, const char *mime_type,
+ bstr data, enum mp_dnd_action action)
+{
+ // (text lists are the only format supported right now)
+ if (mp_event_get_mime_type_score(ictx, mime_type) >= 0) {
+ void *tmp = talloc_new(NULL);
+ int num_files = 0;
+ char **files = NULL;
+ while (data.len) {
+ bstr line = bstr_getline(data, &data);
+ line = bstr_strip_linebreaks(line);
+ if (bstr_startswith0(line, "#") || !line.start[0])
+ continue;
+ char *s = bstrto0(tmp, line);
+ MP_TARRAY_APPEND(tmp, files, num_files, s);
+ }
+ mp_event_drop_files(ictx, num_files, files, action);
+ talloc_free(tmp);
+ return num_files > 0;
+ } else {
+ return -1;
+ }
+}
+
+int mp_event_get_mime_type_score(struct input_ctx *ictx, const char *mime_type)
+{
+ // X11 and Wayland file list format.
+ if (strcmp(mime_type, "text/uri-list") == 0)
+ return 10;
+ // Just text; treat it the same for convenience.
+ if (strcmp(mime_type, "text/plain;charset=utf-8") == 0)
+ return 5;
+ if (strcmp(mime_type, "text/plain") == 0)
+ return 4;
+ if (strcmp(mime_type, "text") == 0)
+ return 0;
+ return -1;
+}
diff --git a/input/event.h b/input/event.h
new file mode 100644
index 0000000..1e2149b
--- /dev/null
+++ b/input/event.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+#ifndef MP_INPUT_EVENT_H_
+#define MP_INPUT_EVENT_H_
+
+#include "misc/bstr.h"
+
+struct input_ctx;
+
+enum mp_dnd_action {
+ DND_REPLACE,
+ DND_APPEND,
+};
+
+// Enqueue files for playback after drag and drop
+void mp_event_drop_files(struct input_ctx *ictx, int num_files, char **files,
+ enum mp_dnd_action append);
+
+// Drop data in a specific format (identified by the mimetype).
+// Returns <0 on error, ==0 if data was ok but empty, >0 on success.
+int mp_event_drop_mime_data(struct input_ctx *ictx, const char *mime_type,
+ bstr data, enum mp_dnd_action append);
+
+// Many drag & drop APIs support multiple mime types, and this function returns
+// whether a type is preferred (higher integer score), or supported (scores
+// below 0 indicate unsupported types).
+int mp_event_get_mime_type_score(struct input_ctx *ictx, const char *mime_type);
+
+#endif
diff --git a/input/input.c b/input/input.c
new file mode 100644
index 0000000..b8d12aa
--- /dev/null
+++ b/input/input.c
@@ -0,0 +1,1695 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <math.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <fcntl.h>
+#include <assert.h>
+
+#include "osdep/io.h"
+#include "misc/rendezvous.h"
+
+#include "input.h"
+#include "keycodes.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "mpv_talloc.h"
+#include "options/options.h"
+#include "misc/bstr.h"
+#include "misc/node.h"
+#include "stream/stream.h"
+#include "common/common.h"
+
+#if HAVE_COCOA
+#include "osdep/macosx_events.h"
+#endif
+
+#define input_lock(ictx) mp_mutex_lock(&ictx->mutex)
+#define input_unlock(ictx) mp_mutex_unlock(&ictx->mutex)
+
+#define MP_MAX_KEY_DOWN 4
+
+struct cmd_bind {
+ int keys[MP_MAX_KEY_DOWN];
+ int num_keys;
+ char *cmd;
+ char *location; // filename/line number of definition
+ char *desc; // human readable description
+ bool is_builtin;
+ struct cmd_bind_section *owner;
+};
+
+struct cmd_bind_section {
+ char *owner;
+ struct cmd_bind *binds;
+ int num_binds;
+ char *section;
+ struct mp_rect mouse_area; // set at runtime, if at all
+ bool mouse_area_set; // mouse_area is valid and should be tested
+};
+
+#define MP_MAX_SOURCES 10
+
+#define MAX_ACTIVE_SECTIONS 50
+
+struct active_section {
+ char *name;
+ int flags;
+};
+
+struct cmd_queue {
+ struct mp_cmd *first;
+};
+
+struct wheel_state {
+ double dead_zone_accum;
+ double unit_accum;
+};
+
+struct input_ctx {
+ mp_mutex mutex;
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct m_config_cache *opts_cache;
+ struct input_opts *opts;
+
+ bool using_ar;
+ bool using_cocoa_media_keys;
+
+ // Autorepeat stuff
+ short ar_state;
+ int64_t last_ar;
+
+ // history of key downs - the newest is in position 0
+ int key_history[MP_MAX_KEY_DOWN];
+ // key code of the last key that triggered MP_KEY_STATE_DOWN
+ int last_key_down;
+ int64_t last_key_down_time;
+ struct mp_cmd *current_down_cmd;
+
+ int last_doubleclick_key_down;
+ double last_doubleclick_time;
+
+ // Mouse position on the consumer side (as command.c sees it)
+ int mouse_x, mouse_y;
+ int mouse_hover; // updated on mouse-enter/leave
+ char *mouse_section; // last section to receive mouse event
+
+ // Mouse position on the producer side (as the VO sees it)
+ // Unlike mouse_x/y, this can be used to resolve mouse click bindings.
+ int mouse_vo_x, mouse_vo_y;
+
+ bool mouse_mangle, mouse_src_mangle;
+ struct mp_rect mouse_src, mouse_dst;
+
+ // Wheel state (MP_WHEEL_*)
+ struct wheel_state wheel_state_y; // MP_WHEEL_UP/MP_WHEEL_DOWN
+ struct wheel_state wheel_state_x; // MP_WHEEL_LEFT/MP_WHEEL_RIGHT
+ struct wheel_state *wheel_current; // The direction currently being scrolled
+ double last_wheel_time; // mp_time_sec() of the last wheel event
+
+ // List of command binding sections
+ struct cmd_bind_section **sections;
+ int num_sections;
+
+ // List currently active command sections
+ struct active_section active_sections[MAX_ACTIVE_SECTIONS];
+ int num_active_sections;
+
+ unsigned int mouse_event_counter;
+
+ struct mp_input_src *sources[MP_MAX_SOURCES];
+ int num_sources;
+
+ struct cmd_queue cmd_queue;
+
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_ctx;
+};
+
+static int parse_config(struct input_ctx *ictx, bool builtin, bstr data,
+ const char *location, const char *restrict_section);
+static void close_input_sources(struct input_ctx *ictx);
+
+#define OPT_BASE_STRUCT struct input_opts
+struct input_opts {
+ char *config_file;
+ int doubleclick_time;
+ // Maximum number of queued commands from keypresses (limit to avoid
+ // repeated slow commands piling up)
+ int key_fifo_size;
+ // Autorepeat config (be aware of mp_input_set_repeat_info())
+ int ar_delay;
+ int ar_rate;
+ bool use_alt_gr;
+ bool use_gamepad;
+ bool use_media_keys;
+ bool default_bindings;
+ bool builtin_bindings;
+ bool enable_mouse_movements;
+ bool vo_key_input;
+ bool test;
+ bool allow_win_drag;
+};
+
+const struct m_sub_options input_config = {
+ .opts = (const m_option_t[]) {
+ {"input-conf", OPT_STRING(config_file), .flags = M_OPT_FILE},
+ {"input-ar-delay", OPT_INT(ar_delay)},
+ {"input-ar-rate", OPT_INT(ar_rate)},
+ {"input-keylist", OPT_PRINT(mp_print_key_list)},
+ {"input-cmdlist", OPT_PRINT(mp_print_cmd_list)},
+ {"input-default-bindings", OPT_BOOL(default_bindings)},
+ {"input-builtin-bindings", OPT_BOOL(builtin_bindings)},
+ {"input-test", OPT_BOOL(test)},
+ {"input-doubleclick-time", OPT_INT(doubleclick_time),
+ M_RANGE(0, 1000)},
+ {"input-right-alt-gr", OPT_BOOL(use_alt_gr)},
+ {"input-key-fifo-size", OPT_INT(key_fifo_size), M_RANGE(2, 65000)},
+ {"input-cursor", OPT_BOOL(enable_mouse_movements)},
+ {"input-vo-keyboard", OPT_BOOL(vo_key_input)},
+ {"input-media-keys", OPT_BOOL(use_media_keys)},
+#if HAVE_SDL2_GAMEPAD
+ {"input-gamepad", OPT_BOOL(use_gamepad)},
+#endif
+ {"window-dragging", OPT_BOOL(allow_win_drag)},
+ {0}
+ },
+ .size = sizeof(struct input_opts),
+ .defaults = &(const struct input_opts){
+ .key_fifo_size = 7,
+ .doubleclick_time = 300,
+ .ar_delay = 200,
+ .ar_rate = 40,
+ .use_alt_gr = true,
+ .enable_mouse_movements = true,
+ .use_media_keys = true,
+ .default_bindings = true,
+ .builtin_bindings = true,
+ .vo_key_input = true,
+ .allow_win_drag = true,
+ },
+ .change_flags = UPDATE_INPUT,
+};
+
+static const char builtin_input_conf[] =
+#include "etc/input.conf.inc"
+;
+
+static bool test_rect(struct mp_rect *rc, int x, int y)
+{
+ return x >= rc->x0 && y >= rc->y0 && x < rc->x1 && y < rc->y1;
+}
+
+static int queue_count_cmds(struct cmd_queue *queue)
+{
+ int res = 0;
+ for (struct mp_cmd *cmd = queue->first; cmd; cmd = cmd->queue_next)
+ res++;
+ return res;
+}
+
+static void queue_remove(struct cmd_queue *queue, struct mp_cmd *cmd)
+{
+ struct mp_cmd **p_prev = &queue->first;
+ while (*p_prev != cmd) {
+ p_prev = &(*p_prev)->queue_next;
+ }
+ // if this fails, cmd was not in the queue
+ assert(*p_prev == cmd);
+ *p_prev = cmd->queue_next;
+}
+
+static struct mp_cmd *queue_remove_head(struct cmd_queue *queue)
+{
+ struct mp_cmd *ret = queue->first;
+ if (ret)
+ queue_remove(queue, ret);
+ return ret;
+}
+
+static void queue_add_tail(struct cmd_queue *queue, struct mp_cmd *cmd)
+{
+ struct mp_cmd **p_prev = &queue->first;
+ while (*p_prev)
+ p_prev = &(*p_prev)->queue_next;
+ *p_prev = cmd;
+ cmd->queue_next = NULL;
+}
+
+static struct mp_cmd *queue_peek_tail(struct cmd_queue *queue)
+{
+ struct mp_cmd *cur = queue->first;
+ while (cur && cur->queue_next)
+ cur = cur->queue_next;
+ return cur;
+}
+
+static void append_bind_info(struct input_ctx *ictx, char **pmsg,
+ struct cmd_bind *bind)
+{
+ char *msg = *pmsg;
+ struct mp_cmd *cmd = mp_input_parse_cmd(ictx, bstr0(bind->cmd),
+ bind->location);
+ char *stripped = cmd ? cmd->original : bind->cmd;
+ msg = talloc_asprintf_append(msg, " '%s'", stripped);
+ if (!cmd)
+ msg = talloc_asprintf_append(msg, " (invalid)");
+ if (strcmp(bind->owner->section, "default") != 0)
+ msg = talloc_asprintf_append(msg, " in section {%s}",
+ bind->owner->section);
+ msg = talloc_asprintf_append(msg, " in %s", bind->location);
+ if (bind->is_builtin)
+ msg = talloc_asprintf_append(msg, " (default)");
+ talloc_free(cmd);
+ *pmsg = msg;
+}
+
+static mp_cmd_t *handle_test(struct input_ctx *ictx, int code)
+{
+ if (code == MP_KEY_CLOSE_WIN) {
+ MP_WARN(ictx,
+ "CLOSE_WIN was received. This pseudo key can be remapped too,\n"
+ "but --input-test will always quit when receiving it.\n");
+ const char *args[] = {"quit", NULL};
+ mp_cmd_t *res = mp_input_parse_cmd_strv(ictx->log, args);
+ return res;
+ }
+
+ char *key_buf = mp_input_get_key_combo_name(&code, 1);
+ char *msg = talloc_asprintf(NULL, "Key %s is bound to:\n", key_buf);
+ talloc_free(key_buf);
+
+ int count = 0;
+ for (int n = 0; n < ictx->num_sections; n++) {
+ struct cmd_bind_section *bs = ictx->sections[n];
+
+ for (int i = 0; i < bs->num_binds; i++) {
+ if (bs->binds[i].num_keys && bs->binds[i].keys[0] == code) {
+ count++;
+ if (count > 1)
+ msg = talloc_asprintf_append(msg, "\n");
+ msg = talloc_asprintf_append(msg, "%d. ", count);
+ append_bind_info(ictx, &msg, &bs->binds[i]);
+ }
+ }
+ }
+
+ if (!count)
+ msg = talloc_asprintf_append(msg, "(nothing)");
+
+ MP_INFO(ictx, "%s\n", msg);
+ const char *args[] = {"show-text", msg, NULL};
+ mp_cmd_t *res = mp_input_parse_cmd_strv(ictx->log, args);
+ talloc_free(msg);
+ return res;
+}
+
+static struct cmd_bind_section *find_section(struct input_ctx *ictx,
+ bstr section)
+{
+ for (int n = 0; n < ictx->num_sections; n++) {
+ struct cmd_bind_section *bs = ictx->sections[n];
+ if (bstr_equals0(section, bs->section))
+ return bs;
+ }
+ return NULL;
+}
+
+static struct cmd_bind_section *get_bind_section(struct input_ctx *ictx,
+ bstr section)
+{
+ if (section.len == 0)
+ section = bstr0("default");
+ struct cmd_bind_section *bind_section = find_section(ictx, section);
+ if (bind_section)
+ return bind_section;
+ bind_section = talloc_ptrtype(ictx, bind_section);
+ *bind_section = (struct cmd_bind_section) {
+ .section = bstrdup0(bind_section, section),
+ .mouse_area = {INT_MIN, INT_MIN, INT_MAX, INT_MAX},
+ .mouse_area_set = true,
+ };
+ MP_TARRAY_APPEND(ictx, ictx->sections, ictx->num_sections, bind_section);
+ return bind_section;
+}
+
+static void key_buf_add(int *buf, int code)
+{
+ for (int n = MP_MAX_KEY_DOWN - 1; n > 0; n--)
+ buf[n] = buf[n - 1];
+ buf[0] = code;
+}
+
+static struct cmd_bind *find_bind_for_key_section(struct input_ctx *ictx,
+ char *section, int code)
+{
+ struct cmd_bind_section *bs = get_bind_section(ictx, bstr0(section));
+
+ if (!bs->num_binds)
+ return NULL;
+
+ int keys[MP_MAX_KEY_DOWN];
+ memcpy(keys, ictx->key_history, sizeof(keys));
+ key_buf_add(keys, code);
+
+ struct cmd_bind *best = NULL;
+
+ // Prefer user-defined keys over builtin bindings
+ for (int builtin = 0; builtin < 2; builtin++) {
+ if (builtin && !ictx->opts->default_bindings)
+ break;
+ if (best)
+ break;
+ for (int n = 0; n < bs->num_binds; n++) {
+ if (bs->binds[n].is_builtin == (bool)builtin) {
+ struct cmd_bind *b = &bs->binds[n];
+ // we have: keys=[key2 key1 keyX ...]
+ // and: b->keys=[key1 key2] (and may be just a prefix)
+ for (int i = 0; i < b->num_keys; i++) {
+ if (b->keys[i] != keys[b->num_keys - 1 - i])
+ goto skip;
+ }
+ if (!best || b->num_keys >= best->num_keys)
+ best = b;
+ skip: ;
+ }
+ }
+ }
+ return best;
+}
+
+static struct cmd_bind *find_any_bind_for_key(struct input_ctx *ictx,
+ char *force_section, int code)
+{
+ if (force_section)
+ return find_bind_for_key_section(ictx, force_section, code);
+
+ bool use_mouse = MP_KEY_DEPENDS_ON_MOUSE_POS(code);
+
+ // First look whether a mouse section is capturing all mouse input
+ // exclusively (regardless of the active section stack order).
+ if (use_mouse && MP_KEY_IS_MOUSE_BTN_SINGLE(ictx->last_key_down)) {
+ struct cmd_bind *bind =
+ find_bind_for_key_section(ictx, ictx->mouse_section, code);
+ if (bind)
+ return bind;
+ }
+
+ struct cmd_bind *best_bind = NULL;
+ for (int i = ictx->num_active_sections - 1; i >= 0; i--) {
+ struct active_section *s = &ictx->active_sections[i];
+ struct cmd_bind *bind = find_bind_for_key_section(ictx, s->name, code);
+ if (bind) {
+ struct cmd_bind_section *bs = bind->owner;
+ if (!use_mouse || (bs->mouse_area_set && test_rect(&bs->mouse_area,
+ ictx->mouse_vo_x,
+ ictx->mouse_vo_y)))
+ {
+ if (!best_bind || (best_bind->is_builtin && !bind->is_builtin))
+ best_bind = bind;
+ }
+ }
+ if (s->flags & MP_INPUT_EXCLUSIVE)
+ break;
+ if (best_bind && (s->flags & MP_INPUT_ON_TOP))
+ break;
+ }
+
+ return best_bind;
+}
+
+static mp_cmd_t *get_cmd_from_keys(struct input_ctx *ictx, char *force_section,
+ int code)
+{
+ if (ictx->opts->test)
+ return handle_test(ictx, code);
+
+ struct cmd_bind *cmd = NULL;
+
+ if (MP_KEY_IS_UNICODE(code))
+ cmd = find_any_bind_for_key(ictx, force_section, MP_KEY_ANY_UNICODE);
+ if (!cmd)
+ cmd = find_any_bind_for_key(ictx, force_section, code);
+ if (!cmd)
+ cmd = find_any_bind_for_key(ictx, force_section, MP_KEY_UNMAPPED);
+ if (!cmd) {
+ if (code == MP_KEY_CLOSE_WIN)
+ return mp_input_parse_cmd_strv(ictx->log, (const char*[]){"quit", 0});
+ int msgl = MSGL_WARN;
+ if (MP_KEY_IS_MOUSE_MOVE(code))
+ msgl = MSGL_TRACE;
+ char *key_buf = mp_input_get_key_combo_name(&code, 1);
+ MP_MSG(ictx, msgl, "No key binding found for key '%s'.\n", key_buf);
+ talloc_free(key_buf);
+ return NULL;
+ }
+ mp_cmd_t *ret = mp_input_parse_cmd(ictx, bstr0(cmd->cmd), cmd->location);
+ if (ret) {
+ ret->input_section = cmd->owner->section;
+ ret->key_name = talloc_steal(ret, mp_input_get_key_combo_name(&code, 1));
+ MP_TRACE(ictx, "key '%s' -> '%s' in '%s'\n",
+ ret->key_name, cmd->cmd, ret->input_section);
+ if (MP_KEY_IS_UNICODE(code)) {
+ bstr text = {0};
+ mp_append_utf8_bstr(ret, &text, code);
+ ret->key_text = text.start;
+ }
+ ret->is_mouse_button = code & MP_KEY_EMIT_ON_UP;
+ } else {
+ char *key_buf = mp_input_get_key_combo_name(&code, 1);
+ MP_ERR(ictx, "Invalid command for key binding '%s': '%s'\n",
+ key_buf, cmd->cmd);
+ talloc_free(key_buf);
+ }
+ return ret;
+}
+
+static void update_mouse_section(struct input_ctx *ictx)
+{
+ struct cmd_bind *bind =
+ find_any_bind_for_key(ictx, NULL, MP_KEY_MOUSE_MOVE);
+
+ char *new_section = bind ? bind->owner->section : "default";
+
+ char *old = ictx->mouse_section;
+ ictx->mouse_section = new_section;
+
+ if (strcmp(old, ictx->mouse_section) != 0) {
+ MP_TRACE(ictx, "input: switch section %s -> %s\n",
+ old, ictx->mouse_section);
+ mp_input_queue_cmd(ictx, get_cmd_from_keys(ictx, old, MP_KEY_MOUSE_LEAVE));
+ }
+}
+
+// Called when the currently held-down key is released. This (usually) sends
+// the a key-up version of the command associated with the keys that were held
+// down.
+// If the drop_current parameter is set to true, then don't send the key-up
+// command. Unless we've already sent a key-down event, in which case the
+// input receiver (the player) must get a key-up event, or it would get stuck
+// thinking a key is still held down.
+static void release_down_cmd(struct input_ctx *ictx, bool drop_current)
+{
+ if (ictx->current_down_cmd && ictx->current_down_cmd->emit_on_up &&
+ (!drop_current || ictx->current_down_cmd->def->on_updown))
+ {
+ memset(ictx->key_history, 0, sizeof(ictx->key_history));
+ ictx->current_down_cmd->is_up = true;
+ mp_input_queue_cmd(ictx, ictx->current_down_cmd);
+ } else {
+ talloc_free(ictx->current_down_cmd);
+ }
+ ictx->current_down_cmd = NULL;
+ ictx->last_key_down = 0;
+ ictx->last_key_down_time = 0;
+ ictx->ar_state = -1;
+ update_mouse_section(ictx);
+}
+
+// We don't want the append to the command queue indefinitely, because that
+// could lead to situations where recovery would take too long.
+static bool should_drop_cmd(struct input_ctx *ictx, struct mp_cmd *cmd)
+{
+ struct cmd_queue *queue = &ictx->cmd_queue;
+ return queue_count_cmds(queue) >= ictx->opts->key_fifo_size;
+}
+
+static struct mp_cmd *resolve_key(struct input_ctx *ictx, int code)
+{
+ update_mouse_section(ictx);
+ struct mp_cmd *cmd = get_cmd_from_keys(ictx, NULL, code);
+ key_buf_add(ictx->key_history, code);
+ if (cmd && !cmd->def->is_ignore && !should_drop_cmd(ictx, cmd))
+ return cmd;
+ talloc_free(cmd);
+ return NULL;
+}
+
+static void interpret_key(struct input_ctx *ictx, int code, double scale,
+ int scale_units)
+{
+ int state = code & (MP_KEY_STATE_DOWN | MP_KEY_STATE_UP);
+ code = code & ~(unsigned)state;
+
+ if (mp_msg_test(ictx->log, MSGL_TRACE)) {
+ char *key = mp_input_get_key_name(code);
+ MP_TRACE(ictx, "key code=%#x '%s'%s%s\n",
+ code, key, (state & MP_KEY_STATE_DOWN) ? " down" : "",
+ (state & MP_KEY_STATE_UP) ? " up" : "");
+ talloc_free(key);
+ }
+
+ if (MP_KEY_DEPENDS_ON_MOUSE_POS(code & ~MP_KEY_MODIFIER_MASK)) {
+ ictx->mouse_event_counter++;
+ mp_input_wakeup(ictx);
+ }
+
+ struct mp_cmd *cmd = NULL;
+
+ if (state == MP_KEY_STATE_DOWN) {
+ // Protect against VOs which send STATE_DOWN with autorepeat
+ if (ictx->last_key_down == code)
+ return;
+ // Cancel current down-event (there can be only one)
+ release_down_cmd(ictx, true);
+ cmd = resolve_key(ictx, code);
+ if (cmd) {
+ cmd->is_up_down = true;
+ cmd->emit_on_up = (code & MP_KEY_EMIT_ON_UP) || cmd->def->on_updown;
+ ictx->current_down_cmd = mp_cmd_clone(cmd);
+ }
+ ictx->last_key_down = code;
+ ictx->last_key_down_time = mp_time_ns();
+ ictx->ar_state = 0;
+ mp_input_wakeup(ictx); // possibly start timer for autorepeat
+ } else if (state == MP_KEY_STATE_UP) {
+ // Most VOs send RELEASE_ALL anyway
+ release_down_cmd(ictx, false);
+ } else {
+ // Press of key with no separate down/up events
+ // Mixing press events and up/down with the same key is not supported,
+ // and input sources shouldn't do this, but can happen anyway if
+ // multiple input sources interfere with each others.
+ if (ictx->last_key_down == code)
+ release_down_cmd(ictx, false);
+ cmd = resolve_key(ictx, code);
+ }
+
+ if (!cmd)
+ return;
+
+ // Don't emit a command on key-down if the key is designed to emit commands
+ // on key-up (like mouse buttons). Also, if the command specifically should
+ // be sent both on key down and key up, still emit the command.
+ if (cmd->emit_on_up && !cmd->def->on_updown) {
+ talloc_free(cmd);
+ return;
+ }
+
+ memset(ictx->key_history, 0, sizeof(ictx->key_history));
+
+ if (mp_input_is_scalable_cmd(cmd)) {
+ cmd->scale = scale;
+ cmd->scale_units = scale_units;
+ mp_input_queue_cmd(ictx, cmd);
+ } else {
+ // Non-scalable commands won't understand cmd->scale, so synthesize
+ // multiple commands with cmd->scale = 1
+ cmd->scale = 1;
+ cmd->scale_units = 1;
+ // Avoid spamming the player with too many commands
+ scale_units = MPMIN(scale_units, 20);
+ for (int i = 0; i < scale_units - 1; i++)
+ mp_input_queue_cmd(ictx, mp_cmd_clone(cmd));
+ if (scale_units)
+ mp_input_queue_cmd(ictx, cmd);
+ }
+}
+
+// Pre-processing for MP_WHEEL_* events. If this returns false, the caller
+// should discard the event.
+static bool process_wheel(struct input_ctx *ictx, int code, double *scale,
+ int *scale_units)
+{
+ // Size of the deadzone in scroll units. The user must scroll at least this
+ // much in any direction before their scroll is registered.
+ static const double DEADZONE_DIST = 0.125;
+ // The deadzone accumulator is reset if no scrolls happened in this many
+ // seconds, eg. the user is assumed to have finished scrolling.
+ static const double DEADZONE_SCROLL_TIME = 0.2;
+ // The scale_units accumulator is reset if no scrolls happened in this many
+ // seconds. This value should be fairly large, so commands will still be
+ // sent when the user scrolls slowly.
+ static const double UNIT_SCROLL_TIME = 0.5;
+
+ // Determine which direction is being scrolled
+ double dir;
+ struct wheel_state *state;
+ switch (code) {
+ case MP_WHEEL_UP: dir = -1; state = &ictx->wheel_state_y; break;
+ case MP_WHEEL_DOWN: dir = +1; state = &ictx->wheel_state_y; break;
+ case MP_WHEEL_LEFT: dir = -1; state = &ictx->wheel_state_x; break;
+ case MP_WHEEL_RIGHT: dir = +1; state = &ictx->wheel_state_x; break;
+ default:
+ return true;
+ }
+
+ // Reset accumulators if it's determined that the user finished scrolling
+ double now = mp_time_sec();
+ if (now > ictx->last_wheel_time + DEADZONE_SCROLL_TIME) {
+ ictx->wheel_current = NULL;
+ ictx->wheel_state_y.dead_zone_accum = 0;
+ ictx->wheel_state_x.dead_zone_accum = 0;
+ }
+ if (now > ictx->last_wheel_time + UNIT_SCROLL_TIME) {
+ ictx->wheel_state_y.unit_accum = 0;
+ ictx->wheel_state_x.unit_accum = 0;
+ }
+ ictx->last_wheel_time = now;
+
+ // Process wheel deadzone. A lot of touchpad drivers don't filter scroll
+ // input, which makes it difficult for the user to send WHEEL_UP/DOWN
+ // without accidentally triggering WHEEL_LEFT/RIGHT. We try to fix this by
+ // implementing a deadzone. When the value of either direction breaks out
+ // of the deadzone, events from the other direction will be ignored until
+ // the user finishes scrolling.
+ if (ictx->wheel_current == NULL) {
+ state->dead_zone_accum += *scale * dir;
+ if (state->dead_zone_accum * dir > DEADZONE_DIST) {
+ ictx->wheel_current = state;
+ *scale = state->dead_zone_accum * dir;
+ }
+ }
+ if (ictx->wheel_current != state)
+ return false;
+
+ // Determine scale_units. This is incremented every time the accumulated
+ // scale value crosses 1.0. Non-scalable input commands will be ran that
+ // many times.
+ state->unit_accum += *scale * dir;
+ *scale_units = trunc(state->unit_accum * dir);
+ state->unit_accum -= *scale_units * dir;
+ return true;
+}
+
+static void mp_input_feed_key(struct input_ctx *ictx, int code, double scale,
+ bool force_mouse)
+{
+ struct input_opts *opts = ictx->opts;
+
+ code = mp_normalize_keycode(code);
+ int unmod = code & ~MP_KEY_MODIFIER_MASK;
+ if (code == MP_INPUT_RELEASE_ALL) {
+ MP_TRACE(ictx, "release all\n");
+ release_down_cmd(ictx, false);
+ return;
+ }
+ if (!opts->enable_mouse_movements && MP_KEY_IS_MOUSE(unmod) && !force_mouse)
+ return;
+ if (unmod == MP_KEY_MOUSE_LEAVE || unmod == MP_KEY_MOUSE_ENTER) {
+ ictx->mouse_hover = unmod == MP_KEY_MOUSE_ENTER;
+ update_mouse_section(ictx);
+
+ mp_cmd_t *cmd = get_cmd_from_keys(ictx, NULL, code);
+ if (!cmd) // queue dummy cmd so that mouse-pos can notify observers
+ cmd = mp_input_parse_cmd(ictx, bstr0("ignore"), "<internal>");
+ mp_input_queue_cmd(ictx, cmd);
+ return;
+ }
+ double now = mp_time_sec();
+ // ignore system-doubleclick if we generate these events ourselves
+ if (!force_mouse && opts->doubleclick_time && MP_KEY_IS_MOUSE_BTN_DBL(unmod))
+ return;
+ int units = 1;
+ if (MP_KEY_IS_WHEEL(unmod) && !process_wheel(ictx, unmod, &scale, &units))
+ return;
+ interpret_key(ictx, code, scale, units);
+ if (code & MP_KEY_STATE_DOWN) {
+ code &= ~MP_KEY_STATE_DOWN;
+ if (ictx->last_doubleclick_key_down == code &&
+ now - ictx->last_doubleclick_time < opts->doubleclick_time / 1000.0)
+ {
+ if (code >= MP_MBTN_LEFT && code <= MP_MBTN_RIGHT) {
+ interpret_key(ictx, code - MP_MBTN_BASE + MP_MBTN_DBL_BASE,
+ 1, 1);
+ }
+ }
+ ictx->last_doubleclick_key_down = code;
+ ictx->last_doubleclick_time = now;
+ }
+}
+
+void mp_input_put_key(struct input_ctx *ictx, int code)
+{
+ input_lock(ictx);
+ mp_input_feed_key(ictx, code, 1, false);
+ input_unlock(ictx);
+}
+
+void mp_input_put_key_artificial(struct input_ctx *ictx, int code)
+{
+ input_lock(ictx);
+ mp_input_feed_key(ictx, code, 1, true);
+ input_unlock(ictx);
+}
+
+void mp_input_put_key_utf8(struct input_ctx *ictx, int mods, struct bstr t)
+{
+ while (t.len) {
+ int code = bstr_decode_utf8(t, &t);
+ if (code < 0)
+ break;
+ mp_input_put_key(ictx, code | mods);
+ }
+}
+
+void mp_input_put_wheel(struct input_ctx *ictx, int direction, double value)
+{
+ if (value == 0.0)
+ return;
+ input_lock(ictx);
+ mp_input_feed_key(ictx, direction, value, false);
+ input_unlock(ictx);
+}
+
+void mp_input_set_mouse_transform(struct input_ctx *ictx, struct mp_rect *dst,
+ struct mp_rect *src)
+{
+ input_lock(ictx);
+ ictx->mouse_mangle = dst || src;
+ if (ictx->mouse_mangle) {
+ ictx->mouse_dst = *dst;
+ ictx->mouse_src_mangle = !!src;
+ if (ictx->mouse_src_mangle)
+ ictx->mouse_src = *src;
+ }
+ input_unlock(ictx);
+}
+
+bool mp_input_mouse_enabled(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ bool r = ictx->opts->enable_mouse_movements;
+ input_unlock(ictx);
+ return r;
+}
+
+bool mp_input_vo_keyboard_enabled(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ bool r = ictx->opts->vo_key_input;
+ input_unlock(ictx);
+ return r;
+}
+
+void mp_input_set_mouse_pos(struct input_ctx *ictx, int x, int y)
+{
+ input_lock(ictx);
+ if (ictx->opts->enable_mouse_movements)
+ mp_input_set_mouse_pos_artificial(ictx, x, y);
+ input_unlock(ictx);
+}
+
+void mp_input_set_mouse_pos_artificial(struct input_ctx *ictx, int x, int y)
+{
+ input_lock(ictx);
+ MP_TRACE(ictx, "mouse move %d/%d\n", x, y);
+
+ if (ictx->mouse_vo_x == x && ictx->mouse_vo_y == y) {
+ input_unlock(ictx);
+ return;
+ }
+
+ if (ictx->mouse_mangle) {
+ struct mp_rect *src = &ictx->mouse_src;
+ struct mp_rect *dst = &ictx->mouse_dst;
+ x = MPCLAMP(x, dst->x0, dst->x1) - dst->x0;
+ y = MPCLAMP(y, dst->y0, dst->y1) - dst->y0;
+ if (ictx->mouse_src_mangle) {
+ x = x * 1.0 / (dst->x1 - dst->x0) * (src->x1 - src->x0) + src->x0;
+ y = y * 1.0 / (dst->y1 - dst->y0) * (src->y1 - src->y0) + src->y0;
+ }
+ MP_TRACE(ictx, "-> %d/%d\n", x, y);
+ }
+
+ ictx->mouse_event_counter++;
+ ictx->mouse_vo_x = x;
+ ictx->mouse_vo_y = y;
+
+ update_mouse_section(ictx);
+ struct mp_cmd *cmd = get_cmd_from_keys(ictx, NULL, MP_KEY_MOUSE_MOVE);
+ if (!cmd)
+ cmd = mp_input_parse_cmd(ictx, bstr0("ignore"), "<internal>");
+
+ if (cmd) {
+ cmd->mouse_move = true;
+ cmd->mouse_x = x;
+ cmd->mouse_y = y;
+ if (should_drop_cmd(ictx, cmd)) {
+ talloc_free(cmd);
+ } else {
+ // Coalesce with previous mouse move events (i.e. replace it)
+ struct mp_cmd *tail = queue_peek_tail(&ictx->cmd_queue);
+ if (tail && tail->mouse_move) {
+ queue_remove(&ictx->cmd_queue, tail);
+ talloc_free(tail);
+ }
+ mp_input_queue_cmd(ictx, cmd);
+ }
+ }
+ input_unlock(ictx);
+}
+
+unsigned int mp_input_get_mouse_event_counter(struct input_ctx *ictx)
+{
+ // Make the frontend always display the mouse cursor (as long as it's not
+ // forced invisible) if mouse input is desired.
+ input_lock(ictx);
+ if (mp_input_test_mouse_active(ictx, ictx->mouse_x, ictx->mouse_y))
+ ictx->mouse_event_counter++;
+ int ret = ictx->mouse_event_counter;
+ input_unlock(ictx);
+ return ret;
+}
+
+// adjust min time to wait until next repeat event
+static void adjust_max_wait_time(struct input_ctx *ictx, double *time)
+{
+ struct input_opts *opts = ictx->opts;
+ if (ictx->last_key_down && opts->ar_rate > 0 && ictx->ar_state >= 0) {
+ *time = MPMIN(*time, 1.0 / opts->ar_rate);
+ *time = MPMIN(*time, opts->ar_delay / 1000.0);
+ }
+}
+
+int mp_input_queue_cmd(struct input_ctx *ictx, mp_cmd_t *cmd)
+{
+ input_lock(ictx);
+ if (cmd) {
+ queue_add_tail(&ictx->cmd_queue, cmd);
+ mp_input_wakeup(ictx);
+ }
+ input_unlock(ictx);
+ return 1;
+}
+
+static mp_cmd_t *check_autorepeat(struct input_ctx *ictx)
+{
+ struct input_opts *opts = ictx->opts;
+
+ // No input : autorepeat ?
+ if (opts->ar_rate <= 0 || !ictx->current_down_cmd || !ictx->last_key_down ||
+ (ictx->last_key_down & MP_NO_REPEAT_KEY) ||
+ !mp_input_is_repeatable_cmd(ictx->current_down_cmd))
+ ictx->ar_state = -1; // disable
+
+ if (ictx->ar_state >= 0) {
+ int64_t t = mp_time_ns();
+ if (ictx->last_ar + MP_TIME_S_TO_NS(2) < t)
+ ictx->last_ar = t;
+ // First time : wait delay
+ if (ictx->ar_state == 0
+ && (t - ictx->last_key_down_time) >= MP_TIME_MS_TO_NS(opts->ar_delay))
+ {
+ ictx->ar_state = 1;
+ ictx->last_ar = ictx->last_key_down_time + MP_TIME_MS_TO_NS(opts->ar_delay);
+ // Then send rate / sec event
+ } else if (ictx->ar_state == 1
+ && (t - ictx->last_ar) >= 1e9 / opts->ar_rate) {
+ ictx->last_ar += 1e9 / opts->ar_rate;
+ } else {
+ return NULL;
+ }
+ struct mp_cmd *ret = mp_cmd_clone(ictx->current_down_cmd);
+ ret->repeated = true;
+ return ret;
+ }
+ return NULL;
+}
+
+double mp_input_get_delay(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ double seconds = INFINITY;
+ adjust_max_wait_time(ictx, &seconds);
+ input_unlock(ictx);
+ return seconds;
+}
+
+void mp_input_wakeup(struct input_ctx *ictx)
+{
+ ictx->wakeup_cb(ictx->wakeup_ctx);
+}
+
+mp_cmd_t *mp_input_read_cmd(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ struct mp_cmd *ret = queue_remove_head(&ictx->cmd_queue);
+ if (!ret)
+ ret = check_autorepeat(ictx);
+ if (ret && ret->mouse_move) {
+ ictx->mouse_x = ret->mouse_x;
+ ictx->mouse_y = ret->mouse_y;
+ }
+ input_unlock(ictx);
+ return ret;
+}
+
+void mp_input_get_mouse_pos(struct input_ctx *ictx, int *x, int *y, int *hover)
+{
+ input_lock(ictx);
+ *x = ictx->mouse_x;
+ *y = ictx->mouse_y;
+ *hover = ictx->mouse_hover;
+ input_unlock(ictx);
+}
+
+// If name is NULL, return "default".
+// Return a statically allocated name of the section (i.e. return value never
+// gets deallocated).
+static char *normalize_section(struct input_ctx *ictx, char *name)
+{
+ return get_bind_section(ictx, bstr0(name))->section;
+}
+
+void mp_input_disable_section(struct input_ctx *ictx, char *name)
+{
+ input_lock(ictx);
+ name = normalize_section(ictx, name);
+
+ // Remove old section, or make sure it's on top if re-enabled
+ for (int i = ictx->num_active_sections - 1; i >= 0; i--) {
+ struct active_section *as = &ictx->active_sections[i];
+ if (strcmp(as->name, name) == 0) {
+ MP_TARRAY_REMOVE_AT(ictx->active_sections,
+ ictx->num_active_sections, i);
+ }
+ }
+ input_unlock(ictx);
+}
+
+void mp_input_enable_section(struct input_ctx *ictx, char *name, int flags)
+{
+ input_lock(ictx);
+ name = normalize_section(ictx, name);
+
+ mp_input_disable_section(ictx, name);
+
+ MP_TRACE(ictx, "enable section '%s'\n", name);
+
+ if (ictx->num_active_sections < MAX_ACTIVE_SECTIONS) {
+ int top = ictx->num_active_sections;
+ if (!(flags & MP_INPUT_ON_TOP)) {
+ // insert before the first top entry
+ for (top = 0; top < ictx->num_active_sections; top++) {
+ if (ictx->active_sections[top].flags & MP_INPUT_ON_TOP)
+ break;
+ }
+ for (int n = ictx->num_active_sections; n > top; n--)
+ ictx->active_sections[n] = ictx->active_sections[n - 1];
+ }
+ ictx->active_sections[top] = (struct active_section){name, flags};
+ ictx->num_active_sections++;
+ }
+
+ MP_TRACE(ictx, "active section stack:\n");
+ for (int n = 0; n < ictx->num_active_sections; n++) {
+ MP_TRACE(ictx, " %s %d\n", ictx->active_sections[n].name,
+ ictx->active_sections[n].flags);
+ }
+
+ input_unlock(ictx);
+}
+
+void mp_input_disable_all_sections(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ ictx->num_active_sections = 0;
+ input_unlock(ictx);
+}
+
+void mp_input_set_section_mouse_area(struct input_ctx *ictx, char *name,
+ int x0, int y0, int x1, int y1)
+{
+ input_lock(ictx);
+ struct cmd_bind_section *s = get_bind_section(ictx, bstr0(name));
+ s->mouse_area = (struct mp_rect){x0, y0, x1, y1};
+ s->mouse_area_set = x0 != x1 && y0 != y1;
+ input_unlock(ictx);
+}
+
+static bool test_mouse(struct input_ctx *ictx, int x, int y, int rej_flags)
+{
+ input_lock(ictx);
+ bool res = false;
+ for (int i = 0; i < ictx->num_active_sections; i++) {
+ struct active_section *as = &ictx->active_sections[i];
+ if (as->flags & rej_flags)
+ continue;
+ struct cmd_bind_section *s = get_bind_section(ictx, bstr0(as->name));
+ if (s->mouse_area_set && test_rect(&s->mouse_area, x, y)) {
+ res = true;
+ break;
+ }
+ }
+ input_unlock(ictx);
+ return res;
+}
+
+bool mp_input_test_mouse_active(struct input_ctx *ictx, int x, int y)
+{
+ return test_mouse(ictx, x, y, MP_INPUT_ALLOW_HIDE_CURSOR);
+}
+
+bool mp_input_test_dragging(struct input_ctx *ictx, int x, int y)
+{
+ input_lock(ictx);
+ bool r = !ictx->opts->allow_win_drag ||
+ test_mouse(ictx, x, y, MP_INPUT_ALLOW_VO_DRAGGING);
+ input_unlock(ictx);
+ return r;
+}
+
+static void bind_dealloc(struct cmd_bind *bind)
+{
+ talloc_free(bind->cmd);
+ talloc_free(bind->location);
+ talloc_free(bind->desc);
+}
+
+// builtin: if true, remove all builtin binds, else remove all user binds
+static void remove_binds(struct cmd_bind_section *bs, bool builtin)
+{
+ for (int n = bs->num_binds - 1; n >= 0; n--) {
+ if (bs->binds[n].is_builtin == builtin) {
+ bind_dealloc(&bs->binds[n]);
+ assert(bs->num_binds >= 1);
+ bs->binds[n] = bs->binds[bs->num_binds - 1];
+ bs->num_binds--;
+ }
+ }
+}
+
+void mp_input_define_section(struct input_ctx *ictx, char *name, char *location,
+ char *contents, bool builtin, char *owner)
+{
+ if (!name || !name[0])
+ return; // parse_config() changes semantics with restrict_section==empty
+ input_lock(ictx);
+ // Delete:
+ struct cmd_bind_section *bs = get_bind_section(ictx, bstr0(name));
+ if ((!bs->owner || (owner && strcmp(bs->owner, owner) != 0)) &&
+ strcmp(bs->section, "default") != 0)
+ {
+ talloc_free(bs->owner);
+ bs->owner = talloc_strdup(bs, owner);
+ }
+ remove_binds(bs, builtin);
+ if (contents && contents[0]) {
+ // Redefine:
+ parse_config(ictx, builtin, bstr0(contents), location, name);
+ } else {
+ // Disable:
+ mp_input_disable_section(ictx, name);
+ }
+ input_unlock(ictx);
+}
+
+void mp_input_remove_sections_by_owner(struct input_ctx *ictx, char *owner)
+{
+ input_lock(ictx);
+ for (int n = 0; n < ictx->num_sections; n++) {
+ struct cmd_bind_section *bs = ictx->sections[n];
+ if (bs->owner && owner && strcmp(bs->owner, owner) == 0) {
+ mp_input_disable_section(ictx, bs->section);
+ remove_binds(bs, false);
+ remove_binds(bs, true);
+ }
+ }
+ input_unlock(ictx);
+}
+
+static bool bind_matches_key(struct cmd_bind *bind, int num_keys, const int *keys)
+{
+ if (bind->num_keys != num_keys)
+ return false;
+ for (int i = 0; i < num_keys; i++) {
+ if (bind->keys[i] != keys[i])
+ return false;
+ }
+ return true;
+}
+
+static void bind_keys(struct input_ctx *ictx, bool builtin, bstr section,
+ const int *keys, int num_keys, bstr command,
+ const char *loc, const char *desc)
+{
+ struct cmd_bind_section *bs = get_bind_section(ictx, section);
+ struct cmd_bind *bind = NULL;
+
+ assert(num_keys <= MP_MAX_KEY_DOWN);
+
+ for (int n = 0; n < bs->num_binds; n++) {
+ struct cmd_bind *b = &bs->binds[n];
+ if (bind_matches_key(b, num_keys, keys) && b->is_builtin == builtin) {
+ bind = b;
+ break;
+ }
+ }
+
+ if (!bind) {
+ struct cmd_bind empty = {{0}};
+ MP_TARRAY_APPEND(bs, bs->binds, bs->num_binds, empty);
+ bind = &bs->binds[bs->num_binds - 1];
+ }
+
+ bind_dealloc(bind);
+
+ *bind = (struct cmd_bind) {
+ .cmd = bstrdup0(bs->binds, command),
+ .location = talloc_strdup(bs->binds, loc),
+ .desc = talloc_strdup(bs->binds, desc),
+ .owner = bs,
+ .is_builtin = builtin,
+ .num_keys = num_keys,
+ };
+ memcpy(bind->keys, keys, num_keys * sizeof(bind->keys[0]));
+ if (mp_msg_test(ictx->log, MSGL_DEBUG)) {
+ char *s = mp_input_get_key_combo_name(keys, num_keys);
+ MP_TRACE(ictx, "add: section='%s' key='%s'%s cmd='%s' location='%s'\n",
+ bind->owner->section, s, bind->is_builtin ? " builtin" : "",
+ bind->cmd, bind->location);
+ talloc_free(s);
+ }
+}
+
+// restrict_section: every entry is forced to this section name
+// if NULL, load normally and allow any sections
+static int parse_config(struct input_ctx *ictx, bool builtin, bstr data,
+ const char *location, const char *restrict_section)
+{
+ int n_binds = 0;
+ int line_no = 0;
+ char *cur_loc = NULL;
+
+ while (data.len) {
+ line_no++;
+ if (cur_loc)
+ talloc_free(cur_loc);
+ cur_loc = talloc_asprintf(NULL, "%s:%d", location, line_no);
+
+ bstr line = bstr_strip_linebreaks(bstr_getline(data, &data));
+ line = bstr_lstrip(line);
+ if (line.len == 0 || bstr_startswith0(line, "#"))
+ continue;
+ if (bstr_eatstart0(&line, "default-bindings ")) {
+ bstr orig = line;
+ bstr_split_tok(line, "#", &line, &(bstr){0});
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "start")) {
+ builtin = true;
+ } else {
+ MP_ERR(ictx, "Broken line: %.*s at %s\n", BSTR_P(orig), cur_loc);
+ }
+ continue;
+ }
+ struct bstr command;
+ // Find the key name starting a line
+ struct bstr keyname = bstr_split(line, WHITESPACE, &command);
+ command = bstr_strip(command);
+ if (command.len == 0) {
+ MP_ERR(ictx, "Unfinished key binding: %.*s at %s\n", BSTR_P(line),
+ cur_loc);
+ continue;
+ }
+ char *name = bstrdup0(NULL, keyname);
+ int keys[MP_MAX_KEY_DOWN];
+ int num_keys = 0;
+ if (!mp_input_get_keys_from_string(name, MP_MAX_KEY_DOWN, &num_keys, keys))
+ {
+ talloc_free(name);
+ MP_ERR(ictx, "Unknown key '%.*s' at %s\n", BSTR_P(keyname), cur_loc);
+ continue;
+ }
+ talloc_free(name);
+
+ bstr section = bstr0(restrict_section);
+ if (!section.len) {
+ if (bstr_startswith0(command, "{")) {
+ int p = bstrchr(command, '}');
+ if (p != -1) {
+ section = bstr_strip(bstr_splice(command, 1, p));
+ command = bstr_lstrip(bstr_cut(command, p + 1));
+ }
+ }
+ }
+
+ // Print warnings if invalid commands are encountered.
+ struct mp_cmd *cmd = mp_input_parse_cmd(ictx, command, cur_loc);
+ const char *desc = NULL;
+ if (cmd) {
+ desc = cmd->desc;
+ command = bstr0(cmd->original);
+ }
+
+ bind_keys(ictx, builtin, section, keys, num_keys, command, cur_loc, desc);
+ n_binds++;
+
+ talloc_free(cmd);
+ }
+
+ talloc_free(cur_loc);
+
+ return n_binds;
+}
+
+static int parse_config_file(struct input_ctx *ictx, char *file, bool warn)
+{
+ int r = 0;
+ void *tmp = talloc_new(NULL);
+ stream_t *s = NULL;
+
+ file = mp_get_user_path(tmp, ictx->global, file);
+
+ s = stream_create(file, STREAM_ORIGIN_DIRECT | STREAM_READ, NULL, ictx->global);
+ if (!s) {
+ MP_ERR(ictx, "Can't open input config file %s.\n", file);
+ goto done;
+ }
+ stream_skip_bom(s);
+ bstr data = stream_read_complete(s, tmp, 1000000);
+ if (data.start) {
+ MP_VERBOSE(ictx, "Parsing input config file %s\n", file);
+ int num = parse_config(ictx, false, data, file, NULL);
+ MP_VERBOSE(ictx, "Input config file %s parsed: %d binds\n", file, num);
+ r = 1;
+ } else {
+ MP_ERR(ictx, "Error reading input config file %s\n", file);
+ }
+
+done:
+ free_stream(s);
+ talloc_free(tmp);
+ return r;
+}
+
+struct input_ctx *mp_input_init(struct mpv_global *global,
+ void (*wakeup_cb)(void *ctx),
+ void *wakeup_ctx)
+{
+
+ struct input_ctx *ictx = talloc_ptrtype(NULL, ictx);
+ *ictx = (struct input_ctx){
+ .global = global,
+ .ar_state = -1,
+ .log = mp_log_new(ictx, global->log, "input"),
+ .mouse_section = "default",
+ .opts_cache = m_config_cache_alloc(ictx, global, &input_config),
+ .wakeup_cb = wakeup_cb,
+ .wakeup_ctx = wakeup_ctx,
+ };
+
+ ictx->opts = ictx->opts_cache->opts;
+
+ mp_mutex_init_type(&ictx->mutex, MP_MUTEX_RECURSIVE);
+
+ // Setup default section, so that it does nothing.
+ mp_input_enable_section(ictx, NULL, MP_INPUT_ALLOW_VO_DRAGGING |
+ MP_INPUT_ALLOW_HIDE_CURSOR);
+
+ return ictx;
+}
+
+static void reload_opts(struct input_ctx *ictx, bool shutdown)
+{
+ m_config_cache_update(ictx->opts_cache);
+
+#if HAVE_COCOA
+ struct input_opts *opts = ictx->opts;
+
+ if (ictx->using_cocoa_media_keys != (opts->use_media_keys && !shutdown)) {
+ ictx->using_cocoa_media_keys = !ictx->using_cocoa_media_keys;
+ if (ictx->using_cocoa_media_keys) {
+ cocoa_init_media_keys();
+ } else {
+ cocoa_uninit_media_keys();
+ }
+ }
+#endif
+}
+
+void mp_input_update_opts(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ reload_opts(ictx, false);
+ input_unlock(ictx);
+}
+
+void mp_input_load_config(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+
+ reload_opts(ictx, false);
+
+ // "Uncomment" the default key bindings in etc/input.conf and add them.
+ // All lines that do not start with '# ' are parsed.
+ bstr builtin = bstr0(builtin_input_conf);
+ while (ictx->opts->builtin_bindings && builtin.len) {
+ bstr line = bstr_getline(builtin, &builtin);
+ bstr_eatstart0(&line, "#");
+ if (!bstr_startswith0(line, " "))
+ parse_config(ictx, true, line, "<builtin>", NULL);
+ }
+
+ bool config_ok = false;
+ if (ictx->opts->config_file && ictx->opts->config_file[0])
+ config_ok = parse_config_file(ictx, ictx->opts->config_file, true);
+ if (!config_ok) {
+ // Try global conf dir
+ void *tmp = talloc_new(NULL);
+ char **files = mp_find_all_config_files(tmp, ictx->global, "input.conf");
+ for (int n = 0; files && files[n]; n++)
+ parse_config_file(ictx, files[n], false);
+ talloc_free(tmp);
+ }
+
+#if HAVE_SDL2_GAMEPAD
+ if (ictx->opts->use_gamepad) {
+ mp_input_sdl_gamepad_add(ictx);
+ }
+#endif
+
+ input_unlock(ictx);
+}
+
+static void clear_queue(struct cmd_queue *queue)
+{
+ while (queue->first) {
+ struct mp_cmd *item = queue->first;
+ queue_remove(queue, item);
+ talloc_free(item);
+ }
+}
+
+void mp_input_uninit(struct input_ctx *ictx)
+{
+ if (!ictx)
+ return;
+
+ input_lock(ictx);
+ reload_opts(ictx, true);
+ input_unlock(ictx);
+
+ close_input_sources(ictx);
+ clear_queue(&ictx->cmd_queue);
+ talloc_free(ictx->current_down_cmd);
+ mp_mutex_destroy(&ictx->mutex);
+ talloc_free(ictx);
+}
+
+bool mp_input_use_alt_gr(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ bool r = ictx->opts->use_alt_gr;
+ input_unlock(ictx);
+ return r;
+}
+
+bool mp_input_use_media_keys(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ bool r = ictx->opts->use_media_keys;
+ input_unlock(ictx);
+ return r;
+}
+
+struct mp_cmd *mp_input_parse_cmd(struct input_ctx *ictx, bstr str,
+ const char *location)
+{
+ return mp_input_parse_cmd_str(ictx->log, str, location);
+}
+
+void mp_input_run_cmd(struct input_ctx *ictx, const char **cmd)
+{
+ mp_input_queue_cmd(ictx, mp_input_parse_cmd_strv(ictx->log, cmd));
+}
+
+void mp_input_bind_key(struct input_ctx *ictx, int key, bstr command)
+{
+ struct cmd_bind_section *bs = get_bind_section(ictx, (bstr){0});
+ struct cmd_bind *bind = NULL;
+
+ for (int n = 0; n < bs->num_binds; n++) {
+ struct cmd_bind *b = &bs->binds[n];
+ if (bind_matches_key(b, 1, &key) && b->is_builtin == false) {
+ bind = b;
+ break;
+ }
+ }
+
+ if (!bind) {
+ struct cmd_bind empty = {{0}};
+ MP_TARRAY_APPEND(bs, bs->binds, bs->num_binds, empty);
+ bind = &bs->binds[bs->num_binds - 1];
+ }
+
+ bind_dealloc(bind);
+
+ *bind = (struct cmd_bind) {
+ .cmd = bstrdup0(bs->binds, command),
+ .location = talloc_strdup(bs->binds, "keybind-command"),
+ .owner = bs,
+ .is_builtin = false,
+ .num_keys = 1,
+ };
+ memcpy(bind->keys, &key, 1 * sizeof(bind->keys[0]));
+ if (mp_msg_test(ictx->log, MSGL_DEBUG)) {
+ char *s = mp_input_get_key_combo_name(&key, 1);
+ MP_TRACE(ictx, "add:section='%s' key='%s'%s cmd='%s' location='%s'\n",
+ bind->owner->section, s, bind->is_builtin ? " builtin" : "",
+ bind->cmd, bind->location);
+ talloc_free(s);
+ }
+}
+
+struct mpv_node mp_input_get_bindings(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ struct mpv_node root;
+ node_init(&root, MPV_FORMAT_NODE_ARRAY, NULL);
+
+ for (int x = 0; x < ictx->num_sections; x++) {
+ struct cmd_bind_section *s = ictx->sections[x];
+ int priority = -1;
+
+ for (int i = 0; i < ictx->num_active_sections; i++) {
+ struct active_section *as = &ictx->active_sections[i];
+ if (strcmp(as->name, s->section) == 0) {
+ priority = i;
+ break;
+ }
+ }
+
+ for (int n = 0; n < s->num_binds; n++) {
+ struct cmd_bind *b = &s->binds[n];
+ struct mpv_node *entry = node_array_add(&root, MPV_FORMAT_NODE_MAP);
+
+ int b_priority = priority;
+ if (b->is_builtin && !ictx->opts->default_bindings)
+ b_priority = -1;
+
+ // Try to fixup the weird logic so consumer of this bindings list
+ // does not get too confused.
+ if (b_priority >= 0 && !b->is_builtin)
+ b_priority += ictx->num_active_sections;
+
+ node_map_add_string(entry, "section", s->section);
+ if (s->owner)
+ node_map_add_string(entry, "owner", s->owner);
+ node_map_add_string(entry, "cmd", b->cmd);
+ node_map_add_flag(entry, "is_weak", b->is_builtin);
+ node_map_add_int64(entry, "priority", b_priority);
+ if (b->desc)
+ node_map_add_string(entry, "comment", b->desc);
+
+ char *key = mp_input_get_key_combo_name(b->keys, b->num_keys);
+ node_map_add_string(entry, "key", key);
+ talloc_free(key);
+ }
+ }
+
+ input_unlock(ictx);
+ return root;
+}
+
+struct mp_input_src_internal {
+ mp_thread thread;
+ bool thread_running;
+ bool init_done;
+
+ char *cmd_buffer;
+ size_t cmd_buffer_size;
+ bool drop;
+};
+
+static struct mp_input_src *mp_input_add_src(struct input_ctx *ictx)
+{
+ input_lock(ictx);
+ if (ictx->num_sources == MP_MAX_SOURCES) {
+ input_unlock(ictx);
+ return NULL;
+ }
+
+ char name[80];
+ snprintf(name, sizeof(name), "#%d", ictx->num_sources + 1);
+ struct mp_input_src *src = talloc_ptrtype(NULL, src);
+ *src = (struct mp_input_src){
+ .global = ictx->global,
+ .log = mp_log_new(src, ictx->log, name),
+ .input_ctx = ictx,
+ .in = talloc_zero(src, struct mp_input_src_internal),
+ };
+
+ ictx->sources[ictx->num_sources++] = src;
+
+ input_unlock(ictx);
+ return src;
+}
+
+static void mp_input_src_kill(struct mp_input_src *src);
+
+static void close_input_sources(struct input_ctx *ictx)
+{
+ // To avoid lock-order issues, we first remove each source from the context,
+ // and then destroy it.
+ while (1) {
+ input_lock(ictx);
+ struct mp_input_src *src = ictx->num_sources ? ictx->sources[0] : NULL;
+ input_unlock(ictx);
+ if (!src)
+ break;
+ mp_input_src_kill(src);
+ }
+}
+
+static void mp_input_src_kill(struct mp_input_src *src)
+{
+ if (!src)
+ return;
+ struct input_ctx *ictx = src->input_ctx;
+ input_lock(ictx);
+ for (int n = 0; n < ictx->num_sources; n++) {
+ if (ictx->sources[n] == src) {
+ MP_TARRAY_REMOVE_AT(ictx->sources, ictx->num_sources, n);
+ input_unlock(ictx);
+ if (src->cancel)
+ src->cancel(src);
+ if (src->in->thread_running)
+ mp_thread_join(src->in->thread);
+ if (src->uninit)
+ src->uninit(src);
+ talloc_free(src);
+ return;
+ }
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+void mp_input_src_init_done(struct mp_input_src *src)
+{
+ assert(!src->in->init_done);
+ assert(src->in->thread_running);
+ assert(mp_thread_id_equal(mp_thread_get_id(src->in->thread), mp_thread_current_id()));
+ src->in->init_done = true;
+ mp_rendezvous(&src->in->init_done, 0);
+}
+
+static MP_THREAD_VOID input_src_thread(void *ptr)
+{
+ void **args = ptr;
+ struct mp_input_src *src = args[0];
+ void (*loop_fn)(struct mp_input_src *src, void *ctx) = args[1];
+ void *ctx = args[2];
+
+ mp_thread_set_name("input");
+
+ src->in->thread_running = true;
+
+ loop_fn(src, ctx);
+
+ if (!src->in->init_done)
+ mp_rendezvous(&src->in->init_done, -1);
+
+ MP_THREAD_RETURN();
+}
+
+int mp_input_add_thread_src(struct input_ctx *ictx, void *ctx,
+ void (*loop_fn)(struct mp_input_src *src, void *ctx))
+{
+ struct mp_input_src *src = mp_input_add_src(ictx);
+ if (!src)
+ return -1;
+
+ void *args[] = {src, loop_fn, ctx};
+ if (mp_thread_create(&src->in->thread, input_src_thread, args)) {
+ mp_input_src_kill(src);
+ return -1;
+ }
+ if (mp_rendezvous(&src->in->init_done, 0) < 0) {
+ mp_input_src_kill(src);
+ return -1;
+ }
+ return 0;
+}
+
+#define CMD_BUFFER (4 * 4096)
+
+void mp_input_src_feed_cmd_text(struct mp_input_src *src, char *buf, size_t len)
+{
+ struct mp_input_src_internal *in = src->in;
+ if (!in->cmd_buffer)
+ in->cmd_buffer = talloc_size(in, CMD_BUFFER);
+ while (len) {
+ char *next = memchr(buf, '\n', len);
+ bool term = !!next;
+ next = next ? next + 1 : buf + len;
+ size_t copy = next - buf;
+ bool overflow = copy > CMD_BUFFER - in->cmd_buffer_size;
+ if (overflow || in->drop) {
+ in->cmd_buffer_size = 0;
+ in->drop = overflow || !term;
+ MP_WARN(src, "Dropping overlong line.\n");
+ } else {
+ memcpy(in->cmd_buffer + in->cmd_buffer_size, buf, copy);
+ in->cmd_buffer_size += copy;
+ buf += copy;
+ len -= copy;
+ if (term) {
+ bstr s = {in->cmd_buffer, in->cmd_buffer_size};
+ s = bstr_strip(s);
+ struct mp_cmd *cmd = mp_input_parse_cmd_str(src->log, s, "<>");
+ if (cmd)
+ mp_input_queue_cmd(src->input_ctx, cmd);
+ in->cmd_buffer_size = 0;
+ }
+ }
+ }
+}
+
+void mp_input_set_repeat_info(struct input_ctx *ictx, int rate, int delay)
+{
+ input_lock(ictx);
+ ictx->opts->ar_rate = rate;
+ ictx->opts->ar_delay = delay;
+ input_unlock(ictx);
+}
diff --git a/input/input.h b/input/input.h
new file mode 100644
index 0000000..5b5e7a9
--- /dev/null
+++ b/input/input.h
@@ -0,0 +1,239 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_INPUT_H
+#define MPLAYER_INPUT_H
+
+#include <stdbool.h>
+#include "misc/bstr.h"
+
+#include "cmd.h"
+
+struct input_ctx;
+struct mp_log;
+
+struct mp_input_src {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct input_ctx *input_ctx;
+
+ struct mp_input_src_internal *in;
+
+ // If not-NULL: called before destroying the input_src. Should unblock the
+ // reader loop, and make it exit. (Use with mp_input_add_thread_src().)
+ void (*cancel)(struct mp_input_src *src);
+ // Called after the reader thread returns, and cancel() won't be called
+ // again. This should make sure that nothing after this call accesses src.
+ void (*uninit)(struct mp_input_src *src);
+
+ // For free use by the implementer.
+ void *priv;
+};
+
+enum mp_input_section_flags {
+ // If a key binding is not defined in the current section, do not search the
+ // other sections for it (like the default section). Instead, an unbound
+ // key warning will be printed.
+ MP_INPUT_EXCLUSIVE = 1,
+ // Prefer it to other sections.
+ MP_INPUT_ON_TOP = 2,
+ // Let mp_input_test_dragging() return true, even if inside the mouse area.
+ MP_INPUT_ALLOW_VO_DRAGGING = 4,
+ // Don't force mouse pointer visible, even if inside the mouse area.
+ MP_INPUT_ALLOW_HIDE_CURSOR = 8,
+};
+
+// Add an input source that runs on a thread. The source is automatically
+// removed if the thread loop exits.
+// ctx: this is passed to loop_fn.
+// loop_fn: this is called once inside of a new thread, and should not return
+// until all input is read, or src->cancel is called by another thread.
+// You must call mp_input_src_init_done(src) early during init to signal
+// success (then src->cancel may be called at a later point); on failure,
+// return from loop_fn immediately.
+// Returns >=0 on success, <0 on failure to allocate resources.
+// Do not set src->cancel after mp_input_src_init_done() has been called.
+int mp_input_add_thread_src(struct input_ctx *ictx, void *ctx,
+ void (*loop_fn)(struct mp_input_src *src, void *ctx));
+
+// Signal successful init.
+// Must be called on the same thread as loop_fn (see mp_input_add_thread_src()).
+// Set src->cancel and src->uninit (if needed) before calling this.
+void mp_input_src_init_done(struct mp_input_src *src);
+
+// Feed text data, which will be split into lines of commands.
+void mp_input_src_feed_cmd_text(struct mp_input_src *src, char *buf, size_t len);
+
+// Process keyboard input. code is a key code from keycodes.h, possibly
+// with modifiers applied. MP_INPUT_RELEASE_ALL is also a valid value.
+void mp_input_put_key(struct input_ctx *ictx, int code);
+
+// Like mp_input_put_key(), but ignore mouse disable option for mouse buttons.
+void mp_input_put_key_artificial(struct input_ctx *ictx, int code);
+
+// Like mp_input_put_key(), but process all UTF-8 characters in the given
+// string as key events.
+void mp_input_put_key_utf8(struct input_ctx *ictx, int mods, struct bstr t);
+
+// Process scrolling input. Support for precise scrolling. Scales the given
+// scroll amount add multiplies it with the command (seeking, sub-delay, etc)
+void mp_input_put_wheel(struct input_ctx *ictx, int direction, double value);
+
+// Update mouse position (in window coordinates).
+void mp_input_set_mouse_pos(struct input_ctx *ictx, int x, int y);
+
+// Like mp_input_set_mouse_pos(), but ignore mouse disable option.
+void mp_input_set_mouse_pos_artificial(struct input_ctx *ictx, int x, int y);
+
+void mp_input_get_mouse_pos(struct input_ctx *ictx, int *x, int *y, int *hover);
+
+// Return whether we want/accept mouse input.
+bool mp_input_mouse_enabled(struct input_ctx *ictx);
+
+bool mp_input_vo_keyboard_enabled(struct input_ctx *ictx);
+
+/* Make mp_input_set_mouse_pos() mangle the mouse coordinates. Hack for certain
+ * VOs. dst=NULL, src=NULL reset it. src can be NULL.
+ */
+struct mp_rect;
+void mp_input_set_mouse_transform(struct input_ctx *ictx, struct mp_rect *dst,
+ struct mp_rect *src);
+
+// Add a command to the command queue.
+int mp_input_queue_cmd(struct input_ctx *ictx, struct mp_cmd *cmd);
+
+// Return next queued command, or NULL.
+struct mp_cmd *mp_input_read_cmd(struct input_ctx *ictx);
+
+// Parse text and return corresponding struct mp_cmd.
+// The location parameter is for error messages.
+struct mp_cmd *mp_input_parse_cmd(struct input_ctx *ictx, bstr str,
+ const char *location);
+
+// Set current input section. The section is appended on top of the list of
+// active sections, so its bindings are considered first. If the section was
+// already active, it's moved to the top as well.
+// name==NULL will behave as if name=="default"
+// flags is a bitfield of enum mp_input_section_flags values
+void mp_input_enable_section(struct input_ctx *ictx, char *name, int flags);
+
+// Undo mp_input_enable_section().
+// name==NULL will behave as if name=="default"
+void mp_input_disable_section(struct input_ctx *ictx, char *name);
+
+// Like mp_input_set_section(ictx, ..., 0) for all sections.
+void mp_input_disable_all_sections(struct input_ctx *ictx);
+
+// Set the contents of an input section.
+// name: name of the section, for mp_input_set_section() etc.
+// location: location string (like filename) for error reporting
+// contents: list of keybindings, like input.conf
+// a value of NULL deletes the section
+// builtin: create as builtin section; this means if the user defines bindings
+// using "{name}", they won't be ignored or overwritten - instead,
+// they are preferred to the bindings defined with this call
+// owner: string ID of the client which defined this, or NULL
+// If the section already exists, its bindings are removed and replaced.
+void mp_input_define_section(struct input_ctx *ictx, char *name, char *location,
+ char *contents, bool builtin, char *owner);
+
+// Remove all sections that have been defined by the given owner.
+void mp_input_remove_sections_by_owner(struct input_ctx *ictx, char *owner);
+
+// Define where on the screen the named input section should receive.
+// Setting a rectangle of size 0 unsets the mouse area.
+// A rectangle with negative size disables mouse input for this section.
+void mp_input_set_section_mouse_area(struct input_ctx *ictx, char *name,
+ int x0, int y0, int x1, int y1);
+
+// Used to detect mouse movement.
+unsigned int mp_input_get_mouse_event_counter(struct input_ctx *ictx);
+
+// Test whether there is any input section which wants to receive events.
+// Note that the mouse event is always delivered, even if this returns false.
+bool mp_input_test_mouse_active(struct input_ctx *ictx, int x, int y);
+
+// Whether input.c wants mouse drag events at this mouse position. If this
+// returns false, some VOs will initiate window dragging.
+bool mp_input_test_dragging(struct input_ctx *ictx, int x, int y);
+
+// Initialize the input system
+struct mpv_global;
+struct input_ctx *mp_input_init(struct mpv_global *global,
+ void (*wakeup_cb)(void *ctx),
+ void *wakeup_ctx);
+
+void mp_input_load_config(struct input_ctx *ictx);
+
+void mp_input_update_opts(struct input_ctx *ictx);
+
+void mp_input_uninit(struct input_ctx *ictx);
+
+// Return number of seconds until the next autorepeat event will be generated.
+// Returns INFINITY if no autorepeated key is active.
+double mp_input_get_delay(struct input_ctx *ictx);
+
+// Wake up sleeping input loop from another thread.
+void mp_input_wakeup(struct input_ctx *ictx);
+
+// If this returns true, use Right Alt key as Alt Gr to produce special
+// characters. If false, count Right Alt as the modifier Alt key.
+bool mp_input_use_alt_gr(struct input_ctx *ictx);
+
+// Return true if mpv should intercept keyboard media keys
+bool mp_input_use_media_keys(struct input_ctx *ictx);
+
+// Like mp_input_parse_cmd_strv, but also run the command.
+void mp_input_run_cmd(struct input_ctx *ictx, const char **cmd);
+
+// Binds a command to a key.
+void mp_input_bind_key(struct input_ctx *ictx, int key, bstr command);
+
+void mp_input_set_repeat_info(struct input_ctx *ictx, int rate, int delay);
+
+struct mpv_node mp_input_get_bindings(struct input_ctx *ictx);
+
+void mp_input_sdl_gamepad_add(struct input_ctx *ictx);
+
+struct mp_ipc_ctx;
+struct mp_client_api;
+struct mpv_handle;
+
+// Platform specific implementation, provided by ipc-*.c.
+struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
+ struct mpv_global *global);
+// Start a thread for the given handle and return a socket in out_fd[0] that
+// is served by this thread. If the FD is not full-duplex, then out_fd[0] is
+// the user's read-end, and out_fd[1] the write-end, otherwise out_fd[1] is set
+// to -1.
+// returns:
+// true: out_fd[0] and out_fd[1] are set, ownership of h is transferred
+// false: out_fd are not touched, caller retains ownership of h
+bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
+ int out_fd[2]);
+void mp_uninit_ipc(struct mp_ipc_ctx *ctx);
+
+// Serialize the given mpv_event structure to JSON. Returns an allocated string.
+struct mpv_event;
+char *mp_json_encode_event(struct mpv_event *event);
+
+// Given the raw IPC input buffer "buf", remove the first newline-separated
+// command, execute it and return the result (if any) as an allocated string.
+struct mpv_handle;
+char *mp_ipc_consume_next_command(struct mpv_handle *client, void *ctx, bstr *buf);
+
+#endif /* MPLAYER_INPUT_H */
diff --git a/input/ipc-dummy.c b/input/ipc-dummy.c
new file mode 100644
index 0000000..f0232b2
--- /dev/null
+++ b/input/ipc-dummy.c
@@ -0,0 +1,19 @@
+#include <stddef.h>
+
+#include "input/input.h"
+
+struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
+ struct mpv_global *global)
+{
+ return NULL;
+}
+
+bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
+ int out_fd[2])
+{
+ return false;
+}
+
+void mp_uninit_ipc(struct mp_ipc_ctx *ctx)
+{
+}
diff --git a/input/ipc-unix.c b/input/ipc-unix.c
new file mode 100644
index 0000000..a416b54
--- /dev/null
+++ b/input/ipc-unix.c
@@ -0,0 +1,444 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+
+#include "osdep/io.h"
+#include "osdep/threads.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "libmpv/client.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "player/client.h"
+
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif
+
+struct mp_ipc_ctx {
+ struct mp_log *log;
+ struct mp_client_api *client_api;
+ const char *path;
+
+ mp_thread thread;
+ int death_pipe[2];
+};
+
+struct client_arg {
+ struct mp_log *log;
+ struct mpv_handle *client;
+
+ const char *client_name;
+ int client_fd;
+ bool close_client_fd;
+ bool quit_on_close;
+
+ bool writable;
+};
+
+static int ipc_write_str(struct client_arg *client, const char *buf)
+{
+ size_t count = strlen(buf);
+ while (count > 0) {
+ ssize_t rc = send(client->client_fd, buf, count, MSG_NOSIGNAL);
+ if (rc <= 0) {
+ if (rc == 0)
+ return -1;
+
+ if (errno == EBADF || errno == ENOTSOCK) {
+ client->writable = false;
+ return 0;
+ }
+
+ if (errno == EINTR || errno == EAGAIN)
+ continue;
+
+ return rc;
+ }
+
+ count -= rc;
+ buf += rc;
+ }
+
+ return 0;
+}
+
+static MP_THREAD_VOID client_thread(void *p)
+{
+ // We don't use MSG_NOSIGNAL because the moldy fruit OS doesn't support it.
+ struct sigaction sa = { .sa_handler = SIG_IGN, .sa_flags = SA_RESTART };
+ sigfillset(&sa.sa_mask);
+ sigaction(SIGPIPE, &sa, NULL);
+
+ int rc;
+
+ struct client_arg *arg = p;
+ bstr client_msg = { talloc_strdup(NULL, ""), 0 };
+
+ char *tname = talloc_asprintf(NULL, "ipc/%s", arg->client_name);
+ mp_thread_set_name(tname);
+ talloc_free(tname);
+
+ int pipe_fd = mpv_get_wakeup_pipe(arg->client);
+ if (pipe_fd < 0) {
+ MP_ERR(arg, "Could not get wakeup pipe\n");
+ goto done;
+ }
+
+ MP_VERBOSE(arg, "Client connected\n");
+
+ struct pollfd fds[2] = {
+ {.events = POLLIN, .fd = pipe_fd},
+ {.events = POLLIN, .fd = arg->client_fd},
+ };
+
+ fcntl(arg->client_fd, F_SETFL, fcntl(arg->client_fd, F_GETFL, 0) | O_NONBLOCK);
+
+ while (1) {
+ rc = poll(fds, 2, 0);
+ if (rc == 0)
+ rc = poll(fds, 2, -1);
+ if (rc < 0) {
+ MP_ERR(arg, "Poll error\n");
+ continue;
+ }
+
+ if (fds[0].revents & POLLIN) {
+ mp_flush_wakeup_pipe(pipe_fd);
+
+ while (1) {
+ mpv_event *event = mpv_wait_event(arg->client, 0);
+
+ if (event->event_id == MPV_EVENT_NONE)
+ break;
+
+ if (event->event_id == MPV_EVENT_SHUTDOWN)
+ goto done;
+
+ if (!arg->writable)
+ continue;
+
+ char *event_msg = mp_json_encode_event(event);
+ if (!event_msg) {
+ MP_ERR(arg, "Encoding error\n");
+ goto done;
+ }
+
+ rc = ipc_write_str(arg, event_msg);
+ talloc_free(event_msg);
+ if (rc < 0) {
+ MP_ERR(arg, "Write error (%s)\n", mp_strerror(errno));
+ goto done;
+ }
+ }
+ }
+
+ if (fds[1].revents & (POLLIN | POLLHUP | POLLNVAL)) {
+ while (1) {
+ char buf[128];
+ bstr append = { buf, 0 };
+
+ ssize_t bytes = read(arg->client_fd, buf, sizeof(buf));
+ if (bytes < 0) {
+ if (errno == EAGAIN)
+ break;
+
+ MP_ERR(arg, "Read error (%s)\n", mp_strerror(errno));
+ goto done;
+ }
+
+ if (bytes == 0) {
+ MP_VERBOSE(arg, "Client disconnected\n");
+ goto done;
+ }
+
+ append.len = bytes;
+
+ bstr_xappend(NULL, &client_msg, append);
+
+ while (bstrchr(client_msg, '\n') != -1) {
+ char *reply_msg = mp_ipc_consume_next_command(arg->client,
+ NULL, &client_msg);
+
+ if (reply_msg && arg->writable) {
+ rc = ipc_write_str(arg, reply_msg);
+ if (rc < 0) {
+ MP_ERR(arg, "Write error (%s)\n", mp_strerror(errno));
+ talloc_free(reply_msg);
+ goto done;
+ }
+ }
+
+ talloc_free(reply_msg);
+ }
+ }
+ }
+ }
+
+done:
+ if (client_msg.len > 0)
+ MP_WARN(arg, "Ignoring unterminated command on disconnect.\n");
+ talloc_free(client_msg.start);
+ if (arg->close_client_fd)
+ close(arg->client_fd);
+ struct mpv_handle *h = arg->client;
+ bool quit = arg->quit_on_close;
+ talloc_free(arg);
+ if (quit) {
+ mpv_terminate_destroy(h);
+ } else {
+ mpv_destroy(h);
+ }
+ MP_THREAD_RETURN();
+}
+
+static bool ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client,
+ bool free_on_init_fail)
+{
+ if (!client->client)
+ client->client = mp_new_client(ctx->client_api, client->client_name);
+ if (!client->client)
+ goto err;
+
+ client->log = mp_client_get_log(client->client);
+
+ mp_thread client_thr;
+ if (mp_thread_create(&client_thr, client_thread, client))
+ goto err;
+ mp_thread_detach(client_thr);
+
+ return true;
+
+err:
+ if (free_on_init_fail) {
+ if (client->client)
+ mpv_destroy(client->client);
+
+ if (client->close_client_fd)
+ close(client->client_fd);
+ }
+
+ talloc_free(client);
+ return false;
+}
+
+static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, int fd)
+{
+ struct client_arg *client = talloc_ptrtype(NULL, client);
+ *client = (struct client_arg){
+ .client_name =
+ id >= 0 ? talloc_asprintf(client, "ipc-%d", id) : "ipc",
+ .client_fd = fd,
+ .close_client_fd = id >= 0,
+ .quit_on_close = id < 0,
+ .writable = true,
+ };
+
+ ipc_start_client(ctx, client, true);
+}
+
+bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
+ int out_fd[2])
+{
+ int pair[2];
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair))
+ return false;
+ mp_set_cloexec(pair[0]);
+ mp_set_cloexec(pair[1]);
+
+ struct client_arg *client = talloc_ptrtype(NULL, client);
+ *client = (struct client_arg){
+ .client = h,
+ .client_name = mpv_client_name(h),
+ .client_fd = pair[1],
+ .close_client_fd = true,
+ .writable = true,
+ };
+
+ if (!ipc_start_client(ctx, client, false)) {
+ close(pair[0]);
+ close(pair[1]);
+ return false;
+ }
+
+ out_fd[0] = pair[0];
+ out_fd[1] = -1;
+ return true;
+}
+
+static MP_THREAD_VOID ipc_thread(void *p)
+{
+ int rc;
+
+ int ipc_fd;
+ struct sockaddr_un ipc_un = {0};
+
+ struct mp_ipc_ctx *arg = p;
+
+ mp_thread_set_name("ipc/socket");
+
+ MP_VERBOSE(arg, "Starting IPC master\n");
+
+ ipc_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (ipc_fd < 0) {
+ MP_ERR(arg, "Could not create IPC socket\n");
+ goto done;
+ }
+
+ fchmod(ipc_fd, 0600);
+
+ size_t path_len = strlen(arg->path);
+ if (path_len >= sizeof(ipc_un.sun_path) - 1) {
+ MP_ERR(arg, "Could not create IPC socket\n");
+ goto done;
+ }
+
+ ipc_un.sun_family = AF_UNIX,
+ strncpy(ipc_un.sun_path, arg->path, sizeof(ipc_un.sun_path) - 1);
+
+ unlink(ipc_un.sun_path);
+
+ if (ipc_un.sun_path[0] == '@') {
+ ipc_un.sun_path[0] = '\0';
+ path_len--;
+ }
+
+ size_t addr_len = offsetof(struct sockaddr_un, sun_path) + 1 + path_len;
+ rc = bind(ipc_fd, (struct sockaddr *) &ipc_un, addr_len);
+ if (rc < 0) {
+ MP_ERR(arg, "Could not bind IPC socket\n");
+ goto done;
+ }
+
+ rc = listen(ipc_fd, 10);
+ if (rc < 0) {
+ MP_ERR(arg, "Could not listen on IPC socket\n");
+ goto done;
+ }
+
+ MP_VERBOSE(arg, "Listening to IPC socket.\n");
+
+ int client_num = 0;
+
+ struct pollfd fds[2] = {
+ {.events = POLLIN, .fd = arg->death_pipe[0]},
+ {.events = POLLIN, .fd = ipc_fd},
+ };
+
+ while (1) {
+ rc = poll(fds, 2, -1);
+ if (rc < 0) {
+ MP_ERR(arg, "Poll error\n");
+ continue;
+ }
+
+ if (fds[0].revents & POLLIN)
+ goto done;
+
+ if (fds[1].revents & POLLIN) {
+ int client_fd = accept(ipc_fd, NULL, NULL);
+ if (client_fd < 0) {
+ MP_ERR(arg, "Could not accept IPC client\n");
+ goto done;
+ }
+
+ ipc_start_client_json(arg, client_num++, client_fd);
+ }
+ }
+
+done:
+ if (ipc_fd >= 0)
+ close(ipc_fd);
+
+ MP_THREAD_RETURN();
+}
+
+struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
+ struct mpv_global *global)
+{
+ struct MPOpts *opts = mp_get_config_group(NULL, global, &mp_opt_root);
+
+ struct mp_ipc_ctx *arg = talloc_ptrtype(NULL, arg);
+ *arg = (struct mp_ipc_ctx){
+ .log = mp_log_new(arg, global->log, "ipc"),
+ .client_api = client_api,
+ .path = mp_get_user_path(arg, global, opts->ipc_path),
+ .death_pipe = {-1, -1},
+ };
+
+ if (opts->ipc_client && opts->ipc_client[0]) {
+ int fd = -1;
+ if (strncmp(opts->ipc_client, "fd://", 5) == 0) {
+ char *end;
+ unsigned long l = strtoul(opts->ipc_client + 5, &end, 0);
+ if (!end[0] && l <= INT_MAX)
+ fd = l;
+ }
+ if (fd < 0) {
+ MP_ERR(arg, "Invalid IPC client argument: '%s'\n", opts->ipc_client);
+ } else {
+ ipc_start_client_json(arg, -1, fd);
+ }
+ }
+
+ talloc_free(opts);
+
+ if (!arg->path || !arg->path[0])
+ goto out;
+
+ if (mp_make_wakeup_pipe(arg->death_pipe) < 0)
+ goto out;
+
+ if (mp_thread_create(&arg->thread, ipc_thread, arg))
+ goto out;
+
+ return arg;
+
+out:
+ if (arg->death_pipe[0] >= 0) {
+ close(arg->death_pipe[0]);
+ close(arg->death_pipe[1]);
+ }
+ talloc_free(arg);
+ return NULL;
+}
+
+void mp_uninit_ipc(struct mp_ipc_ctx *arg)
+{
+ if (!arg)
+ return;
+
+ (void)write(arg->death_pipe[1], &(char){0}, 1);
+ mp_thread_join(arg->thread);
+
+ close(arg->death_pipe[0]);
+ close(arg->death_pipe[1]);
+ talloc_free(arg);
+}
diff --git a/input/ipc-win.c b/input/ipc-win.c
new file mode 100644
index 0000000..b0200ea
--- /dev/null
+++ b/input/ipc-win.c
@@ -0,0 +1,509 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <sddl.h>
+
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/windows_utils.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "libmpv/client.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "player/client.h"
+
+struct mp_ipc_ctx {
+ struct mp_log *log;
+ struct mp_client_api *client_api;
+ const wchar_t *path;
+
+ mp_thread thread;
+ HANDLE death_event;
+};
+
+struct client_arg {
+ struct mp_log *log;
+ struct mpv_handle *client;
+
+ char *client_name;
+ HANDLE client_h;
+ bool writable;
+ OVERLAPPED write_ol;
+};
+
+// Get a string SID representing the current user. Must be freed by LocalFree.
+static char *get_user_sid(void)
+{
+ char *ssid = NULL;
+ TOKEN_USER *info = NULL;
+ HANDLE t;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
+ goto done;
+
+ DWORD info_len;
+ if (!GetTokenInformation(t, TokenUser, NULL, 0, &info_len) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ goto done;
+
+ info = talloc_size(NULL, info_len);
+ if (!GetTokenInformation(t, TokenUser, info, info_len, &info_len))
+ goto done;
+ if (!info->User.Sid)
+ goto done;
+
+ ConvertSidToStringSidA(info->User.Sid, &ssid);
+done:
+ if (t)
+ CloseHandle(t);
+ talloc_free(info);
+ return ssid;
+}
+
+// Get a string SID for the process integrity level. Must be freed by LocalFree.
+static char *get_integrity_sid(void)
+{
+ char *ssid = NULL;
+ TOKEN_MANDATORY_LABEL *info = NULL;
+ HANDLE t;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
+ goto done;
+
+ DWORD info_len;
+ if (!GetTokenInformation(t, TokenIntegrityLevel, NULL, 0, &info_len) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ goto done;
+
+ info = talloc_size(NULL, info_len);
+ if (!GetTokenInformation(t, TokenIntegrityLevel, info, info_len, &info_len))
+ goto done;
+ if (!info->Label.Sid)
+ goto done;
+
+ ConvertSidToStringSidA(info->Label.Sid, &ssid);
+done:
+ if (t)
+ CloseHandle(t);
+ talloc_free(info);
+ return ssid;
+}
+
+// Create a security descriptor that only grants access to processes running
+// under the current user at the current integrity level or higher
+static PSECURITY_DESCRIPTOR create_restricted_sd(void)
+{
+ char *user_sid = get_user_sid();
+ char *integrity_sid = get_integrity_sid();
+ if (!user_sid || !integrity_sid)
+ return NULL;
+
+ char *sddl = talloc_asprintf(NULL,
+ "O:%s" // Set the owner to user_sid
+ "D:(A;;GRGW;;;%s)" // Grant GENERIC_{READ,WRITE} access to user_sid
+ "S:(ML;;NRNWNX;;;%s)", // Disallow read, write and execute permissions
+ // to integrity levels below integrity_sid
+ user_sid, user_sid, integrity_sid);
+ LocalFree(user_sid);
+ LocalFree(integrity_sid);
+
+ PSECURITY_DESCRIPTOR sd = NULL;
+ ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1,
+ &sd, NULL);
+ talloc_free(sddl);
+
+ return sd;
+}
+
+static void wakeup_cb(void *d)
+{
+ HANDLE event = d;
+ SetEvent(event);
+}
+
+// Wrapper for ReadFile that treats ERROR_IO_PENDING as success
+static DWORD async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol)
+{
+ DWORD err = ReadFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
+ return err == ERROR_IO_PENDING ? 0 : err;
+}
+
+// Wrapper for WriteFile that treats ERROR_IO_PENDING as success
+static DWORD async_write(HANDLE file, const void *buf, unsigned size, OVERLAPPED* ol)
+{
+ DWORD err = WriteFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
+ return err == ERROR_IO_PENDING ? 0 : err;
+}
+
+static bool pipe_error_is_fatal(DWORD error)
+{
+ switch (error) {
+ case 0:
+ case ERROR_HANDLE_EOF:
+ case ERROR_BROKEN_PIPE:
+ case ERROR_PIPE_NOT_CONNECTED:
+ case ERROR_NO_DATA:
+ return false;
+ }
+ return true;
+}
+
+static DWORD ipc_write_str(struct client_arg *arg, const char *buf)
+{
+ DWORD error = 0;
+
+ if ((error = async_write(arg->client_h, buf, strlen(buf), &arg->write_ol)))
+ goto done;
+ if (!GetOverlappedResult(arg->client_h, &arg->write_ol, &(DWORD){0}, TRUE)) {
+ error = GetLastError();
+ goto done;
+ }
+
+done:
+ if (pipe_error_is_fatal(error)) {
+ MP_VERBOSE(arg, "Error writing to pipe: %s\n",
+ mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
+ }
+
+ if (error)
+ arg->writable = false;
+ return error;
+}
+
+static void report_read_error(struct client_arg *arg, DWORD error)
+{
+ // Only report the error if it's not just due to the pipe closing
+ if (pipe_error_is_fatal(error)) {
+ MP_ERR(arg, "Error reading from pipe: %s\n",
+ mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
+ } else {
+ MP_VERBOSE(arg, "Client disconnected\n");
+ }
+}
+
+static MP_THREAD_VOID client_thread(void *p)
+{
+ struct client_arg *arg = p;
+ char buf[4096];
+ HANDLE wakeup_event = CreateEventW(NULL, TRUE, FALSE, NULL);
+ OVERLAPPED ol = { .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
+ bstr client_msg = { talloc_strdup(NULL, ""), 0 };
+ DWORD ioerr = 0;
+ DWORD r;
+
+ char *tname = talloc_asprintf(NULL, "ipc/%s", arg->client_name);
+ mp_thread_set_name(tname);
+ talloc_free(tname);
+
+ arg->write_ol.hEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
+ if (!wakeup_event || !ol.hEvent || !arg->write_ol.hEvent) {
+ MP_ERR(arg, "Couldn't create events\n");
+ goto done;
+ }
+
+ MP_VERBOSE(arg, "Client connected\n");
+
+ mpv_set_wakeup_callback(arg->client, wakeup_cb, wakeup_event);
+
+ // Do the first read operation on the pipe
+ if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
+ report_read_error(arg, ioerr);
+ goto done;
+ }
+
+ while (1) {
+ HANDLE handles[] = { wakeup_event, ol.hEvent };
+ int n = WaitForMultipleObjects(2, handles, FALSE, 0);
+ if (n == WAIT_TIMEOUT)
+ n = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
+
+ switch (n) {
+ case WAIT_OBJECT_0: // wakeup_event
+ ResetEvent(wakeup_event);
+
+ while (1) {
+ mpv_event *event = mpv_wait_event(arg->client, 0);
+
+ if (event->event_id == MPV_EVENT_NONE)
+ break;
+
+ if (event->event_id == MPV_EVENT_SHUTDOWN)
+ goto done;
+
+ if (!arg->writable)
+ continue;
+
+ char *event_msg = mp_json_encode_event(event);
+ if (!event_msg) {
+ MP_ERR(arg, "Encoding error\n");
+ goto done;
+ }
+
+ ipc_write_str(arg, event_msg);
+ talloc_free(event_msg);
+ }
+
+ break;
+ case WAIT_OBJECT_0 + 1: // ol.hEvent
+ // Complete the read operation on the pipe
+ if (!GetOverlappedResult(arg->client_h, &ol, &r, TRUE)) {
+ report_read_error(arg, GetLastError());
+ goto done;
+ }
+
+ bstr_xappend(NULL, &client_msg, (bstr){buf, r});
+ while (bstrchr(client_msg, '\n') != -1) {
+ char *reply_msg = mp_ipc_consume_next_command(arg->client,
+ NULL, &client_msg);
+ if (reply_msg && arg->writable)
+ ipc_write_str(arg, reply_msg);
+ talloc_free(reply_msg);
+ }
+
+ // Begin the next read operation on the pipe
+ if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
+ report_read_error(arg, ioerr);
+ goto done;
+ }
+ break;
+ default:
+ MP_ERR(arg, "WaitForMultipleObjects failed\n");
+ goto done;
+ }
+ }
+
+done:
+ if (client_msg.len > 0)
+ MP_WARN(arg, "Ignoring unterminated command on disconnect.\n");
+
+ if (CancelIoEx(arg->client_h, &ol) || GetLastError() != ERROR_NOT_FOUND)
+ GetOverlappedResult(arg->client_h, &ol, &(DWORD){0}, TRUE);
+ if (wakeup_event)
+ CloseHandle(wakeup_event);
+ if (ol.hEvent)
+ CloseHandle(ol.hEvent);
+ if (arg->write_ol.hEvent)
+ CloseHandle(arg->write_ol.hEvent);
+
+ CloseHandle(arg->client_h);
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+ MP_THREAD_RETURN();
+}
+
+static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client)
+{
+ client->client = mp_new_client(ctx->client_api, client->client_name),
+ client->log = mp_client_get_log(client->client);
+
+ mp_thread client_thr;
+ if (mp_thread_create(&client_thr, client_thread, client)) {
+ mpv_destroy(client->client);
+ CloseHandle(client->client_h);
+ talloc_free(client);
+ }
+ mp_thread_detach(client_thr);
+}
+
+static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h)
+{
+ struct client_arg *client = talloc_ptrtype(NULL, client);
+ *client = (struct client_arg){
+ .client_name = talloc_asprintf(client, "ipc-%d", id),
+ .client_h = h,
+ .writable = true,
+ };
+
+ ipc_start_client(ctx, client);
+}
+
+bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
+ int out_fd[2])
+{
+ return false;
+}
+
+static MP_THREAD_VOID ipc_thread(void *p)
+{
+ // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is
+ // maintained for message-mode clients, but byte-mode clients can still
+ // connect, send and receive data. This is the most compatible mode.
+ static const DWORD state =
+ PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT |
+ PIPE_REJECT_REMOTE_CLIENTS;
+ static const DWORD mode =
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED;
+ static const DWORD bufsiz = 4096;
+
+ struct mp_ipc_ctx *arg = p;
+ HANDLE server = INVALID_HANDLE_VALUE;
+ HANDLE client = INVALID_HANDLE_VALUE;
+ int client_num = 0;
+
+ mp_thread_set_name("ipc/named-pipe");
+ MP_VERBOSE(arg, "Starting IPC master\n");
+
+ OVERLAPPED ol = {0};
+ SECURITY_ATTRIBUTES sa = {
+ .nLength = sizeof sa,
+ .lpSecurityDescriptor = create_restricted_sd(),
+ };
+ if (!sa.lpSecurityDescriptor) {
+ MP_ERR(arg, "Couldn't create security descriptor");
+ goto done;
+ }
+
+ ol = (OVERLAPPED){ .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
+ if (!ol.hEvent) {
+ MP_ERR(arg, "Couldn't create event");
+ goto done;
+ }
+
+ server = CreateNamedPipeW(arg->path, mode | FILE_FLAG_FIRST_PIPE_INSTANCE,
+ state, PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
+ if (server == INVALID_HANDLE_VALUE) {
+ MP_ERR(arg, "Couldn't create first pipe instance: %s\n",
+ mp_LastError_to_str());
+ goto done;
+ }
+
+ MP_VERBOSE(arg, "Listening to IPC pipe.\n");
+
+ while (1) {
+ DWORD err = ConnectNamedPipe(server, &ol) ? 0 : GetLastError();
+
+ if (err == ERROR_IO_PENDING) {
+ int n = WaitForMultipleObjects(2, (HANDLE[]) {
+ arg->death_event,
+ ol.hEvent,
+ }, FALSE, INFINITE) - WAIT_OBJECT_0;
+
+ switch (n) {
+ case 0:
+ // Stop waiting for new clients
+ CancelIo(server);
+ GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE);
+ goto done;
+ case 1:
+ // Complete the ConnectNamedPipe request
+ err = GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE)
+ ? 0 : GetLastError();
+ break;
+ default:
+ MP_ERR(arg, "WaitForMultipleObjects failed\n");
+ goto done;
+ }
+ }
+
+ // ERROR_PIPE_CONNECTED is returned if a client connects before
+ // ConnectNamedPipe is called. ERROR_NO_DATA is returned if a client
+ // connects, (possibly) writes data and exits before ConnectNamedPipe
+ // is called. Both cases should be handled as normal connections.
+ if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA)
+ err = 0;
+
+ if (err) {
+ MP_ERR(arg, "ConnectNamedPipe failed: %s\n",
+ mp_HRESULT_to_str(HRESULT_FROM_WIN32(err)));
+ goto done;
+ }
+
+ // Create the next pipe instance before the client thread to avoid the
+ // theoretical race condition where the client thread immediately
+ // closes the handle and there are no active instances of the pipe
+ client = server;
+ server = CreateNamedPipeW(arg->path, mode, state,
+ PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
+ if (server == INVALID_HANDLE_VALUE) {
+ MP_ERR(arg, "Couldn't create additional pipe instance: %s\n",
+ mp_LastError_to_str());
+ goto done;
+ }
+
+ ipc_start_client_json(arg, client_num++, client);
+ client = NULL;
+ }
+
+done:
+ if (sa.lpSecurityDescriptor)
+ LocalFree(sa.lpSecurityDescriptor);
+ if (client != INVALID_HANDLE_VALUE)
+ CloseHandle(client);
+ if (server != INVALID_HANDLE_VALUE)
+ CloseHandle(server);
+ if (ol.hEvent)
+ CloseHandle(ol.hEvent);
+ MP_THREAD_RETURN();
+}
+
+struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
+ struct mpv_global *global)
+{
+ struct MPOpts *opts = mp_get_config_group(NULL, global, &mp_opt_root);
+
+ struct mp_ipc_ctx *arg = talloc_ptrtype(NULL, arg);
+ *arg = (struct mp_ipc_ctx){
+ .log = mp_log_new(arg, global->log, "ipc"),
+ .client_api = client_api,
+ };
+
+ if (!opts->ipc_path || !*opts->ipc_path)
+ goto out;
+
+ // Ensure the path is a legal Win32 pipe name by prepending \\.\pipe\ if
+ // it's not already present. Qt's QLocalSocket uses the same logic, so
+ // cross-platform programs that use paths like /tmp/mpv-socket should just
+ // work. (Win32 converts this path to \Device\NamedPipe\tmp\mpv-socket)
+ if (!strncmp(opts->ipc_path, "\\\\.\\pipe\\", 9)) {
+ arg->path = mp_from_utf8(arg, opts->ipc_path);
+ } else {
+ char *path = talloc_asprintf(NULL, "\\\\.\\pipe\\%s", opts->ipc_path);
+ arg->path = mp_from_utf8(arg, path);
+ talloc_free(path);
+ }
+
+ if (!(arg->death_event = CreateEventW(NULL, TRUE, FALSE, NULL)))
+ goto out;
+
+ if (mp_thread_create(&arg->thread, ipc_thread, arg))
+ goto out;
+
+ talloc_free(opts);
+ return arg;
+
+out:
+ if (arg->death_event)
+ CloseHandle(arg->death_event);
+ talloc_free(arg);
+ talloc_free(opts);
+ return NULL;
+}
+
+void mp_uninit_ipc(struct mp_ipc_ctx *arg)
+{
+ if (!arg)
+ return;
+
+ SetEvent(arg->death_event);
+ mp_thread_join(arg->thread);
+
+ CloseHandle(arg->death_event);
+ talloc_free(arg);
+}
diff --git a/input/ipc.c b/input/ipc.c
new file mode 100644
index 0000000..ea69fb7
--- /dev/null
+++ b/input/ipc.c
@@ -0,0 +1,414 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "misc/json.h"
+#include "misc/node.h"
+#include "options/m_option.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "player/client.h"
+
+static mpv_node *mpv_node_array_get(mpv_node *src, int index)
+{
+ if (src->format != MPV_FORMAT_NODE_ARRAY)
+ return NULL;
+
+ if (src->u.list->num < (index + 1))
+ return NULL;
+
+ return &src->u.list->values[index];
+}
+
+static void mpv_node_map_add(void *ta_parent, mpv_node *src, const char *key, mpv_node *val)
+{
+ if (src->format != MPV_FORMAT_NODE_MAP)
+ return;
+
+ if (!src->u.list)
+ src->u.list = talloc_zero(ta_parent, mpv_node_list);
+
+ MP_TARRAY_GROW(src->u.list, src->u.list->keys, src->u.list->num);
+ MP_TARRAY_GROW(src->u.list, src->u.list->values, src->u.list->num);
+
+ src->u.list->keys[src->u.list->num] = talloc_strdup(ta_parent, key);
+
+ static const struct m_option type = { .type = CONF_TYPE_NODE };
+ m_option_get_node(&type, ta_parent, &src->u.list->values[src->u.list->num], val);
+
+ src->u.list->num++;
+}
+
+static void mpv_node_map_add_null(void *ta_parent, mpv_node *src, const char *key)
+{
+ mpv_node val_node = {.format = MPV_FORMAT_NONE};
+ mpv_node_map_add(ta_parent, src, key, &val_node);
+}
+
+static void mpv_node_map_add_int64(void *ta_parent, mpv_node *src, const char *key, int64_t val)
+{
+ mpv_node val_node = {.format = MPV_FORMAT_INT64, .u.int64 = val};
+ mpv_node_map_add(ta_parent, src, key, &val_node);
+}
+
+static void mpv_node_map_add_string(void *ta_parent, mpv_node *src, const char *key, const char *val)
+{
+ mpv_node val_node = {.format = MPV_FORMAT_STRING, .u.string = (char*)val};
+ mpv_node_map_add(ta_parent, src, key, &val_node);
+}
+
+// This is supposed to write a reply that looks like "normal" command execution.
+static void mpv_format_command_reply(void *ta_parent, mpv_event *event,
+ mpv_node *dst)
+{
+ assert(event->event_id == MPV_EVENT_COMMAND_REPLY);
+ mpv_event_command *cmd = event->data;
+
+ mpv_node_map_add_int64(ta_parent, dst, "request_id", event->reply_userdata);
+
+ mpv_node_map_add_string(ta_parent, dst, "error",
+ mpv_error_string(event->error));
+
+ mpv_node_map_add(ta_parent, dst, "data", &cmd->result);
+}
+
+char *mp_json_encode_event(mpv_event *event)
+{
+ void *ta_parent = talloc_new(NULL);
+
+ struct mpv_node event_node;
+ if (event->event_id == MPV_EVENT_COMMAND_REPLY) {
+ event_node = (mpv_node){.format = MPV_FORMAT_NODE_MAP, .u.list = NULL};
+ mpv_format_command_reply(ta_parent, event, &event_node);
+ } else {
+ mpv_event_to_node(&event_node, event);
+ // Abuse mpv_event_to_node() internals.
+ talloc_steal(ta_parent, node_get_alloc(&event_node));
+ }
+
+ char *output = talloc_strdup(NULL, "");
+ json_write(&output, &event_node);
+ output = ta_talloc_strdup_append(output, "\n");
+
+ talloc_free(ta_parent);
+
+ return output;
+}
+
+// Function is allowed to modify src[n].
+static char *json_execute_command(struct mpv_handle *client, void *ta_parent,
+ char *src)
+{
+ int rc;
+ const char *cmd = NULL;
+ struct mp_log *log = mp_client_get_log(client);
+
+ mpv_node msg_node;
+ mpv_node reply_node = {.format = MPV_FORMAT_NODE_MAP, .u.list = NULL};
+ mpv_node *reqid_node = NULL;
+ int64_t reqid = 0;
+ mpv_node *async_node = NULL;
+ bool async = false;
+ bool send_reply = true;
+
+ rc = json_parse(ta_parent, &msg_node, &src, MAX_JSON_DEPTH);
+ if (rc < 0) {
+ mp_err(log, "malformed JSON received: '%s'\n", src);
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (msg_node.format != MPV_FORMAT_NODE_MAP) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ async_node = node_map_get(&msg_node, "async");
+ if (async_node) {
+ if (async_node->format != MPV_FORMAT_FLAG) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+ async = async_node->u.flag;
+ }
+
+ reqid_node = node_map_get(&msg_node, "request_id");
+ if (reqid_node) {
+ if (reqid_node->format == MPV_FORMAT_INT64) {
+ reqid = reqid_node->u.int64;
+ } else if (async) {
+ mp_err(log, "'request_id' must be an integer for async commands.\n");
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ } else {
+ mp_warn(log, "'request_id' must be an integer. Using other types is "
+ "deprecated and will trigger an error in the future!\n");
+ }
+ }
+
+ mpv_node *cmd_node = node_map_get(&msg_node, "command");
+ if (!cmd_node) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->format == MPV_FORMAT_NODE_ARRAY) {
+ mpv_node *cmd_str_node = mpv_node_array_get(cmd_node, 0);
+ if (!cmd_str_node || (cmd_str_node->format != MPV_FORMAT_STRING)) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ cmd = cmd_str_node->u.string;
+ }
+
+ if (cmd && !strcmp("client_name", cmd)) {
+ const char *client_name = mpv_client_name(client);
+ mpv_node_map_add_string(ta_parent, &reply_node, "data", client_name);
+ rc = MPV_ERROR_SUCCESS;
+ } else if (cmd && !strcmp("get_time_us", cmd)) {
+ int64_t time_us = mpv_get_time_us(client);
+ mpv_node_map_add_int64(ta_parent, &reply_node, "data", time_us);
+ rc = MPV_ERROR_SUCCESS;
+ } else if (cmd && !strcmp("get_version", cmd)) {
+ int64_t ver = mpv_client_api_version();
+ mpv_node_map_add_int64(ta_parent, &reply_node, "data", ver);
+ rc = MPV_ERROR_SUCCESS;
+ } else if (cmd && !strcmp("get_property", cmd)) {
+ mpv_node result_node;
+
+ if (cmd_node->u.list->num != 2) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ rc = mpv_get_property(client, cmd_node->u.list->values[1].u.string,
+ MPV_FORMAT_NODE, &result_node);
+ if (rc >= 0) {
+ mpv_node_map_add(ta_parent, &reply_node, "data", &result_node);
+ mpv_free_node_contents(&result_node);
+ }
+ } else if (cmd && !strcmp("get_property_string", cmd)) {
+ if (cmd_node->u.list->num != 2) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ char *result = mpv_get_property_string(client,
+ cmd_node->u.list->values[1].u.string);
+ if (result) {
+ mpv_node_map_add_string(ta_parent, &reply_node, "data", result);
+ mpv_free(result);
+ } else {
+ mpv_node_map_add_null(ta_parent, &reply_node, "data");
+ }
+ } else if (cmd && (!strcmp("set_property", cmd) ||
+ !strcmp("set_property_string", cmd)))
+ {
+ if (cmd_node->u.list->num != 3) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ rc = mpv_set_property(client, cmd_node->u.list->values[1].u.string,
+ MPV_FORMAT_NODE, &cmd_node->u.list->values[2]);
+ } else if (cmd && !strcmp("observe_property", cmd)) {
+ if (cmd_node->u.list->num != 3) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_INT64) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[2].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ rc = mpv_observe_property(client,
+ cmd_node->u.list->values[1].u.int64,
+ cmd_node->u.list->values[2].u.string,
+ MPV_FORMAT_NODE);
+ } else if (cmd && !strcmp("observe_property_string", cmd)) {
+ if (cmd_node->u.list->num != 3) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_INT64) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[2].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ rc = mpv_observe_property(client,
+ cmd_node->u.list->values[1].u.int64,
+ cmd_node->u.list->values[2].u.string,
+ MPV_FORMAT_STRING);
+ } else if (cmd && !strcmp("unobserve_property", cmd)) {
+ if (cmd_node->u.list->num != 2) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_INT64) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ rc = mpv_unobserve_property(client,
+ cmd_node->u.list->values[1].u.int64);
+ } else if (cmd && !strcmp("request_log_messages", cmd)) {
+ if (cmd_node->u.list->num != 2) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ rc = mpv_request_log_messages(client,
+ cmd_node->u.list->values[1].u.string);
+ } else if (cmd && (!strcmp("enable_event", cmd) ||
+ !strcmp("disable_event", cmd)))
+ {
+ bool enable = !strcmp("enable_event", cmd);
+
+ if (cmd_node->u.list->num != 2) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ if (cmd_node->u.list->values[1].format != MPV_FORMAT_STRING) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+
+ char *name = cmd_node->u.list->values[1].u.string;
+ if (strcmp(name, "all") == 0) {
+ for (int n = 0; n < 64; n++)
+ mpv_request_event(client, n, enable);
+ rc = MPV_ERROR_SUCCESS;
+ } else {
+ int event = -1;
+ for (int n = 0; n < 64; n++) {
+ const char *evname = mpv_event_name(n);
+ if (evname && strcmp(evname, name) == 0)
+ event = n;
+ }
+ if (event < 0) {
+ rc = MPV_ERROR_INVALID_PARAMETER;
+ goto error;
+ }
+ rc = mpv_request_event(client, event, enable);
+ }
+ } else {
+ mpv_node result_node = {0};
+
+ if (async) {
+ rc = mpv_command_node_async(client, reqid, cmd_node);
+ if (rc >= 0)
+ send_reply = false;
+ } else {
+ rc = mpv_command_node(client, cmd_node, &result_node);
+ if (rc >= 0)
+ mpv_node_map_add(ta_parent, &reply_node, "data", &result_node);
+ }
+
+ mpv_free_node_contents(&result_node);
+ }
+
+error:
+ /* If the request contains a "request_id", copy it back into the response.
+ * This makes it easier on the requester to match up the IPC results with
+ * the original requests.
+ */
+ if (reqid_node) {
+ mpv_node_map_add(ta_parent, &reply_node, "request_id", reqid_node);
+ } else {
+ mpv_node_map_add_int64(ta_parent, &reply_node, "request_id", 0);
+ }
+
+ mpv_node_map_add_string(ta_parent, &reply_node, "error", mpv_error_string(rc));
+
+ char *output = talloc_strdup(ta_parent, "");
+
+ if (send_reply) {
+ json_write(&output, &reply_node);
+ output = ta_talloc_strdup_append(output, "\n");
+ }
+
+ return output;
+}
+
+static char *text_execute_command(struct mpv_handle *client, void *tmp, char *src)
+{
+ mpv_command_string(client, src);
+
+ return NULL;
+}
+
+char *mp_ipc_consume_next_command(struct mpv_handle *client, void *ctx, bstr *buf)
+{
+ void *tmp = talloc_new(NULL);
+
+ bstr rest;
+ bstr line = bstr_getline(*buf, &rest);
+ char *line0 = bstrto0(tmp, line);
+ talloc_steal(tmp, buf->start);
+ *buf = bstrdup(NULL, rest);
+
+ json_skip_whitespace(&line0);
+
+ char *reply_msg = NULL;
+ if (line0[0] == '\0' || line0[0] == '#') {
+ // skip
+ } else if (line0[0] == '{') {
+ reply_msg = json_execute_command(client, tmp, line0);
+ } else {
+ reply_msg = text_execute_command(client, tmp, line0);
+ }
+
+ talloc_steal(ctx, reply_msg);
+ talloc_free(tmp);
+ return reply_msg;
+}
diff --git a/input/keycodes.c b/input/keycodes.c
new file mode 100644
index 0000000..bca9e17
--- /dev/null
+++ b/input/keycodes.c
@@ -0,0 +1,379 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <strings.h>
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/msg.h"
+
+#include "keycodes.h"
+
+struct key_name {
+ int key;
+ char *name;
+};
+
+/// The names of the keys as used in input.conf
+/// If you add some new keys, you also need to add them here
+
+static const struct key_name key_names[] = {
+ { ' ', "SPACE" },
+ { '#', "SHARP" },
+ { 0x3000, "IDEOGRAPHIC_SPACE" },
+ { MP_KEY_ENTER, "ENTER" },
+ { MP_KEY_TAB, "TAB" },
+ { MP_KEY_BACKSPACE, "BS" },
+ { MP_KEY_DELETE, "DEL" },
+ { MP_KEY_INSERT, "INS" },
+ { MP_KEY_HOME, "HOME" },
+ { MP_KEY_END, "END" },
+ { MP_KEY_PAGE_UP, "PGUP" },
+ { MP_KEY_PAGE_DOWN, "PGDWN" },
+ { MP_KEY_ESC, "ESC" },
+ { MP_KEY_PRINT, "PRINT" },
+ { MP_KEY_RIGHT, "RIGHT" },
+ { MP_KEY_LEFT, "LEFT" },
+ { MP_KEY_DOWN, "DOWN" },
+ { MP_KEY_UP, "UP" },
+ { MP_KEY_F+1, "F1" },
+ { MP_KEY_F+2, "F2" },
+ { MP_KEY_F+3, "F3" },
+ { MP_KEY_F+4, "F4" },
+ { MP_KEY_F+5, "F5" },
+ { MP_KEY_F+6, "F6" },
+ { MP_KEY_F+7, "F7" },
+ { MP_KEY_F+8, "F8" },
+ { MP_KEY_F+9, "F9" },
+ { MP_KEY_F+10, "F10" },
+ { MP_KEY_F+11, "F11" },
+ { MP_KEY_F+12, "F12" },
+ { MP_KEY_F+13, "F13" },
+ { MP_KEY_F+14, "F14" },
+ { MP_KEY_F+15, "F15" },
+ { MP_KEY_F+16, "F16" },
+ { MP_KEY_F+17, "F17" },
+ { MP_KEY_F+18, "F18" },
+ { MP_KEY_F+19, "F19" },
+ { MP_KEY_F+20, "F20" },
+ { MP_KEY_F+21, "F21" },
+ { MP_KEY_F+22, "F22" },
+ { MP_KEY_F+23, "F23" },
+ { MP_KEY_F+24, "F24" },
+ { MP_KEY_KP0, "KP0" },
+ { MP_KEY_KP1, "KP1" },
+ { MP_KEY_KP2, "KP2" },
+ { MP_KEY_KP3, "KP3" },
+ { MP_KEY_KP4, "KP4" },
+ { MP_KEY_KP5, "KP5" },
+ { MP_KEY_KP6, "KP6" },
+ { MP_KEY_KP7, "KP7" },
+ { MP_KEY_KP8, "KP8" },
+ { MP_KEY_KP9, "KP9" },
+ { MP_KEY_KPDEL, "KP_DEL" },
+ { MP_KEY_KPDEC, "KP_DEC" },
+ { MP_KEY_KPINS, "KP_INS" },
+ { MP_KEY_KPHOME, "KP_HOME" },
+ { MP_KEY_KPEND, "KP_END" },
+ { MP_KEY_KPPGUP, "KP_PGUP" },
+ { MP_KEY_KPPGDOWN, "KP_PGDWN" },
+ { MP_KEY_KPRIGHT, "KP_RIGHT" },
+ { MP_KEY_KPLEFT, "KP_LEFT" },
+ { MP_KEY_KPDOWN, "KP_DOWN" },
+ { MP_KEY_KPUP, "KP_UP" },
+ { MP_KEY_KPENTER, "KP_ENTER" },
+ { MP_MBTN_LEFT, "MBTN_LEFT" },
+ { MP_MBTN_MID, "MBTN_MID" },
+ { MP_MBTN_RIGHT, "MBTN_RIGHT" },
+ { MP_WHEEL_UP, "WHEEL_UP" },
+ { MP_WHEEL_DOWN, "WHEEL_DOWN" },
+ { MP_WHEEL_LEFT, "WHEEL_LEFT" },
+ { MP_WHEEL_RIGHT, "WHEEL_RIGHT" },
+ { MP_MBTN_BACK, "MBTN_BACK" },
+ { MP_MBTN_FORWARD, "MBTN_FORWARD" },
+ { MP_MBTN9, "MBTN9" },
+ { MP_MBTN10, "MBTN10" },
+ { MP_MBTN11, "MBTN11" },
+ { MP_MBTN12, "MBTN12" },
+ { MP_MBTN13, "MBTN13" },
+ { MP_MBTN14, "MBTN14" },
+ { MP_MBTN15, "MBTN15" },
+ { MP_MBTN16, "MBTN16" },
+ { MP_MBTN17, "MBTN17" },
+ { MP_MBTN18, "MBTN18" },
+ { MP_MBTN19, "MBTN19" },
+ { MP_MBTN_LEFT_DBL, "MBTN_LEFT_DBL" },
+ { MP_MBTN_MID_DBL, "MBTN_MID_DBL" },
+ { MP_MBTN_RIGHT_DBL, "MBTN_RIGHT_DBL" },
+
+ { MP_KEY_GAMEPAD_ACTION_DOWN, "GAMEPAD_ACTION_DOWN" },
+ { MP_KEY_GAMEPAD_ACTION_RIGHT, "GAMEPAD_ACTION_RIGHT" },
+ { MP_KEY_GAMEPAD_ACTION_LEFT, "GAMEPAD_ACTION_LEFT" },
+ { MP_KEY_GAMEPAD_ACTION_UP, "GAMEPAD_ACTION_UP" },
+ { MP_KEY_GAMEPAD_BACK, "GAMEPAD_BACK" },
+ { MP_KEY_GAMEPAD_MENU, "GAMEPAD_MENU" },
+ { MP_KEY_GAMEPAD_START, "GAMEPAD_START" },
+ { MP_KEY_GAMEPAD_LEFT_SHOULDER, "GAMEPAD_LEFT_SHOULDER" },
+ { MP_KEY_GAMEPAD_RIGHT_SHOULDER, "GAMEPAD_RIGHT_SHOULDER" },
+ { MP_KEY_GAMEPAD_LEFT_TRIGGER, "GAMEPAD_LEFT_TRIGGER" },
+ { MP_KEY_GAMEPAD_RIGHT_TRIGGER, "GAMEPAD_RIGHT_TRIGGER" },
+ { MP_KEY_GAMEPAD_LEFT_STICK, "GAMEPAD_LEFT_STICK" },
+ { MP_KEY_GAMEPAD_RIGHT_STICK, "GAMEPAD_RIGHT_STICK" },
+ { MP_KEY_GAMEPAD_DPAD_UP, "GAMEPAD_DPAD_UP" },
+ { MP_KEY_GAMEPAD_DPAD_DOWN, "GAMEPAD_DPAD_DOWN" },
+ { MP_KEY_GAMEPAD_DPAD_LEFT, "GAMEPAD_DPAD_LEFT" },
+ { MP_KEY_GAMEPAD_DPAD_RIGHT, "GAMEPAD_DPAD_RIGHT" },
+ { MP_KEY_GAMEPAD_LEFT_STICK_UP, "GAMEPAD_LEFT_STICK_UP" },
+ { MP_KEY_GAMEPAD_LEFT_STICK_DOWN, "GAMEPAD_LEFT_STICK_DOWN" },
+ { MP_KEY_GAMEPAD_LEFT_STICK_LEFT, "GAMEPAD_LEFT_STICK_LEFT" },
+ { MP_KEY_GAMEPAD_LEFT_STICK_RIGHT, "GAMEPAD_LEFT_STICK_RIGHT" },
+ { MP_KEY_GAMEPAD_RIGHT_STICK_UP, "GAMEPAD_RIGHT_STICK_UP" },
+ { MP_KEY_GAMEPAD_RIGHT_STICK_DOWN, "GAMEPAD_RIGHT_STICK_DOWN" },
+ { MP_KEY_GAMEPAD_RIGHT_STICK_LEFT, "GAMEPAD_RIGHT_STICK_LEFT" },
+ { MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT, "GAMEPAD_RIGHT_STICK_RIGHT" },
+
+ { MP_KEY_POWER, "POWER" },
+ { MP_KEY_MENU, "MENU" },
+ { MP_KEY_PLAY, "PLAY" },
+ { MP_KEY_PAUSE, "PAUSE" },
+ { MP_KEY_PLAYPAUSE, "PLAYPAUSE" },
+ { MP_KEY_STOP, "STOP" },
+ { MP_KEY_FORWARD, "FORWARD" },
+ { MP_KEY_REWIND, "REWIND" },
+ { MP_KEY_NEXT, "NEXT" },
+ { MP_KEY_PREV, "PREV" },
+ { MP_KEY_VOLUME_UP, "VOLUME_UP" },
+ { MP_KEY_VOLUME_DOWN, "VOLUME_DOWN" },
+ { MP_KEY_MUTE, "MUTE" },
+ { MP_KEY_HOMEPAGE, "HOMEPAGE" },
+ { MP_KEY_WWW, "WWW" },
+ { MP_KEY_MAIL, "MAIL" },
+ { MP_KEY_FAVORITES, "FAVORITES" },
+ { MP_KEY_SEARCH, "SEARCH" },
+ { MP_KEY_SLEEP, "SLEEP" },
+ { MP_KEY_CANCEL, "CANCEL" },
+ { MP_KEY_RECORD, "RECORD" },
+ { MP_KEY_CHANNEL_UP, "CHANNEL_UP" },
+ { MP_KEY_CHANNEL_DOWN,"CHANNEL_DOWN" },
+ { MP_KEY_PLAYONLY, "PLAYONLY" },
+ { MP_KEY_PAUSEONLY, "PAUSEONLY" },
+ { MP_KEY_BACK, "BACK" },
+ { MP_KEY_TOOLS, "TOOLS" },
+ { MP_KEY_ZOOMIN, "ZOOMIN" },
+ { MP_KEY_ZOOMOUT, "ZOOMOUT" },
+
+ // These are kept for backward compatibility
+ { MP_KEY_PAUSE, "XF86_PAUSE" },
+ { MP_KEY_STOP, "XF86_STOP" },
+ { MP_KEY_PREV, "XF86_PREV" },
+ { MP_KEY_NEXT, "XF86_NEXT" },
+
+ // Deprecated numeric aliases for the mouse buttons
+ { MP_MBTN_LEFT, "MOUSE_BTN0" },
+ { MP_MBTN_MID, "MOUSE_BTN1" },
+ { MP_MBTN_RIGHT, "MOUSE_BTN2" },
+ { MP_WHEEL_UP, "MOUSE_BTN3" },
+ { MP_WHEEL_DOWN, "MOUSE_BTN4" },
+ { MP_WHEEL_LEFT, "MOUSE_BTN5" },
+ { MP_WHEEL_RIGHT, "MOUSE_BTN6" },
+ { MP_MBTN_BACK, "MOUSE_BTN7" },
+ { MP_MBTN_FORWARD, "MOUSE_BTN8" },
+ { MP_MBTN9, "MOUSE_BTN9" },
+ { MP_MBTN10, "MOUSE_BTN10" },
+ { MP_MBTN11, "MOUSE_BTN11" },
+ { MP_MBTN12, "MOUSE_BTN12" },
+ { MP_MBTN13, "MOUSE_BTN13" },
+ { MP_MBTN14, "MOUSE_BTN14" },
+ { MP_MBTN15, "MOUSE_BTN15" },
+ { MP_MBTN16, "MOUSE_BTN16" },
+ { MP_MBTN17, "MOUSE_BTN17" },
+ { MP_MBTN18, "MOUSE_BTN18" },
+ { MP_MBTN19, "MOUSE_BTN19" },
+ { MP_MBTN_LEFT_DBL, "MOUSE_BTN0_DBL" },
+ { MP_MBTN_MID_DBL, "MOUSE_BTN1_DBL" },
+ { MP_MBTN_RIGHT_DBL, "MOUSE_BTN2_DBL" },
+ { MP_WHEEL_UP, "AXIS_UP" },
+ { MP_WHEEL_DOWN, "AXIS_DOWN" },
+ { MP_WHEEL_LEFT, "AXIS_LEFT" },
+ { MP_WHEEL_RIGHT, "AXIS_RIGHT" },
+
+ { MP_KEY_CLOSE_WIN, "CLOSE_WIN" },
+ { MP_KEY_MOUSE_MOVE, "MOUSE_MOVE" },
+ { MP_KEY_MOUSE_LEAVE, "MOUSE_LEAVE" },
+ { MP_KEY_MOUSE_ENTER, "MOUSE_ENTER" },
+
+ { MP_KEY_UNMAPPED, "UNMAPPED" },
+ { MP_KEY_ANY_UNICODE, "ANY_UNICODE" },
+
+ { 0, NULL }
+};
+
+static const struct key_name modifier_names[] = {
+ { MP_KEY_MODIFIER_SHIFT, "Shift" },
+ { MP_KEY_MODIFIER_CTRL, "Ctrl" },
+ { MP_KEY_MODIFIER_ALT, "Alt" },
+ { MP_KEY_MODIFIER_META, "Meta" },
+ { 0 }
+};
+
+int mp_input_get_key_from_name(const char *name)
+{
+ int modifiers = 0;
+ const char *p;
+ while ((p = strchr(name, '+'))) {
+ for (const struct key_name *m = modifier_names; m->name; m++)
+ if (!bstrcasecmp(bstr0(m->name),
+ (struct bstr){(char *)name, p - name})) {
+ modifiers |= m->key;
+ goto found;
+ }
+ if (!strcmp(name, "+"))
+ return '+' + modifiers;
+ return -1;
+found:
+ name = p + 1;
+ }
+
+ struct bstr bname = bstr0(name);
+
+ struct bstr rest;
+ int code = bstr_decode_utf8(bname, &rest);
+ if (code >= 0 && rest.len == 0)
+ return mp_normalize_keycode(code + modifiers);
+
+ if (bstr_startswith0(bname, "0x"))
+ return mp_normalize_keycode(strtol(name, NULL, 16) + modifiers);
+
+ for (int i = 0; key_names[i].name != NULL; i++) {
+ if (strcasecmp(key_names[i].name, name) == 0)
+ return mp_normalize_keycode(key_names[i].key + modifiers);
+ }
+
+ return -1;
+}
+
+static void mp_input_append_key_name(bstr *buf, int key)
+{
+ for (int i = 0; modifier_names[i].name; i++) {
+ if (modifier_names[i].key & key) {
+ bstr_xappend_asprintf(NULL, buf, "%s+", modifier_names[i].name);
+ key -= modifier_names[i].key;
+ }
+ }
+ for (int i = 0; key_names[i].name != NULL; i++) {
+ if (key_names[i].key == key) {
+ bstr_xappend_asprintf(NULL, buf, "%s", key_names[i].name);
+ return;
+ }
+ }
+
+ if (MP_KEY_IS_UNICODE(key)) {
+ mp_append_utf8_bstr(NULL, buf, key);
+ return;
+ }
+
+ // Print the hex key code
+ bstr_xappend_asprintf(NULL, buf, "0x%x", key);
+}
+
+char *mp_input_get_key_name(int key)
+{
+ bstr dst = {0};
+ mp_input_append_key_name(&dst, key);
+ return dst.start;
+}
+
+char *mp_input_get_key_combo_name(const int *keys, int max)
+{
+ bstr dst = {0};
+ while (max > 0) {
+ mp_input_append_key_name(&dst, *keys);
+ if (--max && *++keys)
+ bstr_xappend(NULL, &dst, bstr0("-"));
+ else
+ break;
+ }
+ return dst.start;
+}
+
+int mp_input_get_keys_from_string(char *name, int max_num_keys,
+ int *out_num_keys, int *keys)
+{
+ char *end, *ptr;
+ int n = 0;
+
+ ptr = name;
+ n = 0;
+ for (end = strchr(ptr, '-'); ; end = strchr(ptr, '-')) {
+ if (end && end[1] != '\0') {
+ if (end[1] == '-')
+ end = &end[1];
+ end[0] = '\0';
+ }
+ keys[n] = mp_input_get_key_from_name(ptr);
+ if (keys[n] < 0)
+ return 0;
+ n++;
+ if (end && end[1] != '\0' && n < max_num_keys)
+ ptr = &end[1];
+ else
+ break;
+ }
+ *out_num_keys = n;
+ return 1;
+}
+
+void mp_print_key_list(struct mp_log *out)
+{
+ mp_info(out, "\n");
+ for (int i = 0; key_names[i].name != NULL; i++)
+ mp_info(out, "%s\n", key_names[i].name);
+}
+
+char **mp_get_key_list(void)
+{
+ char **list = NULL;
+ int num = 0;
+ for (int i = 0; key_names[i].name != NULL; i++)
+ MP_TARRAY_APPEND(NULL, list, num, talloc_strdup(NULL, key_names[i].name));
+ MP_TARRAY_APPEND(NULL, list, num, NULL);
+ return list;
+}
+
+int mp_normalize_keycode(int keycode)
+{
+ if (keycode <= 0)
+ return keycode;
+ int code = keycode & ~MP_KEY_MODIFIER_MASK;
+ int mod = keycode & MP_KEY_MODIFIER_MASK;
+ /* On normal keyboards shift changes the character code of non-special
+ * keys, so don't count the modifier separately for those. In other words
+ * we want to have "a" and "A" instead of "a" and "Shift+A"; but a separate
+ * shift modifier is still kept for special keys like arrow keys. */
+ if (code >= 32 && code < MP_KEY_BASE) {
+ /* Still try to support ASCII case-modifications properly. For example,
+ * we want to change "Shift+a" to "A", not "a". Doing this for unicode
+ * in general would require huge lookup tables, or a libc with proper
+ * unicode support, so we don't do that. */
+ if (code >= 'a' && code <= 'z' && (mod & MP_KEY_MODIFIER_SHIFT))
+ code &= 0x5F;
+ mod &= ~MP_KEY_MODIFIER_SHIFT;
+ }
+ return code | mod;
+}
diff --git a/input/keycodes.h b/input/keycodes.h
new file mode 100644
index 0000000..a5a746a
--- /dev/null
+++ b/input/keycodes.h
@@ -0,0 +1,270 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_KEYCODES_H
+#define MPLAYER_KEYCODES_H
+
+// Keys in the range [0, MP_KEY_BASE) follow unicode.
+// Special keys come after this.
+#define MP_KEY_BASE (1<<21)
+
+// printable, and valid unicode range (we don't care too much about whether
+// certain sub-ranges are reserved and disallowed, like surrogate pairs)
+#define MP_KEY_IS_UNICODE(key) ((key) >= 32 && (key) <= 0x10FFFF)
+
+#define MP_KEY_ENTER 13
+#define MP_KEY_TAB 9
+
+/* Control keys */
+#define MP_KEY_BACKSPACE (MP_KEY_BASE+0)
+#define MP_KEY_DELETE (MP_KEY_BASE+1)
+#define MP_KEY_INSERT (MP_KEY_BASE+2)
+#define MP_KEY_HOME (MP_KEY_BASE+3)
+#define MP_KEY_END (MP_KEY_BASE+4)
+#define MP_KEY_PAGE_UP (MP_KEY_BASE+5)
+#define MP_KEY_PAGE_DOWN (MP_KEY_BASE+6)
+#define MP_KEY_ESC (MP_KEY_BASE+7)
+#define MP_KEY_PRINT (MP_KEY_BASE+8)
+
+/* Control keys short name */
+#define MP_KEY_BS MP_KEY_BACKSPACE
+#define MP_KEY_DEL MP_KEY_DELETE
+#define MP_KEY_INS MP_KEY_INSERT
+#define MP_KEY_PGUP MP_KEY_PAGE_UP
+#define MP_KEY_PGDOWN MP_KEY_PAGE_DOWN
+#define MP_KEY_PGDWN MP_KEY_PAGE_DOWN
+
+/* Cursor movement */
+#define MP_KEY_CRSR (MP_KEY_BASE+0x10)
+#define MP_KEY_RIGHT (MP_KEY_CRSR+0)
+#define MP_KEY_LEFT (MP_KEY_CRSR+1)
+#define MP_KEY_DOWN (MP_KEY_CRSR+2)
+#define MP_KEY_UP (MP_KEY_CRSR+3)
+
+/* Multimedia/internet keyboard/remote keys */
+#define MP_KEY_MM_BASE (MP_KEY_BASE+0x20)
+#define MP_KEY_POWER (MP_KEY_MM_BASE+0)
+#define MP_KEY_MENU (MP_KEY_MM_BASE+1)
+#define MP_KEY_PLAY (MP_KEY_MM_BASE+2)
+#define MP_KEY_PAUSE (MP_KEY_MM_BASE+3)
+#define MP_KEY_PLAYPAUSE (MP_KEY_MM_BASE+4)
+#define MP_KEY_STOP (MP_KEY_MM_BASE+5)
+#define MP_KEY_FORWARD (MP_KEY_MM_BASE+6)
+#define MP_KEY_REWIND (MP_KEY_MM_BASE+7)
+#define MP_KEY_NEXT (MP_KEY_MM_BASE+8)
+#define MP_KEY_PREV (MP_KEY_MM_BASE+9)
+#define MP_KEY_VOLUME_UP (MP_KEY_MM_BASE+10)
+#define MP_KEY_VOLUME_DOWN (MP_KEY_MM_BASE+11)
+#define MP_KEY_MUTE (MP_KEY_MM_BASE+12)
+#define MP_KEY_HOMEPAGE (MP_KEY_MM_BASE+13)
+#define MP_KEY_WWW (MP_KEY_MM_BASE+14)
+#define MP_KEY_MAIL (MP_KEY_MM_BASE+15)
+#define MP_KEY_FAVORITES (MP_KEY_MM_BASE+16)
+#define MP_KEY_SEARCH (MP_KEY_MM_BASE+17)
+#define MP_KEY_SLEEP (MP_KEY_MM_BASE+18)
+#define MP_KEY_CANCEL (MP_KEY_MM_BASE+19)
+#define MP_KEY_RECORD (MP_KEY_MM_BASE+20)
+#define MP_KEY_CHANNEL_UP (MP_KEY_MM_BASE+21)
+#define MP_KEY_CHANNEL_DOWN (MP_KEY_MM_BASE+22)
+#define MP_KEY_PLAYONLY (MP_KEY_MM_BASE+23)
+#define MP_KEY_PAUSEONLY (MP_KEY_MM_BASE+24)
+#define MP_KEY_BACK (MP_KEY_MM_BASE+25)
+#define MP_KEY_TOOLS (MP_KEY_MM_BASE+26)
+#define MP_KEY_ZOOMIN (MP_KEY_MM_BASE+27)
+#define MP_KEY_ZOOMOUT (MP_KEY_MM_BASE+28)
+
+/* Function keys */
+#define MP_KEY_F (MP_KEY_BASE+0x40)
+
+/* Keypad keys */
+#define MP_KEY_KEYPAD (MP_KEY_BASE+0x60)
+#define MP_KEY_KP0 (MP_KEY_KEYPAD+0)
+#define MP_KEY_KP1 (MP_KEY_KEYPAD+1)
+#define MP_KEY_KP2 (MP_KEY_KEYPAD+2)
+#define MP_KEY_KP3 (MP_KEY_KEYPAD+3)
+#define MP_KEY_KP4 (MP_KEY_KEYPAD+4)
+#define MP_KEY_KP5 (MP_KEY_KEYPAD+5)
+#define MP_KEY_KP6 (MP_KEY_KEYPAD+6)
+#define MP_KEY_KP7 (MP_KEY_KEYPAD+7)
+#define MP_KEY_KP8 (MP_KEY_KEYPAD+8)
+#define MP_KEY_KP9 (MP_KEY_KEYPAD+9)
+#define MP_KEY_KPDEC (MP_KEY_KEYPAD+10)
+#define MP_KEY_KPINS (MP_KEY_KEYPAD+11)
+#define MP_KEY_KPDEL (MP_KEY_KEYPAD+12)
+#define MP_KEY_KPENTER (MP_KEY_KEYPAD+13)
+#define MP_KEY_KPHOME (MP_KEY_KEYPAD+14)
+#define MP_KEY_KPEND (MP_KEY_KEYPAD+15)
+#define MP_KEY_KPPGUP (MP_KEY_KEYPAD+16)
+#define MP_KEY_KPPGDOWN (MP_KEY_KEYPAD+17)
+#define MP_KEY_KPRIGHT (MP_KEY_KEYPAD+18)
+#define MP_KEY_KPLEFT (MP_KEY_KEYPAD+19)
+#define MP_KEY_KPDOWN (MP_KEY_KEYPAD+20)
+#define MP_KEY_KPUP (MP_KEY_KEYPAD+21)
+
+// Mouse events from VOs
+#define MP_MBTN_BASE ((MP_KEY_BASE+0xA0)|MP_NO_REPEAT_KEY|MP_KEY_EMIT_ON_UP)
+#define MP_MBTN_LEFT (MP_MBTN_BASE+0)
+#define MP_MBTN_MID (MP_MBTN_BASE+1)
+#define MP_MBTN_RIGHT (MP_MBTN_BASE+2)
+#define MP_WHEEL_UP (MP_MBTN_BASE+3)
+#define MP_WHEEL_DOWN (MP_MBTN_BASE+4)
+#define MP_WHEEL_LEFT (MP_MBTN_BASE+5)
+#define MP_WHEEL_RIGHT (MP_MBTN_BASE+6)
+#define MP_MBTN_BACK (MP_MBTN_BASE+7)
+#define MP_MBTN_FORWARD (MP_MBTN_BASE+8)
+#define MP_MBTN9 (MP_MBTN_BASE+9)
+#define MP_MBTN10 (MP_MBTN_BASE+10)
+#define MP_MBTN11 (MP_MBTN_BASE+11)
+#define MP_MBTN12 (MP_MBTN_BASE+12)
+#define MP_MBTN13 (MP_MBTN_BASE+13)
+#define MP_MBTN14 (MP_MBTN_BASE+14)
+#define MP_MBTN15 (MP_MBTN_BASE+15)
+#define MP_MBTN16 (MP_MBTN_BASE+16)
+#define MP_MBTN17 (MP_MBTN_BASE+17)
+#define MP_MBTN18 (MP_MBTN_BASE+18)
+#define MP_MBTN19 (MP_MBTN_BASE+19)
+#define MP_MBTN_END (MP_MBTN_BASE+20)
+
+#define MP_KEY_IS_MOUSE_BTN_SINGLE(code) \
+ ((code) >= MP_MBTN_BASE && (code) < MP_MBTN_END)
+#define MP_KEY_IS_WHEEL(code) \
+ ((code) >= MP_WHEEL_UP && (code) <= MP_WHEEL_RIGHT)
+
+#define MP_MBTN_DBL_BASE ((MP_KEY_BASE+0xC0)|MP_NO_REPEAT_KEY)
+#define MP_MBTN_LEFT_DBL (MP_MBTN_DBL_BASE+0)
+#define MP_MBTN_MID_DBL (MP_MBTN_DBL_BASE+1)
+#define MP_MBTN_RIGHT_DBL (MP_MBTN_DBL_BASE+2)
+#define MP_MBTN_DBL_END (MP_MBTN_DBL_BASE+20)
+
+#define MP_KEY_IS_MOUSE_BTN_DBL(code) \
+ ((code) >= MP_MBTN_DBL_BASE && (code) < MP_MBTN_DBL_END)
+
+#define MP_KEY_MOUSE_BTN_COUNT (MP_MBTN_END - MP_MBTN_BASE)
+
+/* game controller keys */
+#define MP_KEY_GAMEPAD (MP_KEY_BASE+0xF0)
+#define MP_KEY_GAMEPAD_ACTION_DOWN (MP_KEY_GAMEPAD+0)
+#define MP_KEY_GAMEPAD_ACTION_RIGHT (MP_KEY_GAMEPAD+1)
+#define MP_KEY_GAMEPAD_ACTION_LEFT (MP_KEY_GAMEPAD+2)
+#define MP_KEY_GAMEPAD_ACTION_UP (MP_KEY_GAMEPAD+3)
+#define MP_KEY_GAMEPAD_BACK (MP_KEY_GAMEPAD+4)
+#define MP_KEY_GAMEPAD_MENU (MP_KEY_GAMEPAD+5)
+#define MP_KEY_GAMEPAD_START (MP_KEY_GAMEPAD+6)
+#define MP_KEY_GAMEPAD_LEFT_SHOULDER (MP_KEY_GAMEPAD+7)
+#define MP_KEY_GAMEPAD_RIGHT_SHOULDER (MP_KEY_GAMEPAD+8)
+#define MP_KEY_GAMEPAD_LEFT_TRIGGER (MP_KEY_GAMEPAD+9)
+#define MP_KEY_GAMEPAD_RIGHT_TRIGGER (MP_KEY_GAMEPAD+10)
+#define MP_KEY_GAMEPAD_LEFT_STICK (MP_KEY_GAMEPAD+11)
+#define MP_KEY_GAMEPAD_RIGHT_STICK (MP_KEY_GAMEPAD+12)
+#define MP_KEY_GAMEPAD_DPAD_UP (MP_KEY_GAMEPAD+13)
+#define MP_KEY_GAMEPAD_DPAD_DOWN (MP_KEY_GAMEPAD+14)
+#define MP_KEY_GAMEPAD_DPAD_LEFT (MP_KEY_GAMEPAD+15)
+#define MP_KEY_GAMEPAD_DPAD_RIGHT (MP_KEY_GAMEPAD+16)
+#define MP_KEY_GAMEPAD_LEFT_STICK_UP (MP_KEY_GAMEPAD+17)
+#define MP_KEY_GAMEPAD_LEFT_STICK_DOWN (MP_KEY_GAMEPAD+18)
+#define MP_KEY_GAMEPAD_LEFT_STICK_LEFT (MP_KEY_GAMEPAD+19)
+#define MP_KEY_GAMEPAD_LEFT_STICK_RIGHT (MP_KEY_GAMEPAD+20)
+#define MP_KEY_GAMEPAD_RIGHT_STICK_UP (MP_KEY_GAMEPAD+21)
+#define MP_KEY_GAMEPAD_RIGHT_STICK_DOWN (MP_KEY_GAMEPAD+22)
+#define MP_KEY_GAMEPAD_RIGHT_STICK_LEFT (MP_KEY_GAMEPAD+23)
+#define MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT (MP_KEY_GAMEPAD+24)
+
+// Reserved area. Can be used for keys that have no explicit names assigned,
+// but should be mappable by the user anyway.
+#define MP_KEY_UNKNOWN_RESERVED_START (MP_KEY_BASE+0x10000)
+#define MP_KEY_UNKNOWN_RESERVED_LAST (MP_KEY_BASE+0x20000-1)
+
+/* Special keys */
+#define MP_KEY_INTERN (MP_KEY_BASE+0x20000)
+#define MP_KEY_CLOSE_WIN (MP_KEY_INTERN+0)
+// Generated by input.c (VOs use mp_input_set_mouse_pos())
+#define MP_KEY_MOUSE_MOVE ((MP_KEY_INTERN+1)|MP_NO_REPEAT_KEY)
+#define MP_KEY_MOUSE_LEAVE ((MP_KEY_INTERN+2)|MP_NO_REPEAT_KEY)
+#define MP_KEY_MOUSE_ENTER ((MP_KEY_INTERN+3)|MP_NO_REPEAT_KEY)
+
+#define MP_KEY_IS_MOUSE_CLICK(code) \
+ (MP_KEY_IS_MOUSE_BTN_SINGLE(code) || MP_KEY_IS_MOUSE_BTN_DBL(code))
+
+#define MP_KEY_IS_MOUSE_MOVE(code) \
+ ((code) == MP_KEY_MOUSE_MOVE || (code) == MP_KEY_MOUSE_ENTER || \
+ (code) == MP_KEY_MOUSE_LEAVE)
+
+// Whether to dispatch the key binding by current mouse position.
+#define MP_KEY_DEPENDS_ON_MOUSE_POS(code) \
+ (MP_KEY_IS_MOUSE_CLICK(code) || (code) == MP_KEY_MOUSE_MOVE)
+
+#define MP_KEY_IS_MOUSE(code) \
+ (MP_KEY_IS_MOUSE_CLICK(code) || MP_KEY_IS_MOUSE_MOVE(code))
+
+// No input source should generate this.
+#define MP_KEY_UNMAPPED (MP_KEY_INTERN+4)
+#define MP_KEY_ANY_UNICODE (MP_KEY_INTERN+5)
+// For mp_input_put_key(): release all keys that are down.
+#define MP_INPUT_RELEASE_ALL (MP_KEY_INTERN+6)
+
+// Emit a command even on key-up (normally key-up is ignored). This means by
+// default they binding will be triggered on key-up instead of key-down.
+// This is a fixed part of the keycode, not a modifier than can change.
+#define MP_KEY_EMIT_ON_UP (1u<<22)
+
+// Use this when the key shouldn't be auto-repeated (like mouse buttons)
+// Also means both key-down key-up events produce emit bound commands.
+// This is a fixed part of the keycode, not a modifier than can change.
+#define MP_NO_REPEAT_KEY (1u<<23)
+
+/* Modifiers added to individual keys */
+#define MP_KEY_MODIFIER_SHIFT (1u<<24)
+#define MP_KEY_MODIFIER_CTRL (1u<<25)
+#define MP_KEY_MODIFIER_ALT (1u<<26)
+#define MP_KEY_MODIFIER_META (1u<<27)
+
+// Flag for key events. Multiple down events are idempotent. Release keys by
+// sending the key code with KEY_STATE_UP set, or by sending
+// MP_INPUT_RELEASE_ALL as key code.
+#define MP_KEY_STATE_DOWN (1u<<28)
+
+// Flag for key events. Releases a key previously held down with
+// MP_KEY_STATE_DOWN. Do not send redundant UP events and do not forget to
+// release keys at all with UP. If input is unreliable, use MP_INPUT_RELEASE_ALL
+// or don't use MP_KEY_STATE_DOWN in the first place.
+#define MP_KEY_STATE_UP (1u<<29)
+
+#define MP_KEY_MODIFIER_MASK (MP_KEY_MODIFIER_SHIFT | MP_KEY_MODIFIER_CTRL | \
+ MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_META | \
+ MP_KEY_STATE_DOWN | MP_KEY_STATE_UP)
+
+// Makes adjustments like turning "shift+z" into "Z"
+int mp_normalize_keycode(int keycode);
+
+// Get input key from its name.
+int mp_input_get_key_from_name(const char *name);
+
+// Return given key (plus modifiers) as talloc'ed name.
+char *mp_input_get_key_name(int key);
+
+// Combination of multiple keys to string.
+char *mp_input_get_key_combo_name(const int *keys, int max);
+
+// String containing combination of multiple string to keys.
+int mp_input_get_keys_from_string(char *str, int max_num_keys,
+ int *out_num_keys, int *keys);
+
+struct mp_log;
+void mp_print_key_list(struct mp_log *out);
+char **mp_get_key_list(void);
+
+#endif /* MPLAYER_KEYCODES_H */
diff --git a/input/meson.build b/input/meson.build
new file mode 100644
index 0000000..12fe732
--- /dev/null
+++ b/input/meson.build
@@ -0,0 +1,20 @@
+icons = ['16', '32', '64', '128']
+foreach size: icons
+ name = 'mpv-icon-8bit-'+size+'x'+size+'.png'
+ icon = custom_target(name,
+ input: join_paths(source_root, 'etc', name),
+ output: name + '.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+ )
+ sources += icon
+endforeach
+
+etc_files = ['input.conf', 'builtin.conf']
+foreach file: etc_files
+ etc_file = custom_target(file,
+ input: join_paths(source_root, 'etc', file),
+ output: file + '.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+ )
+ sources += etc_file
+endforeach
diff --git a/input/sdl_gamepad.c b/input/sdl_gamepad.c
new file mode 100644
index 0000000..790c945
--- /dev/null
+++ b/input/sdl_gamepad.c
@@ -0,0 +1,287 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+
+#include <SDL.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "input.h"
+#include "input/keycodes.h"
+#include "osdep/threads.h"
+
+struct gamepad_priv {
+ SDL_GameController *controller;
+};
+
+static Uint32 gamepad_cancel_wakeup;
+
+static void initialize_events(void)
+{
+ gamepad_cancel_wakeup = SDL_RegisterEvents(1);
+}
+
+static mp_once events_initialized = MP_STATIC_ONCE_INITIALIZER;
+
+#define INVALID_KEY -1
+
+static const int button_map[][2] = {
+ { SDL_CONTROLLER_BUTTON_A, MP_KEY_GAMEPAD_ACTION_DOWN },
+ { SDL_CONTROLLER_BUTTON_B, MP_KEY_GAMEPAD_ACTION_RIGHT },
+ { SDL_CONTROLLER_BUTTON_X, MP_KEY_GAMEPAD_ACTION_LEFT },
+ { SDL_CONTROLLER_BUTTON_Y, MP_KEY_GAMEPAD_ACTION_UP },
+ { SDL_CONTROLLER_BUTTON_BACK, MP_KEY_GAMEPAD_BACK },
+ { SDL_CONTROLLER_BUTTON_GUIDE, MP_KEY_GAMEPAD_MENU },
+ { SDL_CONTROLLER_BUTTON_START, MP_KEY_GAMEPAD_START },
+ { SDL_CONTROLLER_BUTTON_LEFTSTICK, MP_KEY_GAMEPAD_LEFT_STICK },
+ { SDL_CONTROLLER_BUTTON_RIGHTSTICK, MP_KEY_GAMEPAD_RIGHT_STICK },
+ { SDL_CONTROLLER_BUTTON_LEFTSHOULDER, MP_KEY_GAMEPAD_LEFT_SHOULDER },
+ { SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, MP_KEY_GAMEPAD_RIGHT_SHOULDER },
+ { SDL_CONTROLLER_BUTTON_DPAD_UP, MP_KEY_GAMEPAD_DPAD_UP },
+ { SDL_CONTROLLER_BUTTON_DPAD_DOWN, MP_KEY_GAMEPAD_DPAD_DOWN },
+ { SDL_CONTROLLER_BUTTON_DPAD_LEFT, MP_KEY_GAMEPAD_DPAD_LEFT },
+ { SDL_CONTROLLER_BUTTON_DPAD_RIGHT, MP_KEY_GAMEPAD_DPAD_RIGHT },
+};
+
+static const int analog_map[][5] = {
+ // 0 -> sdl enum
+ // 1 -> negative state
+ // 2 -> neutral-negative state
+ // 3 -> neutral-positive state
+ // 4 -> positive state
+ { SDL_CONTROLLER_AXIS_LEFTX,
+ MP_KEY_GAMEPAD_LEFT_STICK_LEFT | MP_KEY_STATE_DOWN,
+ MP_KEY_GAMEPAD_LEFT_STICK_LEFT | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_LEFT_STICK_RIGHT | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_LEFT_STICK_RIGHT | MP_KEY_STATE_DOWN },
+
+ { SDL_CONTROLLER_AXIS_LEFTY,
+ MP_KEY_GAMEPAD_LEFT_STICK_UP | MP_KEY_STATE_DOWN,
+ MP_KEY_GAMEPAD_LEFT_STICK_UP | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_LEFT_STICK_DOWN | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_LEFT_STICK_DOWN | MP_KEY_STATE_DOWN },
+
+ { SDL_CONTROLLER_AXIS_RIGHTX,
+ MP_KEY_GAMEPAD_RIGHT_STICK_LEFT | MP_KEY_STATE_DOWN,
+ MP_KEY_GAMEPAD_RIGHT_STICK_LEFT | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT | MP_KEY_STATE_DOWN },
+
+ { SDL_CONTROLLER_AXIS_RIGHTY,
+ MP_KEY_GAMEPAD_RIGHT_STICK_UP | MP_KEY_STATE_DOWN,
+ MP_KEY_GAMEPAD_RIGHT_STICK_UP | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_RIGHT_STICK_DOWN | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_RIGHT_STICK_DOWN | MP_KEY_STATE_DOWN },
+
+ { SDL_CONTROLLER_AXIS_TRIGGERLEFT,
+ INVALID_KEY,
+ INVALID_KEY,
+ MP_KEY_GAMEPAD_LEFT_TRIGGER | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_LEFT_TRIGGER | MP_KEY_STATE_DOWN },
+
+ { SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
+ INVALID_KEY,
+ INVALID_KEY,
+ MP_KEY_GAMEPAD_RIGHT_TRIGGER | MP_KEY_STATE_UP,
+ MP_KEY_GAMEPAD_RIGHT_TRIGGER | MP_KEY_STATE_DOWN },
+};
+
+static int lookup_button_mp_key(int sdl_key)
+{
+ for (int i = 0; i < MP_ARRAY_SIZE(button_map); i++) {
+ if (button_map[i][0] == sdl_key) {
+ return button_map[i][1];
+ }
+ }
+ return INVALID_KEY;
+}
+
+static int lookup_analog_mp_key(int sdl_key, int16_t value)
+{
+ const int sdl_axis_max = 32767;
+ const int negative = 1;
+ const int negative_neutral = 2;
+ const int positive_neutral = 3;
+ const int positive = 4;
+
+ const float activation_threshold = sdl_axis_max * 0.33;
+ const float noise_threshold = sdl_axis_max * 0.06;
+
+ // sometimes SDL just keeps shitting out low values around 0 that mess
+ // with key repeating code
+ if (value < noise_threshold && value > -noise_threshold) {
+ return INVALID_KEY;
+ }
+
+ int state = value > 0 ? positive_neutral : negative_neutral;
+
+ if (value >= sdl_axis_max - activation_threshold) {
+ state = positive;
+ }
+
+ if (value <= activation_threshold - sdl_axis_max) {
+ state = negative;
+ }
+
+ for (int i = 0; i < MP_ARRAY_SIZE(analog_map); i++) {
+ if (analog_map[i][0] == sdl_key) {
+ return analog_map[i][state];
+ }
+ }
+
+ return INVALID_KEY;
+}
+
+
+static void request_cancel(struct mp_input_src *src)
+{
+ MP_VERBOSE(src, "exiting...\n");
+ SDL_Event event = { .type = gamepad_cancel_wakeup };
+ SDL_PushEvent(&event);
+}
+
+static void uninit(struct mp_input_src *src)
+{
+ MP_VERBOSE(src, "exited.\n");
+}
+
+#define GUID_LEN 33
+
+static void add_gamepad(struct mp_input_src *src, int id)
+{
+ struct gamepad_priv *p = src->priv;
+
+ if (p->controller) {
+ MP_WARN(src, "can't add more than one controller\n");
+ return;
+ }
+
+ if (SDL_IsGameController(id)) {
+ SDL_GameController *controller = SDL_GameControllerOpen(id);
+
+ if (controller) {
+ const char *name = SDL_GameControllerName(controller);
+ MP_INFO(src, "added controller: %s\n", name);
+ p->controller = controller;
+ return;
+ }
+ }
+}
+
+static void remove_gamepad(struct mp_input_src *src, int id)
+{
+ struct gamepad_priv *p = src->priv;
+ SDL_GameController *controller = p->controller;
+ SDL_Joystick* j = SDL_GameControllerGetJoystick(controller);
+ SDL_JoystickID jid = SDL_JoystickInstanceID(j);
+
+ if (controller && jid == id) {
+ const char *name = SDL_GameControllerName(controller);
+ MP_INFO(src, "removed controller: %s\n", name);
+ SDL_GameControllerClose(controller);
+ p->controller = NULL;
+ }
+}
+
+static void read_gamepad_thread(struct mp_input_src *src, void *param)
+{
+ SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1");
+
+ if (SDL_WasInit(SDL_INIT_EVENTS)) {
+ MP_ERR(src, "Another component is using SDL already.\n");
+ mp_input_src_init_done(src);
+ return;
+ }
+
+ if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER)) {
+ MP_ERR(src, "SDL_Init failed\n");
+ mp_input_src_init_done(src);
+ return;
+ }
+
+ mp_exec_once(&events_initialized, initialize_events);
+
+ if (gamepad_cancel_wakeup == (Uint32)-1) {
+ MP_ERR(src, "Can't register SDL custom events\n");
+ mp_input_src_init_done(src);
+ return;
+ }
+
+ struct gamepad_priv *p =src->priv = talloc_zero(src, struct gamepad_priv);
+ src->cancel = request_cancel;
+ src->uninit = uninit;
+
+ mp_input_src_init_done(src);
+
+ SDL_Event ev;
+
+ while (SDL_WaitEvent(&ev) != 0) {
+ if (ev.type == gamepad_cancel_wakeup) {
+ break;
+ }
+
+ switch (ev.type) {
+ case SDL_CONTROLLERDEVICEADDED: {
+ add_gamepad(src, ev.cdevice.which);
+ continue;
+ }
+ case SDL_CONTROLLERDEVICEREMOVED: {
+ remove_gamepad(src, ev.cdevice.which);
+ continue;
+ }
+ case SDL_CONTROLLERBUTTONDOWN: {
+ const int key = lookup_button_mp_key(ev.cbutton.button);
+ if (key != INVALID_KEY) {
+ mp_input_put_key(src->input_ctx, key | MP_KEY_STATE_DOWN);
+ }
+ continue;
+ }
+ case SDL_CONTROLLERBUTTONUP: {
+ const int key = lookup_button_mp_key(ev.cbutton.button);
+ if (key != INVALID_KEY) {
+ mp_input_put_key(src->input_ctx, key | MP_KEY_STATE_UP);
+ }
+ continue;
+ }
+ case SDL_CONTROLLERAXISMOTION: {
+ const int key =
+ lookup_analog_mp_key(ev.caxis.axis, ev.caxis.value);
+ if (key != INVALID_KEY) {
+ mp_input_put_key(src->input_ctx, key);
+ }
+ continue;
+ }
+
+ }
+ }
+
+ if (p->controller) {
+ SDL_Joystick* j = SDL_GameControllerGetJoystick(p->controller);
+ SDL_JoystickID jid = SDL_JoystickInstanceID(j);
+ remove_gamepad(src, jid);
+ }
+
+ // must be called on the same thread of SDL_InitSubSystem, so uninit
+ // callback can't be used for this
+ SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
+}
+
+void mp_input_sdl_gamepad_add(struct input_ctx *ictx)
+{
+ mp_input_add_thread_src(ictx, NULL, read_gamepad_thread);
+}
diff --git a/libmpv/client.h b/libmpv/client.h
new file mode 100644
index 0000000..0a548a5
--- /dev/null
+++ b/libmpv/client.h
@@ -0,0 +1,2032 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Note: the client API is licensed under ISC (see above) to enable
+ * other wrappers outside of mpv. But keep in mind that the
+ * mpv core is by default still GPLv2+ - unless built with
+ * -Dgpl=false, which makes it LGPLv2+.
+ */
+
+#ifndef MPV_CLIENT_API_H_
+#define MPV_CLIENT_API_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef _WIN32
+#define MPV_EXPORT __declspec(dllexport)
+#define MPV_SELECTANY __declspec(selectany)
+#elif defined(__GNUC__) || defined(__clang__)
+#define MPV_EXPORT __attribute__((visibility("default")))
+#define MPV_SELECTANY
+#else
+#define MPV_EXPORT
+#define MPV_SELECTANY
+#endif
+
+#ifdef __cpp_decltype
+#define MPV_DECLTYPE decltype
+#else
+#define MPV_DECLTYPE __typeof__
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Mechanisms provided by this API
+ * -------------------------------
+ *
+ * This API provides general control over mpv playback. It does not give you
+ * direct access to individual components of the player, only the whole thing.
+ * It's somewhat equivalent to MPlayer's slave mode. You can send commands,
+ * retrieve or set playback status or settings with properties, and receive
+ * events.
+ *
+ * The API can be used in two ways:
+ * 1) Internally in mpv, to provide additional features to the command line
+ * player. Lua scripting uses this. (Currently there is no plugin API to
+ * get a client API handle in external user code. It has to be a fixed
+ * part of the player at compilation time.)
+ * 2) Using mpv as a library with mpv_create(). This basically allows embedding
+ * mpv in other applications.
+ *
+ * Documentation
+ * -------------
+ *
+ * The libmpv C API is documented directly in this header. Note that most
+ * actual interaction with this player is done through
+ * options/commands/properties, which can be accessed through this API.
+ * Essentially everything is done with them, including loading a file,
+ * retrieving playback progress, and so on.
+ *
+ * These are documented elsewhere:
+ * * http://mpv.io/manual/master/#options
+ * * http://mpv.io/manual/master/#list-of-input-commands
+ * * http://mpv.io/manual/master/#properties
+ *
+ * You can also look at the examples here:
+ * * https://github.com/mpv-player/mpv-examples/tree/master/libmpv
+ *
+ * Event loop
+ * ----------
+ *
+ * In general, the API user should run an event loop in order to receive events.
+ * This event loop should call mpv_wait_event(), which will return once a new
+ * mpv client API is available. It is also possible to integrate client API
+ * usage in other event loops (e.g. GUI toolkits) with the
+ * mpv_set_wakeup_callback() function, and then polling for events by calling
+ * mpv_wait_event() with a 0 timeout.
+ *
+ * Note that the event loop is detached from the actual player. Not calling
+ * mpv_wait_event() will not stop playback. It will eventually congest the
+ * event queue of your API handle, though.
+ *
+ * Synchronous vs. asynchronous calls
+ * ----------------------------------
+ *
+ * The API allows both synchronous and asynchronous calls. Synchronous calls
+ * have to wait until the playback core is ready, which currently can take
+ * an unbounded time (e.g. if network is slow or unresponsive). Asynchronous
+ * calls just queue operations as requests, and return the result of the
+ * operation as events.
+ *
+ * Asynchronous calls
+ * ------------------
+ *
+ * The client API includes asynchronous functions. These allow you to send
+ * requests instantly, and get replies as events at a later point. The
+ * requests are made with functions carrying the _async suffix, and replies
+ * are returned by mpv_wait_event() (interleaved with the normal event stream).
+ *
+ * A 64 bit userdata value is used to allow the user to associate requests
+ * with replies. The value is passed as reply_userdata parameter to the request
+ * function. The reply to the request will have the reply
+ * mpv_event->reply_userdata field set to the same value as the
+ * reply_userdata parameter of the corresponding request.
+ *
+ * This userdata value is arbitrary and is never interpreted by the API. Note
+ * that the userdata value 0 is also allowed, but then the client must be
+ * careful not accidentally interpret the mpv_event->reply_userdata if an
+ * event is not a reply. (For non-replies, this field is set to 0.)
+ *
+ * Asynchronous calls may be reordered in arbitrarily with other synchronous
+ * and asynchronous calls. If you want a guaranteed order, you need to wait
+ * until asynchronous calls report completion before doing the next call.
+ *
+ * See also the section "Asynchronous command details" in the manpage.
+ *
+ * Multithreading
+ * --------------
+ *
+ * The client API is generally fully thread-safe, unless otherwise noted.
+ * Currently, there is no real advantage in using more than 1 thread to access
+ * the client API, since everything is serialized through a single lock in the
+ * playback core.
+ *
+ * Basic environment requirements
+ * ------------------------------
+ *
+ * This documents basic requirements on the C environment. This is especially
+ * important if mpv is used as library with mpv_create().
+ *
+ * - The LC_NUMERIC locale category must be set to "C". If your program calls
+ * setlocale(), be sure not to use LC_ALL, or if you do, reset LC_NUMERIC
+ * to its sane default: setlocale(LC_NUMERIC, "C").
+ * - If a X11 based VO is used, mpv will set the xlib error handler. This error
+ * handler is process-wide, and there's no proper way to share it with other
+ * xlib users within the same process. This might confuse GUI toolkits.
+ * - mpv uses some other libraries that are not library-safe, such as Fribidi
+ * (used through libass), ALSA, FFmpeg, and possibly more.
+ * - The FPU precision must be set at least to double precision.
+ * - On Windows, mpv will call timeBeginPeriod(1).
+ * - On memory exhaustion, mpv will kill the process.
+ * - In certain cases, mpv may start sub processes (such as with the ytdl
+ * wrapper script).
+ * - Using UNIX IPC (off by default) will override the SIGPIPE signal handler,
+ * and set it to SIG_IGN. Some invocations of the "subprocess" command will
+ * also do that.
+ * - mpv may start sub processes, so overriding SIGCHLD, or waiting on all PIDs
+ * (such as calling wait()) by the parent process or any other library within
+ * the process must be avoided. libmpv itself only waits for its own PIDs.
+ * - If anything in the process registers signal handlers, they must set the
+ * SA_RESTART flag. Otherwise you WILL get random failures on signals.
+ *
+ * Encoding of filenames
+ * ---------------------
+ *
+ * mpv uses UTF-8 everywhere.
+ *
+ * On some platforms (like Linux), filenames actually do not have to be UTF-8;
+ * for this reason libmpv supports non-UTF-8 strings. libmpv uses what the
+ * kernel uses and does not recode filenames. At least on Linux, passing a
+ * string to libmpv is like passing a string to the fopen() function.
+ *
+ * On Windows, filenames are always UTF-8, libmpv converts between UTF-8 and
+ * UTF-16 when using win32 API functions. libmpv never uses or accepts
+ * filenames in the local 8 bit encoding. It does not use fopen() either;
+ * it uses _wfopen().
+ *
+ * On OS X, filenames and other strings taken/returned by libmpv can have
+ * inconsistent unicode normalization. This can sometimes lead to problems.
+ * You have to hope for the best.
+ *
+ * Also see the remarks for MPV_FORMAT_STRING.
+ *
+ * Embedding the video window
+ * --------------------------
+ *
+ * Using the render API (in render.h) is recommended. This API requires
+ * you to create and maintain an OpenGL context, to which you can render
+ * video using a specific API call. This API does not include keyboard or mouse
+ * input directly.
+ *
+ * There is an older way to embed the native mpv window into your own. You have
+ * to get the raw window handle, and set it as "wid" option. This works on X11,
+ * win32, and OSX only. It's much easier to use than the render API, but
+ * also has various problems.
+ *
+ * Also see client API examples and the mpv manpage. There is an extensive
+ * discussion here:
+ * https://github.com/mpv-player/mpv-examples/tree/master/libmpv#methods-of-embedding-the-video-window
+ *
+ * Compatibility
+ * -------------
+ *
+ * mpv development doesn't stand still, and changes to mpv internals as well as
+ * to its interface can cause compatibility issues to client API users.
+ *
+ * The API is versioned (see MPV_CLIENT_API_VERSION), and changes to it are
+ * documented in DOCS/client-api-changes.rst. The C API itself will probably
+ * remain compatible for a long time, but the functionality exposed by it
+ * could change more rapidly. For example, it's possible that options are
+ * renamed, or change the set of allowed values.
+ *
+ * Defensive programming should be used to potentially deal with the fact that
+ * options, commands, and properties could disappear, change their value range,
+ * or change the underlying datatypes. It might be a good idea to prefer
+ * MPV_FORMAT_STRING over other types to decouple your code from potential
+ * mpv changes.
+ *
+ * Also see: DOCS/compatibility.rst
+ *
+ * Future changes
+ * --------------
+ *
+ * This are the planned changes that will most likely be done on the next major
+ * bump of the library:
+ *
+ * - remove all symbols that are marked as deprecated
+ * - reassign enum numerical values to remove gaps
+ * - disabling all events by default
+ */
+
+/**
+ * The version is incremented on each API change. The 16 lower bits form the
+ * minor version number, and the 16 higher bits the major version number. If
+ * the API becomes incompatible to previous versions, the major version
+ * number is incremented. This affects only C part, and not properties and
+ * options.
+ *
+ * Every API bump is described in DOCS/client-api-changes.rst
+ *
+ * You can use MPV_MAKE_VERSION() and compare the result with integer
+ * relational operators (<, >, <=, >=).
+ */
+#define MPV_MAKE_VERSION(major, minor) (((major) << 16) | (minor) | 0UL)
+#define MPV_CLIENT_API_VERSION MPV_MAKE_VERSION(2, 2)
+
+/**
+ * The API user is allowed to "#define MPV_ENABLE_DEPRECATED 0" before
+ * including any libmpv headers. Then deprecated symbols will be excluded
+ * from the headers. (Of course, deprecated properties and commands and
+ * other functionality will still work.)
+ */
+#ifndef MPV_ENABLE_DEPRECATED
+#define MPV_ENABLE_DEPRECATED 1
+#endif
+
+/**
+ * Return the MPV_CLIENT_API_VERSION the mpv source has been compiled with.
+ */
+MPV_EXPORT unsigned long mpv_client_api_version(void);
+
+/**
+ * Client context used by the client API. Every client has its own private
+ * handle.
+ */
+typedef struct mpv_handle mpv_handle;
+
+/**
+ * List of error codes than can be returned by API functions. 0 and positive
+ * return values always mean success, negative values are always errors.
+ */
+typedef enum mpv_error {
+ /**
+ * No error happened (used to signal successful operation).
+ * Keep in mind that many API functions returning error codes can also
+ * return positive values, which also indicate success. API users can
+ * hardcode the fact that ">= 0" means success.
+ */
+ MPV_ERROR_SUCCESS = 0,
+ /**
+ * The event ringbuffer is full. This means the client is choked, and can't
+ * receive any events. This can happen when too many asynchronous requests
+ * have been made, but not answered. Probably never happens in practice,
+ * unless the mpv core is frozen for some reason, and the client keeps
+ * making asynchronous requests. (Bugs in the client API implementation
+ * could also trigger this, e.g. if events become "lost".)
+ */
+ MPV_ERROR_EVENT_QUEUE_FULL = -1,
+ /**
+ * Memory allocation failed.
+ */
+ MPV_ERROR_NOMEM = -2,
+ /**
+ * The mpv core wasn't configured and initialized yet. See the notes in
+ * mpv_create().
+ */
+ MPV_ERROR_UNINITIALIZED = -3,
+ /**
+ * Generic catch-all error if a parameter is set to an invalid or
+ * unsupported value. This is used if there is no better error code.
+ */
+ MPV_ERROR_INVALID_PARAMETER = -4,
+ /**
+ * Trying to set an option that doesn't exist.
+ */
+ MPV_ERROR_OPTION_NOT_FOUND = -5,
+ /**
+ * Trying to set an option using an unsupported MPV_FORMAT.
+ */
+ MPV_ERROR_OPTION_FORMAT = -6,
+ /**
+ * Setting the option failed. Typically this happens if the provided option
+ * value could not be parsed.
+ */
+ MPV_ERROR_OPTION_ERROR = -7,
+ /**
+ * The accessed property doesn't exist.
+ */
+ MPV_ERROR_PROPERTY_NOT_FOUND = -8,
+ /**
+ * Trying to set or get a property using an unsupported MPV_FORMAT.
+ */
+ MPV_ERROR_PROPERTY_FORMAT = -9,
+ /**
+ * The property exists, but is not available. This usually happens when the
+ * associated subsystem is not active, e.g. querying audio parameters while
+ * audio is disabled.
+ */
+ MPV_ERROR_PROPERTY_UNAVAILABLE = -10,
+ /**
+ * Error setting or getting a property.
+ */
+ MPV_ERROR_PROPERTY_ERROR = -11,
+ /**
+ * General error when running a command with mpv_command and similar.
+ */
+ MPV_ERROR_COMMAND = -12,
+ /**
+ * Generic error on loading (usually used with mpv_event_end_file.error).
+ */
+ MPV_ERROR_LOADING_FAILED = -13,
+ /**
+ * Initializing the audio output failed.
+ */
+ MPV_ERROR_AO_INIT_FAILED = -14,
+ /**
+ * Initializing the video output failed.
+ */
+ MPV_ERROR_VO_INIT_FAILED = -15,
+ /**
+ * There was no audio or video data to play. This also happens if the
+ * file was recognized, but did not contain any audio or video streams,
+ * or no streams were selected.
+ */
+ MPV_ERROR_NOTHING_TO_PLAY = -16,
+ /**
+ * When trying to load the file, the file format could not be determined,
+ * or the file was too broken to open it.
+ */
+ MPV_ERROR_UNKNOWN_FORMAT = -17,
+ /**
+ * Generic error for signaling that certain system requirements are not
+ * fulfilled.
+ */
+ MPV_ERROR_UNSUPPORTED = -18,
+ /**
+ * The API function which was called is a stub only.
+ */
+ MPV_ERROR_NOT_IMPLEMENTED = -19,
+ /**
+ * Unspecified error.
+ */
+ MPV_ERROR_GENERIC = -20
+} mpv_error;
+
+/**
+ * Return a string describing the error. For unknown errors, the string
+ * "unknown error" is returned.
+ *
+ * @param error error number, see enum mpv_error
+ * @return A static string describing the error. The string is completely
+ * static, i.e. doesn't need to be deallocated, and is valid forever.
+ */
+MPV_EXPORT const char *mpv_error_string(int error);
+
+/**
+ * General function to deallocate memory returned by some of the API functions.
+ * Call this only if it's explicitly documented as allowed. Calling this on
+ * mpv memory not owned by the caller will lead to undefined behavior.
+ *
+ * @param data A valid pointer returned by the API, or NULL.
+ */
+MPV_EXPORT void mpv_free(void *data);
+
+/**
+ * Return the name of this client handle. Every client has its own unique
+ * name, which is mostly used for user interface purposes.
+ *
+ * @return The client name. The string is read-only and is valid until the
+ * mpv_handle is destroyed.
+ */
+MPV_EXPORT const char *mpv_client_name(mpv_handle *ctx);
+
+/**
+ * Return the ID of this client handle. Every client has its own unique ID. This
+ * ID is never reused by the core, even if the mpv_handle at hand gets destroyed
+ * and new handles get allocated.
+ *
+ * IDs are never 0 or negative.
+ *
+ * Some mpv APIs (not necessarily all) accept a name in the form "@<id>" in
+ * addition of the proper mpv_client_name(), where "<id>" is the ID in decimal
+ * form (e.g. "@123"). For example, the "script-message-to" command takes the
+ * client name as first argument, but also accepts the client ID formatted in
+ * this manner.
+ *
+ * @return The client ID.
+ */
+MPV_EXPORT int64_t mpv_client_id(mpv_handle *ctx);
+
+/**
+ * Create a new mpv instance and an associated client API handle to control
+ * the mpv instance. This instance is in a pre-initialized state,
+ * and needs to be initialized to be actually used with most other API
+ * functions.
+ *
+ * Some API functions will return MPV_ERROR_UNINITIALIZED in the uninitialized
+ * state. You can call mpv_set_property() (or mpv_set_property_string() and
+ * other variants, and before mpv 0.21.0 mpv_set_option() etc.) to set initial
+ * options. After this, call mpv_initialize() to start the player, and then use
+ * e.g. mpv_command() to start playback of a file.
+ *
+ * The point of separating handle creation and actual initialization is that
+ * you can configure things which can't be changed during runtime.
+ *
+ * Unlike the command line player, this will have initial settings suitable
+ * for embedding in applications. The following settings are different:
+ * - stdin/stdout/stderr and the terminal will never be accessed. This is
+ * equivalent to setting the --no-terminal option.
+ * (Technically, this also suppresses C signal handling.)
+ * - No config files will be loaded. This is roughly equivalent to using
+ * --config=no. Since libmpv 1.15, you can actually re-enable this option,
+ * which will make libmpv load config files during mpv_initialize(). If you
+ * do this, you are strongly encouraged to set the "config-dir" option too.
+ * (Otherwise it will load the mpv command line player's config.)
+ * For example:
+ * mpv_set_option_string(mpv, "config-dir", "/my/path"); // set config root
+ * mpv_set_option_string(mpv, "config", "yes"); // enable config loading
+ * (call mpv_initialize() _after_ this)
+ * - Idle mode is enabled, which means the playback core will enter idle mode
+ * if there are no more files to play on the internal playlist, instead of
+ * exiting. This is equivalent to the --idle option.
+ * - Disable parts of input handling.
+ * - Most of the different settings can be viewed with the command line player
+ * by running "mpv --show-profile=libmpv".
+ *
+ * All this assumes that API users want a mpv instance that is strictly
+ * isolated from the command line player's configuration, user settings, and
+ * so on. You can re-enable disabled features by setting the appropriate
+ * options.
+ *
+ * The mpv command line parser is not available through this API, but you can
+ * set individual options with mpv_set_property(). Files for playback must be
+ * loaded with mpv_command() or others.
+ *
+ * Note that you should avoid doing concurrent accesses on the uninitialized
+ * client handle. (Whether concurrent access is definitely allowed or not has
+ * yet to be decided.)
+ *
+ * @return a new mpv client API handle. Returns NULL on error. Currently, this
+ * can happen in the following situations:
+ * - out of memory
+ * - LC_NUMERIC is not set to "C" (see general remarks)
+ */
+MPV_EXPORT mpv_handle *mpv_create(void);
+
+/**
+ * Initialize an uninitialized mpv instance. If the mpv instance is already
+ * running, an error is returned.
+ *
+ * This function needs to be called to make full use of the client API if the
+ * client API handle was created with mpv_create().
+ *
+ * Only the following options are required to be set _before_ mpv_initialize():
+ * - options which are only read at initialization time:
+ * - config
+ * - config-dir
+ * - input-conf
+ * - load-scripts
+ * - script
+ * - player-operation-mode
+ * - input-app-events (OSX)
+ * - all encoding mode options
+ *
+ * @return error code
+ */
+MPV_EXPORT int mpv_initialize(mpv_handle *ctx);
+
+/**
+ * Disconnect and destroy the mpv_handle. ctx will be deallocated with this
+ * API call.
+ *
+ * If the last mpv_handle is detached, the core player is destroyed. In
+ * addition, if there are only weak mpv_handles (such as created by
+ * mpv_create_weak_client() or internal scripts), these mpv_handles will
+ * be sent MPV_EVENT_SHUTDOWN. This function may block until these clients
+ * have responded to the shutdown event, and the core is finally destroyed.
+ */
+MPV_EXPORT void mpv_destroy(mpv_handle *ctx);
+
+/**
+ * Similar to mpv_destroy(), but brings the player and all clients down
+ * as well, and waits until all of them are destroyed. This function blocks. The
+ * advantage over mpv_destroy() is that while mpv_destroy() merely
+ * detaches the client handle from the player, this function quits the player,
+ * waits until all other clients are destroyed (i.e. all mpv_handles are
+ * detached), and also waits for the final termination of the player.
+ *
+ * Since mpv_destroy() is called somewhere on the way, it's not safe to
+ * call other functions concurrently on the same context.
+ *
+ * Since mpv client API version 1.29:
+ * The first call on any mpv_handle will block until the core is destroyed.
+ * This means it will wait until other mpv_handle have been destroyed. If you
+ * want asynchronous destruction, just run the "quit" command, and then react
+ * to the MPV_EVENT_SHUTDOWN event.
+ * If another mpv_handle already called mpv_terminate_destroy(), this call will
+ * not actually block. It will destroy the mpv_handle, and exit immediately,
+ * while other mpv_handles might still be uninitializing.
+ *
+ * Before mpv client API version 1.29:
+ * If this is called on a mpv_handle that was not created with mpv_create(),
+ * this function will merely send a quit command and then call
+ * mpv_destroy(), without waiting for the actual shutdown.
+ */
+MPV_EXPORT void mpv_terminate_destroy(mpv_handle *ctx);
+
+/**
+ * Create a new client handle connected to the same player core as ctx. This
+ * context has its own event queue, its own mpv_request_event() state, its own
+ * mpv_request_log_messages() state, its own set of observed properties, and
+ * its own state for asynchronous operations. Otherwise, everything is shared.
+ *
+ * This handle should be destroyed with mpv_destroy() if no longer
+ * needed. The core will live as long as there is at least 1 handle referencing
+ * it. Any handle can make the core quit, which will result in every handle
+ * receiving MPV_EVENT_SHUTDOWN.
+ *
+ * This function can not be called before the main handle was initialized with
+ * mpv_initialize(). The new handle is always initialized, unless ctx=NULL was
+ * passed.
+ *
+ * @param ctx Used to get the reference to the mpv core; handle-specific
+ * settings and parameters are not used.
+ * If NULL, this function behaves like mpv_create() (ignores name).
+ * @param name The client name. This will be returned by mpv_client_name(). If
+ * the name is already in use, or contains non-alphanumeric
+ * characters (other than '_'), the name is modified to fit.
+ * If NULL, an arbitrary name is automatically chosen.
+ * @return a new handle, or NULL on error
+ */
+MPV_EXPORT mpv_handle *mpv_create_client(mpv_handle *ctx, const char *name);
+
+/**
+ * This is the same as mpv_create_client(), but the created mpv_handle is
+ * treated as a weak reference. If all mpv_handles referencing a core are
+ * weak references, the core is automatically destroyed. (This still goes
+ * through normal uninit of course. Effectively, if the last non-weak mpv_handle
+ * is destroyed, then the weak mpv_handles receive MPV_EVENT_SHUTDOWN and are
+ * asked to terminate as well.)
+ *
+ * Note if you want to use this like refcounting: you have to be aware that
+ * mpv_terminate_destroy() _and_ mpv_destroy() for the last non-weak
+ * mpv_handle will block until all weak mpv_handles are destroyed.
+ */
+MPV_EXPORT mpv_handle *mpv_create_weak_client(mpv_handle *ctx, const char *name);
+
+/**
+ * Load a config file. This loads and parses the file, and sets every entry in
+ * the config file's default section as if mpv_set_option_string() is called.
+ *
+ * The filename should be an absolute path. If it isn't, the actual path used
+ * is unspecified. (Note: an absolute path starts with '/' on UNIX.) If the
+ * file wasn't found, MPV_ERROR_INVALID_PARAMETER is returned.
+ *
+ * If a fatal error happens when parsing a config file, MPV_ERROR_OPTION_ERROR
+ * is returned. Errors when setting options as well as other types or errors
+ * are ignored (even if options do not exist). You can still try to capture
+ * the resulting error messages with mpv_request_log_messages(). Note that it's
+ * possible that some options were successfully set even if any of these errors
+ * happen.
+ *
+ * @param filename absolute path to the config file on the local filesystem
+ * @return error code
+ */
+MPV_EXPORT int mpv_load_config_file(mpv_handle *ctx, const char *filename);
+
+/**
+ * Return the internal time in nanoseconds. This has an arbitrary start offset,
+ * but will never wrap or go backwards.
+ *
+ * Note that this is always the real time, and doesn't necessarily have to do
+ * with playback time. For example, playback could go faster or slower due to
+ * playback speed, or due to playback being paused. Use the "time-pos" property
+ * instead to get the playback status.
+ *
+ * Unlike other libmpv APIs, this can be called at absolutely any time (even
+ * within wakeup callbacks), as long as the context is valid.
+ *
+ * Safe to be called from mpv render API threads.
+ */
+MPV_EXPORT int64_t mpv_get_time_ns(mpv_handle *ctx);
+
+/**
+ * Same as mpv_get_time_ns but in microseconds.
+ */
+MPV_EXPORT int64_t mpv_get_time_us(mpv_handle *ctx);
+
+/**
+ * Data format for options and properties. The API functions to get/set
+ * properties and options support multiple formats, and this enum describes
+ * them.
+ */
+typedef enum mpv_format {
+ /**
+ * Invalid. Sometimes used for empty values. This is always defined to 0,
+ * so a normal 0-init of mpv_format (or e.g. mpv_node) is guaranteed to set
+ * this it to MPV_FORMAT_NONE (which makes some things saner as consequence).
+ */
+ MPV_FORMAT_NONE = 0,
+ /**
+ * The basic type is char*. It returns the raw property string, like
+ * using ${=property} in input.conf (see input.rst).
+ *
+ * NULL isn't an allowed value.
+ *
+ * Warning: although the encoding is usually UTF-8, this is not always the
+ * case. File tags often store strings in some legacy codepage,
+ * and even filenames don't necessarily have to be in UTF-8 (at
+ * least on Linux). If you pass the strings to code that requires
+ * valid UTF-8, you have to sanitize it in some way.
+ * On Windows, filenames are always UTF-8, and libmpv converts
+ * between UTF-8 and UTF-16 when using win32 API functions. See
+ * the "Encoding of filenames" section for details.
+ *
+ * Example for reading:
+ *
+ * char *result = NULL;
+ * if (mpv_get_property(ctx, "property", MPV_FORMAT_STRING, &result) < 0)
+ * goto error;
+ * printf("%s\n", result);
+ * mpv_free(result);
+ *
+ * Or just use mpv_get_property_string().
+ *
+ * Example for writing:
+ *
+ * char *value = "the new value";
+ * // yep, you pass the address to the variable
+ * // (needed for symmetry with other types and mpv_get_property)
+ * mpv_set_property(ctx, "property", MPV_FORMAT_STRING, &value);
+ *
+ * Or just use mpv_set_property_string().
+ *
+ */
+ MPV_FORMAT_STRING = 1,
+ /**
+ * The basic type is char*. It returns the OSD property string, like
+ * using ${property} in input.conf (see input.rst). In many cases, this
+ * is the same as the raw string, but in other cases it's formatted for
+ * display on OSD. It's intended to be human readable. Do not attempt to
+ * parse these strings.
+ *
+ * Only valid when doing read access. The rest works like MPV_FORMAT_STRING.
+ */
+ MPV_FORMAT_OSD_STRING = 2,
+ /**
+ * The basic type is int. The only allowed values are 0 ("no")
+ * and 1 ("yes").
+ *
+ * Example for reading:
+ *
+ * int result;
+ * if (mpv_get_property(ctx, "property", MPV_FORMAT_FLAG, &result) < 0)
+ * goto error;
+ * printf("%s\n", result ? "true" : "false");
+ *
+ * Example for writing:
+ *
+ * int flag = 1;
+ * mpv_set_property(ctx, "property", MPV_FORMAT_FLAG, &flag);
+ */
+ MPV_FORMAT_FLAG = 3,
+ /**
+ * The basic type is int64_t.
+ */
+ MPV_FORMAT_INT64 = 4,
+ /**
+ * The basic type is double.
+ */
+ MPV_FORMAT_DOUBLE = 5,
+ /**
+ * The type is mpv_node.
+ *
+ * For reading, you usually would pass a pointer to a stack-allocated
+ * mpv_node value to mpv, and when you're done you call
+ * mpv_free_node_contents(&node).
+ * You're expected not to write to the data - if you have to, copy it
+ * first (which you have to do manually).
+ *
+ * For writing, you construct your own mpv_node, and pass a pointer to the
+ * API. The API will never write to your data (and copy it if needed), so
+ * you're free to use any form of allocation or memory management you like.
+ *
+ * Warning: when reading, always check the mpv_node.format member. For
+ * example, properties might change their type in future versions
+ * of mpv, or sometimes even during runtime.
+ *
+ * Example for reading:
+ *
+ * mpv_node result;
+ * if (mpv_get_property(ctx, "property", MPV_FORMAT_NODE, &result) < 0)
+ * goto error;
+ * printf("format=%d\n", (int)result.format);
+ * mpv_free_node_contents(&result).
+ *
+ * Example for writing:
+ *
+ * mpv_node value;
+ * value.format = MPV_FORMAT_STRING;
+ * value.u.string = "hello";
+ * mpv_set_property(ctx, "property", MPV_FORMAT_NODE, &value);
+ */
+ MPV_FORMAT_NODE = 6,
+ /**
+ * Used with mpv_node only. Can usually not be used directly.
+ */
+ MPV_FORMAT_NODE_ARRAY = 7,
+ /**
+ * See MPV_FORMAT_NODE_ARRAY.
+ */
+ MPV_FORMAT_NODE_MAP = 8,
+ /**
+ * A raw, untyped byte array. Only used only with mpv_node, and only in
+ * some very specific situations. (Some commands use it.)
+ */
+ MPV_FORMAT_BYTE_ARRAY = 9
+} mpv_format;
+
+/**
+ * Generic data storage.
+ *
+ * If mpv writes this struct (e.g. via mpv_get_property()), you must not change
+ * the data. In some cases (mpv_get_property()), you have to free it with
+ * mpv_free_node_contents(). If you fill this struct yourself, you're also
+ * responsible for freeing it, and you must not call mpv_free_node_contents().
+ */
+typedef struct mpv_node {
+ union {
+ char *string; /** valid if format==MPV_FORMAT_STRING */
+ int flag; /** valid if format==MPV_FORMAT_FLAG */
+ int64_t int64; /** valid if format==MPV_FORMAT_INT64 */
+ double double_; /** valid if format==MPV_FORMAT_DOUBLE */
+ /**
+ * valid if format==MPV_FORMAT_NODE_ARRAY
+ * or if format==MPV_FORMAT_NODE_MAP
+ */
+ struct mpv_node_list *list;
+ /**
+ * valid if format==MPV_FORMAT_BYTE_ARRAY
+ */
+ struct mpv_byte_array *ba;
+ } u;
+ /**
+ * Type of the data stored in this struct. This value rules what members in
+ * the given union can be accessed. The following formats are currently
+ * defined to be allowed in mpv_node:
+ *
+ * MPV_FORMAT_STRING (u.string)
+ * MPV_FORMAT_FLAG (u.flag)
+ * MPV_FORMAT_INT64 (u.int64)
+ * MPV_FORMAT_DOUBLE (u.double_)
+ * MPV_FORMAT_NODE_ARRAY (u.list)
+ * MPV_FORMAT_NODE_MAP (u.list)
+ * MPV_FORMAT_BYTE_ARRAY (u.ba)
+ * MPV_FORMAT_NONE (no member)
+ *
+ * If you encounter a value you don't know, you must not make any
+ * assumptions about the contents of union u.
+ */
+ mpv_format format;
+} mpv_node;
+
+/**
+ * (see mpv_node)
+ */
+typedef struct mpv_node_list {
+ /**
+ * Number of entries. Negative values are not allowed.
+ */
+ int num;
+ /**
+ * MPV_FORMAT_NODE_ARRAY:
+ * values[N] refers to value of the Nth item
+ *
+ * MPV_FORMAT_NODE_MAP:
+ * values[N] refers to value of the Nth key/value pair
+ *
+ * If num > 0, values[0] to values[num-1] (inclusive) are valid.
+ * Otherwise, this can be NULL.
+ */
+ mpv_node *values;
+ /**
+ * MPV_FORMAT_NODE_ARRAY:
+ * unused (typically NULL), access is not allowed
+ *
+ * MPV_FORMAT_NODE_MAP:
+ * keys[N] refers to key of the Nth key/value pair. If num > 0, keys[0] to
+ * keys[num-1] (inclusive) are valid. Otherwise, this can be NULL.
+ * The keys are in random order. The only guarantee is that keys[N] belongs
+ * to the value values[N]. NULL keys are not allowed.
+ */
+ char **keys;
+} mpv_node_list;
+
+/**
+ * (see mpv_node)
+ */
+typedef struct mpv_byte_array {
+ /**
+ * Pointer to the data. In what format the data is stored is up to whatever
+ * uses MPV_FORMAT_BYTE_ARRAY.
+ */
+ void *data;
+ /**
+ * Size of the data pointed to by ptr.
+ */
+ size_t size;
+} mpv_byte_array;
+
+/**
+ * Frees any data referenced by the node. It doesn't free the node itself.
+ * Call this only if the mpv client API set the node. If you constructed the
+ * node yourself (manually), you have to free it yourself.
+ *
+ * If node->format is MPV_FORMAT_NONE, this call does nothing. Likewise, if
+ * the client API sets a node with this format, this function doesn't need to
+ * be called. (This is just a clarification that there's no danger of anything
+ * strange happening in these cases.)
+ */
+MPV_EXPORT void mpv_free_node_contents(mpv_node *node);
+
+/**
+ * Set an option. Note that you can't normally set options during runtime. It
+ * works in uninitialized state (see mpv_create()), and in some cases in at
+ * runtime.
+ *
+ * Using a format other than MPV_FORMAT_NODE is equivalent to constructing a
+ * mpv_node with the given format and data, and passing the mpv_node to this
+ * function.
+ *
+ * Note: this is semi-deprecated. For most purposes, this is not needed anymore.
+ * Starting with mpv version 0.21.0 (version 1.23) most options can be set
+ * with mpv_set_property() (and related functions), and even before
+ * mpv_initialize(). In some obscure corner cases, using this function
+ * to set options might still be required (see
+ * "Inconsistencies between options and properties" in the manpage). Once
+ * these are resolved, the option setting functions might be fully
+ * deprecated.
+ *
+ * @param name Option name. This is the same as on the mpv command line, but
+ * without the leading "--".
+ * @param format see enum mpv_format.
+ * @param[in] data Option value (according to the format).
+ * @return error code
+ */
+MPV_EXPORT int mpv_set_option(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data);
+
+/**
+ * Convenience function to set an option to a string value. This is like
+ * calling mpv_set_option() with MPV_FORMAT_STRING.
+ *
+ * @return error code
+ */
+MPV_EXPORT int mpv_set_option_string(mpv_handle *ctx, const char *name, const char *data);
+
+/**
+ * Send a command to the player. Commands are the same as those used in
+ * input.conf, except that this function takes parameters in a pre-split
+ * form.
+ *
+ * The commands and their parameters are documented in input.rst.
+ *
+ * Does not use OSD and string expansion by default (unlike mpv_command_string()
+ * and input.conf).
+ *
+ * @param[in] args NULL-terminated list of strings. Usually, the first item
+ * is the command, and the following items are arguments.
+ * @return error code
+ */
+MPV_EXPORT int mpv_command(mpv_handle *ctx, const char **args);
+
+/**
+ * Same as mpv_command(), but allows passing structured data in any format.
+ * In particular, calling mpv_command() is exactly like calling
+ * mpv_command_node() with the format set to MPV_FORMAT_NODE_ARRAY, and
+ * every arg passed in order as MPV_FORMAT_STRING.
+ *
+ * Does not use OSD and string expansion by default.
+ *
+ * The args argument can have one of the following formats:
+ *
+ * MPV_FORMAT_NODE_ARRAY:
+ * Positional arguments. Each entry is an argument using an arbitrary
+ * format (the format must be compatible to the used command). Usually,
+ * the first item is the command name (as MPV_FORMAT_STRING). The order
+ * of arguments is as documented in each command description.
+ *
+ * MPV_FORMAT_NODE_MAP:
+ * Named arguments. This requires at least an entry with the key "name"
+ * to be present, which must be a string, and contains the command name.
+ * The special entry "_flags" is optional, and if present, must be an
+ * array of strings, each being a command prefix to apply. All other
+ * entries are interpreted as arguments. They must use the argument names
+ * as documented in each command description. Some commands do not
+ * support named arguments at all, and must use MPV_FORMAT_NODE_ARRAY.
+ *
+ * @param[in] args mpv_node with format set to one of the values documented
+ * above (see there for details)
+ * @param[out] result Optional, pass NULL if unused. If not NULL, and if the
+ * function succeeds, this is set to command-specific return
+ * data. You must call mpv_free_node_contents() to free it
+ * (again, only if the command actually succeeds).
+ * Not many commands actually use this at all.
+ * @return error code (the result parameter is not set on error)
+ */
+MPV_EXPORT int mpv_command_node(mpv_handle *ctx, mpv_node *args, mpv_node *result);
+
+/**
+ * This is essentially identical to mpv_command() but it also returns a result.
+ *
+ * Does not use OSD and string expansion by default.
+ *
+ * @param[in] args NULL-terminated list of strings. Usually, the first item
+ * is the command, and the following items are arguments.
+ * @param[out] result Optional, pass NULL if unused. If not NULL, and if the
+ * function succeeds, this is set to command-specific return
+ * data. You must call mpv_free_node_contents() to free it
+ * (again, only if the command actually succeeds).
+ * Not many commands actually use this at all.
+ * @return error code (the result parameter is not set on error)
+ */
+MPV_EXPORT int mpv_command_ret(mpv_handle *ctx, const char **args, mpv_node *result);
+
+/**
+ * Same as mpv_command, but use input.conf parsing for splitting arguments.
+ * This is slightly simpler, but also more error prone, since arguments may
+ * need quoting/escaping.
+ *
+ * This also has OSD and string expansion enabled by default.
+ */
+MPV_EXPORT int mpv_command_string(mpv_handle *ctx, const char *args);
+
+/**
+ * Same as mpv_command, but run the command asynchronously.
+ *
+ * Commands are executed asynchronously. You will receive a
+ * MPV_EVENT_COMMAND_REPLY event. This event will also have an
+ * error code set if running the command failed. For commands that
+ * return data, the data is put into mpv_event_command.result.
+ *
+ * The only case when you do not receive an event is when the function call
+ * itself fails. This happens only if parsing the command itself (or otherwise
+ * validating it) fails, i.e. the return code of the API call is not 0 or
+ * positive.
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param reply_userdata the value mpv_event.reply_userdata of the reply will
+ * be set to (see section about asynchronous calls)
+ * @param args NULL-terminated list of strings (see mpv_command())
+ * @return error code (if parsing or queuing the command fails)
+ */
+MPV_EXPORT int mpv_command_async(mpv_handle *ctx, uint64_t reply_userdata,
+ const char **args);
+
+/**
+ * Same as mpv_command_node(), but run it asynchronously. Basically, this
+ * function is to mpv_command_node() what mpv_command_async() is to
+ * mpv_command().
+ *
+ * See mpv_command_async() for details.
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param reply_userdata the value mpv_event.reply_userdata of the reply will
+ * be set to (see section about asynchronous calls)
+ * @param args as in mpv_command_node()
+ * @return error code (if parsing or queuing the command fails)
+ */
+MPV_EXPORT int mpv_command_node_async(mpv_handle *ctx, uint64_t reply_userdata,
+ mpv_node *args);
+
+/**
+ * Signal to all async requests with the matching ID to abort. This affects
+ * the following API calls:
+ *
+ * mpv_command_async
+ * mpv_command_node_async
+ *
+ * All of these functions take a reply_userdata parameter. This API function
+ * tells all requests with the matching reply_userdata value to try to return
+ * as soon as possible. If there are multiple requests with matching ID, it
+ * aborts all of them.
+ *
+ * This API function is mostly asynchronous itself. It will not wait until the
+ * command is aborted. Instead, the command will terminate as usual, but with
+ * some work not done. How this is signaled depends on the specific command (for
+ * example, the "subprocess" command will indicate it by "killed_by_us" set to
+ * true in the result). How long it takes also depends on the situation. The
+ * aborting process is completely asynchronous.
+ *
+ * Not all commands may support this functionality. In this case, this function
+ * will have no effect. The same is true if the request using the passed
+ * reply_userdata has already terminated, has not been started yet, or was
+ * never in use at all.
+ *
+ * You have to be careful of race conditions: the time during which the abort
+ * request will be effective is _after_ e.g. mpv_command_async() has returned,
+ * and before the command has signaled completion with MPV_EVENT_COMMAND_REPLY.
+ *
+ * @param reply_userdata ID of the request to be aborted (see above)
+ */
+MPV_EXPORT void mpv_abort_async_command(mpv_handle *ctx, uint64_t reply_userdata);
+
+/**
+ * Set a property to a given value. Properties are essentially variables which
+ * can be queried or set at runtime. For example, writing to the pause property
+ * will actually pause or unpause playback.
+ *
+ * If the format doesn't match with the internal format of the property, access
+ * usually will fail with MPV_ERROR_PROPERTY_FORMAT. In some cases, the data
+ * is automatically converted and access succeeds. For example, MPV_FORMAT_INT64
+ * is always converted to MPV_FORMAT_DOUBLE, and access using MPV_FORMAT_STRING
+ * usually invokes a string parser. The same happens when calling this function
+ * with MPV_FORMAT_NODE: the underlying format may be converted to another
+ * type if possible.
+ *
+ * Using a format other than MPV_FORMAT_NODE is equivalent to constructing a
+ * mpv_node with the given format and data, and passing the mpv_node to this
+ * function. (Before API version 1.21, this was different.)
+ *
+ * Note: starting with mpv 0.21.0 (client API version 1.23), this can be used to
+ * set options in general. It even can be used before mpv_initialize()
+ * has been called. If called before mpv_initialize(), setting properties
+ * not backed by options will result in MPV_ERROR_PROPERTY_UNAVAILABLE.
+ * In some cases, properties and options still conflict. In these cases,
+ * mpv_set_property() accesses the options before mpv_initialize(), and
+ * the properties after mpv_initialize(). These conflicts will be removed
+ * in mpv 0.23.0. See mpv_set_option() for further remarks.
+ *
+ * @param name The property name. See input.rst for a list of properties.
+ * @param format see enum mpv_format.
+ * @param[in] data Option value.
+ * @return error code
+ */
+MPV_EXPORT int mpv_set_property(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data);
+
+/**
+ * Convenience function to set a property to a string value.
+ *
+ * This is like calling mpv_set_property() with MPV_FORMAT_STRING.
+ */
+MPV_EXPORT int mpv_set_property_string(mpv_handle *ctx, const char *name, const char *data);
+
+/**
+ * Convenience function to delete a property.
+ *
+ * This is equivalent to running the command "del [name]".
+ *
+ * @param name The property name. See input.rst for a list of properties.
+ * @return error code
+ */
+MPV_EXPORT int mpv_del_property(mpv_handle *ctx, const char *name);
+
+/**
+ * Set a property asynchronously. You will receive the result of the operation
+ * as MPV_EVENT_SET_PROPERTY_REPLY event. The mpv_event.error field will contain
+ * the result status of the operation. Otherwise, this function is similar to
+ * mpv_set_property().
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param reply_userdata see section about asynchronous calls
+ * @param name The property name.
+ * @param format see enum mpv_format.
+ * @param[in] data Option value. The value will be copied by the function. It
+ * will never be modified by the client API.
+ * @return error code if sending the request failed
+ */
+MPV_EXPORT int mpv_set_property_async(mpv_handle *ctx, uint64_t reply_userdata,
+ const char *name, mpv_format format, void *data);
+
+/**
+ * Read the value of the given property.
+ *
+ * If the format doesn't match with the internal format of the property, access
+ * usually will fail with MPV_ERROR_PROPERTY_FORMAT. In some cases, the data
+ * is automatically converted and access succeeds. For example, MPV_FORMAT_INT64
+ * is always converted to MPV_FORMAT_DOUBLE, and access using MPV_FORMAT_STRING
+ * usually invokes a string formatter.
+ *
+ * @param name The property name.
+ * @param format see enum mpv_format.
+ * @param[out] data Pointer to the variable holding the option value. On
+ * success, the variable will be set to a copy of the option
+ * value. For formats that require dynamic memory allocation,
+ * you can free the value with mpv_free() (strings) or
+ * mpv_free_node_contents() (MPV_FORMAT_NODE).
+ * @return error code
+ */
+MPV_EXPORT int mpv_get_property(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data);
+
+/**
+ * Return the value of the property with the given name as string. This is
+ * equivalent to mpv_get_property() with MPV_FORMAT_STRING.
+ *
+ * See MPV_FORMAT_STRING for character encoding issues.
+ *
+ * On error, NULL is returned. Use mpv_get_property() if you want fine-grained
+ * error reporting.
+ *
+ * @param name The property name.
+ * @return Property value, or NULL if the property can't be retrieved. Free
+ * the string with mpv_free().
+ */
+MPV_EXPORT char *mpv_get_property_string(mpv_handle *ctx, const char *name);
+
+/**
+ * Return the property as "OSD" formatted string. This is the same as
+ * mpv_get_property_string, but using MPV_FORMAT_OSD_STRING.
+ *
+ * @return Property value, or NULL if the property can't be retrieved. Free
+ * the string with mpv_free().
+ */
+MPV_EXPORT char *mpv_get_property_osd_string(mpv_handle *ctx, const char *name);
+
+/**
+ * Get a property asynchronously. You will receive the result of the operation
+ * as well as the property data with the MPV_EVENT_GET_PROPERTY_REPLY event.
+ * You should check the mpv_event.error field on the reply event.
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param reply_userdata see section about asynchronous calls
+ * @param name The property name.
+ * @param format see enum mpv_format.
+ * @return error code if sending the request failed
+ */
+MPV_EXPORT int mpv_get_property_async(mpv_handle *ctx, uint64_t reply_userdata,
+ const char *name, mpv_format format);
+
+/**
+ * Get a notification whenever the given property changes. You will receive
+ * updates as MPV_EVENT_PROPERTY_CHANGE. Note that this is not very precise:
+ * for some properties, it may not send updates even if the property changed.
+ * This depends on the property, and it's a valid feature request to ask for
+ * better update handling of a specific property. (For some properties, like
+ * ``clock``, which shows the wall clock, this mechanism doesn't make too
+ * much sense anyway.)
+ *
+ * Property changes are coalesced: the change events are returned only once the
+ * event queue becomes empty (e.g. mpv_wait_event() would block or return
+ * MPV_EVENT_NONE), and then only one event per changed property is returned.
+ *
+ * You always get an initial change notification. This is meant to initialize
+ * the user's state to the current value of the property.
+ *
+ * Normally, change events are sent only if the property value changes according
+ * to the requested format. mpv_event_property will contain the property value
+ * as data member.
+ *
+ * Warning: if a property is unavailable or retrieving it caused an error,
+ * MPV_FORMAT_NONE will be set in mpv_event_property, even if the
+ * format parameter was set to a different value. In this case, the
+ * mpv_event_property.data field is invalid.
+ *
+ * If the property is observed with the format parameter set to MPV_FORMAT_NONE,
+ * you get low-level notifications whether the property _may_ have changed, and
+ * the data member in mpv_event_property will be unset. With this mode, you
+ * will have to determine yourself whether the property really changed. On the
+ * other hand, this mechanism can be faster and uses less resources.
+ *
+ * Observing a property that doesn't exist is allowed. (Although it may still
+ * cause some sporadic change events.)
+ *
+ * Keep in mind that you will get change notifications even if you change a
+ * property yourself. Try to avoid endless feedback loops, which could happen
+ * if you react to the change notifications triggered by your own change.
+ *
+ * Only the mpv_handle on which this was called will receive the property
+ * change events, or can unobserve them.
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param reply_userdata This will be used for the mpv_event.reply_userdata
+ * field for the received MPV_EVENT_PROPERTY_CHANGE
+ * events. (Also see section about asynchronous calls,
+ * although this function is somewhat different from
+ * actual asynchronous calls.)
+ * If you have no use for this, pass 0.
+ * Also see mpv_unobserve_property().
+ * @param name The property name.
+ * @param format see enum mpv_format. Can be MPV_FORMAT_NONE to omit values
+ * from the change events.
+ * @return error code (usually fails only on OOM or unsupported format)
+ */
+MPV_EXPORT int mpv_observe_property(mpv_handle *mpv, uint64_t reply_userdata,
+ const char *name, mpv_format format);
+
+/**
+ * Undo mpv_observe_property(). This will remove all observed properties for
+ * which the given number was passed as reply_userdata to mpv_observe_property.
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param registered_reply_userdata ID that was passed to mpv_observe_property
+ * @return negative value is an error code, >=0 is number of removed properties
+ * on success (includes the case when 0 were removed)
+ */
+MPV_EXPORT int mpv_unobserve_property(mpv_handle *mpv, uint64_t registered_reply_userdata);
+
+typedef enum mpv_event_id {
+ /**
+ * Nothing happened. Happens on timeouts or sporadic wakeups.
+ */
+ MPV_EVENT_NONE = 0,
+ /**
+ * Happens when the player quits. The player enters a state where it tries
+ * to disconnect all clients. Most requests to the player will fail, and
+ * the client should react to this and quit with mpv_destroy() as soon as
+ * possible.
+ */
+ MPV_EVENT_SHUTDOWN = 1,
+ /**
+ * See mpv_request_log_messages().
+ */
+ MPV_EVENT_LOG_MESSAGE = 2,
+ /**
+ * Reply to a mpv_get_property_async() request.
+ * See also mpv_event and mpv_event_property.
+ */
+ MPV_EVENT_GET_PROPERTY_REPLY = 3,
+ /**
+ * Reply to a mpv_set_property_async() request.
+ * (Unlike MPV_EVENT_GET_PROPERTY, mpv_event_property is not used.)
+ */
+ MPV_EVENT_SET_PROPERTY_REPLY = 4,
+ /**
+ * Reply to a mpv_command_async() or mpv_command_node_async() request.
+ * See also mpv_event and mpv_event_command.
+ */
+ MPV_EVENT_COMMAND_REPLY = 5,
+ /**
+ * Notification before playback start of a file (before the file is loaded).
+ * See also mpv_event and mpv_event_start_file.
+ */
+ MPV_EVENT_START_FILE = 6,
+ /**
+ * Notification after playback end (after the file was unloaded).
+ * See also mpv_event and mpv_event_end_file.
+ */
+ MPV_EVENT_END_FILE = 7,
+ /**
+ * Notification when the file has been loaded (headers were read etc.), and
+ * decoding starts.
+ */
+ MPV_EVENT_FILE_LOADED = 8,
+#if MPV_ENABLE_DEPRECATED
+ /**
+ * Idle mode was entered. In this mode, no file is played, and the playback
+ * core waits for new commands. (The command line player normally quits
+ * instead of entering idle mode, unless --idle was specified. If mpv
+ * was started with mpv_create(), idle mode is enabled by default.)
+ *
+ * @deprecated This is equivalent to using mpv_observe_property() on the
+ * "idle-active" property. The event is redundant, and might be
+ * removed in the far future. As a further warning, this event
+ * is not necessarily sent at the right point anymore (at the
+ * start of the program), while the property behaves correctly.
+ */
+ MPV_EVENT_IDLE = 11,
+ /**
+ * Sent every time after a video frame is displayed. Note that currently,
+ * this will be sent in lower frequency if there is no video, or playback
+ * is paused - but that will be removed in the future, and it will be
+ * restricted to video frames only.
+ *
+ * @deprecated Use mpv_observe_property() with relevant properties instead
+ * (such as "playback-time").
+ */
+ MPV_EVENT_TICK = 14,
+#endif
+ /**
+ * Triggered by the script-message input command. The command uses the
+ * first argument of the command as client name (see mpv_client_name()) to
+ * dispatch the message, and passes along all arguments starting from the
+ * second argument as strings.
+ * See also mpv_event and mpv_event_client_message.
+ */
+ MPV_EVENT_CLIENT_MESSAGE = 16,
+ /**
+ * Happens after video changed in some way. This can happen on resolution
+ * changes, pixel format changes, or video filter changes. The event is
+ * sent after the video filters and the VO are reconfigured. Applications
+ * embedding a mpv window should listen to this event in order to resize
+ * the window if needed.
+ * Note that this event can happen sporadically, and you should check
+ * yourself whether the video parameters really changed before doing
+ * something expensive.
+ */
+ MPV_EVENT_VIDEO_RECONFIG = 17,
+ /**
+ * Similar to MPV_EVENT_VIDEO_RECONFIG. This is relatively uninteresting,
+ * because there is no such thing as audio output embedding.
+ */
+ MPV_EVENT_AUDIO_RECONFIG = 18,
+ /**
+ * Happens when a seek was initiated. Playback stops. Usually it will
+ * resume with MPV_EVENT_PLAYBACK_RESTART as soon as the seek is finished.
+ */
+ MPV_EVENT_SEEK = 20,
+ /**
+ * There was a discontinuity of some sort (like a seek), and playback
+ * was reinitialized. Usually happens on start of playback and after
+ * seeking. The main purpose is allowing the client to detect when a seek
+ * request is finished.
+ */
+ MPV_EVENT_PLAYBACK_RESTART = 21,
+ /**
+ * Event sent due to mpv_observe_property().
+ * See also mpv_event and mpv_event_property.
+ */
+ MPV_EVENT_PROPERTY_CHANGE = 22,
+ /**
+ * Happens if the internal per-mpv_handle ringbuffer overflows, and at
+ * least 1 event had to be dropped. This can happen if the client doesn't
+ * read the event queue quickly enough with mpv_wait_event(), or if the
+ * client makes a very large number of asynchronous calls at once.
+ *
+ * Event delivery will continue normally once this event was returned
+ * (this forces the client to empty the queue completely).
+ */
+ MPV_EVENT_QUEUE_OVERFLOW = 24,
+ /**
+ * Triggered if a hook handler was registered with mpv_hook_add(), and the
+ * hook is invoked. If you receive this, you must handle it, and continue
+ * the hook with mpv_hook_continue().
+ * See also mpv_event and mpv_event_hook.
+ */
+ MPV_EVENT_HOOK = 25,
+ // Internal note: adjust INTERNAL_EVENT_BASE when adding new events.
+} mpv_event_id;
+
+/**
+ * Return a string describing the event. For unknown events, NULL is returned.
+ *
+ * Note that all events actually returned by the API will also yield a non-NULL
+ * string with this function.
+ *
+ * @param event event ID, see see enum mpv_event_id
+ * @return A static string giving a short symbolic name of the event. It
+ * consists of lower-case alphanumeric characters and can include "-"
+ * characters. This string is suitable for use in e.g. scripting
+ * interfaces.
+ * The string is completely static, i.e. doesn't need to be deallocated,
+ * and is valid forever.
+ */
+MPV_EXPORT const char *mpv_event_name(mpv_event_id event);
+
+typedef struct mpv_event_property {
+ /**
+ * Name of the property.
+ */
+ const char *name;
+ /**
+ * Format of the data field in the same struct. See enum mpv_format.
+ * This is always the same format as the requested format, except when
+ * the property could not be retrieved (unavailable, or an error happened),
+ * in which case the format is MPV_FORMAT_NONE.
+ */
+ mpv_format format;
+ /**
+ * Received property value. Depends on the format. This is like the
+ * pointer argument passed to mpv_get_property().
+ *
+ * For example, for MPV_FORMAT_STRING you get the string with:
+ *
+ * char *value = *(char **)(event_property->data);
+ *
+ * Note that this is set to NULL if retrieving the property failed (the
+ * format will be MPV_FORMAT_NONE).
+ */
+ void *data;
+} mpv_event_property;
+
+/**
+ * Numeric log levels. The lower the number, the more important the message is.
+ * MPV_LOG_LEVEL_NONE is never used when receiving messages. The string in
+ * the comment after the value is the name of the log level as used for the
+ * mpv_request_log_messages() function.
+ * Unused numeric values are unused, but reserved for future use.
+ */
+typedef enum mpv_log_level {
+ MPV_LOG_LEVEL_NONE = 0, /// "no" - disable absolutely all messages
+ MPV_LOG_LEVEL_FATAL = 10, /// "fatal" - critical/aborting errors
+ MPV_LOG_LEVEL_ERROR = 20, /// "error" - simple errors
+ MPV_LOG_LEVEL_WARN = 30, /// "warn" - possible problems
+ MPV_LOG_LEVEL_INFO = 40, /// "info" - informational message
+ MPV_LOG_LEVEL_V = 50, /// "v" - noisy informational message
+ MPV_LOG_LEVEL_DEBUG = 60, /// "debug" - very noisy technical information
+ MPV_LOG_LEVEL_TRACE = 70, /// "trace" - extremely noisy
+} mpv_log_level;
+
+typedef struct mpv_event_log_message {
+ /**
+ * The module prefix, identifies the sender of the message. As a special
+ * case, if the message buffer overflows, this will be set to the string
+ * "overflow" (which doesn't appear as prefix otherwise), and the text
+ * field will contain an informative message.
+ */
+ const char *prefix;
+ /**
+ * The log level as string. See mpv_request_log_messages() for possible
+ * values. The level "no" is never used here.
+ */
+ const char *level;
+ /**
+ * The log message. It consists of 1 line of text, and is terminated with
+ * a newline character. (Before API version 1.6, it could contain multiple
+ * or partial lines.)
+ */
+ const char *text;
+ /**
+ * The same contents as the level field, but as a numeric ID.
+ * Since API version 1.6.
+ */
+ mpv_log_level log_level;
+} mpv_event_log_message;
+
+/// Since API version 1.9.
+typedef enum mpv_end_file_reason {
+ /**
+ * The end of file was reached. Sometimes this may also happen on
+ * incomplete or corrupted files, or if the network connection was
+ * interrupted when playing a remote file. It also happens if the
+ * playback range was restricted with --end or --frames or similar.
+ */
+ MPV_END_FILE_REASON_EOF = 0,
+ /**
+ * Playback was stopped by an external action (e.g. playlist controls).
+ */
+ MPV_END_FILE_REASON_STOP = 2,
+ /**
+ * Playback was stopped by the quit command or player shutdown.
+ */
+ MPV_END_FILE_REASON_QUIT = 3,
+ /**
+ * Some kind of error happened that lead to playback abort. Does not
+ * necessarily happen on incomplete or broken files (in these cases, both
+ * MPV_END_FILE_REASON_ERROR or MPV_END_FILE_REASON_EOF are possible).
+ *
+ * mpv_event_end_file.error will be set.
+ */
+ MPV_END_FILE_REASON_ERROR = 4,
+ /**
+ * The file was a playlist or similar. When the playlist is read, its
+ * entries will be appended to the playlist after the entry of the current
+ * file, the entry of the current file is removed, and a MPV_EVENT_END_FILE
+ * event is sent with reason set to MPV_END_FILE_REASON_REDIRECT. Then
+ * playback continues with the playlist contents.
+ * Since API version 1.18.
+ */
+ MPV_END_FILE_REASON_REDIRECT = 5,
+} mpv_end_file_reason;
+
+/// Since API version 1.108.
+typedef struct mpv_event_start_file {
+ /**
+ * Playlist entry ID of the file being loaded now.
+ */
+ int64_t playlist_entry_id;
+} mpv_event_start_file;
+
+typedef struct mpv_event_end_file {
+ /**
+ * Corresponds to the values in enum mpv_end_file_reason.
+ *
+ * Unknown values should be treated as unknown.
+ */
+ mpv_end_file_reason reason;
+ /**
+ * If reason==MPV_END_FILE_REASON_ERROR, this contains a mpv error code
+ * (one of MPV_ERROR_...) giving an approximate reason why playback
+ * failed. In other cases, this field is 0 (no error).
+ * Since API version 1.9.
+ */
+ int error;
+ /**
+ * Playlist entry ID of the file that was being played or attempted to be
+ * played. This has the same value as the playlist_entry_id field in the
+ * corresponding mpv_event_start_file event.
+ * Since API version 1.108.
+ */
+ int64_t playlist_entry_id;
+ /**
+ * If loading ended, because the playlist entry to be played was for example
+ * a playlist, and the current playlist entry is replaced with a number of
+ * other entries. This may happen at least with MPV_END_FILE_REASON_REDIRECT
+ * (other event types may use this for similar but different purposes in the
+ * future). In this case, playlist_insert_id will be set to the playlist
+ * entry ID of the first inserted entry, and playlist_insert_num_entries to
+ * the total number of inserted playlist entries. Note this in this specific
+ * case, the ID of the last inserted entry is playlist_insert_id+num-1.
+ * Beware that depending on circumstances, you may observe the new playlist
+ * entries before seeing the event (e.g. reading the "playlist" property or
+ * getting a property change notification before receiving the event).
+ * Since API version 1.108.
+ */
+ int64_t playlist_insert_id;
+ /**
+ * See playlist_insert_id. Only non-0 if playlist_insert_id is valid. Never
+ * negative.
+ * Since API version 1.108.
+ */
+ int playlist_insert_num_entries;
+} mpv_event_end_file;
+
+typedef struct mpv_event_client_message {
+ /**
+ * Arbitrary arguments chosen by the sender of the message. If num_args > 0,
+ * you can access args[0] through args[num_args - 1] (inclusive). What
+ * these arguments mean is up to the sender and receiver.
+ * None of the valid items are NULL.
+ */
+ int num_args;
+ const char **args;
+} mpv_event_client_message;
+
+typedef struct mpv_event_hook {
+ /**
+ * The hook name as passed to mpv_hook_add().
+ */
+ const char *name;
+ /**
+ * Internal ID that must be passed to mpv_hook_continue().
+ */
+ uint64_t id;
+} mpv_event_hook;
+
+// Since API version 1.102.
+typedef struct mpv_event_command {
+ /**
+ * Result data of the command. Note that success/failure is signaled
+ * separately via mpv_event.error. This field is only for result data
+ * in case of success. Most commands leave it at MPV_FORMAT_NONE. Set
+ * to MPV_FORMAT_NONE on failure.
+ */
+ mpv_node result;
+} mpv_event_command;
+
+typedef struct mpv_event {
+ /**
+ * One of mpv_event. Keep in mind that later ABI compatible releases might
+ * add new event types. These should be ignored by the API user.
+ */
+ mpv_event_id event_id;
+ /**
+ * This is mainly used for events that are replies to (asynchronous)
+ * requests. It contains a status code, which is >= 0 on success, or < 0
+ * on error (a mpv_error value). Usually, this will be set if an
+ * asynchronous request fails.
+ * Used for:
+ * MPV_EVENT_GET_PROPERTY_REPLY
+ * MPV_EVENT_SET_PROPERTY_REPLY
+ * MPV_EVENT_COMMAND_REPLY
+ */
+ int error;
+ /**
+ * If the event is in reply to a request (made with this API and this
+ * API handle), this is set to the reply_userdata parameter of the request
+ * call. Otherwise, this field is 0.
+ * Used for:
+ * MPV_EVENT_GET_PROPERTY_REPLY
+ * MPV_EVENT_SET_PROPERTY_REPLY
+ * MPV_EVENT_COMMAND_REPLY
+ * MPV_EVENT_PROPERTY_CHANGE
+ * MPV_EVENT_HOOK
+ */
+ uint64_t reply_userdata;
+ /**
+ * The meaning and contents of the data member depend on the event_id:
+ * MPV_EVENT_GET_PROPERTY_REPLY: mpv_event_property*
+ * MPV_EVENT_PROPERTY_CHANGE: mpv_event_property*
+ * MPV_EVENT_LOG_MESSAGE: mpv_event_log_message*
+ * MPV_EVENT_CLIENT_MESSAGE: mpv_event_client_message*
+ * MPV_EVENT_START_FILE: mpv_event_start_file* (since v1.108)
+ * MPV_EVENT_END_FILE: mpv_event_end_file*
+ * MPV_EVENT_HOOK: mpv_event_hook*
+ * MPV_EVENT_COMMAND_REPLY* mpv_event_command*
+ * other: NULL
+ *
+ * Note: future enhancements might add new event structs for existing or new
+ * event types.
+ */
+ void *data;
+} mpv_event;
+
+/**
+ * Convert the given src event to a mpv_node, and set *dst to the result. *dst
+ * is set to a MPV_FORMAT_NODE_MAP, with fields for corresponding mpv_event and
+ * mpv_event.data/mpv_event_* fields.
+ *
+ * The exact details are not completely documented out of laziness. A start
+ * is located in the "Events" section of the manpage.
+ *
+ * *dst may point to newly allocated memory, or pointers in mpv_event. You must
+ * copy the entire mpv_node if you want to reference it after mpv_event becomes
+ * invalid (such as making a new mpv_wait_event() call, or destroying the
+ * mpv_handle from which it was returned). Call mpv_free_node_contents() to free
+ * any memory allocations made by this API function.
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param dst Target. This is not read and fully overwritten. Must be released
+ * with mpv_free_node_contents(). Do not write to pointers returned
+ * by it. (On error, this may be left as an empty node.)
+ * @param src The source event. Not modified (it's not const due to the author's
+ * prejudice of the C version of const).
+ * @return error code (MPV_ERROR_NOMEM only, if at all)
+ */
+MPV_EXPORT int mpv_event_to_node(mpv_node *dst, mpv_event *src);
+
+/**
+ * Enable or disable the given event.
+ *
+ * Some events are enabled by default. Some events can't be disabled.
+ *
+ * (Informational note: currently, all events are enabled by default, except
+ * MPV_EVENT_TICK.)
+ *
+ * Safe to be called from mpv render API threads.
+ *
+ * @param event See enum mpv_event_id.
+ * @param enable 1 to enable receiving this event, 0 to disable it.
+ * @return error code
+ */
+MPV_EXPORT int mpv_request_event(mpv_handle *ctx, mpv_event_id event, int enable);
+
+/**
+ * Enable or disable receiving of log messages. These are the messages the
+ * command line player prints to the terminal. This call sets the minimum
+ * required log level for a message to be received with MPV_EVENT_LOG_MESSAGE.
+ *
+ * @param min_level Minimal log level as string. Valid log levels:
+ * no fatal error warn info v debug trace
+ * The value "no" disables all messages. This is the default.
+ * An exception is the value "terminal-default", which uses the
+ * log level as set by the "--msg-level" option. This works
+ * even if the terminal is disabled. (Since API version 1.19.)
+ * Also see mpv_log_level.
+ * @return error code
+ */
+MPV_EXPORT int mpv_request_log_messages(mpv_handle *ctx, const char *min_level);
+
+/**
+ * Wait for the next event, or until the timeout expires, or if another thread
+ * makes a call to mpv_wakeup(). Passing 0 as timeout will never wait, and
+ * is suitable for polling.
+ *
+ * The internal event queue has a limited size (per client handle). If you
+ * don't empty the event queue quickly enough with mpv_wait_event(), it will
+ * overflow and silently discard further events. If this happens, making
+ * asynchronous requests will fail as well (with MPV_ERROR_EVENT_QUEUE_FULL).
+ *
+ * Only one thread is allowed to call this on the same mpv_handle at a time.
+ * The API won't complain if more than one thread calls this, but it will cause
+ * race conditions in the client when accessing the shared mpv_event struct.
+ * Note that most other API functions are not restricted by this, and no API
+ * function internally calls mpv_wait_event(). Additionally, concurrent calls
+ * to different mpv_handles are always safe.
+ *
+ * As long as the timeout is 0, this is safe to be called from mpv render API
+ * threads.
+ *
+ * @param timeout Timeout in seconds, after which the function returns even if
+ * no event was received. A MPV_EVENT_NONE is returned on
+ * timeout. A value of 0 will disable waiting. Negative values
+ * will wait with an infinite timeout.
+ * @return A struct containing the event ID and other data. The pointer (and
+ * fields in the struct) stay valid until the next mpv_wait_event()
+ * call, or until the mpv_handle is destroyed. You must not write to
+ * the struct, and all memory referenced by it will be automatically
+ * released by the API on the next mpv_wait_event() call, or when the
+ * context is destroyed. The return value is never NULL.
+ */
+MPV_EXPORT mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout);
+
+/**
+ * Interrupt the current mpv_wait_event() call. This will wake up the thread
+ * currently waiting in mpv_wait_event(). If no thread is waiting, the next
+ * mpv_wait_event() call will return immediately (this is to avoid lost
+ * wakeups).
+ *
+ * mpv_wait_event() will receive a MPV_EVENT_NONE if it's woken up due to
+ * this call. But note that this dummy event might be skipped if there are
+ * already other events queued. All what counts is that the waiting thread
+ * is woken up at all.
+ *
+ * Safe to be called from mpv render API threads.
+ */
+MPV_EXPORT void mpv_wakeup(mpv_handle *ctx);
+
+/**
+ * Set a custom function that should be called when there are new events. Use
+ * this if blocking in mpv_wait_event() to wait for new events is not feasible.
+ *
+ * Keep in mind that the callback will be called from foreign threads. You
+ * must not make any assumptions of the environment, and you must return as
+ * soon as possible (i.e. no long blocking waits). Exiting the callback through
+ * any other means than a normal return is forbidden (no throwing exceptions,
+ * no longjmp() calls). You must not change any local thread state (such as
+ * the C floating point environment).
+ *
+ * You are not allowed to call any client API functions inside of the callback.
+ * In particular, you should not do any processing in the callback, but wake up
+ * another thread that does all the work. The callback is meant strictly for
+ * notification only, and is called from arbitrary core parts of the player,
+ * that make no considerations for reentrant API use or allowing the callee to
+ * spend a lot of time doing other things. Keep in mind that it's also possible
+ * that the callback is called from a thread while a mpv API function is called
+ * (i.e. it can be reentrant).
+ *
+ * In general, the client API expects you to call mpv_wait_event() to receive
+ * notifications, and the wakeup callback is merely a helper utility to make
+ * this easier in certain situations. Note that it's possible that there's
+ * only one wakeup callback invocation for multiple events. You should call
+ * mpv_wait_event() with no timeout until MPV_EVENT_NONE is reached, at which
+ * point the event queue is empty.
+ *
+ * If you actually want to do processing in a callback, spawn a thread that
+ * does nothing but call mpv_wait_event() in a loop and dispatches the result
+ * to a callback.
+ *
+ * Only one wakeup callback can be set.
+ *
+ * @param cb function that should be called if a wakeup is required
+ * @param d arbitrary userdata passed to cb
+ */
+MPV_EXPORT void mpv_set_wakeup_callback(mpv_handle *ctx, void (*cb)(void *d), void *d);
+
+/**
+ * Block until all asynchronous requests are done. This affects functions like
+ * mpv_command_async(), which return immediately and return their result as
+ * events.
+ *
+ * This is a helper, and somewhat equivalent to calling mpv_wait_event() in a
+ * loop until all known asynchronous requests have sent their reply as event,
+ * except that the event queue is not emptied.
+ *
+ * In case you called mpv_suspend() before, this will also forcibly reset the
+ * suspend counter of the given handle.
+ */
+MPV_EXPORT void mpv_wait_async_requests(mpv_handle *ctx);
+
+/**
+ * A hook is like a synchronous event that blocks the player. You register
+ * a hook handler with this function. You will get an event, which you need
+ * to handle, and once things are ready, you can let the player continue with
+ * mpv_hook_continue().
+ *
+ * Currently, hooks can't be removed explicitly. But they will be implicitly
+ * removed if the mpv_handle it was registered with is destroyed. This also
+ * continues the hook if it was being handled by the destroyed mpv_handle (but
+ * this should be avoided, as it might mess up order of hook execution).
+ *
+ * Hook handlers are ordered globally by priority and order of registration.
+ * Handlers for the same hook with same priority are invoked in order of
+ * registration (the handler registered first is run first). Handlers with
+ * lower priority are run first (which seems backward).
+ *
+ * See the "Hooks" section in the manpage to see which hooks are currently
+ * defined.
+ *
+ * Some hooks might be reentrant (so you get multiple MPV_EVENT_HOOK for the
+ * same hook). If this can happen for a specific hook type, it will be
+ * explicitly documented in the manpage.
+ *
+ * Only the mpv_handle on which this was called will receive the hook events,
+ * or can "continue" them.
+ *
+ * @param reply_userdata This will be used for the mpv_event.reply_userdata
+ * field for the received MPV_EVENT_HOOK events.
+ * If you have no use for this, pass 0.
+ * @param name The hook name. This should be one of the documented names. But
+ * if the name is unknown, the hook event will simply be never
+ * raised.
+ * @param priority See remarks above. Use 0 as a neutral default.
+ * @return error code (usually fails only on OOM)
+ */
+MPV_EXPORT int mpv_hook_add(mpv_handle *ctx, uint64_t reply_userdata,
+ const char *name, int priority);
+
+/**
+ * Respond to a MPV_EVENT_HOOK event. You must call this after you have handled
+ * the event. There is no way to "cancel" or "stop" the hook.
+ *
+ * Calling this will will typically unblock the player for whatever the hook
+ * is responsible for (e.g. for the "on_load" hook it lets it continue
+ * playback).
+ *
+ * It is explicitly undefined behavior to call this more than once for each
+ * MPV_EVENT_HOOK, to pass an incorrect ID, or to call this on a mpv_handle
+ * different from the one that registered the handler and received the event.
+ *
+ * @param id This must be the value of the mpv_event_hook.id field for the
+ * corresponding MPV_EVENT_HOOK.
+ * @return error code
+ */
+MPV_EXPORT int mpv_hook_continue(mpv_handle *ctx, uint64_t id);
+
+#if MPV_ENABLE_DEPRECATED
+
+/**
+ * Return a UNIX file descriptor referring to the read end of a pipe. This
+ * pipe can be used to wake up a poll() based processing loop. The purpose of
+ * this function is very similar to mpv_set_wakeup_callback(), and provides
+ * a primitive mechanism to handle coordinating a foreign event loop and the
+ * libmpv event loop. The pipe is non-blocking. It's closed when the mpv_handle
+ * is destroyed. This function always returns the same value (on success).
+ *
+ * This is in fact implemented using the same underlying code as for
+ * mpv_set_wakeup_callback() (though they don't conflict), and it is as if each
+ * callback invocation writes a single 0 byte to the pipe. When the pipe
+ * becomes readable, the code calling poll() (or select()) on the pipe should
+ * read all contents of the pipe and then call mpv_wait_event(c, 0) until
+ * no new events are returned. The pipe contents do not matter and can just
+ * be discarded. There is not necessarily one byte per readable event in the
+ * pipe. For example, the pipes are non-blocking, and mpv won't block if the
+ * pipe is full. Pipes are normally limited to 4096 bytes, so if there are
+ * more than 4096 events, the number of readable bytes can not equal the number
+ * of events queued. Also, it's possible that mpv does not write to the pipe
+ * once it's guaranteed that the client was already signaled. See the example
+ * below how to do it correctly.
+ *
+ * Example:
+ *
+ * int pipefd = mpv_get_wakeup_pipe(mpv);
+ * if (pipefd < 0)
+ * error();
+ * while (1) {
+ * struct pollfd pfds[1] = {
+ * { .fd = pipefd, .events = POLLIN },
+ * };
+ * // Wait until there are possibly new mpv events.
+ * poll(pfds, 1, -1);
+ * if (pfds[0].revents & POLLIN) {
+ * // Empty the pipe. Doing this before calling mpv_wait_event()
+ * // ensures that no wakeups are missed. It's not so important to
+ * // make sure the pipe is really empty (it will just cause some
+ * // additional wakeups in unlikely corner cases).
+ * char unused[256];
+ * read(pipefd, unused, sizeof(unused));
+ * while (1) {
+ * mpv_event *ev = mpv_wait_event(mpv, 0);
+ * // If MPV_EVENT_NONE is received, the event queue is empty.
+ * if (ev->event_id == MPV_EVENT_NONE)
+ * break;
+ * // Process the event.
+ * ...
+ * }
+ * }
+ * }
+ *
+ * @deprecated this function will be removed in the future. If you need this
+ * functionality, use mpv_set_wakeup_callback(), create a pipe
+ * manually, and call write() on your pipe in the callback.
+ *
+ * @return A UNIX FD of the read end of the wakeup pipe, or -1 on error.
+ * On MS Windows/MinGW, this will always return -1.
+ */
+MPV_EXPORT int mpv_get_wakeup_pipe(mpv_handle *ctx);
+
+#endif
+
+/**
+ * Defining MPV_CPLUGIN_DYNAMIC_SYM during plugin compilation will replace mpv_*
+ * functions with function pointers. Those pointer will be initialized when
+ * loading the plugin.
+ *
+ * It is recommended to use this symbol table when targeting Windows. The loader
+ * does not have notion of global symbols. Loading cplugin into mpv process will
+ * not allow this plugin to call any of the symbols that may be available in
+ * other modules. Instead cplugin has to link explicitly to specific PE binary,
+ * libmpv-2.dll/mpv.exe or any other binary that may have linked mpv statically.
+ * This limits portability of cplugin as it would need to be compiled separately
+ * for each of target PE binary that includes mpv's symbols. Which in practice
+ * is unrealistic, as we want one cplugin to be loaded without those restrictions.
+ *
+ * Instead of linking to any PE binary, we create function pointers for all mpv's
+ * exported symbols. For convenience names of entrypoints are redefined to those
+ * pointer, so no changes are required in cplugin source code, except of defining
+ * MPV_CPLUGIN_DYNAMIC_SYM. Those function pointer are exported to make them
+ * available for mpv to init with correct values during runtime, before calling
+ * `mpv_open_cplugin`.
+ *
+ * Note that those pointers are decorated with `selectany` attribute, so no need
+ * to worry about multiple definitions, linker will keep only single instance.
+ */
+#ifdef MPV_CPLUGIN_DYNAMIC_SYM
+
+#define MPV_DEFINE_SYM_PTR(name) \
+ MPV_SELECTANY MPV_EXPORT \
+ MPV_DECLTYPE(name) *pfn_##name;
+
+MPV_DEFINE_SYM_PTR(mpv_client_api_version)
+#define mpv_client_api_version pfn_mpv_client_api_version
+MPV_DEFINE_SYM_PTR(mpv_error_string)
+#define mpv_error_string pfn_mpv_error_string
+MPV_DEFINE_SYM_PTR(mpv_free)
+#define mpv_free pfn_mpv_free
+MPV_DEFINE_SYM_PTR(mpv_client_name)
+#define mpv_client_name pfn_mpv_client_name
+MPV_DEFINE_SYM_PTR(mpv_client_id)
+#define mpv_client_id pfn_mpv_client_id
+MPV_DEFINE_SYM_PTR(mpv_create)
+#define mpv_create pfn_mpv_create
+MPV_DEFINE_SYM_PTR(mpv_initialize)
+#define mpv_initialize pfn_mpv_initialize
+MPV_DEFINE_SYM_PTR(mpv_destroy)
+#define mpv_destroy pfn_mpv_destroy
+MPV_DEFINE_SYM_PTR(mpv_terminate_destroy)
+#define mpv_terminate_destroy pfn_mpv_terminate_destroy
+MPV_DEFINE_SYM_PTR(mpv_create_client)
+#define mpv_create_client pfn_mpv_create_client
+MPV_DEFINE_SYM_PTR(mpv_create_weak_client)
+#define mpv_create_weak_client pfn_mpv_create_weak_client
+MPV_DEFINE_SYM_PTR(mpv_load_config_file)
+#define mpv_load_config_file pfn_mpv_load_config_file
+MPV_DEFINE_SYM_PTR(mpv_get_time_ns)
+#define mpv_get_time_ns pfn_mpv_get_time_ns
+MPV_DEFINE_SYM_PTR(mpv_get_time_us)
+#define mpv_get_time_us pfn_mpv_get_time_us
+MPV_DEFINE_SYM_PTR(mpv_free_node_contents)
+#define mpv_free_node_contents pfn_mpv_free_node_contents
+MPV_DEFINE_SYM_PTR(mpv_set_option)
+#define mpv_set_option pfn_mpv_set_option
+MPV_DEFINE_SYM_PTR(mpv_set_option_string)
+#define mpv_set_option_string pfn_mpv_set_option_string
+MPV_DEFINE_SYM_PTR(mpv_command)
+#define mpv_command pfn_mpv_command
+MPV_DEFINE_SYM_PTR(mpv_command_node)
+#define mpv_command_node pfn_mpv_command_node
+MPV_DEFINE_SYM_PTR(mpv_command_ret)
+#define mpv_command_ret pfn_mpv_command_ret
+MPV_DEFINE_SYM_PTR(mpv_command_string)
+#define mpv_command_string pfn_mpv_command_string
+MPV_DEFINE_SYM_PTR(mpv_command_async)
+#define mpv_command_async pfn_mpv_command_async
+MPV_DEFINE_SYM_PTR(mpv_command_node_async)
+#define mpv_command_node_async pfn_mpv_command_node_async
+MPV_DEFINE_SYM_PTR(mpv_abort_async_command)
+#define mpv_abort_async_command pfn_mpv_abort_async_command
+MPV_DEFINE_SYM_PTR(mpv_set_property)
+#define mpv_set_property pfn_mpv_set_property
+MPV_DEFINE_SYM_PTR(mpv_set_property_string)
+#define mpv_set_property_string pfn_mpv_set_property_string
+MPV_DEFINE_SYM_PTR(mpv_del_property)
+#define mpv_del_property pfn_mpv_del_property
+MPV_DEFINE_SYM_PTR(mpv_set_property_async)
+#define mpv_set_property_async pfn_mpv_set_property_async
+MPV_DEFINE_SYM_PTR(mpv_get_property)
+#define mpv_get_property pfn_mpv_get_property
+MPV_DEFINE_SYM_PTR(mpv_get_property_string)
+#define mpv_get_property_string pfn_mpv_get_property_string
+MPV_DEFINE_SYM_PTR(mpv_get_property_osd_string)
+#define mpv_get_property_osd_string pfn_mpv_get_property_osd_string
+MPV_DEFINE_SYM_PTR(mpv_get_property_async)
+#define mpv_get_property_async pfn_mpv_get_property_async
+MPV_DEFINE_SYM_PTR(mpv_observe_property)
+#define mpv_observe_property pfn_mpv_observe_property
+MPV_DEFINE_SYM_PTR(mpv_unobserve_property)
+#define mpv_unobserve_property pfn_mpv_unobserve_property
+MPV_DEFINE_SYM_PTR(mpv_event_name)
+#define mpv_event_name pfn_mpv_event_name
+MPV_DEFINE_SYM_PTR(mpv_event_to_node)
+#define mpv_event_to_node pfn_mpv_event_to_node
+MPV_DEFINE_SYM_PTR(mpv_request_event)
+#define mpv_request_event pfn_mpv_request_event
+MPV_DEFINE_SYM_PTR(mpv_request_log_messages)
+#define mpv_request_log_messages pfn_mpv_request_log_messages
+MPV_DEFINE_SYM_PTR(mpv_wait_event)
+#define mpv_wait_event pfn_mpv_wait_event
+MPV_DEFINE_SYM_PTR(mpv_wakeup)
+#define mpv_wakeup pfn_mpv_wakeup
+MPV_DEFINE_SYM_PTR(mpv_set_wakeup_callback)
+#define mpv_set_wakeup_callback pfn_mpv_set_wakeup_callback
+MPV_DEFINE_SYM_PTR(mpv_wait_async_requests)
+#define mpv_wait_async_requests pfn_mpv_wait_async_requests
+MPV_DEFINE_SYM_PTR(mpv_hook_add)
+#define mpv_hook_add pfn_mpv_hook_add
+MPV_DEFINE_SYM_PTR(mpv_hook_continue)
+#define mpv_hook_continue pfn_mpv_hook_continue
+MPV_DEFINE_SYM_PTR(mpv_get_wakeup_pipe)
+#define mpv_get_wakeup_pipe pfn_mpv_get_wakeup_pipe
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/libmpv/render.h b/libmpv/render.h
new file mode 100644
index 0000000..862ffde
--- /dev/null
+++ b/libmpv/render.h
@@ -0,0 +1,759 @@
+/* Copyright (C) 2018 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MPV_CLIENT_API_RENDER_H_
+#define MPV_CLIENT_API_RENDER_H_
+
+#include "client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Overview
+ * --------
+ *
+ * This API can be used to make mpv render using supported graphic APIs (such
+ * as OpenGL). It can be used to handle video display.
+ *
+ * The renderer needs to be created with mpv_render_context_create() before
+ * you start playback (or otherwise cause a VO to be created). Then (with most
+ * backends) mpv_render_context_render() can be used to explicitly render the
+ * current video frame. Use mpv_render_context_set_update_callback() to get
+ * notified when there is a new frame to draw.
+ *
+ * Preferably rendering should be done in a separate thread. If you call
+ * normal libmpv API functions on the renderer thread, deadlocks can result
+ * (these are made non-fatal with timeouts, but user experience will obviously
+ * suffer). See "Threading" section below.
+ *
+ * You can output and embed video without this API by setting the mpv "wid"
+ * option to a native window handle (see "Embedding the video window" section
+ * in the client.h header). In general, using the render API is recommended,
+ * because window embedding can cause various issues, especially with GUI
+ * toolkits and certain platforms.
+ *
+ * Supported backends
+ * ------------------
+ *
+ * OpenGL: via MPV_RENDER_API_TYPE_OPENGL, see render_gl.h header.
+ * Software: via MPV_RENDER_API_TYPE_SW, see section "Software renderer"
+ *
+ * Threading
+ * ---------
+ *
+ * You are recommended to do rendering on a separate thread than normal libmpv
+ * use.
+ *
+ * The mpv_render_* functions can be called from any thread, under the
+ * following conditions:
+ * - only one of the mpv_render_* functions can be called at the same time
+ * (unless they belong to different mpv cores created by mpv_create())
+ * - never can be called from within the callbacks set with
+ * mpv_set_wakeup_callback() or mpv_render_context_set_update_callback()
+ * - if the OpenGL backend is used, for all functions the OpenGL context
+ * must be "current" in the calling thread, and it must be the same OpenGL
+ * context as the mpv_render_context was created with. Otherwise, undefined
+ * behavior will occur.
+ * - the thread does not call libmpv API functions other than the mpv_render_*
+ * functions, except APIs which are declared as safe (see below). Likewise,
+ * there must be no lock or wait dependency from the render thread to a
+ * thread using other libmpv functions. Basically, the situation that your
+ * render thread waits for a "not safe" libmpv API function to return must
+ * not happen. If you ignore this requirement, deadlocks can happen, which
+ * are made non-fatal with timeouts; then playback quality will be degraded,
+ * and the message
+ * mpv_render_context_render() not being called or stuck.
+ * is logged. If you set MPV_RENDER_PARAM_ADVANCED_CONTROL, you promise that
+ * this won't happen, and must absolutely guarantee it, or a real deadlock
+ * will freeze the mpv core thread forever.
+ *
+ * libmpv functions which are safe to call from a render thread are:
+ * - functions marked with "Safe to be called from mpv render API threads."
+ * - client.h functions which don't have an explicit or implicit mpv_handle
+ * parameter
+ * - mpv_render_* functions; but only for the same mpv_render_context pointer.
+ * If the pointer is different, mpv_render_context_free() is not safe. (The
+ * reason is that if MPV_RENDER_PARAM_ADVANCED_CONTROL is set, it may have
+ * to process still queued requests from the core, which it can do only for
+ * the current context, while requests for other contexts would deadlock.
+ * Also, it may have to wait and block for the core to terminate the video
+ * chain to make sure no resources are used after context destruction.)
+ * - if the mpv_handle parameter refers to a different mpv core than the one
+ * you're rendering for (very obscure, but allowed)
+ *
+ * Note about old libmpv version:
+ *
+ * Before API version 1.105 (basically in mpv 0.29.x), simply enabling
+ * MPV_RENDER_PARAM_ADVANCED_CONTROL could cause deadlock issues. This can
+ * be worked around by setting the "vd-lavc-dr" option to "no".
+ * In addition, you were required to call all mpv_render*() API functions
+ * from the same thread on which mpv_render_context_create() was originally
+ * run (for the same the mpv_render_context). Not honoring it led to UB
+ * (deadlocks, use of invalid mp_thread handles), even if you moved your GL
+ * context to a different thread correctly.
+ * These problems were addressed in API version 1.105 (mpv 0.30.0).
+ *
+ * Context and handle lifecycle
+ * ----------------------------
+ *
+ * Video initialization will fail if the render context was not initialized yet
+ * (with mpv_render_context_create()), or it will revert to a VO that creates
+ * its own window.
+ *
+ * Currently, there can be only 1 mpv_render_context at a time per mpv core.
+ *
+ * Calling mpv_render_context_free() while a VO is using the render context is
+ * active will disable video.
+ *
+ * You must free the context with mpv_render_context_free() before the mpv core
+ * is destroyed. If this doesn't happen, undefined behavior will result.
+ *
+ * Software renderer
+ * -----------------
+ *
+ * MPV_RENDER_API_TYPE_SW provides an extremely simple (but slow) renderer to
+ * memory surfaces. You probably don't want to use this. Use other render API
+ * types, or other methods of video embedding.
+ *
+ * Use mpv_render_context_create() with MPV_RENDER_PARAM_API_TYPE set to
+ * MPV_RENDER_API_TYPE_SW.
+ *
+ * Call mpv_render_context_render() with various MPV_RENDER_PARAM_SW_* fields
+ * to render the video frame to an in-memory surface. The following fields are
+ * required: MPV_RENDER_PARAM_SW_SIZE, MPV_RENDER_PARAM_SW_FORMAT,
+ * MPV_RENDER_PARAM_SW_STRIDE, MPV_RENDER_PARAM_SW_POINTER.
+ *
+ * This method of rendering is very slow, because everything, including color
+ * conversion, scaling, and OSD rendering, is done on the CPU, single-threaded.
+ * In particular, large video or display sizes, as well as presence of OSD or
+ * subtitles can make it too slow for realtime. As with other software rendering
+ * VOs, setting "sw-fast" may help. Enabling or disabling zimg may help,
+ * depending on the platform.
+ *
+ * In addition, certain multimedia job creation measures like HDR may not work
+ * properly, and will have to be manually handled by for example inserting
+ * filters.
+ *
+ * This API is not really suitable to extract individual frames from video etc.
+ * (basically non-playback uses) - there are better libraries for this. It can
+ * be used this way, but it may be clunky and tricky.
+ *
+ * Further notes:
+ * - MPV_RENDER_PARAM_FLIP_Y is currently ignored (unsupported)
+ * - MPV_RENDER_PARAM_DEPTH is ignored (meaningless)
+ */
+
+/**
+ * Opaque context, returned by mpv_render_context_create().
+ */
+typedef struct mpv_render_context mpv_render_context;
+
+/**
+ * Parameters for mpv_render_param (which is used in a few places such as
+ * mpv_render_context_create().
+ *
+ * Also see mpv_render_param for conventions and how to use it.
+ */
+typedef enum mpv_render_param_type {
+ /**
+ * Not a valid value, but also used to terminate a params array. Its value
+ * is always guaranteed to be 0 (even if the ABI changes in the future).
+ */
+ MPV_RENDER_PARAM_INVALID = 0,
+ /**
+ * The render API to use. Valid for mpv_render_context_create().
+ *
+ * Type: char*
+ *
+ * Defined APIs:
+ *
+ * MPV_RENDER_API_TYPE_OPENGL:
+ * OpenGL desktop 2.1 or later (preferably core profile compatible to
+ * OpenGL 3.2), or OpenGLES 2.0 or later.
+ * Providing MPV_RENDER_PARAM_OPENGL_INIT_PARAMS is required.
+ * It is expected that an OpenGL context is valid and "current" when
+ * calling mpv_render_* functions (unless specified otherwise). It
+ * must be the same context for the same mpv_render_context.
+ */
+ MPV_RENDER_PARAM_API_TYPE = 1,
+ /**
+ * Required parameters for initializing the OpenGL renderer. Valid for
+ * mpv_render_context_create().
+ * Type: mpv_opengl_init_params*
+ */
+ MPV_RENDER_PARAM_OPENGL_INIT_PARAMS = 2,
+ /**
+ * Describes a GL render target. Valid for mpv_render_context_render().
+ * Type: mpv_opengl_fbo*
+ */
+ MPV_RENDER_PARAM_OPENGL_FBO = 3,
+ /**
+ * Control flipped rendering. Valid for mpv_render_context_render().
+ * Type: int*
+ * If the value is set to 0, render normally. Otherwise, render it flipped,
+ * which is needed e.g. when rendering to an OpenGL default framebuffer
+ * (which has a flipped coordinate system).
+ */
+ MPV_RENDER_PARAM_FLIP_Y = 4,
+ /**
+ * Control surface depth. Valid for mpv_render_context_render().
+ * Type: int*
+ * This implies the depth of the surface passed to the render function in
+ * bits per channel. If omitted or set to 0, the renderer will assume 8.
+ * Typically used to control dithering.
+ */
+ MPV_RENDER_PARAM_DEPTH = 5,
+ /**
+ * ICC profile blob. Valid for mpv_render_context_set_parameter().
+ * Type: mpv_byte_array*
+ * Set an ICC profile for use with the "icc-profile-auto" option. (If the
+ * option is not enabled, the ICC data will not be used.)
+ */
+ MPV_RENDER_PARAM_ICC_PROFILE = 6,
+ /**
+ * Ambient light in lux. Valid for mpv_render_context_set_parameter().
+ * Type: int*
+ * This can be used for automatic gamma correction.
+ */
+ MPV_RENDER_PARAM_AMBIENT_LIGHT = 7,
+ /**
+ * X11 Display, sometimes used for hwdec. Valid for
+ * mpv_render_context_create(). The Display must stay valid for the lifetime
+ * of the mpv_render_context.
+ * Type: Display*
+ */
+ MPV_RENDER_PARAM_X11_DISPLAY = 8,
+ /**
+ * Wayland display, sometimes used for hwdec. Valid for
+ * mpv_render_context_create(). The wl_display must stay valid for the
+ * lifetime of the mpv_render_context.
+ * Type: struct wl_display*
+ */
+ MPV_RENDER_PARAM_WL_DISPLAY = 9,
+ /**
+ * Better control about rendering and enabling some advanced features. Valid
+ * for mpv_render_context_create().
+ *
+ * This conflates multiple requirements the API user promises to abide if
+ * this option is enabled:
+ *
+ * - The API user's render thread, which is calling the mpv_render_*()
+ * functions, never waits for the core. Otherwise deadlocks can happen.
+ * See "Threading" section.
+ * - The callback set with mpv_render_context_set_update_callback() can now
+ * be called even if there is no new frame. The API user should call the
+ * mpv_render_context_update() function, and interpret the return value
+ * for whether a new frame should be rendered.
+ * - Correct functionality is impossible if the update callback is not set,
+ * or not set soon enough after mpv_render_context_create() (the core can
+ * block while waiting for you to call mpv_render_context_update(), and
+ * if the update callback is not correctly set, it will deadlock, or
+ * block for too long).
+ *
+ * In general, setting this option will enable the following features (and
+ * possibly more):
+ *
+ * - "Direct rendering", which means the player decodes directly to a
+ * texture, which saves a copy per video frame ("vd-lavc-dr" option
+ * needs to be enabled, and the rendering backend as well as the
+ * underlying GPU API/driver needs to have support for it).
+ * - Rendering screenshots with the GPU API if supported by the backend
+ * (instead of using a suboptimal software fallback via libswscale).
+ *
+ * Warning: do not just add this without reading the "Threading" section
+ * above, and then wondering that deadlocks happen. The
+ * requirements are tricky. But also note that even if advanced
+ * control is disabled, not adhering to the rules will lead to
+ * playback problems. Enabling advanced controls simply makes
+ * violating these rules fatal.
+ *
+ * Type: int*: 0 for disable (default), 1 for enable
+ */
+ MPV_RENDER_PARAM_ADVANCED_CONTROL = 10,
+ /**
+ * Return information about the next frame to render. Valid for
+ * mpv_render_context_get_info().
+ *
+ * Type: mpv_render_frame_info*
+ *
+ * It strictly returns information about the _next_ frame. The implication
+ * is that e.g. mpv_render_context_update()'s return value will have
+ * MPV_RENDER_UPDATE_FRAME set, and the user is supposed to call
+ * mpv_render_context_render(). If there is no next frame, then the
+ * return value will have is_valid set to 0.
+ */
+ MPV_RENDER_PARAM_NEXT_FRAME_INFO = 11,
+ /**
+ * Enable or disable video timing. Valid for mpv_render_context_render().
+ *
+ * Type: int*: 0 for disable, 1 for enable (default)
+ *
+ * When video is timed to audio, the player attempts to render video a bit
+ * ahead, and then do a blocking wait until the target display time is
+ * reached. This blocks mpv_render_context_render() for up to the amount
+ * specified with the "video-timing-offset" global option. You can set
+ * this parameter to 0 to disable this kind of waiting. If you do, it's
+ * recommended to use the target time value in mpv_render_frame_info to
+ * wait yourself, or to set the "video-timing-offset" to 0 instead.
+ *
+ * Disabling this without doing anything in addition will result in A/V sync
+ * being slightly off.
+ */
+ MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME = 12,
+ /**
+ * Use to skip rendering in mpv_render_context_render().
+ *
+ * Type: int*: 0 for rendering (default), 1 for skipping
+ *
+ * If this is set, you don't need to pass a target surface to the render
+ * function (and if you do, it's completely ignored). This can still call
+ * into the lower level APIs (i.e. if you use OpenGL, the OpenGL context
+ * must be set).
+ *
+ * Be aware that the render API will consider this frame as having been
+ * rendered. All other normal rules also apply, for example about whether
+ * you have to call mpv_render_context_report_swap(). It also does timing
+ * in the same way.
+ */
+ MPV_RENDER_PARAM_SKIP_RENDERING = 13,
+ /**
+ * Deprecated. Not supported. Use MPV_RENDER_PARAM_DRM_DISPLAY_V2 instead.
+ * Type : struct mpv_opengl_drm_params*
+ */
+ MPV_RENDER_PARAM_DRM_DISPLAY = 14,
+ /**
+ * DRM draw surface size, contains draw surface dimensions.
+ * Valid for mpv_render_context_create().
+ * Type : struct mpv_opengl_drm_draw_surface_size*
+ */
+ MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE = 15,
+ /**
+ * DRM display, contains drm display handles.
+ * Valid for mpv_render_context_create().
+ * Type : struct mpv_opengl_drm_params_v2*
+ */
+ MPV_RENDER_PARAM_DRM_DISPLAY_V2 = 16,
+ /**
+ * MPV_RENDER_API_TYPE_SW only: rendering target surface size, mandatory.
+ * Valid for MPV_RENDER_API_TYPE_SW & mpv_render_context_render().
+ * Type: int[2] (e.g.: int s[2] = {w, h}; param.data = &s[0];)
+ *
+ * The video frame is transformed as with other VOs. Typically, this means
+ * the video gets scaled and black bars are added if the video size or
+ * aspect ratio mismatches with the target size.
+ */
+ MPV_RENDER_PARAM_SW_SIZE = 17,
+ /**
+ * MPV_RENDER_API_TYPE_SW only: rendering target surface pixel format,
+ * mandatory.
+ * Valid for MPV_RENDER_API_TYPE_SW & mpv_render_context_render().
+ * Type: char* (e.g.: char *f = "rgb0"; param.data = f;)
+ *
+ * Valid values are:
+ * "rgb0", "bgr0", "0bgr", "0rgb"
+ * 4 bytes per pixel RGB, 1 byte (8 bit) per component, component bytes
+ * with increasing address from left to right (e.g. "rgb0" has r at
+ * address 0), the "0" component contains uninitialized garbage (often
+ * the value 0, but not necessarily; the bad naming is inherited from
+ * FFmpeg)
+ * Pixel alignment size: 4 bytes
+ * "rgb24"
+ * 3 bytes per pixel RGB. This is strongly discouraged because it is
+ * very slow.
+ * Pixel alignment size: 1 bytes
+ * other
+ * The API may accept other pixel formats, using mpv internal format
+ * names, as long as it's internally marked as RGB, has exactly 1
+ * plane, and is supported as conversion output. It is not a good idea
+ * to rely on any of these. Their semantics and handling could change.
+ */
+ MPV_RENDER_PARAM_SW_FORMAT = 18,
+ /**
+ * MPV_RENDER_API_TYPE_SW only: rendering target surface bytes per line,
+ * mandatory.
+ * Valid for MPV_RENDER_API_TYPE_SW & mpv_render_context_render().
+ * Type: size_t*
+ *
+ * This is the number of bytes between a pixel (x, y) and (x, y + 1) on the
+ * target surface. It must be a multiple of the pixel size, and have space
+ * for the surface width as specified by MPV_RENDER_PARAM_SW_SIZE.
+ *
+ * Both stride and pointer value should be a multiple of 64 to facilitate
+ * fast SIMD operation. Lower alignment might trigger slower code paths,
+ * and in the worst case, will copy the entire target frame. If mpv is built
+ * with zimg (and zimg is not disabled), the performance impact might be
+ * less.
+ * In either cases, the pointer and stride must be aligned at least to the
+ * pixel alignment size. Otherwise, crashes and undefined behavior is
+ * possible on platforms which do not support unaligned accesses (either
+ * through normal memory access or aligned SIMD memory access instructions).
+ */
+ MPV_RENDER_PARAM_SW_STRIDE = 19,
+ /*
+ * MPV_RENDER_API_TYPE_SW only: rendering target surface pixel data pointer,
+ * mandatory.
+ * Valid for MPV_RENDER_API_TYPE_SW & mpv_render_context_render().
+ * Type: void*
+ *
+ * This points to the first pixel at the left/top corner (0, 0). In
+ * particular, each line y starts at (pointer + stride * y). Upon rendering,
+ * all data between pointer and (pointer + stride * h) is overwritten.
+ * Whether the padding between (w, y) and (0, y + 1) is overwritten is left
+ * unspecified (it should not be, but unfortunately some scaler backends
+ * will do it anyway). It is assumed that even the padding after the last
+ * line (starting at bytepos(w, h) until (pointer + stride * h)) is
+ * writable.
+ *
+ * See MPV_RENDER_PARAM_SW_STRIDE for alignment requirements.
+ */
+ MPV_RENDER_PARAM_SW_POINTER = 20,
+} mpv_render_param_type;
+
+/**
+ * For backwards compatibility with the old naming of
+ * MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE
+ */
+#define MPV_RENDER_PARAM_DRM_OSD_SIZE MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE
+
+/**
+ * Used to pass arbitrary parameters to some mpv_render_* functions. The
+ * meaning of the data parameter is determined by the type, and each
+ * MPV_RENDER_PARAM_* documents what type the value must point to.
+ *
+ * Each value documents the required data type as the pointer you cast to
+ * void* and set on mpv_render_param.data. For example, if MPV_RENDER_PARAM_FOO
+ * documents the type as Something* , then the code should look like this:
+ *
+ * Something foo = {...};
+ * mpv_render_param param;
+ * param.type = MPV_RENDER_PARAM_FOO;
+ * param.data = & foo;
+ *
+ * Normally, the data field points to exactly 1 object. If the type is char*,
+ * it points to a 0-terminated string.
+ *
+ * In all cases (unless documented otherwise) the pointers need to remain
+ * valid during the call only. Unless otherwise documented, the API functions
+ * will not write to the params array or any data pointed to it.
+ *
+ * As a convention, parameter arrays are always terminated by type==0. There
+ * is no specific order of the parameters required. The order of the 2 fields in
+ * this struct is guaranteed (even after ABI changes).
+ */
+typedef struct mpv_render_param {
+ enum mpv_render_param_type type;
+ void *data;
+} mpv_render_param;
+
+
+/**
+ * Predefined values for MPV_RENDER_PARAM_API_TYPE.
+ */
+// See render_gl.h
+#define MPV_RENDER_API_TYPE_OPENGL "opengl"
+// See section "Software renderer"
+#define MPV_RENDER_API_TYPE_SW "sw"
+
+/**
+ * Flags used in mpv_render_frame_info.flags. Each value represents a bit in it.
+ */
+typedef enum mpv_render_frame_info_flag {
+ /**
+ * Set if there is actually a next frame. If unset, there is no next frame
+ * yet, and other flags and fields that require a frame to be queued will
+ * be unset.
+ *
+ * This is set for _any_ kind of frame, even for redraw requests.
+ *
+ * Note that when this is unset, it simply means no new frame was
+ * decoded/queued yet, not necessarily that the end of the video was
+ * reached. A new frame can be queued after some time.
+ *
+ * If the return value of mpv_render_context_render() had the
+ * MPV_RENDER_UPDATE_FRAME flag set, this flag will usually be set as well,
+ * unless the frame is rendered, or discarded by other asynchronous events.
+ */
+ MPV_RENDER_FRAME_INFO_PRESENT = 1 << 0,
+ /**
+ * If set, the frame is not an actual new video frame, but a redraw request.
+ * For example if the video is paused, and an option that affects video
+ * rendering was changed (or any other reason), an update request can be
+ * issued and this flag will be set.
+ *
+ * Typically, redraw frames will not be subject to video timing.
+ *
+ * Implies MPV_RENDER_FRAME_INFO_PRESENT.
+ */
+ MPV_RENDER_FRAME_INFO_REDRAW = 1 << 1,
+ /**
+ * If set, this is supposed to reproduce the previous frame perfectly. This
+ * is usually used for certain "video-sync" options ("display-..." modes).
+ * Typically the renderer will blit the video from a FBO. Unset otherwise.
+ *
+ * Implies MPV_RENDER_FRAME_INFO_PRESENT.
+ */
+ MPV_RENDER_FRAME_INFO_REPEAT = 1 << 2,
+ /**
+ * If set, the player timing code expects that the user thread blocks on
+ * vsync (by either delaying the render call, or by making a call to
+ * mpv_render_context_report_swap() at vsync time).
+ *
+ * Implies MPV_RENDER_FRAME_INFO_PRESENT.
+ */
+ MPV_RENDER_FRAME_INFO_BLOCK_VSYNC = 1 << 3,
+} mpv_render_frame_info_flag;
+
+/**
+ * Information about the next video frame that will be rendered. Can be
+ * retrieved with MPV_RENDER_PARAM_NEXT_FRAME_INFO.
+ */
+typedef struct mpv_render_frame_info {
+ /**
+ * A bitset of mpv_render_frame_info_flag values (i.e. multiple flags are
+ * combined with bitwise or).
+ */
+ uint64_t flags;
+ /**
+ * Absolute time at which the frame is supposed to be displayed. This is in
+ * the same unit and base as the time returned by mpv_get_time_us(). For
+ * frames that are redrawn, or if vsync locked video timing is used (see
+ * "video-sync" option), then this can be 0. The "video-timing-offset"
+ * option determines how much "headroom" the render thread gets (but a high
+ * enough frame rate can reduce it anyway). mpv_render_context_render() will
+ * normally block until the time is elapsed, unless you pass it
+ * MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME = 0.
+ */
+ int64_t target_time;
+} mpv_render_frame_info;
+
+/**
+ * Initialize the renderer state. Depending on the backend used, this will
+ * access the underlying GPU API and initialize its own objects.
+ *
+ * You must free the context with mpv_render_context_free(). Not doing so before
+ * the mpv core is destroyed may result in memory leaks or crashes.
+ *
+ * Currently, only at most 1 context can exists per mpv core (it represents the
+ * main video output).
+ *
+ * You should pass the following parameters:
+ * - MPV_RENDER_PARAM_API_TYPE to select the underlying backend/GPU API.
+ * - Backend-specific init parameter, like MPV_RENDER_PARAM_OPENGL_INIT_PARAMS.
+ * - Setting MPV_RENDER_PARAM_ADVANCED_CONTROL and following its rules is
+ * strongly recommended.
+ * - If you want to use hwdec, possibly hwdec interop resources.
+ *
+ * @param res set to the context (on success) or NULL (on failure). The value
+ * is never read and always overwritten.
+ * @param mpv handle used to get the core (the mpv_render_context won't depend
+ * on this specific handle, only the core referenced by it)
+ * @param params an array of parameters, terminated by type==0. It's left
+ * unspecified what happens with unknown parameters. At least
+ * MPV_RENDER_PARAM_API_TYPE is required, and most backends will
+ * require another backend-specific parameter.
+ * @return error code, including but not limited to:
+ * MPV_ERROR_UNSUPPORTED: the OpenGL version is not supported
+ * (or required extensions are missing)
+ * MPV_ERROR_NOT_IMPLEMENTED: an unknown API type was provided, or
+ * support for the requested API was not
+ * built in the used libmpv binary.
+ * MPV_ERROR_INVALID_PARAMETER: at least one of the provided parameters was
+ * not valid.
+ */
+MPV_EXPORT int mpv_render_context_create(mpv_render_context **res, mpv_handle *mpv,
+ mpv_render_param *params);
+
+/**
+ * Attempt to change a single parameter. Not all backends and parameter types
+ * support all kinds of changes.
+ *
+ * @param ctx a valid render context
+ * @param param the parameter type and data that should be set
+ * @return error code. If a parameter could actually be changed, this returns
+ * success, otherwise an error code depending on the parameter type
+ * and situation.
+ */
+MPV_EXPORT int mpv_render_context_set_parameter(mpv_render_context *ctx,
+ mpv_render_param param);
+
+/**
+ * Retrieve information from the render context. This is NOT a counterpart to
+ * mpv_render_context_set_parameter(), because you generally can't read
+ * parameters set with it, and this function is not meant for this purpose.
+ * Instead, this is for communicating information from the renderer back to the
+ * user. See mpv_render_param_type; entries which support this function
+ * explicitly mention it, and for other entries you can assume it will fail.
+ *
+ * You pass param with param.type set and param.data pointing to a variable
+ * of the required data type. The function will then overwrite that variable
+ * with the returned value (at least on success).
+ *
+ * @param ctx a valid render context
+ * @param param the parameter type and data that should be retrieved
+ * @return error code. If a parameter could actually be retrieved, this returns
+ * success, otherwise an error code depending on the parameter type
+ * and situation. MPV_ERROR_NOT_IMPLEMENTED is used for unknown
+ * param.type, or if retrieving it is not supported.
+ */
+MPV_EXPORT int mpv_render_context_get_info(mpv_render_context *ctx,
+ mpv_render_param param);
+
+typedef void (*mpv_render_update_fn)(void *cb_ctx);
+
+/**
+ * Set the callback that notifies you when a new video frame is available, or
+ * if the video display configuration somehow changed and requires a redraw.
+ * Similar to mpv_set_wakeup_callback(), you must not call any mpv API from
+ * the callback, and all the other listed restrictions apply (such as not
+ * exiting the callback by throwing exceptions).
+ *
+ * This can be called from any thread, except from an update callback. In case
+ * of the OpenGL backend, no OpenGL state or API is accessed.
+ *
+ * Calling this will raise an update callback immediately.
+ *
+ * @param callback callback(callback_ctx) is called if the frame should be
+ * redrawn
+ * @param callback_ctx opaque argument to the callback
+ */
+MPV_EXPORT void mpv_render_context_set_update_callback(mpv_render_context *ctx,
+ mpv_render_update_fn callback,
+ void *callback_ctx);
+
+/**
+ * The API user is supposed to call this when the update callback was invoked
+ * (like all mpv_render_* functions, this has to happen on the render thread,
+ * and _not_ from the update callback itself).
+ *
+ * This is optional if MPV_RENDER_PARAM_ADVANCED_CONTROL was not set (default).
+ * Otherwise, it's a hard requirement that this is called after each update
+ * callback. If multiple update callback happened, and the function could not
+ * be called sooner, it's OK to call it once after the last callback.
+ *
+ * If an update callback happens during or after this function, the function
+ * must be called again at the soonest possible time.
+ *
+ * If MPV_RENDER_PARAM_ADVANCED_CONTROL was set, this will do additional work
+ * such as allocating textures for the video decoder.
+ *
+ * @return a bitset of mpv_render_update_flag values (i.e. multiple flags are
+ * combined with bitwise or). Typically, this will tell the API user
+ * what should happen next. E.g. if the MPV_RENDER_UPDATE_FRAME flag is
+ * set, mpv_render_context_render() should be called. If flags unknown
+ * to the API user are set, or if the return value is 0, nothing needs
+ * to be done.
+ */
+MPV_EXPORT uint64_t mpv_render_context_update(mpv_render_context *ctx);
+
+/**
+ * Flags returned by mpv_render_context_update(). Each value represents a bit
+ * in the function's return value.
+ */
+typedef enum mpv_render_update_flag {
+ /**
+ * A new video frame must be rendered. mpv_render_context_render() must be
+ * called.
+ */
+ MPV_RENDER_UPDATE_FRAME = 1 << 0,
+} mpv_render_context_flag;
+
+/**
+ * Render video.
+ *
+ * Typically renders the video to a target surface provided via mpv_render_param
+ * (the details depend on the backend in use). Options like "panscan" are
+ * applied to determine which part of the video should be visible and how the
+ * video should be scaled. You can change these options at runtime by using the
+ * mpv property API.
+ *
+ * The renderer will reconfigure itself every time the target surface
+ * configuration (such as size) is changed.
+ *
+ * This function implicitly pulls a video frame from the internal queue and
+ * renders it. If no new frame is available, the previous frame is redrawn.
+ * The update callback set with mpv_render_context_set_update_callback()
+ * notifies you when a new frame was added. The details potentially depend on
+ * the backends and the provided parameters.
+ *
+ * Generally, libmpv will invoke your update callback some time before the video
+ * frame should be shown, and then lets this function block until the supposed
+ * display time. This will limit your rendering to video FPS. You can prevent
+ * this by setting the "video-timing-offset" global option to 0. (This applies
+ * only to "audio" video sync mode.)
+ *
+ * You should pass the following parameters:
+ * - Backend-specific target object, such as MPV_RENDER_PARAM_OPENGL_FBO.
+ * - Possibly transformations, such as MPV_RENDER_PARAM_FLIP_Y.
+ *
+ * @param ctx a valid render context
+ * @param params an array of parameters, terminated by type==0. Which parameters
+ * are required depends on the backend. It's left unspecified what
+ * happens with unknown parameters.
+ * @return error code
+ */
+MPV_EXPORT int mpv_render_context_render(mpv_render_context *ctx, mpv_render_param *params);
+
+/**
+ * Tell the renderer that a frame was flipped at the given time. This is
+ * optional, but can help the player to achieve better timing.
+ *
+ * Note that calling this at least once informs libmpv that you will use this
+ * function. If you use it inconsistently, expect bad video playback.
+ *
+ * If this is called while no video is initialized, it is ignored.
+ *
+ * @param ctx a valid render context
+ */
+MPV_EXPORT void mpv_render_context_report_swap(mpv_render_context *ctx);
+
+/**
+ * Destroy the mpv renderer state.
+ *
+ * If video is still active (e.g. a file playing), video will be disabled
+ * forcefully.
+ *
+ * @param ctx a valid render context. After this function returns, this is not
+ * a valid pointer anymore. NULL is also allowed and does nothing.
+ */
+MPV_EXPORT void mpv_render_context_free(mpv_render_context *ctx);
+
+#ifdef MPV_CPLUGIN_DYNAMIC_SYM
+
+MPV_DEFINE_SYM_PTR(mpv_render_context_create)
+#define mpv_render_context_create pfn_mpv_render_context_create
+MPV_DEFINE_SYM_PTR(mpv_render_context_set_parameter)
+#define mpv_render_context_set_parameter pfn_mpv_render_context_set_parameter
+MPV_DEFINE_SYM_PTR(mpv_render_context_get_info)
+#define mpv_render_context_get_info pfn_mpv_render_context_get_info
+MPV_DEFINE_SYM_PTR(mpv_render_context_set_update_callback)
+#define mpv_render_context_set_update_callback pfn_mpv_render_context_set_update_callback
+MPV_DEFINE_SYM_PTR(mpv_render_context_update)
+#define mpv_render_context_update pfn_mpv_render_context_update
+MPV_DEFINE_SYM_PTR(mpv_render_context_render)
+#define mpv_render_context_render pfn_mpv_render_context_render
+MPV_DEFINE_SYM_PTR(mpv_render_context_report_swap)
+#define mpv_render_context_report_swap pfn_mpv_render_context_report_swap
+MPV_DEFINE_SYM_PTR(mpv_render_context_free)
+#define mpv_render_context_free pfn_mpv_render_context_free
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/libmpv/render_gl.h b/libmpv/render_gl.h
new file mode 100644
index 0000000..a2c31f0
--- /dev/null
+++ b/libmpv/render_gl.h
@@ -0,0 +1,211 @@
+/* Copyright (C) 2018 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MPV_CLIENT_API_RENDER_GL_H_
+#define MPV_CLIENT_API_RENDER_GL_H_
+
+#include "render.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * OpenGL backend
+ * --------------
+ *
+ * This header contains definitions for using OpenGL with the render.h API.
+ *
+ * OpenGL interop
+ * --------------
+ *
+ * The OpenGL backend has some special rules, because OpenGL itself uses
+ * implicit per-thread contexts, which causes additional API problems.
+ *
+ * This assumes the OpenGL context lives on a certain thread controlled by the
+ * API user. All mpv_render_* APIs have to be assumed to implicitly use the
+ * OpenGL context if you pass a mpv_render_context using the OpenGL backend,
+ * unless specified otherwise.
+ *
+ * The OpenGL context is indirectly accessed through the OpenGL function
+ * pointers returned by the get_proc_address callback in mpv_opengl_init_params.
+ * Generally, mpv will not load the system OpenGL library when using this API.
+ *
+ * OpenGL state
+ * ------------
+ *
+ * OpenGL has a large amount of implicit state. All the mpv functions mentioned
+ * above expect that the OpenGL state is reasonably set to OpenGL standard
+ * defaults. Likewise, mpv will attempt to leave the OpenGL context with
+ * standard defaults. The following state is excluded from this:
+ *
+ * - the glViewport state
+ * - the glScissor state (but GL_SCISSOR_TEST is in its default value)
+ * - glBlendFuncSeparate() state (but GL_BLEND is in its default value)
+ * - glClearColor() state
+ * - mpv may overwrite the callback set with glDebugMessageCallback()
+ * - mpv always disables GL_DITHER at init
+ *
+ * Messing with the state could be avoided by creating shared OpenGL contexts,
+ * but this is avoided for the sake of compatibility and interoperability.
+ *
+ * On OpenGL 2.1, mpv will strictly call functions like glGenTextures() to
+ * create OpenGL objects. You will have to do the same. This ensures that
+ * objects created by mpv and the API users don't clash. Also, legacy state
+ * must be either in its defaults, or not interfere with core state.
+ *
+ * API use
+ * -------
+ *
+ * The mpv_render_* API is used. That API supports multiple backends, and this
+ * section documents specifics for the OpenGL backend.
+ *
+ * Use mpv_render_context_create() with MPV_RENDER_PARAM_API_TYPE set to
+ * MPV_RENDER_API_TYPE_OPENGL, and MPV_RENDER_PARAM_OPENGL_INIT_PARAMS provided.
+ *
+ * Call mpv_render_context_render() with MPV_RENDER_PARAM_OPENGL_FBO to render
+ * the video frame to an FBO.
+ *
+ * Hardware decoding
+ * -----------------
+ *
+ * Hardware decoding via this API is fully supported, but requires some
+ * additional setup. (At least if direct hardware decoding modes are wanted,
+ * instead of copying back surface data from GPU to CPU RAM.)
+ *
+ * There may be certain requirements on the OpenGL implementation:
+ *
+ * - Windows: ANGLE is required (although in theory GL/DX interop could be used)
+ * - Intel/Linux: EGL is required, and also the native display resource needs
+ * to be provided (e.g. MPV_RENDER_PARAM_X11_DISPLAY for X11 and
+ * MPV_RENDER_PARAM_WL_DISPLAY for Wayland)
+ * - nVidia/Linux: Both GLX and EGL should work (GLX is required if vdpau is
+ * used, e.g. due to old drivers.)
+ * - OSX: CGL is required (CGLGetCurrentContext() returning non-NULL)
+ * - iOS: EAGL is required (EAGLContext.currentContext returning non-nil)
+ *
+ * Once these things are setup, hardware decoding can be enabled/disabled at
+ * any time by setting the "hwdec" property.
+ */
+
+/**
+ * For initializing the mpv OpenGL state via MPV_RENDER_PARAM_OPENGL_INIT_PARAMS.
+ */
+typedef struct mpv_opengl_init_params {
+ /**
+ * This retrieves OpenGL function pointers, and will use them in subsequent
+ * operation.
+ * Usually, you can simply call the GL context APIs from this callback (e.g.
+ * glXGetProcAddressARB or wglGetProcAddress), but some APIs do not always
+ * return pointers for all standard functions (even if present); in this
+ * case you have to compensate by looking up these functions yourself when
+ * libmpv wants to resolve them through this callback.
+ * libmpv will not normally attempt to resolve GL functions on its own, nor
+ * does it link to GL libraries directly.
+ */
+ void *(*get_proc_address)(void *ctx, const char *name);
+ /**
+ * Value passed as ctx parameter to get_proc_address().
+ */
+ void *get_proc_address_ctx;
+} mpv_opengl_init_params;
+
+/**
+ * For MPV_RENDER_PARAM_OPENGL_FBO.
+ */
+typedef struct mpv_opengl_fbo {
+ /**
+ * Framebuffer object name. This must be either a valid FBO generated by
+ * glGenFramebuffers() that is complete and color-renderable, or 0. If the
+ * value is 0, this refers to the OpenGL default framebuffer.
+ */
+ int fbo;
+ /**
+ * Valid dimensions. This must refer to the size of the framebuffer. This
+ * must always be set.
+ */
+ int w, h;
+ /**
+ * Underlying texture internal format (e.g. GL_RGBA8), or 0 if unknown. If
+ * this is the default framebuffer, this can be an equivalent.
+ */
+ int internal_format;
+} mpv_opengl_fbo;
+
+/**
+ * Deprecated. For MPV_RENDER_PARAM_DRM_DISPLAY.
+ */
+typedef struct mpv_opengl_drm_params {
+ int fd;
+ int crtc_id;
+ int connector_id;
+ struct _drmModeAtomicReq **atomic_request_ptr;
+ int render_fd;
+} mpv_opengl_drm_params;
+
+/**
+ * For MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE.
+ */
+typedef struct mpv_opengl_drm_draw_surface_size {
+ /**
+ * size of the draw plane surface in pixels.
+ */
+ int width, height;
+} mpv_opengl_drm_draw_surface_size;
+
+/**
+ * For MPV_RENDER_PARAM_DRM_DISPLAY_V2.
+ */
+typedef struct mpv_opengl_drm_params_v2 {
+ /**
+ * DRM fd (int). Set to -1 if invalid.
+ */
+ int fd;
+
+ /**
+ * Currently used crtc id
+ */
+ int crtc_id;
+
+ /**
+ * Currently used connector id
+ */
+ int connector_id;
+
+ /**
+ * Pointer to a drmModeAtomicReq pointer that is being used for the renderloop.
+ * This pointer should hold a pointer to the atomic request pointer
+ * The atomic request pointer is usually changed at every renderloop.
+ */
+ struct _drmModeAtomicReq **atomic_request_ptr;
+
+ /**
+ * DRM render node. Used for VAAPI interop.
+ * Set to -1 if invalid.
+ */
+ int render_fd;
+} mpv_opengl_drm_params_v2;
+
+
+/**
+ * For backwards compatibility with the old naming of mpv_opengl_drm_draw_surface_size
+ */
+#define mpv_opengl_drm_osd_size mpv_opengl_drm_draw_surface_size
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/libmpv/stream_cb.h b/libmpv/stream_cb.h
new file mode 100644
index 0000000..9ae6f31
--- /dev/null
+++ b/libmpv/stream_cb.h
@@ -0,0 +1,247 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MPV_CLIENT_API_STREAM_CB_H_
+#define MPV_CLIENT_API_STREAM_CB_H_
+
+#include "client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Warning: this API is not stable yet.
+ *
+ * Overview
+ * --------
+ *
+ * This API can be used to make mpv read from a stream with a custom
+ * implementation. This interface is inspired by funopen on BSD and
+ * fopencookie on linux. The stream is backed by user-defined callbacks
+ * which can implement customized open, read, seek, size and close behaviors.
+ *
+ * Usage
+ * -----
+ *
+ * Register your stream callbacks with the mpv_stream_cb_add_ro() function. You
+ * have to provide a mpv_stream_cb_open_ro_fn callback to it (open_fn argument).
+ *
+ * Once registered, you can `loadfile myprotocol://myfile`. Your open_fn will be
+ * invoked with the URI and you must fill out the provided mpv_stream_cb_info
+ * struct. This includes your stream callbacks (like read_fn), and an opaque
+ * cookie, which will be passed as the first argument to all the remaining
+ * stream callbacks.
+ *
+ * Note that your custom callbacks must not invoke libmpv APIs as that would
+ * cause a deadlock. (Unless you call a different mpv_handle than the one the
+ * callback was registered for, and the mpv_handles refer to different mpv
+ * instances.)
+ *
+ * Stream lifetime
+ * ---------------
+ *
+ * A stream remains valid until its close callback has been called. It's up to
+ * libmpv to call the close callback, and the libmpv user cannot close it
+ * directly with the stream_cb API.
+ *
+ * For example, if you consider your custom stream to become suddenly invalid
+ * (maybe because the underlying stream died), libmpv will continue using your
+ * stream. All you can do is returning errors from each callback, until libmpv
+ * gives up and closes it.
+ *
+ * Protocol registration and lifetime
+ * ----------------------------------
+ *
+ * Protocols remain registered until the mpv instance is terminated. This means
+ * in particular that it can outlive the mpv_handle that was used to register
+ * it, but once mpv_terminate_destroy() is called, your registered callbacks
+ * will not be called again.
+ *
+ * Protocol unregistration is finished after the mpv core has been destroyed
+ * (e.g. after mpv_terminate_destroy() has returned).
+ *
+ * If you do not call mpv_terminate_destroy() yourself (e.g. plugin-style code),
+ * you will have to deal with the registration or even streams outliving your
+ * code. Here are some possible ways to do this:
+ * - call mpv_terminate_destroy(), which destroys the core, and will make sure
+ * all streams are closed once this function returns
+ * - you refcount all resources your stream "cookies" reference, so that it
+ * doesn't matter if streams live longer than expected
+ * - create "cancellation" semantics: after your protocol has been unregistered,
+ * notify all your streams that are still opened, and make them drop all
+ * referenced resources - then return errors from the stream callbacks as
+ * long as the stream is still opened
+ *
+ */
+
+/**
+ * Read callback used to implement a custom stream. The semantics of the
+ * callback match read(2) in blocking mode. Short reads are allowed (you can
+ * return less bytes than requested, and libmpv will retry reading the rest
+ * with another call). If no data can be immediately read, the callback must
+ * block until there is new data. A return of 0 will be interpreted as final
+ * EOF, although libmpv might retry the read, or seek to a different position.
+ *
+ * @param cookie opaque cookie identifying the stream,
+ * returned from mpv_stream_cb_open_fn
+ * @param buf buffer to read data into
+ * @param size of the buffer
+ * @return number of bytes read into the buffer
+ * @return 0 on EOF
+ * @return -1 on error
+ */
+typedef int64_t (*mpv_stream_cb_read_fn)(void *cookie, char *buf, uint64_t nbytes);
+
+/**
+ * Seek callback used to implement a custom stream.
+ *
+ * Note that mpv will issue a seek to position 0 immediately after opening. This
+ * is used to test whether the stream is seekable (since seekability might
+ * depend on the URI contents, not just the protocol). Return
+ * MPV_ERROR_UNSUPPORTED if seeking is not implemented for this stream. This
+ * seek also serves to establish the fact that streams start at position 0.
+ *
+ * This callback can be NULL, in which it behaves as if always returning
+ * MPV_ERROR_UNSUPPORTED.
+ *
+ * @param cookie opaque cookie identifying the stream,
+ * returned from mpv_stream_cb_open_fn
+ * @param offset target absolute stream position
+ * @return the resulting offset of the stream
+ * MPV_ERROR_UNSUPPORTED or MPV_ERROR_GENERIC if the seek failed
+ */
+typedef int64_t (*mpv_stream_cb_seek_fn)(void *cookie, int64_t offset);
+
+/**
+ * Size callback used to implement a custom stream.
+ *
+ * Return MPV_ERROR_UNSUPPORTED if no size is known.
+ *
+ * This callback can be NULL, in which it behaves as if always returning
+ * MPV_ERROR_UNSUPPORTED.
+ *
+ * @param cookie opaque cookie identifying the stream,
+ * returned from mpv_stream_cb_open_fn
+ * @return the total size in bytes of the stream
+ */
+typedef int64_t (*mpv_stream_cb_size_fn)(void *cookie);
+
+/**
+ * Close callback used to implement a custom stream.
+ *
+ * @param cookie opaque cookie identifying the stream,
+ * returned from mpv_stream_cb_open_fn
+ */
+typedef void (*mpv_stream_cb_close_fn)(void *cookie);
+
+/**
+ * Cancel callback used to implement a custom stream.
+ *
+ * This callback is used to interrupt any current or future read and seek
+ * operations. It will be called from a separate thread than the demux
+ * thread, and should not block.
+ *
+ * This callback can be NULL.
+ *
+ * Available since API 1.106.
+ *
+ * @param cookie opaque cookie identifying the stream,
+ * returned from mpv_stream_cb_open_fn
+ */
+typedef void (*mpv_stream_cb_cancel_fn)(void *cookie);
+
+/**
+ * See mpv_stream_cb_open_ro_fn callback.
+ */
+typedef struct mpv_stream_cb_info {
+ /**
+ * Opaque user-provided value, which will be passed to the other callbacks.
+ * The close callback will be called to release the cookie. It is not
+ * interpreted by mpv. It doesn't even need to be a valid pointer.
+ *
+ * The user sets this in the mpv_stream_cb_open_ro_fn callback.
+ */
+ void *cookie;
+
+ /**
+ * Callbacks set by the user in the mpv_stream_cb_open_ro_fn callback. Some
+ * of them are optional, and can be left unset.
+ *
+ * The following callbacks are mandatory: read_fn, close_fn
+ */
+ mpv_stream_cb_read_fn read_fn;
+ mpv_stream_cb_seek_fn seek_fn;
+ mpv_stream_cb_size_fn size_fn;
+ mpv_stream_cb_close_fn close_fn;
+ mpv_stream_cb_cancel_fn cancel_fn; /* since API 1.106 */
+} mpv_stream_cb_info;
+
+/**
+ * Open callback used to implement a custom read-only (ro) stream. The user
+ * must set the callback fields in the passed info struct. The cookie field
+ * also can be set to store state associated to the stream instance.
+ *
+ * Note that the info struct is valid only for the duration of this callback.
+ * You can't change the callbacks or the pointer to the cookie at a later point.
+ *
+ * Each stream instance created by the open callback can have different
+ * callbacks.
+ *
+ * The close_fn callback will terminate the stream instance. The pointers to
+ * your callbacks and cookie will be discarded, and the callbacks will not be
+ * called again.
+ *
+ * @param user_data opaque user data provided via mpv_stream_cb_add()
+ * @param uri name of the stream to be opened (with protocol prefix)
+ * @param info fields which the user should fill
+ * @return 0 on success, MPV_ERROR_LOADING_FAILED if the URI cannot be opened.
+ */
+typedef int (*mpv_stream_cb_open_ro_fn)(void *user_data, char *uri,
+ mpv_stream_cb_info *info);
+
+/**
+ * Add a custom stream protocol. This will register a protocol handler under
+ * the given protocol prefix, and invoke the given callbacks if an URI with the
+ * matching protocol prefix is opened.
+ *
+ * The "ro" is for read-only - only read-only streams can be registered with
+ * this function.
+ *
+ * The callback remains registered until the mpv core is registered.
+ *
+ * If a custom stream with the same name is already registered, then the
+ * MPV_ERROR_INVALID_PARAMETER error is returned.
+ *
+ * @param protocol protocol prefix, for example "foo" for "foo://" URIs
+ * @param user_data opaque pointer passed into the mpv_stream_cb_open_fn
+ * callback.
+ * @return error code
+ */
+MPV_EXPORT int mpv_stream_cb_add_ro(mpv_handle *ctx, const char *protocol, void *user_data,
+ mpv_stream_cb_open_ro_fn open_fn);
+
+#ifdef MPV_CPLUGIN_DYNAMIC_SYM
+
+MPV_DEFINE_SYM_PTR(mpv_stream_cb_add_ro)
+#define mpv_stream_cb_add_ro pfn_mpv_stream_cb_add_ro
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..fdfc526
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,1765 @@
+project('mpv',
+ 'c',
+ license: ['GPL2+', 'LGPL2.1+'],
+ version: files('./VERSION'),
+ meson_version: '>=0.62.0',
+ default_options: [
+ 'buildtype=debugoptimized',
+ 'b_lundef=false',
+ 'c_std=c11',
+ 'warning_level=1',
+ ]
+)
+
+build_root = meson.project_build_root()
+source_root = meson.project_source_root()
+python = find_program('python3')
+
+# ffmpeg
+libavcodec = dependency('libavcodec', version: '>= 58.134.100')
+libavfilter = dependency('libavfilter', version: '>= 7.110.100')
+libavformat = dependency('libavformat', version: '>= 58.76.100')
+libavutil = dependency('libavutil', version: '>= 56.70.100')
+libswresample = dependency('libswresample', version: '>= 3.9.100')
+libswscale = dependency('libswscale', version: '>= 5.9.100')
+
+libplacebo = dependency('libplacebo', version: '>=6.338.0')
+
+libass = dependency('libass', version: '>= 0.12.2')
+
+# the dependency order of libass -> ffmpeg is necessary due to
+# static linking symbol resolution between fontconfig and MinGW
+dependencies = [libass,
+ libavcodec,
+ libavfilter,
+ libavformat,
+ libavutil,
+ libplacebo,
+ libswresample,
+ libswscale]
+
+# Keeps track of all enabled/disabled features
+features = {
+ 'debug': get_option('debug'),
+ 'ffmpeg': true,
+ 'gpl': get_option('gpl'),
+ 'jpegxl': libavformat.version().version_compare('>= 59.27.100'),
+ 'avif-muxer': libavformat.version().version_compare('>= 59.24.100'),
+ 'libass': true,
+ 'threads': true,
+ 'libplacebo': true,
+}
+
+
+# generic sources
+sources = files(
+ ## Audio
+ 'audio/aframe.c',
+ 'audio/chmap.c',
+ 'audio/chmap_sel.c',
+ 'audio/decode/ad_lavc.c',
+ 'audio/decode/ad_spdif.c',
+ 'audio/filter/af_drop.c',
+ 'audio/filter/af_format.c',
+ 'audio/filter/af_lavcac3enc.c',
+ 'audio/filter/af_scaletempo.c',
+ 'audio/filter/af_scaletempo2.c',
+ 'audio/filter/af_scaletempo2_internals.c',
+ 'audio/fmt-conversion.c',
+ 'audio/format.c',
+ 'audio/out/ao.c',
+ 'audio/out/ao_lavc.c',
+ 'audio/out/ao_null.c',
+ 'audio/out/ao_pcm.c',
+ 'audio/out/buffer.c',
+
+ ## Core
+ 'common/av_common.c',
+ 'common/av_log.c',
+ 'common/codecs.c',
+ 'common/common.c',
+ 'common/encode_lavc.c',
+ 'common/msg.c',
+ 'common/playlist.c',
+ 'common/recorder.c',
+ 'common/stats.c',
+ 'common/tags.c',
+ 'common/version.c',
+
+ ## Demuxers
+ 'demux/codec_tags.c',
+ 'demux/cue.c',
+ 'demux/cache.c',
+ 'demux/demux.c',
+ 'demux/demux_cue.c',
+ 'demux/demux_disc.c',
+ 'demux/demux_edl.c',
+ 'demux/demux_lavf.c',
+ 'demux/demux_mf.c',
+ 'demux/demux_mkv.c',
+ 'demux/demux_mkv_timeline.c',
+ 'demux/demux_null.c',
+ 'demux/demux_playlist.c',
+ 'demux/demux_raw.c',
+ 'demux/demux_timeline.c',
+ 'demux/ebml.c',
+ 'demux/packet.c',
+ 'demux/timeline.c',
+
+ ## Filters
+ 'filters/f_async_queue.c',
+ 'filters/f_autoconvert.c',
+ 'filters/f_auto_filters.c',
+ 'filters/f_decoder_wrapper.c',
+ 'filters/f_demux_in.c',
+ 'filters/f_hwtransfer.c',
+ 'filters/f_lavfi.c',
+ 'filters/f_output_chain.c',
+ 'filters/f_swresample.c',
+ 'filters/f_swscale.c',
+ 'filters/f_utils.c',
+ 'filters/filter.c',
+ 'filters/frame.c',
+ 'filters/user_filters.c',
+
+ ## Input
+ 'input/cmd.c',
+ 'input/event.c',
+ 'input/input.c',
+ 'input/ipc.c',
+ 'input/keycodes.c',
+
+ ## Misc
+ 'misc/bstr.c',
+ 'misc/charset_conv.c',
+ 'misc/dispatch.c',
+ 'misc/json.c',
+ 'misc/language.c',
+ 'misc/natural_sort.c',
+ 'misc/node.c',
+ 'misc/random.c',
+ 'misc/rendezvous.c',
+ 'misc/thread_pool.c',
+ 'misc/thread_tools.c',
+
+ ## Options
+ 'options/m_config_core.c',
+ 'options/m_config_frontend.c',
+ 'options/m_option.c',
+ 'options/m_property.c',
+ 'options/options.c',
+ 'options/parse_commandline.c',
+ 'options/parse_configfile.c',
+ 'options/path.c',
+
+ ## Player
+ 'player/audio.c',
+ 'player/client.c',
+ 'player/command.c',
+ 'player/configfiles.c',
+ 'player/external_files.c',
+ 'player/loadfile.c',
+ 'player/main.c',
+ 'player/misc.c',
+ 'player/osd.c',
+ 'player/playloop.c',
+ 'player/screenshot.c',
+ 'player/scripting.c',
+ 'player/sub.c',
+ 'player/video.c',
+
+ ## Streams
+ 'stream/cookies.c',
+ 'stream/stream.c',
+ 'stream/stream_avdevice.c',
+ 'stream/stream_cb.c',
+ 'stream/stream_concat.c',
+ 'stream/stream_edl.c',
+ 'stream/stream_file.c',
+ 'stream/stream_lavf.c',
+ 'stream/stream_memory.c',
+ 'stream/stream_mf.c',
+ 'stream/stream_null.c',
+ 'stream/stream_slice.c',
+
+ ## Subtitles
+ 'sub/ass_mp.c',
+ 'sub/dec_sub.c',
+ 'sub/draw_bmp.c',
+ 'sub/filter_sdh.c',
+ 'sub/img_convert.c',
+ 'sub/lavc_conv.c',
+ 'sub/osd.c',
+ 'sub/osd_libass.c',
+ 'sub/sd_ass.c',
+ 'sub/sd_lavc.c',
+
+ ## Video
+ 'video/csputils.c',
+ 'video/decode/vd_lavc.c',
+ 'video/filter/refqueue.c',
+ 'video/filter/vf_format.c',
+ 'video/filter/vf_sub.c',
+ 'video/fmt-conversion.c',
+ 'video/hwdec.c',
+ 'video/image_loader.c',
+ 'video/image_writer.c',
+ 'video/img_format.c',
+ 'video/mp_image.c',
+ 'video/mp_image_pool.c',
+ 'video/out/aspect.c',
+ 'video/out/bitmap_packer.c',
+ 'video/out/dither.c',
+ 'video/out/dr_helper.c',
+ 'video/out/filter_kernels.c',
+ 'video/out/gpu/context.c',
+ 'video/out/gpu/error_diffusion.c',
+ 'video/out/gpu/hwdec.c',
+ 'video/out/gpu/lcms.c',
+ 'video/out/gpu/libmpv_gpu.c',
+ 'video/out/gpu/osd.c',
+ 'video/out/gpu/ra.c',
+ 'video/out/gpu/shader_cache.c',
+ 'video/out/gpu/spirv.c',
+ 'video/out/gpu/user_shaders.c',
+ 'video/out/gpu/utils.c',
+ 'video/out/gpu/video.c',
+ 'video/out/gpu/video_shaders.c',
+ 'video/out/libmpv_sw.c',
+ 'video/out/vo.c',
+ 'video/out/vo_gpu.c',
+ 'video/out/vo_image.c',
+ 'video/out/vo_lavc.c',
+ 'video/out/vo_libmpv.c',
+ 'video/out/vo_null.c',
+ 'video/out/vo_tct.c',
+ 'video/out/vo_kitty.c',
+ 'video/out/win_state.c',
+ 'video/repack.c',
+ 'video/sws_utils.c',
+
+ ## libplacebo
+ 'video/out/placebo/ra_pl.c',
+ 'video/out/placebo/utils.c',
+ 'video/out/vo_gpu_next.c',
+ 'video/out/gpu_next/context.c',
+
+ ## osdep
+ 'osdep/io.c',
+ 'osdep/semaphore_osx.c',
+ 'osdep/subprocess.c',
+ 'osdep/timer.c',
+
+ ## tree_allocator
+ 'ta/ta.c',
+ 'ta/ta_talloc.c',
+ 'ta/ta_utils.c'
+)
+
+
+# compiler stuff
+cc = meson.get_compiler('c')
+
+flags = ['-D_GNU_SOURCE', '-D_FILE_OFFSET_BITS=64']
+link_flags = []
+
+test_flags = ['-Werror=implicit-function-declaration',
+ '-Wempty-body',
+ '-Wdisabled-optimization',
+ '-Wstrict-prototypes',
+ '-Wno-format-zero-length',
+ '-Wno-redundant-decls',
+ '-Wvla',
+ '-Wno-format-truncation',
+ '-Wimplicit-fallthrough',
+ '-fno-math-errno']
+
+flags += cc.get_supported_arguments(test_flags)
+
+if cc.has_multi_arguments('-Wformat', '-Werror=format-security')
+ flags += ['-Wformat', '-Werror=format-security']
+endif
+
+if cc.get_id() == 'gcc'
+ gcc_flags = ['-Wundef', '-Wmissing-prototypes', '-Wshadow',
+ '-Wno-switch', '-Wparentheses', '-Wpointer-arith',
+ '-Wno-pointer-sign',
+ # GCC bug 66425
+ '-Wno-unused-result']
+ flags += gcc_flags
+endif
+
+if cc.get_id() == 'clang'
+ clang_flags = ['-Wno-logical-op-parentheses', '-Wno-switch',
+ '-Wno-tautological-compare', '-Wno-pointer-sign',
+ '-Wno-tautological-constant-out-of-range-compare']
+ flags += clang_flags
+endif
+
+darwin = host_machine.system() == 'darwin'
+win32 = host_machine.system() == 'cygwin' or host_machine.system() == 'windows'
+posix = not win32
+
+features += {'darwin': darwin}
+features += {'posix': posix}
+features += {'dos-paths': win32, 'win32': win32}
+
+mswin_flags = ['-D_WIN32_WINNT=0x0602', '-DUNICODE', '-DCOBJMACROS',
+ '-DINITGUID', '-U__STRICT_ANSI__']
+
+if host_machine.system() == 'windows'
+ flags += [mswin_flags, '-D__USE_MINGW_ANSI_STDIO=1']
+endif
+
+if host_machine.system() == 'cygwin'
+ flags += [mswin_flags, '-mwin32']
+endif
+
+noexecstack = false
+if cc.has_link_argument('-Wl,-z,noexecstack')
+ link_flags += '-Wl,-z,noexecstack'
+ noexecstack = true
+endif
+
+if cc.has_link_argument('-Wl,--nxcompat,--no-seh,--dynamicbase')
+ link_flags += '-Wl,--nxcompat,--no-seh,--dynamicbase'
+ noexecstack = true
+endif
+
+features += {'noexecstack': noexecstack}
+
+features += {'build-date': get_option('build-date')}
+if not features['build-date']
+ flags += '-DNO_BUILD_TIMESTAMPS'
+endif
+
+features += {'ta-leak-report': get_option('ta-leak-report')}
+
+libdl = dependency('dl', required: false)
+features += {'libdl': libdl.found()}
+if features['libdl']
+ dependencies += libdl
+endif
+
+# C11 atomics are mandatory but linking to the library is not always required.
+dependencies += cc.find_library('atomic', required: false)
+
+cplugins = get_option('cplugins').require(
+ win32 or (features['libdl'] and cc.has_link_argument('-rdynamic')),
+ error_message: 'cplugins not supported by the os or compiler!',
+)
+features += {'cplugins': cplugins.allowed()}
+if features['cplugins'] and not win32
+ link_flags += '-rdynamic'
+endif
+
+win32_threads = get_option('win32-threads').require(win32)
+
+features += {'win32-threads': win32_threads.allowed()}
+if not features['win32-threads']
+ pthreads = dependency('threads')
+ sources += files('osdep/threads-posix.c')
+ features += {'pthread-condattr-setclock':
+ cc.has_header_symbol('pthread.h',
+ 'pthread_condattr_setclock',
+ dependencies: pthreads)}
+ dependencies += pthreads
+endif
+
+pthread_debug = get_option('pthread-debug').require(
+ win32_threads.disabled(),
+ error_message: 'win32-threads was found!',
+)
+features += {'pthread-debug': pthread_debug.allowed()}
+
+add_project_arguments(flags, language: 'c')
+add_project_link_arguments(link_flags, language: ['c', 'objc'])
+
+
+# osdep
+cocoa = dependency('appleframeworks', modules: ['Cocoa', 'IOKit', 'QuartzCore'],
+ required: get_option('cocoa'))
+features += {'cocoa': cocoa.found()}
+if features['cocoa']
+ dependencies += cocoa
+ sources += files('osdep/apple_utils.c',
+ 'osdep/language-apple.c',
+ 'osdep/macosx_application.m',
+ 'osdep/macosx_events.m',
+ 'osdep/macosx_menubar.m',
+ 'osdep/main-fn-cocoa.c',
+ 'osdep/path-macosx.m')
+endif
+
+if posix
+ path_source = files('osdep/path-unix.c')
+ subprocess_source = files('osdep/subprocess-posix.c')
+ sources += files('input/ipc-unix.c',
+ 'osdep/poll_wrapper.c',
+ 'osdep/terminal-unix.c',
+ 'sub/filter_regex.c')
+endif
+
+if posix and not features['cocoa']
+ sources += files('osdep/main-fn-unix.c',
+ 'osdep/language-posix.c')
+endif
+
+if darwin
+ path_source = files('osdep/path-darwin.c')
+ timer_source = files('osdep/timer-darwin.c')
+endif
+
+if posix and not darwin
+ timer_source = files('osdep/timer-linux.c')
+endif
+
+features += {'ppoll': cc.has_function('ppoll', args: '-D_GNU_SOURCE',
+ prefix: '#include <poll.h>')}
+
+cd_devices = {
+ 'windows': 'D:',
+ 'cygwin': 'D:',
+ 'darwin': '/dev/disk1',
+ 'freebsd': '/dev/cd0',
+ 'openbsd': '/dev/rcd0c',
+ 'linux': '/dev/sr0',
+}
+if host_machine.system() in cd_devices
+ cd_device = cd_devices[host_machine.system()]
+else
+ cd_device = '/dev/cdrom'
+endif
+
+dvd_devices = {
+ 'windows': 'D:',
+ 'cygwin': 'D:',
+ 'darwin': '/dev/diskN',
+ 'freebsd': '/dev/cd0',
+ 'openbsd': '/dev/rcd0c',
+ 'linux': '/dev/sr0',
+}
+if host_machine.system() in cd_devices
+ dvd_device = dvd_devices[host_machine.system()]
+else
+ dvd_device = '/dev/dvd'
+endif
+
+features += {'android': host_machine.system() == 'android'}
+if features['android']
+ dependencies += cc.find_library('android')
+ sources += files('audio/out/ao_audiotrack.c',
+ 'misc/jni.c',
+ 'osdep/android/strnlen.c',
+ 'video/out/android_common.c',
+ 'video/out/vo_mediacodec_embed.c')
+endif
+
+uwp_opt = get_option('uwp').require(
+ not get_option('cplayer'),
+ error_message: 'cplayer is not false!',
+)
+uwp = cc.find_library('windowsapp', required: uwp_opt)
+features += {'uwp': uwp.found()}
+if features['uwp']
+ dependencies += uwp
+ path_source = files('osdep/path-uwp.c')
+ subprocess_source = []
+endif
+
+features += {'win32-executable': win32 and get_option('cplayer')}
+if win32
+ timer_source = files('osdep/timer-win32.c')
+ sources += files('osdep/w32_keyboard.c',
+ 'osdep/windows_utils.c')
+endif
+
+features += {'win32-desktop': win32 and not uwp.found()}
+if features['win32-desktop']
+ win32_desktop_libs = [cc.find_library('avrt'),
+ cc.find_library('dwmapi'),
+ cc.find_library('gdi32'),
+ cc.find_library('ole32'),
+ cc.find_library('uuid'),
+ cc.find_library('uxtheme'),
+ cc.find_library('version'),
+ cc.find_library('winmm')]
+ dependencies += win32_desktop_libs
+ path_source = files('osdep/path-win.c')
+ subprocess_source = files('osdep/subprocess-win.c')
+ sources += files('input/ipc-win.c',
+ 'osdep/language-win.c',
+ 'osdep/main-fn-win.c',
+ 'osdep/terminal-win.c',
+ 'video/out/w32_common.c',
+ 'video/out/win32/displayconfig.c',
+ 'video/out/win32/droptarget.c')
+endif
+
+if not posix and not features['win32-desktop']
+ subprocess_source = files('osdep/subprocess-dummy.c')
+ sources += files('input/ipc-dummy.c',
+ 'osdep/terminal-dummy.c')
+endif
+
+features += {'glob-posix': cc.has_function('glob', prefix: '#include <glob.h>')}
+
+features += {'glob-win32': win32 and not posix}
+if features['glob-win32']
+ sources += files('osdep/glob-win.c')
+endif
+
+features += {'glob': features['glob-posix'] or features['glob-win32']}
+
+features += {'vt.h': cc.has_header_symbol('sys/vt.h', 'VT_GETMODE')}
+
+features += {'consio.h': not features['vt.h'] and cc.has_header_symbol('sys/consio.h', 'VT_GETMODE')}
+
+# macOS's pthread_setname_np is a special snowflake and differs from literally every other platform.
+features += {'osx-thread-name': darwin}
+
+features += {'glibc-thread-name': false}
+if not features['osx-thread-name']
+ features += {'glibc-thread-name': posix and cc.has_function('pthread_setname_np', args: '-D_GNU_SOURCE',
+ dependencies: pthreads, prefix: '#include <pthread.h>')}
+endif
+
+features += {'bsd-thread-name': false}
+if not features['osx-thread-name'] and not features['glibc-thread-name']
+ features += {'bsd-thread-name': posix and cc.has_function('pthread_set_name_np', dependencies: pthreads,
+ prefix: '#include <pthread.h>\n#include <pthread_np.h>')}
+endif
+
+features += {'bsd-fstatfs': cc.has_function('fstatfs', prefix: '#include <sys/mount.h>\n#include <sys/param.h>')}
+
+features += {'linux-fstatfs': cc.has_function('fstatfs', prefix: '#include <sys/vfs.h>')}
+
+vector_attribute = '''int main() {
+float v __attribute__((vector_size(32)));
+}
+'''
+vector = get_option('vector').require(
+ cc.compiles(vector_attribute, name: 'vector check'),
+ error_message: 'the compiler does not support gcc vectors!',
+)
+features += {'vector': vector.allowed()}
+
+sources += path_source + subprocess_source + timer_source
+
+
+# various file generations
+tools_directory = join_paths(source_root, 'TOOLS')
+docutils_wrapper = find_program(join_paths(tools_directory, 'docutils-wrapper.py'))
+file2string = find_program(join_paths(tools_directory, 'file2string.py'))
+matroska = find_program(join_paths(tools_directory, 'matroska.py'))
+
+ebml_defs = custom_target('ebml_defs.inc',
+ output: 'ebml_defs.inc',
+ command: [matroska, '--generate-definitions', '@OUTPUT@'],
+)
+
+ebml_types = custom_target('ebml_types.h',
+ output: 'ebml_types.h',
+ command: [matroska, '--generate-header', '@OUTPUT@'],
+)
+
+sources += [ebml_defs, ebml_types]
+
+subdir('common')
+subdir('etc')
+subdir('player')
+subdir('sub')
+
+if darwin
+ subdir(join_paths('TOOLS', 'osxbundle'))
+endif
+
+
+# misc dependencies
+features += {'av-channel-layout': libavutil.version().version_compare('>= 57.24.100')}
+if features['av-channel-layout']
+ sources += files('audio/chmap_avchannel.c')
+endif
+
+cdda_opt = get_option('cdda').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+cdio = dependency('libcdio', version: '>= 0.90', required: cdda_opt)
+cdda = dependency('libcdio_paranoia', required: cdda_opt)
+features += {'cdda': cdda.found() and cdio.found()}
+if features['cdda']
+ dependencies += [cdda, cdio]
+ sources += files('stream/stream_cdda.c')
+endif
+
+dvbin = get_option('dvbin').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+features += {'dvbin': dvbin.allowed()}
+if features['dvbin']
+ sources += files('stream/dvb_tune.c',
+ 'stream/stream_dvb.c')
+endif
+
+dvdnav_opt = get_option('dvdnav').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+dvdnav = dependency('dvdnav', version: '>= 4.2.0', required: dvdnav_opt)
+dvdread = dependency('dvdread', version: '>= 4.1.0', required: dvdnav_opt)
+features += {'dvdnav': dvdnav.found() and dvdread.found()}
+if features['dvdnav']
+ dependencies += [dvdnav, dvdread]
+ sources += files('stream/stream_dvdnav.c')
+endif
+
+iconv = dependency('iconv', required: get_option('iconv'))
+features += {'iconv': iconv.found()}
+if features['iconv']
+ dependencies += iconv
+endif
+
+javascript = dependency('mujs', version: '>= 1.0.0', required: get_option('javascript'))
+features += {'javascript': javascript.found()}
+if features['javascript']
+ dependencies += javascript
+ sources += files('player/javascript.c',
+ 'sub/filter_jsre.c')
+endif
+
+lcms2 = dependency('lcms2', version: '>= 2.6', required: get_option('lcms2'))
+features += {'lcms2': lcms2.found()}
+if features['lcms2']
+ dependencies += lcms2
+endif
+
+libarchive = dependency('libarchive', version: '>= 3.4.0', required: get_option('libarchive'))
+features += {'libarchive': libarchive.found()}
+if features['libarchive']
+ dependencies += libarchive
+ sources += files('demux/demux_libarchive.c',
+ 'stream/stream_libarchive.c')
+endif
+
+libavdevice = dependency('libavdevice', version: '>= 58.13.100', required: get_option('libavdevice'))
+features += {'libavdevice': libavdevice.found()}
+if features['libavdevice']
+ dependencies += libavdevice
+endif
+
+libbluray = dependency('libbluray', version: '>= 0.3.0', required: get_option('libbluray'))
+features += {'libbluray': libbluray.found()}
+if features['libbluray']
+ dependencies += libbluray
+ sources += files('stream/stream_bluray.c')
+endif
+
+libm = cc.find_library('m', required: false)
+features += {'libm': libm.found()}
+if features['libm']
+ dependencies += libm
+endif
+
+librt = cc.find_library('rt', required: false)
+features += {'librt': librt.found()}
+if features['librt']
+ dependencies += librt
+endif
+
+lua = dependency('', required: false)
+lua_opt = get_option('lua')
+if lua_opt != 'disabled'
+ lua_version = [['lua', ['>=5.1.0', '<5.3.0']], # generic lua.pc
+ ['lua52', '>= 5.2.0'],
+ ['lua5.2', '>= 5.2.0'],
+ ['lua-5.2', '>= 5.2.0'],
+ ['luajit', '>= 2.0.0'],
+ ['lua51', '>= 5.1.0'],
+ ['lua5.1', '>= 5.1.0'],
+ ['lua-5.1', '>= 5.1.0']]
+ foreach version : lua_version
+ if lua_opt == 'auto' or lua_opt == 'enabled'
+ lua = dependency(version[0], version: version[1], required: false)
+ if lua.found()
+ break
+ endif
+ elif lua_opt == version[0]
+ lua = dependency(version[0], version: version[1])
+ if lua.found()
+ break
+ endif
+ endif
+ endforeach
+endif
+
+features += {'lua': lua.found()}
+lua_version = lua.name()
+if features['lua']
+ dependencies += lua
+ sources += files('player/lua.c')
+endif
+if not features['lua'] and lua_opt == 'enabled'
+ error('lua enabled but no suitable lua version could be found!')
+endif
+
+rubberband = dependency('rubberband', version: '>= 1.8.0', required: get_option('rubberband'))
+features += {'rubberband': rubberband.found()}
+features += {'rubberband-3': rubberband.version().version_compare('>= 3.0.0')}
+if features['rubberband']
+ dependencies += rubberband
+ sources += files('audio/filter/af_rubberband.c')
+endif
+
+sdl2 = dependency('sdl2', required: get_option('sdl2'))
+features += {'sdl2': sdl2.found()}
+if features['sdl2']
+ dependencies += sdl2
+endif
+
+sdl2_gamepad = get_option('sdl2-gamepad').require(
+ features['sdl2'],
+ error_message: 'sdl2 was not found!',
+)
+features += {'sdl2-gamepad': sdl2_gamepad.allowed()}
+if features['sdl2-gamepad']
+ sources += files('input/sdl_gamepad.c')
+endif
+
+uchardet_opt = get_option('uchardet').require(
+ features['iconv'],
+ error_message: 'iconv was not found!',
+)
+uchardet = dependency('uchardet', required: uchardet_opt)
+features += {'uchardet': uchardet.found()}
+if features['uchardet']
+ dependencies += uchardet
+endif
+
+features += {'lavu-uuid': libavutil.version().version_compare('>= 57.27.100')}
+if not features['lavu-uuid']
+ sources += files('misc/uuid.c')
+endif
+
+vapoursynth = dependency('vapoursynth', version: '>= 26', required: get_option('vapoursynth'))
+vapoursynth_script = dependency('vapoursynth-script', version: '>= 26',
+ required: get_option('vapoursynth'))
+features += {'vapoursynth': vapoursynth.found() and vapoursynth_script.found()}
+if features['vapoursynth']
+ dependencies += [vapoursynth, vapoursynth_script]
+ sources += files('video/filter/vf_vapoursynth.c')
+endif
+
+zimg = dependency('zimg', version: '>= 2.9', required: get_option('zimg'))
+features += {'zimg': zimg.found()}
+if features['zimg']
+ dependencies += zimg
+ sources += files('video/filter/vf_fingerprint.c',
+ 'video/zimg.c')
+ features += {'zimg-st428': zimg.version().version_compare('>= 3.0.5')}
+endif
+
+zlib = dependency('zlib', required: get_option('zlib'))
+features += {'zlib': zlib.found()}
+if features['zlib']
+ dependencies += zlib
+endif
+
+
+# audio output dependencies
+alsa = dependency('alsa', version: '>= 1.0.18', required: get_option('alsa'))
+features += {'alsa': alsa.found()}
+if features['alsa']
+ dependencies += alsa
+ sources += files('audio/out/ao_alsa.c')
+endif
+
+audiounit = {
+ 'deps': dependency('appleframeworks', modules: ['Foundation', 'AudioToolbox'],
+ required: get_option('audiounit')),
+ 'symbol': cc.has_header_symbol('AudioToolbox/AudioToolbox.h', 'kAudioUnitSubType_RemoteIO',
+ required: get_option('audiounit')),
+}
+features += {'audiounit': audiounit['deps'].found() and audiounit['symbol']}
+if features['audiounit']
+ dependencies += audiounit['deps']
+ sources += files('audio/out/ao_audiounit.m')
+endif
+
+coreaudio = dependency('appleframeworks', modules: ['CoreFoundation', 'CoreAudio',
+ 'AudioUnit', 'AudioToolbox'], required: get_option('coreaudio'))
+features += {'coreaudio': coreaudio.found()}
+if features['coreaudio']
+ dependencies += coreaudio
+ sources += files('audio/out/ao_coreaudio.c',
+ 'audio/out/ao_coreaudio_exclusive.c',
+ 'audio/out/ao_coreaudio_properties.c')
+endif
+
+if features['audiounit'] or features['coreaudio']
+ sources += files('audio/out/ao_coreaudio_chmap.c',
+ 'audio/out/ao_coreaudio_utils.c')
+endif
+
+jack_opt = get_option('jack').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+jack = dependency('jack', required: jack_opt)
+features += {'jack': jack.found()}
+if features['jack']
+ dependencies += jack
+ sources += files('audio/out/ao_jack.c')
+endif
+
+openal = dependency('openal', version: '>= 1.13', required: get_option('openal'))
+features += {'openal': openal.found()}
+if features['openal']
+ dependencies += openal
+ sources += files('audio/out/ao_openal.c')
+endif
+
+opensles = cc.find_library('OpenSLES', required: get_option('opensles'))
+features += {'opensles': opensles.found()}
+if features['opensles']
+ dependencies += opensles
+ sources += files('audio/out/ao_opensles.c')
+endif
+
+oss_opt = get_option('oss-audio').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+features += {'oss-audio': cc.has_header_symbol('sys/soundcard.h', 'SNDCTL_DSP_HALT',
+ required: oss_opt)}
+if features['oss-audio']
+ sources += files('audio/out/ao_oss.c')
+endif
+
+pipewire = dependency('libpipewire-0.3', version: '>= 0.3.48', required: get_option('pipewire'))
+features += {'pipewire': pipewire.found()}
+if features['pipewire']
+ dependencies += pipewire
+ sources += files('audio/out/ao_pipewire.c')
+endif
+
+pulse = dependency('libpulse', version: '>= 1.0', required: get_option('pulse'))
+features += {'pulse': pulse.found()}
+if features['pulse']
+ dependencies += pulse
+ sources += files('audio/out/ao_pulse.c')
+endif
+
+sdl2_audio = get_option('sdl2-audio').require(
+ features['sdl2'],
+ error_message: 'sdl2 was not found!',
+)
+features += {'sdl2-audio': sdl2_audio.allowed()}
+if features['sdl2-audio']
+ sources += files('audio/out/ao_sdl.c')
+endif
+
+sndio = dependency('sndio', required: get_option('sndio'))
+features += {'sndio': sndio.found()}
+features += {'sndio-1-9': sndio.version().version_compare('>= 1.9.0')}
+if features['sndio']
+ dependencies += sndio
+ sources += files('audio/out/ao_sndio.c')
+endif
+
+wasapi = cc.has_header_symbol('audioclient.h', 'IAudioClient', required: get_option('wasapi'))
+features += {'wasapi': wasapi}
+if features['wasapi']
+ sources += files('audio/out/ao_wasapi.c',
+ 'audio/out/ao_wasapi_changenotify.c',
+ 'audio/out/ao_wasapi_utils.c')
+endif
+
+
+# video output dependencies
+caca_opt = get_option('caca').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+caca = dependency('caca', version: '>= 0.99.beta18', required: caca_opt)
+features += {'caca': caca.found()}
+if features['caca']
+ dependencies += caca
+ sources += files('video/out/vo_caca.c')
+endif
+
+direct3d_opt = get_option('direct3d').require(
+ get_option('gpl') and features['win32-desktop'],
+ error_message: 'the build is not GPL or this is not a win32 desktop!',
+)
+direct3d = cc.has_header('d3d9.h', required: direct3d_opt)
+features += {'direct3d': direct3d}
+if features['direct3d']
+ sources += files('video/out/vo_direct3d.c')
+endif
+
+drm = dependency('libdrm', version: '>= 2.4.105', required: get_option('drm'))
+features += {'drm': drm.found() and (features['vt.h'] or features['consio.h'])}
+if features['drm']
+ dependencies += drm
+ sources += files('video/drmprime.c',
+ 'video/out/drm_atomic.c',
+ 'video/out/drm_common.c',
+ 'video/out/drm_prime.c',
+ 'video/out/hwdec/hwdec_drmprime.c',
+ 'video/out/hwdec/hwdec_drmprime_overlay.c',
+ 'video/out/vo_drm.c')
+endif
+
+gbm = dependency('gbm', version: '>=17.1.0', required: get_option('gbm'))
+features += {'gbm': gbm.found()}
+if features['gbm']
+ dependencies += gbm
+endif
+
+jpeg = dependency('libjpeg', required: get_option('jpeg'))
+features += {'jpeg': jpeg.found()}
+if features['jpeg']
+ dependencies += jpeg
+endif
+
+sdl2_video = get_option('sdl2-video').require(
+ features['sdl2'],
+ error_message: 'sdl2 was not found!',
+)
+features += {'sdl2-video': sdl2_video.allowed()}
+if features['sdl2-video']
+ sources += files('video/out/vo_sdl.c')
+endif
+
+shaderc = dependency('shaderc', required: get_option('shaderc'))
+features += {'shaderc': shaderc.found()}
+if features['shaderc']
+ dependencies += shaderc
+ sources += files('video/out/gpu/spirv_shaderc.c')
+endif
+
+sixel = dependency('libsixel', version: '>= 1.5', required: get_option('sixel'))
+features += {'sixel': sixel.found()}
+if features['sixel']
+ dependencies += sixel
+ sources += files('video/out/vo_sixel.c')
+endif
+
+features += {'posix-shm': false}
+if features['posix']
+ features += {'posix-shm': cc.has_function('shm_open', prefix: '#include <sys/mman.h>')}
+endif
+
+spirv_cross = dependency('spirv-cross-c-shared', required: get_option('spirv-cross'))
+features += {'spirv-cross': spirv_cross.found()}
+if features['spirv-cross']
+ dependencies += spirv_cross
+endif
+
+d3d11 = get_option('d3d11').require(
+ features['win32-desktop'] and features['shaderc'] and features['spirv-cross'],
+ error_message: 'Either is not a win32 desktop or shaderc nor spirv-cross were found!',
+)
+features += {'d3d11': d3d11.allowed()}
+if features['d3d11']
+ sources += files('video/out/d3d11/context.c',
+ 'video/out/d3d11/ra_d3d11.c')
+endif
+
+wayland = {
+ 'deps': [dependency('wayland-client', version: '>= 1.20.0', required: get_option('wayland')),
+ dependency('wayland-cursor', version: '>= 1.20.0', required: get_option('wayland')),
+ dependency('wayland-protocols', version: '>= 1.25', required: get_option('wayland')),
+ dependency('xkbcommon', version: '>= 0.3.0', required: get_option('wayland'))],
+ 'header': cc.has_header('linux/input-event-codes.h', required: get_option('wayland'),
+ # Pass CFLAGS from a related package as a hint for non-Linux
+ dependencies: dependency('wayland-client', required: get_option('wayland'))),
+ 'scanner': find_program('wayland-scanner', required: get_option('wayland')),
+}
+wayland_deps = true
+foreach dep: wayland['deps']
+ if not dep.found()
+ wayland_deps = false
+ break
+ endif
+endforeach
+features += {'wayland': wayland_deps and wayland['header'] and wayland['scanner'].found()}
+
+if features['wayland']
+ subdir(join_paths('video', 'out'))
+endif
+
+features += {'memfd-create': false}
+if features['wayland']
+ features += {'memfd-create': cc.has_function('memfd_create',
+ prefix: '#define _GNU_SOURCE\n#include <sys/mman.h>')}
+endif
+if features['wayland'] and features['memfd-create']
+ sources += files('video/out/vo_wlshm.c')
+endif
+
+dmabuf_wayland = get_option('dmabuf-wayland').require(
+ features['drm'] and features['memfd-create'] and features['wayland'],
+ error_message: 'drm, memfd-create or wayland was not found!',
+)
+features += {'dmabuf-wayland': dmabuf_wayland.allowed()}
+if features['dmabuf-wayland']
+ sources += files('video/out/vo_dmabuf_wayland.c')
+ sources += files('video/out/hwdec/dmabuf_interop_wl.c')
+ sources += files('video/out/wldmabuf/context_wldmabuf.c')
+ sources += files('video/out/wldmabuf/ra_wldmabuf.c')
+endif
+
+x11_opt = get_option('x11').require(
+ get_option('gpl'),
+ error_message: 'the build is not GPL!',
+)
+x11 = {
+ 'deps': [dependency('x11', version: '>= 1.0.0', required: x11_opt),
+ dependency('xscrnsaver', version: '>= 1.0.0', required: x11_opt),
+ dependency('xext', version: '>= 1.0.0', required: x11_opt),
+ dependency('xpresent', version: '>= 1.0.0', required: x11_opt),
+ dependency('xrandr', version: '>= 1.4.0', required: x11_opt)],
+}
+x11_deps = true
+foreach dep: x11['deps']
+ if not dep.found()
+ x11_deps = false
+ break
+ endif
+endforeach
+features += {'x11': x11_deps}
+
+if features['x11']
+ dependencies += x11['deps']
+ sources += files('video/out/vo_x11.c',
+ 'video/out/x11_common.c')
+endif
+
+xv_opt = get_option('xv').require(
+ features['x11'],
+ error_message: 'x11 could not be found!',
+)
+xv = dependency('xv', required: xv_opt)
+features += {'xv': xv.found()}
+if features['xv']
+ dependencies += xv
+ sources += files('video/out/vo_xv.c')
+endif
+
+if features['wayland'] or features['x11']
+ sources += ('video/out/present_sync.c')
+endif
+
+
+# OpenGL feature checking
+gl_allowed = get_option('gl').allowed()
+features += {'gl': false}
+
+GL = dependency('', required: false)
+if darwin
+ GL = dependency('appleframeworks', modules: 'OpenGL', required: get_option('gl-cocoa'))
+elif features['win32-desktop']
+ GL = dependency('GL', required: get_option('gl-win32'))
+elif features['x11']
+ GL = dependency('GL', required: get_option('gl-x11'))
+endif
+
+gl_cocoa = get_option('gl-cocoa').require(
+ features['cocoa'] and GL.found() and gl_allowed,
+ error_message: 'cocoa and GL were not found!',
+)
+features += {'gl-cocoa': gl_cocoa.allowed()}
+if features['gl-cocoa']
+ dependencies += GL
+ features += {'gl': true}
+endif
+
+gl_win32 = get_option('gl-win32').require(
+ GL.found() and gl_allowed and features['win32-desktop'],
+ error_message: 'GL and win32 desktop were not found!',
+)
+features += {'gl-win32': gl_win32.allowed()}
+if features['gl-win32']
+ dependencies += GL
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_win.c')
+endif
+
+gl_x11 = get_option('gl-x11').require(
+ GL.found() and gl_allowed and features['x11'],
+ error_message: 'GL and x11 were not found!',
+)
+features += {'gl-x11': gl_x11.allowed()}
+if features['gl-x11']
+ dependencies += GL
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_glx.c')
+endif
+
+gl_dxinterop_d3d = gl_win32.allowed() and \
+ cc.has_header_symbol('GL/wglext.h', 'WGL_ACCESS_READ_ONLY_NV',
+ prefix: '#include <GL/gl.h>')
+gl_dxinterop_gl = features['gl-win32'] and cc.has_header_symbol('d3d9.h', 'IDirect3D9Ex')
+gl_dxinterop = get_option('gl-dxinterop').require(
+ gl_dxinterop_d3d and gl_dxinterop_gl and gl_win32.allowed(),
+ error_message: 'gl-dxinterop could not be found!',
+)
+features += {'gl-dxinterop': gl_dxinterop.allowed()}
+if features['gl-dxinterop']
+ sources += files('video/out/opengl/context_dxinterop.c')
+endif
+
+egl_angle = get_option('egl-angle').require(
+ features['gl-win32'] and
+ cc.has_header_symbol('EGL/eglext.h',
+ 'EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE',
+ prefix: '#include <EGL/egl.h>') and
+ cc.has_header_symbol('EGL/eglext_angle.h',
+ 'PFNEGLCREATEDEVICEANGLEPROC',
+ # TODO: change to list when meson 1.0.0 is required
+ prefix: '#include <EGL/egl.h>\n#include <EGL/eglext.h>'),
+ error_message: 'egl-angle could not be found!',
+)
+features += {'egl-angle': egl_angle.allowed()}
+if features['egl-angle']
+ sources += files('video/out/opengl/angle_dynamic.c')
+endif
+
+egl_dep = cc.find_library('EGL', required: get_option('egl-angle-lib'))
+egl_angle_lib = get_option('egl-angle-lib').require(
+ features['egl-angle'] and cc.has_function('eglCreateWindowSurface',
+ dependencies: egl_dep,
+ prefix: '#include <EGL/egl.h>'),
+ error_message: 'egl-angle-lib could not be found!',
+)
+features += {'egl-angle-lib': egl_angle_lib.allowed()}
+if features['egl-angle-lib']
+ dependencies += egl_dep
+endif
+
+egl_angle_win32 = get_option('egl-angle-win32').require(
+ features['egl-angle'] and features['win32-desktop'],
+ error_message: 'either this is not a win32 desktop or egl-angle was not found!',
+)
+features += {'egl-angle-win32': egl_angle_win32.allowed()}
+if features['egl-angle-win32']
+ sources += files('video/out/opengl/context_angle.c')
+endif
+
+if features['d3d11'] or features['egl-angle-win32']
+ sources += files('video/out/gpu/d3d11_helpers.c')
+endif
+
+egl = dependency('egl', version: '> 1.4.0', required: get_option('egl'))
+features += {'egl': egl.found() and gl_allowed}
+if features['egl']
+ dependencies += egl
+endif
+
+egl_android_opt = get_option('egl-android').require(
+ features['android'] and gl_allowed,
+ error_message: 'the OS is not android!',
+)
+egl_android = cc.find_library('EGL', required: egl_android_opt)
+features += {'egl-android': egl_android.found()}
+if features['egl-android']
+ dependencies += egl_android
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_android.c')
+endif
+
+egl_drm = get_option('egl-drm').require(
+ features['drm'] and features['egl'] and gbm.found() and gl_allowed,
+ error_message: 'either drm, egl, or gbm could not be found!',
+)
+features += {'egl-drm': egl_drm.allowed()}
+if features['egl-drm']
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_drm_egl.c')
+endif
+
+egl_wayland = dependency('wayland-egl', version: '>= 9.0.0', required: get_option('egl-wayland'))
+features += {'egl-wayland': features['egl'] and egl_wayland.found() and gl_allowed and features['wayland']}
+if features['egl-wayland']
+ dependencies += egl_wayland
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_wayland.c')
+endif
+
+egl_x11 = get_option('egl-x11').require(
+ features['egl'] and gl_allowed and features['x11'],
+ error_message: 'either egl or x11 could not be found!',
+)
+features += {'egl-x11': egl_x11.allowed()}
+if features['egl-x11']
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_x11egl.c')
+endif
+
+plain_gl = get_option('plain-gl').require(
+ gl_allowed,
+ error_message: 'gl was not enabled!',
+)
+if plain_gl.allowed()
+ features += {'gl': true}
+endif
+
+rpi = dependency('/opt/vc/lib/pkgconfig/brcmegl.pc', 'brcmegl', required: get_option('rpi'))
+features += {'rpi': gl_allowed and rpi.found()}
+if features['rpi']
+ dependencies += rpi
+ features += {'gl': true}
+ sources += files('video/out/opengl/context_rpi.c')
+endif
+
+features += {'egl-helpers': features['egl'] or egl_android.found() or
+ egl_angle_win32.allowed() or features['rpi']}
+if features['egl-helpers']
+ sources += files('video/out/opengl/egl_helpers.c')
+endif
+
+if features['egl'] and features['egl-helpers']
+ sources += files('video/filter/vf_gpu.c')
+endif
+
+if features['gl']
+ sources += files('video/out/opengl/common.c',
+ 'video/out/opengl/context.c',
+ 'video/out/opengl/formats.c',
+ 'video/out/opengl/libmpv_gl.c',
+ 'video/out/opengl/ra_gl.c',
+ 'video/out/opengl/utils.c')
+elif not features['gl'] and get_option('gl').enabled()
+ error('gl enabled but no OpenGL video output could be found!')
+endif
+
+
+# vulkan
+vulkan_opt = get_option('vulkan').require(
+ libplacebo.get_variable('pl_has_vulkan', default_value: '0') == '1',
+ error_message: 'libplacebo compiled without vulkan support!',
+)
+vulkan = dependency('vulkan', version: '>= 1.1.70', required: vulkan_opt)
+features += {'vulkan': vulkan.found()}
+if features['vulkan']
+ dependencies += vulkan
+ sources += files('video/out/vulkan/context.c',
+ 'video/out/vulkan/utils.c')
+endif
+
+if features['vulkan'] and features['android']
+ sources += files('video/out/vulkan/context_android.c')
+endif
+
+if features['vulkan'] and features['wayland']
+ sources += files('video/out/vulkan/context_wayland.c')
+endif
+
+if features['vulkan'] and features['win32-desktop']
+ sources += files('video/out/vulkan/context_win.c')
+endif
+
+if features['vulkan'] and features['x11']
+ sources += files('video/out/vulkan/context_xlib.c')
+endif
+
+features += {'vk-khr-display': cc.has_function('vkCreateDisplayPlaneSurfaceKHR', prefix: '#include <vulkan/vulkan_core.h>',
+ dependencies: [vulkan])}
+if features['vk-khr-display']
+ sources += files('video/out/vulkan/context_display.c')
+endif
+
+
+# hwaccel
+ffnvcodec = dependency('ffnvcodec', version: '>= 11.1.5.1', required: false)
+features += {'ffnvcodec': ffnvcodec.found()}
+if features['ffnvcodec']
+ dependencies += ffnvcodec
+ sources += files('video/cuda.c')
+endif
+
+android_media_ndk = get_option('android-media-ndk').require(
+ features['android'] and cc.has_header_symbol('media/NdkImageReader.h', 'AIMAGE_FORMAT_PRIVATE')
+)
+features += {'android-media-ndk': android_media_ndk.allowed()}
+if features['android-media-ndk']
+ # header only, library is dynamically loaded
+ sources += files('video/out/hwdec/hwdec_aimagereader.c')
+endif
+
+cuda_hwaccel = get_option('cuda-hwaccel').require(
+ features['ffnvcodec'],
+ error_message: 'ffnvcodec was not found!',
+)
+features += {'cuda-hwaccel': cuda_hwaccel.allowed()}
+if features['cuda-hwaccel']
+ sources += files('video/out/hwdec/hwdec_cuda.c')
+endif
+
+cuda_interop = get_option('cuda-interop').require(
+ features['cuda-hwaccel'] and (features['gl'] or features['vulkan']),
+ error_message: 'cuda-hwaccel and either gl or vulkan were not found!',
+)
+features += {'cuda-interop': cuda_interop.allowed() and (features['gl'] or features['vulkan'])}
+if features['cuda-interop'] and features['gl']
+ sources += files('video/out/hwdec/hwdec_cuda_gl.c')
+endif
+if features['cuda-interop'] and features['vulkan']
+ sources += files('video/out/hwdec/hwdec_cuda_vk.c')
+endif
+
+vulkan_interop = get_option('vulkan-interop').require(
+ features['vulkan'] and vulkan.version().version_compare('>=1.3.238') and
+ libavutil.version().version_compare('>=58.11.100'),
+ error_message: 'Vulkan Interop requires vulkan headers >= 1.3.238, and libavutil >= 58.11.100',
+)
+features += {'vulkan-interop': vulkan_interop.allowed()}
+if vulkan_interop.allowed()
+ sources += files('video/out/hwdec/hwdec_vulkan.c')
+endif
+
+d3d_hwaccel = get_option('d3d-hwaccel').require(
+ win32,
+ error_message: 'the os is not win32!',
+)
+features += {'d3d-hwaccel': d3d_hwaccel.allowed()}
+if features['d3d-hwaccel']
+ sources += files('video/d3d.c',
+ 'video/filter/vf_d3d11vpp.c')
+endif
+
+if features['d3d-hwaccel'] and egl_angle.allowed()
+ sources += files('video/out/opengl/hwdec_d3d11egl.c')
+endif
+if features['d3d-hwaccel'] and features['d3d11']
+ sources += files('video/out/d3d11/hwdec_d3d11va.c')
+endif
+
+d3d9_hwaccel = get_option('d3d9-hwaccel').require(
+ features['d3d-hwaccel'],
+ error_message: 'd3d-hwaccel was not found!',
+)
+features += {'d3d9-hwaccel': d3d9_hwaccel.allowed()}
+if features['d3d9-hwaccel'] and features['egl-angle']
+ sources += files('video/out/opengl/hwdec_dxva2egl.c')
+endif
+if features['d3d9-hwaccel'] and features['d3d11']
+ sources += files('video/out/d3d11/hwdec_dxva2dxgi.c')
+endif
+
+gl_dxinterop_d3d9 = get_option('gl-dxinterop-d3d9').require(
+ features['gl-dxinterop'] and features['d3d9-hwaccel'],
+ error_message: 'gl-dxinterop and d3d9-hwaccel were not found!',
+)
+features += {'gl-dxinterop-d3d9': gl_dxinterop_d3d9.allowed()}
+if features['gl-dxinterop-d3d9']
+ sources += files('video/out/opengl/hwdec_dxva2gldx.c')
+endif
+
+ios_gl = cc.has_header_symbol('OpenGLES/ES3/glext.h', 'GL_RGB32F', required: get_option('ios-gl'))
+features += {'ios-gl': ios_gl}
+if features['ios-gl']
+ sources += files('video/out/hwdec/hwdec_ios_gl.m')
+endif
+
+rpi_mmal_opt = get_option('rpi-mmal').require(
+ features['rpi'],
+ error_message: 'rpi was not found!',
+)
+rpi_mmal = dependency('/opt/vc/lib/pkgconfig/mmal.pc', 'mmal', required: rpi_mmal_opt)
+features += {'rpi-mmal': rpi_mmal.found()}
+if features['rpi-mmal']
+ dependencies += rpi_mmal
+ sources += files('video/out/opengl/hwdec_rpi.c',
+ 'video/out/vo_rpi.c')
+endif
+
+libva = dependency('libva', version: '>= 1.1.0', required: get_option('vaapi'))
+
+vaapi_drm = dependency('libva-drm', version: '>= 1.1.0',
+ required: get_option('vaapi-drm').require(libva.found() and features['drm']))
+features += {'vaapi-drm': vaapi_drm.found()}
+if features['vaapi-drm']
+ dependencies += vaapi_drm
+endif
+
+vaapi_wayland = dependency('libva-wayland', version: '>= 1.1.0',
+ required: get_option('vaapi-wayland').require(libva.found() and features['wayland']))
+features += {'vaapi-wayland': vaapi_wayland.found()}
+if features['vaapi-wayland']
+ dependencies += vaapi_wayland
+endif
+
+vaapi_x11 = dependency('libva-x11', version: '>= 1.1.0',
+ required: get_option('vaapi-x11').require(libva.found() and features['x11']))
+features += {'vaapi-x11': vaapi_x11.found()}
+if features['vaapi-x11']
+ dependencies += vaapi_x11
+ sources += files('video/out/vo_vaapi.c')
+endif
+
+vaapi = get_option('vaapi').require(libva.found() and (features['vaapi-drm'] or
+ features['vaapi-wayland'] or features['vaapi-x11']))
+features += {'vaapi': vaapi.allowed()}
+
+if features['vaapi']
+ dependencies += libva
+ sources += files('video/filter/vf_vavpp.c',
+ 'video/vaapi.c',
+ 'video/out/hwdec/hwdec_vaapi.c',
+ 'video/out/hwdec/dmabuf_interop_pl.c')
+endif
+
+dmabuf_interop_gl = features['egl'] and features['drm']
+features += {'dmabuf-interop-gl': dmabuf_interop_gl}
+if features['dmabuf-interop-gl']
+ sources += files('video/out/hwdec/dmabuf_interop_gl.c')
+endif
+
+vdpau_opt = get_option('vdpau').require(
+ features['x11'],
+ error_message: 'x11 was not found!',
+)
+vdpau = dependency('vdpau', version: '>= 0.2', required: vdpau_opt)
+features += {'vdpau': vdpau.found()}
+if features['vdpau']
+ dependencies += vdpau
+ sources += files('video/filter/vf_vdpaupp.c',
+ 'video/out/vo_vdpau.c',
+ 'video/vdpau.c',
+ 'video/vdpau_mixer.c')
+endif
+
+features += {'vdpau-gl-x11': vdpau.found() and gl_x11.allowed()}
+if features['vdpau'] and features['vdpau-gl-x11']
+ sources += files('video/out/opengl/hwdec_vdpau.c')
+endif
+
+videotoolbox_gl = get_option('videotoolbox-gl').require(
+ features['gl-cocoa'] or features['ios-gl'],
+ error_message: 'gl-cocoa nor ios-gl could be found!',
+)
+features += {'videotoolbox-gl': videotoolbox_gl.allowed()}
+corevideo = dependency('appleframeworks', modules: ['CoreVideo'], required: get_option('videotoolbox-pl'))
+videotoolbox_pl = get_option('videotoolbox-pl').require(
+ features['vulkan'] and corevideo.found(),
+ error_message: 'vulkan or CV metal support could be found!',
+)
+features += {'videotoolbox-pl': videotoolbox_pl.allowed()}
+if features['videotoolbox-gl'] or features['videotoolbox-pl']
+ sources += files('video/out/hwdec/hwdec_vt.c')
+endif
+if features['videotoolbox-gl']
+ sources += files('video/out/hwdec/hwdec_mac_gl.c')
+endif
+if features['videotoolbox-pl']
+ dependencies += corevideo
+ sources += files('video/out/hwdec/hwdec_vt_pl.m')
+endif
+
+
+# macOS features
+macos_sdk_version_py = ''
+if darwin
+ macos_sdk_version_py = find_program(join_paths(source_root, 'TOOLS', 'macos-sdk-version.py'),
+ required: true)
+endif
+
+macos_sdk_path = ''
+macos_sdk_version = '0.0'
+if darwin and macos_sdk_version_py.found()
+ macos_sdk_info = run_command(macos_sdk_version_py, check: true).stdout().split(',')
+ macos_sdk_path = macos_sdk_info[0]
+ macos_sdk_version = macos_sdk_info[1]
+endif
+
+if macos_sdk_path != ''
+ message('Detected macOS sdk path: ' + macos_sdk_path)
+endif
+
+if macos_sdk_version != '0.0'
+ message('Detected macOS SDK: ' + macos_sdk_version)
+ add_languages('objc')
+ objc_link_flags = ['-isysroot', macos_sdk_path, '-L/usr/lib', '-L/usr/local/lib']
+ add_project_link_arguments(objc_link_flags, language: ['c', 'objc'])
+endif
+
+xcrun = find_program('xcrun', required: get_option('swift-build').require(darwin))
+swift_ver = '0.0'
+if xcrun.found()
+ swift_prog = find_program(run_command(xcrun, '-find', 'swift', check: true).stdout().strip())
+ swift_ver_string = run_command(swift_prog, '-version', check: true).stdout()
+ verRe = '''
+#!/usr/bin/env python3
+import re
+import sys
+verRe = re.compile("(?i)version\s?([\d.]+)")
+swift_ver = verRe.search(sys.argv[1]).group(1)
+sys.stdout.write(swift_ver)
+'''
+ swift_ver = run_command(python, '-c', verRe, swift_ver_string, check: true).stdout()
+ message('Detected Swift version: ' + swift_ver)
+endif
+
+swift = get_option('swift-build').require(
+ darwin and macos_sdk_version.version_compare('>= 10.15') and swift_ver.version_compare('>= 4.1'),
+ error_message: 'A suitable macos sdk version or swift version could not be found!',
+)
+features += {'swift': swift.allowed()}
+
+swift_sources = []
+if features['cocoa'] and features['swift']
+ swift_sources += files('osdep/macos/libmpv_helper.swift',
+ 'osdep/macos/log_helper.swift',
+ 'osdep/macos/mpv_helper.swift',
+ 'osdep/macos/precise_timer.swift',
+ 'osdep/macos/swift_compat.swift',
+ 'osdep/macos/swift_extensions.swift',
+ 'video/out/mac/common.swift',
+ 'video/out/mac/title_bar.swift',
+ 'video/out/mac/view.swift',
+ 'video/out/mac/window.swift')
+endif
+
+macos_cocoa_cb = get_option('macos-cocoa-cb').require(
+ features['cocoa'] and features['gl-cocoa'] and features['swift'],
+ error_message: 'Either cocoa, gl-cocoa or swift could not be found!',
+)
+features += {'macos-cocoa-cb': macos_cocoa_cb.allowed()}
+if features['macos-cocoa-cb']
+ swift_sources += files('video/out/cocoa_cb_common.swift',
+ 'video/out/mac/gl_layer.swift')
+endif
+if features['cocoa'] and features['vulkan'] and features['swift']
+ swift_sources += files('video/out/mac_common.swift',
+ 'video/out/mac/metal_layer.swift')
+ sources += files('video/out/vulkan/context_mac.m')
+endif
+
+macos_media_player = get_option('macos-media-player').require(
+ features['swift'],
+ error_message: 'Swift was not found!',
+)
+features += {'macos-media-player': macos_media_player.allowed()}
+if features['macos-media-player']
+ swift_sources += files('osdep/macos/remote_command_center.swift')
+endif
+
+if features['swift'] and swift_sources.length() > 0
+ subdir('osdep')
+endif
+
+macos_touchbar = get_option('macos-touchbar').require(
+ features['cocoa'] and cc.has_header('AppKit/NSTouchBar.h'),
+ error_message: 'Either cocoa could not be found or AppKit/NSTouchBar.h could not be found!',
+)
+features += {'macos-touchbar': macos_touchbar.allowed()}
+if features['macos-touchbar']
+ sources += files('osdep/macosx_touchbar.m')
+endif
+
+
+# manpages
+manpage = 'DOCS/man/mpv.rst'
+rst2man = find_program('rst2man', 'rst2man.py', required: get_option('manpage-build'))
+features += {'manpage-build': rst2man.found()}
+if features['manpage-build']
+ mandir = get_option('mandir')
+ custom_target('manpages',
+ input: manpage,
+ output: 'mpv.1',
+ command: [
+ docutils_wrapper, rst2man,
+ '--record-dependencies', '@DEPFILE@',
+ '--strip-elements-with-class=contents',
+ '@INPUT@', '@OUTPUT@'],
+ depfile: 'mpv.1.dep',
+ install: true,
+ install_dir: join_paths(mandir, 'man1')
+ )
+endif
+
+rst2html = find_program('rst2html', 'rst2html.py', required: get_option('html-build'))
+features += {'html-build': rst2html.found()}
+if features['html-build']
+ datadir = get_option('datadir')
+ custom_target('html-manpages',
+ input: manpage,
+ output: 'mpv.html',
+ command: [
+ docutils_wrapper, rst2html,
+ '--record-dependencies', '@DEPFILE@',
+ '@INPUT@', '@OUTPUT@'],
+ depfile: 'mpv.html.dep',
+ install: true,
+ install_dir: join_paths(datadir, 'doc', 'mpv')
+ )
+endif
+
+rst2pdf = find_program('rst2pdf', required: get_option('pdf-build'))
+features += {'pdf-build': rst2pdf.found()}
+if features['pdf-build']
+ dependency_file = rst2pdf.version().version_compare('>=0.100')
+ datadir = get_option('datadir')
+ custom_target('pdf-manpages',
+ input: manpage,
+ output: 'mpv.pdf',
+ command: [
+ docutils_wrapper, rst2pdf,
+ '-c', '-b', '1', '--repeat-table-rows',
+ dependency_file ? ['--record-dependencies', '@DEPFILE@'] : [],
+ '@INPUT@', '-o', '@OUTPUT@'],
+ depfile: 'mpv.pdf.dep',
+ install: true,
+ install_dir: join_paths(datadir, 'doc', 'mpv')
+ )
+endif
+
+
+if meson.version().version_compare('>= 1.1.0')
+ configuration = meson.build_options()
+else
+ # Arbitrary hardcoded things to pass if the meson version is too
+ # old to have the build_options method.
+ configuration = 'meson configure build ' + '-Dprefix=' + get_option('prefix') + \
+ ' -Dbuildtype=' + get_option('buildtype') + \
+ ' -Doptimization=' + get_option('optimization')
+endif
+
+
+# Set config.h
+conf_data = configuration_data()
+conf_data.set_quoted('CONFIGURATION', configuration)
+conf_data.set_quoted('DEFAULT_DVD_DEVICE', dvd_device)
+conf_data.set_quoted('DEFAULT_CDROM_DEVICE', cd_device)
+
+# Loop over all features in the build, create a define and add them to config.h
+feature_keys = []
+foreach feature, allowed: features
+ define = 'HAVE_@0@'.format(feature.underscorify().to_upper())
+ conf_data.set10(define, allowed)
+ # special handling for lua
+ if feature == 'lua' and allowed
+ feature_keys += lua_version
+ continue
+ endif
+ if allowed
+ feature_keys += feature
+ endif
+endforeach
+
+
+# Script to sort the feature_keys object.
+feature_sort = '''
+#!/usr/bin/env python3
+import sys
+features = " ".join(sorted(sys.argv[1:]))
+sys.stdout.write(features)
+'''
+feature_str = run_command(python, '-c', feature_sort, feature_keys, check: true).stdout()
+conf_data.set_quoted('FULLCONFIG', feature_str)
+conf_data.set_quoted('MPV_CONFDIR', join_paths(get_option('prefix'), get_option('sysconfdir'), 'mpv'))
+conf_data.set_quoted('PLATFORM', host_machine.system())
+configure_file(output : 'config.h', configuration : conf_data)
+message('List of enabled features: ' + feature_str)
+
+# These are intentionally not added to conf_data.
+features += {'cplayer': get_option('cplayer')}
+features += {'libmpv-' + get_option('default_library'): get_option('libmpv')}
+
+
+# build targets
+if win32
+ windows = import('windows')
+ res_flags = ['--codepage=65001']
+
+ # Unintuitively, this compile operates out of the osdep subdirectory.
+ # Hence, these includes are needed.
+ res_includes = [source_root, build_root]
+
+ resources = ['etc/mpv-icon-8bit-16x16.png',
+ 'etc/mpv-icon-8bit-32x32.png',
+ 'etc/mpv-icon-8bit-64x64.png',
+ 'etc/mpv-icon-8bit-128x128.png',
+ 'etc/mpv-icon.ico',
+ 'osdep/mpv.exe.manifest']
+
+ sources += windows.compile_resources('osdep/mpv.rc', args: res_flags, depend_files: resources,
+ depends: version_h, include_directories: res_includes)
+endif
+
+
+client_h_define = cc.get_define('MPV_CLIENT_API_VERSION', prefix: '#include "libmpv/client.h"',
+ include_directories: include_directories('.'))
+major = client_h_define.split('|')[0].split('<<')[0].strip('() ')
+minor = client_h_define.split('|')[1].strip('() ')
+client_api_version = major + '.' + minor + '.0'
+
+libmpv = library('mpv', sources, dependencies: dependencies, gnu_symbol_visibility: 'hidden',
+ link_args: cc.get_supported_link_arguments(['-Wl,-Bsymbolic']),
+ version: client_api_version, install: get_option('libmpv'),
+ build_by_default: get_option('libmpv'))
+
+
+if get_option('libmpv')
+ pkg = import('pkgconfig')
+ pkg.generate(libmpv, version: client_api_version,
+ description: 'mpv media player client library')
+
+ headers = ['libmpv/client.h', 'libmpv/render.h',
+ 'libmpv/render_gl.h', 'libmpv/stream_cb.h']
+ install_headers(headers, subdir: 'mpv')
+
+ # Allow projects to build with libmpv by cloning into ./subprojects/mpv
+ libmpv_dep = declare_dependency(link_with: libmpv)
+ meson.override_dependency('mpv', libmpv_dep)
+endif
+
+if get_option('cplayer')
+ datadir = get_option('datadir')
+ confdir = get_option('sysconfdir')
+
+ conf_files = ['etc/mpv.conf', 'etc/input.conf',
+ 'etc/mplayer-input.conf', 'etc/restore-old-bindings.conf']
+ install_data(conf_files, install_dir: join_paths(datadir, 'doc', 'mpv'))
+
+ bash_install_dir = join_paths(datadir, 'bash-completion', 'completions')
+ install_data('etc/mpv.bash-completion', install_dir: bash_install_dir, rename: 'mpv')
+
+ zsh_install_dir = join_paths(datadir, 'zsh', 'site-functions')
+ install_data('etc/_mpv.zsh', install_dir: zsh_install_dir, rename: '_mpv')
+
+ install_data('etc/mpv.desktop', install_dir: join_paths(datadir, 'applications'))
+ install_data('etc/mpv.metainfo.xml', install_dir: join_paths(datadir, 'metainfo'))
+ install_data('etc/encoding-profiles.conf', install_dir: join_paths(confdir, 'mpv'))
+
+ foreach size: ['16x16', '32x32', '64x64', '128x128']
+ icon_dir = join_paths(datadir, 'icons', 'hicolor', size, 'apps')
+ install_data('etc/mpv-icon-8bit-' + size + '.png', install_dir: icon_dir, rename: 'mpv.png')
+ endforeach
+
+ hicolor_dir = join_paths(datadir, 'icons', 'hicolor')
+ install_data('etc/mpv-gradient.svg', install_dir: join_paths(hicolor_dir, 'scalable', 'apps'),
+ rename: 'mpv.svg')
+ install_data('etc/mpv-symbolic.svg', install_dir: join_paths(hicolor_dir, 'symbolic', 'apps'))
+
+ mpv = executable('mpv', objects: libmpv.extract_all_objects(recursive: true), dependencies: dependencies,
+ win_subsystem: 'windows,6.0', install: true)
+endif
+
+if get_option('tests')
+ subdir('test')
+endif
+
+summary({'d3d11': features['d3d11'],
+ 'javascript': features['javascript'],
+ 'libmpv': get_option('libmpv'),
+ 'lua': features['lua'],
+ 'opengl': features['gl'],
+ 'vulkan': features['vulkan'],
+ 'wayland': features['wayland'],
+ 'x11': features['x11']},
+ bool_yn: true)
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..b0edb80
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,117 @@
+# booleans
+option('gpl', type: 'boolean', value: true, description: 'GPL (version 2 or later) build')
+option('cplayer', type: 'boolean', value: true, description: 'mpv CLI player')
+option('libmpv', type: 'boolean', value: false, description: 'libmpv library')
+option('build-date', type: 'boolean', value: true, description: 'whether to include binary compile time')
+option('tests', type: 'boolean', value: false, description: 'meson unit tests')
+# Reminder: normally always built, but enabled by MPV_LEAK_REPORT.
+# Building it can be disabled only by defining NDEBUG through CFLAGS.
+option('ta-leak-report', type: 'boolean', value: false, description: 'enable ta leak report by default (development only)')
+
+# misc features
+option('cdda', type: 'feature', value: 'disabled', description: 'cdda support (libcdio)')
+option('cplugins', type: 'feature', value: 'auto', description: 'C plugins')
+option('dvbin', type: 'feature', value: 'disabled', description: 'DVB input module')
+option('dvdnav', type: 'feature', value: 'disabled', description: 'dvdnav support')
+option('iconv', type: 'feature', value: 'auto', description: 'iconv')
+option('javascript', type: 'feature', value: 'auto', description: 'Javascript (MuJS backend)')
+option('lcms2', type: 'feature', value: 'auto', description: 'LCMS2 support')
+option('libarchive', type: 'feature', value: 'auto', description: 'libarchive wrapper for reading zip files and more')
+option('libavdevice', type: 'feature', value: 'auto', description: 'libavdevice')
+option('libbluray', type: 'feature', value: 'auto', description: 'Bluray support')
+option('lua',
+ type: 'combo',
+ choices: ['lua', 'lua52', 'lua5.2', 'lua-5.2', 'luajit', 'lua51',
+ 'lua5.1', 'lua-5.1', 'auto', 'enabled', 'disabled'],
+ value: 'auto',
+ description: 'Lua'
+)
+option('pthread-debug', type: 'feature', value: 'disabled', description: 'pthread runtime debugging wrappers')
+option('rubberband', type: 'feature', value: 'auto', description: 'librubberband support')
+option('sdl2', type: 'feature', value: 'disabled', description: 'SDL2')
+option('sdl2-gamepad', type: 'feature', value: 'auto', description: 'SDL2 gamepad input')
+option('uchardet', type: 'feature', value: 'auto', description: 'uchardet support')
+option('uwp', type: 'feature', value: 'disabled', description: 'Universal Windows Platform')
+option('vapoursynth', type: 'feature', value: 'auto', description: 'VapourSynth filter bridge')
+option('vector', type: 'feature', value: 'auto', description: 'GCC vector instructions')
+option('win32-threads', type: 'feature', value: 'auto', description: 'win32 threads')
+option('zimg', type: 'feature', value: 'auto', description: 'libzimg support (high quality software scaler)')
+option('zlib', type: 'feature', value: 'auto', description: 'zlib')
+
+# audio output features
+option('alsa', type: 'feature', value: 'auto', description: 'ALSA audio output')
+option('audiounit', type: 'feature', value: 'auto', description: 'AudioUnit output for iOS')
+option('coreaudio', type: 'feature', value: 'auto', description: 'CoreAudio audio output')
+option('jack', type: 'feature', value: 'auto', description: 'JACK audio output')
+option('openal', type: 'feature', value: 'disabled', description: 'OpenAL audio output')
+option('opensles', type: 'feature', value: 'auto', description: 'OpenSL ES audio output')
+option('oss-audio', type: 'feature', value: 'auto', description: 'OSSv4 audio output')
+option('pipewire', type: 'feature', value: 'auto', description: 'PipeWire audio output')
+option('pulse', type: 'feature', value: 'auto', description: 'PulseAudio audio output')
+option('sdl2-audio', type: 'feature', value: 'auto', description: 'SDL2 audio output')
+option('sndio', type: 'feature', value: 'auto', description: 'sndio audio output')
+option('wasapi', type: 'feature', value: 'auto', description: 'WASAPI audio output')
+
+# video output features
+option('caca', type: 'feature', value: 'auto', description: 'CACA')
+option('cocoa', type: 'feature', value: 'auto', description: 'Cocoa')
+option('d3d11', type: 'feature', value: 'auto', description: 'Direct3D 11 video output')
+option('direct3d', type: 'feature', value: 'auto', description: 'Direct3D support')
+option('dmabuf-wayland', type: 'feature', value: 'auto', description: 'dmabuf-wayland video output')
+option('drm', type: 'feature', value: 'auto', description: 'DRM')
+option('egl', type: 'feature', value: 'auto', description: 'EGL 1.4')
+option('egl-android', type: 'feature', value: 'auto', description: 'Android EGL support')
+option('egl-angle', type: 'feature', value: 'auto', description: 'OpenGL ANGLE headers')
+option('egl-angle-lib', type: 'feature', value: 'auto', description: 'OpenGL Win32 ANGLE library')
+option('egl-angle-win32', type: 'feature', value: 'auto', description: 'OpenGL Win32 ANGLE Backend')
+option('egl-drm', type: 'feature', value: 'auto', description: 'OpenGL DRM EGL Backend')
+option('egl-wayland', type: 'feature', value: 'auto', description: 'OpenGL Wayland Backend')
+option('egl-x11', type: 'feature', value: 'auto', description: 'OpenGL X11 EGL Backend')
+option('gbm', type: 'feature', value: 'auto', description: 'GBM')
+option('gl', type: 'feature', value: 'enabled', description: 'OpenGL context support')
+option('gl-cocoa', type: 'feature', value: 'auto', description: 'gl-cocoa')
+option('gl-dxinterop', type: 'feature', value: 'auto', description: 'OpenGL/DirectX Interop Backend')
+option('gl-win32', type: 'feature', value: 'auto', description: 'OpenGL Win32 Backend')
+option('gl-x11', type: 'feature', value: 'disabled', description: 'OpenGL X11/GLX (deprecated/legacy)')
+option('jpeg', type: 'feature', value: 'auto', description: 'JPEG support')
+option('rpi', type: 'feature', value: 'disabled', description: 'Raspberry Pi support')
+option('sdl2-video', type: 'feature', value: 'auto', description: 'SDL2 video output')
+option('shaderc', type: 'feature', value: 'auto', description: 'libshaderc SPIR-V compiler')
+option('sixel', type: 'feature', value:'auto', description: 'Sixel')
+option('spirv-cross', type: 'feature', value: 'auto', description: 'SPIRV-Cross SPIR-V shader converter')
+option('plain-gl', type: 'feature', value: 'auto', description: 'OpenGL without platform-specific code (e.g. for libmpv)')
+option('vdpau', type: 'feature', value: 'auto', description: 'VDPAU acceleration')
+option('vdpau-gl-x11', type: 'feature', value: 'auto', description: 'VDPAU with OpenGL/X11')
+option('vaapi', type: 'feature', value: 'auto', description: 'VAAPI acceleration')
+option('vaapi-drm', type: 'feature', value: 'auto', description: 'VAAPI (DRM support)')
+option('vaapi-wayland', type: 'feature', value: 'auto', description: 'VAAPI (Wayland support)')
+option('vaapi-x11', type: 'feature', value: 'auto', description: 'VAAPI (X11 support)')
+option('vulkan', type: 'feature', value: 'auto', description: 'Vulkan context support')
+option('wayland', type: 'feature', value: 'auto', description: 'Wayland')
+option('x11', type: 'feature', value: 'auto', description: 'X11')
+option('xv', type: 'feature', value: 'auto', description: 'Xv video output')
+
+# hwaccel features
+option('android-media-ndk', type: 'feature', value: 'auto', description: 'Android Media APIs')
+option('cuda-hwaccel', type: 'feature', value: 'auto', description: 'CUDA acceleration')
+option('cuda-interop', type: 'feature', value: 'auto', description: 'CUDA with graphics interop')
+option('d3d-hwaccel', type: 'feature', value: 'auto', description: 'D3D11VA hwaccel')
+option('d3d9-hwaccel', type: 'feature', value: 'auto', description: 'DXVA2 hwaccel')
+option('gl-dxinterop-d3d9', type: 'feature', value: 'auto', description: 'OpenGL/DirectX Interop Backend DXVA2 interop')
+option('ios-gl', type: 'feature', value: 'auto', description: 'iOS OpenGL ES hardware decoding interop support')
+option('rpi-mmal', type: 'feature', value: 'auto', description: 'Raspberry Pi MMAL hwaccel')
+option('videotoolbox-gl', type: 'feature', value: 'auto', description: 'Videotoolbox with OpenGL')
+option('videotoolbox-pl', type: 'feature', value: 'auto', description: 'Videotoolbox with libplacebo')
+option('vulkan-interop', type: 'feature', value: 'auto', description: 'Vulkan graphics interop')
+
+# macOS features
+option('macos-cocoa-cb', type: 'feature', value: 'auto', description: 'macOS libmpv backend')
+option('macos-media-player', type: 'feature', value: 'auto', description: 'macOS Media Player support')
+option('macos-touchbar', type: 'feature', value: 'auto', description: 'macOS Touch Bar support')
+option('swift-build', type: 'feature', value: 'auto', description: 'macOS Swift build tools')
+option('swift-flags', type: 'string', description: 'Optional Swift compiler flags')
+
+# manpages
+option('html-build', type: 'feature', value: 'disabled', description: 'html manual generation')
+option('manpage-build', type: 'feature', value: 'auto', description: 'manpage generation')
+option('pdf-build', type: 'feature', value: 'disabled', description: 'pdf manual generation')
diff --git a/misc/bstr.c b/misc/bstr.c
new file mode 100644
index 0000000..4f1e862
--- /dev/null
+++ b/misc/bstr.c
@@ -0,0 +1,469 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "mpv_talloc.h"
+
+#include "common/common.h"
+#include "misc/ctype.h"
+#include "bstr.h"
+
+int bstrcmp(struct bstr str1, struct bstr str2)
+{
+ int ret = 0;
+ if (str1.len && str2.len)
+ ret = memcmp(str1.start, str2.start, MPMIN(str1.len, str2.len));
+
+ if (!ret) {
+ if (str1.len == str2.len)
+ return 0;
+ else if (str1.len > str2.len)
+ return 1;
+ else
+ return -1;
+ }
+ return ret;
+}
+
+int bstrcasecmp(struct bstr str1, struct bstr str2)
+{
+ int ret = 0;
+ if (str1.len && str2.len)
+ ret = strncasecmp(str1.start, str2.start, MPMIN(str1.len, str2.len));
+
+ if (!ret) {
+ if (str1.len == str2.len)
+ return 0;
+ else if (str1.len > str2.len)
+ return 1;
+ else
+ return -1;
+ }
+ return ret;
+}
+
+int bstrchr(struct bstr str, int c)
+{
+ for (int i = 0; i < str.len; i++)
+ if (str.start[i] == c)
+ return i;
+ return -1;
+}
+
+int bstrrchr(struct bstr str, int c)
+{
+ for (int i = str.len - 1; i >= 0; i--)
+ if (str.start[i] == c)
+ return i;
+ return -1;
+}
+
+int bstrcspn(struct bstr str, const char *reject)
+{
+ int i;
+ for (i = 0; i < str.len; i++)
+ if (strchr(reject, str.start[i]))
+ break;
+ return i;
+}
+
+int bstrspn(struct bstr str, const char *accept)
+{
+ int i;
+ for (i = 0; i < str.len; i++)
+ if (!strchr(accept, str.start[i]))
+ break;
+ return i;
+}
+
+int bstr_find(struct bstr haystack, struct bstr needle)
+{
+ for (int i = 0; i < haystack.len; i++)
+ if (bstr_startswith(bstr_splice(haystack, i, haystack.len), needle))
+ return i;
+ return -1;
+}
+
+struct bstr bstr_lstrip(struct bstr str)
+{
+ while (str.len && mp_isspace(*str.start)) {
+ str.start++;
+ str.len--;
+ }
+ return str;
+}
+
+struct bstr bstr_strip(struct bstr str)
+{
+ str = bstr_lstrip(str);
+ while (str.len && mp_isspace(str.start[str.len - 1]))
+ str.len--;
+ return str;
+}
+
+struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest)
+{
+ int start;
+ for (start = 0; start < str.len; start++)
+ if (!strchr(sep, str.start[start]))
+ break;
+ str = bstr_cut(str, start);
+ int end = bstrcspn(str, sep);
+ if (rest) {
+ *rest = bstr_cut(str, end);
+ }
+ return bstr_splice(str, 0, end);
+}
+
+// Unlike with bstr_split(), tok is a string, and not a set of char.
+// If tok is in str, return true, and: concat(out_left, tok, out_right) == str
+// Otherwise, return false, and set out_left==str, out_right==""
+bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right)
+{
+ bstr bsep = bstr0(tok);
+ int pos = bstr_find(str, bsep);
+ if (pos < 0)
+ pos = str.len;
+ *out_left = bstr_splice(str, 0, pos);
+ *out_right = bstr_cut(str, pos + bsep.len);
+ return pos != str.len;
+}
+
+struct bstr bstr_splice(struct bstr str, int start, int end)
+{
+ if (start < 0)
+ start += str.len;
+ if (end < 0)
+ end += str.len;
+ end = MPMIN(end, str.len);
+ start = MPMAX(start, 0);
+ end = MPMAX(end, start);
+ str.start += start;
+ str.len = end - start;
+ return str;
+}
+
+long long bstrtoll(struct bstr str, struct bstr *rest, int base)
+{
+ str = bstr_lstrip(str);
+ char buf[51];
+ int len = MPMIN(str.len, 50);
+ memcpy(buf, str.start, len);
+ buf[len] = 0;
+ char *endptr;
+ long long r = strtoll(buf, &endptr, base);
+ if (rest)
+ *rest = bstr_cut(str, endptr - buf);
+ return r;
+}
+
+double bstrtod(struct bstr str, struct bstr *rest)
+{
+ str = bstr_lstrip(str);
+ char buf[101];
+ int len = MPMIN(str.len, 100);
+ memcpy(buf, str.start, len);
+ buf[len] = 0;
+ char *endptr;
+ double r = strtod(buf, &endptr);
+ if (rest)
+ *rest = bstr_cut(str, endptr - buf);
+ return r;
+}
+
+struct bstr bstr_splitchar(struct bstr str, struct bstr *rest, const char c)
+{
+ int pos = bstrchr(str, c);
+ if (pos < 0)
+ pos = str.len;
+ if (rest)
+ *rest = bstr_cut(str, pos + 1);
+ return bstr_splice(str, 0, pos + 1);
+}
+
+struct bstr bstr_strip_linebreaks(struct bstr str)
+{
+ if (bstr_endswith0(str, "\r\n")) {
+ str = bstr_splice(str, 0, str.len - 2);
+ } else if (bstr_endswith0(str, "\n")) {
+ str = bstr_splice(str, 0, str.len - 1);
+ }
+ return str;
+}
+
+bool bstr_eatstart(struct bstr *s, struct bstr prefix)
+{
+ if (!bstr_startswith(*s, prefix))
+ return false;
+ *s = bstr_cut(*s, prefix.len);
+ return true;
+}
+
+bool bstr_eatend(struct bstr *s, struct bstr prefix)
+{
+ if (!bstr_endswith(*s, prefix))
+ return false;
+ s->len -= prefix.len;
+ return true;
+}
+
+void bstr_lower(struct bstr str)
+{
+ for (int i = 0; i < str.len; i++)
+ str.start[i] = mp_tolower(str.start[i]);
+}
+
+int bstr_sscanf(struct bstr str, const char *format, ...)
+{
+ char *ptr = bstrdup0(NULL, str);
+ va_list va;
+ va_start(va, format);
+ int ret = vsscanf(ptr, format, va);
+ va_end(va);
+ talloc_free(ptr);
+ return ret;
+}
+
+int bstr_parse_utf8_code_length(unsigned char b)
+{
+ if (b < 128)
+ return 1;
+ int bytes = 7 - mp_log2(b ^ 255);
+ return (bytes >= 2 && bytes <= 4) ? bytes : -1;
+}
+
+int bstr_decode_utf8(struct bstr s, struct bstr *out_next)
+{
+ if (s.len == 0)
+ return -1;
+ unsigned int codepoint = s.start[0];
+ s.start++; s.len--;
+ if (codepoint >= 128) {
+ int bytes = bstr_parse_utf8_code_length(codepoint);
+ if (bytes < 1 || s.len < bytes - 1)
+ return -1;
+ codepoint &= 127 >> bytes;
+ for (int n = 1; n < bytes; n++) {
+ int tmp = (unsigned char)s.start[0];
+ if ((tmp & 0xC0) != 0x80)
+ return -1;
+ codepoint = (codepoint << 6) | (tmp & ~0xC0);
+ s.start++; s.len--;
+ }
+ if (codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF))
+ return -1;
+ // Overlong sequences - check taken from libavcodec.
+ // (The only reason we even bother with this is to make libavcodec's
+ // retarded subtitle utf-8 check happy.)
+ unsigned int min = bytes == 2 ? 0x80 : 1 << (5 * bytes - 4);
+ if (codepoint < min)
+ return -1;
+ }
+ if (out_next)
+ *out_next = s;
+ return codepoint;
+}
+
+struct bstr bstr_split_utf8(struct bstr str, struct bstr *out_next)
+{
+ bstr rest;
+ int code = bstr_decode_utf8(str, &rest);
+ if (code < 0)
+ return (bstr){0};
+ if (out_next)
+ *out_next = rest;
+ return bstr_splice(str, 0, str.len - rest.len);
+}
+
+int bstr_validate_utf8(struct bstr s)
+{
+ while (s.len) {
+ if (bstr_decode_utf8(s, &s) < 0) {
+ // Try to guess whether the sequence was just cut-off.
+ unsigned int codepoint = (unsigned char)s.start[0];
+ int bytes = bstr_parse_utf8_code_length(codepoint);
+ if (bytes > 1 && s.len < 6) {
+ // Manually check validity of left bytes
+ for (int n = 1; n < bytes; n++) {
+ if (n >= s.len) {
+ // Everything valid until now - just cut off.
+ return -(bytes - s.len);
+ }
+ int tmp = (unsigned char)s.start[n];
+ if ((tmp & 0xC0) != 0x80)
+ break;
+ }
+ }
+ return -8;
+ }
+ }
+ return 0;
+}
+
+struct bstr bstr_sanitize_utf8_latin1(void *talloc_ctx, struct bstr s)
+{
+ bstr new = {0};
+ bstr left = s;
+ unsigned char *first_ok = s.start;
+ while (left.len) {
+ int r = bstr_decode_utf8(left, &left);
+ if (r < 0) {
+ bstr_xappend(talloc_ctx, &new, (bstr){first_ok, left.start - first_ok});
+ mp_append_utf8_bstr(talloc_ctx, &new, (unsigned char)left.start[0]);
+ left.start += 1;
+ left.len -= 1;
+ first_ok = left.start;
+ }
+ }
+ if (!new.start)
+ return s;
+ if (first_ok != left.start)
+ bstr_xappend(talloc_ctx, &new, (bstr){first_ok, left.start - first_ok});
+ return new;
+}
+
+static void resize_append(void *talloc_ctx, bstr *s, size_t append_min)
+{
+ size_t size = talloc_get_size(s->start);
+ assert(s->len <= size);
+ if (append_min > size - s->len) {
+ if (append_min < size)
+ append_min = size; // preallocate in power of 2s
+ if (size >= SIZE_MAX / 2 || append_min >= SIZE_MAX / 2)
+ abort(); // oom
+ s->start = talloc_realloc_size(talloc_ctx, s->start, size + append_min);
+ }
+}
+
+// Append the string, so that *s = *s + append. s->start is expected to be
+// a talloc allocation (which can be realloced) or NULL.
+// This function will always implicitly append a \0 after the new string for
+// convenience.
+// talloc_ctx will be used as parent context, if s->start is NULL.
+void bstr_xappend(void *talloc_ctx, bstr *s, bstr append)
+{
+ if (!append.len)
+ return;
+ resize_append(talloc_ctx, s, append.len + 1);
+ memcpy(s->start + s->len, append.start, append.len);
+ s->len += append.len;
+ s->start[s->len] = '\0';
+}
+
+void bstr_xappend_asprintf(void *talloc_ctx, bstr *s, const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ bstr_xappend_vasprintf(talloc_ctx, s, fmt, ap);
+ va_end(ap);
+}
+
+// Exactly as bstr_xappend(), but with a formatted string.
+void bstr_xappend_vasprintf(void *talloc_ctx, bstr *s, const char *fmt,
+ va_list ap)
+{
+ int size;
+ va_list copy;
+ va_copy(copy, ap);
+ size_t avail = talloc_get_size(s->start) - s->len;
+ char *dest = s->start ? s->start + s->len : NULL;
+ char c;
+ if (avail < 1)
+ dest = &c;
+ size = vsnprintf(dest, MPMAX(avail, 1), fmt, copy);
+ va_end(copy);
+
+ if (size < 0)
+ abort();
+
+ if (avail < 1 || size + 1 > avail) {
+ resize_append(talloc_ctx, s, size + 1);
+ vsnprintf(s->start + s->len, size + 1, fmt, ap);
+ }
+ s->len += size;
+}
+
+bool bstr_case_startswith(struct bstr s, struct bstr prefix)
+{
+ struct bstr start = bstr_splice(s, 0, prefix.len);
+ return start.len == prefix.len && bstrcasecmp(start, prefix) == 0;
+}
+
+bool bstr_case_endswith(struct bstr s, struct bstr suffix)
+{
+ struct bstr end = bstr_cut(s, -suffix.len);
+ return end.len == suffix.len && bstrcasecmp(end, suffix) == 0;
+}
+
+struct bstr bstr_strip_ext(struct bstr str)
+{
+ int dotpos = bstrrchr(str, '.');
+ if (dotpos < 0)
+ return str;
+ return (struct bstr){str.start, dotpos};
+}
+
+struct bstr bstr_get_ext(struct bstr s)
+{
+ int dotpos = bstrrchr(s, '.');
+ if (dotpos < 0)
+ return (struct bstr){NULL, 0};
+ return bstr_splice(s, dotpos + 1, s.len);
+}
+
+static int h_to_i(unsigned char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+
+ return -1; // invalid char
+}
+
+bool bstr_decode_hex(void *talloc_ctx, struct bstr hex, struct bstr *out)
+{
+ if (!out)
+ return false;
+
+ char *arr = talloc_array(talloc_ctx, char, hex.len / 2);
+ int len = 0;
+
+ while (hex.len >= 2) {
+ int a = h_to_i(hex.start[0]);
+ int b = h_to_i(hex.start[1]);
+ hex = bstr_splice(hex, 2, hex.len);
+
+ if (a < 0 || b < 0) {
+ talloc_free(arr);
+ return false;
+ }
+
+ arr[len++] = (a << 4) | b;
+ }
+
+ *out = (struct bstr){ .start = arr, .len = len };
+ return true;
+}
diff --git a/misc/bstr.h b/misc/bstr.h
new file mode 100644
index 0000000..dc8ad40
--- /dev/null
+++ b/misc/bstr.h
@@ -0,0 +1,231 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_BSTR_H
+#define MPLAYER_BSTR_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#include "mpv_talloc.h"
+#include "osdep/compiler.h"
+
+/* NOTE: 'len' is size_t, but most string-handling functions below assume
+ * that input size has been sanity checked and len fits in an int.
+ */
+typedef struct bstr {
+ unsigned char *start;
+ size_t len;
+} bstr;
+
+// If str.start is NULL, return NULL.
+static inline char *bstrdup0(void *talloc_ctx, struct bstr str)
+{
+ return talloc_strndup(talloc_ctx, (char *)str.start, str.len);
+}
+
+// Like bstrdup0(), but always return a valid C-string.
+static inline char *bstrto0(void *talloc_ctx, struct bstr str)
+{
+ return str.start ? bstrdup0(talloc_ctx, str) : talloc_strdup(talloc_ctx, "");
+}
+
+// Return start = NULL iff that is true for the original.
+static inline struct bstr bstrdup(void *talloc_ctx, struct bstr str)
+{
+ struct bstr r = { NULL, str.len };
+ if (str.start)
+ r.start = (unsigned char *)talloc_memdup(talloc_ctx, str.start, str.len);
+ return r;
+}
+
+static inline struct bstr bstr0(const char *s)
+{
+ return (struct bstr){(unsigned char *)s, s ? strlen(s) : 0};
+}
+
+int bstrcmp(struct bstr str1, struct bstr str2);
+int bstrcasecmp(struct bstr str1, struct bstr str2);
+int bstrchr(struct bstr str, int c);
+int bstrrchr(struct bstr str, int c);
+int bstrspn(struct bstr str, const char *accept);
+int bstrcspn(struct bstr str, const char *reject);
+
+int bstr_find(struct bstr haystack, struct bstr needle);
+struct bstr bstr_lstrip(struct bstr str);
+struct bstr bstr_strip(struct bstr str);
+struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest);
+bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right);
+struct bstr bstr_splice(struct bstr str, int start, int end);
+long long bstrtoll(struct bstr str, struct bstr *rest, int base);
+double bstrtod(struct bstr str, struct bstr *rest);
+void bstr_lower(struct bstr str);
+int bstr_sscanf(struct bstr str, const char *format, ...);
+
+// Decode a string containing hexadecimal data. All whitespace will be silently
+// ignored. When successful, this allocates a new array to store the output.
+bool bstr_decode_hex(void *talloc_ctx, struct bstr hex, struct bstr *out);
+
+// Decode the UTF-8 code point at the start of the string, and return the
+// character.
+// After calling this function, *out_next will point to the next character.
+// out_next can be NULL.
+// On error, -1 is returned, and *out_next is not modified.
+int bstr_decode_utf8(struct bstr str, struct bstr *out_next);
+
+// Return the UTF-8 code point at the start of the string.
+// After calling this function, *out_next will point to the next character.
+// out_next can be NULL.
+// On error, an empty string is returned, and *out_next is not modified.
+struct bstr bstr_split_utf8(struct bstr str, struct bstr *out_next);
+
+// Return the length of the UTF-8 sequence that starts with the given byte.
+// Given a string char *s, the next UTF-8 code point is to be expected at
+// s + bstr_parse_utf8_code_length(s[0])
+// On error, -1 is returned. On success, it returns a value in the range [1, 4].
+int bstr_parse_utf8_code_length(unsigned char b);
+
+// Return >= 0 if the string is valid UTF-8, otherwise negative error code.
+// Embedded \0 bytes are considered valid.
+// This returns -N if the UTF-8 string was likely just cut-off in the middle of
+// an UTF-8 sequence: -1 means 1 byte was missing, -5 5 bytes missing.
+// If the string was likely not cut off, -8 is returned.
+// Use (return_value > -8) to check whether the string is valid UTF-8 or valid
+// but cut-off UTF-8.
+int bstr_validate_utf8(struct bstr s);
+
+// Force the input string to valid UTF-8. If invalid UTF-8 encoding is
+// encountered, the invalid bytes are interpreted as Latin-1.
+// Embedded \0 bytes are considered valid.
+// If replacement happens, a newly allocated string is returned (with a \0
+// byte added past its end for convenience). The string is allocated via
+// talloc, with talloc_ctx as parent.
+struct bstr bstr_sanitize_utf8_latin1(void *talloc_ctx, struct bstr s);
+
+// Return the text before the occurrence of a character, and return it. Change
+// *rest to point to the text following this character. (rest can be NULL.)
+struct bstr bstr_splitchar(struct bstr str, struct bstr *rest, const char c);
+
+// Like bstr_splitchar. Trailing newlines are not stripped.
+static inline struct bstr bstr_getline(struct bstr str, struct bstr *rest)
+{
+ return bstr_splitchar(str, rest, '\n');
+}
+
+// Strip one trailing line break. This is intended for use with bstr_getline,
+// and will remove the trailing \n or \r\n sequence.
+struct bstr bstr_strip_linebreaks(struct bstr str);
+
+void bstr_xappend(void *talloc_ctx, bstr *s, bstr append);
+void bstr_xappend_asprintf(void *talloc_ctx, bstr *s, const char *fmt, ...)
+ PRINTF_ATTRIBUTE(3, 4);
+void bstr_xappend_vasprintf(void *talloc_ctx, bstr *s, const char *fmt, va_list va)
+ PRINTF_ATTRIBUTE(3, 0);
+
+// If s starts/ends with prefix, return true and return the rest of the string
+// in s.
+bool bstr_eatstart(struct bstr *s, struct bstr prefix);
+bool bstr_eatend(struct bstr *s, struct bstr prefix);
+
+bool bstr_case_startswith(struct bstr s, struct bstr prefix);
+bool bstr_case_endswith(struct bstr s, struct bstr suffix);
+struct bstr bstr_strip_ext(struct bstr str);
+struct bstr bstr_get_ext(struct bstr s);
+
+static inline struct bstr bstr_cut(struct bstr str, int n)
+{
+ if (n < 0) {
+ n += str.len;
+ if (n < 0)
+ n = 0;
+ }
+ if (((size_t)n) > str.len)
+ n = str.len;
+ return (struct bstr){str.start + n, str.len - n};
+}
+
+static inline bool bstr_startswith(struct bstr str, struct bstr prefix)
+{
+ if (str.len < prefix.len)
+ return false;
+ return !memcmp(str.start, prefix.start, prefix.len);
+}
+
+static inline bool bstr_startswith0(struct bstr str, const char *prefix)
+{
+ return bstr_startswith(str, bstr0(prefix));
+}
+
+static inline bool bstr_endswith(struct bstr str, struct bstr suffix)
+{
+ if (str.len < suffix.len)
+ return false;
+ return !memcmp(str.start + str.len - suffix.len, suffix.start, suffix.len);
+}
+
+static inline bool bstr_endswith0(struct bstr str, const char *suffix)
+{
+ return bstr_endswith(str, bstr0(suffix));
+}
+
+static inline int bstrcmp0(struct bstr str1, const char *str2)
+{
+ return bstrcmp(str1, bstr0(str2));
+}
+
+static inline bool bstr_equals(struct bstr str1, struct bstr str2)
+{
+ if (str1.len != str2.len)
+ return false;
+
+ return str1.start == str2.start || bstrcmp(str1, str2) == 0;
+}
+
+static inline bool bstr_equals0(struct bstr str1, const char *str2)
+{
+ return bstr_equals(str1, bstr0(str2));
+}
+
+static inline int bstrcasecmp0(struct bstr str1, const char *str2)
+{
+ return bstrcasecmp(str1, bstr0(str2));
+}
+
+static inline int bstr_find0(struct bstr haystack, const char *needle)
+{
+ return bstr_find(haystack, bstr0(needle));
+}
+
+static inline bool bstr_eatstart0(struct bstr *s, const char *prefix)
+{
+ return bstr_eatstart(s, bstr0(prefix));
+}
+
+static inline bool bstr_eatend0(struct bstr *s, const char *prefix)
+{
+ return bstr_eatend(s, bstr0(prefix));
+}
+
+// create a pair (not single value!) for "%.*s" printf syntax
+#define BSTR_P(bstr) (int)((bstr).len), ((bstr).start ? (char*)(bstr).start : "")
+
+#define WHITESPACE " \f\n\r\t\v"
+
+#endif /* MPLAYER_BSTR_H */
diff --git a/misc/charset_conv.c b/misc/charset_conv.c
new file mode 100644
index 0000000..b54f636
--- /dev/null
+++ b/misc/charset_conv.c
@@ -0,0 +1,235 @@
+/*
+ * This file is part of mpv.
+ *
+ * Based on code taken from libass (ISC license), which was originally part
+ * of MPlayer (GPL).
+ * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <errno.h>
+#include <strings.h>
+#include <assert.h>
+
+#include "config.h"
+
+#include "common/msg.h"
+
+#if HAVE_UCHARDET
+#include <uchardet.h>
+#endif
+
+#if HAVE_ICONV
+#include <iconv.h>
+#endif
+
+#include "charset_conv.h"
+
+bool mp_charset_is_utf8(const char *user_cp)
+{
+ return user_cp && (strcasecmp(user_cp, "utf8") == 0 ||
+ strcasecmp(user_cp, "utf-8") == 0);
+}
+
+bool mp_charset_is_utf16(const char *user_cp)
+{
+ bstr s = bstr0(user_cp);
+ return bstr_case_startswith(s, bstr0("utf16")) ||
+ bstr_case_startswith(s, bstr0("utf-16"));
+}
+
+static const char *const utf_bom[3] = {"\xEF\xBB\xBF", "\xFF\xFE", "\xFE\xFF"};
+static const char *const utf_enc[3] = {"utf-8", "utf-16le", "utf-16be"};
+
+static const char *ms_bom_guess(bstr buf)
+{
+ for (int n = 0; n < 3; n++) {
+ if (bstr_startswith0(buf, utf_bom[n]))
+ return utf_enc[n];
+ }
+ return NULL;
+}
+
+#if HAVE_UCHARDET
+static const char *mp_uchardet(void *talloc_ctx, struct mp_log *log, bstr buf)
+{
+ uchardet_t det = uchardet_new();
+ if (!det)
+ return NULL;
+ if (uchardet_handle_data(det, buf.start, buf.len) != 0) {
+ uchardet_delete(det);
+ return NULL;
+ }
+ uchardet_data_end(det);
+ char *res = talloc_strdup(talloc_ctx, uchardet_get_charset(det));
+ if (res && !res[0])
+ res = NULL;
+ if (res) {
+ mp_verbose(log, "libuchardet detected charset as %s\n", res);
+ iconv_t icdsc = iconv_open("UTF-8", res);
+ if (icdsc == (iconv_t)(-1)) {
+ mp_warn(log, "Charset '%s' not supported by iconv.\n", res);
+ res = NULL;
+ } else {
+ iconv_close(icdsc);
+ }
+ }
+ uchardet_delete(det);
+ return res;
+}
+#endif
+
+// Runs charset auto-detection on the input buffer, and returns the result.
+// If auto-detection fails, NULL is returned.
+// If user_cp doesn't refer to any known auto-detection (for example because
+// it's a real iconv codepage), user_cp is returned without even looking at
+// the buf data.
+// The return value may (but doesn't have to) be allocated under talloc_ctx.
+const char *mp_charset_guess(void *talloc_ctx, struct mp_log *log, bstr buf,
+ const char *user_cp, int flags)
+{
+ if (user_cp[0] == '+') {
+ mp_verbose(log, "Forcing charset '%s'.\n", user_cp + 1);
+ return user_cp + 1;
+ }
+
+ const char *bom_cp = ms_bom_guess(buf);
+ if (bom_cp) {
+ mp_verbose(log, "Data has a BOM, assuming %s as charset.\n", bom_cp);
+ return bom_cp;
+ }
+
+ int r = bstr_validate_utf8(buf);
+ if (r >= 0 || (r > -8 && (flags & MP_ICONV_ALLOW_CUTOFF))) {
+ if (strcmp(user_cp, "auto") != 0 && !mp_charset_is_utf8(user_cp))
+ mp_verbose(log, "Data looks like UTF-8, ignoring user-provided charset.\n");
+ return "utf-8";
+ }
+
+ const char *res = NULL;
+ if (strcasecmp(user_cp, "auto") == 0) {
+#if HAVE_UCHARDET
+ res = mp_uchardet(talloc_ctx, log, buf);
+#endif
+ if (!res) {
+ mp_verbose(log, "Charset auto-detection failed.\n");
+ res = "UTF-8-BROKEN";
+ }
+ } else {
+ res = user_cp;
+ }
+
+ mp_verbose(log, "Using charset '%s'.\n", res);
+ return res;
+}
+
+// Use iconv to convert buf to UTF-8.
+// Returns buf.start==NULL on error. Returns buf if cp is NULL, or if there is
+// obviously no conversion required (e.g. if cp is "UTF-8").
+// Returns a newly allocated buffer if conversion is done and succeeds. The
+// buffer will be terminated with 0 for convenience (the terminating 0 is not
+// included in the returned length).
+// Free the returned buffer with talloc_free().
+// buf: input data
+// cp: iconv codepage (or NULL)
+// flags: combination of MP_ICONV_* flags
+// returns: buf (no conversion), .start==NULL (error), or allocated buffer
+bstr mp_iconv_to_utf8(struct mp_log *log, bstr buf, const char *cp, int flags)
+{
+#if HAVE_ICONV
+ if (!buf.len)
+ return buf;
+
+ if (!cp || !cp[0] || mp_charset_is_utf8(cp))
+ return buf;
+
+ if (strcasecmp(cp, "ASCII") == 0)
+ return buf;
+
+ if (strcasecmp(cp, "UTF-8-BROKEN") == 0)
+ return bstr_sanitize_utf8_latin1(NULL, buf);
+
+ // Force CP949 over EUC-KR since iconv distinguishes them and
+ // EUC-KR causes error on CP949 encoded data
+ if (strcasecmp(cp, "EUC-KR") == 0)
+ cp = "CP949";
+
+ iconv_t icdsc;
+ if ((icdsc = iconv_open("UTF-8", cp)) == (iconv_t) (-1)) {
+ if (flags & MP_ICONV_VERBOSE)
+ mp_err(log, "Error opening iconv with codepage '%s'\n", cp);
+ goto failure;
+ }
+
+ size_t size = buf.len;
+ size_t osize = size;
+ size_t ileft = size;
+ size_t oleft = size - 1;
+
+ char *outbuf = talloc_size(NULL, osize);
+ char *ip = buf.start;
+ char *op = outbuf;
+
+ while (1) {
+ int clear = 0;
+ size_t rc;
+ if (ileft)
+ rc = iconv(icdsc, &ip, &ileft, &op, &oleft);
+ else {
+ clear = 1; // clear the conversion state and leave
+ rc = iconv(icdsc, NULL, NULL, &op, &oleft);
+ }
+ if (rc == (size_t) (-1)) {
+ if (errno == E2BIG) {
+ size_t offset = op - outbuf;
+ outbuf = talloc_realloc_size(NULL, outbuf, osize + size);
+ op = outbuf + offset;
+ osize += size;
+ oleft += size;
+ } else {
+ if (errno == EINVAL && (flags & MP_ICONV_ALLOW_CUTOFF)) {
+ // This is intended for cases where the input buffer is cut
+ // at a random byte position. If this happens in the middle
+ // of the buffer, it should still be an error. We say it's
+ // fine if the error is within 10 bytes of the end.
+ if (ileft <= 10)
+ break;
+ }
+ if (flags & MP_ICONV_VERBOSE) {
+ mp_err(log, "Error recoding text with codepage '%s'\n", cp);
+ }
+ talloc_free(outbuf);
+ iconv_close(icdsc);
+ goto failure;
+ }
+ } else if (clear)
+ break;
+ }
+
+ iconv_close(icdsc);
+
+ outbuf[osize - oleft - 1] = 0;
+ return (bstr){outbuf, osize - oleft - 1};
+
+failure:
+#endif
+
+ if (flags & MP_NO_LATIN1_FALLBACK) {
+ return buf;
+ } else {
+ return bstr_sanitize_utf8_latin1(NULL, buf);
+ }
+}
diff --git a/misc/charset_conv.h b/misc/charset_conv.h
new file mode 100644
index 0000000..ccaa17e
--- /dev/null
+++ b/misc/charset_conv.h
@@ -0,0 +1,22 @@
+#ifndef MP_CHARSET_CONV_H
+#define MP_CHARSET_CONV_H
+
+#include <stdbool.h>
+#include "misc/bstr.h"
+
+struct mp_log;
+
+enum {
+ MP_ICONV_VERBOSE = 1, // print errors instead of failing silently
+ MP_ICONV_ALLOW_CUTOFF = 2, // allow partial input data
+ MP_STRICT_UTF8 = 4, // don't fall back to UTF-8-BROKEN when guessing
+ MP_NO_LATIN1_FALLBACK = 8, // fall back to input buffer instead of latin1
+};
+
+bool mp_charset_is_utf8(const char *user_cp);
+bool mp_charset_is_utf16(const char *user_cp);
+const char *mp_charset_guess(void *talloc_ctx, struct mp_log *log, bstr buf,
+ const char *user_cp, int flags);
+bstr mp_iconv_to_utf8(struct mp_log *log, bstr buf, const char *cp, int flags);
+
+#endif
diff --git a/misc/ctype.h b/misc/ctype.h
new file mode 100644
index 0000000..cbff799
--- /dev/null
+++ b/misc/ctype.h
@@ -0,0 +1,19 @@
+#ifndef MP_CTYPE_H_
+#define MP_CTYPE_H_
+
+// Roughly follows C semantics, but doesn't account for EOF, allows char as
+// parameter, and is locale independent (always uses "C" locale).
+
+static inline int mp_isprint(char c) { return (unsigned char)c >= 32; }
+static inline int mp_isspace(char c) { return c == ' ' || c == '\f' || c == '\n' ||
+ c == '\r' || c == '\t' || c =='\v'; }
+static inline int mp_isupper(char c) { return c >= 'A' && c <= 'Z'; }
+static inline int mp_islower(char c) { return c >= 'a' && c <= 'z'; }
+static inline int mp_isdigit(char c) { return c >= '0' && c <= '9'; }
+static inline int mp_isalpha(char c) { return mp_isupper(c) || mp_islower(c); }
+static inline int mp_isalnum(char c) { return mp_isalpha(c) || mp_isdigit(c); }
+
+static inline char mp_tolower(char c) { return mp_isupper(c) ? c - 'A' + 'a' : c; }
+static inline char mp_toupper(char c) { return mp_islower(c) ? c - 'a' + 'A' : c; }
+
+#endif
diff --git a/misc/dispatch.c b/misc/dispatch.c
new file mode 100644
index 0000000..6fd9fe1
--- /dev/null
+++ b/misc/dispatch.c
@@ -0,0 +1,417 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "dispatch.h"
+
+struct mp_dispatch_queue {
+ struct mp_dispatch_item *head, *tail;
+ mp_mutex lock;
+ mp_cond cond;
+ void (*wakeup_fn)(void *wakeup_ctx);
+ void *wakeup_ctx;
+ void (*onlock_fn)(void *onlock_ctx);
+ void *onlock_ctx;
+ // Time at which mp_dispatch_queue_process() should return.
+ int64_t wait;
+ // Make mp_dispatch_queue_process() exit if it's idle.
+ bool interrupted;
+ // The target thread is in mp_dispatch_queue_process() (and either idling,
+ // locked, or running a dispatch callback).
+ bool in_process;
+ mp_thread_id in_process_thread_id;
+ // The target thread is in mp_dispatch_queue_process(), and currently
+ // something has exclusive access to it (e.g. running a dispatch callback,
+ // or a different thread got it with mp_dispatch_lock()).
+ bool locked;
+ // A mp_dispatch_lock() call is requesting an exclusive lock.
+ size_t lock_requests;
+ // locked==true is due to a mp_dispatch_lock() call (for debugging).
+ bool locked_explicit;
+ mp_thread_id locked_explicit_thread_id;
+};
+
+struct mp_dispatch_item {
+ mp_dispatch_fn fn;
+ void *fn_data;
+ bool asynchronous;
+ bool mergeable;
+ bool completed;
+ struct mp_dispatch_item *next;
+};
+
+static void queue_dtor(void *p)
+{
+ struct mp_dispatch_queue *queue = p;
+ assert(!queue->head);
+ assert(!queue->in_process);
+ assert(!queue->lock_requests);
+ assert(!queue->locked);
+ mp_cond_destroy(&queue->cond);
+ mp_mutex_destroy(&queue->lock);
+}
+
+// A dispatch queue lets other threads run callbacks in a target thread.
+// The target thread is the thread which calls mp_dispatch_queue_process().
+// Free the dispatch queue with talloc_free(). At the time of destruction,
+// the queue must be empty. The easiest way to guarantee this is to
+// terminate all potential senders, then call mp_dispatch_run() with a
+// function that e.g. makes the target thread exit, then mp_thread_join() the
+// target thread, and finally destroy the queue. Another way is calling
+// mp_dispatch_queue_process() after terminating all potential senders, and
+// then destroying the queue.
+struct mp_dispatch_queue *mp_dispatch_create(void *ta_parent)
+{
+ struct mp_dispatch_queue *queue = talloc_ptrtype(ta_parent, queue);
+ *queue = (struct mp_dispatch_queue){0};
+ talloc_set_destructor(queue, queue_dtor);
+ mp_mutex_init(&queue->lock);
+ mp_cond_init(&queue->cond);
+ return queue;
+}
+
+// Set a custom function that should be called to guarantee that the target
+// thread wakes up. This is intended for use with code that needs to block
+// on non-pthread primitives, such as e.g. select(). In the case of select(),
+// the wakeup_fn could for example write a byte into a "wakeup" pipe in order
+// to unblock the select(). The wakeup_fn is called from the dispatch queue
+// when there are new dispatch items, and the target thread should then enter
+// mp_dispatch_queue_process() as soon as possible.
+// Note that this setter does not do internal synchronization, so you must set
+// it before other threads see it.
+void mp_dispatch_set_wakeup_fn(struct mp_dispatch_queue *queue,
+ void (*wakeup_fn)(void *wakeup_ctx),
+ void *wakeup_ctx)
+{
+ queue->wakeup_fn = wakeup_fn;
+ queue->wakeup_ctx = wakeup_ctx;
+}
+
+// Set a function that will be called by mp_dispatch_lock() if the target thread
+// is not calling mp_dispatch_queue_process() right now. This is an obscure,
+// optional mechanism to make a worker thread react to external events more
+// quickly. The idea is that the callback will make the worker thread to stop
+// doing whatever (e.g. by setting a flag), and call mp_dispatch_queue_process()
+// in order to let mp_dispatch_lock() calls continue sooner.
+// Like wakeup_fn, this setter does no internal synchronization, and you must
+// not access the dispatch queue itself from the callback.
+void mp_dispatch_set_onlock_fn(struct mp_dispatch_queue *queue,
+ void (*onlock_fn)(void *onlock_ctx),
+ void *onlock_ctx)
+{
+ queue->onlock_fn = onlock_fn;
+ queue->onlock_ctx = onlock_ctx;
+}
+
+static void mp_dispatch_append(struct mp_dispatch_queue *queue,
+ struct mp_dispatch_item *item)
+{
+ mp_mutex_lock(&queue->lock);
+ if (item->mergeable) {
+ for (struct mp_dispatch_item *cur = queue->head; cur; cur = cur->next) {
+ if (cur->mergeable && cur->fn == item->fn &&
+ cur->fn_data == item->fn_data)
+ {
+ talloc_free(item);
+ mp_mutex_unlock(&queue->lock);
+ return;
+ }
+ }
+ }
+
+ if (queue->tail) {
+ queue->tail->next = item;
+ } else {
+ queue->head = item;
+ }
+ queue->tail = item;
+
+ // Wake up the main thread; note that other threads might wait on this
+ // condition for reasons, so broadcast the condition.
+ mp_cond_broadcast(&queue->cond);
+ // No wakeup callback -> assume mp_dispatch_queue_process() needs to be
+ // interrupted instead.
+ if (!queue->wakeup_fn)
+ queue->interrupted = true;
+ mp_mutex_unlock(&queue->lock);
+
+ if (queue->wakeup_fn)
+ queue->wakeup_fn(queue->wakeup_ctx);
+}
+
+// Enqueue a callback to run it on the target thread asynchronously. The target
+// thread will run fn(fn_data) as soon as it enter mp_dispatch_queue_process.
+// Note that mp_dispatch_enqueue() will usually return long before that happens.
+// It's up to the user to signal completion of the callback. It's also up to
+// the user to guarantee that the context fn_data has correct lifetime, i.e.
+// lives until the callback is run, and is freed after that.
+void mp_dispatch_enqueue(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data)
+{
+ struct mp_dispatch_item *item = talloc_ptrtype(NULL, item);
+ *item = (struct mp_dispatch_item){
+ .fn = fn,
+ .fn_data = fn_data,
+ .asynchronous = true,
+ };
+ mp_dispatch_append(queue, item);
+}
+
+// Like mp_dispatch_enqueue(), but the queue code will call talloc_free(fn_data)
+// after the fn callback has been run. (The callback could trivially do that
+// itself, but it makes it easier to implement synchronous and asynchronous
+// requests with the same callback implementation.)
+void mp_dispatch_enqueue_autofree(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data)
+{
+ struct mp_dispatch_item *item = talloc_ptrtype(NULL, item);
+ *item = (struct mp_dispatch_item){
+ .fn = fn,
+ .fn_data = talloc_steal(item, fn_data),
+ .asynchronous = true,
+ };
+ mp_dispatch_append(queue, item);
+}
+
+// Like mp_dispatch_enqueue(), but
+void mp_dispatch_enqueue_notify(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data)
+{
+ struct mp_dispatch_item *item = talloc_ptrtype(NULL, item);
+ *item = (struct mp_dispatch_item){
+ .fn = fn,
+ .fn_data = fn_data,
+ .mergeable = true,
+ .asynchronous = true,
+ };
+ mp_dispatch_append(queue, item);
+}
+
+// Remove already queued item. Only items enqueued with the following functions
+// can be canceled:
+// - mp_dispatch_enqueue()
+// - mp_dispatch_enqueue_notify()
+// Items which were enqueued, and which are currently executing, can not be
+// canceled anymore. This function is mostly for being called from the same
+// context as mp_dispatch_queue_process(), where the "currently executing" case
+// can be excluded.
+void mp_dispatch_cancel_fn(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data)
+{
+ mp_mutex_lock(&queue->lock);
+ struct mp_dispatch_item **pcur = &queue->head;
+ queue->tail = NULL;
+ while (*pcur) {
+ struct mp_dispatch_item *cur = *pcur;
+ if (cur->fn == fn && cur->fn_data == fn_data) {
+ *pcur = cur->next;
+ talloc_free(cur);
+ } else {
+ queue->tail = cur;
+ pcur = &cur->next;
+ }
+ }
+ mp_mutex_unlock(&queue->lock);
+}
+
+// Run fn(fn_data) on the target thread synchronously. This function enqueues
+// the callback and waits until the target thread is done doing this.
+// This is redundant to calling the function inside mp_dispatch_[un]lock(),
+// but can be helpful with code that relies on TLS (such as OpenGL).
+void mp_dispatch_run(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data)
+{
+ struct mp_dispatch_item item = {
+ .fn = fn,
+ .fn_data = fn_data,
+ };
+ mp_dispatch_append(queue, &item);
+
+ mp_mutex_lock(&queue->lock);
+ while (!item.completed)
+ mp_cond_wait(&queue->cond, &queue->lock);
+ mp_mutex_unlock(&queue->lock);
+}
+
+// Process any outstanding dispatch items in the queue. This also handles
+// suspending or locking the this thread from another thread via
+// mp_dispatch_lock().
+// The timeout specifies the minimum wait time. The actual time spent in this
+// function can be much higher if the suspending/locking functions are used, or
+// if executing the dispatch items takes time. On the other hand, this function
+// can return much earlier than the timeout due to sporadic wakeups.
+// Note that this will strictly return only after:
+// - timeout has passed,
+// - all queue items were processed,
+// - the possibly acquired lock has been released
+// It's possible to cancel the timeout by calling mp_dispatch_interrupt().
+// Reentrant calls are not allowed. There can be only 1 thread calling
+// mp_dispatch_queue_process() at a time. In addition, mp_dispatch_lock() can
+// not be called from a thread that is calling mp_dispatch_queue_process() (i.e.
+// no enqueued callback can call the lock/unlock functions).
+void mp_dispatch_queue_process(struct mp_dispatch_queue *queue, double timeout)
+{
+ mp_mutex_lock(&queue->lock);
+ queue->wait = timeout > 0 ? mp_time_ns_add(mp_time_ns(), timeout) : 0;
+ assert(!queue->in_process); // recursion not allowed
+ queue->in_process = true;
+ queue->in_process_thread_id = mp_thread_current_id();
+ // Wake up thread which called mp_dispatch_lock().
+ if (queue->lock_requests)
+ mp_cond_broadcast(&queue->cond);
+ while (1) {
+ if (queue->lock_requests) {
+ // Block due to something having called mp_dispatch_lock().
+ mp_cond_wait(&queue->cond, &queue->lock);
+ } else if (queue->head) {
+ struct mp_dispatch_item *item = queue->head;
+ queue->head = item->next;
+ if (!queue->head)
+ queue->tail = NULL;
+ item->next = NULL;
+ // Unlock, because we want to allow other threads to queue items
+ // while the dispatch item is processed.
+ // At the same time, we must prevent other threads from returning
+ // from mp_dispatch_lock(), which is done by locked=true.
+ assert(!queue->locked);
+ queue->locked = true;
+ mp_mutex_unlock(&queue->lock);
+
+ item->fn(item->fn_data);
+
+ mp_mutex_lock(&queue->lock);
+ assert(queue->locked);
+ queue->locked = false;
+ // Wakeup mp_dispatch_run(), also mp_dispatch_lock().
+ mp_cond_broadcast(&queue->cond);
+ if (item->asynchronous) {
+ talloc_free(item);
+ } else {
+ item->completed = true;
+ }
+ } else if (queue->wait > 0 && !queue->interrupted) {
+ if (mp_cond_timedwait_until(&queue->cond, &queue->lock, queue->wait))
+ queue->wait = 0;
+ } else {
+ break;
+ }
+ }
+ assert(!queue->locked);
+ queue->in_process = false;
+ queue->interrupted = false;
+ mp_mutex_unlock(&queue->lock);
+}
+
+// If the queue is inside of mp_dispatch_queue_process(), make it return as
+// soon as all work items have been run, without waiting for the timeout. This
+// does not make it return early if it's blocked by a mp_dispatch_lock().
+// If the queue is _not_ inside of mp_dispatch_queue_process(), make the next
+// call of it use a timeout of 0 (this is useful behavior if you need to
+// wakeup the main thread from another thread in a race free way).
+void mp_dispatch_interrupt(struct mp_dispatch_queue *queue)
+{
+ mp_mutex_lock(&queue->lock);
+ queue->interrupted = true;
+ mp_cond_broadcast(&queue->cond);
+ mp_mutex_unlock(&queue->lock);
+}
+
+// If a mp_dispatch_queue_process() call is in progress, then adjust the maximum
+// time it blocks due to its timeout argument. Otherwise does nothing. (It
+// makes sense to call this in code that uses both mp_dispatch_[un]lock() and
+// a normal event loop.)
+// Does not work correctly with queues that have mp_dispatch_set_wakeup_fn()
+// called on them, because this implies you actually do waiting via
+// mp_dispatch_queue_process(), while wakeup callbacks are used when you need
+// to wait in external APIs.
+void mp_dispatch_adjust_timeout(struct mp_dispatch_queue *queue, int64_t until)
+{
+ mp_mutex_lock(&queue->lock);
+ if (queue->in_process && queue->wait > until) {
+ queue->wait = until;
+ mp_cond_broadcast(&queue->cond);
+ }
+ mp_mutex_unlock(&queue->lock);
+}
+
+// Grant exclusive access to the target thread's state. While this is active,
+// no other thread can return from mp_dispatch_lock() (i.e. it behaves like
+// a pthread mutex), and no other thread can get dispatch items completed.
+// Other threads can still queue asynchronous dispatch items without waiting,
+// and the mutex behavior applies to this function and dispatch callbacks only.
+// The lock is non-recursive, and dispatch callback functions can be thought of
+// already holding the dispatch lock.
+void mp_dispatch_lock(struct mp_dispatch_queue *queue)
+{
+ mp_mutex_lock(&queue->lock);
+ // Must not be called recursively from dispatched callbacks.
+ if (queue->in_process)
+ assert(!mp_thread_id_equal(queue->in_process_thread_id, mp_thread_current_id()));
+ // Must not be called recursively at all.
+ if (queue->locked_explicit)
+ assert(!mp_thread_id_equal(queue->locked_explicit_thread_id, mp_thread_current_id()));
+ queue->lock_requests += 1;
+ // And now wait until the target thread gets "trapped" within the
+ // mp_dispatch_queue_process() call, which will mean we get exclusive
+ // access to the target's thread state.
+ if (queue->onlock_fn)
+ queue->onlock_fn(queue->onlock_ctx);
+ while (!queue->in_process) {
+ mp_mutex_unlock(&queue->lock);
+ if (queue->wakeup_fn)
+ queue->wakeup_fn(queue->wakeup_ctx);
+ mp_mutex_lock(&queue->lock);
+ if (queue->in_process)
+ break;
+ mp_cond_wait(&queue->cond, &queue->lock);
+ }
+ // Wait until we can get the lock.
+ while (!queue->in_process || queue->locked)
+ mp_cond_wait(&queue->cond, &queue->lock);
+ // "Lock".
+ assert(queue->lock_requests);
+ assert(!queue->locked);
+ assert(!queue->locked_explicit);
+ queue->locked = true;
+ queue->locked_explicit = true;
+ queue->locked_explicit_thread_id = mp_thread_current_id();
+ mp_mutex_unlock(&queue->lock);
+}
+
+// Undo mp_dispatch_lock().
+void mp_dispatch_unlock(struct mp_dispatch_queue *queue)
+{
+ mp_mutex_lock(&queue->lock);
+ assert(queue->locked);
+ // Must be called after a mp_dispatch_lock(), from the same thread.
+ assert(queue->locked_explicit);
+ assert(mp_thread_id_equal(queue->locked_explicit_thread_id, mp_thread_current_id()));
+ // "Unlock".
+ queue->locked = false;
+ queue->locked_explicit = false;
+ queue->lock_requests -= 1;
+ // Wakeup mp_dispatch_queue_process(), and maybe other mp_dispatch_lock()s.
+ // (Would be nice to wake up only 1 other locker if lock_requests>0.)
+ mp_cond_broadcast(&queue->cond);
+ mp_mutex_unlock(&queue->lock);
+}
diff --git a/misc/dispatch.h b/misc/dispatch.h
new file mode 100644
index 0000000..fbf8260
--- /dev/null
+++ b/misc/dispatch.h
@@ -0,0 +1,32 @@
+#ifndef MP_DISPATCH_H_
+#define MP_DISPATCH_H_
+
+#include <stdint.h>
+
+typedef void (*mp_dispatch_fn)(void *data);
+struct mp_dispatch_queue;
+
+struct mp_dispatch_queue *mp_dispatch_create(void *talloc_parent);
+void mp_dispatch_set_wakeup_fn(struct mp_dispatch_queue *queue,
+ void (*wakeup_fn)(void *wakeup_ctx),
+ void *wakeup_ctx);
+void mp_dispatch_set_onlock_fn(struct mp_dispatch_queue *queue,
+ void (*onlock_fn)(void *onlock_ctx),
+ void *onlock_ctx);
+void mp_dispatch_enqueue(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data);
+void mp_dispatch_enqueue_autofree(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data);
+void mp_dispatch_enqueue_notify(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data);
+void mp_dispatch_cancel_fn(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data);
+void mp_dispatch_run(struct mp_dispatch_queue *queue,
+ mp_dispatch_fn fn, void *fn_data);
+void mp_dispatch_queue_process(struct mp_dispatch_queue *queue, double timeout);
+void mp_dispatch_interrupt(struct mp_dispatch_queue *queue);
+void mp_dispatch_adjust_timeout(struct mp_dispatch_queue *queue, int64_t until);
+void mp_dispatch_lock(struct mp_dispatch_queue *queue);
+void mp_dispatch_unlock(struct mp_dispatch_queue *queue);
+
+#endif
diff --git a/misc/jni.c b/misc/jni.c
new file mode 100644
index 0000000..82f6356
--- /dev/null
+++ b/misc/jni.c
@@ -0,0 +1,429 @@
+/*
+ * JNI utility functions
+ *
+ * Copyright (c) 2015-2016 Matthieu Bouron <matthieu.bouron stupeflix.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavcodec/jni.h>
+#include <libavutil/mem.h>
+#include <libavutil/bprint.h>
+#include <stdlib.h>
+
+#include "jni.h"
+#include "osdep/threads.h"
+
+static JavaVM *java_vm;
+static pthread_key_t current_env;
+static mp_once once = MP_STATIC_ONCE_INITIALIZER;
+static mp_static_mutex lock = MP_STATIC_MUTEX_INITIALIZER;
+
+static void jni_detach_env(void *data)
+{
+ if (java_vm) {
+ (*java_vm)->DetachCurrentThread(java_vm);
+ }
+}
+
+static void jni_create_pthread_key(void)
+{
+ pthread_key_create(&current_env, jni_detach_env);
+}
+
+JNIEnv *mp_jni_get_env(struct mp_log *log)
+{
+ int ret = 0;
+ JNIEnv *env = NULL;
+
+ mp_mutex_lock(&lock);
+ if (java_vm == NULL) {
+ java_vm = av_jni_get_java_vm(NULL);
+ }
+
+ if (!java_vm) {
+ mp_err(log, "No Java virtual machine has been registered\n");
+ goto done;
+ }
+
+ mp_exec_once(&once, jni_create_pthread_key);
+
+ if ((env = pthread_getspecific(current_env)) != NULL) {
+ goto done;
+ }
+
+ ret = (*java_vm)->GetEnv(java_vm, (void **)&env, JNI_VERSION_1_6);
+ switch(ret) {
+ case JNI_EDETACHED:
+ if ((*java_vm)->AttachCurrentThread(java_vm, &env, NULL) != 0) {
+ mp_err(log, "Failed to attach the JNI environment to the current thread\n");
+ env = NULL;
+ } else {
+ pthread_setspecific(current_env, env);
+ }
+ break;
+ case JNI_OK:
+ break;
+ case JNI_EVERSION:
+ mp_err(log, "The specified JNI version is not supported\n");
+ break;
+ default:
+ mp_err(log, "Failed to get the JNI environment attached to this thread\n");
+ break;
+ }
+
+done:
+ mp_mutex_unlock(&lock);
+ return env;
+}
+
+char *mp_jni_jstring_to_utf_chars(JNIEnv *env, jstring string, struct mp_log *log)
+{
+ char *ret = NULL;
+ const char *utf_chars = NULL;
+
+ jboolean copy = 0;
+
+ if (!string) {
+ return NULL;
+ }
+
+ utf_chars = (*env)->GetStringUTFChars(env, string, &copy);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "String.getStringUTFChars() threw an exception\n");
+ return NULL;
+ }
+
+ ret = av_strdup(utf_chars);
+
+ (*env)->ReleaseStringUTFChars(env, string, utf_chars);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "String.releaseStringUTFChars() threw an exception\n");
+ return NULL;
+ }
+
+ return ret;
+}
+
+jstring mp_jni_utf_chars_to_jstring(JNIEnv *env, const char *utf_chars, struct mp_log *log)
+{
+ jstring ret;
+
+ ret = (*env)->NewStringUTF(env, utf_chars);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "NewStringUTF() threw an exception\n");
+ return NULL;
+ }
+
+ return ret;
+}
+
+int mp_jni_exception_get_summary(JNIEnv *env, jthrowable exception, char **error, struct mp_log *log)
+{
+ int ret = 0;
+
+ AVBPrint bp;
+
+ char *name = NULL;
+ char *message = NULL;
+
+ jclass class_class = NULL;
+ jmethodID get_name_id = NULL;
+
+ jclass exception_class = NULL;
+ jmethodID get_message_id = NULL;
+
+ jstring string = NULL;
+
+ av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
+
+ exception_class = (*env)->GetObjectClass(env, exception);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "Could not find Throwable class\n");
+ ret = -1;
+ goto done;
+ }
+
+ class_class = (*env)->GetObjectClass(env, exception_class);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "Could not find Throwable class's class\n");
+ ret = -1;
+ goto done;
+ }
+
+ get_name_id = (*env)->GetMethodID(env, class_class, "getName", "()Ljava/lang/String;");
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "Could not find method Class.getName()\n");
+ ret = -1;
+ goto done;
+ }
+
+ string = (*env)->CallObjectMethod(env, exception_class, get_name_id);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "Class.getName() threw an exception\n");
+ ret = -1;
+ goto done;
+ }
+
+ if (string) {
+ name = mp_jni_jstring_to_utf_chars(env, string, log);
+ (*env)->DeleteLocalRef(env, string);
+ string = NULL;
+ }
+
+ get_message_id = (*env)->GetMethodID(env, exception_class, "getMessage", "()Ljava/lang/String;");
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "Could not find method java/lang/Throwable.getMessage()\n");
+ ret = -1;
+ goto done;
+ }
+
+ string = (*env)->CallObjectMethod(env, exception, get_message_id);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionClear(env);
+ mp_err(log, "Throwable.getMessage() threw an exception\n");
+ ret = -1;
+ goto done;
+ }
+
+ if (string) {
+ message = mp_jni_jstring_to_utf_chars(env, string, log);
+ (*env)->DeleteLocalRef(env, string);
+ string = NULL;
+ }
+
+ if (name && message) {
+ av_bprintf(&bp, "%s: %s", name, message);
+ } else if (name && !message) {
+ av_bprintf(&bp, "%s occurred", name);
+ } else if (!name && message) {
+ av_bprintf(&bp, "Exception: %s", message);
+ } else {
+ mp_warn(log, "Could not retrieve exception name and message\n");
+ av_bprintf(&bp, "Exception occurred");
+ }
+
+ ret = av_bprint_finalize(&bp, error);
+done:
+
+ av_free(name);
+ av_free(message);
+
+ if (class_class) {
+ (*env)->DeleteLocalRef(env, class_class);
+ }
+
+ if (exception_class) {
+ (*env)->DeleteLocalRef(env, exception_class);
+ }
+
+ if (string) {
+ (*env)->DeleteLocalRef(env, string);
+ }
+
+ return ret;
+}
+
+int mp_jni_exception_check(JNIEnv *env, int logging, struct mp_log *log)
+{
+ int ret;
+
+ jthrowable exception;
+
+ char *message = NULL;
+
+ if (!(*(env))->ExceptionCheck((env))) {
+ return 0;
+ }
+
+ if (!logging) {
+ (*(env))->ExceptionClear((env));
+ return -1;
+ }
+
+ exception = (*env)->ExceptionOccurred(env);
+ (*(env))->ExceptionClear((env));
+
+ if ((ret = mp_jni_exception_get_summary(env, exception, &message, log)) < 0) {
+ (*env)->DeleteLocalRef(env, exception);
+ return ret;
+ }
+
+ (*env)->DeleteLocalRef(env, exception);
+
+ mp_err(log, "%s\n", message);
+ av_free(message);
+
+ return -1;
+}
+
+int mp_jni_init_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log)
+{
+ int i, ret = 0;
+ jclass last_clazz = NULL;
+
+ for (i = 0; jfields_mapping[i].name; i++) {
+ int mandatory = jfields_mapping[i].mandatory;
+ enum MPJniFieldType type = jfields_mapping[i].type;
+
+ if (type == MP_JNI_CLASS) {
+ jclass clazz;
+
+ last_clazz = NULL;
+
+ clazz = (*env)->FindClass(env, jfields_mapping[i].name);
+ if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) {
+ goto done;
+ }
+
+ last_clazz = *(jclass*)((uint8_t*)jfields + jfields_mapping[i].offset) =
+ global ? (*env)->NewGlobalRef(env, clazz) : clazz;
+
+ if (global) {
+ (*env)->DeleteLocalRef(env, clazz);
+ }
+
+ } else {
+
+ if (!last_clazz) {
+ ret = -1;
+ break;
+ }
+
+ switch(type) {
+ case MP_JNI_FIELD: {
+ jfieldID field_id = (*env)->GetFieldID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature);
+ if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) {
+ goto done;
+ }
+
+ *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = field_id;
+ break;
+ }
+ case MP_JNI_STATIC_FIELD_AS_INT:
+ case MP_JNI_STATIC_FIELD: {
+ jfieldID field_id = (*env)->GetStaticFieldID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature);
+ if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) {
+ goto done;
+ }
+
+ if (type == MP_JNI_STATIC_FIELD_AS_INT) {
+ if (field_id) {
+ jint value = (*env)->GetStaticIntField(env, last_clazz, field_id);
+ if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) {
+ goto done;
+ }
+ *(jint*)((uint8_t*)jfields + jfields_mapping[i].offset) = value;
+ }
+ } else {
+ *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = field_id;
+ }
+ break;
+ }
+ case MP_JNI_METHOD: {
+ jmethodID method_id = (*env)->GetMethodID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature);
+ if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) {
+ goto done;
+ }
+
+ *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = method_id;
+ break;
+ }
+ case MP_JNI_STATIC_METHOD: {
+ jmethodID method_id = (*env)->GetStaticMethodID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature);
+ if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) {
+ goto done;
+ }
+
+ *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = method_id;
+ break;
+ }
+ default:
+ mp_err(log, "Unknown JNI field type\n");
+ ret = -1;
+ goto done;
+ }
+
+ ret = 0;
+ }
+ }
+
+done:
+ if (ret < 0) {
+ /* reset jfields in case of failure so it does not leak references */
+ mp_jni_reset_jfields(env, jfields, jfields_mapping, global, log);
+ }
+
+ return ret;
+}
+
+int mp_jni_reset_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log)
+{
+ int i;
+
+ for (i = 0; jfields_mapping[i].name; i++) {
+ enum MPJniFieldType type = jfields_mapping[i].type;
+
+ switch(type) {
+ case MP_JNI_CLASS: {
+ jclass clazz = *(jclass*)((uint8_t*)jfields + jfields_mapping[i].offset);
+ if (!clazz)
+ continue;
+
+ if (global) {
+ (*env)->DeleteGlobalRef(env, clazz);
+ } else {
+ (*env)->DeleteLocalRef(env, clazz);
+ }
+
+ *(jclass*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL;
+ break;
+ }
+ case MP_JNI_FIELD: {
+ *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL;
+ break;
+ }
+ case MP_JNI_STATIC_FIELD: {
+ *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL;
+ break;
+ }
+ case MP_JNI_STATIC_FIELD_AS_INT: {
+ *(jint*)((uint8_t*)jfields + jfields_mapping[i].offset) = 0;
+ break;
+ }
+ case MP_JNI_METHOD: {
+ *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL;
+ break;
+ }
+ case MP_JNI_STATIC_METHOD: {
+ *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL;
+ break;
+ }
+ default:
+ mp_err(log, "Unknown JNI field type\n");
+ }
+ }
+
+ return 0;
+}
diff --git a/misc/jni.h b/misc/jni.h
new file mode 100644
index 0000000..c9e4c28
--- /dev/null
+++ b/misc/jni.h
@@ -0,0 +1,161 @@
+/*
+ * JNI utility functions
+ *
+ * Copyright (c) 2015-2016 Matthieu Bouron <matthieu.bouron stupeflix.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_JNI_H
+#define MP_JNI_H
+
+#include <jni.h>
+#include "common/msg.h"
+
+/* Convenience macros */
+#define MP_JNI_GET_ENV(obj) mp_jni_get_env((obj)->log)
+#define MP_JNI_EXCEPTION_CHECK() mp_jni_exception_check(env, 0, NULL)
+#define MP_JNI_EXCEPTION_LOG(obj) mp_jni_exception_check(env, 1, (obj)->log)
+#define MP_JNI_DO(what, obj, method, ...) (*env)->what(env, obj, method, ##__VA_ARGS__)
+#define MP_JNI_NEW(clazz, method, ...) MP_JNI_DO(NewObject, clazz, method, ##__VA_ARGS__)
+#define MP_JNI_CALL_INT(obj, method, ...) MP_JNI_DO(CallIntMethod, obj, method, ##__VA_ARGS__)
+#define MP_JNI_CALL_BOOL(obj, method, ...) MP_JNI_DO(CallBooleanMethod, obj, method, ##__VA_ARGS__)
+#define MP_JNI_CALL_VOID(obj, method, ...) MP_JNI_DO(CallVoidMethod, obj, method, ##__VA_ARGS__)
+#define MP_JNI_CALL_STATIC_INT(clazz, method, ...) MP_JNI_DO(CallStaticIntMethod, clazz, method, ##__VA_ARGS__)
+#define MP_JNI_CALL_OBJECT(obj, method, ...) MP_JNI_DO(CallObjectMethod, obj, method, ##__VA_ARGS__)
+#define MP_JNI_GET_INT(obj, field) MP_JNI_DO(GetIntField, obj, field)
+#define MP_JNI_GET_LONG(obj, field) MP_JNI_DO(GetLongField, obj, field)
+#define MP_JNI_GET_BOOL(obj, field) MP_JNI_DO(GetBoolField, obj, field)
+
+/*
+ * Attach permanently a JNI environment to the current thread and retrieve it.
+ *
+ * If successfully attached, the JNI environment will automatically be detached
+ * at thread destruction.
+ *
+ * @param attached pointer to an integer that will be set to 1 if the
+ * environment has been attached to the current thread or 0 if it is
+ * already attached.
+ * @param log context used for logging
+ * @return the JNI environment on success, NULL otherwise
+ */
+JNIEnv *mp_jni_get_env(struct mp_log *log);
+
+/*
+ * Convert a jstring to its utf characters equivalent.
+ *
+ * @param env JNI environment
+ * @param string Java string to convert
+ * @param log context used for logging
+ * @return a pointer to an array of unicode characters on success, NULL
+ * otherwise
+ */
+char *mp_jni_jstring_to_utf_chars(JNIEnv *env, jstring string, struct mp_log *log);
+
+/*
+ * Convert utf chars to its jstring equivalent.
+ *
+ * @param env JNI environment
+ * @param utf_chars a pointer to an array of unicode characters
+ * @param log context used for logging
+ * @return a Java string object on success, NULL otherwise
+ */
+jstring mp_jni_utf_chars_to_jstring(JNIEnv *env, const char *utf_chars, struct mp_log *log);
+
+/*
+ * Extract the error summary from a jthrowable in the form of "className: errorMessage"
+ *
+ * @param env JNI environment
+ * @param exception exception to get the summary from
+ * @param error address pointing to the error, the value is updated if a
+ * summary can be extracted
+ * @param log context used for logging
+ * @return 0 on success, < 0 otherwise
+ */
+int mp_jni_exception_get_summary(JNIEnv *env, jthrowable exception, char **error, struct mp_log *log);
+
+/*
+ * Check if an exception has occurred,log it using av_log and clear it.
+ *
+ * @param env JNI environment
+ * @param value used to enable logging if an exception has occurred,
+ * 0 disables logging, != 0 enables logging
+ * @param log context used for logging
+ */
+int mp_jni_exception_check(JNIEnv *env, int logging, struct mp_log *log);
+
+/*
+ * Jni field type.
+ */
+enum MPJniFieldType {
+
+ MP_JNI_CLASS,
+ MP_JNI_FIELD,
+ MP_JNI_STATIC_FIELD,
+ MP_JNI_STATIC_FIELD_AS_INT,
+ MP_JNI_METHOD,
+ MP_JNI_STATIC_METHOD
+
+};
+
+/*
+ * Jni field describing a class, a field or a method to be retrieved using
+ * the mp_jni_init_jfields method.
+ */
+struct MPJniField {
+
+ const char *name;
+ const char *method;
+ const char *signature;
+ enum MPJniFieldType type;
+ int offset;
+ int mandatory;
+
+};
+
+/*
+ * Retrieve class references, field ids and method ids to an arbitrary structure.
+ *
+ * @param env JNI environment
+ * @param jfields a pointer to an arbitrary structure where the different
+ * fields are declared and where the MPJNIField mapping table offsets are
+ * pointing to
+ * @param jfields_mapping null terminated array of MPJNIFields describing
+ * the class/field/method to be retrieved
+ * @param global make the classes references global. It is the caller
+ * responsibility to properly release global references.
+ * @param log_ctx context used for logging, can be NULL
+ * @return 0 on success, < 0 otherwise
+ */
+int mp_jni_init_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log);
+
+/*
+ * Delete class references, field ids and method ids of an arbitrary structure.
+ *
+ * @param env JNI environment
+ * @param jfields a pointer to an arbitrary structure where the different
+ * fields are declared and where the MPJNIField mapping table offsets are
+ * pointing to
+ * @param jfields_mapping null terminated array of MPJNIFields describing
+ * the class/field/method to be deleted
+ * @param global treat the classes references as global and delete them
+ * accordingly
+ * @param log_ctx context used for logging, can be NULL
+ * @return 0 on success, < 0 otherwise
+ */
+int mp_jni_reset_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log);
+
+#endif
diff --git a/misc/json.c b/misc/json.c
new file mode 100644
index 0000000..608cfad
--- /dev/null
+++ b/misc/json.c
@@ -0,0 +1,359 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* JSON parser:
+ *
+ * Unlike standard JSON, \u escapes don't allow you to specify UTF-16 surrogate
+ * pairs. There may be some differences how numbers are parsed (this parser
+ * doesn't verify what's passed to strtod(), and also prefers parsing numbers
+ * as integers with stroll() if possible).
+ *
+ * It has some non-standard extensions which shouldn't conflict with JSON:
+ * - a list or object item can have a trailing ","
+ * - object syntax accepts "=" in addition of ":"
+ * - object keys can be unquoted, if they start with a character in [A-Za-z_]
+ * and contain only characters in [A-Za-z0-9_]
+ * - byte escapes with "\xAB" are allowed (with AB being a 2 digit hex number)
+ *
+ * Also see: http://tools.ietf.org/html/rfc8259
+ *
+ * JSON writer:
+ *
+ * Doesn't insert whitespace. It's literally a waste of space.
+ *
+ * Can output invalid UTF-8, if input is invalid UTF-8. Consumers are supposed
+ * to deal with somehow: either by using byte-strings for JSON, or by running
+ * a "fixup" pass on the input data. The latter could for example change
+ * invalid UTF-8 sequences to replacement characters.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "misc/ctype.h"
+
+#include "json.h"
+
+static bool eat_c(char **s, char c)
+{
+ if (**s == c) {
+ *s += 1;
+ return true;
+ }
+ return false;
+}
+
+static void eat_ws(char **src)
+{
+ while (1) {
+ char c = **src;
+ if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
+ return;
+ *src += 1;
+ }
+}
+
+void json_skip_whitespace(char **src)
+{
+ eat_ws(src);
+}
+
+static int read_id(void *ta_parent, struct mpv_node *dst, char **src)
+{
+ char *start = *src;
+ if (!mp_isalpha(**src) && **src != '_')
+ return -1;
+ while (mp_isalnum(**src) || **src == '_')
+ *src += 1;
+ if (**src == ' ') {
+ **src = '\0'; // we're allowed to mutate it => can avoid the strndup
+ *src += 1;
+ } else {
+ start = talloc_strndup(ta_parent, start, *src - start);
+ }
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = start;
+ return 0;
+}
+
+static int read_str(void *ta_parent, struct mpv_node *dst, char **src)
+{
+ if (!eat_c(src, '"'))
+ return -1; // not a string
+ char *str = *src;
+ char *cur = str;
+ bool has_escapes = false;
+ while (cur[0] && cur[0] != '"') {
+ if (cur[0] == '\\') {
+ has_escapes = true;
+ // skip >\"< and >\\< (latter to handle >\\"< correctly)
+ if (cur[1] == '"' || cur[1] == '\\')
+ cur++;
+ }
+ cur++;
+ }
+ if (cur[0] != '"')
+ return -1; // invalid termination
+ // Mutate input string so we have a null-terminated string to the literal.
+ // This is a stupid micro-optimization, so we can avoid allocation.
+ cur[0] = '\0';
+ *src = cur + 1;
+ if (has_escapes) {
+ bstr unescaped = {0};
+ bstr r = bstr0(str);
+ if (!mp_append_escaped_string(ta_parent, &unescaped, &r))
+ return -1; // broken escapes
+ str = unescaped.start; // the function guarantees null-termination
+ }
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = str;
+ return 0;
+}
+
+static int read_sub(void *ta_parent, struct mpv_node *dst, char **src,
+ int max_depth)
+{
+ bool is_arr = eat_c(src, '[');
+ bool is_obj = !is_arr && eat_c(src, '{');
+ if (!is_arr && !is_obj)
+ return -1; // not an array or object
+ char term = is_obj ? '}' : ']';
+ struct mpv_node_list *list = talloc_zero(ta_parent, struct mpv_node_list);
+ while (1) {
+ eat_ws(src);
+ if (eat_c(src, term))
+ break;
+ if (list->num > 0 && !eat_c(src, ','))
+ return -1; // missing ','
+ eat_ws(src);
+ // non-standard extension: allow a trailing ","
+ if (eat_c(src, term))
+ break;
+ if (is_obj) {
+ struct mpv_node keynode;
+ // non-standard extension: allow unquoted strings as keys
+ if (read_id(list, &keynode, src) < 0 &&
+ read_str(list, &keynode, src) < 0)
+ return -1; // key is not a string
+ eat_ws(src);
+ // non-standard extension: allow "=" instead of ":"
+ if (!eat_c(src, ':') && !eat_c(src, '='))
+ return -1; // ':' missing
+ eat_ws(src);
+ MP_TARRAY_GROW(list, list->keys, list->num);
+ list->keys[list->num] = keynode.u.string;
+ }
+ MP_TARRAY_GROW(list, list->values, list->num);
+ if (json_parse(ta_parent, &list->values[list->num], src, max_depth) < 0)
+ return -1;
+ list->num++;
+ }
+ dst->format = is_obj ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY;
+ dst->u.list = list;
+ return 0;
+}
+
+/* Parse the string in *src as JSON, and write the result into *dst.
+ * max_depth limits the recursion and JSON tree depth.
+ * Warning: this overwrites the input string (what *src points to)!
+ * Returns:
+ * 0: success, *dst is valid, *src points to the end (the caller must check
+ * whether *src really terminates)
+ * -1: failure, *dst is invalid, there may be dead allocs under ta_parent
+ * (ta_free_children(ta_parent) is the only way to free them)
+ * The input string can be mutated in both cases. *dst might contain string
+ * elements, which point into the (mutated) input string.
+ */
+int json_parse(void *ta_parent, struct mpv_node *dst, char **src, int max_depth)
+{
+ max_depth -= 1;
+ if (max_depth < 0)
+ return -1;
+
+ eat_ws(src);
+
+ char c = **src;
+ if (!c)
+ return -1; // early EOF
+ if (c == 'n' && strncmp(*src, "null", 4) == 0) {
+ *src += 4;
+ dst->format = MPV_FORMAT_NONE;
+ return 0;
+ } else if (c == 't' && strncmp(*src, "true", 4) == 0) {
+ *src += 4;
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = 1;
+ return 0;
+ } else if (c == 'f' && strncmp(*src, "false", 5) == 0) {
+ *src += 5;
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = 0;
+ return 0;
+ } else if (c == '"') {
+ return read_str(ta_parent, dst, src);
+ } else if (c == '[' || c == '{') {
+ return read_sub(ta_parent, dst, src, max_depth);
+ } else if (c == '-' || (c >= '0' && c <= '9')) {
+ // The number could be either a float or an int. JSON doesn't make a
+ // difference, but the client API does.
+ char *nsrci = *src, *nsrcf = *src;
+ errno = 0;
+ long long int numi = strtoll(*src, &nsrci, 0);
+ if (errno)
+ nsrci = *src;
+ errno = 0;
+ double numf = strtod(*src, &nsrcf);
+ if (errno)
+ nsrcf = *src;
+ if (nsrci >= nsrcf) {
+ *src = nsrci;
+ dst->format = MPV_FORMAT_INT64; // long long is usually 64 bits
+ dst->u.int64 = numi;
+ return 0;
+ }
+ if (nsrcf > *src && isfinite(numf)) {
+ *src = nsrcf;
+ dst->format = MPV_FORMAT_DOUBLE;
+ dst->u.double_ = numf;
+ return 0;
+ }
+ return -1;
+ }
+ return -1; // character doesn't start a valid token
+}
+
+
+#define APPEND(b, s) bstr_xappend(NULL, (b), bstr0(s))
+
+static const char special_escape[] = {
+ ['\b'] = 'b',
+ ['\f'] = 'f',
+ ['\n'] = 'n',
+ ['\r'] = 'r',
+ ['\t'] = 't',
+};
+
+static void write_json_str(bstr *b, unsigned char *str)
+{
+ APPEND(b, "\"");
+ while (1) {
+ unsigned char *cur = str;
+ while (cur[0] >= 32 && cur[0] != '"' && cur[0] != '\\')
+ cur++;
+ if (!cur[0])
+ break;
+ bstr_xappend(NULL, b, (bstr){str, cur - str});
+ if (cur[0] == '\"') {
+ bstr_xappend(NULL, b, (bstr){"\\\"", 2});
+ } else if (cur[0] == '\\') {
+ bstr_xappend(NULL, b, (bstr){"\\\\", 2});
+ } else if (cur[0] < sizeof(special_escape) && special_escape[cur[0]]) {
+ bstr_xappend_asprintf(NULL, b, "\\%c", special_escape[cur[0]]);
+ } else {
+ bstr_xappend_asprintf(NULL, b, "\\u%04x", (unsigned char)cur[0]);
+ }
+ str = cur + 1;
+ }
+ APPEND(b, str);
+ APPEND(b, "\"");
+}
+
+static void add_indent(bstr *b, int indent)
+{
+ if (indent < 0)
+ return;
+ bstr_xappend(NULL, b, bstr0("\n"));
+ for (int n = 0; n < indent; n++)
+ bstr_xappend(NULL, b, bstr0(" "));
+}
+
+static int json_append(bstr *b, const struct mpv_node *src, int indent)
+{
+ switch (src->format) {
+ case MPV_FORMAT_NONE:
+ APPEND(b, "null");
+ return 0;
+ case MPV_FORMAT_FLAG:
+ APPEND(b, src->u.flag ? "true" : "false");
+ return 0;
+ case MPV_FORMAT_INT64:
+ bstr_xappend_asprintf(NULL, b, "%"PRId64, src->u.int64);
+ return 0;
+ case MPV_FORMAT_DOUBLE: {
+ const char *px = (isfinite(src->u.double_) || indent == 0) ? "" : "\"";
+ bstr_xappend_asprintf(NULL, b, "%s%f%s", px, src->u.double_, px);
+ return 0;
+ }
+ case MPV_FORMAT_STRING:
+ if (indent == 0)
+ APPEND(b, src->u.string);
+ else
+ write_json_str(b, src->u.string);
+ return 0;
+ case MPV_FORMAT_NODE_ARRAY:
+ case MPV_FORMAT_NODE_MAP: {
+ struct mpv_node_list *list = src->u.list;
+ bool is_obj = src->format == MPV_FORMAT_NODE_MAP;
+ APPEND(b, is_obj ? "{" : "[");
+ int next_indent = indent >= 0 ? indent + 1 : -1;
+ for (int n = 0; n < list->num; n++) {
+ if (n)
+ APPEND(b, ",");
+ add_indent(b, next_indent);
+ if (is_obj) {
+ write_json_str(b, list->keys[n]);
+ APPEND(b, ":");
+ }
+ json_append(b, &list->values[n], next_indent);
+ }
+ add_indent(b, indent);
+ APPEND(b, is_obj ? "}" : "]");
+ return 0;
+ }
+ }
+ return -1; // unknown format
+}
+
+static int json_append_str(char **dst, struct mpv_node *src, int indent)
+{
+ bstr buffer = bstr0(*dst);
+ int r = json_append(&buffer, src, indent);
+ *dst = buffer.start;
+ return r;
+}
+
+/* Write the contents of *src as JSON, and append the JSON string to *dst.
+ * This will use strlen() to determine the start offset, and ta_get_size()
+ * and ta_realloc() to extend the memory allocation of *dst.
+ * Returns: 0 on success, <0 on failure.
+ */
+int json_write(char **dst, struct mpv_node *src)
+{
+ return json_append_str(dst, src, -1);
+}
+
+// Same as json_write(), but add whitespace to make it readable.
+int json_write_pretty(char **dst, struct mpv_node *src)
+{
+ return json_append_str(dst, src, 0);
+}
diff --git a/misc/json.h b/misc/json.h
new file mode 100644
index 0000000..b99fc36
--- /dev/null
+++ b/misc/json.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_JSON_H
+#define MP_JSON_H
+
+// We reuse mpv_node.
+#include "libmpv/client.h"
+
+#define MAX_JSON_DEPTH 50
+
+int json_parse(void *ta_parent, struct mpv_node *dst, char **src, int max_depth);
+void json_skip_whitespace(char **src);
+int json_write(char **s, struct mpv_node *src);
+int json_write_pretty(char **s, struct mpv_node *src);
+
+#endif
diff --git a/misc/language.c b/misc/language.c
new file mode 100644
index 0000000..92857f7
--- /dev/null
+++ b/misc/language.c
@@ -0,0 +1,362 @@
+/*
+ * Language code utility functions
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "language.h"
+
+#include "common/common.h"
+#include "osdep/strnlen.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+static const struct lang {
+ char match[4];
+ char canonical[4];
+} langmap[] = {
+ {"aa", "aar"},
+ {"ab", "abk"},
+ {"ae", "ave"},
+ {"af", "afr"},
+ {"ak", "aka"},
+ {"am", "amh"},
+ {"an", "arg"},
+ {"ar", "ara"},
+ {"as", "asm"},
+ {"av", "ava"},
+ {"ay", "aym"},
+ {"az", "aze"},
+ {"ba", "bak"},
+ {"be", "bel"},
+ {"bg", "bul"},
+ {"bh", "bih"},
+ {"bi", "bis"},
+ {"bm", "bam"},
+ {"bn", "ben"},
+ {"bo", "tib"},
+ {"bod", "tib"},
+ {"br", "bre"},
+ {"bs", "bos"},
+ {"ca", "cat"},
+ {"ce", "che"},
+ {"ces", "cze"},
+ {"ch", "cha"},
+ {"co", "cos"},
+ {"cr", "cre"},
+ {"cs", "cze"},
+ {"cu", "chu"},
+ {"cv", "chv"},
+ {"cy", "wel"},
+ {"cym", "wel"},
+ {"da", "dan"},
+ {"de", "ger"},
+ {"deu", "ger"},
+ {"dv", "div"},
+ {"dz", "dzo"},
+ {"ee", "ewe"},
+ {"el", "gre"},
+ {"ell", "gre"},
+ {"en", "eng"},
+ {"eo", "epo"},
+ {"es", "spa"},
+ {"et", "est"},
+ {"eu", "baq"},
+ {"eus", "baq"},
+ {"fa", "per"},
+ {"fas", "per"},
+ {"ff", "ful"},
+ {"fi", "fin"},
+ {"fj", "fij"},
+ {"fo", "fao"},
+ {"fr", "fre"},
+ {"fra", "fre"},
+ {"fy", "fry"},
+ {"ga", "gle"},
+ {"gd", "gla"},
+ {"gl", "glg"},
+ {"gn", "grn"},
+ {"gu", "guj"},
+ {"gv", "glv"},
+ {"ha", "hau"},
+ {"he", "heb"},
+ {"hi", "hin"},
+ {"ho", "hmo"},
+ {"hr", "hrv"},
+ {"ht", "hat"},
+ {"hu", "hun"},
+ {"hy", "arm"},
+ {"hye", "arm"},
+ {"hz", "her"},
+ {"ia", "ina"},
+ {"id", "ind"},
+ {"ie", "ile"},
+ {"ig", "ibo"},
+ {"ii", "iii"},
+ {"ik", "ipk"},
+ {"io", "ido"},
+ {"is", "ice"},
+ {"isl", "ice"},
+ {"it", "ita"},
+ {"iu", "iku"},
+ {"ja", "jpn"},
+ {"jv", "jav"},
+ {"ka", "geo"},
+ {"kat", "geo"},
+ {"kg", "kon"},
+ {"ki", "kik"},
+ {"kj", "kua"},
+ {"kk", "kaz"},
+ {"kl", "kal"},
+ {"km", "khm"},
+ {"kn", "kan"},
+ {"ko", "kor"},
+ {"kr", "kau"},
+ {"ks", "kas"},
+ {"ku", "kur"},
+ {"kv", "kom"},
+ {"kw", "cor"},
+ {"ky", "kir"},
+ {"la", "lat"},
+ {"lb", "ltz"},
+ {"lg", "lug"},
+ {"li", "lim"},
+ {"ln", "lin"},
+ {"lo", "lao"},
+ {"lt", "lit"},
+ {"lu", "lub"},
+ {"lv", "lav"},
+ {"mg", "mlg"},
+ {"mh", "mah"},
+ {"mi", "mao"},
+ {"mk", "mac"},
+ {"mkd", "mac"},
+ {"ml", "mal"},
+ {"mn", "mon"},
+ {"mr", "mar"},
+ {"mri", "mao"},
+ {"ms", "may"},
+ {"msa", "may"},
+ {"mt", "mlt"},
+ {"my", "bur"},
+ {"mya", "bur"},
+ {"na", "nau"},
+ {"nb", "nob"},
+ {"nd", "nde"},
+ {"ne", "nep"},
+ {"ng", "ndo"},
+ {"nl", "dut"},
+ {"nld", "dut"},
+ {"nn", "nno"},
+ {"no", "nor"},
+ {"nr", "nbl"},
+ {"nv", "nav"},
+ {"ny", "nya"},
+ {"oc", "oci"},
+ {"oj", "oji"},
+ {"om", "orm"},
+ {"or", "ori"},
+ {"os", "oss"},
+ {"pa", "pan"},
+ {"pi", "pli"},
+ {"pl", "pol"},
+ {"ps", "pus"},
+ {"pt", "por"},
+ {"qu", "que"},
+ {"rm", "roh"},
+ {"rn", "run"},
+ {"ro", "rum"},
+ {"ron", "rum"},
+ {"ru", "rus"},
+ {"rw", "kin"},
+ {"sa", "san"},
+ {"sc", "srd"},
+ {"sd", "snd"},
+ {"se", "sme"},
+ {"sg", "sag"},
+ {"si", "sin"},
+ {"sk", "slo"},
+ {"sl", "slv"},
+ {"slk", "slo"},
+ {"sm", "smo"},
+ {"sn", "sna"},
+ {"so", "som"},
+ {"sq", "alb"},
+ {"sqi", "alb"},
+ {"sr", "srp"},
+ {"ss", "ssw"},
+ {"st", "sot"},
+ {"su", "sun"},
+ {"sv", "swe"},
+ {"sw", "swa"},
+ {"ta", "tam"},
+ {"te", "tel"},
+ {"tg", "tgk"},
+ {"th", "tha"},
+ {"ti", "tir"},
+ {"tk", "tuk"},
+ {"tl", "tgl"},
+ {"tn", "tsn"},
+ {"to", "ton"},
+ {"tr", "tur"},
+ {"ts", "tso"},
+ {"tt", "tat"},
+ {"tw", "twi"},
+ {"ty", "tah"},
+ {"ug", "uig"},
+ {"uk", "ukr"},
+ {"ur", "urd"},
+ {"uz", "uzb"},
+ {"ve", "ven"},
+ {"vi", "vie"},
+ {"vo", "vol"},
+ {"wa", "wln"},
+ {"wo", "wol"},
+ {"xh", "xho"},
+ {"yi", "yid"},
+ {"yo", "yor"},
+ {"za", "zha"},
+ {"zh", "chi"},
+ {"zho", "chi"},
+ {"zu", "zul"},
+};
+
+struct langsearch {
+ const char *str;
+ size_t size;
+};
+
+static int lang_compare(const void *s, const void *k)
+{
+ const struct langsearch *search = s;
+ const struct lang *key = k;
+
+ int ret = strncasecmp(search->str, key->match, search->size);
+ if (!ret && search->size < sizeof(key->match) && key->match[search->size])
+ return 1;
+ return ret;
+}
+
+static void canonicalize(const char **lang, size_t *size)
+{
+ if (*size > sizeof(langmap[0].match))
+ return;
+
+ struct langsearch search = {*lang, *size};
+ struct lang *l = bsearch(&search, langmap, MP_ARRAY_SIZE(langmap), sizeof(langmap[0]),
+ &lang_compare);
+
+ if (l) {
+ *lang = l->canonical;
+ *size = strnlen(l->canonical, sizeof(l->canonical));
+ }
+}
+
+static bool tag_matches(const char *l1, size_t s1, const char *l2, size_t s2)
+{
+ return s1 == s2 && !strncasecmp(l1, l2, s1);
+}
+
+int mp_match_lang_single(const char *l1, const char *l2)
+{
+ // We never consider null or empty strings to match
+ if (!l1 || !l2 || !*l1 || !*l2)
+ return 0;
+
+ // The first subtag should always be a language; canonicalize to 3-letter ISO 639-2B (arbitrarily chosen)
+ size_t s1 = strcspn(l1, "-_");
+ size_t s2 = strcspn(l2, "-_");
+
+ const char *l1c = l1;
+ const char *l2c = l2;
+ size_t s1c = s1;
+ size_t s2c = s2;
+
+ canonicalize(&l1c, &s1c);
+ canonicalize(&l2c, &s2c);
+
+ // If the first subtags don't match, we have no match at all
+ if (!tag_matches(l1c, s1c, l2c, s2c))
+ return 0;
+
+ // Attempt to match each subtag in each string against each in the other
+ int score = 1;
+ bool x1 = false;
+ int count = 0;
+ for (;;) {
+ l1 += s1;
+
+ while (*l1 == '-' || *l1 == '_')
+ l1++;
+
+ if (!*l1)
+ break;
+
+ s1 = strcspn(l1, "-_");
+ if (tag_matches(l1, s1, "x", 1)) {
+ x1 = true;
+ continue;
+ }
+
+ const char *l2o = l2;
+ size_t s2o = s2;
+ bool x2 = false;
+ for (;;) {
+ l2 += s2;
+
+ while (*l2 == '-' || *l2 == '_')
+ l2++;
+
+ if (!*l2)
+ break;
+
+ s2 = strcspn(l2, "-_");
+ if (tag_matches(l2, s2, "x", 1)) {
+ x2 = true;
+ if (!x1)
+ break;
+ continue;
+ }
+
+ // Private-use subtags only match against other private-use subtags
+ if (x1 && !x2)
+ continue;
+
+ if (tag_matches(l1c, s1c, l2c, s2c)) {
+ // Matches for subtags earlier in the user's string take priority over later ones,
+ // for up to LANGUAGE_SCORE_BITS subtags
+ int shift = (LANGUAGE_SCORE_BITS - count - 1);
+ if (shift < 0)
+ shift = 0;
+ score += (1 << shift);
+
+ if (score >= LANGUAGE_SCORE_MAX)
+ return LANGUAGE_SCORE_MAX;
+ }
+ }
+
+ l2 = l2o;
+ s2 = s2o;
+
+ count++;
+ }
+
+ return score;
+}
diff --git a/misc/language.h b/misc/language.h
new file mode 100644
index 0000000..250d391
--- /dev/null
+++ b/misc/language.h
@@ -0,0 +1,31 @@
+/*
+ * Language code utility functions
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_LANGUAGE_H
+#define MP_LANGUAGE_H
+
+#define LANGUAGE_SCORE_BITS 16
+#define LANGUAGE_SCORE_MAX (1 << LANGUAGE_SCORE_BITS)
+
+// Where applicable, l1 is the user-specified code and l2 is the code being checked against it
+int mp_match_lang_single(const char *l1, const char *l2);
+
+char **mp_get_user_langs(void);
+
+#endif /* MP_LANGUAGE_H */
diff --git a/misc/linked_list.h b/misc/linked_list.h
new file mode 100644
index 0000000..b43b227
--- /dev/null
+++ b/misc/linked_list.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include <stddef.h>
+
+/*
+ * Doubly linked list macros. All of these require that each list item is a
+ * struct, that contains a field, that is another struct with prev/next fields:
+ *
+ * struct example_item {
+ * struct {
+ * struct example_item *prev, *next;
+ * } mylist;
+ * };
+ *
+ * And a struct somewhere that represents the "list" and has head/tail fields:
+ *
+ * struct {
+ * struct example_item *head, *tail;
+ * } mylist_var;
+ *
+ * Then you can e.g. insert elements like this:
+ *
+ * struct example_item item;
+ * LL_APPEND(mylist, &mylist_var, &item);
+ *
+ * The first macro argument is always the name if the field in the item that
+ * contains the prev/next pointers, in this case struct example_item.mylist.
+ * This was done so that a single item can be in multiple lists.
+ *
+ * The list is started/terminated with NULL. Nothing ever points _to_ the
+ * list head, so the list head memory location can be safely moved.
+ *
+ * General rules are:
+ * - list head is initialized by setting head/tail to NULL
+ * - list items do not need to be initialized before inserting them
+ * - next/prev fields of list items are not cleared when they are removed
+ * - there's no way to know whether an item is in the list or not (unless
+ * you clear prev/next on init/removal, _and_ check whether items with
+ * prev/next==NULL are referenced by head/tail)
+ */
+
+// Insert item at the end of the list (list->tail == item).
+// Undefined behavior if item is already in the list.
+#define LL_APPEND(field, list, item) do { \
+ (item)->field.prev = (list)->tail; \
+ (item)->field.next = NULL; \
+ LL_RELINK_(field, list, item) \
+} while (0)
+
+// Insert item enew after eprev (i.e. eprev->next == enew). If eprev is NULL,
+// then insert it as head (list->head == enew).
+// Undefined behavior if enew is already in the list, or eprev isn't.
+#define LL_INSERT_AFTER(field, list, eprev, enew) do { \
+ (enew)->field.prev = (eprev); \
+ (enew)->field.next = (eprev) ? (eprev)->field.next : (list)->head; \
+ LL_RELINK_(field, list, enew) \
+} while (0)
+
+// Insert item at the start of the list (list->head == item).
+// Undefined behavior if item is already in the list.
+#define LL_PREPEND(field, list, item) do { \
+ (item)->field.prev = NULL; \
+ (item)->field.next = (list)->head; \
+ LL_RELINK_(field, list, item) \
+} while (0)
+
+// Insert item enew before enext (i.e. enew->next == enext). If enext is NULL,
+// then insert it as tail (list->tail == enew).
+// Undefined behavior if enew is already in the list, or enext isn't.
+#define LL_INSERT_BEFORE(field, list, enext, enew) do { \
+ (enew)->field.prev = (enext) ? (enext)->field.prev : (list)->tail; \
+ (enew)->field.next = (enext); \
+ LL_RELINK_(field, list, enew) \
+} while (0)
+
+// Remove the item from the list.
+// Undefined behavior if item is not in the list.
+#define LL_REMOVE(field, list, item) do { \
+ if ((item)->field.prev) { \
+ (item)->field.prev->field.next = (item)->field.next; \
+ } else { \
+ (list)->head = (item)->field.next; \
+ } \
+ if ((item)->field.next) { \
+ (item)->field.next->field.prev = (item)->field.prev; \
+ } else { \
+ (list)->tail = (item)->field.prev; \
+ } \
+} while (0)
+
+// Remove all items from the list.
+#define LL_CLEAR(field, list) do { \
+ (list)->head = (list)->tail = NULL; \
+} while (0)
+
+// Internal helper.
+#define LL_RELINK_(field, list, item) \
+ if ((item)->field.prev) { \
+ (item)->field.prev->field.next = (item); \
+ } else { \
+ (list)->head = (item); \
+ } \
+ if ((item)->field.next) { \
+ (item)->field.next->field.prev = (item); \
+ } else { \
+ (list)->tail = (item); \
+ }
diff --git a/misc/natural_sort.c b/misc/natural_sort.c
new file mode 100644
index 0000000..3e0bab0
--- /dev/null
+++ b/misc/natural_sort.c
@@ -0,0 +1,67 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc/ctype.h"
+
+#include "natural_sort.h"
+
+// Comparison function for an ASCII-only "natural" sort. Case is ignored and
+// numbers are ordered by value regardless of padding. Two filenames that differ
+// only in the padding of numbers will be considered equal and end up in
+// arbitrary order. Bytes outside of A-Z/a-z/0-9 will by sorted by byte value.
+int mp_natural_sort_cmp(const char *name1, const char *name2)
+{
+ while (name1[0] && name2[0]) {
+ if (mp_isdigit(name1[0]) && mp_isdigit(name2[0])) {
+ while (name1[0] == '0')
+ name1++;
+ while (name2[0] == '0')
+ name2++;
+ const char *end1 = name1, *end2 = name2;
+ while (mp_isdigit(*end1))
+ end1++;
+ while (mp_isdigit(*end2))
+ end2++;
+ // With padding stripped, a number with more digits is bigger.
+ if ((end1 - name1) < (end2 - name2))
+ return -1;
+ if ((end1 - name1) > (end2 - name2))
+ return 1;
+ // Same length, lexicographical works.
+ while (name1 < end1) {
+ if (name1[0] < name2[0])
+ return -1;
+ if (name1[0] > name2[0])
+ return 1;
+ name1++;
+ name2++;
+ }
+ } else {
+ if (mp_tolower(name1[0]) < mp_tolower(name2[0]))
+ return -1;
+ if (mp_tolower(name1[0]) > mp_tolower(name2[0]))
+ return 1;
+ name1++;
+ name2++;
+ }
+ }
+ if (name2[0])
+ return -1;
+ if (name1[0])
+ return 1;
+ return 0;
+}
diff --git a/misc/natural_sort.h b/misc/natural_sort.h
new file mode 100644
index 0000000..47b9a7a
--- /dev/null
+++ b/misc/natural_sort.h
@@ -0,0 +1,23 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_NATURAL_SORT_H
+#define MP_NATURAL_SORT_H
+
+int mp_natural_sort_cmp(const char *name1, const char *name2);
+
+#endif
diff --git a/misc/node.c b/misc/node.c
new file mode 100644
index 0000000..5bf3211
--- /dev/null
+++ b/misc/node.c
@@ -0,0 +1,159 @@
+#include "common/common.h"
+
+#include "node.h"
+
+// Init a node with the given format. If parent is not NULL, it is set as
+// parent allocation according to m_option_type_node rules (which means
+// the mpv_node_list allocs are used for chaining the TA allocations).
+// format == MPV_FORMAT_NONE will simply initialize it with all-0.
+void node_init(struct mpv_node *dst, int format, struct mpv_node *parent)
+{
+ // Other formats need to be initialized manually.
+ assert(format == MPV_FORMAT_NODE_MAP || format == MPV_FORMAT_NODE_ARRAY ||
+ format == MPV_FORMAT_FLAG || format == MPV_FORMAT_INT64 ||
+ format == MPV_FORMAT_DOUBLE || format == MPV_FORMAT_BYTE_ARRAY ||
+ format == MPV_FORMAT_NONE);
+
+ void *ta_parent = NULL;
+ if (parent) {
+ assert(parent->format == MPV_FORMAT_NODE_MAP ||
+ parent->format == MPV_FORMAT_NODE_ARRAY);
+ ta_parent = parent->u.list;
+ }
+
+ *dst = (struct mpv_node){ .format = format };
+ if (format == MPV_FORMAT_NODE_MAP || format == MPV_FORMAT_NODE_ARRAY)
+ dst->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ if (format == MPV_FORMAT_BYTE_ARRAY)
+ dst->u.ba = talloc_zero(ta_parent, struct mpv_byte_array);
+}
+
+// Add an entry to a MPV_FORMAT_NODE_ARRAY.
+// m_option_type_node memory management rules apply.
+struct mpv_node *node_array_add(struct mpv_node *dst, int format)
+{
+ struct mpv_node_list *list = dst->u.list;
+ assert(dst->format == MPV_FORMAT_NODE_ARRAY && dst->u.list);
+ MP_TARRAY_GROW(list, list->values, list->num);
+ node_init(&list->values[list->num], format, dst);
+ return &list->values[list->num++];
+}
+
+// Add an entry to a MPV_FORMAT_NODE_MAP. Keep in mind that this does
+// not check for already existing entries under the same key.
+// m_option_type_node memory management rules apply.
+struct mpv_node *node_map_add(struct mpv_node *dst, const char *key, int format)
+{
+ assert(key);
+ return node_map_badd(dst, bstr0(key), format);
+}
+
+struct mpv_node *node_map_badd(struct mpv_node *dst, struct bstr key, int format)
+{
+ assert(key.start);
+
+ struct mpv_node_list *list = dst->u.list;
+ assert(dst->format == MPV_FORMAT_NODE_MAP && dst->u.list);
+ MP_TARRAY_GROW(list, list->values, list->num);
+ MP_TARRAY_GROW(list, list->keys, list->num);
+ list->keys[list->num] = bstrdup0(list, key);
+ node_init(&list->values[list->num], format, dst);
+ return &list->values[list->num++];
+}
+
+// Add a string entry to a MPV_FORMAT_NODE_MAP. Keep in mind that this does
+// not check for already existing entries under the same key.
+// m_option_type_node memory management rules apply.
+void node_map_add_string(struct mpv_node *dst, const char *key, const char *val)
+{
+ assert(val);
+
+ struct mpv_node *entry = node_map_add(dst, key, MPV_FORMAT_NONE);
+ entry->format = MPV_FORMAT_STRING;
+ entry->u.string = talloc_strdup(dst->u.list, val);
+}
+
+void node_map_add_int64(struct mpv_node *dst, const char *key, int64_t v)
+{
+ node_map_add(dst, key, MPV_FORMAT_INT64)->u.int64 = v;
+}
+
+void node_map_add_double(struct mpv_node *dst, const char *key, double v)
+{
+ node_map_add(dst, key, MPV_FORMAT_DOUBLE)->u.double_ = v;
+}
+
+void node_map_add_flag(struct mpv_node *dst, const char *key, bool v)
+{
+ node_map_add(dst, key, MPV_FORMAT_FLAG)->u.flag = v;
+}
+
+mpv_node *node_map_get(mpv_node *src, const char *key)
+{
+ return node_map_bget(src, bstr0(key));
+}
+
+mpv_node *node_map_bget(mpv_node *src, struct bstr key)
+{
+ if (src->format != MPV_FORMAT_NODE_MAP)
+ return NULL;
+
+ for (int i = 0; i < src->u.list->num; i++) {
+ if (bstr_equals0(key, src->u.list->keys[i]))
+ return &src->u.list->values[i];
+ }
+
+ return NULL;
+}
+
+// Note: for MPV_FORMAT_NODE_MAP, this (incorrectly) takes the order into
+// account, instead of treating it as set.
+bool equal_mpv_value(const void *a, const void *b, mpv_format format)
+{
+ switch (format) {
+ case MPV_FORMAT_NONE:
+ return true;
+ case MPV_FORMAT_STRING:
+ case MPV_FORMAT_OSD_STRING:
+ return strcmp(*(char **)a, *(char **)b) == 0;
+ case MPV_FORMAT_FLAG:
+ return *(int *)a == *(int *)b;
+ case MPV_FORMAT_INT64:
+ return *(int64_t *)a == *(int64_t *)b;
+ case MPV_FORMAT_DOUBLE:
+ return *(double *)a == *(double *)b;
+ case MPV_FORMAT_NODE:
+ return equal_mpv_node(a, b);
+ case MPV_FORMAT_BYTE_ARRAY: {
+ const struct mpv_byte_array *a_r = a, *b_r = b;
+ if (a_r->size != b_r->size)
+ return false;
+ return memcmp(a_r->data, b_r->data, a_r->size) == 0;
+ }
+ case MPV_FORMAT_NODE_ARRAY:
+ case MPV_FORMAT_NODE_MAP:
+ {
+ mpv_node_list *l_a = *(mpv_node_list **)a, *l_b = *(mpv_node_list **)b;
+ if (l_a->num != l_b->num)
+ return false;
+ for (int n = 0; n < l_a->num; n++) {
+ if (format == MPV_FORMAT_NODE_MAP) {
+ if (strcmp(l_a->keys[n], l_b->keys[n]) != 0)
+ return false;
+ }
+ if (!equal_mpv_node(&l_a->values[n], &l_b->values[n]))
+ return false;
+ }
+ return true;
+ }
+ }
+ MP_ASSERT_UNREACHABLE(); // supposed to be able to handle all defined types
+}
+
+// Remarks see equal_mpv_value().
+bool equal_mpv_node(const struct mpv_node *a, const struct mpv_node *b)
+{
+ if (a->format != b->format)
+ return false;
+ return equal_mpv_value(&a->u, &b->u, a->format);
+}
diff --git a/misc/node.h b/misc/node.h
new file mode 100644
index 0000000..688b0a8
--- /dev/null
+++ b/misc/node.h
@@ -0,0 +1,20 @@
+#ifndef MP_MISC_NODE_H_
+#define MP_MISC_NODE_H_
+
+#include "libmpv/client.h"
+#include "misc/bstr.h"
+
+void node_init(struct mpv_node *dst, int format, struct mpv_node *parent);
+struct mpv_node *node_array_add(struct mpv_node *dst, int format);
+struct mpv_node *node_map_add(struct mpv_node *dst, const char *key, int format);
+struct mpv_node *node_map_badd(struct mpv_node *dst, struct bstr key, int format);
+void node_map_add_string(struct mpv_node *dst, const char *key, const char *val);
+void node_map_add_int64(struct mpv_node *dst, const char *key, int64_t v);
+void node_map_add_double(struct mpv_node *dst, const char *key, double v);
+void node_map_add_flag(struct mpv_node *dst, const char *key, bool v);
+mpv_node *node_map_get(mpv_node *src, const char *key);
+mpv_node *node_map_bget(mpv_node *src, struct bstr key);
+bool equal_mpv_value(const void *a, const void *b, mpv_format format);
+bool equal_mpv_node(const struct mpv_node *a, const struct mpv_node *b);
+
+#endif
diff --git a/misc/random.c b/misc/random.c
new file mode 100644
index 0000000..e622ab7
--- /dev/null
+++ b/misc/random.c
@@ -0,0 +1,75 @@
+/*
+ * Implementation of non-cryptographic pseudo-random number
+ * generator algorithm known as xoshiro.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+
+#include "osdep/threads.h"
+#include "random.h"
+
+static uint64_t state[4];
+static mp_static_mutex state_mutex = MP_STATIC_MUTEX_INITIALIZER;
+
+static inline uint64_t rotl_u64(const uint64_t x, const int k)
+{
+ return (x << k) | (x >> (64 - k));
+}
+
+static inline uint64_t splitmix64(uint64_t *const x)
+{
+ uint64_t z = (*x += UINT64_C(0x9e3779b97f4a7c15));
+ z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
+ z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb);
+ return z ^ (z >> 31);
+}
+
+void mp_rand_seed(uint64_t seed)
+{
+ mp_mutex_lock(&state_mutex);
+ state[0] = seed;
+ for (int i = 1; i < 4; i++)
+ state[i] = splitmix64(&seed);
+ mp_mutex_unlock(&state_mutex);
+}
+
+uint64_t mp_rand_next(void)
+{
+ uint64_t result, t;
+
+ mp_mutex_lock(&state_mutex);
+
+ result = rotl_u64(state[1] * 5, 7) * 9;
+ t = state[1] << 17;
+
+ state[2] ^= state[0];
+ state[3] ^= state[1];
+ state[1] ^= state[2];
+ state[0] ^= state[3];
+ state[2] ^= t;
+ state[3] = rotl_u64(state[3], 45);
+
+ mp_mutex_unlock(&state_mutex);
+
+ return result;
+}
+
+double mp_rand_next_double(void)
+{
+ return (mp_rand_next() >> 11) * 0x1.0p-53;
+}
diff --git a/misc/random.h b/misc/random.h
new file mode 100644
index 0000000..dae66a0
--- /dev/null
+++ b/misc/random.h
@@ -0,0 +1,41 @@
+/*
+ * Implementation of non-cryptographic pseudo-random number
+ * generator algorithm known as xoshiro.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+/*
+ * Initialize the pseudo-random number generator's state with
+ * the given 64-bit seed.
+ */
+void mp_rand_seed(uint64_t seed);
+
+/*
+ * Return the next 64-bit pseudo-random integer, and update the state
+ * accordingly.
+ */
+uint64_t mp_rand_next(void);
+
+/*
+ * Return a double value in the range of [0.0, 1.0) with uniform
+ * distribution, and update the state accordingly.
+ */
+double mp_rand_next_double(void);
diff --git a/misc/rendezvous.c b/misc/rendezvous.c
new file mode 100644
index 0000000..1fe5724
--- /dev/null
+++ b/misc/rendezvous.c
@@ -0,0 +1,55 @@
+
+#include "rendezvous.h"
+
+#include "osdep/threads.h"
+
+static mp_static_mutex lock = MP_STATIC_MUTEX_INITIALIZER;
+static mp_cond wakeup = MP_STATIC_COND_INITIALIZER;
+
+static struct waiter *waiters;
+
+struct waiter {
+ void *tag;
+ struct waiter *next;
+ intptr_t *value;
+};
+
+/* A barrier for 2 threads, which can exchange a value when they meet.
+ * The first thread to call this function will block. As soon as two threads
+ * are calling this function with the same tag value, they will unblock, and
+ * on each thread the call returns the value parameter of the _other_ thread.
+ *
+ * tag is an arbitrary value, but it must be an unique pointer. If there are
+ * more than 2 threads using the same tag, things won't work. Typically, it
+ * will have to point to a memory allocation or to the stack, while pointing
+ * it to static data is always a bug.
+ *
+ * This shouldn't be used for performance critical code (uses a linked list
+ * of _all_ waiters in the process, and temporarily wakes up _all_ waiters on
+ * each second call).
+ *
+ * This is inspired by: http://9atom.org/magic/man2html/2/rendezvous */
+intptr_t mp_rendezvous(void *tag, intptr_t value)
+{
+ struct waiter wait = { .tag = tag, .value = &value };
+ mp_mutex_lock(&lock);
+ struct waiter **prev = &waiters;
+ while (*prev) {
+ if ((*prev)->tag == tag) {
+ intptr_t tmp = *(*prev)->value;
+ *(*prev)->value = value;
+ value = tmp;
+ (*prev)->value = NULL; // signals completion
+ *prev = (*prev)->next; // unlink
+ mp_cond_broadcast(&wakeup);
+ goto done;
+ }
+ prev = &(*prev)->next;
+ }
+ *prev = &wait;
+ while (wait.value)
+ mp_cond_wait(&wakeup, &lock);
+done:
+ mp_mutex_unlock(&lock);
+ return value;
+}
diff --git a/misc/rendezvous.h b/misc/rendezvous.h
new file mode 100644
index 0000000..ffcc89a
--- /dev/null
+++ b/misc/rendezvous.h
@@ -0,0 +1,8 @@
+#ifndef MP_RENDEZVOUS_H_
+#define MP_RENDEZVOUS_H_
+
+#include <stdint.h>
+
+intptr_t mp_rendezvous(void *tag, intptr_t value);
+
+#endif
diff --git a/misc/thread_pool.c b/misc/thread_pool.c
new file mode 100644
index 0000000..e20d9d0
--- /dev/null
+++ b/misc/thread_pool.c
@@ -0,0 +1,223 @@
+/* Copyright (C) 2018 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "common/common.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "thread_pool.h"
+
+// Threads destroy themselves after this many seconds, if there's no new work
+// and the thread count is above the configured minimum.
+#define DESTROY_TIMEOUT 10
+
+struct work {
+ void (*fn)(void *ctx);
+ void *fn_ctx;
+};
+
+struct mp_thread_pool {
+ int min_threads, max_threads;
+
+ mp_mutex lock;
+ mp_cond wakeup;
+
+ // --- the following fields are protected by lock
+
+ mp_thread *threads;
+ int num_threads;
+
+ // Number of threads which have taken up work and are still processing it.
+ int busy_threads;
+
+ bool terminate;
+
+ struct work *work;
+ int num_work;
+};
+
+static MP_THREAD_VOID worker_thread(void *arg)
+{
+ struct mp_thread_pool *pool = arg;
+
+ mp_thread_set_name("worker");
+
+ mp_mutex_lock(&pool->lock);
+
+ int64_t destroy_deadline = 0;
+ bool got_timeout = false;
+ while (1) {
+ struct work work = {0};
+ if (pool->num_work > 0) {
+ work = pool->work[pool->num_work - 1];
+ pool->num_work -= 1;
+ }
+
+ if (!work.fn) {
+ if (got_timeout || pool->terminate)
+ break;
+
+ if (pool->num_threads > pool->min_threads) {
+ if (!destroy_deadline)
+ destroy_deadline = mp_time_ns() + MP_TIME_S_TO_NS(DESTROY_TIMEOUT);
+ if (mp_cond_timedwait_until(&pool->wakeup, &pool->lock, destroy_deadline))
+ got_timeout = pool->num_threads > pool->min_threads;
+ } else {
+ mp_cond_wait(&pool->wakeup, &pool->lock);
+ }
+ continue;
+ }
+
+ pool->busy_threads += 1;
+ mp_mutex_unlock(&pool->lock);
+
+ work.fn(work.fn_ctx);
+
+ mp_mutex_lock(&pool->lock);
+ pool->busy_threads -= 1;
+
+ destroy_deadline = 0;
+ got_timeout = false;
+ }
+
+ // If no termination signal was given, it must mean we died because of a
+ // timeout, and nobody is waiting for us. We have to remove ourselves.
+ if (!pool->terminate) {
+ for (int n = 0; n < pool->num_threads; n++) {
+ if (mp_thread_id_equal(mp_thread_get_id(pool->threads[n]),
+ mp_thread_current_id()))
+ {
+ mp_thread_detach(pool->threads[n]);
+ MP_TARRAY_REMOVE_AT(pool->threads, pool->num_threads, n);
+ mp_mutex_unlock(&pool->lock);
+ MP_THREAD_RETURN();
+ }
+ }
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ mp_mutex_unlock(&pool->lock);
+ MP_THREAD_RETURN();
+}
+
+static void thread_pool_dtor(void *ctx)
+{
+ struct mp_thread_pool *pool = ctx;
+
+
+ mp_mutex_lock(&pool->lock);
+
+ pool->terminate = true;
+ mp_cond_broadcast(&pool->wakeup);
+
+ mp_thread *threads = pool->threads;
+ int num_threads = pool->num_threads;
+
+ pool->threads = NULL;
+ pool->num_threads = 0;
+
+ mp_mutex_unlock(&pool->lock);
+
+ for (int n = 0; n < num_threads; n++)
+ mp_thread_join(threads[n]);
+
+ assert(pool->num_work == 0);
+ assert(pool->num_threads == 0);
+ mp_cond_destroy(&pool->wakeup);
+ mp_mutex_destroy(&pool->lock);
+}
+
+static bool add_thread(struct mp_thread_pool *pool)
+{
+ mp_thread thread;
+
+ if (mp_thread_create(&thread, worker_thread, pool) != 0)
+ return false;
+
+ MP_TARRAY_APPEND(pool, pool->threads, pool->num_threads, thread);
+ return true;
+}
+
+struct mp_thread_pool *mp_thread_pool_create(void *ta_parent, int init_threads,
+ int min_threads, int max_threads)
+{
+ assert(min_threads >= 0);
+ assert(init_threads <= min_threads);
+ assert(max_threads > 0 && max_threads >= min_threads);
+
+ struct mp_thread_pool *pool = talloc_zero(ta_parent, struct mp_thread_pool);
+ talloc_set_destructor(pool, thread_pool_dtor);
+
+ mp_mutex_init(&pool->lock);
+ mp_cond_init(&pool->wakeup);
+
+ pool->min_threads = min_threads;
+ pool->max_threads = max_threads;
+
+ mp_mutex_lock(&pool->lock);
+ for (int n = 0; n < init_threads; n++)
+ add_thread(pool);
+ bool ok = pool->num_threads >= init_threads;
+ mp_mutex_unlock(&pool->lock);
+
+ if (!ok)
+ TA_FREEP(&pool);
+
+ return pool;
+}
+
+static bool thread_pool_add(struct mp_thread_pool *pool, void (*fn)(void *ctx),
+ void *fn_ctx, bool allow_queue)
+{
+ bool ok = true;
+
+ assert(fn);
+
+ mp_mutex_lock(&pool->lock);
+ struct work work = {fn, fn_ctx};
+
+ // If there are not enough threads to process all at once, but we can
+ // create a new thread, then do so. If work is queued quickly, it can
+ // happen that not all available threads have picked up work yet (up to
+ // num_threads - busy_threads threads), which has to be accounted for.
+ if (pool->busy_threads + pool->num_work + 1 > pool->num_threads &&
+ pool->num_threads < pool->max_threads)
+ {
+ if (!add_thread(pool)) {
+ // If we can queue it, it'll get done as long as there is 1 thread.
+ ok = allow_queue && pool->num_threads > 0;
+ }
+ }
+
+ if (ok) {
+ MP_TARRAY_INSERT_AT(pool, pool->work, pool->num_work, 0, work);
+ mp_cond_signal(&pool->wakeup);
+ }
+
+ mp_mutex_unlock(&pool->lock);
+ return ok;
+}
+
+bool mp_thread_pool_queue(struct mp_thread_pool *pool, void (*fn)(void *ctx),
+ void *fn_ctx)
+{
+ return thread_pool_add(pool, fn, fn_ctx, true);
+}
+
+bool mp_thread_pool_run(struct mp_thread_pool *pool, void (*fn)(void *ctx),
+ void *fn_ctx)
+{
+ return thread_pool_add(pool, fn, fn_ctx, false);
+}
diff --git a/misc/thread_pool.h b/misc/thread_pool.h
new file mode 100644
index 0000000..39106ee
--- /dev/null
+++ b/misc/thread_pool.h
@@ -0,0 +1,35 @@
+#ifndef MPV_MP_THREAD_POOL_H
+#define MPV_MP_THREAD_POOL_H
+
+#include <stdbool.h>
+struct mp_thread_pool;
+
+// Create a thread pool with the given number of worker threads. This can return
+// NULL if the worker threads could not be created. The thread pool can be
+// destroyed with talloc_free(pool), or indirectly with talloc_free(ta_parent).
+// If there are still work items on freeing, it will block until all work items
+// are done, and the threads terminate.
+// init_threads is the number of threads created in this function (and it fails
+// if it could not be done). min_threads must be >=, if it's >, then the
+// remaining threads will be created on demand, but never destroyed.
+// If init_threads > 0, then mp_thread_pool_queue() can never fail.
+// If init_threads == 0, mp_thread_pool_create() itself can never fail.
+struct mp_thread_pool *mp_thread_pool_create(void *ta_parent, int init_threads,
+ int min_threads, int max_threads);
+
+// Queue a function to be run on a worker thread: fn(fn_ctx)
+// If no worker thread is currently available, it's appended to a list in memory
+// with unbounded size. This function always returns immediately.
+// Concurrent queue calls are allowed, as long as it does not overlap with
+// pool destruction.
+// This function is explicitly thread-safe.
+// Cannot fail if thread pool was created with at least 1 thread.
+bool mp_thread_pool_queue(struct mp_thread_pool *pool, void (*fn)(void *ctx),
+ void *fn_ctx);
+
+// Like mp_thread_pool_queue(), but only queue the item and succeed if a thread
+// can be reserved for the item (i.e. minimal wait time instead of unbounded).
+bool mp_thread_pool_run(struct mp_thread_pool *pool, void (*fn)(void *ctx),
+ void *fn_ctx);
+
+#endif
diff --git a/misc/thread_tools.c b/misc/thread_tools.c
new file mode 100644
index 0000000..0f7fe8f
--- /dev/null
+++ b/misc/thread_tools.c
@@ -0,0 +1,276 @@
+/* Copyright (C) 2018 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdatomic.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifdef __MINGW32__
+#include <windows.h>
+#else
+#include <poll.h>
+#endif
+
+#include "common/common.h"
+#include "misc/linked_list.h"
+#include "osdep/io.h"
+#include "osdep/timer.h"
+
+#include "thread_tools.h"
+
+uintptr_t mp_waiter_wait(struct mp_waiter *waiter)
+{
+ mp_mutex_lock(&waiter->lock);
+ while (!waiter->done)
+ mp_cond_wait(&waiter->wakeup, &waiter->lock);
+ mp_mutex_unlock(&waiter->lock);
+
+ uintptr_t ret = waiter->value;
+
+ // We document that after mp_waiter_wait() the waiter object becomes
+ // invalid. (It strictly returns only after mp_waiter_wakeup() has returned,
+ // and the object is "single-shot".) So destroy it here.
+
+ // Normally, we expect that the system uses futexes, in which case the
+ // following functions will do nearly nothing. This is true for Windows
+ // and Linux. But some lesser OSes still might allocate kernel objects
+ // when initializing mutexes, so destroy them here.
+ mp_mutex_destroy(&waiter->lock);
+ mp_cond_destroy(&waiter->wakeup);
+
+ memset(waiter, 0xCA, sizeof(*waiter)); // for debugging
+
+ return ret;
+}
+
+void mp_waiter_wakeup(struct mp_waiter *waiter, uintptr_t value)
+{
+ mp_mutex_lock(&waiter->lock);
+ assert(!waiter->done);
+ waiter->done = true;
+ waiter->value = value;
+ mp_cond_signal(&waiter->wakeup);
+ mp_mutex_unlock(&waiter->lock);
+}
+
+bool mp_waiter_poll(struct mp_waiter *waiter)
+{
+ mp_mutex_lock(&waiter->lock);
+ bool r = waiter->done;
+ mp_mutex_unlock(&waiter->lock);
+ return r;
+}
+
+struct mp_cancel {
+ mp_mutex lock;
+ mp_cond wakeup;
+
+ // Semaphore state and "mirrors".
+ atomic_bool triggered;
+ void (*cb)(void *ctx);
+ void *cb_ctx;
+ int wakeup_pipe[2];
+ void *win32_event; // actually HANDLE
+
+ // Slave list. These are automatically notified as well.
+ struct {
+ struct mp_cancel *head, *tail;
+ } slaves;
+
+ // For slaves. Synchronization is managed by parent.lock!
+ struct mp_cancel *parent;
+ struct {
+ struct mp_cancel *next, *prev;
+ } siblings;
+};
+
+static void cancel_destroy(void *p)
+{
+ struct mp_cancel *c = p;
+
+ assert(!c->slaves.head); // API user error
+
+ mp_cancel_set_parent(c, NULL);
+
+ if (c->wakeup_pipe[0] >= 0) {
+ close(c->wakeup_pipe[0]);
+ close(c->wakeup_pipe[1]);
+ }
+
+#ifdef __MINGW32__
+ if (c->win32_event)
+ CloseHandle(c->win32_event);
+#endif
+
+ mp_mutex_destroy(&c->lock);
+ mp_cond_destroy(&c->wakeup);
+}
+
+struct mp_cancel *mp_cancel_new(void *talloc_ctx)
+{
+ struct mp_cancel *c = talloc_ptrtype(talloc_ctx, c);
+ talloc_set_destructor(c, cancel_destroy);
+ *c = (struct mp_cancel){
+ .triggered = false,
+ .wakeup_pipe = {-1, -1},
+ };
+ mp_mutex_init(&c->lock);
+ mp_cond_init(&c->wakeup);
+ return c;
+}
+
+static void trigger_locked(struct mp_cancel *c)
+{
+ atomic_store(&c->triggered, true);
+
+ mp_cond_broadcast(&c->wakeup); // condition bound to c->triggered
+
+ if (c->cb)
+ c->cb(c->cb_ctx);
+
+ for (struct mp_cancel *sub = c->slaves.head; sub; sub = sub->siblings.next)
+ mp_cancel_trigger(sub);
+
+ if (c->wakeup_pipe[1] >= 0)
+ (void)write(c->wakeup_pipe[1], &(char){0}, 1);
+
+#ifdef __MINGW32__
+ if (c->win32_event)
+ SetEvent(c->win32_event);
+#endif
+}
+
+void mp_cancel_trigger(struct mp_cancel *c)
+{
+ mp_mutex_lock(&c->lock);
+ trigger_locked(c);
+ mp_mutex_unlock(&c->lock);
+}
+
+void mp_cancel_reset(struct mp_cancel *c)
+{
+ mp_mutex_lock(&c->lock);
+
+ atomic_store(&c->triggered, false);
+
+ if (c->wakeup_pipe[0] >= 0) {
+ // Flush it fully.
+ while (1) {
+ int r = read(c->wakeup_pipe[0], &(char[256]){0}, 256);
+ if (r <= 0 && !(r < 0 && errno == EINTR))
+ break;
+ }
+ }
+
+#ifdef __MINGW32__
+ if (c->win32_event)
+ ResetEvent(c->win32_event);
+#endif
+
+ mp_mutex_unlock(&c->lock);
+}
+
+bool mp_cancel_test(struct mp_cancel *c)
+{
+ return c ? atomic_load_explicit(&c->triggered, memory_order_relaxed) : false;
+}
+
+bool mp_cancel_wait(struct mp_cancel *c, double timeout)
+{
+ int64_t wait_until = mp_time_ns_add(mp_time_ns(), timeout);
+ mp_mutex_lock(&c->lock);
+ while (!mp_cancel_test(c)) {
+ if (mp_cond_timedwait_until(&c->wakeup, &c->lock, wait_until))
+ break;
+ }
+ mp_mutex_unlock(&c->lock);
+
+ return mp_cancel_test(c);
+}
+
+// If a new notification mechanism was added, and the mp_cancel state was
+// already triggered, make sure the newly added mechanism is also triggered.
+static void retrigger_locked(struct mp_cancel *c)
+{
+ if (mp_cancel_test(c))
+ trigger_locked(c);
+}
+
+void mp_cancel_set_cb(struct mp_cancel *c, void (*cb)(void *ctx), void *ctx)
+{
+ mp_mutex_lock(&c->lock);
+ c->cb = cb;
+ c->cb_ctx = ctx;
+ retrigger_locked(c);
+ mp_mutex_unlock(&c->lock);
+}
+
+void mp_cancel_set_parent(struct mp_cancel *slave, struct mp_cancel *parent)
+{
+ // We can access c->parent without synchronization, because:
+ // - concurrent mp_cancel_set_parent() calls to slave are not allowed
+ // - slave->parent needs to stay valid as long as the slave exists
+ if (slave->parent == parent)
+ return;
+ if (slave->parent) {
+ mp_mutex_lock(&slave->parent->lock);
+ LL_REMOVE(siblings, &slave->parent->slaves, slave);
+ mp_mutex_unlock(&slave->parent->lock);
+ }
+ slave->parent = parent;
+ if (slave->parent) {
+ mp_mutex_lock(&slave->parent->lock);
+ LL_APPEND(siblings, &slave->parent->slaves, slave);
+ retrigger_locked(slave->parent);
+ mp_mutex_unlock(&slave->parent->lock);
+ }
+}
+
+int mp_cancel_get_fd(struct mp_cancel *c)
+{
+ mp_mutex_lock(&c->lock);
+ if (c->wakeup_pipe[0] < 0) {
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wstringop-overflow="
+#endif
+ mp_make_wakeup_pipe(c->wakeup_pipe);
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic pop
+#endif
+ retrigger_locked(c);
+ }
+ mp_mutex_unlock(&c->lock);
+
+
+ return c->wakeup_pipe[0];
+}
+
+#ifdef __MINGW32__
+void *mp_cancel_get_event(struct mp_cancel *c)
+{
+ mp_mutex_lock(&c->lock);
+ if (!c->win32_event) {
+ c->win32_event = CreateEventW(NULL, TRUE, FALSE, NULL);
+ retrigger_locked(c);
+ }
+ mp_mutex_unlock(&c->lock);
+
+ return c->win32_event;
+}
+#endif
diff --git a/misc/thread_tools.h b/misc/thread_tools.h
new file mode 100644
index 0000000..a07257b
--- /dev/null
+++ b/misc/thread_tools.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "osdep/threads.h"
+
+// This is basically a single-shot semaphore, intended as light-weight solution
+// for just making a thread wait for another thread.
+struct mp_waiter {
+ // All fields are considered private. Use MP_WAITER_INITIALIZER to init.
+ mp_mutex lock;
+ mp_cond wakeup;
+ bool done;
+ uintptr_t value;
+};
+
+// Initialize a mp_waiter object for use with mp_waiter_*().
+#define MP_WAITER_INITIALIZER { \
+ .lock = MP_STATIC_MUTEX_INITIALIZER, \
+ .wakeup = MP_STATIC_COND_INITIALIZER, \
+ }
+
+// Block until some other thread calls mp_waiter_wakeup(). The function returns
+// the value argument of that wakeup call. After this, the waiter object must
+// not be used anymore. Although you can reinit it with MP_WAITER_INITIALIZER
+// (then you must make sure nothing calls mp_waiter_wakeup() before this).
+uintptr_t mp_waiter_wait(struct mp_waiter *waiter);
+
+// Unblock the thread waiting with mp_waiter_wait(), and make it return the
+// provided value. If the other thread did not enter that call yet, it will
+// return immediately once it does (mp_waiter_wakeup() always returns
+// immediately). Calling this more than once is not allowed.
+void mp_waiter_wakeup(struct mp_waiter *waiter, uintptr_t value);
+
+// Query whether the waiter was woken up. If true, mp_waiter_wait() will return
+// immediately. This is useful if you want to use another way to block and
+// wakeup (in parallel to mp_waiter).
+// You still need to call mp_waiter_wait() to free resources.
+bool mp_waiter_poll(struct mp_waiter *waiter);
+
+// Basically a binary semaphore that supports signaling the semaphore value to
+// a bunch of other complicated mechanisms (such as wakeup pipes). It was made
+// for aborting I/O and thus has according naming.
+struct mp_cancel;
+
+struct mp_cancel *mp_cancel_new(void *talloc_ctx);
+
+// Request abort.
+void mp_cancel_trigger(struct mp_cancel *c);
+
+// Return whether the caller should abort.
+// For convenience, c==NULL is allowed.
+bool mp_cancel_test(struct mp_cancel *c);
+
+// Wait until the even is signaled. If the timeout (in seconds) expires, return
+// false. timeout==0 polls, timeout<0 waits forever.
+bool mp_cancel_wait(struct mp_cancel *c, double timeout);
+
+// Restore original state. (Allows reusing a mp_cancel.)
+void mp_cancel_reset(struct mp_cancel *c);
+
+// Add a callback to invoke when mp_cancel gets triggered. If it's already
+// triggered, call it from mp_cancel_add_cb() directly. May be called multiple
+// times even if the trigger state changes; not called if it resets. In all
+// cases, this may be called with internal locks held (either in mp_cancel, or
+// other locks held by whoever calls mp_cancel_trigger()).
+// There is only one callback. Create a slave mp_cancel to get a private one.
+void mp_cancel_set_cb(struct mp_cancel *c, void (*cb)(void *ctx), void *ctx);
+
+// If parent gets triggered, automatically trigger slave. There is only 1
+// parent; setting NULL clears the parent. Freeing slave also automatically
+// ends the parent link, but the parent mp_cancel must remain valid until the
+// slave is manually removed or destroyed. Destroying a mp_cancel that still
+// has slaves is an error.
+void mp_cancel_set_parent(struct mp_cancel *slave, struct mp_cancel *parent);
+
+// win32 "Event" HANDLE that indicates the current mp_cancel state.
+void *mp_cancel_get_event(struct mp_cancel *c);
+
+// The FD becomes readable if mp_cancel_test() would return true.
+// Don't actually read from it, just use it for poll().
+int mp_cancel_get_fd(struct mp_cancel *c);
diff --git a/misc/uuid.c b/misc/uuid.c
new file mode 100644
index 0000000..c739b3c
--- /dev/null
+++ b/misc/uuid.c
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2022 Pierre-Anthony Lemieux <pal@palemieux.com>
+ * Zane van Iperen <zane@zanevaniperen.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/*
+ * Copyright (C) 1996, 1997 Theodore Ts'o.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, and the entire permission notice in its entirety,
+ * including the disclaimer of warranties.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF
+ * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+
+/**
+ * @file
+ * UUID parsing and serialization utilities.
+ * The library treat the UUID as an opaque sequence of 16 unsigned bytes,
+ * i.e. ignoring the internal layout of the UUID, which depends on the type
+ * of the UUID.
+ *
+ * @author Pierre-Anthony Lemieux <pal@palemieux.com>
+ * @author Zane van Iperen <zane@zanevaniperen.com>
+ */
+
+#include "uuid.h"
+#include "libavutil/error.h"
+#include "libavutil/avstring.h"
+
+int av_uuid_parse(const char *in, AVUUID uu)
+{
+ if (strlen(in) != 36)
+ return AVERROR(EINVAL);
+
+ return av_uuid_parse_range(in, in + 36, uu);
+}
+
+static int xdigit_to_int(char c)
+{
+ c = av_tolower(c);
+
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+
+ if (c >= '0' && c <= '9')
+ return c - '0';
+
+ return -1;
+}
+
+int av_uuid_parse_range(const char *in_start, const char *in_end, AVUUID uu)
+{
+ int i;
+ const char *cp;
+
+ if ((in_end - in_start) != 36)
+ return AVERROR(EINVAL);
+
+ for (i = 0, cp = in_start; i < 16; i++) {
+ int hi;
+ int lo;
+
+ if (i == 4 || i == 6 || i == 8 || i == 10)
+ cp++;
+
+ hi = xdigit_to_int(*cp++);
+ lo = xdigit_to_int(*cp++);
+
+ if (hi == -1 || lo == -1)
+ return AVERROR(EINVAL);
+
+ uu[i] = (hi << 4) + lo;
+ }
+
+ return 0;
+}
+
+static const char hexdigits_lower[16] = "0123456789abcdef";
+
+void av_uuid_unparse(const AVUUID uuid, char *out)
+{
+ char *p = out;
+
+ for (int i = 0; i < 16; i++) {
+ uint8_t tmp;
+
+ if (i == 4 || i == 6 || i == 8 || i == 10)
+ *p++ = '-';
+
+ tmp = uuid[i];
+ *p++ = hexdigits_lower[tmp >> 4];
+ *p++ = hexdigits_lower[tmp & 15];
+ }
+
+ *p = '\0';
+}
+
+int av_uuid_urn_parse(const char *in, AVUUID uu)
+{
+ if (av_stristr(in, "urn:uuid:") != in)
+ return AVERROR(EINVAL);
+
+ return av_uuid_parse(in + 9, uu);
+}
diff --git a/misc/uuid.h b/misc/uuid.h
new file mode 100644
index 0000000..748b7ed
--- /dev/null
+++ b/misc/uuid.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2022 Pierre-Anthony Lemieux <pal@palemieux.com>
+ * Zane van Iperen <zane@zanevaniperen.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * UUID parsing and serialization utilities.
+ * The library treats the UUID as an opaque sequence of 16 unsigned bytes,
+ * i.e. ignoring the internal layout of the UUID, which depends on the type
+ * of the UUID.
+ *
+ * @author Pierre-Anthony Lemieux <pal@palemieux.com>
+ * @author Zane van Iperen <zane@zanevaniperen.com>
+ */
+
+#ifndef AVUTIL_UUID_H
+#define AVUTIL_UUID_H
+
+#include <stdint.h>
+#include <string.h>
+
+#define AV_PRI_UUID \
+ "%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-" \
+ "%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx"
+
+#define AV_PRI_URN_UUID \
+ "urn:uuid:%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-" \
+ "%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx"
+
+/* AV_UUID_ARG() is used together with AV_PRI_UUID() or AV_PRI_URN_UUID
+ * to print UUIDs, e.g.
+ * av_log(NULL, AV_LOG_DEBUG, "UUID: " AV_PRI_UUID, AV_UUID_ARG(uuid));
+ */
+#define AV_UUID_ARG(x) \
+ (x)[ 0], (x)[ 1], (x)[ 2], (x)[ 3], \
+ (x)[ 4], (x)[ 5], (x)[ 6], (x)[ 7], \
+ (x)[ 8], (x)[ 9], (x)[10], (x)[11], \
+ (x)[12], (x)[13], (x)[14], (x)[15]
+
+#define AV_UUID_LEN 16
+
+/* Binary representation of a UUID */
+typedef uint8_t AVUUID[AV_UUID_LEN];
+
+/**
+ * Parses a string representation of a UUID formatted according to IETF RFC 4122
+ * into an AVUUID. The parsing is case-insensitive. The string must be 37
+ * characters long, including the terminating NUL character.
+ *
+ * Example string representation: "2fceebd0-7017-433d-bafb-d073a7116696"
+ *
+ * @param[in] in String representation of a UUID,
+ * e.g. 2fceebd0-7017-433d-bafb-d073a7116696
+ * @param[out] uu AVUUID
+ * @return A non-zero value in case of an error.
+ */
+int av_uuid_parse(const char *in, AVUUID uu);
+
+/**
+ * Parses a URN representation of a UUID, as specified at IETF RFC 4122,
+ * into an AVUUID. The parsing is case-insensitive. The string must be 46
+ * characters long, including the terminating NUL character.
+ *
+ * Example string representation: "urn:uuid:2fceebd0-7017-433d-bafb-d073a7116696"
+ *
+ * @param[in] in URN UUID
+ * @param[out] uu AVUUID
+ * @return A non-zero value in case of an error.
+ */
+int av_uuid_urn_parse(const char *in, AVUUID uu);
+
+/**
+ * Parses a string representation of a UUID formatted according to IETF RFC 4122
+ * into an AVUUID. The parsing is case-insensitive.
+ *
+ * @param[in] in_start Pointer to the first character of the string representation
+ * @param[in] in_end Pointer to the character after the last character of the
+ * string representation. That memory location is never
+ * accessed. It is an error if `in_end - in_start != 36`.
+ * @param[out] uu AVUUID
+ * @return A non-zero value in case of an error.
+ */
+int av_uuid_parse_range(const char *in_start, const char *in_end, AVUUID uu);
+
+/**
+ * Serializes a AVUUID into a string representation according to IETF RFC 4122.
+ * The string is lowercase and always 37 characters long, including the
+ * terminating NUL character.
+ *
+ * @param[in] uu AVUUID
+ * @param[out] out Pointer to an array of no less than 37 characters.
+ */
+void av_uuid_unparse(const AVUUID uu, char *out);
+
+/**
+ * Compares two UUIDs for equality.
+ *
+ * @param[in] uu1 AVUUID
+ * @param[in] uu2 AVUUID
+ * @return Nonzero if uu1 and uu2 are identical, 0 otherwise
+ */
+static inline int av_uuid_equal(const AVUUID uu1, const AVUUID uu2)
+{
+ return memcmp(uu1, uu2, AV_UUID_LEN) == 0;
+}
+
+/**
+ * Copies the bytes of src into dest.
+ *
+ * @param[out] dest AVUUID
+ * @param[in] src AVUUID
+ */
+static inline void av_uuid_copy(AVUUID dest, const AVUUID src)
+{
+ memcpy(dest, src, AV_UUID_LEN);
+}
+
+/**
+ * Sets a UUID to the nil UUID, i.e. a UUID with have all
+ * its 128 bits set to zero.
+ *
+ * @param[in,out] uu UUID to be set to the nil UUID
+ */
+static inline void av_uuid_nil(AVUUID uu)
+{
+ memset(uu, 0, AV_UUID_LEN);
+}
+
+#endif /* AVUTIL_UUID_H */
diff --git a/mpv_talloc.h b/mpv_talloc.h
new file mode 100644
index 0000000..11f996b
--- /dev/null
+++ b/mpv_talloc.h
@@ -0,0 +1,7 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+#include "osdep/compiler.h"
+
+#include "ta/ta_talloc.h"
diff --git a/options/m_config.h b/options/m_config.h
new file mode 100644
index 0000000..d2ce2b4
--- /dev/null
+++ b/options/m_config.h
@@ -0,0 +1 @@
+#include "m_config_core.h" \ No newline at end of file
diff --git a/options/m_config_core.c b/options/m_config_core.c
new file mode 100644
index 0000000..08a76eb
--- /dev/null
+++ b/options/m_config_core.c
@@ -0,0 +1,876 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg_control.h"
+#include "common/msg.h"
+#include "m_config_core.h"
+#include "misc/dispatch.h"
+#include "options/m_option.h"
+#include "osdep/threads.h"
+
+// For use with m_config_cache.
+struct m_config_shadow {
+ mp_mutex lock;
+ // Incremented on every option change.
+ _Atomic uint64_t ts;
+ // -- immutable after init
+ // List of m_sub_options instances.
+ // Index 0 is the top-level and is always present.
+ // Immutable after init.
+ // Invariant: a parent is always at a lower index than any of its children.
+ struct m_config_group *groups;
+ int num_groups;
+ // -- protected by lock
+ struct m_config_data *data; // protected shadow copy of the option data
+ struct config_cache **listeners;
+ int num_listeners;
+};
+
+// Represents a sub-struct (OPT_SUBSTRUCT()).
+struct m_config_group {
+ const struct m_sub_options *group;
+ int opt_count; // cached opt. count; group->opts[opt_count].name==NULL
+ int group_count; // 1 + number of all sub groups owned by this (so
+ // m_config_shadow.groups[idx..idx+group_count] is used
+ // by the entire tree of sub groups included by this
+ // group)
+ int parent_group; // index of parent group into m_config_shadow.groups[],
+ // or -1 for group 0
+ int parent_ptr; // ptr offset in the parent group's data, or -1 if
+ // none
+ const char *prefix; // concat_name(_, prefix, opt->name) => full name
+ // (the parent names are already included in this)
+};
+
+// A copy of option data. Used for the main option struct, the shadow data,
+// and copies for m_config_cache.
+struct m_config_data {
+ struct m_config_shadow *shadow; // option definitions etc., main data copy
+ int group_index; // start index into m_config.groups[]
+ struct m_group_data *gdata; // user struct allocation (our copy of data)
+ int num_gdata; // (group_index+num_gdata = end index)
+};
+
+struct config_cache {
+ struct m_config_cache *public;
+
+ struct m_config_data *data; // public data
+ struct m_config_data *src; // global data (currently ==shadow->data)
+ struct m_config_shadow *shadow; // global metadata
+ int group_start, group_end; // derived from data->group_index etc.
+ uint64_t ts; // timestamp of this data copy
+ bool in_list; // part of m_config_shadow->listeners[]
+ int upd_group; // for "incremental" change notification
+ int upd_opt;
+
+
+ // --- Implicitly synchronized by setting/unsetting wakeup_cb.
+ struct mp_dispatch_queue *wakeup_dispatch_queue;
+ void (*wakeup_dispatch_cb)(void *ctx);
+ void *wakeup_dispatch_cb_ctx;
+
+ // --- Protected by shadow->lock
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_cb_ctx;
+};
+
+// Per m_config_data state for each m_config_group.
+struct m_group_data {
+ char *udata; // pointer to group user option struct
+ uint64_t ts; // timestamp of the data copy
+};
+
+static void add_sub_group(struct m_config_shadow *shadow, const char *name_prefix,
+ int parent_group_index, int parent_ptr,
+ const struct m_sub_options *subopts);
+
+static struct m_group_data *m_config_gdata(struct m_config_data *data,
+ int group_index)
+{
+ if (group_index < data->group_index ||
+ group_index >= data->group_index + data->num_gdata)
+ return NULL;
+
+ return &data->gdata[group_index - data->group_index];
+}
+
+// Like concat_name(), but returns either a, b, or buf. buf/buf_size is used as
+// target for snprintf(). (buf_size is recommended to be MAX_OPT_NAME_LEN.)
+static const char *concat_name_buf(char *buf, size_t buf_size,
+ const char *a, const char *b)
+{
+ assert(a);
+ assert(b);
+ if (!a[0])
+ return b;
+ if (!b[0])
+ return a;
+ snprintf(buf, buf_size, "%s-%s", a, b);
+ return buf;
+}
+
+// Return full option name from prefix (a) and option name (b). Returns either
+// a, b, or a talloc'ed string under ta_parent.
+static const char *concat_name(void *ta_parent, const char *a, const char *b)
+{
+ char buf[M_CONFIG_MAX_OPT_NAME_LEN];
+ const char *r = concat_name_buf(buf, sizeof(buf), a, b);
+ return r == buf ? talloc_strdup(ta_parent, r) : r;
+}
+
+static bool iter_next(struct m_config_shadow *shadow, int group_start,
+ int group_end, int32_t *p_id)
+{
+ int32_t id = *p_id;
+ int group_index = id == -1 ? group_start : id >> 16;
+ int opt_index = id == -1 ? -1 : id & 0xFFFF;
+
+ assert(group_index >= group_start && group_index <= group_end);
+
+ while (1) {
+ if (group_index >= group_end)
+ return false;
+
+ struct m_config_group *g = &shadow->groups[group_index];
+ const struct m_option *opts = g->group->opts;
+
+ assert(opt_index >= -1 && opt_index < g->opt_count);
+
+ opt_index += 1;
+
+ if (!opts || !opts[opt_index].name) {
+ group_index += 1;
+ opt_index = -1;
+ continue;
+ }
+
+ if (opts[opt_index].type == &m_option_type_subconfig)
+ continue;
+
+ *p_id = (group_index << 16) | opt_index;
+ return true;
+ }
+}
+
+bool m_config_shadow_get_next_opt(struct m_config_shadow *shadow, int32_t *p_id)
+{
+ return iter_next(shadow, 0, shadow->num_groups, p_id);
+}
+
+bool m_config_cache_get_next_opt(struct m_config_cache *cache, int32_t *p_id)
+{
+ return iter_next(cache->shadow, cache->internal->group_start,
+ cache->internal->group_end, p_id);
+}
+
+static void get_opt_from_id(struct m_config_shadow *shadow, int32_t id,
+ int *out_group_index, int *out_opt_index)
+{
+ int group_index = id >> 16;
+ int opt_index = id & 0xFFFF;
+
+ assert(group_index >= 0 && group_index < shadow->num_groups);
+ assert(opt_index >= 0 && opt_index < shadow->groups[group_index].opt_count);
+
+ *out_group_index = group_index;
+ *out_opt_index = opt_index;
+}
+
+const struct m_option *m_config_shadow_get_opt(struct m_config_shadow *shadow,
+ int32_t id)
+{
+ int group_index, opt_index;
+ get_opt_from_id(shadow, id, &group_index, &opt_index);
+
+ return &shadow->groups[group_index].group->opts[opt_index];
+}
+
+const char *m_config_shadow_get_opt_name(struct m_config_shadow *shadow,
+ int32_t id, char *buf, size_t buf_size)
+{
+ int group_index, opt_index;
+ get_opt_from_id(shadow, id, &group_index, &opt_index);
+
+ struct m_config_group *g = &shadow->groups[group_index];
+ return concat_name_buf(buf, buf_size, g->prefix,
+ g->group->opts[opt_index].name);
+}
+
+const void *m_config_shadow_get_opt_default(struct m_config_shadow *shadow,
+ int32_t id)
+{
+ int group_index, opt_index;
+ get_opt_from_id(shadow, id, &group_index, &opt_index);
+
+ const struct m_sub_options *subopt = shadow->groups[group_index].group;
+ const struct m_option *opt = &subopt->opts[opt_index];
+
+ if (opt->offset < 0)
+ return NULL;
+
+ if (opt->defval)
+ return opt->defval;
+
+ if (subopt->defaults)
+ return (char *)subopt->defaults + opt->offset;
+
+ return &m_option_value_default;
+}
+
+void *m_config_cache_get_opt_data(struct m_config_cache *cache, int32_t id)
+{
+ int group_index, opt_index;
+ get_opt_from_id(cache->shadow, id, &group_index, &opt_index);
+
+ assert(group_index >= cache->internal->group_start &&
+ group_index < cache->internal->group_end);
+
+ struct m_group_data *gd = m_config_gdata(cache->internal->data, group_index);
+ const struct m_option *opt =
+ &cache->shadow->groups[group_index].group->opts[opt_index];
+
+ return gd && opt->offset >= 0 ? gd->udata + opt->offset : NULL;
+}
+
+static uint64_t get_opt_change_mask(struct m_config_shadow *shadow, int group_index,
+ int group_root, const struct m_option *opt)
+{
+ uint64_t changed = opt->flags & UPDATE_OPTS_MASK;
+ while (group_index != group_root) {
+ struct m_config_group *g = &shadow->groups[group_index];
+ changed |= g->group->change_flags;
+ group_index = g->parent_group;
+ }
+ return changed;
+}
+
+uint64_t m_config_cache_get_option_change_mask(struct m_config_cache *cache,
+ int32_t id)
+{
+ struct m_config_shadow *shadow = cache->shadow;
+ int group_index, opt_index;
+ get_opt_from_id(shadow, id, &group_index, &opt_index);
+
+ assert(group_index >= cache->internal->group_start &&
+ group_index < cache->internal->group_end);
+
+ return get_opt_change_mask(cache->shadow, group_index,
+ cache->internal->data->group_index,
+ &shadow->groups[group_index].group->opts[opt_index]);
+}
+
+// The memcpys are supposed to work around the strict aliasing violation,
+// that would result if we just dereferenced a void** (where the void** is
+// actually casted from struct some_type* ). The dummy struct type is in
+// theory needed, because void* and struct pointers could have different
+// representations, while pointers to different struct types don't.
+static void *substruct_read_ptr(const void *ptr)
+{
+ struct mp_dummy_ *res;
+ memcpy(&res, ptr, sizeof(res));
+ return res;
+}
+static void substruct_write_ptr(void *ptr, void *val)
+{
+ struct mp_dummy_ *src = val;
+ memcpy(ptr, &src, sizeof(src));
+}
+
+// Initialize a field with a given value. In case this is dynamic data, it has
+// to be allocated and copied. src can alias dst.
+static void init_opt_inplace(const struct m_option *opt, void *dst,
+ const void *src)
+{
+ // The option will use dynamic memory allocation iff it has a free callback.
+ if (opt->type->free) {
+ union m_option_value temp;
+ memcpy(&temp, src, opt->type->size);
+ memset(dst, 0, opt->type->size);
+ m_option_copy(opt, dst, &temp);
+ } else if (src != dst) {
+ memcpy(dst, src, opt->type->size);
+ }
+}
+
+static void alloc_group(struct m_config_data *data, int group_index,
+ struct m_config_data *copy)
+{
+ assert(group_index == data->group_index + data->num_gdata);
+ assert(group_index < data->shadow->num_groups);
+ struct m_config_group *group = &data->shadow->groups[group_index];
+ const struct m_sub_options *opts = group->group;
+
+ MP_TARRAY_GROW(data, data->gdata, data->num_gdata);
+ struct m_group_data *gdata = &data->gdata[data->num_gdata++];
+
+ struct m_group_data *copy_gdata =
+ copy ? m_config_gdata(copy, group_index) : NULL;
+
+ *gdata = (struct m_group_data){
+ .udata = talloc_zero_size(data, opts->size),
+ .ts = copy_gdata ? copy_gdata->ts : 0,
+ };
+
+ if (opts->defaults)
+ memcpy(gdata->udata, opts->defaults, opts->size);
+
+ char *copy_src = copy_gdata ? copy_gdata->udata : NULL;
+
+ for (int n = 0; opts->opts && opts->opts[n].name; n++) {
+ const struct m_option *opt = &opts->opts[n];
+
+ if (opt->offset < 0 || opt->type->size == 0)
+ continue;
+
+ void *dst = gdata->udata + opt->offset;
+ const void *defptr = opt->defval ? opt->defval : dst;
+ if (copy_src)
+ defptr = copy_src + opt->offset;
+
+ init_opt_inplace(opt, dst, defptr);
+ }
+
+ // If there's a parent, update its pointer to the new struct.
+ if (group->parent_group >= data->group_index && group->parent_ptr >= 0) {
+ struct m_group_data *parent_gdata =
+ m_config_gdata(data, group->parent_group);
+ assert(parent_gdata);
+
+ substruct_write_ptr(parent_gdata->udata + group->parent_ptr, gdata->udata);
+ }
+}
+
+static void free_option_data(void *p)
+{
+ struct m_config_data *data = p;
+
+ for (int i = 0; i < data->num_gdata; i++) {
+ struct m_group_data *gdata = &data->gdata[i];
+ struct m_config_group *group =
+ &data->shadow->groups[data->group_index + i];
+ const struct m_option *opts = group->group->opts;
+
+ for (int n = 0; opts && opts[n].name; n++) {
+ const struct m_option *opt = &opts[n];
+
+ if (opt->offset >= 0 && opt->type->size > 0)
+ m_option_free(opt, gdata->udata + opt->offset);
+ }
+ }
+}
+
+// Allocate data using the option description in shadow, starting at group_index
+// (index into m_config.groups[]).
+// If copy is not NULL, copy all data from there (for groups which are in both
+// m_config_data instances), in all other cases init the data with the defaults.
+static struct m_config_data *allocate_option_data(void *ta_parent,
+ struct m_config_shadow *shadow,
+ int group_index,
+ struct m_config_data *copy)
+{
+ assert(group_index >= 0 && group_index < shadow->num_groups);
+ struct m_config_data *data = talloc_zero(ta_parent, struct m_config_data);
+ talloc_set_destructor(data, free_option_data);
+
+ data->shadow = shadow;
+ data->group_index = group_index;
+
+ struct m_config_group *root_group = &shadow->groups[group_index];
+ assert(root_group->group_count > 0);
+
+ for (int n = group_index; n < group_index + root_group->group_count; n++)
+ alloc_group(data, n, copy);
+
+ return data;
+}
+
+static void shadow_destroy(void *p)
+{
+ struct m_config_shadow *shadow = p;
+
+ // must all have been unregistered
+ assert(shadow->num_listeners == 0);
+
+ talloc_free(shadow->data);
+ mp_mutex_destroy(&shadow->lock);
+}
+
+struct m_config_shadow *m_config_shadow_new(const struct m_sub_options *root)
+{
+ struct m_config_shadow *shadow = talloc_zero(NULL, struct m_config_shadow);
+ talloc_set_destructor(shadow, shadow_destroy);
+ mp_mutex_init(&shadow->lock);
+
+ add_sub_group(shadow, NULL, -1, -1, root);
+
+ if (!root->size)
+ return shadow;
+
+ shadow->data = allocate_option_data(shadow, shadow, 0, NULL);
+
+ return shadow;
+}
+
+static void init_obj_settings_list(struct m_config_shadow *shadow,
+ int parent_group_index,
+ const struct m_obj_list *list)
+{
+ struct m_obj_desc desc;
+ for (int n = 0; ; n++) {
+ if (!list->get_desc(&desc, n))
+ break;
+ if (desc.global_opts) {
+ add_sub_group(shadow, NULL, parent_group_index, -1,
+ desc.global_opts);
+ }
+ if (list->use_global_options && desc.options) {
+ struct m_sub_options *conf = talloc_ptrtype(shadow, conf);
+ *conf = (struct m_sub_options){
+ .prefix = desc.options_prefix,
+ .opts = desc.options,
+ .defaults = desc.priv_defaults,
+ .size = desc.priv_size,
+ };
+ add_sub_group(shadow, NULL, parent_group_index, -1, conf);
+ }
+ }
+}
+
+static void add_sub_group(struct m_config_shadow *shadow, const char *name_prefix,
+ int parent_group_index, int parent_ptr,
+ const struct m_sub_options *subopts)
+{
+ // Can't be used multiple times.
+ for (int n = 0; n < shadow->num_groups; n++)
+ assert(shadow->groups[n].group != subopts);
+
+ if (!name_prefix)
+ name_prefix = "";
+ if (subopts->prefix && subopts->prefix[0]) {
+ assert(!name_prefix[0]);
+ name_prefix = subopts->prefix;
+ }
+
+ // You can only use UPDATE_ flags here.
+ assert(!(subopts->change_flags & ~(unsigned)UPDATE_OPTS_MASK));
+
+ assert(parent_group_index >= -1 && parent_group_index < shadow->num_groups);
+
+ int group_index = shadow->num_groups++;
+ MP_TARRAY_GROW(shadow, shadow->groups, group_index);
+ shadow->groups[group_index] = (struct m_config_group){
+ .group = subopts,
+ .parent_group = parent_group_index,
+ .parent_ptr = parent_ptr,
+ .prefix = name_prefix,
+ };
+
+ for (int i = 0; subopts->opts && subopts->opts[i].name; i++) {
+ const struct m_option *opt = &subopts->opts[i];
+
+ if (opt->type == &m_option_type_subconfig) {
+ const struct m_sub_options *new_subopts = opt->priv;
+
+ // Providing default structs in-place is not allowed.
+ if (opt->offset >= 0 && subopts->defaults) {
+ void *ptr = (char *)subopts->defaults + opt->offset;
+ assert(!substruct_read_ptr(ptr));
+ }
+
+ const char *prefix = concat_name(shadow, name_prefix, opt->name);
+ add_sub_group(shadow, prefix, group_index, opt->offset, new_subopts);
+
+ } else if (opt->type == &m_option_type_obj_settings_list) {
+ const struct m_obj_list *objlist = opt->priv;
+ init_obj_settings_list(shadow, group_index, objlist);
+ }
+
+ shadow->groups[group_index].opt_count = i + 1;
+ }
+
+ if (subopts->get_sub_options) {
+ for (int i = 0; ; i++) {
+ const struct m_sub_options *sub = NULL;
+ if (!subopts->get_sub_options(i, &sub))
+ break;
+ if (sub)
+ add_sub_group(shadow, NULL, group_index, -1, sub);
+ }
+ }
+
+ shadow->groups[group_index].group_count = shadow->num_groups - group_index;
+}
+
+static void cache_destroy(void *p)
+{
+ struct m_config_cache *cache = p;
+
+ // (technically speaking, being able to call them both without anything
+ // breaking is a feature provided by these functions)
+ m_config_cache_set_wakeup_cb(cache, NULL, NULL);
+ m_config_cache_set_dispatch_change_cb(cache, NULL, NULL, NULL);
+}
+
+struct m_config_cache *m_config_cache_from_shadow(void *ta_parent,
+ struct m_config_shadow *shadow,
+ const struct m_sub_options *group)
+{
+ int group_index = -1;
+
+ for (int n = 0; n < shadow->num_groups; n++) {
+ if (shadow->groups[n].group == group) {
+ group_index = n;
+ break;
+ }
+ }
+
+ assert(group_index >= 0); // invalid group (or not in option tree)
+
+ struct cache_alloc {
+ struct m_config_cache a;
+ struct config_cache b;
+ };
+ struct cache_alloc *alloc = talloc_zero(ta_parent, struct cache_alloc);
+ assert((void *)&alloc->a == (void *)alloc);
+ struct m_config_cache *cache = &alloc->a;
+ talloc_set_destructor(cache, cache_destroy);
+ cache->internal = &alloc->b;
+ cache->shadow = shadow;
+
+ struct config_cache *in = cache->internal;
+ in->shadow = shadow;
+ in->src = shadow->data;
+
+ mp_mutex_lock(&shadow->lock);
+ in->data = allocate_option_data(cache, shadow, group_index, in->src);
+ mp_mutex_unlock(&shadow->lock);
+
+ cache->opts = in->data->gdata[0].udata;
+
+ in->group_start = in->data->group_index;
+ in->group_end = in->group_start + in->data->num_gdata;
+ assert(shadow->groups[in->group_start].group_count == in->data->num_gdata);
+
+ in->upd_group = -1;
+
+ return cache;
+}
+
+struct m_config_cache *m_config_cache_alloc(void *ta_parent,
+ struct mpv_global *global,
+ const struct m_sub_options *group)
+{
+ return m_config_cache_from_shadow(ta_parent, global->config, group);
+}
+
+static void update_next_option(struct m_config_cache *cache, void **p_opt)
+{
+ struct config_cache *in = cache->internal;
+ struct m_config_data *dst = in->data;
+ struct m_config_data *src = in->src;
+
+ assert(src->group_index == 0); // must be the option root currently
+
+ *p_opt = NULL;
+
+ while (in->upd_group < dst->group_index + dst->num_gdata) {
+ struct m_group_data *gsrc = m_config_gdata(src, in->upd_group);
+ struct m_group_data *gdst = m_config_gdata(dst, in->upd_group);
+ assert(gsrc && gdst);
+
+ if (gdst->ts < gsrc->ts) {
+ struct m_config_group *g = &dst->shadow->groups[in->upd_group];
+ const struct m_option *opts = g->group->opts;
+
+ while (opts && opts[in->upd_opt].name) {
+ const struct m_option *opt = &opts[in->upd_opt];
+
+ if (opt->offset >= 0 && opt->type->size) {
+ void *dsrc = gsrc->udata + opt->offset;
+ void *ddst = gdst->udata + opt->offset;
+
+ if (!m_option_equal(opt, ddst, dsrc)) {
+ uint64_t ch = get_opt_change_mask(dst->shadow,
+ in->upd_group, dst->group_index, opt);
+
+ if (cache->debug) {
+ char *vdst = m_option_print(opt, ddst);
+ char *vsrc = m_option_print(opt, dsrc);
+ mp_warn(cache->debug, "Option '%s' changed from "
+ "'%s' to' %s' (flags = 0x%"PRIx64")\n",
+ opt->name, vdst, vsrc, ch);
+ talloc_free(vdst);
+ talloc_free(vsrc);
+ }
+
+ m_option_copy(opt, ddst, dsrc);
+ cache->change_flags |= ch;
+
+ in->upd_opt++; // skip this next time
+ *p_opt = ddst;
+ return;
+ }
+ }
+
+ in->upd_opt++;
+ }
+
+ gdst->ts = gsrc->ts;
+ }
+
+ in->upd_group++;
+ in->upd_opt = 0;
+ }
+
+ in->upd_group = -1;
+}
+
+static bool cache_check_update(struct m_config_cache *cache)
+{
+ struct config_cache *in = cache->internal;
+ struct m_config_shadow *shadow = in->shadow;
+
+ // Using atomics and checking outside of the lock - it's unknown whether
+ // this makes it faster or slower. Just cargo culting it.
+ uint64_t new_ts = atomic_load(&shadow->ts);
+ if (in->ts >= new_ts)
+ return false;
+
+ in->ts = new_ts;
+ in->upd_group = in->data->group_index;
+ in->upd_opt = 0;
+ return true;
+}
+
+bool m_config_cache_update(struct m_config_cache *cache)
+{
+ struct config_cache *in = cache->internal;
+ struct m_config_shadow *shadow = in->shadow;
+
+ if (!cache_check_update(cache))
+ return false;
+
+ mp_mutex_lock(&shadow->lock);
+ bool res = false;
+ while (1) {
+ void *p;
+ update_next_option(cache, &p);
+ if (!p)
+ break;
+ res = true;
+ }
+ mp_mutex_unlock(&shadow->lock);
+ return res;
+}
+
+bool m_config_cache_get_next_changed(struct m_config_cache *cache, void **opt)
+{
+ struct config_cache *in = cache->internal;
+ struct m_config_shadow *shadow = in->shadow;
+
+ *opt = NULL;
+ if (!cache_check_update(cache) && in->upd_group < 0)
+ return false;
+
+ mp_mutex_lock(&shadow->lock);
+ update_next_option(cache, opt);
+ mp_mutex_unlock(&shadow->lock);
+ return !!*opt;
+}
+
+static void find_opt(struct m_config_shadow *shadow, struct m_config_data *data,
+ void *ptr, int *group_idx, int *opt_idx)
+{
+ *group_idx = -1;
+ *opt_idx = -1;
+
+ for (int n = data->group_index; n < data->group_index + data->num_gdata; n++)
+ {
+ struct m_group_data *gd = m_config_gdata(data, n);
+ struct m_config_group *g = &shadow->groups[n];
+ const struct m_option *opts = g->group->opts;
+
+ for (int i = 0; opts && opts[i].name; i++) {
+ const struct m_option *opt = &opts[i];
+
+ if (opt->offset >= 0 && opt->type->size &&
+ ptr == gd->udata + opt->offset)
+ {
+ *group_idx = n;
+ *opt_idx = i;
+ return;
+ }
+ }
+ }
+}
+
+bool m_config_cache_write_opt(struct m_config_cache *cache, void *ptr)
+{
+ struct config_cache *in = cache->internal;
+ struct m_config_shadow *shadow = in->shadow;
+
+ int group_idx = -1;
+ int opt_idx = -1;
+ find_opt(shadow, in->data, ptr, &group_idx, &opt_idx);
+
+ // ptr was not in cache->opts, or no option declaration matching it.
+ assert(group_idx >= 0);
+
+ struct m_config_group *g = &shadow->groups[group_idx];
+ const struct m_option *opt = &g->group->opts[opt_idx];
+
+ mp_mutex_lock(&shadow->lock);
+
+ struct m_group_data *gdst = m_config_gdata(in->data, group_idx);
+ struct m_group_data *gsrc = m_config_gdata(in->src, group_idx);
+ assert(gdst && gsrc);
+
+ bool changed = !m_option_equal(opt, gsrc->udata + opt->offset, ptr);
+ if (changed) {
+ m_option_copy(opt, gsrc->udata + opt->offset, ptr);
+
+ gsrc->ts = atomic_fetch_add(&shadow->ts, 1) + 1;
+
+ for (int n = 0; n < shadow->num_listeners; n++) {
+ struct config_cache *listener = shadow->listeners[n];
+ if (listener->wakeup_cb && m_config_gdata(listener->data, group_idx))
+ listener->wakeup_cb(listener->wakeup_cb_ctx);
+ }
+ }
+
+ mp_mutex_unlock(&shadow->lock);
+
+ return changed;
+}
+
+void m_config_cache_set_wakeup_cb(struct m_config_cache *cache,
+ void (*cb)(void *ctx), void *cb_ctx)
+{
+ struct config_cache *in = cache->internal;
+ struct m_config_shadow *shadow = in->shadow;
+
+ mp_mutex_lock(&shadow->lock);
+ if (in->in_list) {
+ for (int n = 0; n < shadow->num_listeners; n++) {
+ if (shadow->listeners[n] == in) {
+ MP_TARRAY_REMOVE_AT(shadow->listeners, shadow->num_listeners, n);
+ break;
+ }
+ }
+ for (int n = 0; n < shadow->num_listeners; n++)
+ assert(shadow->listeners[n] != in); // only 1 wakeup_cb per cache
+ // (The deinitialization path relies on this to free all memory.)
+ if (!shadow->num_listeners) {
+ talloc_free(shadow->listeners);
+ shadow->listeners = NULL;
+ }
+ }
+ if (cb) {
+ MP_TARRAY_APPEND(NULL, shadow->listeners, shadow->num_listeners, in);
+ in->in_list = true;
+ in->wakeup_cb = cb;
+ in->wakeup_cb_ctx = cb_ctx;
+ }
+ mp_mutex_unlock(&shadow->lock);
+}
+
+static void dispatch_notify(void *p)
+{
+ struct config_cache *in = p;
+
+ assert(in->wakeup_dispatch_queue);
+ mp_dispatch_enqueue_notify(in->wakeup_dispatch_queue,
+ in->wakeup_dispatch_cb,
+ in->wakeup_dispatch_cb_ctx);
+}
+
+void m_config_cache_set_dispatch_change_cb(struct m_config_cache *cache,
+ struct mp_dispatch_queue *dispatch,
+ void (*cb)(void *ctx), void *cb_ctx)
+{
+ struct config_cache *in = cache->internal;
+
+ // Removing the old one is tricky. First make sure no new notifications will
+ // come.
+ m_config_cache_set_wakeup_cb(cache, NULL, NULL);
+ // Remove any pending notifications (assume we're on the same thread as
+ // any potential mp_dispatch_queue_process() callers).
+ if (in->wakeup_dispatch_queue) {
+ mp_dispatch_cancel_fn(in->wakeup_dispatch_queue,
+ in->wakeup_dispatch_cb,
+ in->wakeup_dispatch_cb_ctx);
+ }
+
+ in->wakeup_dispatch_queue = NULL;
+ in->wakeup_dispatch_cb = NULL;
+ in->wakeup_dispatch_cb_ctx = NULL;
+
+ if (cb) {
+ in->wakeup_dispatch_queue = dispatch;
+ in->wakeup_dispatch_cb = cb;
+ in->wakeup_dispatch_cb_ctx = cb_ctx;
+ m_config_cache_set_wakeup_cb(cache, dispatch_notify, in);
+ }
+}
+
+void *mp_get_config_group(void *ta_parent, struct mpv_global *global,
+ const struct m_sub_options *group)
+{
+ struct m_config_cache *cache = m_config_cache_alloc(NULL, global, group);
+ // Make talloc_free(cache->opts) free the entire cache.
+ ta_set_parent(cache->opts, ta_parent);
+ ta_set_parent(cache, cache->opts);
+ return cache->opts;
+}
+
+static const struct m_config_group *find_group(struct mpv_global *global,
+ const struct m_option *cfg)
+{
+ struct m_config_shadow *shadow = global->config;
+
+ for (int n = 0; n < shadow->num_groups; n++) {
+ if (shadow->groups[n].group->opts == cfg)
+ return &shadow->groups[n];
+ }
+
+ return NULL;
+}
+
+void *m_config_group_from_desc(void *ta_parent, struct mp_log *log,
+ struct mpv_global *global, struct m_obj_desc *desc, const char *name)
+{
+ const struct m_config_group *group = find_group(global, desc->options);
+ if (group) {
+ return mp_get_config_group(ta_parent, global, group->group);
+ } else {
+ void *d = talloc_zero_size(ta_parent, desc->priv_size);
+ if (desc->priv_defaults)
+ memcpy(d, desc->priv_defaults, desc->priv_size);
+ return d;
+ }
+}
diff --git a/options/m_config_core.h b/options/m_config_core.h
new file mode 100644
index 0000000..a955842
--- /dev/null
+++ b/options/m_config_core.h
@@ -0,0 +1,194 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_M_CONFIG_H
+#define MPLAYER_M_CONFIG_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+struct mp_dispatch_queue;
+struct m_sub_options;
+struct m_option_type;
+struct m_option;
+struct mpv_global;
+
+// This can be used to create and synchronize per-thread option structs,
+// which then can be read without synchronization. No concurrent access to
+// the cache itself is allowed.
+struct m_config_cache {
+ // The struct as indicated by m_config_cache_alloc's group parameter.
+ // (Internally the same as internal->gdata[0]->udata.)
+ void *opts;
+ // Accumulated change flags. The user can set this to 0 to unset all flags.
+ // They are set when calling any of the update functions. A flag is only set
+ // once the new value is visible in ->opts.
+ uint64_t change_flags;
+
+ // Set to non-NULL for logging all option changes as they are retrieved
+ // with one of the update functions (like m_config_cache_update()).
+ struct mp_log *debug;
+
+ // Global instance of option data. Read only.
+ struct m_config_shadow *shadow;
+
+ // Do not access.
+ struct config_cache *internal;
+};
+
+// Maximum possibly option name buffer length (as it appears to the user).
+#define M_CONFIG_MAX_OPT_NAME_LEN 80
+
+// Create a mirror copy from the global options.
+// Keep in mind that a m_config_cache object is not thread-safe; it merely
+// provides thread-safe access to the global options. All API functions for
+// the same m_config_cache object must synchronized, unless otherwise noted.
+// This does not create an initial change event (m_config_cache_update() will
+// return false), but note that a change might be asynchronously signaled at any
+// time.
+// This simply calls m_config_cache_from_shadow(ta_parent, global->shadow, group).
+// ta_parent: parent for the returned allocation
+// global: option data source
+// group: the option group to return
+struct m_config_cache *m_config_cache_alloc(void *ta_parent,
+ struct mpv_global *global,
+ const struct m_sub_options *group);
+
+// If any of the options in the group possibly changes, call this callback. The
+// callback must not actually access the cache or anything option related.
+// Instead, it must wake up the thread that normally accesses the cache.
+void m_config_cache_set_wakeup_cb(struct m_config_cache *cache,
+ void (*cb)(void *ctx), void *cb_ctx);
+
+// If any of the options in the group change, call this callback on the given
+// dispatch queue. This is higher level than m_config_cache_set_wakeup_cb(),
+// and you can do anything you want in the callback (assuming the dispatch
+// queue is processed in the same thread that accesses m_config_cache API).
+// To ensure clean shutdown, you must destroy the m_config_cache (or unset the
+// callback) before the dispatch queue is destroyed.
+void m_config_cache_set_dispatch_change_cb(struct m_config_cache *cache,
+ struct mp_dispatch_queue *dispatch,
+ void (*cb)(void *ctx), void *cb_ctx);
+
+// Update the options in cache->opts to current global values. Return whether
+// there was an update notification at all (which may or may not indicate that
+// some options have changed).
+// Keep in mind that while the cache->opts pointer does not change, the option
+// data itself will (e.g. string options might be reallocated).
+// New change flags are or-ed into cache->change_flags with this call (if you
+// use them, you should probably do cache->change_flags=0 before this call).
+bool m_config_cache_update(struct m_config_cache *cache);
+
+// Check for changes and return fine grained change information.
+// Warning: this conflicts with m_config_cache_update(). If you call
+// m_config_cache_update(), all options will be marked as "not changed",
+// and this function will return false. Also, calling this function and
+// then m_config_cache_update() is not supported, and may skip updating
+// some fields.
+// This returns true as long as there is a changed option, and false if all
+// changed options have been returned.
+// If multiple options have changed, the new option value is visible only once
+// this function has returned the change for it.
+// out_ptr: pointer to a void*, which is set to the cache->opts field associated
+// with the changed option if the function returns true; set to NULL
+// if no option changed.
+// returns: *out_ptr!=NULL (true if there was a changed option)
+bool m_config_cache_get_next_changed(struct m_config_cache *cache, void **out_ptr);
+
+// Copy the option field pointed to by ptr to the global option storage. This
+// is sort of similar to m_config_set_option_raw(), except doesn't require
+// access to the main thread. (And you can't pass any flags.)
+// You write the new value to the option struct, and then call this function
+// with the pointer to it. You will not get a change notification for it (though
+// you might still get a redundant wakeup callback).
+// Changing the option struct and not calling this function before any update
+// function (like m_config_cache_update()) will leave the value inconsistent,
+// and will possibly (but not necessarily) overwrite it with the next update
+// call.
+// ptr: points to any field in cache->opts that is managed by an option. If
+// this is not the case, the function crashes for your own good.
+// returns: if true, this was an update; if false, shadow had same value
+bool m_config_cache_write_opt(struct m_config_cache *cache, void *ptr);
+
+// Like m_config_cache_alloc(), but return the struct (m_config_cache->opts)
+// directly, with no way to update the config. Basically this returns a copy
+// with a snapshot of the current option values.
+void *mp_get_config_group(void *ta_parent, struct mpv_global *global,
+ const struct m_sub_options *group);
+
+// Allocate a priv struct that is backed by global options (like AOs and VOs,
+// anything that uses m_obj_list.use_global_options == true).
+// The result contains a snapshot of the current option values of desc->options.
+// For convenience, desc->options can be NULL; then priv struct is allocated
+// with just zero (or priv_defaults if set).
+// Bad function.
+struct m_obj_desc;
+void *m_config_group_from_desc(void *ta_parent, struct mp_log *log,
+ struct mpv_global *global, struct m_obj_desc *desc, const char *name);
+
+// Allocate new option shadow storage with all options set to defaults.
+// root must stay valid for the lifetime of the return value.
+// Result can be freed with ta_free().
+struct m_config_shadow *m_config_shadow_new(const struct m_sub_options *root);
+
+// See m_config_cache_alloc().
+struct m_config_cache *m_config_cache_from_shadow(void *ta_parent,
+ struct m_config_shadow *shadow,
+ const struct m_sub_options *group);
+
+// Iterate over all registered global options. *p_id must be set to -1 when this
+// is called for the first time. Each time this call returns true, *p_id is set
+// to a new valid option ID. p_id must not be changed for the next call. If
+// false is returned, iteration ends.
+bool m_config_shadow_get_next_opt(struct m_config_shadow *shadow, int32_t *p_id);
+
+// Similar to m_config_shadow_get_next_opt(), but return only options that are
+// covered by the m_config_cache.
+bool m_config_cache_get_next_opt(struct m_config_cache *cache, int32_t *p_id);
+
+// Return the m_option that was used to declare this option.
+// id must be a valid option ID as returned by m_config_shadow_get_next_opt() or
+// m_config_cache_get_next_opt().
+const struct m_option *m_config_shadow_get_opt(struct m_config_shadow *shadow,
+ int32_t id);
+
+// Return the full (global) option name. buf must be supplied, but may not
+// always be used. It should have the size M_CONFIG_MAX_OPT_NAME_LEN.
+// The returned point points either to buf or a static string.
+// id must be a valid option ID as returned by m_config_shadow_get_next_opt() or
+// m_config_cache_get_next_opt().
+const char *m_config_shadow_get_opt_name(struct m_config_shadow *shadow,
+ int32_t id, char *buf, size_t buf_size);
+
+// Pointer to default value, using m_option.type. NULL if option without data.
+// id must be a valid option ID as returned by m_config_shadow_get_next_opt() or
+// m_config_cache_get_next_opt().
+const void *m_config_shadow_get_opt_default(struct m_config_shadow *shadow,
+ int32_t id);
+
+// Return the pointer to the allocated option data (the same pointers that are
+// returned by m_config_cache_get_next_changed()). NULL if option without data.
+// id must be a valid option ID as returned by m_config_cache_get_next_opt().
+void *m_config_cache_get_opt_data(struct m_config_cache *cache, int32_t id);
+
+// Return or-ed UPDATE_OPTS_MASK part of the option and containing sub-options.
+// id must be a valid option ID as returned by m_config_cache_get_next_opt().
+uint64_t m_config_cache_get_option_change_mask(struct m_config_cache *cache,
+ int32_t id);
+
+#endif /* MPLAYER_M_CONFIG_H */
diff --git a/options/m_config_frontend.c b/options/m_config_frontend.c
new file mode 100644
index 0000000..9b54389
--- /dev/null
+++ b/options/m_config_frontend.c
@@ -0,0 +1,1080 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <float.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "libmpv/client.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg_control.h"
+#include "common/msg.h"
+#include "m_config_frontend.h"
+#include "m_config.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "options/m_option.h"
+#include "osdep/threads.h"
+
+extern const char mp_help_text[];
+
+// Profiles allow to predefine some sets of options that can then
+// be applied later on with the internal -profile option.
+#define MAX_PROFILE_DEPTH 20
+// Maximal include depth.
+#define MAX_RECURSION_DEPTH 8
+
+struct m_profile {
+ struct m_profile *next;
+ char *name;
+ char *desc;
+ char *cond;
+ int restore_mode;
+ int num_opts;
+ // Option/value pair array.
+ // name,value = opts[n*2+0],opts[n*2+1]
+ char **opts;
+ // For profile restoring.
+ struct m_opt_backup *backups;
+};
+
+// In the file local case, this contains the old global value.
+// It's also used for profile restoring.
+struct m_opt_backup {
+ struct m_opt_backup *next;
+ struct m_config_option *co;
+ int flags;
+ void *backup, *nval;
+};
+
+static const struct m_option profile_restore_mode_opt = {
+ .name = "profile-restore",
+ .type = &m_option_type_choice,
+ M_CHOICES({"default", 0}, {"copy", 1}, {"copy-equal", 2}),
+};
+
+static void list_profiles(struct m_config *config)
+{
+ MP_INFO(config, "Available profiles:\n");
+ for (struct m_profile *p = config->profiles; p; p = p->next)
+ MP_INFO(config, "\t%s\t%s\n", p->name, p->desc ? p->desc : "");
+ MP_INFO(config, "\n");
+}
+
+static int show_profile(struct m_config *config, bstr param)
+{
+ struct m_profile *p;
+ if (!param.len) {
+ list_profiles(config);
+ return M_OPT_EXIT;
+ }
+ if (!(p = m_config_get_profile(config, param))) {
+ MP_ERR(config, "Unknown profile '%.*s'.\n", BSTR_P(param));
+ return M_OPT_EXIT;
+ }
+ if (!config->profile_depth)
+ MP_INFO(config, "Profile %s: %s\n", p->name,
+ p->desc ? p->desc : "");
+ config->profile_depth++;
+ if (p->cond) {
+ MP_INFO(config, "%*sprofile-cond=%s\n", config->profile_depth, "",
+ p->cond);
+ }
+ for (int i = 0; i < p->num_opts; i++) {
+ MP_INFO(config, "%*s%s=%s\n", config->profile_depth, "",
+ p->opts[2 * i], p->opts[2 * i + 1]);
+
+ if (config->profile_depth < MAX_PROFILE_DEPTH
+ && !strcmp(p->opts[2*i], "profile")) {
+ char *e, *list = p->opts[2 * i + 1];
+ while ((e = strchr(list, ','))) {
+ int l = e - list;
+ if (!l)
+ continue;
+ show_profile(config, (bstr){list, e - list});
+ list = e + 1;
+ }
+ if (list[0] != '\0')
+ show_profile(config, bstr0(list));
+ }
+ }
+ config->profile_depth--;
+ if (!config->profile_depth)
+ MP_INFO(config, "\n");
+ return M_OPT_EXIT;
+}
+
+static struct m_config *m_config_from_obj_desc(void *talloc_ctx,
+ struct mp_log *log,
+ struct mpv_global *global,
+ struct m_obj_desc *desc)
+{
+ struct m_sub_options *root = talloc_ptrtype(NULL, root);
+ *root = (struct m_sub_options){
+ .opts = desc->options,
+ // (global == NULL got repurposed to mean "no alloc")
+ .size = global ? desc->priv_size : 0,
+ .defaults = desc->priv_defaults,
+ };
+
+ struct m_config *c = m_config_new(talloc_ctx, log, root);
+ talloc_steal(c, root);
+ c->global = global;
+ return c;
+}
+
+struct m_config *m_config_from_obj_desc_noalloc(void *talloc_ctx,
+ struct mp_log *log,
+ struct m_obj_desc *desc)
+{
+ return m_config_from_obj_desc(talloc_ctx, log, NULL, desc);
+}
+
+static int m_config_set_obj_params(struct m_config *config, struct mp_log *log,
+ struct mpv_global *global,
+ struct m_obj_desc *desc, char **args)
+{
+ for (int n = 0; args && args[n * 2 + 0]; n++) {
+ bstr opt = bstr0(args[n * 2 + 0]);
+ bstr val = bstr0(args[n * 2 + 1]);
+ if (m_config_set_option_cli(config, opt, val, 0) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+struct m_config *m_config_from_obj_desc_and_args(void *ta_parent,
+ struct mp_log *log, struct mpv_global *global, struct m_obj_desc *desc,
+ char **args)
+{
+ struct m_config *config = m_config_from_obj_desc(ta_parent, log, global, desc);
+ if (m_config_set_obj_params(config, log, global, desc, args) < 0)
+ goto error;
+
+ return config;
+error:
+ talloc_free(config);
+ return NULL;
+}
+
+static void backup_dtor(void *p)
+{
+ struct m_opt_backup *bc = p;
+ m_option_free(bc->co->opt, bc->backup);
+ if (bc->nval)
+ m_option_free(bc->co->opt, bc->nval);
+}
+
+#define BACKUP_LOCAL 1
+#define BACKUP_NVAL 2
+static void ensure_backup(struct m_opt_backup **list, int flags,
+ struct m_config_option *co)
+{
+ if (!co->data)
+ return;
+ for (struct m_opt_backup *cur = *list; cur; cur = cur->next) {
+ if (cur->co->data == co->data) // comparing data ptr catches aliases
+ return;
+ }
+ struct m_opt_backup *bc = talloc_ptrtype(NULL, bc);
+ talloc_set_destructor(bc, backup_dtor);
+ *bc = (struct m_opt_backup) {
+ .co = co,
+ .backup = talloc_zero_size(bc, co->opt->type->size),
+ .nval = flags & BACKUP_NVAL
+ ? talloc_zero_size(bc, co->opt->type->size) : NULL,
+ .flags = flags,
+ };
+ m_option_copy(co->opt, bc->backup, co->data);
+ bc->next = *list;
+ *list = bc;
+ if (bc->flags & BACKUP_LOCAL)
+ co->is_set_locally = true;
+}
+
+static void restore_backups(struct m_opt_backup **list, struct m_config *config)
+{
+ while (*list) {
+ struct m_opt_backup *bc = *list;
+ *list = bc->next;
+
+ if (!bc->nval || m_option_equal(bc->co->opt, bc->co->data, bc->nval))
+ m_config_set_option_raw(config, bc->co, bc->backup, 0);
+
+ if (bc->flags & BACKUP_LOCAL)
+ bc->co->is_set_locally = false;
+ talloc_free(bc);
+ }
+}
+
+void m_config_restore_backups(struct m_config *config)
+{
+ restore_backups(&config->backup_opts, config);
+}
+
+bool m_config_watch_later_backup_opt_changed(struct m_config *config,
+ char *opt_name)
+{
+ struct m_config_option *co = m_config_get_co(config, bstr0(opt_name));
+ if (!co) {
+ MP_ERR(config, "Option %s not found.\n", opt_name);
+ return false;
+ }
+
+ for (struct m_opt_backup *bc = config->watch_later_backup_opts; bc;
+ bc = bc->next) {
+ if (strcmp(bc->co->name, co->name) == 0) {
+ struct m_config_option *bc_co = (struct m_config_option *)bc->backup;
+ return !m_option_equal(co->opt, co->data, bc_co);
+ }
+ }
+
+ return false;
+}
+
+void m_config_backup_opt(struct m_config *config, const char *opt)
+{
+ struct m_config_option *co = m_config_get_co(config, bstr0(opt));
+ if (co) {
+ ensure_backup(&config->backup_opts, BACKUP_LOCAL, co);
+ } else {
+ MP_ERR(config, "Option %s not found.\n", opt);
+ }
+}
+
+void m_config_backup_all_opts(struct m_config *config)
+{
+ for (int n = 0; n < config->num_opts; n++)
+ ensure_backup(&config->backup_opts, BACKUP_LOCAL, &config->opts[n]);
+}
+
+void m_config_backup_watch_later_opts(struct m_config *config)
+{
+ for (int n = 0; n < config->num_opts; n++)
+ ensure_backup(&config->watch_later_backup_opts, 0, &config->opts[n]);
+}
+
+struct m_config_option *m_config_get_co_raw(const struct m_config *config,
+ struct bstr name)
+{
+ if (!name.len)
+ return NULL;
+
+ for (int n = 0; n < config->num_opts; n++) {
+ struct m_config_option *co = &config->opts[n];
+ struct bstr coname = bstr0(co->name);
+ if (bstrcmp(coname, name) == 0)
+ return co;
+ }
+
+ return NULL;
+}
+
+// Like m_config_get_co_raw(), but resolve aliases.
+static struct m_config_option *m_config_get_co_any(const struct m_config *config,
+ struct bstr name)
+{
+ struct m_config_option *co = m_config_get_co_raw(config, name);
+ if (!co)
+ return NULL;
+
+ const char *prefix = config->is_toplevel ? "--" : "";
+ if (co->opt->type == &m_option_type_alias) {
+ const char *alias = (const char *)co->opt->priv;
+ if (co->opt->deprecation_message && !co->warning_was_printed) {
+ if (co->opt->deprecation_message[0]) {
+ MP_WARN(config, "Warning: option %s%s was replaced with "
+ "%s%s: %s\n", prefix, co->name, prefix, alias,
+ co->opt->deprecation_message);
+ } else {
+ MP_WARN(config, "Warning: option %s%s was replaced with "
+ "%s%s and might be removed in the future.\n",
+ prefix, co->name, prefix, alias);
+ }
+ co->warning_was_printed = true;
+ }
+ return m_config_get_co_any(config, bstr0(alias));
+ } else if (co->opt->type == &m_option_type_removed) {
+ if (!co->warning_was_printed) {
+ char *msg = co->opt->priv;
+ if (msg) {
+ MP_FATAL(config, "Option %s%s was removed: %s\n",
+ prefix, co->name, msg);
+ } else {
+ MP_FATAL(config, "Option %s%s was removed.\n",
+ prefix, co->name);
+ }
+ co->warning_was_printed = true;
+ }
+ return NULL;
+ } else if (co->opt->deprecation_message) {
+ if (!co->warning_was_printed) {
+ MP_WARN(config, "Warning: option %s%s is deprecated "
+ "and might be removed in the future (%s).\n",
+ prefix, co->name, co->opt->deprecation_message);
+ co->warning_was_printed = true;
+ }
+ }
+ return co;
+}
+
+struct m_config_option *m_config_get_co(const struct m_config *config,
+ struct bstr name)
+{
+ struct m_config_option *co = m_config_get_co_any(config, name);
+ // CLI aliases should not be real options, and are explicitly handled by
+ // m_config_set_option_cli(). So pretend it does not exist.
+ if (co && co->opt->type == &m_option_type_cli_alias)
+ co = NULL;
+ return co;
+}
+
+int m_config_get_co_count(struct m_config *config)
+{
+ return config->num_opts;
+}
+
+struct m_config_option *m_config_get_co_index(struct m_config *config, int index)
+{
+ return &config->opts[index];
+}
+
+const void *m_config_get_co_default(const struct m_config *config,
+ struct m_config_option *co)
+{
+ return m_config_shadow_get_opt_default(config->shadow, co->opt_id);
+}
+
+const char *m_config_get_positional_option(const struct m_config *config, int p)
+{
+ int pos = 0;
+ for (int n = 0; n < config->num_opts; n++) {
+ struct m_config_option *co = &config->opts[n];
+ if (!co->opt->deprecation_message) {
+ if (pos == p)
+ return co->name;
+ pos++;
+ }
+ }
+ return NULL;
+}
+
+// return: <0: M_OPT_ error, 0: skip, 1: check, 2: set
+static int handle_set_opt_flags(struct m_config *config,
+ struct m_config_option *co, int flags)
+{
+ int optflags = co->opt->flags;
+ bool set = !(flags & M_SETOPT_CHECK_ONLY);
+
+ if ((flags & M_SETOPT_PRE_PARSE_ONLY) && !(optflags & M_OPT_PRE_PARSE))
+ return 0;
+
+ if ((flags & M_SETOPT_PRESERVE_CMDLINE) && co->is_set_from_cmdline)
+ set = false;
+
+ if ((flags & M_SETOPT_NO_OVERWRITE) &&
+ (co->is_set_from_cmdline || co->is_set_from_config))
+ set = false;
+
+ if ((flags & M_SETOPT_NO_PRE_PARSE) && (optflags & M_OPT_PRE_PARSE))
+ return M_OPT_INVALID;
+
+ // Check if this option isn't forbidden in the current mode
+ if ((flags & M_SETOPT_FROM_CONFIG_FILE) && (optflags & M_OPT_NOCFG)) {
+ MP_ERR(config, "The %s option can't be used in a config file.\n",
+ co->name);
+ return M_OPT_INVALID;
+ }
+ if ((flags & M_SETOPT_BACKUP) && set)
+ ensure_backup(&config->backup_opts, BACKUP_LOCAL, co);
+
+ return set ? 2 : 1;
+}
+
+void m_config_mark_co_flags(struct m_config_option *co, int flags)
+{
+ if (flags & M_SETOPT_FROM_CMDLINE)
+ co->is_set_from_cmdline = true;
+
+ if (flags & M_SETOPT_FROM_CONFIG_FILE)
+ co->is_set_from_config = true;
+}
+
+// Special options that don't really fit into the option handling model. They
+// usually store no data, but trigger actions. Caller is assumed to have called
+// handle_set_opt_flags() to make sure the option can be set.
+// Returns M_OPT_UNKNOWN if the option is not a special option.
+static int m_config_handle_special_options(struct m_config *config,
+ struct m_config_option *co,
+ void *data, int flags)
+{
+ if (config->use_profiles && strcmp(co->name, "profile") == 0) {
+ char **list = *(char ***)data;
+
+ if (list && list[0] && !list[1] && strcmp(list[0], "help") == 0) {
+ if (!config->profiles) {
+ MP_INFO(config, "No profiles have been defined.\n");
+ return M_OPT_EXIT;
+ }
+ list_profiles(config);
+ return M_OPT_EXIT;
+ }
+
+ for (int n = 0; list && list[n]; n++) {
+ int r = m_config_set_profile(config, list[n], flags);
+ if (r < 0)
+ return r;
+ }
+ return 0;
+ }
+
+ if (config->includefunc && strcmp(co->name, "include") == 0) {
+ char *param = *(char **)data;
+ if (!param || !param[0])
+ return M_OPT_MISSING_PARAM;
+ if (config->recursion_depth >= MAX_RECURSION_DEPTH) {
+ MP_ERR(config, "Maximum 'include' nesting depth exceeded.\n");
+ return M_OPT_INVALID;
+ }
+ config->recursion_depth += 1;
+ config->includefunc(config->includefunc_ctx, param, flags);
+ config->recursion_depth -= 1;
+ if (config->recursion_depth == 0 && config->profile_depth == 0)
+ m_config_finish_default_profile(config, flags);
+ return 1;
+ }
+
+ if (config->use_profiles && strcmp(co->name, "show-profile") == 0)
+ return show_profile(config, bstr0(*(char **)data));
+
+ if (config->is_toplevel && (strcmp(co->name, "h") == 0 ||
+ strcmp(co->name, "help") == 0))
+ {
+ char *h = *(char **)data;
+ mp_info(config->log, "%s", mp_help_text);
+ if (h && h[0])
+ m_config_print_option_list(config, h);
+ return M_OPT_EXIT;
+ }
+
+ if (strcmp(co->name, "list-options") == 0) {
+ m_config_print_option_list(config, "*");
+ return M_OPT_EXIT;
+ }
+
+ return M_OPT_UNKNOWN;
+}
+
+// This notification happens when anyone other than m_config->cache (i.e. not
+// through m_config_set_option_raw() or related) changes any options.
+static void async_change_cb(void *p)
+{
+ struct m_config *config = p;
+
+ void *ptr;
+ while (m_config_cache_get_next_changed(config->cache, &ptr)) {
+ // Regrettable linear search, might degenerate to quadratic.
+ for (int n = 0; n < config->num_opts; n++) {
+ struct m_config_option *co = &config->opts[n];
+ if (co->data == ptr) {
+ if (config->option_change_callback) {
+ config->option_change_callback(
+ config->option_change_callback_ctx, co,
+ config->cache->change_flags, false);
+ }
+ break;
+ }
+ }
+ config->cache->change_flags = 0;
+ }
+}
+
+void m_config_set_update_dispatch_queue(struct m_config *config,
+ struct mp_dispatch_queue *dispatch)
+{
+ m_config_cache_set_dispatch_change_cb(config->cache, dispatch,
+ async_change_cb, config);
+}
+
+static void config_destroy(void *p)
+{
+ struct m_config *config = p;
+ config->option_change_callback = NULL;
+ m_config_restore_backups(config);
+
+ struct m_opt_backup **list = &config->watch_later_backup_opts;
+ while (*list) {
+ struct m_opt_backup *bc = *list;
+ *list = bc->next;
+ talloc_free(bc);
+ }
+
+ talloc_free(config->cache);
+ talloc_free(config->shadow);
+}
+
+struct m_config *m_config_new(void *talloc_ctx, struct mp_log *log,
+ const struct m_sub_options *root)
+{
+ struct m_config *config = talloc(talloc_ctx, struct m_config);
+ talloc_set_destructor(config, config_destroy);
+ *config = (struct m_config){.log = log,};
+
+ config->shadow = m_config_shadow_new(root);
+
+ if (root->size) {
+ config->cache = m_config_cache_from_shadow(config, config->shadow, root);
+ config->optstruct = config->cache->opts;
+ }
+
+ int32_t optid = -1;
+ while (m_config_shadow_get_next_opt(config->shadow, &optid)) {
+ char buf[M_CONFIG_MAX_OPT_NAME_LEN];
+ const char *opt_name =
+ m_config_shadow_get_opt_name(config->shadow, optid, buf, sizeof(buf));
+
+ struct m_config_option co = {
+ .name = talloc_strdup(config, opt_name),
+ .opt = m_config_shadow_get_opt(config->shadow, optid),
+ .opt_id = optid,
+ };
+
+ if (config->cache)
+ co.data = m_config_cache_get_opt_data(config->cache, optid);
+
+ MP_TARRAY_APPEND(config, config->opts, config->num_opts, co);
+ }
+
+ return config;
+}
+
+// Normally m_config_cache will not send notifications when _we_ change our
+// own stuff. For whatever funny reasons, we need that, though.
+static void force_self_notify_change_opt(struct m_config *config,
+ struct m_config_option *co,
+ bool self_notification)
+{
+ int changed =
+ m_config_cache_get_option_change_mask(config->cache, co->opt_id);
+
+ if (config->option_change_callback) {
+ config->option_change_callback(config->option_change_callback_ctx, co,
+ changed, self_notification);
+ }
+}
+
+static void notify_opt(struct m_config *config, void *ptr, bool self_notification)
+{
+ for (int n = 0; n < config->num_opts; n++) {
+ struct m_config_option *co = &config->opts[n];
+ if (co->data == ptr) {
+ if (m_config_cache_write_opt(config->cache, co->data))
+ force_self_notify_change_opt(config, co, self_notification);
+ return;
+ }
+ }
+ // ptr doesn't point to any config->optstruct field declared in the
+ // option list?
+ assert(false);
+}
+
+void m_config_notify_change_opt_ptr(struct m_config *config, void *ptr)
+{
+ notify_opt(config, ptr, true);
+}
+
+void m_config_notify_change_opt_ptr_notify(struct m_config *config, void *ptr)
+{
+ // (the notify bool is inverted: by not marking it as self-notification,
+ // the mpctx option change handler actually applies it)
+ notify_opt(config, ptr, false);
+}
+
+int m_config_set_option_raw(struct m_config *config,
+ struct m_config_option *co,
+ void *data, int flags)
+{
+ if (!co)
+ return M_OPT_UNKNOWN;
+
+ int r = handle_set_opt_flags(config, co, flags);
+ if (r <= 1)
+ return r;
+
+ r = m_config_handle_special_options(config, co, data, flags);
+ if (r != M_OPT_UNKNOWN)
+ return r;
+
+ // This affects some special options like "playlist", "v". Maybe these
+ // should work, or maybe not. For now they would require special code.
+ if (!co->data)
+ return flags & M_SETOPT_FROM_CMDLINE ? 0 : M_OPT_UNKNOWN;
+
+ if (config->profile_backup_tmp)
+ ensure_backup(config->profile_backup_tmp, config->profile_backup_flags, co);
+
+ m_config_mark_co_flags(co, flags);
+
+ m_option_copy(co->opt, co->data, data);
+ if (m_config_cache_write_opt(config->cache, co->data))
+ force_self_notify_change_opt(config, co, false);
+
+ return 0;
+}
+
+// Handle CLI exceptions to option handling.
+// Used to turn "--no-foo" into "--foo=no".
+// It also handles looking up "--vf-add" as "--vf".
+static struct m_config_option *m_config_mogrify_cli_opt(struct m_config *config,
+ struct bstr *name,
+ bool *out_negate,
+ int *out_add_flags)
+{
+ *out_negate = false;
+ *out_add_flags = 0;
+
+ struct m_config_option *co = m_config_get_co(config, *name);
+ if (co)
+ return co;
+
+ // Turn "--no-foo" into "foo" + set *out_negate.
+ bstr no_name = *name;
+ if (!co && bstr_eatstart0(&no_name, "no-")) {
+ co = m_config_get_co(config, no_name);
+
+ // Not all choice types have this value - if they don't, then parsing
+ // them will simply result in an error. Good enough.
+ if (!co || !(co->opt->type->flags & M_OPT_TYPE_CHOICE))
+ return NULL;
+
+ *name = no_name;
+ *out_negate = true;
+ return co;
+ }
+
+ // Resolve CLI alias. (We don't allow you to combine them with "--no-".)
+ co = m_config_get_co_any(config, *name);
+ if (co && co->opt->type == &m_option_type_cli_alias)
+ *name = bstr0((char *)co->opt->priv);
+
+ // Might be a suffix "action", like "--vf-add". Expensively check for
+ // matches. (We don't allow you to combine them with "--no-".)
+ for (int n = 0; n < config->num_opts; n++) {
+ co = &config->opts[n];
+ struct bstr basename = bstr0(co->name);
+
+ if (!bstr_startswith(*name, basename))
+ continue;
+
+ // Aliased option + a suffix action, e.g. --opengl-shaders-append
+ if (co->opt->type == &m_option_type_alias)
+ co = m_config_get_co_any(config, basename);
+ if (!co)
+ continue;
+
+ const struct m_option_type *type = co->opt->type;
+ for (int i = 0; type->actions && type->actions[i].name; i++) {
+ const struct m_option_action *action = &type->actions[i];
+ bstr suffix = bstr0(action->name);
+
+ if (bstr_endswith(*name, suffix) &&
+ (name->len == basename.len + 1 + suffix.len) &&
+ name->start[basename.len] == '-')
+ {
+ *out_add_flags = action->flags;
+ return co;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+int m_config_set_option_cli(struct m_config *config, struct bstr name,
+ struct bstr param, int flags)
+{
+ int r;
+ assert(config != NULL);
+
+ bool negate;
+ struct m_config_option *co =
+ m_config_mogrify_cli_opt(config, &name, &negate, &(int){0});
+
+ if (!co) {
+ r = M_OPT_UNKNOWN;
+ goto done;
+ }
+
+ if (negate) {
+ if (param.len) {
+ r = M_OPT_DISALLOW_PARAM;
+ goto done;
+ }
+
+ param = bstr0("no");
+ }
+
+ // This is the only mandatory function
+ assert(co->opt->type->parse);
+
+ r = handle_set_opt_flags(config, co, flags);
+ if (r <= 0)
+ goto done;
+
+ if (r == 2) {
+ MP_VERBOSE(config, "Setting option '%.*s' = '%.*s' (flags = %d)\n",
+ BSTR_P(name), BSTR_P(param), flags);
+ }
+
+ union m_option_value val = m_option_value_default;
+
+ // Some option types are "impure" and work on the existing data.
+ // (Prime examples: --vf-add, --sub-file)
+ if (co->data)
+ m_option_copy(co->opt, &val, co->data);
+
+ r = m_option_parse(config->log, co->opt, name, param, &val);
+
+ if (r >= 0)
+ r = m_config_set_option_raw(config, co, &val, flags);
+
+ m_option_free(co->opt, &val);
+
+done:
+ if (r < 0 && r != M_OPT_EXIT) {
+ MP_ERR(config, "Error parsing option %.*s (%s)\n",
+ BSTR_P(name), m_option_strerror(r));
+ r = M_OPT_INVALID;
+ }
+ return r;
+}
+
+int m_config_set_option_node(struct m_config *config, bstr name,
+ struct mpv_node *data, int flags)
+{
+ int r;
+
+ struct m_config_option *co = m_config_get_co(config, name);
+ if (!co)
+ return M_OPT_UNKNOWN;
+
+ // Do this on an "empty" type to make setting the option strictly overwrite
+ // the old value, as opposed to e.g. appending to lists.
+ union m_option_value val = m_option_value_default;
+
+ if (data->format == MPV_FORMAT_STRING) {
+ bstr param = bstr0(data->u.string);
+ r = m_option_parse(mp_null_log, co->opt, name, param, &val);
+ } else {
+ r = m_option_set_node(co->opt, &val, data);
+ }
+
+ if (r >= 0)
+ r = m_config_set_option_raw(config, co, &val, flags);
+
+ if (mp_msg_test(config->log, MSGL_V)) {
+ char *s = m_option_type_node.print(NULL, data);
+ MP_DBG(config, "Setting option '%.*s' = %s (flags = %d) -> %d\n",
+ BSTR_P(name), s ? s : "?", flags, r);
+ talloc_free(s);
+ }
+
+ m_option_free(co->opt, &val);
+ return r;
+}
+
+int m_config_option_requires_param(struct m_config *config, bstr name)
+{
+ bool negate;
+ int flags;
+ struct m_config_option *co =
+ m_config_mogrify_cli_opt(config, &name, &negate, &flags);
+
+ if (!co)
+ return M_OPT_UNKNOWN;
+
+ if (negate || (flags & M_OPT_TYPE_OPTIONAL_PARAM))
+ return 0;
+
+ return m_option_required_params(co->opt);
+}
+
+static int sort_opt_compare(const void *pa, const void *pb)
+{
+ const struct m_config_option *a = pa;
+ const struct m_config_option *b = pb;
+ return strcasecmp(a->name, b->name);
+}
+
+void m_config_print_option_list(const struct m_config *config, const char *name)
+{
+ char min[50], max[50];
+ int count = 0;
+ const char *prefix = config->is_toplevel ? "--" : "";
+
+ struct m_config_option *sorted =
+ talloc_memdup(NULL, config->opts, config->num_opts * sizeof(sorted[0]));
+ if (config->is_toplevel)
+ qsort(sorted, config->num_opts, sizeof(sorted[0]), sort_opt_compare);
+
+ MP_INFO(config, "Options:\n\n");
+ for (int i = 0; i < config->num_opts; i++) {
+ struct m_config_option *co = &sorted[i];
+ const struct m_option *opt = co->opt;
+ if (strcmp(name, "*") != 0 && !strstr(co->name, name))
+ continue;
+ MP_INFO(config, " %s%-30s", prefix, co->name);
+ if (opt->type == &m_option_type_choice) {
+ MP_INFO(config, " Choices:");
+ const struct m_opt_choice_alternatives *alt = opt->priv;
+ for (int n = 0; alt[n].name; n++)
+ MP_INFO(config, " %s", alt[n].name);
+ if (opt->min < opt->max)
+ MP_INFO(config, " (or an integer)");
+ } else {
+ MP_INFO(config, " %s", opt->type->name);
+ }
+ if ((opt->type->flags & M_OPT_TYPE_USES_RANGE) && opt->min < opt->max) {
+ snprintf(min, sizeof(min), "any");
+ snprintf(max, sizeof(max), "any");
+ if (opt->min != DBL_MIN)
+ snprintf(min, sizeof(min), "%.14g", opt->min);
+ if (opt->max != DBL_MAX)
+ snprintf(max, sizeof(max), "%.14g", opt->max);
+ MP_INFO(config, " (%s to %s)", min, max);
+ }
+ char *def = NULL;
+ const void *defptr = m_config_get_co_default(config, co);
+ if (!defptr)
+ defptr = &m_option_value_default;
+ if (defptr)
+ def = m_option_pretty_print(opt, defptr);
+ if (def) {
+ MP_INFO(config, " (default: %s)", def);
+ talloc_free(def);
+ }
+ if (opt->flags & M_OPT_NOCFG)
+ MP_INFO(config, " [not in config files]");
+ if (opt->flags & M_OPT_FILE)
+ MP_INFO(config, " [file]");
+ if (opt->deprecation_message)
+ MP_INFO(config, " [deprecated]");
+ if (opt->type == &m_option_type_alias)
+ MP_INFO(config, " for %s", (char *)opt->priv);
+ if (opt->type == &m_option_type_cli_alias)
+ MP_INFO(config, " for --%s (CLI/config files only)", (char *)opt->priv);
+ MP_INFO(config, "\n");
+ for (int n = 0; opt->type->actions && opt->type->actions[n].name; n++) {
+ const struct m_option_action *action = &opt->type->actions[n];
+ MP_INFO(config, " %s%s-%s\n", prefix, co->name, action->name);
+ count++;
+ }
+ count++;
+ }
+ MP_INFO(config, "\nTotal: %d options\n", count);
+ talloc_free(sorted);
+}
+
+char **m_config_list_options(void *ta_parent, const struct m_config *config)
+{
+ char **list = talloc_new(ta_parent);
+ int count = 0;
+ for (int i = 0; i < config->num_opts; i++) {
+ struct m_config_option *co = &config->opts[i];
+ // For use with CONF_TYPE_STRING_LIST, it's important not to set list
+ // as allocation parent.
+ char *s = talloc_strdup(ta_parent, co->name);
+ MP_TARRAY_APPEND(ta_parent, list, count, s);
+ }
+ MP_TARRAY_APPEND(ta_parent, list, count, NULL);
+ return list;
+}
+
+struct m_profile *m_config_get_profile(const struct m_config *config, bstr name)
+{
+ for (struct m_profile *p = config->profiles; p; p = p->next) {
+ if (bstr_equals0(name, p->name))
+ return p;
+ }
+ return NULL;
+}
+
+struct m_profile *m_config_get_profile0(const struct m_config *config,
+ char *name)
+{
+ return m_config_get_profile(config, bstr0(name));
+}
+
+struct m_profile *m_config_add_profile(struct m_config *config, char *name)
+{
+ if (!name || !name[0])
+ name = "default";
+ struct m_profile *p = m_config_get_profile0(config, name);
+ if (p)
+ return p;
+ p = talloc_zero(config, struct m_profile);
+ p->name = talloc_strdup(p, name);
+ p->next = config->profiles;
+ config->profiles = p;
+ return p;
+}
+
+int m_config_set_profile_option(struct m_config *config, struct m_profile *p,
+ bstr name, bstr val)
+{
+ if (bstr_equals0(name, "profile-desc")) {
+ talloc_free(p->desc);
+ p->desc = bstrto0(p, val);
+ return 0;
+ }
+ if (bstr_equals0(name, "profile-cond")) {
+ TA_FREEP(&p->cond);
+ val = bstr_strip(val);
+ if (val.len)
+ p->cond = bstrto0(p, val);
+ return 0;
+ }
+ if (bstr_equals0(name, profile_restore_mode_opt.name)) {
+ return m_option_parse(config->log, &profile_restore_mode_opt, name, val,
+ &p->restore_mode);
+ }
+
+ int i = m_config_set_option_cli(config, name, val,
+ M_SETOPT_CHECK_ONLY |
+ M_SETOPT_FROM_CONFIG_FILE);
+ if (i < 0)
+ return i;
+ p->opts = talloc_realloc(p, p->opts, char *, 2 * (p->num_opts + 2));
+ p->opts[p->num_opts * 2] = bstrto0(p, name);
+ p->opts[p->num_opts * 2 + 1] = bstrto0(p, val);
+ p->num_opts++;
+ p->opts[p->num_opts * 2] = p->opts[p->num_opts * 2 + 1] = NULL;
+ return 1;
+}
+
+static struct m_profile *find_check_profile(struct m_config *config, char *name)
+{
+ struct m_profile *p = m_config_get_profile0(config, name);
+ if (!p) {
+ MP_WARN(config, "Unknown profile '%s'.\n", name);
+ return NULL;
+ }
+ if (config->profile_depth > MAX_PROFILE_DEPTH) {
+ MP_WARN(config, "WARNING: Profile inclusion too deep.\n");
+ return NULL;
+ }
+ return p;
+}
+
+int m_config_set_profile(struct m_config *config, char *name, int flags)
+{
+ MP_VERBOSE(config, "Applying profile '%s'...\n", name);
+ struct m_profile *p = find_check_profile(config, name);
+ if (!p)
+ return M_OPT_INVALID;
+
+ if (!config->profile_backup_tmp && p->restore_mode) {
+ config->profile_backup_tmp = &p->backups;
+ config->profile_backup_flags = p->restore_mode == 2 ? BACKUP_NVAL : 0;
+ }
+
+ config->profile_depth++;
+ for (int i = 0; i < p->num_opts; i++) {
+ m_config_set_option_cli(config,
+ bstr0(p->opts[2 * i]),
+ bstr0(p->opts[2 * i + 1]),
+ flags | M_SETOPT_FROM_CONFIG_FILE);
+ }
+ config->profile_depth--;
+
+ if (config->profile_backup_tmp == &p->backups) {
+ config->profile_backup_tmp = NULL;
+
+ for (struct m_opt_backup *bc = p->backups; bc; bc = bc->next) {
+ if (bc && bc->nval)
+ m_option_copy(bc->co->opt, bc->nval, bc->co->data);
+ talloc_steal(p, bc);
+ }
+ }
+
+ return 0;
+}
+
+int m_config_restore_profile(struct m_config *config, char *name)
+{
+ MP_VERBOSE(config, "Restoring from profile '%s'...\n", name);
+ struct m_profile *p = find_check_profile(config, name);
+ if (!p)
+ return M_OPT_INVALID;
+
+ if (!p->backups)
+ MP_WARN(config, "Profile '%s' contains no restore data.\n", name);
+
+ restore_backups(&p->backups, config);
+
+ return 0;
+}
+
+void m_config_finish_default_profile(struct m_config *config, int flags)
+{
+ struct m_profile *p = m_config_add_profile(config, NULL);
+ m_config_set_profile(config, p->name, flags);
+ p->num_opts = 0;
+}
+
+struct mpv_node m_config_get_profiles(struct m_config *config)
+{
+ struct mpv_node root;
+ node_init(&root, MPV_FORMAT_NODE_ARRAY, NULL);
+
+ for (m_profile_t *profile = config->profiles; profile; profile = profile->next)
+ {
+ struct mpv_node *entry = node_array_add(&root, MPV_FORMAT_NODE_MAP);
+
+ node_map_add_string(entry, "name", profile->name);
+ if (profile->desc)
+ node_map_add_string(entry, "profile-desc", profile->desc);
+ if (profile->cond)
+ node_map_add_string(entry, "profile-cond", profile->cond);
+ if (profile->restore_mode) {
+ char *s =
+ m_option_print(&profile_restore_mode_opt, &profile->restore_mode);
+ node_map_add_string(entry, profile_restore_mode_opt.name, s);
+ talloc_free(s);
+ }
+
+ struct mpv_node *opts =
+ node_map_add(entry, "options", MPV_FORMAT_NODE_ARRAY);
+
+ for (int n = 0; n < profile->num_opts; n++) {
+ struct mpv_node *opt_entry = node_array_add(opts, MPV_FORMAT_NODE_MAP);
+ node_map_add_string(opt_entry, "key", profile->opts[n * 2 + 0]);
+ node_map_add_string(opt_entry, "value", profile->opts[n * 2 + 1]);
+ }
+ }
+
+ return root;
+}
diff --git a/options/m_config_frontend.h b/options/m_config_frontend.h
new file mode 100644
index 0000000..6108d9f
--- /dev/null
+++ b/options/m_config_frontend.h
@@ -0,0 +1,266 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "m_config_core.h"
+#include "misc/bstr.h"
+#include "misc/dispatch.h"
+#include "options/m_option.h"
+
+// m_config provides an API to manipulate the config variables in MPlayer.
+// It makes use of the Options API to provide a context stack that
+// allows saving and later restoring the state of all variables.
+
+typedef struct m_profile m_profile_t;
+struct m_option;
+struct m_option_type;
+struct m_sub_options;
+struct m_obj_desc;
+struct m_obj_settings;
+struct mp_log;
+struct mp_dispatch_queue;
+
+// Config option
+struct m_config_option {
+ bool is_set_from_cmdline : 1; // Set by user from command line
+ bool is_set_from_config : 1; // Set by a config file
+ bool is_set_locally : 1; // Has a backup entry
+ bool warning_was_printed : 1;
+ int32_t opt_id; // For some m_config APIs
+ const char *name; // Full name (ie option-subopt)
+ const struct m_option *opt; // Option description
+ void *data; // Raw value of the option
+};
+
+// Config object
+/** \ingroup Config */
+typedef struct m_config {
+ struct mp_log *log;
+ struct mpv_global *global; // can be NULL
+
+ // Registered options.
+ struct m_config_option *opts; // all options, even suboptions
+ int num_opts;
+
+ // List of defined profiles.
+ struct m_profile *profiles;
+ // Depth when recursively including profiles.
+ int profile_depth;
+ // Temporary during profile application.
+ struct m_opt_backup **profile_backup_tmp;
+ int profile_backup_flags;
+
+ struct m_opt_backup *backup_opts;
+ struct m_opt_backup *watch_later_backup_opts;
+
+ bool use_profiles;
+ bool is_toplevel;
+ int (*includefunc)(void *ctx, char *filename, int flags);
+ void *includefunc_ctx;
+
+ // Notification after an option was successfully written to.
+ // Uses flags as set in UPDATE_OPTS_MASK.
+ // self_update==true means the update was caused by a call to
+ // m_config_notify_change_opt_ptr(). If false, it's caused either by
+ // m_config_set_option_*() (and similar) calls or external updates.
+ void (*option_change_callback)(void *ctx, struct m_config_option *co,
+ int flags, bool self_update);
+ void *option_change_callback_ctx;
+
+ // For the command line parser
+ int recursion_depth;
+
+ void *optstruct; // struct mpopts or other
+
+ // Private. Non-NULL if data was allocated. m_config_option.data uses it.
+ // API users call m_config_set_update_dispatch_queue() to get async updates.
+ struct m_config_cache *cache;
+
+ // Private. Thread-safe shadow memory; only set for the main m_config.
+ struct m_config_shadow *shadow;
+} m_config_t;
+
+// Create a new config object.
+// talloc_ctx: talloc parent context for the m_config allocation
+// root: description of all options
+// Note that the m_config object will keep pointers to root and log.
+struct m_config *m_config_new(void *talloc_ctx, struct mp_log *log,
+ const struct m_sub_options *root);
+
+// Create a m_config for the given desc. This is for --af/--vf, which have
+// different sub-options for every filter (represented by separate desc
+// structs).
+// args is an array of key/value pairs (args=[k0, v0, k1, v1, ..., NULL]).
+struct m_config *m_config_from_obj_desc_and_args(void *ta_parent,
+ struct mp_log *log, struct mpv_global *global, struct m_obj_desc *desc,
+ char **args);
+
+// Like m_config_from_obj_desc_and_args(), but don't allocate option the
+// struct, i.e. m_config.optstruct==NULL. This is used by the sub-option
+// parser (--af/--vf, to a lesser degree --ao/--vo) to check sub-option names
+// and types.
+struct m_config *m_config_from_obj_desc_noalloc(void *talloc_ctx,
+ struct mp_log *log,
+ struct m_obj_desc *desc);
+
+// Make sure the option is backed up. If it's already backed up, do nothing.
+// All backed up options can be restored with m_config_restore_backups().
+void m_config_backup_opt(struct m_config *config, const char *opt);
+
+// Call m_config_backup_opt() on all options.
+void m_config_backup_all_opts(struct m_config *config);
+
+// Backup options on startup so that quit-watch-later can compare the current
+// values to their backups, and save them only if they have been changed.
+void m_config_backup_watch_later_opts(struct m_config *config);
+
+// Restore all options backed up with m_config_backup_opt(), and delete the
+// backups afterwards.
+void m_config_restore_backups(struct m_config *config);
+
+// Whether opt_name is different from its initial value.
+bool m_config_watch_later_backup_opt_changed(struct m_config *config,
+ char *opt_name);
+
+enum {
+ M_SETOPT_PRE_PARSE_ONLY = 1, // Silently ignore non-M_OPT_PRE_PARSE opt.
+ M_SETOPT_CHECK_ONLY = 2, // Don't set, just check name/value
+ M_SETOPT_FROM_CONFIG_FILE = 4, // Reject M_OPT_NOCFG opt. (print error)
+ M_SETOPT_FROM_CMDLINE = 8, // Mark as set by command line
+ M_SETOPT_BACKUP = 16, // Call m_config_backup_opt() before
+ M_SETOPT_PRESERVE_CMDLINE = 32, // Don't set if already marked as FROM_CMDLINE
+ M_SETOPT_NO_PRE_PARSE = 128, // Reject M_OPT_PREPARSE options
+ M_SETOPT_NO_OVERWRITE = 256, // Skip options marked with FROM_*
+};
+
+// Set the named option to the given string. This is for command line and config
+// file use only.
+// flags: combination of M_SETOPT_* flags (0 for normal operation)
+// Returns >= 0 on success, otherwise see OptionParserReturn.
+int m_config_set_option_cli(struct m_config *config, struct bstr name,
+ struct bstr param, int flags);
+
+// Similar to m_config_set_option_cli(), but set as data in its native format.
+// This takes care of some details like sending change notifications.
+// The type data points to is as in: co->opt
+int m_config_set_option_raw(struct m_config *config, struct m_config_option *co,
+ void *data, int flags);
+
+void m_config_mark_co_flags(struct m_config_option *co, int flags);
+
+// Convert the mpv_node to raw option data, then call m_config_set_option_raw().
+struct mpv_node;
+int m_config_set_option_node(struct m_config *config, bstr name,
+ struct mpv_node *data, int flags);
+
+// Return option descriptor. You shouldn't use this.
+struct m_config_option *m_config_get_co(const struct m_config *config,
+ struct bstr name);
+// Same as above, but does not resolve aliases or trigger warning messages.
+struct m_config_option *m_config_get_co_raw(const struct m_config *config,
+ struct bstr name);
+
+// Special uses only. Look away.
+int m_config_get_co_count(struct m_config *config);
+struct m_config_option *m_config_get_co_index(struct m_config *config, int index);
+const void *m_config_get_co_default(const struct m_config *config,
+ struct m_config_option *co);
+
+// Return the n-th option by position. n==0 is the first option. If there are
+// less than (n + 1) options, return NULL.
+const char *m_config_get_positional_option(const struct m_config *config, int n);
+
+// Return a hint to the option parser whether a parameter is/may be required.
+// The option may still accept empty/non-empty parameters independent from
+// this, and this function is useful only for handling ambiguous options like
+// flags (e.g. "--a" is ok, "--a=yes" is also ok).
+// Returns: error code (<0), or number of expected params (0, 1)
+int m_config_option_requires_param(struct m_config *config, bstr name);
+
+// Notify m_config_cache users that the option has (probably) changed its value.
+// This will force a self-notification back to config->option_change_callback.
+void m_config_notify_change_opt_ptr(struct m_config *config, void *ptr);
+
+// Exactly like m_config_notify_change_opt_ptr(), but the option change callback
+// (config->option_change_callback()) is invoked with self_update=false, if at all.
+void m_config_notify_change_opt_ptr_notify(struct m_config *config, void *ptr);
+
+// Return all (visible) option names as NULL terminated string list.
+char **m_config_list_options(void *ta_parent, const struct m_config *config);
+
+void m_config_print_option_list(const struct m_config *config, const char *name);
+
+
+/* Find the profile with the given name.
+ * \param config The config object.
+ * \param arg The profile's name.
+ * \return The profile object or NULL.
+ */
+struct m_profile *m_config_get_profile0(const struct m_config *config,
+ char *name);
+struct m_profile *m_config_get_profile(const struct m_config *config, bstr name);
+
+// Apply and clear the default profile - it's the only profile that new config
+// files do not simply append to (for configfile parser).
+void m_config_finish_default_profile(struct m_config *config, int flags);
+
+/* Get the profile with the given name, creating it if necessary.
+ * \param config The config object.
+ * \param arg The profile's name.
+ * \return The profile object.
+ */
+struct m_profile *m_config_add_profile(struct m_config *config, char *name);
+
+/* Add an option to a profile.
+ * Used by the config file parser when defining a profile.
+ *
+ * \param config The config object.
+ * \param p The profile object.
+ * \param name The option's name.
+ * \param val The option's value.
+ */
+int m_config_set_profile_option(struct m_config *config, struct m_profile *p,
+ bstr name, bstr val);
+
+/* Enables profile usage
+ * Used by the config file parser when loading a profile.
+ *
+ * \param config The config object.
+ * \param p The profile object.
+ * \param flags M_SETOPT_* bits
+ * Returns error code (<0) or 0 on success
+ */
+int m_config_set_profile(struct m_config *config, char *name, int flags);
+
+// Attempt to "unset" a profile if possible.
+int m_config_restore_profile(struct m_config *config, char *name);
+
+struct mpv_node m_config_get_profiles(struct m_config *config);
+
+// Run async option updates here. This will call option_change_callback() on it.
+void m_config_set_update_dispatch_queue(struct m_config *config,
+ struct mp_dispatch_queue *dispatch);
diff --git a/options/m_option.c b/options/m_option.c
new file mode 100644
index 0000000..1b1ac0a
--- /dev/null
+++ b/options/m_option.c
@@ -0,0 +1,3866 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/// \file
+/// \ingroup Options
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "libmpv/client.h"
+#include "player/client.h"
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "misc/json.h"
+#include "misc/node.h"
+#include "m_option.h"
+#include "m_config_frontend.h"
+
+#if HAVE_DOS_PATHS
+#define OPTION_PATH_SEPARATOR ';'
+#else
+#define OPTION_PATH_SEPARATOR ':'
+#endif
+
+const char m_option_path_separator = OPTION_PATH_SEPARATOR;
+
+// For integer types: since min/max are floats and may not be able to represent
+// the real min/max, and since opt.min/.max may use +/-INFINITY, some care has
+// to be taken. (Also tricky rounding.)
+#define OPT_INT_MIN(opt, T, Tm) ((opt)->min < (opt)->max \
+ ? ((opt)->min <= (double)(Tm) ? (Tm) : (T)((opt)->min)) : (Tm))
+#define OPT_INT_MAX(opt, T, Tm) ((opt)->min < (opt)->max \
+ ? ((opt)->max >= (double)(Tm) ? (Tm) : (T)((opt)->max)) : (Tm))
+
+int m_option_parse(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ int r = M_OPT_INVALID;
+ if (bstr_equals0(param, "help") && opt->help) {
+ r = opt->help(log, opt, name);
+ if (r < 0)
+ return r;
+ }
+
+ r = opt->type->parse(log, opt, name, param, dst);
+ if (r < 0)
+ return r;
+
+ if (opt->validate) {
+ r = opt->validate(log, opt, name, dst);
+ if (r < 0) {
+ if (opt->type->free)
+ opt->type->free(dst);
+ return r;
+ }
+ }
+ return 1;
+}
+
+char *m_option_strerror(int code)
+{
+ switch (code) {
+ case M_OPT_UNKNOWN:
+ return "option not found";
+ case M_OPT_MISSING_PARAM:
+ return "option requires parameter";
+ case M_OPT_INVALID:
+ return "option parameter could not be parsed";
+ case M_OPT_OUT_OF_RANGE:
+ return "parameter is outside values allowed for option";
+ case M_OPT_DISALLOW_PARAM:
+ return "option doesn't take a parameter";
+ default:
+ return "parser error";
+ }
+}
+
+int m_option_required_params(const m_option_t *opt)
+{
+ if (opt->type->flags & M_OPT_TYPE_OPTIONAL_PARAM)
+ return 0;
+ if (opt->flags & M_OPT_OPTIONAL_PARAM)
+ return 0;
+ if (opt->type == &m_option_type_choice) {
+ const struct m_opt_choice_alternatives *alt;
+ for (alt = opt->priv; alt->name; alt++) {
+ if (strcmp(alt->name, "yes") == 0)
+ return 0;
+ }
+ }
+ return 1;
+}
+
+int m_option_set_node_or_string(struct mp_log *log, const m_option_t *opt,
+ const char *name, void *dst, struct mpv_node *src)
+{
+ if (src->format == MPV_FORMAT_STRING) {
+ // The af and vf option unfortunately require this, because the
+ // option name includes the "action".
+ bstr optname = bstr0(name), a, b;
+ if (bstr_split_tok(optname, "/", &a, &b))
+ optname = b;
+ return m_option_parse(log, opt, optname, bstr0(src->u.string), dst);
+ } else {
+ return m_option_set_node(opt, dst, src);
+ }
+}
+
+// Default function that just does a memcpy
+
+static void copy_opt(const m_option_t *opt, void *dst, const void *src)
+{
+ if (dst && src)
+ memcpy(dst, src, opt->type->size);
+}
+
+// Bool
+
+#define VAL(x) (*(bool *)(x))
+
+static int parse_bool(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (bstr_equals0(param, "yes") || !param.len) {
+ if (dst)
+ VAL(dst) = 1;
+ return 1;
+ }
+ if (bstr_equals0(param, "no")) {
+ if (dst)
+ VAL(dst) = 0;
+ return 1;
+ }
+ bool is_help = bstr_equals0(param, "help");
+ if (is_help) {
+ mp_info(log, "Valid values for %.*s flag are:\n", BSTR_P(name));
+ } else {
+ mp_fatal(log, "Invalid parameter for %.*s flag: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ mp_info(log, "Valid values are:\n");
+ }
+ mp_info(log, " yes\n");
+ mp_info(log, " no\n");
+ mp_info(log, " (passing nothing)\n");
+ return is_help ? M_OPT_EXIT : M_OPT_INVALID;
+}
+
+static char *print_bool(const m_option_t *opt, const void *val)
+{
+ return talloc_strdup(NULL, VAL(val) ? "yes" : "no");
+}
+
+static void add_bool(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ if (fabs(add) < 0.5)
+ return;
+ bool state = !!VAL(val);
+ state = wrap ? !state : add > 0;
+ VAL(val) = state ? 1 : 0;
+}
+
+static int bool_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ if (src->format != MPV_FORMAT_FLAG)
+ return M_OPT_UNKNOWN;
+ VAL(dst) = !!src->u.flag;
+ return 1;
+}
+
+static int bool_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = !!VAL(src);
+ return 1;
+}
+
+static bool bool_equal(const m_option_t *opt, void *a, void *b)
+{
+ return VAL(a) == VAL(b);
+}
+
+const m_option_type_t m_option_type_bool = {
+ .name = "Flag", // same as m_option_type_flag; transparent to user
+ .size = sizeof(bool),
+ .flags = M_OPT_TYPE_OPTIONAL_PARAM | M_OPT_TYPE_CHOICE,
+ .parse = parse_bool,
+ .print = print_bool,
+ .copy = copy_opt,
+ .add = add_bool,
+ .set = bool_set,
+ .get = bool_get,
+ .equal = bool_equal,
+};
+
+#undef VAL
+
+// Flag
+
+#define VAL(x) (*(int *)(x))
+
+static int parse_flag(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ bool bdst = false;
+ int r = parse_bool(log, opt, name, param, &bdst);
+ if (dst)
+ VAL(dst) = bdst;
+ return r;
+}
+
+static char *print_flag(const m_option_t *opt, const void *val)
+{
+ return print_bool(opt, &(bool){VAL(val)});
+}
+
+static void add_flag(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ bool bval = VAL(val);
+ add_bool(opt, &bval, add, wrap);
+ VAL(val) = bval;
+}
+
+static int flag_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ bool bdst = false;
+ int r = bool_set(opt, &bdst, src);
+ if (r >= 0)
+ VAL(dst) = bdst;
+ return r;
+}
+
+static int flag_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ return bool_get(opt, ta_parent, dst, &(bool){VAL(src)});
+}
+
+static bool flag_equal(const m_option_t *opt, void *a, void *b)
+{
+ return VAL(a) == VAL(b);
+}
+
+// Only exists for libmpv interopability and should not be used anywhere.
+const m_option_type_t m_option_type_flag = {
+ // need yes or no in config files
+ .name = "Flag",
+ .size = sizeof(int),
+ .flags = M_OPT_TYPE_OPTIONAL_PARAM | M_OPT_TYPE_CHOICE,
+ .parse = parse_flag,
+ .print = print_flag,
+ .copy = copy_opt,
+ .add = add_flag,
+ .set = flag_set,
+ .get = flag_get,
+ .equal = flag_equal,
+};
+
+// Integer
+
+#undef VAL
+
+static int clamp_longlong(const m_option_t *opt, long long i_min, long long i_max,
+ void *val)
+{
+ long long v = *(long long *)val;
+ int r = 0;
+ long long min = OPT_INT_MIN(opt, long long, i_min);
+ long long max = OPT_INT_MAX(opt, long long, i_max);
+ if (v > max) {
+ v = max;
+ r = M_OPT_OUT_OF_RANGE;
+ }
+ if (v < min) {
+ v = min;
+ r = M_OPT_OUT_OF_RANGE;
+ }
+ *(long long *)val = v;
+ return r;
+}
+
+static int parse_longlong(struct mp_log *log, const m_option_t *opt,
+ long long i_min, long long i_max,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ struct bstr rest;
+ long long tmp_int = bstrtoll(param, &rest, 10);
+ if (rest.len)
+ tmp_int = bstrtoll(param, &rest, 0);
+ if (rest.len) {
+ mp_err(log, "The %.*s option must be an integer: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+ }
+
+ long long min = OPT_INT_MIN(opt, long long, i_min);
+ if (tmp_int < min) {
+ mp_err(log, "The %.*s option must be >= %lld: %.*s\n",
+ BSTR_P(name), min, BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+ long long max = OPT_INT_MAX(opt, long long, i_max);
+ if (tmp_int > max) {
+ mp_err(log, "The %.*s option must be <= %lld: %.*s\n",
+ BSTR_P(name), max, BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+ if (dst)
+ *(long long *)dst = tmp_int;
+
+ return 1;
+}
+
+static int clamp_int64(const m_option_t *opt, void *val)
+{
+ long long tmp = *(int64_t *)val;
+ int r = clamp_longlong(opt, INT64_MIN, INT64_MAX, &tmp);
+ *(int64_t *)val = tmp;
+ return r;
+}
+
+static int parse_int(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ long long tmp;
+ int r = parse_longlong(log, opt, INT_MIN, INT_MAX, name, param, &tmp);
+ if (r >= 0 && dst)
+ *(int *)dst = tmp;
+ return r;
+}
+
+static int parse_int64(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ long long tmp;
+ int r = parse_longlong(log, opt, INT64_MIN, INT64_MAX, name, param, &tmp);
+ if (r >= 0 && dst)
+ *(int64_t *)dst = tmp;
+ return r;
+}
+
+static char *print_int(const m_option_t *opt, const void *val)
+{
+ if (opt->type->size == sizeof(int64_t))
+ return talloc_asprintf(NULL, "%"PRId64, *(const int64_t *)val);
+ return talloc_asprintf(NULL, "%d", *(const int *)val);
+}
+
+static void add_int64(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ int64_t v = *(int64_t *)val;
+
+ clamp_int64(opt, &v);
+
+ v = v + add;
+
+ bool is64 = opt->type->size == sizeof(int64_t);
+ int64_t nmin = is64 ? INT64_MIN : INT_MIN;
+ int64_t nmax = is64 ? INT64_MAX : INT_MAX;
+
+ int64_t min = OPT_INT_MIN(opt, int64_t, nmin);
+ int64_t max = OPT_INT_MAX(opt, int64_t, nmax);
+
+ if (v < min)
+ v = wrap ? max : min;
+ if (v > max)
+ v = wrap ? min : max;
+
+ *(int64_t *)val = v;
+}
+
+static void add_int(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ int64_t tmp = *(int *)val;
+ add_int64(opt, &tmp, add, wrap);
+ *(int *)val = tmp;
+}
+
+static void multiply_int64(const m_option_t *opt, void *val, double f)
+{
+ double v = *(int64_t *)val * f;
+ int64_t iv = v;
+ if (v < INT64_MIN)
+ iv = INT64_MIN;
+ if (v >= (double)INT64_MAX)
+ iv = INT64_MAX;
+ *(int64_t *)val = iv;
+ clamp_int64(opt, val);
+}
+
+static void multiply_int(const m_option_t *opt, void *val, double f)
+{
+ int64_t tmp = *(int *)val;
+ multiply_int64(opt, &tmp, f);
+ *(int *)val = MPCLAMP(tmp, INT_MIN, INT_MAX);
+}
+
+static int int64_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ if (src->format != MPV_FORMAT_INT64)
+ return M_OPT_UNKNOWN;
+ int64_t val = src->u.int64;
+ if (val < OPT_INT_MIN(opt, int64_t, INT64_MIN))
+ return M_OPT_OUT_OF_RANGE;
+ if (val > OPT_INT_MAX(opt, int64_t, INT64_MAX))
+ return M_OPT_OUT_OF_RANGE;
+ *(int64_t *)dst = val;
+ return 1;
+}
+
+static int int_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ int64_t val;
+ int r = int64_set(opt, &val, src);
+ if (r >= 0) {
+ if (val < INT_MIN || val > INT_MAX)
+ return M_OPT_OUT_OF_RANGE;
+ *(int *)dst = val;
+ }
+ return r;
+}
+
+static int int64_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = *(int64_t *)src;
+ return 1;
+}
+
+static int int_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = *(int *)src;
+ return 1;
+}
+
+static bool int_equal(const m_option_t *opt, void *a, void *b)
+{
+ return *(int *)a == *(int *)b;
+}
+
+static bool int64_equal(const m_option_t *opt, void *a, void *b)
+{
+ return *(int64_t *)a == *(int64_t *)b;
+}
+
+const m_option_type_t m_option_type_int = {
+ .name = "Integer",
+ .flags = M_OPT_TYPE_USES_RANGE,
+ .size = sizeof(int),
+ .parse = parse_int,
+ .print = print_int,
+ .copy = copy_opt,
+ .add = add_int,
+ .multiply = multiply_int,
+ .set = int_set,
+ .get = int_get,
+ .equal = int_equal,
+};
+
+const m_option_type_t m_option_type_int64 = {
+ .name = "Integer64",
+ .flags = M_OPT_TYPE_USES_RANGE,
+ .size = sizeof(int64_t),
+ .parse = parse_int64,
+ .print = print_int,
+ .copy = copy_opt,
+ .add = add_int64,
+ .multiply = multiply_int64,
+ .set = int64_set,
+ .get = int64_get,
+ .equal = int64_equal,
+};
+
+static int parse_byte_size(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ struct bstr r;
+ long long tmp_int = bstrtoll(param, &r, 0);
+ int64_t unit = 1;
+ if (r.len) {
+ if (bstrcasecmp0(r, "b") == 0) {
+ unit = 1;
+ } else if (bstrcasecmp0(r, "kib") == 0 || bstrcasecmp0(r, "k") == 0) {
+ unit = 1024;
+ } else if (bstrcasecmp0(r, "mib") == 0 || bstrcasecmp0(r, "m") == 0) {
+ unit = 1024 * 1024;
+ } else if (bstrcasecmp0(r, "gib") == 0 || bstrcasecmp0(r, "g") == 0) {
+ unit = 1024 * 1024 * 1024;
+ } else if (bstrcasecmp0(r, "tib") == 0 || bstrcasecmp0(r, "t") == 0) {
+ unit = 1024 * 1024 * 1024 * 1024LL;
+ } else {
+ mp_err(log, "The %.*s option must be an integer: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ mp_err(log, "The following suffixes are also allowed: "
+ "KiB, MiB, GiB, TiB, B, K, M, G, T.\n");
+ return M_OPT_INVALID;
+ }
+ }
+
+ if (tmp_int < 0) {
+ mp_err(log, "The %.*s option does not support negative numbers: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+ if (INT64_MAX / unit < tmp_int) {
+ mp_err(log, "The %.*s option overflows: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+ tmp_int *= unit;
+
+ int64_t min = OPT_INT_MIN(opt, int64_t, INT64_MIN);
+ if (tmp_int < min) {
+ mp_err(log, "The %.*s option must be >= %"PRId64": %.*s\n",
+ BSTR_P(name), min, BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+ int64_t max = OPT_INT_MAX(opt, int64_t, INT64_MAX);
+ if (tmp_int > max) {
+ mp_err(log, "The %.*s option must be <= %"PRId64": %.*s\n",
+ BSTR_P(name), max, BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+ if (dst)
+ *(int64_t *)dst = tmp_int;
+
+ return 1;
+}
+
+char *format_file_size(int64_t size)
+{
+ double s = size;
+ if (size < 1024)
+ return talloc_asprintf(NULL, "%.0f B", s);
+
+ if (size < (1024 * 1024))
+ return talloc_asprintf(NULL, "%.3f KiB", s / (1024.0));
+
+ if (size < (1024 * 1024 * 1024))
+ return talloc_asprintf(NULL, "%.3f MiB", s / (1024.0 * 1024.0));
+
+ if (size < (1024LL * 1024LL * 1024LL * 1024LL))
+ return talloc_asprintf(NULL, "%.3f GiB", s / (1024.0 * 1024.0 * 1024.0));
+
+ return talloc_asprintf(NULL, "%.3f TiB", s / (1024.0 * 1024.0 * 1024.0 * 1024.0));
+}
+
+static char *pretty_print_byte_size(const m_option_t *opt, const void *val)
+{
+ return format_file_size(*(int64_t *)val);
+}
+
+const m_option_type_t m_option_type_byte_size = {
+ .name = "ByteSize",
+ .flags = M_OPT_TYPE_USES_RANGE,
+ .size = sizeof(int64_t),
+ .parse = parse_byte_size,
+ .print = print_int,
+ .pretty_print = pretty_print_byte_size,
+ .copy = copy_opt,
+ .add = add_int64,
+ .multiply = multiply_int64,
+ .set = int64_set,
+ .get = int64_get,
+ .equal = int64_equal,
+};
+
+const char *m_opt_choice_str(const struct m_opt_choice_alternatives *choices,
+ int value)
+{
+ for (const struct m_opt_choice_alternatives *c = choices; c->name; c++) {
+ if (c->value == value)
+ return c->name;
+ }
+ return NULL;
+}
+
+static void print_choice_values(struct mp_log *log, const struct m_option *opt)
+{
+ const struct m_opt_choice_alternatives *alt = opt->priv;
+ for ( ; alt->name; alt++)
+ mp_info(log, " %s\n", alt->name[0] ? alt->name : "(passing nothing)");
+ if (opt->min < opt->max)
+ mp_info(log, " %g-%g (integer range)\n", opt->min, opt->max);
+}
+
+static int parse_choice(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ const struct m_opt_choice_alternatives *alt = opt->priv;
+ for ( ; alt->name; alt++) {
+ if (!bstrcmp0(param, alt->name))
+ break;
+ }
+ if (!alt->name && param.len == 0) {
+ // allow flag-style options, e.g. "--mute" implies "--mute=yes"
+ for (alt = opt->priv; alt->name; alt++) {
+ if (!strcmp("yes", alt->name))
+ break;
+ }
+ }
+ if (!alt->name) {
+ if (!bstrcmp0(param, "help")) {
+ mp_info(log, "Valid values for option %.*s are:\n", BSTR_P(name));
+ print_choice_values(log, opt);
+ return M_OPT_EXIT;
+ }
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+ if (opt->min < opt->max) {
+ long long val;
+ if (parse_longlong(mp_null_log, opt, INT_MIN, INT_MAX, name, param,
+ &val) == 1)
+ {
+ if (dst)
+ *(int *)dst = val;
+ return 1;
+ }
+ }
+ mp_fatal(log, "Invalid value for option %.*s: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ mp_info(log, "Valid values are:\n");
+ print_choice_values(log, opt);
+ return M_OPT_INVALID;
+ }
+ if (dst)
+ *(int *)dst = alt->value;
+
+ return 1;
+}
+
+static void choice_get_min_max(const struct m_option *opt, int *min, int *max)
+{
+ assert(opt->type == &m_option_type_choice);
+ *min = INT_MAX;
+ *max = INT_MIN;
+ for (const struct m_opt_choice_alternatives *alt = opt->priv; alt->name; alt++) {
+ *min = MPMIN(*min, alt->value);
+ *max = MPMAX(*max, alt->value);
+ }
+ if (opt->min < opt->max) {
+ *min = MPMIN(*min, opt->min);
+ *max = MPMAX(*max, opt->max);
+ }
+}
+
+static void check_choice(int dir, int val, bool *found, int *best, int choice)
+{
+ if ((dir == -1 && (!(*found) || choice > (*best)) && choice < val) ||
+ (dir == +1 && (!(*found) || choice < (*best)) && choice > val))
+ {
+ *found = true;
+ *best = choice;
+ }
+}
+
+static void add_choice(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ assert(opt->type == &m_option_type_choice);
+ int dir = add > 0 ? +1 : -1;
+ bool found = false;
+ int ival = *(int *)val;
+ int best = 0; // init. value unused
+
+ if (fabs(add) < 0.5)
+ return;
+
+ if (opt->min < opt->max) {
+ int newval = ival + add;
+ if (ival >= opt->min && ival <= opt->max &&
+ newval >= opt->min && newval <= opt->max)
+ {
+ found = true;
+ best = newval;
+ } else {
+ check_choice(dir, ival, &found, &best, opt->min);
+ check_choice(dir, ival, &found, &best, opt->max);
+ }
+ }
+
+ for (const struct m_opt_choice_alternatives *alt = opt->priv; alt->name; alt++)
+ check_choice(dir, ival, &found, &best, alt->value);
+
+ if (!found) {
+ int min, max;
+ choice_get_min_max(opt, &min, &max);
+ best = (dir == -1) ^ wrap ? min : max;
+ }
+
+ *(int *)val = best;
+}
+
+static int choice_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ char buf[80];
+ char *src_str = NULL;
+ if (src->format == MPV_FORMAT_INT64) {
+ snprintf(buf, sizeof(buf), "%" PRId64, src->u.int64);
+ src_str = buf;
+ } else if (src->format == MPV_FORMAT_STRING) {
+ src_str = src->u.string;
+ } else if (src->format == MPV_FORMAT_FLAG) {
+ src_str = src->u.flag ? "yes" : "no";
+ }
+ if (!src_str)
+ return M_OPT_UNKNOWN;
+ int val = 0;
+ int r = parse_choice(mp_null_log, opt, (bstr){0}, bstr0(src_str), &val);
+ if (r >= 0)
+ *(int *)dst = val;
+ return r;
+}
+
+static const struct m_opt_choice_alternatives *get_choice(const m_option_t *opt,
+ const void *val,
+ int *out_val)
+{
+ int v = *(int *)val;
+ const struct m_opt_choice_alternatives *alt;
+ for (alt = opt->priv; alt->name; alt++) {
+ if (alt->value == v)
+ return alt;
+ }
+ if (opt->min < opt->max) {
+ if (v >= opt->min && v <= opt->max) {
+ *out_val = v;
+ return NULL;
+ }
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+static int choice_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ int ival = 0;
+ const struct m_opt_choice_alternatives *alt = get_choice(opt, src, &ival);
+ // If a choice string looks like a number, return it as number
+ if (alt) {
+ char *end = NULL;
+ ival = strtol(alt->name, &end, 10);
+ if (end && !end[0])
+ alt = NULL;
+ }
+ if (alt) {
+ int b = -1;
+ if (strcmp(alt->name, "yes") == 0) {
+ b = 1;
+ } else if (strcmp(alt->name, "no") == 0) {
+ b = 0;
+ }
+ if (b >= 0) {
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = b;
+ } else {
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(ta_parent, alt->name);
+ }
+ } else {
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = ival;
+ }
+ return 1;
+}
+
+static char *print_choice(const m_option_t *opt, const void *val)
+{
+ int ival = 0;
+ const struct m_opt_choice_alternatives *alt = get_choice(opt, val, &ival);
+ return alt ? talloc_strdup(NULL, alt->name)
+ : talloc_asprintf(NULL, "%d", ival);
+}
+
+const struct m_option_type m_option_type_choice = {
+ .name = "Choice",
+ .size = sizeof(int),
+ .flags = M_OPT_TYPE_CHOICE | M_OPT_TYPE_USES_RANGE,
+ .parse = parse_choice,
+ .print = print_choice,
+ .copy = copy_opt,
+ .add = add_choice,
+ .set = choice_set,
+ .get = choice_get,
+ .equal = int_equal,
+};
+
+static int apply_flag(const struct m_option *opt, int *val, bstr flag)
+{
+ const struct m_opt_choice_alternatives *alt;
+ for (alt = opt->priv; alt->name; alt++) {
+ if (bstr_equals0(flag, alt->name)) {
+ if (*val & alt->value)
+ return M_OPT_INVALID;
+ *val |= alt->value;
+ return 0;
+ }
+ }
+ return M_OPT_UNKNOWN;
+}
+
+static const char *find_next_flag(const struct m_option *opt, int *val)
+{
+ const struct m_opt_choice_alternatives *best = NULL;
+ const struct m_opt_choice_alternatives *alt;
+ for (alt = opt->priv; alt->name; alt++) {
+ if (alt->value && (alt->value & (*val)) == alt->value) {
+ if (!best || av_popcount64(alt->value) > av_popcount64(best->value))
+ best = alt;
+ }
+ }
+ if (best) {
+ *val = *val & ~(unsigned)best->value;
+ return best->name;
+ }
+ *val = 0; // if there are still flags left, there's not much we can do
+ return NULL;
+}
+
+static int parse_flags(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ int value = 0;
+ while (param.len) {
+ bstr flag;
+ bstr_split_tok(param, "+", &flag, &param);
+ int r = apply_flag(opt, &value, flag);
+ if (r == M_OPT_UNKNOWN) {
+ mp_fatal(log, "Invalid flag for option %.*s: %.*s\n",
+ BSTR_P(name), BSTR_P(flag));
+ mp_info(log, "Valid flags are:\n");
+ const struct m_opt_choice_alternatives *alt;
+ for (alt = opt->priv; alt->name; alt++)
+ mp_info(log, " %s\n", alt->name);
+ mp_info(log, "Flags can usually be combined with '+'.\n");
+ return M_OPT_INVALID;
+ } else if (r < 0) {
+ mp_fatal(log, "Option %.*s: flag '%.*s' conflicts with a previous "
+ "flag value.\n", BSTR_P(name), BSTR_P(flag));
+ return M_OPT_INVALID;
+ }
+ }
+ if (dst)
+ *(int *)dst = value;
+ return 1;
+}
+
+static int flags_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ int value = 0;
+ if (src->format != MPV_FORMAT_NODE_ARRAY)
+ return M_OPT_UNKNOWN;
+ struct mpv_node_list *srclist = src->u.list;
+ for (int n = 0; n < srclist->num; n++) {
+ if (srclist->values[n].format != MPV_FORMAT_STRING)
+ return M_OPT_INVALID;
+ if (apply_flag(opt, &value, bstr0(srclist->values[n].u.string)) < 0)
+ return M_OPT_INVALID;
+ }
+ *(int *)dst = value;
+ return 0;
+}
+
+static int flags_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ int value = *(int *)src;
+
+ dst->format = MPV_FORMAT_NODE_ARRAY;
+ dst->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ struct mpv_node_list *list = dst->u.list;
+ while (1) {
+ const char *flag = find_next_flag(opt, &value);
+ if (!flag)
+ break;
+
+ struct mpv_node node;
+ node.format = MPV_FORMAT_STRING;
+ node.u.string = (char *)flag;
+ MP_TARRAY_APPEND(list, list->values, list->num, node);
+ }
+
+ return 1;
+}
+
+static char *print_flags(const m_option_t *opt, const void *val)
+{
+ int value = *(int *)val;
+ char *res = talloc_strdup(NULL, "");
+ while (1) {
+ const char *flag = find_next_flag(opt, &value);
+ if (!flag)
+ break;
+
+ res = talloc_asprintf_append_buffer(res, "%s%s", res[0] ? "+" : "", flag);
+ }
+ return res;
+}
+
+const struct m_option_type m_option_type_flags = {
+ .name = "Flags",
+ .size = sizeof(int),
+ .parse = parse_flags,
+ .print = print_flags,
+ .copy = copy_opt,
+ .set = flags_set,
+ .get = flags_get,
+ .equal = int_equal,
+};
+
+// Float
+
+#undef VAL
+#define VAL(x) (*(double *)(x))
+
+static int clamp_double(const m_option_t *opt, void *val)
+{
+ double v = VAL(val);
+ int r = 0;
+ if (opt->min < opt->max) {
+ if (v > opt->max) {
+ v = opt->max;
+ r = M_OPT_OUT_OF_RANGE;
+ }
+ if (v < opt->min) {
+ v = opt->min;
+ r = M_OPT_OUT_OF_RANGE;
+ }
+ }
+ // (setting max/min to INFINITY/-INFINITY is allowed)
+ if (!isfinite(v) && v != opt->max && v != opt->min) {
+ v = opt->min;
+ r = M_OPT_OUT_OF_RANGE;
+ }
+ VAL(val) = v;
+ return r;
+}
+
+static int parse_double(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ struct bstr rest;
+ double tmp_float = bstrtod(param, &rest);
+
+ if (bstr_eatstart0(&rest, ":") || bstr_eatstart0(&rest, "/"))
+ tmp_float /= bstrtod(rest, &rest);
+
+ if ((opt->flags & M_OPT_DEFAULT_NAN) && bstr_equals0(param, "default")) {
+ tmp_float = NAN;
+ goto done;
+ }
+
+ if (rest.len) {
+ mp_err(log, "The %.*s option must be a floating point number or a "
+ "ratio (numerator[:/]denominator): %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+ }
+
+ if (clamp_double(opt, &tmp_float) < 0) {
+ mp_err(log, "The %.*s option is out of range: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_OUT_OF_RANGE;
+ }
+
+done:
+ if (dst)
+ VAL(dst) = tmp_float;
+ return 1;
+}
+
+static char *print_double(const m_option_t *opt, const void *val)
+{
+ double f = VAL(val);
+ if (isnan(f) && (opt->flags & M_OPT_DEFAULT_NAN))
+ return talloc_strdup(NULL, "default");
+ return talloc_asprintf(NULL, "%f", f);
+}
+
+static char *print_double_7g(const m_option_t *opt, const void *val)
+{
+ double f = VAL(val);
+ if (isnan(f))
+ return print_double(opt, val);
+ // Truncate anything < 1e-4 to avoid switching to scientific notation
+ if (fabs(f) < 1e-4) {
+ return talloc_strdup(NULL, "0");
+ } else {
+ return talloc_asprintf(NULL, "%.7g", f);
+ }
+}
+
+static void add_double(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ double v = VAL(val);
+
+ v = v + add;
+
+ double min = opt->min < opt->max ? opt->min : -INFINITY;
+ double max = opt->min < opt->max ? opt->max : +INFINITY;
+
+ if (v < min)
+ v = wrap ? max : min;
+ if (v > max)
+ v = wrap ? min : max;
+
+ VAL(val) = v;
+}
+
+static void multiply_double(const m_option_t *opt, void *val, double f)
+{
+ *(double *)val *= f;
+ clamp_double(opt, val);
+}
+
+static int double_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ double val;
+ if (src->format == MPV_FORMAT_INT64) {
+ // Can't always be represented exactly, but don't care.
+ val = src->u.int64;
+ } else if (src->format == MPV_FORMAT_DOUBLE) {
+ val = src->u.double_;
+ } else {
+ return M_OPT_UNKNOWN;
+ }
+ if (clamp_double(opt, &val) < 0)
+ return M_OPT_OUT_OF_RANGE;
+ *(double *)dst = val;
+ return 1;
+}
+
+static int double_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ double f = *(double *)src;
+ if (isnan(f) && (opt->flags & M_OPT_DEFAULT_NAN)) {
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(ta_parent, "default");
+ } else {
+ dst->format = MPV_FORMAT_DOUBLE;
+ dst->u.double_ = f;
+ }
+ return 1;
+}
+
+static bool double_equal(const m_option_t *opt, void *a, void *b)
+{
+ double fa = VAL(a), fb = VAL(b);
+ if (isnan(fa) || isnan(fb))
+ return isnan(fa) == isnan(fb);
+ return fa == fb;
+}
+
+const m_option_type_t m_option_type_double = {
+ // double precision float or ratio (numerator[:/]denominator)
+ .name = "Double",
+ .flags = M_OPT_TYPE_USES_RANGE,
+ .size = sizeof(double),
+ .parse = parse_double,
+ .print = print_double,
+ .pretty_print = print_double_7g,
+ .copy = copy_opt,
+ .add = add_double,
+ .multiply = multiply_double,
+ .set = double_set,
+ .get = double_get,
+ .equal = double_equal,
+};
+
+static int parse_double_aspect(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (bstr_equals0(param, "no")) {
+ if (dst)
+ VAL(dst) = 0.0;
+ return 1;
+ }
+ return parse_double(log, opt, name, param, dst);
+}
+
+const m_option_type_t m_option_type_aspect = {
+ .name = "Aspect",
+ .size = sizeof(double),
+ .flags = M_OPT_TYPE_CHOICE | M_OPT_TYPE_USES_RANGE,
+ .parse = parse_double_aspect,
+ .print = print_double,
+ .pretty_print = print_double_7g,
+ .copy = copy_opt,
+ .add = add_double,
+ .multiply = multiply_double,
+ .set = double_set,
+ .get = double_get,
+ .equal = double_equal,
+};
+
+#undef VAL
+#define VAL(x) (*(float *)(x))
+
+static int parse_float(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ double tmp;
+ int r = parse_double(log, opt, name, param, &tmp);
+ if (r == 1 && dst)
+ VAL(dst) = tmp;
+ return r;
+}
+
+static char *print_float(const m_option_t *opt, const void *val)
+{
+ double tmp = VAL(val);
+ return print_double(opt, &tmp);
+}
+
+static char *print_float_f3(const m_option_t *opt, const void *val)
+{
+ double tmp = VAL(val);
+ return print_double_7g(opt, &tmp);
+}
+
+static void add_float(const m_option_t *opt, void *val, double add, bool wrap)
+{
+ double tmp = VAL(val);
+ add_double(opt, &tmp, add, wrap);
+ VAL(val) = tmp;
+}
+
+static void multiply_float(const m_option_t *opt, void *val, double f)
+{
+ double tmp = VAL(val);
+ multiply_double(opt, &tmp, f);
+ VAL(val) = tmp;
+}
+
+static int float_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ double tmp;
+ int r = double_set(opt, &tmp, src);
+ if (r >= 0)
+ VAL(dst) = tmp;
+ return r;
+}
+
+static int float_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ double tmp = VAL(src);
+ return double_get(opt, ta_parent, dst, &tmp);
+}
+
+static bool float_equal(const m_option_t *opt, void *a, void *b)
+{
+ return double_equal(opt, &(double){VAL(a)}, &(double){VAL(b)});
+}
+
+const m_option_type_t m_option_type_float = {
+ // floating point number or ratio (numerator[:/]denominator)
+ .name = "Float",
+ .flags = M_OPT_TYPE_USES_RANGE,
+ .size = sizeof(float),
+ .parse = parse_float,
+ .print = print_float,
+ .pretty_print = print_float_f3,
+ .copy = copy_opt,
+ .add = add_float,
+ .multiply = multiply_float,
+ .set = float_set,
+ .get = float_get,
+ .equal = float_equal,
+};
+
+///////////// String
+
+#undef VAL
+#define VAL(x) (*(char **)(x))
+
+static int parse_str(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (dst) {
+ talloc_free(VAL(dst));
+ VAL(dst) = bstrdup0(NULL, param);
+ }
+
+ return 0;
+}
+
+static char *print_str(const m_option_t *opt, const void *val)
+{
+ return talloc_strdup(NULL, VAL(val) ? VAL(val) : "");
+}
+
+static void copy_str(const m_option_t *opt, void *dst, const void *src)
+{
+ if (dst && src) {
+ talloc_free(VAL(dst));
+ VAL(dst) = talloc_strdup(NULL, VAL(src));
+ }
+}
+
+static int str_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ if (src->format != MPV_FORMAT_STRING)
+ return M_OPT_UNKNOWN;
+ char *s = src->u.string;
+ int r = s ? 0 : M_OPT_INVALID;
+ if (r >= 0)
+ copy_str(opt, dst, &s);
+ return r;
+}
+
+static int str_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(ta_parent, VAL(src) ? VAL(src) : "");
+ return 1;
+}
+
+static bool str_equal(const m_option_t *opt, void *a, void *b)
+{
+ return bstr_equals(bstr0(VAL(a)), bstr0(VAL(b)));
+}
+
+static void free_str(void *src)
+{
+ if (src && VAL(src)) {
+ talloc_free(VAL(src));
+ VAL(src) = NULL;
+ }
+}
+
+const m_option_type_t m_option_type_string = {
+ .name = "String",
+ .size = sizeof(char *),
+ .parse = parse_str,
+ .print = print_str,
+ .copy = copy_str,
+ .free = free_str,
+ .set = str_set,
+ .get = str_get,
+ .equal = str_equal,
+};
+
+//////////// String list
+
+#undef VAL
+#define VAL(x) (*(char ***)(x))
+
+#define OP_NONE 0
+#define OP_ADD 1
+#define OP_PRE 2
+#define OP_CLR 3
+#define OP_TOGGLE 4
+#define OP_APPEND 5
+#define OP_REMOVE 6
+
+static void free_str_list(void *dst)
+{
+ char **d;
+ int i;
+
+ if (!dst || !VAL(dst))
+ return;
+ d = VAL(dst);
+
+ for (i = 0; d[i] != NULL; i++)
+ talloc_free(d[i]);
+ talloc_free(d);
+ VAL(dst) = NULL;
+}
+
+static int str_list_add(char **add, int n, void *dst, int pre)
+{
+ char **lst = VAL(dst);
+
+ int ln;
+ for (ln = 0; lst && lst[ln]; ln++)
+ /**/;
+
+ lst = talloc_realloc(NULL, lst, char *, n + ln + 1);
+
+ if (pre) {
+ memmove(&lst[n], lst, ln * sizeof(char *));
+ memcpy(lst, add, n * sizeof(char *));
+ } else
+ memcpy(&lst[ln], add, n * sizeof(char *));
+ // (re-)add NULL-termination
+ lst[ln + n] = NULL;
+
+ talloc_free(add);
+
+ VAL(dst) = lst;
+
+ return 1;
+}
+
+static struct bstr get_nextsep(struct bstr *ptr, char sep, bool modify)
+{
+ struct bstr str = *ptr;
+ struct bstr orig = str;
+ for (;;) {
+ int idx = sep ? bstrchr(str, sep) : -1;
+ if (idx > 0 && str.start[idx - 1] == '\\') {
+ if (modify) {
+ memmove(str.start + idx - 1, str.start + idx, str.len - idx);
+ str.len--;
+ str = bstr_cut(str, idx);
+ } else
+ str = bstr_cut(str, idx + 1);
+ } else {
+ str = bstr_cut(str, idx < 0 ? str.len : idx);
+ break;
+ }
+ }
+ *ptr = str;
+ return bstr_splice(orig, 0, str.start - orig.start);
+}
+
+static int find_list_bstr(char **list, bstr item)
+{
+ for (int n = 0; list && list[n]; n++) {
+ if (bstr_equals0(item, list[n]))
+ return n;
+ }
+ return -1;
+}
+
+static int parse_str_list_impl(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst,
+ int default_op)
+{
+ char **res;
+ int op = default_op;
+ bool multi = true;
+
+ if (bstr_endswith0(name, "-add")) {
+ op = OP_ADD;
+ } else if (bstr_endswith0(name, "-append")) {
+ op = OP_ADD;
+ multi = false;
+ } else if (bstr_endswith0(name, "-pre")) {
+ op = OP_PRE;
+ } else if (bstr_endswith0(name, "-clr")) {
+ op = OP_CLR;
+ } else if (bstr_endswith0(name, "-set")) {
+ op = OP_NONE;
+ } else if (bstr_endswith0(name, "-toggle")) {
+ op = OP_TOGGLE;
+ } else if (bstr_endswith0(name, "-remove")) {
+ op = OP_REMOVE;
+ }
+
+ if (op == OP_TOGGLE || op == OP_REMOVE) {
+ if (dst) {
+ char **list = VAL(dst);
+ bool found = false;
+ int index = 0;
+ do {
+ index = find_list_bstr(list, param);
+ if (index >= 0) {
+ found = true;
+ char *old = list[index];
+ for (int n = index; list[n]; n++)
+ list[n] = list[n + 1];
+ talloc_free(old);
+ }
+ } while (index >= 0);
+ if (found)
+ return 1;
+ }
+ if (op == OP_REMOVE)
+ return 1; // ignore if not found
+ op = OP_ADD;
+ multi = false;
+ }
+
+ // Clear the list ??
+ if (op == OP_CLR) {
+ if (dst)
+ free_str_list(dst);
+ return 0;
+ }
+
+ // All other ops need a param
+ if (param.len == 0 && op != OP_NONE)
+ return M_OPT_MISSING_PARAM;
+
+ char separator = opt->priv ? *(char *)opt->priv : OPTION_LIST_SEPARATOR;
+ if (!multi)
+ separator = 0; // specially handled
+ int n = 0;
+ struct bstr str = param;
+ while (str.len) {
+ get_nextsep(&str, separator, 0);
+ str = bstr_cut(str, 1);
+ n++;
+ }
+ if (n == 0 && op != OP_NONE)
+ return M_OPT_INVALID;
+
+ if (!dst)
+ return 1;
+
+ res = talloc_array(NULL, char *, n + 2);
+ str = bstrdup(NULL, param);
+ char *ptr = str.start;
+ n = 0;
+
+ while (1) {
+ struct bstr el = get_nextsep(&str, separator, 1);
+ res[n] = bstrdup0(NULL, el);
+ n++;
+ if (!str.len)
+ break;
+ str = bstr_cut(str, 1);
+ }
+ res[n] = NULL;
+ talloc_free(ptr);
+
+ if (op != OP_NONE && n > 1) {
+ mp_warn(log, "Passing multiple arguments to %.*s is deprecated!\n",
+ BSTR_P(name));
+ }
+
+ switch (op) {
+ case OP_ADD:
+ return str_list_add(res, n, dst, 0);
+ case OP_PRE:
+ return str_list_add(res, n, dst, 1);
+ }
+
+ if (VAL(dst))
+ free_str_list(dst);
+ VAL(dst) = res;
+
+ if (!res[0])
+ free_str_list(dst);
+
+ return 1;
+}
+
+static void copy_str_list(const m_option_t *opt, void *dst, const void *src)
+{
+ int n;
+ char **d, **s;
+
+ if (!(dst && src))
+ return;
+ s = VAL(src);
+
+ if (VAL(dst))
+ free_str_list(dst);
+
+ if (!s) {
+ VAL(dst) = NULL;
+ return;
+ }
+
+ for (n = 0; s[n] != NULL; n++)
+ /* NOTHING */;
+ d = talloc_array(NULL, char *, n + 1);
+ for (; n >= 0; n--)
+ d[n] = talloc_strdup(NULL, s[n]);
+
+ VAL(dst) = d;
+}
+
+static char *print_str_list(const m_option_t *opt, const void *src)
+{
+ char **lst = NULL;
+ char *ret = NULL;
+
+ if (!(src && VAL(src)))
+ return talloc_strdup(NULL, "");
+ lst = VAL(src);
+
+ for (int i = 0; lst[i]; i++) {
+ if (ret)
+ ret = talloc_strdup_append_buffer(ret, ",");
+ ret = talloc_strdup_append_buffer(ret, lst[i]);
+ }
+ return ret;
+}
+
+static int str_list_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ if (src->format != MPV_FORMAT_NODE_ARRAY)
+ return M_OPT_UNKNOWN;
+ struct mpv_node_list *srclist = src->u.list;
+ for (int n = 0; n < srclist->num; n++) {
+ if (srclist->values[n].format != MPV_FORMAT_STRING)
+ return M_OPT_INVALID;
+ }
+ free_str_list(dst);
+ if (srclist->num > 0) {
+ VAL(dst) = talloc_array(NULL, char*, srclist->num + 1);
+ for (int n = 0; n < srclist->num; n++)
+ VAL(dst)[n] = talloc_strdup(NULL, srclist->values[n].u.string);
+ VAL(dst)[srclist->num] = NULL;
+ }
+ return 1;
+}
+
+static int str_list_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ dst->format = MPV_FORMAT_NODE_ARRAY;
+ dst->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ struct mpv_node_list *list = dst->u.list;
+ for (int n = 0; VAL(src) && VAL(src)[n]; n++) {
+ struct mpv_node node;
+ node.format = MPV_FORMAT_STRING;
+ node.u.string = talloc_strdup(list, VAL(src)[n]);
+ MP_TARRAY_APPEND(list, list->values, list->num, node);
+ }
+ return 1;
+}
+
+static int parse_str_list(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ return parse_str_list_impl(log, opt, name, param, dst, OP_NONE);
+}
+
+static bool str_list_equal(const m_option_t *opt, void *a, void *b)
+{
+ char **la = VAL(a);
+ char **lb = VAL(b);
+
+ bool a_empty = !la || !la[0];
+ bool b_empty = !lb || !lb[0];
+ if (a_empty || b_empty)
+ return a_empty == b_empty;
+
+ for (int n = 0; la[n] || lb[n]; n++) {
+ if (!la[n] || !lb[n])
+ return false;
+ if (strcmp(la[n], lb[n]) != 0)
+ return false;
+ }
+
+ return true;
+}
+
+const m_option_type_t m_option_type_string_list = {
+ .name = "String list",
+ .size = sizeof(char **),
+ .parse = parse_str_list,
+ .print = print_str_list,
+ .copy = copy_str_list,
+ .free = free_str_list,
+ .get = str_list_get,
+ .set = str_list_set,
+ .equal = str_list_equal,
+ .actions = (const struct m_option_action[]){
+ {"add"},
+ {"append"},
+ {"clr", M_OPT_TYPE_OPTIONAL_PARAM},
+ {"pre"},
+ {"set"},
+ {"toggle"},
+ {"remove"},
+ {0}
+ },
+};
+
+static int read_subparam(struct mp_log *log, bstr optname, char *termset,
+ bstr *str, bstr *out_subparam);
+
+static int keyvalue_list_find_key(char **lst, bstr str)
+{
+ for (int n = 0; lst && lst[n] && lst[n + 1]; n += 2) {
+ if (bstr_equals0(str, lst[n]))
+ return n / 2;
+ }
+ return -1;
+}
+
+static void keyvalue_list_del_key(char **lst, int index)
+{
+ int count = 0;
+ for (int n = 0; lst && lst[n]; n++)
+ count++;
+ assert(index * 2 + 1 < count);
+ count += 1; // terminating item
+ talloc_free(lst[index * 2 + 0]);
+ talloc_free(lst[index * 2 + 1]);
+ MP_TARRAY_REMOVE_AT(lst, count, index * 2 + 1);
+ MP_TARRAY_REMOVE_AT(lst, count, index * 2 + 0);
+}
+
+static int parse_keyvalue_list(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ char **lst = NULL;
+ int num = 0;
+ int r = 0;
+ bool append = false;
+ bool full_value = false;
+
+ if ((opt->flags & M_OPT_HAVE_HELP) && bstr_equals0(param, "help"))
+ param = bstr0("help=");
+
+ if (bstr_endswith0(name, "-add")) {
+ append = true;
+ } else if (bstr_endswith0(name, "-append")) {
+ append = full_value = true;
+ } else if (bstr_endswith0(name, "-remove")) {
+ lst = dst ? VAL(dst) : NULL;
+ int index = dst ? keyvalue_list_find_key(lst, param) : -1;
+ if (index >= 0) {
+ keyvalue_list_del_key(lst, index);
+ VAL(dst) = lst;
+ }
+ return 1;
+ }
+
+ if (append && dst) {
+ lst = VAL(dst);
+ for (int n = 0; lst && lst[n]; n++)
+ num++;
+ }
+
+ while (param.len) {
+ bstr key, val;
+ r = read_subparam(log, name, "=", &param, &key);
+ if (r < 0)
+ break;
+ if (!bstr_eatstart0(&param, "=")) {
+ mp_err(log, "Expected '=' and a value.\n");
+ r = M_OPT_INVALID;
+ break;
+ }
+ if (full_value) {
+ val = param;
+ param.len = 0;
+ } else {
+ r = read_subparam(log, name, ",", &param, &val);
+ if (r < 0)
+ break;
+ }
+ if (dst) {
+ int index = keyvalue_list_find_key(lst, key);
+ if (index >= 0) {
+ keyvalue_list_del_key(lst, index);
+ num -= 2;
+ }
+ MP_TARRAY_APPEND(NULL, lst, num, bstrto0(NULL, key));
+ MP_TARRAY_APPEND(NULL, lst, num, bstrto0(NULL, val));
+ MP_TARRAY_APPEND(NULL, lst, num, NULL);
+ num -= 1;
+ }
+
+ if (!bstr_eatstart0(&param, ",") && !bstr_eatstart0(&param, ":"))
+ break;
+
+ if (append) {
+ mp_warn(log, "Passing more than 1 argument to %.*s is deprecated!\n",
+ BSTR_P(name));
+ }
+ }
+
+ if (param.len) {
+ mp_err(log, "Unparsable garbage at end of option value: '%.*s'\n",
+ BSTR_P(param));
+ r = M_OPT_INVALID;
+ }
+
+ if (dst) {
+ if (!append)
+ free_str_list(dst);
+ VAL(dst) = lst;
+ if (r < 0)
+ free_str_list(dst);
+ } else {
+ free_str_list(&lst);
+ }
+ return r;
+}
+
+static char *print_keyvalue_list(const m_option_t *opt, const void *src)
+{
+ char **lst = VAL(src);
+ char *ret = talloc_strdup(NULL, "");
+ for (int n = 0; lst && lst[n] && lst[n + 1]; n += 2) {
+ if (ret[0])
+ ret = talloc_strdup_append(ret, ",");
+ ret = talloc_asprintf_append(ret, "%s=%s", lst[n], lst[n + 1]);
+ }
+ return ret;
+}
+
+static int keyvalue_list_set(const m_option_t *opt, void *dst,
+ struct mpv_node *src)
+{
+ if (src->format != MPV_FORMAT_NODE_MAP)
+ return M_OPT_UNKNOWN;
+ struct mpv_node_list *srclist = src->u.list;
+ for (int n = 0; n < srclist->num; n++) {
+ if (srclist->values[n].format != MPV_FORMAT_STRING)
+ return M_OPT_INVALID;
+ }
+ free_str_list(dst);
+ if (srclist->num > 0) {
+ VAL(dst) = talloc_array(NULL, char*, (srclist->num + 1) * 2);
+ for (int n = 0; n < srclist->num; n++) {
+ VAL(dst)[n * 2 + 0] = talloc_strdup(NULL, srclist->keys[n]);
+ VAL(dst)[n * 2 + 1] = talloc_strdup(NULL, srclist->values[n].u.string);
+ }
+ VAL(dst)[srclist->num * 2 + 0] = NULL;
+ VAL(dst)[srclist->num * 2 + 1] = NULL;
+ }
+ return 1;
+}
+
+static int keyvalue_list_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ dst->format = MPV_FORMAT_NODE_MAP;
+ dst->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ struct mpv_node_list *list = dst->u.list;
+ for (int n = 0; VAL(src) && VAL(src)[n * 2 + 0]; n++) {
+ MP_TARRAY_GROW(list, list->values, list->num);
+ MP_TARRAY_GROW(list, list->keys, list->num);
+ list->keys[list->num] = talloc_strdup(list, VAL(src)[n * 2 + 0]);
+ list->values[list->num] = (struct mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = talloc_strdup(list, VAL(src)[n * 2 + 1]),
+ };
+ list->num++;
+ }
+ return 1;
+}
+
+const m_option_type_t m_option_type_keyvalue_list = {
+ .name = "Key/value list",
+ .size = sizeof(char **),
+ .parse = parse_keyvalue_list,
+ .print = print_keyvalue_list,
+ .copy = copy_str_list,
+ .free = free_str_list,
+ .get = keyvalue_list_get,
+ .set = keyvalue_list_set,
+ .equal = str_list_equal,
+ .actions = (const struct m_option_action[]){
+ {"add"},
+ {"append"},
+ {"set"},
+ {"remove"},
+ {0}
+ },
+};
+
+
+#undef VAL
+#define VAL(x) (*(char **)(x))
+
+static int check_msg_levels(struct mp_log *log, char **list)
+{
+ for (int n = 0; list && list[n * 2 + 0]; n++) {
+ char *level = list[n * 2 + 1];
+ if (mp_msg_find_level(level) < 0 && strcmp(level, "no") != 0) {
+ mp_err(log, "Invalid message level '%s'\n", level);
+ return M_OPT_INVALID;
+ }
+ }
+ return 1;
+}
+
+static int parse_msglevels(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (bstr_equals0(param, "help")) {
+ mp_info(log, "Syntax:\n\n --msg-level=module1=level,module2=level,...\n\n"
+ "'module' is output prefix as shown with -v, or a prefix\n"
+ "of it. level is one of:\n\n"
+ " fatal error warn info status v debug trace\n\n"
+ "The level specifies the minimum log level a message\n"
+ "must have to be printed.\n"
+ "The special module name 'all' affects all modules.\n");
+ return M_OPT_EXIT;
+ }
+
+ char **dst_copy = NULL;
+ int r = m_option_type_keyvalue_list.parse(log, opt, name, param, &dst_copy);
+ if (r >= 0)
+ r = check_msg_levels(log, dst_copy);
+
+ if (r >= 0)
+ m_option_type_keyvalue_list.copy(opt, dst, &dst_copy);
+ m_option_type_keyvalue_list.free(&dst_copy);
+ return r;
+}
+
+static int set_msglevels(const m_option_t *opt, void *dst,
+ struct mpv_node *src)
+{
+ char **dst_copy = NULL;
+ int r = m_option_type_keyvalue_list.set(opt, &dst_copy, src);
+ if (r >= 0)
+ r = check_msg_levels(mp_null_log, dst_copy);
+
+ if (r >= 0)
+ m_option_type_keyvalue_list.copy(opt, dst, &dst_copy);
+ m_option_type_keyvalue_list.free(&dst_copy);
+ return r;
+}
+
+const m_option_type_t m_option_type_msglevels = {
+ .name = "Output verbosity levels",
+ .size = sizeof(char **),
+ .parse = parse_msglevels,
+ .print = print_keyvalue_list,
+ .copy = copy_str_list,
+ .free = free_str_list,
+ .get = keyvalue_list_get,
+ .set = set_msglevels,
+ .equal = str_list_equal,
+};
+
+static int parse_print(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ ((m_opt_print_fn) opt->priv)(log);
+ return M_OPT_EXIT;
+}
+
+const m_option_type_t m_option_type_print_fn = {
+ .name = "Print",
+ .flags = M_OPT_TYPE_OPTIONAL_PARAM,
+ .parse = parse_print,
+};
+
+static int parse_dummy_flag(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len) {
+ mp_err(log, "Invalid parameter for %.*s flag: %.*s\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_DISALLOW_PARAM;
+ }
+ return 0;
+}
+
+const m_option_type_t m_option_type_dummy_flag = {
+ // can only be activated
+ .name = "Flag",
+ .flags = M_OPT_TYPE_OPTIONAL_PARAM,
+ .parse = parse_dummy_flag,
+};
+
+#undef VAL
+
+// Read s sub-option name, or a positional sub-opt value.
+// termset is a string containing the set of chars that terminate an option.
+// Return 0 on success, M_OPT_ error code otherwise.
+// optname is for error reporting.
+static int read_subparam(struct mp_log *log, bstr optname, char *termset,
+ bstr *str, bstr *out_subparam)
+{
+ bstr p = *str;
+ bstr subparam = {0};
+
+ if (bstr_eatstart0(&p, "\"")) {
+ int optlen = bstrcspn(p, "\"");
+ subparam = bstr_splice(p, 0, optlen);
+ p = bstr_cut(p, optlen);
+ if (!bstr_startswith0(p, "\"")) {
+ mp_err(log, "Terminating '\"' missing for '%.*s'\n",
+ BSTR_P(optname));
+ return M_OPT_INVALID;
+ }
+ p = bstr_cut(p, 1);
+ } else if (bstr_eatstart0(&p, "[")) {
+ bstr s = p;
+ int balance = 1;
+ while (p.len && balance > 0) {
+ if (p.start[0] == '[') {
+ balance++;
+ } else if (p.start[0] == ']') {
+ balance--;
+ }
+ p = bstr_cut(p, 1);
+ }
+ if (balance != 0) {
+ mp_err(log, "Terminating ']' missing for '%.*s'\n",
+ BSTR_P(optname));
+ return M_OPT_INVALID;
+ }
+ subparam = bstr_splice(s, 0, s.len - p.len - 1);
+ } else if (bstr_eatstart0(&p, "%")) {
+ int optlen = bstrtoll(p, &p, 0);
+ if (!bstr_startswith0(p, "%") || (optlen > p.len - 1)) {
+ mp_err(log, "Invalid length %d for '%.*s'\n",
+ optlen, BSTR_P(optname));
+ return M_OPT_INVALID;
+ }
+ subparam = bstr_splice(p, 1, optlen + 1);
+ p = bstr_cut(p, optlen + 1);
+ } else {
+ // Skip until the next character that could possibly be a meta
+ // character in option parsing.
+ int optlen = bstrcspn(p, termset);
+ subparam = bstr_splice(p, 0, optlen);
+ p = bstr_cut(p, optlen);
+ }
+
+ *str = p;
+ *out_subparam = subparam;
+ return 0;
+}
+
+// Return 0 on success, otherwise error code
+// On success, set *out_name and *out_val, and advance *str
+// out_val.start is NULL if there was no parameter.
+// optname is for error reporting.
+static int split_subconf(struct mp_log *log, bstr optname, bstr *str,
+ bstr *out_name, bstr *out_val)
+{
+ bstr p = *str;
+ bstr subparam = {0};
+ bstr subopt;
+ int r = read_subparam(log, optname, ":=,\\%\"'[]", &p, &subopt);
+ if (r < 0)
+ return r;
+ if (bstr_eatstart0(&p, "=")) {
+ r = read_subparam(log, subopt, ":=,\\%\"'[]", &p, &subparam);
+ if (r < 0)
+ return r;
+ }
+ *str = p;
+ *out_name = subopt;
+ *out_val = subparam;
+ return 0;
+}
+
+#undef VAL
+
+// Split the string on the given split character.
+// out_arr is at least max entries long.
+// Return number of out_arr entries filled.
+static int split_char(bstr str, unsigned char split, int max, bstr *out_arr)
+{
+ if (max < 1)
+ return 0;
+
+ int count = 0;
+ while (1) {
+ int next = bstrchr(str, split);
+ if (next >= 0 && max - count > 1) {
+ out_arr[count++] = bstr_splice(str, 0, next);
+ str = bstr_cut(str, next + 1);
+ } else {
+ out_arr[count++] = str;
+ break;
+ }
+ }
+ return count;
+}
+
+static int parse_color(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ bool is_help = bstr_equals0(param, "help");
+ if (is_help)
+ goto exit;
+
+ bstr val = param;
+ struct m_color color = {0};
+
+ if (bstr_eatstart0(&val, "#")) {
+ // #[AA]RRGGBB
+ if (val.len != 6 && val.len != 8)
+ goto exit;
+ bool has_alpha = val.len == 8;
+ uint32_t c = bstrtoll(val, &val, 16);
+ if (val.len)
+ goto exit;
+ color = (struct m_color) {
+ (c >> 16) & 0xFF,
+ (c >> 8) & 0xFF,
+ c & 0xFF,
+ has_alpha ? (c >> 24) & 0xFF : 0xFF,
+ };
+ } else {
+ bstr comp_str[5];
+ int num = split_char(param, '/', 5, comp_str);
+ if (num < 1 || num > 4)
+ goto exit;
+ double comp[4] = {0, 0, 0, 1};
+ for (int n = 0; n < num; n++) {
+ bstr rest;
+ double d = bstrtod(comp_str[n], &rest);
+ if (rest.len || !comp_str[n].len || d < 0 || d > 1 || !isfinite(d))
+ goto exit;
+ comp[n] = d;
+ }
+ if (num == 2)
+ comp[3] = comp[1];
+ if (num < 3)
+ comp[2] = comp[1] = comp[0];
+ color = (struct m_color) { comp[0] * 0xFF, comp[1] * 0xFF,
+ comp[2] * 0xFF, comp[3] * 0xFF };
+ }
+
+ if (dst)
+ *((struct m_color *)dst) = color;
+
+ return 1;
+
+exit:
+ if (!is_help) {
+ mp_err(log, "Option %.*s: invalid color: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ }
+ mp_info(log, "Valid colors must be in the form #RRGGBB or #AARRGGBB (in hex)\n"
+ "or in the form 'r/g/b/a', where each component is a value in the\n"
+ "range 0.0-1.0. (Also allowed: 'gray', 'gray/a', 'r/g/b').\n");
+ return is_help ? M_OPT_EXIT : M_OPT_INVALID;
+}
+
+static char *print_color(const m_option_t *opt, const void *val)
+{
+ const struct m_color *c = val;
+ return talloc_asprintf(NULL, "#%02X%02X%02X%02X", c->a, c->r, c->g, c->b);
+}
+
+static bool color_equal(const m_option_t *opt, void *a, void *b)
+{
+ struct m_color *ca = a;
+ struct m_color *cb = b;
+ return ca->a == cb->a && ca->r == cb->r && ca->g == cb->g && ca->b == cb->b;
+}
+
+const m_option_type_t m_option_type_color = {
+ .name = "Color",
+ .size = sizeof(struct m_color),
+ .parse = parse_color,
+ .print = print_color,
+ .copy = copy_opt,
+ .equal = color_equal,
+};
+
+
+// Parse a >=0 number starting at s. Set s to the string following the number.
+// If the number ends with '%', eat that and set *out_per to true, but only
+// if the number is between 0-100; if not, don't eat anything, even the number.
+static bool eat_num_per(bstr *s, int *out_num, bool *out_per)
+{
+ bstr rest;
+ long long v = bstrtoll(*s, &rest, 10);
+ if (s->len == rest.len || v < INT_MIN || v > INT_MAX)
+ return false;
+ *out_num = v;
+ *out_per = false;
+ *s = rest;
+ if (bstr_eatstart0(&rest, "%") && v >= 0 && v <= 100) {
+ *out_per = true;
+ *s = rest;
+ }
+ return true;
+}
+
+static bool parse_geometry_str(struct m_geometry *gm, bstr s)
+{
+ *gm = (struct m_geometry) { .x = INT_MIN, .y = INT_MIN };
+ if (s.len == 0)
+ return true;
+ // Approximate grammar:
+ // [[W][xH]][{+-}X{+-}Y][/WS] | [X:Y]
+ // (meaning: [optional] {one character of} one|alternative)
+ // Every number can be followed by '%'
+ int num;
+ bool per;
+
+#define READ_NUM(F, F_PER) do { \
+ if (!eat_num_per(&s, &num, &per)) \
+ goto error; \
+ gm->F = num; \
+ gm->F_PER = per; \
+} while(0)
+
+#define READ_SIGN(F) do { \
+ if (bstr_eatstart0(&s, "+")) { \
+ gm->F = false; \
+ } else if (bstr_eatstart0(&s, "-")) {\
+ gm->F = true; \
+ } else goto error; \
+} while(0)
+
+ if (bstrchr(s, ':') < 0) {
+ gm->wh_valid = true;
+ if (!bstr_startswith0(s, "+") && !bstr_startswith0(s, "-")) {
+ if (!bstr_startswith0(s, "x"))
+ READ_NUM(w, w_per);
+ if (bstr_eatstart0(&s, "x"))
+ READ_NUM(h, h_per);
+ }
+ if (s.len > 0) {
+ gm->xy_valid = true;
+ READ_SIGN(x_sign);
+ READ_NUM(x, x_per);
+ READ_SIGN(y_sign);
+ READ_NUM(y, y_per);
+ }
+ if (bstr_eatstart0(&s, "/")) {
+ bstr rest;
+ long long v = bstrtoll(s, &rest, 10);
+ if (s.len == rest.len || v < 1 || v > INT_MAX)
+ goto error;
+ s = rest;
+ gm->ws = v;
+ }
+ } else {
+ gm->xy_valid = true;
+ READ_NUM(x, x_per);
+ if (!bstr_eatstart0(&s, ":"))
+ goto error;
+ READ_NUM(y, y_per);
+ }
+
+ return s.len == 0;
+
+error:
+ return false;
+}
+
+#undef READ_NUM
+#undef READ_SIGN
+
+#define APPEND_PER(F, F_PER) \
+ res = talloc_asprintf_append(res, "%d%s", gm->F, gm->F_PER ? "%" : "")
+
+static char *print_geometry(const m_option_t *opt, const void *val)
+{
+ const struct m_geometry *gm = val;
+ char *res = talloc_strdup(NULL, "");
+ if (gm->wh_valid || gm->xy_valid) {
+ if (gm->wh_valid) {
+ APPEND_PER(w, w_per);
+ res = talloc_asprintf_append(res, "x");
+ APPEND_PER(h, h_per);
+ }
+ if (gm->xy_valid) {
+ res = talloc_asprintf_append(res, gm->x_sign ? "-" : "+");
+ APPEND_PER(x, x_per);
+ res = talloc_asprintf_append(res, gm->y_sign ? "-" : "+");
+ APPEND_PER(y, y_per);
+ }
+ if (gm->ws > 0)
+ res = talloc_asprintf_append(res, "/%d", gm->ws);
+ }
+ return res;
+}
+
+#undef APPEND_PER
+
+// xpos,ypos: position of the left upper corner
+// widw,widh: width and height of the window
+// scrw,scrh: width and height of the current screen
+// The input parameters should be set to a centered window (default fallbacks).
+void m_geometry_apply(int *xpos, int *ypos, int *widw, int *widh,
+ int scrw, int scrh, struct m_geometry *gm)
+{
+ if (gm->wh_valid) {
+ int prew = *widw, preh = *widh;
+ if (gm->w > 0)
+ *widw = gm->w_per ? scrw * (gm->w / 100.0) : gm->w;
+ if (gm->h > 0)
+ *widh = gm->h_per ? scrh * (gm->h / 100.0) : gm->h;
+ // keep aspect if the other value is not set
+ double asp = (double)prew / preh;
+ if (gm->w > 0 && !(gm->h > 0)) {
+ *widh = *widw / asp;
+ } else if (!(gm->w > 0) && gm->h > 0) {
+ *widw = *widh * asp;
+ }
+ // Center window after resize. If valid x:y values are passed to
+ // geometry, then those values will be overridden.
+ *xpos += prew / 2 - *widw / 2;
+ *ypos += preh / 2 - *widh / 2;
+ }
+
+ if (gm->xy_valid) {
+ if (gm->x != INT_MIN) {
+ *xpos = gm->x;
+ if (gm->x_per)
+ *xpos = (scrw - *widw) * (*xpos / 100.0);
+ if (gm->x_sign)
+ *xpos = scrw - *widw - *xpos;
+ }
+ if (gm->y != INT_MIN) {
+ *ypos = gm->y;
+ if (gm->y_per)
+ *ypos = (scrh - *widh) * (*ypos / 100.0);
+ if (gm->y_sign)
+ *ypos = scrh - *widh - *ypos;
+ }
+ }
+}
+
+static int parse_geometry(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ bool is_help = bstr_equals0(param, "help");
+ if (is_help)
+ goto exit;
+
+ struct m_geometry gm;
+ if (!parse_geometry_str(&gm, param))
+ goto exit;
+
+ if (dst)
+ *((struct m_geometry *)dst) = gm;
+
+ return 1;
+
+exit:
+ if (!is_help) {
+ mp_err(log, "Option %.*s: invalid geometry: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ }
+ mp_info(log,
+ "Valid format: [W[%%][xH[%%]]][{+-}X[%%]{+-}Y[%%]] | [X[%%]:Y[%%]]\n");
+ return is_help ? M_OPT_EXIT : M_OPT_INVALID;
+}
+
+static bool geometry_equal(const m_option_t *opt, void *a, void *b)
+{
+ struct m_geometry *ga = a;
+ struct m_geometry *gb = b;
+ return ga->x == gb->x && ga->y == gb->y && ga->w == gb->w && ga->h == gb->h &&
+ ga->xy_valid == gb->xy_valid && ga->wh_valid == gb->wh_valid &&
+ ga->w_per == gb->w_per && ga->h_per == gb->h_per &&
+ ga->x_per == gb->x_per && ga->y_per == gb->y_per &&
+ ga->x_sign == gb->x_sign && ga->y_sign == gb->y_sign &&
+ ga->ws == gb->ws;
+}
+
+const m_option_type_t m_option_type_geometry = {
+ .name = "Window geometry",
+ .size = sizeof(struct m_geometry),
+ .parse = parse_geometry,
+ .print = print_geometry,
+ .copy = copy_opt,
+ .equal = geometry_equal,
+};
+
+static int parse_size_box(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ bool is_help = bstr_equals0(param, "help");
+ if (is_help)
+ goto exit;
+
+ struct m_geometry gm;
+ if (!parse_geometry_str(&gm, param))
+ goto exit;
+
+ if (gm.xy_valid)
+ goto exit;
+
+ if (dst)
+ *((struct m_geometry *)dst) = gm;
+
+ return 1;
+
+exit:
+ if (!is_help) {
+ mp_err(log, "Option %.*s: invalid size: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ }
+ mp_info(log, "Valid format: W[%%][xH[%%]] or empty string\n");
+ return is_help ? M_OPT_EXIT : M_OPT_INVALID;
+}
+
+const m_option_type_t m_option_type_size_box = {
+ .name = "Window size",
+ .size = sizeof(struct m_geometry),
+ .parse = parse_size_box,
+ .print = print_geometry,
+ .copy = copy_opt,
+ .equal = geometry_equal,
+};
+
+void m_rect_apply(struct mp_rect *rc, int w, int h, struct m_geometry *gm)
+{
+ *rc = (struct mp_rect){0, 0, w, h};
+ if (!w || !h)
+ return;
+ m_geometry_apply(&rc->x0, &rc->y0, &rc->x1, &rc->y1, w, h, gm);
+ if (!gm->xy_valid && gm->wh_valid && rc->x1 == 0 && rc->y1 == 0)
+ return;
+ if (!gm->wh_valid || rc->x1 == 0 || rc->x1 == INT_MIN)
+ rc->x1 = w - rc->x0;
+ if (!gm->wh_valid || rc->y1 == 0 || rc->y1 == INT_MIN)
+ rc->y1 = h - rc->y0;
+ if (gm->wh_valid && (gm->w || gm->h))
+ rc->x1 += rc->x0;
+ if (gm->wh_valid && (gm->w || gm->h))
+ rc->y1 += rc->y0;
+}
+
+static int parse_rect(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ bool is_help = bstr_equals0(param, "help");
+ if (is_help)
+ goto exit;
+
+ struct m_geometry gm;
+ if (!parse_geometry_str(&gm, param))
+ goto exit;
+
+ bool invalid = gm.x_sign || gm.y_sign || gm.ws;
+ invalid |= gm.wh_valid && (gm.w < 0 || gm.h < 0);
+ invalid |= gm.wh_valid && !gm.xy_valid && gm.w <= 0 && gm.h <= 0;
+
+ if (invalid)
+ goto exit;
+
+ if (dst)
+ *((struct m_geometry *)dst) = gm;
+
+ return 1;
+
+exit:
+ if (!is_help) {
+ mp_err(log, "Option %.*s: invalid rect: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ }
+ mp_info(log, "Valid format: W[%%][xH[%%]][+x+y]\n");
+ return is_help ? M_OPT_EXIT : M_OPT_INVALID;
+}
+
+const m_option_type_t m_option_type_rect = {
+ .name = "Video rect",
+ .size = sizeof(struct m_geometry),
+ .parse = parse_rect,
+ .print = print_geometry,
+ .copy = copy_opt,
+ .equal = geometry_equal,
+};
+
+#include "video/img_format.h"
+
+static int parse_imgfmt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ if (!bstrcmp0(param, "help")) {
+ mp_info(log, "Available formats:");
+ char **list = mp_imgfmt_name_list();
+ for (int i = 0; list[i]; i++)
+ mp_info(log, " %s", list[i]);
+ mp_info(log, " no");
+ mp_info(log, "\n");
+ talloc_free(list);
+ return M_OPT_EXIT;
+ }
+
+ unsigned int fmt = mp_imgfmt_from_name(param);
+ if (!fmt && !bstr_equals0(param, "no")) {
+ mp_err(log, "Option %.*s: unknown format name: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+ }
+
+ if (dst)
+ *((int *)dst) = fmt;
+
+ return 1;
+}
+
+static char *print_imgfmt(const m_option_t *opt, const void *val)
+{
+ int fmt = *(int *)val;
+ return talloc_strdup(NULL, fmt ? mp_imgfmt_to_name(fmt) : "no");
+}
+
+const m_option_type_t m_option_type_imgfmt = {
+ .name = "Image format",
+ .size = sizeof(int),
+ .parse = parse_imgfmt,
+ .print = print_imgfmt,
+ .copy = copy_opt,
+ .equal = int_equal,
+};
+
+static int parse_fourcc(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ unsigned int value;
+
+ if (param.len == 4) {
+ uint8_t *s = param.start;
+ value = s[0] | (s[1] << 8) | (s[2] << 16) | (s[3] << 24);
+ } else {
+ bstr rest;
+ value = bstrtoll(param, &rest, 16);
+ if (rest.len != 0) {
+ mp_err(log, "Option %.*s: invalid FourCC: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+ }
+ }
+
+ if (dst)
+ *((unsigned int *)dst) = value;
+
+ return 1;
+}
+
+static char *print_fourcc(const m_option_t *opt, const void *val)
+{
+ unsigned int fourcc = *(unsigned int *)val;
+ return talloc_asprintf(NULL, "%08x", fourcc);
+}
+
+const m_option_type_t m_option_type_fourcc = {
+ .name = "FourCC",
+ .size = sizeof(unsigned int),
+ .parse = parse_fourcc,
+ .print = print_fourcc,
+ .copy = copy_opt,
+ .equal = int_equal,
+};
+
+#include "audio/format.h"
+
+static int parse_afmt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ if (!bstrcmp0(param, "help")) {
+ mp_info(log, "Available formats:");
+ for (int i = 1; i < AF_FORMAT_COUNT; i++)
+ mp_info(log, " %s", af_fmt_to_str(i));
+ mp_info(log, "\n");
+ return M_OPT_EXIT;
+ }
+
+ int fmt = 0;
+ for (int i = 1; i < AF_FORMAT_COUNT; i++) {
+ if (bstr_equals0(param, af_fmt_to_str(i)))
+ fmt = i;
+ }
+ if (!fmt) {
+ mp_err(log, "Option %.*s: unknown format name: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+ }
+
+ if (dst)
+ *((int *)dst) = fmt;
+
+ return 1;
+}
+
+static char *print_afmt(const m_option_t *opt, const void *val)
+{
+ int fmt = *(int *)val;
+ return talloc_strdup(NULL, fmt ? af_fmt_to_str(fmt) : "no");
+}
+
+const m_option_type_t m_option_type_afmt = {
+ .name = "Audio format",
+ .size = sizeof(int),
+ .parse = parse_afmt,
+ .print = print_afmt,
+ .copy = copy_opt,
+ .equal = int_equal,
+};
+
+#include "audio/chmap.h"
+
+static int parse_channels(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ bool limited = opt->flags & M_OPT_CHANNELS_LIMITED;
+
+ struct m_channels res = {0};
+
+ if (bstr_equals0(param, "help")) {
+ mp_chmap_print_help(log);
+ if (!limited) {
+ mp_info(log, "\nOther values:\n"
+ " auto-safe\n");
+ }
+ return M_OPT_EXIT;
+ }
+
+ bool auto_safe = bstr_equals0(param, "auto-safe");
+ if (bstr_equals0(param, "auto") || bstr_equals0(param, "empty") || auto_safe) {
+ if (limited) {
+ mp_err(log, "Disallowed parameter.\n");
+ return M_OPT_INVALID;
+ }
+ param.len = 0;
+ res.set = true;
+ res.auto_safe = auto_safe;
+ }
+
+ while (param.len) {
+ bstr item;
+ if (limited) {
+ item = param;
+ param.len = 0;
+ } else {
+ bstr_split_tok(param, ",", &item, &param);
+ }
+
+ struct mp_chmap map = {0};
+ if (!mp_chmap_from_str(&map, item) || !mp_chmap_is_valid(&map)) {
+ mp_err(log, "Invalid channel layout: %.*s\n", BSTR_P(item));
+ talloc_free(res.chmaps);
+ return M_OPT_INVALID;
+ }
+
+ MP_TARRAY_APPEND(NULL, res.chmaps, res.num_chmaps, map);
+ res.set = true;
+ }
+
+ if (dst) {
+ *(struct m_channels *)dst = res;
+ } else {
+ talloc_free(res.chmaps);
+ }
+
+ return 1;
+}
+
+static char *print_channels(const m_option_t *opt, const void *val)
+{
+ const struct m_channels *ch = val;
+ if (!ch->set)
+ return talloc_strdup(NULL, "");
+ if (ch->auto_safe)
+ return talloc_strdup(NULL, "auto-safe");
+ if (ch->num_chmaps > 0) {
+ char *res = talloc_strdup(NULL, "");
+ for (int n = 0; n < ch->num_chmaps; n++) {
+ if (n > 0)
+ res = talloc_strdup_append(res, ",");
+ res = talloc_strdup_append(res, mp_chmap_to_str(&ch->chmaps[n]));
+ }
+ return res;
+ }
+ return talloc_strdup(NULL, "auto");
+}
+
+static void free_channels(void *src)
+{
+ if (!src)
+ return;
+
+ struct m_channels *ch = src;
+ talloc_free(ch->chmaps);
+ *ch = (struct m_channels){0};
+}
+
+static void copy_channels(const m_option_t *opt, void *dst, const void *src)
+{
+ if (!(dst && src))
+ return;
+
+ struct m_channels *ch = dst;
+ free_channels(dst);
+ *ch = *(struct m_channels *)src;
+ ch->chmaps =
+ talloc_memdup(NULL, ch->chmaps, sizeof(ch->chmaps[0]) * ch->num_chmaps);
+}
+
+static bool channels_equal(const m_option_t *opt, void *a, void *b)
+{
+ struct m_channels *ca = a;
+ struct m_channels *cb = b;
+
+ if (ca->set != cb->set ||
+ ca->auto_safe != cb->auto_safe ||
+ ca->num_chmaps != cb->num_chmaps)
+ return false;
+
+ for (int n = 0; n < ca->num_chmaps; n++) {
+ if (!mp_chmap_equals(&ca->chmaps[n], &cb->chmaps[n]))
+ return false;
+ }
+
+ return true;
+}
+
+const m_option_type_t m_option_type_channels = {
+ .name = "Audio channels or channel map",
+ .size = sizeof(struct m_channels),
+ .parse = parse_channels,
+ .print = print_channels,
+ .copy = copy_channels,
+ .free = free_channels,
+ .equal = channels_equal,
+};
+
+static int parse_timestring(struct bstr str, double *time, char endchar)
+{
+ int h, m, len;
+ double s;
+ *time = 0; /* ensure initialization for error cases */
+ bool neg = bstr_eatstart0(&str, "-");
+ if (!neg)
+ bstr_eatstart0(&str, "+");
+ if (bstrchr(str, '-') >= 0 || bstrchr(str, '+') >= 0)
+ return 0; /* the timestamp shouldn't contain anymore +/- after this point */
+ if (bstr_sscanf(str, "%d:%d:%lf%n", &h, &m, &s, &len) >= 3) {
+ if (m >= 60 || s >= 60)
+ return 0; /* minutes or seconds are out of range */
+ *time = 3600 * h + 60 * m + s;
+ } else if (bstr_sscanf(str, "%d:%lf%n", &m, &s, &len) >= 2) {
+ if (s >= 60)
+ return 0; /* seconds are out of range */
+ *time = 60 * m + s;
+ } else if (bstr_sscanf(str, "%lf%n", &s, &len) >= 1) {
+ *time = s;
+ } else {
+ return 0; /* unsupported time format */
+ }
+ if (len < str.len && str.start[len] != endchar)
+ return 0; /* invalid extra characters at the end */
+ if (!isfinite(*time))
+ return 0;
+ if (neg)
+ *time = -*time;
+ return len;
+}
+
+#define HAS_NOPTS(opt) ((opt)->flags & M_OPT_ALLOW_NO)
+
+static int parse_time(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ double time = MP_NOPTS_VALUE;
+ if (HAS_NOPTS(opt) && bstr_equals0(param, "no")) {
+ // nothing
+ } else if (!parse_timestring(param, &time, 0)) {
+ mp_err(log, "Option %.*s: invalid time: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+ }
+
+ if (dst)
+ *(double *)dst = time;
+ return 1;
+}
+
+static char *print_time(const m_option_t *opt, const void *val)
+{
+ double pts = *(double *)val;
+ if (pts == MP_NOPTS_VALUE && HAS_NOPTS(opt))
+ return talloc_strdup(NULL, "no"); // symmetry with parsing
+ return talloc_asprintf(NULL, "%f", pts);
+}
+
+static char *pretty_print_time(const m_option_t *opt, const void *val)
+{
+ double pts = *(double *)val;
+ if (pts == MP_NOPTS_VALUE && HAS_NOPTS(opt))
+ return talloc_strdup(NULL, "no"); // symmetry with parsing
+ return mp_format_time(pts, false);
+}
+
+static int time_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ if (HAS_NOPTS(opt) && src->format == MPV_FORMAT_STRING) {
+ if (strcmp(src->u.string, "no") == 0) {
+ *(double *)dst = MP_NOPTS_VALUE;
+ return 1;
+ }
+ }
+ return double_set(opt, dst, src);
+}
+
+static int time_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ if (HAS_NOPTS(opt) && *(double *)src == MP_NOPTS_VALUE) {
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(ta_parent, "no");
+ return 1;
+ }
+ return double_get(opt, ta_parent, dst, src);
+}
+
+const m_option_type_t m_option_type_time = {
+ .name = "Time",
+ .size = sizeof(double),
+ .parse = parse_time,
+ .print = print_time,
+ .pretty_print = pretty_print_time,
+ .copy = copy_opt,
+ .add = add_double,
+ .set = time_set,
+ .get = time_get,
+ .equal = double_equal,
+};
+
+
+// Relative time
+
+static int parse_rel_time(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ struct m_rel_time t = {0};
+
+ if (param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ if (bstr_equals0(param, "none")) {
+ t.type = REL_TIME_NONE;
+ goto out;
+ }
+
+ // Percent pos
+ if (bstr_endswith0(param, "%")) {
+ double percent = bstrtod(bstr_splice(param, 0, -1), &param);
+ if (param.len == 0 && percent >= 0 && percent <= 100) {
+ t.type = REL_TIME_PERCENT;
+ t.pos = percent;
+ goto out;
+ }
+ }
+
+ // Chapter pos
+ if (bstr_startswith0(param, "#")) {
+ int chapter = bstrtoll(bstr_cut(param, 1), &param, 10);
+ if (param.len == 0 && chapter >= 1) {
+ t.type = REL_TIME_CHAPTER;
+ t.pos = chapter - 1;
+ goto out;
+ }
+ }
+
+ double time;
+ if (parse_timestring(param, &time, 0)) {
+ if (bstr_startswith0(param, "+") || bstr_startswith0(param, "-")) {
+ t.type = REL_TIME_RELATIVE;
+ } else {
+ t.type = REL_TIME_ABSOLUTE;
+ }
+ t.pos = time;
+ goto out;
+ }
+
+ mp_err(log, "Option %.*s: invalid time or position: '%.*s'\n",
+ BSTR_P(name), BSTR_P(param));
+ return M_OPT_INVALID;
+
+out:
+ if (dst)
+ *(struct m_rel_time *)dst = t;
+ return 1;
+}
+
+static char *print_rel_time(const m_option_t *opt, const void *val)
+{
+ const struct m_rel_time *t = val;
+ switch(t->type) {
+ case REL_TIME_ABSOLUTE:
+ return talloc_asprintf(NULL, "%g", t->pos);
+ case REL_TIME_RELATIVE:
+ return talloc_asprintf(NULL, "%s%g",
+ (t->pos >= 0) ? "+" : "-", fabs(t->pos));
+ case REL_TIME_CHAPTER:
+ return talloc_asprintf(NULL, "#%g", t->pos);
+ case REL_TIME_PERCENT:
+ return talloc_asprintf(NULL, "%g%%", t->pos);
+ }
+ return talloc_strdup(NULL, "none");
+}
+
+static bool rel_time_equal(const m_option_t *opt, void *a, void *b)
+{
+ struct m_rel_time *ta = a;
+ struct m_rel_time *tb = b;
+ return ta->type == tb->type && ta->pos == tb->pos;
+}
+
+const m_option_type_t m_option_type_rel_time = {
+ .name = "Relative time or percent position",
+ .size = sizeof(struct m_rel_time),
+ .parse = parse_rel_time,
+ .print = print_rel_time,
+ .copy = copy_opt,
+ .equal = rel_time_equal,
+};
+
+
+//// Objects (i.e. filters, etc) settings
+
+#undef VAL
+#define VAL(x) (*(m_obj_settings_t **)(x))
+
+bool m_obj_list_find(struct m_obj_desc *dst, const struct m_obj_list *l,
+ bstr name)
+{
+ for (int i = 0; ; i++) {
+ if (!l->get_desc(dst, i))
+ break;
+ if (bstr_equals0(name, dst->name))
+ return true;
+ }
+ for (int i = 0; l->aliases[i][0]; i++) {
+ const char *aname = l->aliases[i][0];
+ const char *alias = l->aliases[i][1];
+ if (bstr_equals0(name, aname) && m_obj_list_find(dst, l, bstr0(alias)))
+ {
+ dst->replaced_name = aname;
+ return true;
+ }
+ }
+ return false;
+}
+
+static void obj_setting_free(m_obj_settings_t *item)
+{
+ talloc_free(item->name);
+ talloc_free(item->label);
+ free_str_list(&(item->attribs));
+}
+
+// If at least one item has a label, compare labels only - otherwise ignore them.
+static bool obj_setting_match(m_obj_settings_t *a, m_obj_settings_t *b)
+{
+ bstr la = bstr0(a->label), lb = bstr0(b->label);
+ if (la.len || lb.len)
+ return bstr_equals(la, lb);
+
+ return m_obj_settings_equal(a, b);
+}
+
+static int obj_settings_list_num_items(m_obj_settings_t *obj_list)
+{
+ int num = 0;
+ while (obj_list && obj_list[num].name)
+ num++;
+ return num;
+}
+
+static void obj_settings_list_del_at(m_obj_settings_t **p_obj_list, int idx)
+{
+ m_obj_settings_t *obj_list = *p_obj_list;
+ int num = obj_settings_list_num_items(obj_list);
+
+ assert(idx >= 0 && idx < num);
+
+ obj_setting_free(&obj_list[idx]);
+
+ // Note: the NULL-terminating element is moved down as part of this
+ memmove(&obj_list[idx], &obj_list[idx + 1],
+ sizeof(m_obj_settings_t) * (num - idx));
+
+ *p_obj_list = talloc_realloc(NULL, obj_list, struct m_obj_settings, num);
+}
+
+// Insert such that *p_obj_list[idx] is set to item.
+// If idx < 0, set idx = count + idx + 1 (i.e. -1 inserts it as last element).
+// Memory referenced by *item is not copied.
+static void obj_settings_list_insert_at(m_obj_settings_t **p_obj_list, int idx,
+ m_obj_settings_t *item)
+{
+ int num = obj_settings_list_num_items(*p_obj_list);
+ if (idx < 0)
+ idx = num + idx + 1;
+ assert(idx >= 0 && idx <= num);
+ *p_obj_list = talloc_realloc(NULL, *p_obj_list, struct m_obj_settings,
+ num + 2);
+ memmove(*p_obj_list + idx + 1, *p_obj_list + idx,
+ (num - idx) * sizeof(m_obj_settings_t));
+ (*p_obj_list)[idx] = *item;
+ (*p_obj_list)[num + 1] = (m_obj_settings_t){0};
+}
+
+static int obj_settings_list_find_by_label(m_obj_settings_t *obj_list,
+ bstr label)
+{
+ for (int n = 0; obj_list && obj_list[n].name; n++) {
+ if (label.len && bstr_equals0(label, obj_list[n].label))
+ return n;
+ }
+ return -1;
+}
+
+static int obj_settings_list_find_by_label0(m_obj_settings_t *obj_list,
+ const char *label)
+{
+ return obj_settings_list_find_by_label(obj_list, bstr0(label));
+}
+
+static int obj_settings_find_by_content(m_obj_settings_t *obj_list,
+ m_obj_settings_t *item)
+{
+ for (int n = 0; obj_list && obj_list[n].name; n++) {
+ if (obj_setting_match(&obj_list[n], item))
+ return n;
+ }
+ return -1;
+}
+
+static void free_obj_settings_list(void *dst)
+{
+ int n;
+ m_obj_settings_t *d;
+
+ if (!dst || !VAL(dst))
+ return;
+
+ d = VAL(dst);
+ for (n = 0; d[n].name; n++)
+ obj_setting_free(&d[n]);
+ talloc_free(d);
+ VAL(dst) = NULL;
+}
+
+static void copy_obj_settings_list(const m_option_t *opt, void *dst,
+ const void *src)
+{
+ m_obj_settings_t *d, *s;
+ int n;
+
+ if (!(dst && src))
+ return;
+
+ s = VAL(src);
+
+ if (VAL(dst))
+ free_obj_settings_list(dst);
+ if (!s)
+ return;
+
+ for (n = 0; s[n].name; n++)
+ /* NOP */;
+ d = talloc_array(NULL, struct m_obj_settings, n + 1);
+ for (n = 0; s[n].name; n++) {
+ d[n].name = talloc_strdup(NULL, s[n].name);
+ d[n].label = talloc_strdup(NULL, s[n].label);
+ d[n].enabled = s[n].enabled;
+ d[n].attribs = NULL;
+ copy_str_list(NULL, &(d[n].attribs), &(s[n].attribs));
+ }
+ d[n].name = NULL;
+ d[n].label = NULL;
+ d[n].attribs = NULL;
+ VAL(dst) = d;
+}
+
+// Consider -vf a=b=c:d=e. This verifies "b"="c" and "d"="e" and that the
+// option names/values are correct. Try to determine whether an option
+// without '=' sets a flag, or whether it's a positional argument.
+static int get_obj_param(struct mp_log *log, bstr opt_name, bstr obj_name,
+ struct m_config *config, bstr name, bstr val,
+ int flags, bool nopos,
+ int *nold, bstr *out_name, bstr *out_val,
+ char *tmp, size_t tmp_size)
+{
+ int r;
+
+ if (!config) {
+ // Duplicates the logic below, but with unknown parameter types/names.
+ if (val.start || nopos) {
+ *out_name = name;
+ *out_val = val;
+ } else {
+ val = name;
+ // positional fields
+ if (val.len == 0) { // Empty field, count it and go on
+ (*nold)++;
+ return 0;
+ }
+ // Positional naming convention for/followed by mp_set_avopts().
+ snprintf(tmp, tmp_size, "@%d", *nold);
+ *out_name = bstr0(tmp);
+ *out_val = val;
+ (*nold)++;
+ }
+ return 1;
+ }
+
+ // val.start != NULL => of the form name=val (not positional)
+ // If it's just "name", and the associated option exists and is a flag,
+ // don't accept it as positional argument.
+ if (val.start || m_config_option_requires_param(config, name) == 0 || nopos) {
+ r = m_config_set_option_cli(config, name, val, flags);
+ if (r < 0) {
+ if (r == M_OPT_UNKNOWN) {
+ mp_err(log, "Option %.*s: %.*s doesn't have a %.*s parameter.\n",
+ BSTR_P(opt_name), BSTR_P(obj_name), BSTR_P(name));
+ return M_OPT_UNKNOWN;
+ }
+ if (r != M_OPT_EXIT)
+ mp_err(log, "Option %.*s: "
+ "Error while parsing %.*s parameter %.*s (%.*s)\n",
+ BSTR_P(opt_name), BSTR_P(obj_name), BSTR_P(name),
+ BSTR_P(val));
+ return r;
+ }
+ *out_name = name;
+ *out_val = val;
+ return 1;
+ } else {
+ val = name;
+ // positional fields
+ if (val.len == 0) { // Empty field, count it and go on
+ (*nold)++;
+ return 0;
+ }
+ const char *opt = m_config_get_positional_option(config, *nold);
+ if (!opt) {
+ mp_err(log, "Option %.*s: %.*s has only %d "
+ "params, so you can't give more than %d unnamed params.\n",
+ BSTR_P(opt_name), BSTR_P(obj_name), *nold, *nold);
+ return M_OPT_OUT_OF_RANGE;
+ }
+ r = m_config_set_option_cli(config, bstr0(opt), val, flags);
+ if (r < 0) {
+ if (r != M_OPT_EXIT)
+ mp_err(log, "Option %.*s: "
+ "Error while parsing %.*s parameter %s (%.*s)\n",
+ BSTR_P(opt_name), BSTR_P(obj_name), opt, BSTR_P(val));
+ return r;
+ }
+ *out_name = bstr0(opt);
+ *out_val = val;
+ (*nold)++;
+ return 1;
+ }
+}
+
+// Consider -vf a=b:c:d. This parses "b:c:d" into name/value pairs, stored as
+// linear array in *_ret. In particular, config contains what options a the
+// object takes, and verifies the option values as well.
+// If config is NULL, all parameters are accepted without checking.
+// _ret set to NULL can be used for checking-only.
+// flags can contain any M_SETOPT_* flag.
+// desc is optional.
+static int m_obj_parse_sub_config(struct mp_log *log, struct bstr opt_name,
+ struct bstr name, struct bstr *pstr,
+ struct m_config *config, int flags, bool nopos,
+ struct m_obj_desc *desc,
+ const struct m_obj_list *list, char ***ret)
+{
+ int nold = 0;
+ char **args = NULL;
+ int num_args = 0;
+ int r = 1;
+ char tmp[80];
+
+ if (ret) {
+ args = *ret;
+ while (args && args[num_args])
+ num_args++;
+ }
+
+ while (pstr->len > 0) {
+ bstr fname, fval;
+ r = split_subconf(log, opt_name, pstr, &fname, &fval);
+ if (r < 0)
+ goto exit;
+
+ if (list->use_global_options) {
+ mp_err(log, "Option %.*s: this option does not accept sub-options.\n",
+ BSTR_P(opt_name));
+ mp_err(log, "Sub-options for --vo and --ao were removed from mpv in "
+ "release 0.23.0.\nSee https://0x0.st/uM for details.\n");
+ r = M_OPT_INVALID;
+ goto exit;
+ }
+
+ if (bstr_equals0(fname, "help"))
+ goto print_help;
+ r = get_obj_param(log, opt_name, name, config, fname, fval, flags,
+ nopos, &nold, &fname, &fval, tmp, sizeof(tmp));
+ if (r < 0)
+ goto exit;
+
+ if (r > 0 && ret) {
+ MP_TARRAY_APPEND(NULL, args, num_args, bstrto0(NULL, fname));
+ MP_TARRAY_APPEND(NULL, args, num_args, bstrto0(NULL, fval));
+ MP_TARRAY_APPEND(NULL, args, num_args, NULL);
+ MP_TARRAY_APPEND(NULL, args, num_args, NULL);
+ num_args -= 2;
+ }
+
+ if (!bstr_eatstart0(pstr, ":"))
+ break;
+ }
+
+ if (ret) {
+ if (num_args > 0) {
+ *ret = args;
+ args = NULL;
+ } else {
+ *ret = NULL;
+ }
+ }
+
+ goto exit;
+
+print_help: ;
+ if (config) {
+ if (desc->print_help)
+ desc->print_help(log);
+ m_config_print_option_list(config, "*");
+ } else if (list->print_unknown_entry_help) {
+ list->print_unknown_entry_help(log, mp_tprintf(80, "%.*s", BSTR_P(name)));
+ } else {
+ mp_warn(log, "Option %.*s: item %.*s doesn't exist.\n",
+ BSTR_P(opt_name), BSTR_P(name));
+ }
+ r = M_OPT_EXIT;
+
+exit:
+ free_str_list(&args);
+ return r;
+}
+
+// Characters which may appear in a filter name
+#define NAMECH "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
+
+// Parse one item, e.g. -vf a=b:c:d,e=f:g => parse a=b:c:d into "a" and "b:c:d"
+static int parse_obj_settings(struct mp_log *log, struct bstr opt, int op,
+ struct bstr *pstr, const struct m_obj_list *list,
+ m_obj_settings_t **_ret)
+{
+ int r;
+ char **plist = NULL;
+ struct m_obj_desc desc;
+ bstr str = {0};
+ bstr label = {0};
+ bool nopos = list->disallow_positional_parameters;
+ bool enabled = true;
+
+ if (bstr_eatstart0(pstr, "@")) {
+ bstr rest;
+ if (!bstr_split_tok(*pstr, ":", &label, &rest)) {
+ // "@labelname" is the special enable/disable toggle syntax
+ if (op == OP_TOGGLE) {
+ int idx = bstrspn(*pstr, NAMECH);
+ label = bstr_splice(*pstr, 0, idx);
+ if (label.len) {
+ *pstr = bstr_cut(*pstr, idx);
+ goto done;
+ }
+ }
+ mp_err(log, "Option %.*s: ':' expected after label.\n", BSTR_P(opt));
+ return M_OPT_INVALID;
+ }
+ *pstr = rest;
+ if (label.len == 0) {
+ mp_err(log, "Option %.*s: label name expected.\n", BSTR_P(opt));
+ return M_OPT_INVALID;
+ }
+ }
+
+ if (list->allow_disable_entries && bstr_eatstart0(pstr, "!"))
+ enabled = false;
+
+ bool has_param = false;
+ int idx = bstrspn(*pstr, NAMECH);
+ str = bstr_splice(*pstr, 0, idx);
+ if (!str.len) {
+ mp_err(log, "Option %.*s: filter name expected.\n", BSTR_P(opt));
+ return M_OPT_INVALID;
+ }
+ *pstr = bstr_cut(*pstr, idx);
+ // video filters use "=", VOs use ":"
+ if (bstr_eatstart0(pstr, "=") || bstr_eatstart0(pstr, ":"))
+ has_param = true;
+
+ bool skip = false;
+ if (m_obj_list_find(&desc, list, str)) {
+ if (desc.replaced_name)
+ mp_warn(log, "Driver '%s' has been replaced with '%s'!\n",
+ desc.replaced_name, desc.name);
+ } else {
+ char name[80];
+ snprintf(name, sizeof(name), "%.*s", BSTR_P(str));
+ if (list->check_unknown_entry && !list->check_unknown_entry(name)) {
+ mp_err(log, "Option %.*s: %.*s doesn't exist.\n",
+ BSTR_P(opt), BSTR_P(str));
+ return M_OPT_INVALID;
+ }
+ desc = (struct m_obj_desc){0};
+ skip = true;
+ }
+
+ if (has_param) {
+ struct m_config *config = NULL;
+ if (!skip)
+ config = m_config_from_obj_desc_noalloc(NULL, log, &desc);
+ r = m_obj_parse_sub_config(log, opt, str, pstr, config,
+ M_SETOPT_CHECK_ONLY, nopos, &desc, list,
+ _ret ? &plist : NULL);
+ talloc_free(config);
+ if (r < 0)
+ return r;
+ }
+ if (!_ret)
+ return 1;
+
+done: ;
+ m_obj_settings_t item = {
+ .name = bstrto0(NULL, str),
+ .label = bstrdup0(NULL, label),
+ .enabled = enabled,
+ .attribs = plist,
+ };
+ obj_settings_list_insert_at(_ret, -1, &item);
+ return 1;
+}
+
+// Parse a single entry for -vf-remove (return 0 if not applicable)
+// mark_del is bounded by the number of items in dst
+static int parse_obj_settings_del(struct mp_log *log, struct bstr opt_name,
+ struct bstr *param, void *dst, bool *mark_del)
+{
+ bstr s = *param;
+ if (bstr_eatstart0(&s, "@")) {
+ // '@name:' -> parse as normal filter entry
+ // '@name,' or '@name<end>' -> parse here
+ int idx = bstrspn(s, NAMECH);
+ bstr label = bstr_splice(s, 0, idx);
+ s = bstr_cut(s, idx);
+ if (bstr_startswith0(s, ":"))
+ return 0;
+ if (dst) {
+ int label_index = 0;
+ label_index = obj_settings_list_find_by_label(VAL(dst), label);
+ if (label_index >= 0) {
+ mark_del[label_index] = true;
+ } else {
+ mp_warn(log, "Option %.*s: item label @%.*s not found.\n",
+ BSTR_P(opt_name), BSTR_P(label));
+ }
+ }
+ *param = s;
+ return 1;
+ }
+ return 0;
+}
+
+static int parse_obj_settings_list(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ m_obj_settings_t *res = NULL;
+ int op = OP_NONE;
+ bool *mark_del = NULL;
+ int num_items = obj_settings_list_num_items(dst ? VAL(dst) : 0);
+ const struct m_obj_list *ol = opt->priv;
+
+ assert(opt->priv);
+
+ if (bstr_endswith0(name, "-add")) {
+ op = OP_ADD;
+ } else if (bstr_endswith0(name, "-append")) {
+ op = OP_APPEND;
+ } else if (bstr_endswith0(name, "-set")) {
+ op = OP_NONE;
+ } else if (bstr_endswith0(name, "-pre")) {
+ op = OP_PRE;
+ } else if (bstr_endswith0(name, "-remove")) {
+ op = OP_REMOVE;
+ } else if (bstr_endswith0(name, "-clr")) {
+ op = OP_CLR;
+ } else if (bstr_endswith0(name, "-toggle")) {
+ op = OP_TOGGLE;
+ } else if (bstr_endswith0(name, "-help")) {
+ mp_err(log, "Option %s:\n"
+ "Supported operations are:\n"
+ " %s-set\n"
+ " Overwrite the old list with the given list\n\n"
+ " %s-append\n"
+ " Append the given filter to the current list\n\n"
+ " %s-add\n"
+ " Append the given list to the current list\n\n"
+ " %s-pre\n"
+ " Prepend the given list to the current list\n\n"
+ " %s-remove\n"
+ " Remove the given filter from the current list\n\n"
+ " %s-toggle\n"
+ " Add the filter to the list, or remove it if it's already added.\n\n"
+ " %s-clr\n"
+ " Clear the current list.\n\n",
+ opt->name, opt->name, opt->name, opt->name, opt->name,
+ opt->name, opt->name, opt->name);
+
+ return M_OPT_EXIT;
+ }
+
+ if (!bstrcmp0(param, "help")) {
+ mp_info(log, "Available %s:\n", ol->description);
+ for (int n = 0; ; n++) {
+ struct m_obj_desc desc;
+ if (!ol->get_desc(&desc, n))
+ break;
+ if (!desc.hidden) {
+ mp_info(log, " %-16s %s\n",
+ desc.name, desc.description);
+ }
+ }
+ mp_info(log, "\n");
+ if (ol->print_help_list)
+ ol->print_help_list(log);
+ if (!ol->use_global_options) {
+ mp_info(log, "Get help on individual entries via: --%s=entry=help\n",
+ opt->name);
+ }
+ return M_OPT_EXIT;
+ }
+
+ if (op == OP_CLR) {
+ if (param.len) {
+ mp_err(log, "Option %.*s: -clr does not take an argument.\n",
+ BSTR_P(name));
+ return M_OPT_INVALID;
+ }
+ if (dst)
+ free_obj_settings_list(dst);
+ return 0;
+ } else if (op == OP_REMOVE) {
+ mark_del = talloc_zero_array(NULL, bool, num_items + 1);
+ }
+
+ if (op != OP_NONE && param.len == 0)
+ return M_OPT_MISSING_PARAM;
+
+ while (param.len > 0) {
+ int r = 0;
+ if (op == OP_REMOVE)
+ r = parse_obj_settings_del(log, name, &param, dst, mark_del);
+ if (r == 0) {
+ r = parse_obj_settings(log, name, op, &param, ol, dst ? &res : NULL);
+ }
+ if (r < 0)
+ return r;
+ if (param.len > 0) {
+ const char sep[2] = {OPTION_LIST_SEPARATOR, 0};
+ if (!bstr_eatstart0(&param, sep))
+ return M_OPT_INVALID;
+ if (param.len == 0) {
+ if (!ol->allow_trailer)
+ return M_OPT_INVALID;
+ if (dst) {
+ m_obj_settings_t item = {
+ .name = talloc_strdup(NULL, ""),
+ };
+ obj_settings_list_insert_at(&res, -1, &item);
+ }
+ }
+ }
+ }
+
+ if (op != OP_NONE && res && res[0].name && res[1].name) {
+ if (op == OP_APPEND) {
+ mp_err(log, "Option %.*s: -append takes only 1 filter (no ',').\n",
+ BSTR_P(name));
+ return M_OPT_INVALID;
+ }
+ mp_warn(log, "Passing more than 1 argument to %.*s is deprecated!\n",
+ BSTR_P(name));
+ }
+
+ if (dst) {
+ m_obj_settings_t *list = VAL(dst);
+ if (op == OP_PRE) {
+ int prepend_counter = 0;
+ for (int n = 0; res && res[n].name; n++) {
+ int label = obj_settings_list_find_by_label0(list, res[n].label);
+ if (label < 0) {
+ obj_settings_list_insert_at(&list, prepend_counter, &res[n]);
+ prepend_counter++;
+ } else {
+ // Prefer replacement semantics, instead of actually
+ // prepending.
+ obj_setting_free(&list[label]);
+ list[label] = res[n];
+ }
+ }
+ talloc_free(res);
+ } else if (op == OP_ADD || op == OP_APPEND) {
+ for (int n = 0; res && res[n].name; n++) {
+ int label = obj_settings_list_find_by_label0(list, res[n].label);
+ if (label < 0) {
+ obj_settings_list_insert_at(&list, -1, &res[n]);
+ } else {
+ // Prefer replacement semantics, instead of actually
+ // appending.
+ obj_setting_free(&list[label]);
+ list[label] = res[n];
+ }
+ }
+ talloc_free(res);
+ } else if (op == OP_TOGGLE) {
+ for (int n = 0; res && res[n].name; n++) {
+ if (res[n].label && !res[n].name[0]) {
+ // Toggle enable/disable special case.
+ int found =
+ obj_settings_list_find_by_label0(list, res[n].label);
+ if (found < 0) {
+ mp_warn(log, "Option %.*s: Label %s not found\n",
+ BSTR_P(name), res[n].label);
+ } else {
+ list[found].enabled = !list[found].enabled;
+ }
+ obj_setting_free(&res[n]);
+ } else {
+ int found = obj_settings_find_by_content(list, &res[n]);
+ if (found < 0) {
+ obj_settings_list_insert_at(&list, -1, &res[n]);
+ } else {
+ obj_settings_list_del_at(&list, found);
+ obj_setting_free(&res[n]);
+ }
+ }
+ }
+ talloc_free(res);
+ } else if (op == OP_REMOVE) {
+ for (int n = num_items - 1; n >= 0; n--) {
+ if (mark_del[n])
+ obj_settings_list_del_at(&list, n);
+ }
+ for (int n = 0; res && res[n].name; n++) {
+ int found = obj_settings_find_by_content(list, &res[n]);
+ if (found >= 0)
+ obj_settings_list_del_at(&list, found);
+ }
+ free_obj_settings_list(&res);
+ } else {
+ assert(op == OP_NONE);
+ free_obj_settings_list(&list);
+ list = res;
+ }
+ VAL(dst) = list;
+ }
+
+ talloc_free(mark_del);
+ return 1;
+}
+
+static void append_param(char **res, char *param)
+{
+ if (strspn(param, NAMECH) == strlen(param)) {
+ *res = talloc_strdup_append(*res, param);
+ } else {
+ // Simple escaping: %BYTECOUNT%STRING
+ *res = talloc_asprintf_append(*res, "%%%zd%%%s", strlen(param), param);
+ }
+}
+
+static char *print_obj_settings_list(const m_option_t *opt, const void *val)
+{
+ m_obj_settings_t *list = VAL(val);
+ char *res = talloc_strdup(NULL, "");
+ for (int n = 0; list && list[n].name; n++) {
+ m_obj_settings_t *entry = &list[n];
+ if (n > 0)
+ res = talloc_strdup_append(res, ",");
+ // Assume labels and names don't need escaping
+ if (entry->label && entry->label[0])
+ res = talloc_asprintf_append(res, "@%s:", entry->label);
+ if (!entry->enabled)
+ res = talloc_strdup_append(res, "!");
+ res = talloc_strdup_append(res, entry->name);
+ if (entry->attribs && entry->attribs[0]) {
+ res = talloc_strdup_append(res, "=");
+ for (int i = 0; entry->attribs[i * 2 + 0]; i++) {
+ if (i > 0)
+ res = talloc_strdup_append(res, ":");
+ append_param(&res, entry->attribs[i * 2 + 0]);
+ res = talloc_strdup_append(res, "=");
+ append_param(&res, entry->attribs[i * 2 + 1]);
+ }
+ }
+ }
+ return res;
+}
+
+static int set_obj_settings_list(const m_option_t *opt, void *dst,
+ struct mpv_node *src)
+{
+ if (src->format != MPV_FORMAT_NODE_ARRAY)
+ return M_OPT_INVALID;
+ m_obj_settings_t *entries =
+ talloc_zero_array(NULL, m_obj_settings_t, src->u.list->num + 1);
+ for (int n = 0; n < src->u.list->num; n++) {
+ m_obj_settings_t *entry = &entries[n];
+ entry->enabled = true;
+ if (src->u.list->values[n].format != MPV_FORMAT_NODE_MAP)
+ goto error;
+ struct mpv_node_list *src_entry = src->u.list->values[n].u.list;
+ for (int i = 0; i < src_entry->num; i++) {
+ const char *key = src_entry->keys[i];
+ struct mpv_node *val = &src_entry->values[i];
+ if (strcmp(key, "name") == 0) {
+ if (val->format != MPV_FORMAT_STRING)
+ goto error;
+ entry->name = talloc_strdup(NULL, val->u.string);
+ } else if (strcmp(key, "label") == 0) {
+ if (val->format != MPV_FORMAT_STRING)
+ goto error;
+ entry->label = talloc_strdup(NULL, val->u.string);
+ } else if (strcmp(key, "enabled") == 0) {
+ if (val->format != MPV_FORMAT_FLAG)
+ goto error;
+ entry->enabled = val->u.flag;
+ } else if (strcmp(key, "params") == 0) {
+ if (val->format != MPV_FORMAT_NODE_MAP)
+ goto error;
+ struct mpv_node_list *src_params = val->u.list;
+ entry->attribs =
+ talloc_zero_array(NULL, char*, (src_params->num + 1) * 2);
+ for (int x = 0; x < src_params->num; x++) {
+ if (src_params->values[x].format != MPV_FORMAT_STRING)
+ goto error;
+ entry->attribs[x * 2 + 0] =
+ talloc_strdup(NULL, src_params->keys[x]);
+ entry->attribs[x * 2 + 1] =
+ talloc_strdup(NULL, src_params->values[x].u.string);
+ }
+ }
+ }
+ }
+ free_obj_settings_list(dst);
+ VAL(dst) = entries;
+ return 0;
+error:
+ free_obj_settings_list(&entries);
+ return M_OPT_INVALID;
+}
+
+static struct mpv_node *add_array_entry(struct mpv_node *dst)
+{
+ struct mpv_node_list *list = dst->u.list;
+ assert(dst->format == MPV_FORMAT_NODE_ARRAY&& dst->u.list);
+ MP_TARRAY_GROW(list, list->values, list->num);
+ return &list->values[list->num++];
+}
+
+static struct mpv_node *add_map_entry(struct mpv_node *dst, const char *key)
+{
+ struct mpv_node_list *list = dst->u.list;
+ assert(dst->format == MPV_FORMAT_NODE_MAP && dst->u.list);
+ MP_TARRAY_GROW(list, list->values, list->num);
+ MP_TARRAY_GROW(list, list->keys, list->num);
+ list->keys[list->num] = talloc_strdup(list, key);
+ return &list->values[list->num++];
+}
+
+static void add_map_string(struct mpv_node *dst, const char *key, const char *val)
+{
+ struct mpv_node *entry = add_map_entry(dst, key);
+ entry->format = MPV_FORMAT_STRING;
+ entry->u.string = talloc_strdup(dst->u.list, val);
+}
+
+static int get_obj_settings_list(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *val)
+{
+ m_obj_settings_t *list = VAL(val);
+ dst->format = MPV_FORMAT_NODE_ARRAY;
+ dst->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ ta_parent = dst->u.list;
+ for (int n = 0; list && list[n].name; n++) {
+ m_obj_settings_t *entry = &list[n];
+ struct mpv_node *nentry = add_array_entry(dst);
+ nentry->format = MPV_FORMAT_NODE_MAP;
+ nentry->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ add_map_string(nentry, "name", entry->name);
+ if (entry->label && entry->label[0])
+ add_map_string(nentry, "label", entry->label);
+ struct mpv_node *enabled = add_map_entry(nentry, "enabled");
+ enabled->format = MPV_FORMAT_FLAG;
+ enabled->u.flag = entry->enabled;
+ struct mpv_node *params = add_map_entry(nentry, "params");
+ params->format = MPV_FORMAT_NODE_MAP;
+ params->u.list = talloc_zero(ta_parent, struct mpv_node_list);
+ for (int i = 0; entry->attribs && entry->attribs[i * 2 + 0]; i++) {
+ add_map_string(params, entry->attribs[i * 2 + 0],
+ entry->attribs[i * 2 + 1]);
+ }
+ }
+ return 1;
+}
+
+static bool obj_settings_list_equal(const m_option_t *opt, void *pa, void *pb)
+{
+ struct m_obj_settings *a = VAL(pa);
+ struct m_obj_settings *b = VAL(pb);
+
+ if (a == b || !a || !b)
+ return a == b || (!a && !b[0].name) || (!b && !a[0].name);
+
+ for (int n = 0; a[n].name || b[n].name; n++) {
+ if (!a[n].name || !b[n].name)
+ return false;
+ if (!m_obj_settings_equal(&a[n], &b[n]))
+ return false;
+ }
+
+ return true;
+}
+
+bool m_obj_settings_equal(struct m_obj_settings *a, struct m_obj_settings *b)
+{
+ if (!str_equal(NULL, &a->name, &b->name))
+ return false;
+
+ if (!str_equal(NULL, &a->label, &b->label))
+ return false;
+
+ if (a->enabled != b->enabled)
+ return false;
+
+ return str_list_equal(NULL, &a->attribs, &b->attribs);
+}
+
+const m_option_type_t m_option_type_obj_settings_list = {
+ .name = "Object settings list",
+ .size = sizeof(m_obj_settings_t *),
+ .parse = parse_obj_settings_list,
+ .print = print_obj_settings_list,
+ .copy = copy_obj_settings_list,
+ .free = free_obj_settings_list,
+ .set = set_obj_settings_list,
+ .get = get_obj_settings_list,
+ .equal = obj_settings_list_equal,
+ .actions = (const struct m_option_action[]){
+ {"add"},
+ {"append"},
+ {"clr", M_OPT_TYPE_OPTIONAL_PARAM},
+ {"help", M_OPT_TYPE_OPTIONAL_PARAM},
+ {"pre"},
+ {"set"},
+ {"toggle"},
+ {"remove"},
+ {0}
+ },
+};
+
+#undef VAL
+#define VAL(x) (*(struct mpv_node *)(x))
+
+static int parse_node(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst)
+{
+ // Maybe use JSON?
+ mp_err(log, "option type doesn't accept strings");
+ return M_OPT_INVALID;
+}
+
+static char *print_node(const m_option_t *opt, const void *val)
+{
+ char *t = talloc_strdup(NULL, "");
+ if (json_write(&t, &VAL(val)) < 0) {
+ talloc_free(t);
+ t = NULL;
+ }
+ return t;
+}
+
+static char *pretty_print_node(const m_option_t *opt, const void *val)
+{
+ char *t = talloc_strdup(NULL, "");
+ if (json_write_pretty(&t, &VAL(val)) < 0) {
+ talloc_free(t);
+ t = NULL;
+ }
+ return t;
+}
+
+static void dup_node(void *ta_parent, struct mpv_node *node)
+{
+ switch (node->format) {
+ case MPV_FORMAT_STRING:
+ node->u.string = talloc_strdup(ta_parent, node->u.string);
+ break;
+ case MPV_FORMAT_NODE_ARRAY:
+ case MPV_FORMAT_NODE_MAP: {
+ struct mpv_node_list *oldlist = node->u.list;
+ struct mpv_node_list *new = talloc_zero(ta_parent, struct mpv_node_list);
+ node->u.list = new;
+ if (oldlist->num > 0) {
+ *new = *oldlist;
+ new->values = talloc_array(new, struct mpv_node, new->num);
+ for (int n = 0; n < new->num; n++) {
+ new->values[n] = oldlist->values[n];
+ dup_node(new, &new->values[n]);
+ }
+ if (node->format == MPV_FORMAT_NODE_MAP) {
+ new->keys = talloc_array(new, char*, new->num);
+ for (int n = 0; n < new->num; n++)
+ new->keys[n] = talloc_strdup(new, oldlist->keys[n]);
+ }
+ }
+ break;
+ }
+ case MPV_FORMAT_BYTE_ARRAY: {
+ struct mpv_byte_array *old = node->u.ba;
+ struct mpv_byte_array *new = talloc_zero(ta_parent, struct mpv_byte_array);
+ node->u.ba = new;
+ if (old->size > 0) {
+ *new = *old;
+ new->data = talloc_memdup(new, old->data, old->size);
+ }
+ break;
+ }
+ case MPV_FORMAT_NONE:
+ case MPV_FORMAT_FLAG:
+ case MPV_FORMAT_INT64:
+ case MPV_FORMAT_DOUBLE:
+ break;
+ default:
+ // unknown entry - mark as invalid
+ node->format = (mpv_format)-1;
+ }
+}
+
+static void copy_node(const m_option_t *opt, void *dst, const void *src)
+{
+ assert(sizeof(struct mpv_node) <= sizeof(union m_option_value));
+
+ if (!(dst && src))
+ return;
+
+ opt->type->free(dst);
+ VAL(dst) = VAL(src);
+ dup_node(NULL, &VAL(dst));
+}
+
+void *node_get_alloc(struct mpv_node *node)
+{
+ // Assume it was allocated with copy_node(), which allocates all
+ // sub-nodes with the parent node as talloc parent.
+ switch (node->format) {
+ case MPV_FORMAT_STRING:
+ return node->u.string;
+ case MPV_FORMAT_NODE_ARRAY:
+ case MPV_FORMAT_NODE_MAP:
+ return node->u.list;
+ default:
+ return NULL;
+ }
+}
+
+static void free_node(void *src)
+{
+ if (src) {
+ struct mpv_node *node = &VAL(src);
+ talloc_free(node_get_alloc(node));
+ *node = (struct mpv_node){{0}};
+ }
+}
+
+// idempotent functions for convenience
+static int node_set(const m_option_t *opt, void *dst, struct mpv_node *src)
+{
+ copy_node(opt, dst, src);
+ return 1;
+}
+
+static int node_get(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ *dst = VAL(src);
+ dup_node(ta_parent, dst);
+ return 1;
+}
+
+static bool node_equal(const m_option_t *opt, void *a, void *b)
+{
+ return equal_mpv_node(&VAL(a), &VAL(b));
+}
+
+const m_option_type_t m_option_type_node = {
+ .name = "Complex",
+ .size = sizeof(struct mpv_node),
+ .parse = parse_node,
+ .print = print_node,
+ .pretty_print = pretty_print_node,
+ .copy = copy_node,
+ .free = free_node,
+ .set = node_set,
+ .get = node_get,
+ .equal = node_equal,
+};
+
+// Special-cased by m_config.c.
+const m_option_type_t m_option_type_alias = {
+ .name = "alias",
+};
+const m_option_type_t m_option_type_cli_alias = {
+ .name = "alias",
+};
+const m_option_type_t m_option_type_removed = {
+ .name = "removed",
+};
+const m_option_type_t m_option_type_subconfig = {
+ .name = "Subconfig",
+};
diff --git a/options/m_option.h b/options/m_option.h
new file mode 100644
index 0000000..e62fa0f
--- /dev/null
+++ b/options/m_option.h
@@ -0,0 +1,764 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_M_OPTION_H
+#define MPLAYER_M_OPTION_H
+
+#include <float.h>
+#include <string.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "misc/bstr.h"
+#include "audio/chmap.h"
+#include "common/common.h"
+
+// m_option allows to parse, print and copy data of various types.
+
+typedef struct m_option_type m_option_type_t;
+typedef struct m_option m_option_t;
+struct m_config;
+struct mp_log;
+struct mpv_node;
+struct mpv_global;
+
+///////////////////////////// Options types declarations ////////////////////
+
+// Simple types
+extern const m_option_type_t m_option_type_bool;
+extern const m_option_type_t m_option_type_flag;
+extern const m_option_type_t m_option_type_dummy_flag;
+extern const m_option_type_t m_option_type_int;
+extern const m_option_type_t m_option_type_int64;
+extern const m_option_type_t m_option_type_byte_size;
+extern const m_option_type_t m_option_type_float;
+extern const m_option_type_t m_option_type_double;
+extern const m_option_type_t m_option_type_string;
+extern const m_option_type_t m_option_type_string_list;
+extern const m_option_type_t m_option_type_string_append_list;
+extern const m_option_type_t m_option_type_keyvalue_list;
+extern const m_option_type_t m_option_type_time;
+extern const m_option_type_t m_option_type_rel_time;
+extern const m_option_type_t m_option_type_choice;
+extern const m_option_type_t m_option_type_flags;
+extern const m_option_type_t m_option_type_msglevels;
+extern const m_option_type_t m_option_type_print_fn;
+extern const m_option_type_t m_option_type_imgfmt;
+extern const m_option_type_t m_option_type_fourcc;
+extern const m_option_type_t m_option_type_afmt;
+extern const m_option_type_t m_option_type_color;
+extern const m_option_type_t m_option_type_geometry;
+extern const m_option_type_t m_option_type_size_box;
+extern const m_option_type_t m_option_type_channels;
+extern const m_option_type_t m_option_type_aspect;
+extern const m_option_type_t m_option_type_obj_settings_list;
+extern const m_option_type_t m_option_type_node;
+extern const m_option_type_t m_option_type_rect;
+
+// Used internally by m_config.c
+extern const m_option_type_t m_option_type_alias;
+extern const m_option_type_t m_option_type_cli_alias;
+extern const m_option_type_t m_option_type_removed;
+extern const m_option_type_t m_option_type_subconfig;
+
+// Callback used by m_option_type_print_fn options.
+typedef void (*m_opt_print_fn)(struct mp_log *log);
+
+enum m_rel_time_type {
+ REL_TIME_NONE,
+ REL_TIME_ABSOLUTE,
+ REL_TIME_RELATIVE,
+ REL_TIME_PERCENT,
+ REL_TIME_CHAPTER,
+};
+
+struct m_rel_time {
+ double pos;
+ enum m_rel_time_type type;
+};
+
+struct m_color {
+ uint8_t r, g, b, a;
+};
+
+struct m_geometry {
+ int x, y, w, h;
+ bool xy_valid : 1, wh_valid : 1;
+ bool w_per : 1, h_per : 1;
+ bool x_sign : 1, y_sign : 1, x_per : 1, y_per : 1;
+ int ws; // workspace; valid if !=0
+};
+
+void m_geometry_apply(int *xpos, int *ypos, int *widw, int *widh,
+ int scrw, int scrh, struct m_geometry *gm);
+void m_rect_apply(struct mp_rect *rc, int w, int h, struct m_geometry *gm);
+
+struct m_channels {
+ bool set : 1;
+ bool auto_safe : 1;
+ struct mp_chmap *chmaps;
+ int num_chmaps;
+};
+
+struct m_obj_desc {
+ // Name which will be used in the option string
+ const char *name;
+ // Will be printed when "help" is passed
+ const char *description;
+ // Size of the private struct
+ int priv_size;
+ // If not NULL, default values for private struct
+ const void *priv_defaults;
+ // Options which refer to members in the private struct
+ const struct m_option *options;
+ // Prefix for each of the above options (none if NULL).
+ const char *options_prefix;
+ // For free use by the implementer of m_obj_list.get_desc
+ const void *p;
+ // Don't list entry with "help"
+ bool hidden;
+ // Callback to print custom help if "vf=entry=help" is passed
+ void (*print_help)(struct mp_log *log);
+ // Set by m_obj_list_find(). If the requested name is an old alias, this
+ // is set to the old name (while the name field uses the new name).
+ const char *replaced_name;
+ // For convenience: these are added as global command-line options.
+ const struct m_sub_options *global_opts;
+};
+
+// Extra definition needed for \ref m_option_type_obj_settings_list options.
+struct m_obj_list {
+ bool (*get_desc)(struct m_obj_desc *dst, int index);
+ const char *description;
+ // Can be set to a NULL terminated array of aliases
+ const char *aliases[5][2];
+ // Allow a trailing ",", which adds an entry with name=""
+ bool allow_trailer;
+ // Callback to test whether an unknown entry should be allowed. (This can
+ // be useful if adding them as explicit entries is too much work.)
+ bool (*check_unknown_entry)(const char *name);
+ // Allow syntax for disabling entries.
+ bool allow_disable_entries;
+ // This helps with confusing error messages if unknown flag options are used.
+ bool disallow_positional_parameters;
+ // Each sub-item is backed by global options (for AOs and VOs).
+ bool use_global_options;
+ // Callback to print additional custom help if "vf=help" is passed
+ void (*print_help_list)(struct mp_log *log);
+ // Callback to print help for _unknown_ entries with "vf=entry=help"
+ void (*print_unknown_entry_help)(struct mp_log *log, const char *name);
+};
+
+// Find entry by name
+bool m_obj_list_find(struct m_obj_desc *dst, const struct m_obj_list *list,
+ bstr name);
+
+// The data type used by \ref m_option_type_obj_settings_list.
+typedef struct m_obj_settings {
+ // Type of the object.
+ char *name;
+ // Optional user-defined name.
+ char *label;
+ // User enable flag.
+ bool enabled;
+ // NULL terminated array of parameter/value pairs.
+ char **attribs;
+} m_obj_settings_t;
+
+bool m_obj_settings_equal(struct m_obj_settings *a, struct m_obj_settings *b);
+
+struct m_opt_choice_alternatives {
+ char *name;
+ int value;
+};
+
+const char *m_opt_choice_str(const struct m_opt_choice_alternatives *choices,
+ int value);
+
+// Validator function signatures. Required to properly type the param value.
+typedef int (*m_opt_generic_validate_fn)(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, void *value);
+
+typedef int (*m_opt_string_validate_fn)(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+typedef int (*m_opt_int_validate_fn)(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const int *value);
+
+
+// m_option.priv points to this if OPT_SUBSTRUCT is used
+struct m_sub_options {
+ const char *prefix;
+ const struct m_option *opts;
+ size_t size;
+ const void *defaults;
+ // Change flags passed to mp_option_change_callback() if any option that is
+ // directly or indirectly part of this group is changed.
+ int change_flags;
+ // Return further sub-options, for example for optional components. If set,
+ // this is called with increasing index (starting from 0), as long as true
+ // is returned. If true is returned and *sub is set in any of these calls,
+ // they are added as options.
+ bool (*get_sub_options)(int index, const struct m_sub_options **sub);
+};
+
+#define CONF_TYPE_BOOL (&m_option_type_bool)
+#define CONF_TYPE_FLAG (&m_option_type_flag)
+#define CONF_TYPE_INT (&m_option_type_int)
+#define CONF_TYPE_INT64 (&m_option_type_int64)
+#define CONF_TYPE_FLOAT (&m_option_type_float)
+#define CONF_TYPE_DOUBLE (&m_option_type_double)
+#define CONF_TYPE_STRING (&m_option_type_string)
+#define CONF_TYPE_STRING_LIST (&m_option_type_string_list)
+#define CONF_TYPE_IMGFMT (&m_option_type_imgfmt)
+#define CONF_TYPE_FOURCC (&m_option_type_fourcc)
+#define CONF_TYPE_AFMT (&m_option_type_afmt)
+#define CONF_TYPE_OBJ_SETTINGS_LIST (&m_option_type_obj_settings_list)
+#define CONF_TYPE_TIME (&m_option_type_time)
+#define CONF_TYPE_CHOICE (&m_option_type_choice)
+#define CONF_TYPE_NODE (&m_option_type_node)
+
+// Possible option values. Code is allowed to access option data without going
+// through this union. It serves for self-documentation and to get minimal
+// size/alignment requirements for option values in general.
+union m_option_value {
+ bool bool_;
+ int flag; // not the C type "bool"!
+ int int_;
+ int64_t int64;
+ float float_;
+ double double_;
+ char *string;
+ char **string_list;
+ char **keyvalue_list;
+ int imgfmt;
+ unsigned int fourcc;
+ int afmt;
+ m_obj_settings_t *obj_settings_list;
+ double time;
+ struct m_rel_time rel_time;
+ struct m_color color;
+ struct m_geometry geometry;
+ struct m_geometry size_box;
+ struct m_channels channels;
+};
+
+// Keep fully zeroed instance of m_option_value to use as a default value, before
+// any specific union member is used. C standard says that `= {0}` activates and
+// initializes only the first member of the union, leaving padding bits undefined.
+static const union m_option_value m_option_value_default;
+
+////////////////////////////////////////////////////////////////////////////
+
+struct m_option_action {
+ // The name of the suffix, e.g. "add" for a list. If the option is named
+ // "foo", this will be available as "--foo-add". Note that no suffix (i.e.
+ // "--foo" is implicitly always available.
+ const char *name;
+ // One of M_OPT_TYPE*.
+ unsigned int flags;
+};
+
+// Option type description
+struct m_option_type {
+ const char *name;
+ // Size needed for the data.
+ unsigned int size;
+ // One of M_OPT_TYPE*.
+ unsigned int flags;
+
+ // Parse the data from a string.
+ /** It is the only required function, all others can be NULL.
+ * Generally should not be called directly outside of the options module,
+ * but instead through \ref m_option_parse which calls additional option
+ * specific callbacks during the process.
+ *
+ * \param log for outputting parser error or help messages
+ * \param opt The option that is parsed.
+ * \param name The full option name.
+ * \param param The parameter to parse.
+ * may not be an argument meant for this option
+ * \param dst Pointer to the memory where the data should be written.
+ * If NULL the parameter validity should still be checked.
+ * \return On error a negative value is returned, on success the number
+ * of arguments consumed. For details see \ref OptionParserReturn.
+ */
+ int (*parse)(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst);
+
+ // Print back a value in string form.
+ /** \param opt The option to print.
+ * \param val Pointer to the memory holding the data to be printed.
+ * \return An allocated string containing the text value or (void*)-1
+ * on error.
+ */
+ char *(*print)(const m_option_t *opt, const void *val);
+
+ // Print the value in a human readable form. Unlike print(), it doesn't
+ // necessarily return the exact value, and is generally not parseable with
+ // parse().
+ char *(*pretty_print)(const m_option_t *opt, const void *val);
+
+ // Copy data between two locations. Deep copy if the data has pointers.
+ // The implementation must free *dst if memory allocation is involved.
+ /** \param opt The option to copy.
+ * \param dst Pointer to the destination memory.
+ * \param src Pointer to the source memory.
+ */
+ void (*copy)(const m_option_t *opt, void *dst, const void *src);
+
+ // Free the data allocated for a save slot.
+ /** This is only needed for dynamic types like strings.
+ * \param dst Pointer to the data, usually a pointer that should be freed and
+ * set to NULL.
+ */
+ void (*free)(void *dst);
+
+ // Add the value add to the value in val. For types that are not numeric,
+ // add gives merely the direction. The wrap parameter determines whether
+ // the value is clipped, or wraps around to the opposite max/min.
+ void (*add)(const m_option_t *opt, void *val, double add, bool wrap);
+
+ // Multiply the value with the factor f. The callback must clip the result
+ // to the valid value range of the option.
+ void (*multiply)(const m_option_t *opt, void *val, double f);
+
+ // Set the option value in dst to the contents of src.
+ // (If the option is dynamic, the old value in *dst has to be freed.)
+ // Return values:
+ // M_OPT_UNKNOWN: src is in an unknown format
+ // M_OPT_INVALID: src is incorrectly formatted
+ // >= 0: success
+ // other error code: some other error, essentially M_OPT_INVALID refined
+ int (*set)(const m_option_t *opt, void *dst, struct mpv_node *src);
+
+ // Copy the option value in src to dst. Use ta_parent for any dynamic
+ // memory allocations. It's explicitly allowed to have mpv_node reference
+ // static strings (and even mpv_node_list.keys), though.
+ int (*get)(const m_option_t *opt, void *ta_parent, struct mpv_node *dst,
+ void *src);
+
+ // Return whether the values are the same. (There are no "unordered"
+ // results; for example, two floats with the value NaN compare equal. Other
+ // ambiguous floats, such as +0 and -0 compare equal. Some option types may
+ // incorrectly report unequal for values that are equal, such as sets (if
+ // the element order is different, which incorrectly matters), but values
+ // duplicated with m_option_copy() always return as equal. Empty strings
+ // and NULL strings are equal. Ambiguous unicode representations compare
+ // unequal.)
+ // If not set, values are always considered equal (=> not really optional).
+ bool (*equal)(const m_option_t *opt, void *a, void *b);
+
+ // Optional: list of suffixes, terminated with a {0} entry. An empty list
+ // behaves like the list being NULL.
+ const struct m_option_action *actions;
+};
+
+// Option description
+struct m_option {
+ // Option name.
+ // Option declarations can use this as positional field.
+ const char *name;
+
+ // Option type.
+ const m_option_type_t *type;
+
+ // See \ref OptionFlags.
+ unsigned int flags;
+
+ int offset;
+
+ // Most numeric types restrict the range to [min, max] if min<max (this
+ // implies that if min/max are not set, the full range is used). In all
+ // cases, the actual range is clamped to the type's native range.
+ // Float types use [DBL_MIN, DBL_MAX], though by setting min or max to
+ // -/+INFINITY, the range can be extended to INFINITY. (This part is buggy
+ // for "float".)
+ // Preferably use M_RANGE() to set these fields.
+ double min, max;
+
+ // Type dependent data (for all kinds of extended settings).
+ void *priv;
+
+ // Initialize variable to given default before parsing options
+ const void *defval;
+
+ // Print a warning when this option is used (for options with no direct
+ // replacement.)
+ const char *deprecation_message;
+
+ // Optional function that validates a param value for this option.
+ m_opt_generic_validate_fn validate;
+
+ // Optional function that displays help. Will replace type-specific help.
+ int (*help)(struct mp_log *log, const m_option_t *opt, struct bstr name);
+};
+
+char *format_file_size(int64_t size);
+
+// The option is forbidden in config files.
+#define M_OPT_NOCFG (1 << 2)
+
+// The option should be set during command line pre-parsing
+#define M_OPT_PRE_PARSE (1 << 4)
+
+// The option expects a file name (or a list of file names)
+#define M_OPT_FILE (1 << 5)
+
+// Do not add as property.
+#define M_OPT_NOPROP (1 << 6)
+
+// Enable special semantics for some options when parsing the string "help".
+#define M_OPT_HAVE_HELP (1 << 7)
+
+// The following are also part of the M_OPT_* flags, and are used to update
+// certain groups of options.
+#define UPDATE_OPT_FIRST (1 << 8)
+#define UPDATE_TERM (1 << 8) // terminal options
+#define UPDATE_SUB_FILT (1 << 9) // subtitle filter options
+#define UPDATE_OSD (1 << 10) // related to OSD rendering
+#define UPDATE_BUILTIN_SCRIPTS (1 << 11) // osc/ytdl/stats
+#define UPDATE_IMGPAR (1 << 12) // video image params overrides
+#define UPDATE_INPUT (1 << 13) // mostly --input-* options
+#define UPDATE_AUDIO (1 << 14) // --audio-channels etc.
+#define UPDATE_PRIORITY (1 << 15) // --priority (Windows-only)
+#define UPDATE_SCREENSAVER (1 << 16) // --stop-screensaver
+#define UPDATE_VOL (1 << 17) // softvol related options
+#define UPDATE_LAVFI_COMPLEX (1 << 18) // --lavfi-complex
+#define UPDATE_HWDEC (1 << 20) // --hwdec
+#define UPDATE_DVB_PROG (1 << 21) // some --dvbin-...
+#define UPDATE_SUB_HARD (1 << 22) // subtitle opts. that need full reinit
+#define UPDATE_SUB_EXTS (1 << 23) // update internal list of sub exts
+#define UPDATE_OPT_LAST (1 << 23)
+
+// All bits between _FIRST and _LAST (inclusive)
+#define UPDATE_OPTS_MASK \
+ (((UPDATE_OPT_LAST << 1) - 1) & ~(unsigned)(UPDATE_OPT_FIRST - 1))
+
+// type_float/type_double: string "default" is parsed as NaN (and reverse)
+#define M_OPT_DEFAULT_NAN (1 << 25)
+
+// type time: string "no" maps to MP_NOPTS_VALUE (if unset, NOPTS is rejected)
+#define M_OPT_ALLOW_NO (1 << 26)
+
+// type channels: disallow "auto" (still accept ""), limit list to at most 1 item.
+#define M_OPT_CHANNELS_LIMITED (1 << 27)
+
+// Like M_OPT_TYPE_OPTIONAL_PARAM.
+#define M_OPT_OPTIONAL_PARAM (1 << 30)
+
+// These are kept for compatibility with older code.
+#define CONF_NOCFG M_OPT_NOCFG
+#define CONF_PRE_PARSE M_OPT_PRE_PARSE
+
+// These flags are used to describe special parser capabilities or behavior.
+
+// The parameter is optional and by default no parameter is preferred. If
+// ambiguous syntax is used ("--opt value"), the command line parser will
+// assume that the argument takes no parameter. In config files, these
+// options can be used without "=" and value.
+#define M_OPT_TYPE_OPTIONAL_PARAM (1 << 0)
+
+// Behaves fundamentally like a choice or a superset of it (all allowed string
+// values are from a fixed set, although other types of values like numbers
+// might be allowed too). E.g. m_option_type_choice and m_option_type_flag.
+#define M_OPT_TYPE_CHOICE (1 << 1)
+
+// When m_option.min/max are set, they denote a value range.
+#define M_OPT_TYPE_USES_RANGE (1 << 2)
+
+///////////////////////////// Parser flags /////////////////////////////////
+
+// OptionParserReturn
+//
+// On success parsers return a number >= 0.
+//
+// To indicate that MPlayer should exit without playing anything,
+// parsers return M_OPT_EXIT.
+//
+// On error one of the following (negative) error codes is returned:
+
+// For use by higher level APIs when the option name is invalid.
+#define M_OPT_UNKNOWN -1
+
+// Returned when a parameter is needed but wasn't provided.
+#define M_OPT_MISSING_PARAM -2
+
+// Returned when the given parameter couldn't be parsed.
+#define M_OPT_INVALID -3
+
+// Returned if the value is "out of range". The exact meaning may
+// vary from type to type.
+#define M_OPT_OUT_OF_RANGE -4
+
+// The option doesn't take a parameter.
+#define M_OPT_DISALLOW_PARAM -5
+
+// Returned when MPlayer should exit. Used by various help stuff.
+#define M_OPT_EXIT -6
+
+char *m_option_strerror(int code);
+
+// Base function to parse options. Includes calling help and validation
+// callbacks. Only when this functionality is for some reason required to not
+// happen should the parse function pointer be utilized by itself.
+//
+// See \ref m_option_type::parse.
+int m_option_parse(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, struct bstr param, void *dst);
+
+// Helper to print options, see \ref m_option_type::print.
+static inline char *m_option_print(const m_option_t *opt, const void *val_ptr)
+{
+ if (opt->type->print)
+ return opt->type->print(opt, val_ptr);
+ else
+ return NULL;
+}
+
+static inline char *m_option_pretty_print(const m_option_t *opt,
+ const void *val_ptr)
+{
+ if (opt->type->pretty_print)
+ return opt->type->pretty_print(opt, val_ptr);
+ else
+ return m_option_print(opt, val_ptr);
+}
+
+// Helper around \ref m_option_type::copy.
+static inline void m_option_copy(const m_option_t *opt, void *dst,
+ const void *src)
+{
+ if (opt->type->copy)
+ opt->type->copy(opt, dst, src);
+}
+
+// Helper around \ref m_option_type::free.
+static inline void m_option_free(const m_option_t *opt, void *dst)
+{
+ if (opt->type->free)
+ opt->type->free(dst);
+}
+
+// see m_option_type.set
+static inline int m_option_set_node(const m_option_t *opt, void *dst,
+ struct mpv_node *src)
+{
+ if (opt->type->set)
+ return opt->type->set(opt, dst, src);
+ return M_OPT_UNKNOWN;
+}
+
+// Call m_option_parse for strings, m_option_set_node otherwise.
+int m_option_set_node_or_string(struct mp_log *log, const m_option_t *opt,
+ const char *name, void *dst, struct mpv_node *src);
+
+// see m_option_type.get
+static inline int m_option_get_node(const m_option_t *opt, void *ta_parent,
+ struct mpv_node *dst, void *src)
+{
+ if (opt->type->get)
+ return opt->type->get(opt, ta_parent, dst, src);
+ return M_OPT_UNKNOWN;
+}
+
+static inline bool m_option_equal(const m_option_t *opt, void *a, void *b)
+{
+ // Handle trivial equivalence.
+ // If not implemented, assume this type has no actual values => always equal.
+ if (a == b || !opt->type->equal)
+ return true;
+ return opt->type->equal(opt, a, b);
+}
+
+int m_option_required_params(const m_option_t *opt);
+
+extern const char m_option_path_separator;
+
+// Cause a compilation warning if typeof(expr) != type.
+// Should be used with pointer types only.
+#define MP_EXPECT_TYPE(type, expr) (0 ? (type)0 : (expr))
+
+// This behaves like offsetof(type, member), but will cause a compilation
+// warning if typeof(member) != expected_member_type.
+// It uses some trickery to make it compile as expression.
+#define MP_CHECKED_OFFSETOF(type, member, expected_member_type) \
+ (offsetof(type, member) + (0 && MP_EXPECT_TYPE(expected_member_type*, \
+ &((type*)0)->member)))
+
+#define OPT_TYPED_FIELD(type_, c_type, field) \
+ .type = &type_, \
+ .offset = MP_CHECKED_OFFSETOF(OPT_BASE_STRUCT, field, c_type)
+
+#define OPTION_LIST_SEPARATOR ','
+
+#define OPTDEF_STR(s) .defval = (void *)&(char * const){s}
+#define OPTDEF_INT(i) .defval = (void *)&(const int){i}
+#define OPTDEF_INT64(i) .defval = (void *)&(const int64_t){i}
+#define OPTDEF_FLOAT(f) .defval = (void *)&(const float){f}
+#define OPTDEF_DOUBLE(d) .defval = (void *)&(const double){d}
+
+#define M_RANGE(a, b) .min = (double) (a), .max = (double) (b)
+
+#define OPT_BOOL(field) \
+ OPT_TYPED_FIELD(m_option_type_bool, bool, field)
+
+#define OPT_INT(field) \
+ OPT_TYPED_FIELD(m_option_type_int, int, field)
+
+#define OPT_INT64(field) \
+ OPT_TYPED_FIELD(m_option_type_int64, int64_t, field)
+
+#define OPT_FLOAT(field) \
+ OPT_TYPED_FIELD(m_option_type_float, float, field)
+
+#define OPT_DOUBLE(field) \
+ OPT_TYPED_FIELD(m_option_type_double, double, field)
+
+#define OPT_STRING(field) \
+ OPT_TYPED_FIELD(m_option_type_string, char*, field)
+
+#define OPT_STRINGLIST(field) \
+ OPT_TYPED_FIELD(m_option_type_string_list, char**, field)
+
+#define OPT_KEYVALUELIST(field) \
+ OPT_TYPED_FIELD(m_option_type_keyvalue_list, char**, field)
+
+#define OPT_PATHLIST(field) \
+ OPT_TYPED_FIELD(m_option_type_string_list, char**, field), \
+ .priv = (void *)&m_option_path_separator
+
+#define OPT_TIME(field) \
+ OPT_TYPED_FIELD(m_option_type_time, double, field)
+
+#define OPT_REL_TIME(field) \
+ OPT_TYPED_FIELD(m_option_type_rel_time, struct m_rel_time, field)
+
+#define OPT_COLOR(field) \
+ OPT_TYPED_FIELD(m_option_type_color, struct m_color, field)
+
+#define OPT_BYTE_SIZE(field) \
+ OPT_TYPED_FIELD(m_option_type_byte_size, int64_t, field)
+
+// (Approximation of x<=SIZE_MAX/2 for m_option.max, which is double.)
+#define M_MAX_MEM_BYTES MPMIN((1ULL << 62), (size_t)-1 / 2)
+
+#define OPT_GEOMETRY(field) \
+ OPT_TYPED_FIELD(m_option_type_geometry, struct m_geometry, field)
+
+#define OPT_SIZE_BOX(field) \
+ OPT_TYPED_FIELD(m_option_type_size_box, struct m_geometry, field)
+
+#define OPT_RECT(field) \
+ OPT_TYPED_FIELD(m_option_type_rect, struct m_geometry, field)
+
+#define OPT_TRACKCHOICE(field) \
+ OPT_CHOICE(field, {"no", -2}, {"auto", -1}), \
+ M_RANGE(0, 8190)
+
+#define OPT_MSGLEVELS(field) \
+ OPT_TYPED_FIELD(m_option_type_msglevels, char **, field)
+
+#define OPT_ASPECT(field) \
+ OPT_TYPED_FIELD(m_option_type_aspect, double, field)
+
+#define OPT_IMAGEFORMAT(field) \
+ OPT_TYPED_FIELD(m_option_type_imgfmt, int, field)
+
+#define OPT_AUDIOFORMAT(field) \
+ OPT_TYPED_FIELD(m_option_type_afmt, int, field)
+
+#define OPT_CHANNELS(field) \
+ OPT_TYPED_FIELD(m_option_type_channels, struct m_channels, field)
+
+#define OPT_INT_VALIDATE(field, validate_fn) \
+ OPT_TYPED_FIELD(m_option_type_int, int, field), \
+ .validate = (m_opt_generic_validate_fn) \
+ MP_EXPECT_TYPE(m_opt_int_validate_fn, validate_fn)
+
+#define OPT_STRING_VALIDATE(field, validate_fn) \
+ OPT_TYPED_FIELD(m_option_type_string, char*, field), \
+ .validate = (m_opt_generic_validate_fn) \
+ MP_EXPECT_TYPE(m_opt_string_validate_fn, validate_fn)
+
+#define M_CHOICES(...) \
+ .priv = (void *)&(const struct m_opt_choice_alternatives[]){ __VA_ARGS__, {0}}
+
+// Variant which takes a pointer to struct m_opt_choice_alternatives directly
+#define OPT_CHOICE_C(field, choices) \
+ OPT_TYPED_FIELD(m_option_type_choice, int, field), \
+ .priv = (void *)MP_EXPECT_TYPE(const struct m_opt_choice_alternatives*, choices)
+
+// Variant where you pass a struct m_opt_choice_alternatives initializer
+#define OPT_CHOICE(field, ...) \
+ OPT_TYPED_FIELD(m_option_type_choice, int, field), \
+ M_CHOICES(__VA_ARGS__)
+
+#define OPT_FLAGS(field, ...) \
+ OPT_TYPED_FIELD(m_option_type_flags, int, field), \
+ M_CHOICES(__VA_ARGS__)
+
+#define OPT_SETTINGSLIST(field, objlist) \
+ OPT_TYPED_FIELD(m_option_type_obj_settings_list, m_obj_settings_t*, field), \
+ .priv = (void*)MP_EXPECT_TYPE(const struct m_obj_list*, objlist)
+
+#define OPT_FOURCC(field) \
+ OPT_TYPED_FIELD(m_option_type_fourcc, int, field)
+
+#define OPT_CYCLEDIR(field) \
+ OPT_TYPED_FIELD(m_option_type_cycle_dir, double, field)
+
+// subconf must have the type struct m_sub_options.
+// All sub-options are prefixed with "name-" and are added to the current
+// (containing) option list.
+// If name is "", add the sub-options directly instead.
+// "field" refers to the field, that must be a pointer to a field described by
+// the subconf struct.
+#define OPT_SUBSTRUCT(field, subconf) \
+ .offset = offsetof(OPT_BASE_STRUCT, field), \
+ .type = &m_option_type_subconfig, .priv = (void*)&subconf
+
+// Non-fields
+
+#define OPT_ALIAS(newname) \
+ .type = &m_option_type_alias, .priv = newname, .offset = -1
+
+// If "--optname" was removed, but "--newname" has the same semantics.
+// It will be redirected, and a warning will be printed on first use.
+#define OPT_REPLACED_MSG(newname, msg) \
+ .type = &m_option_type_alias, .priv = newname, \
+ .deprecation_message = (msg), .offset = -1
+
+// Same, with a generic deprecation message.
+#define OPT_REPLACED(newname) OPT_REPLACED_MSG(newname, "")
+
+// Alias, resolved on the CLI/config file/profile parser level only.
+#define OPT_CLI_ALIAS(newname) \
+ .type = &m_option_type_cli_alias, .priv = newname, \
+ .flags = M_OPT_NOPROP, .offset = -1
+
+// "--optname" doesn't exist, but inform the user about a replacement with msg.
+#define OPT_REMOVED(msg) \
+ .type = &m_option_type_removed, .priv = msg, \
+ .deprecation_message = "", .flags = M_OPT_NOPROP, .offset = -1
+
+#define OPT_PRINT(fn) \
+ .flags = M_OPT_NOCFG | M_OPT_PRE_PARSE | M_OPT_NOPROP, \
+ .type = &m_option_type_print_fn, \
+ .priv = MP_EXPECT_TYPE(m_opt_print_fn, fn), \
+ .offset = -1
+
+#endif /* MPLAYER_M_OPTION_H */
diff --git a/options/m_property.c b/options/m_property.c
new file mode 100644
index 0000000..1b76f05
--- /dev/null
+++ b/options/m_property.c
@@ -0,0 +1,630 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/// \file
+/// \ingroup Properties
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "libmpv/client.h"
+
+#include "mpv_talloc.h"
+#include "m_option.h"
+#include "m_property.h"
+#include "common/msg.h"
+#include "common/common.h"
+
+static int m_property_multiply(struct mp_log *log,
+ const struct m_property *prop_list,
+ const char *property, double f, void *ctx)
+{
+ union m_option_value val = m_option_value_default;
+ struct m_option opt = {0};
+ int r;
+
+ r = m_property_do(log, prop_list, property, M_PROPERTY_GET_CONSTRICTED_TYPE,
+ &opt, ctx);
+ if (r != M_PROPERTY_OK)
+ return r;
+ assert(opt.type);
+
+ if (!opt.type->multiply)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ r = m_property_do(log, prop_list, property, M_PROPERTY_GET, &val, ctx);
+ if (r != M_PROPERTY_OK)
+ return r;
+ opt.type->multiply(&opt, &val, f);
+ r = m_property_do(log, prop_list, property, M_PROPERTY_SET, &val, ctx);
+ m_option_free(&opt, &val);
+ return r;
+}
+
+struct m_property *m_property_list_find(const struct m_property *list,
+ const char *name)
+{
+ for (int n = 0; list && list[n].name; n++) {
+ if (strcmp(list[n].name, name) == 0)
+ return (struct m_property *)&list[n];
+ }
+ return NULL;
+}
+
+static int do_action(const struct m_property *prop_list, const char *name,
+ int action, void *arg, void *ctx)
+{
+ struct m_property *prop;
+ struct m_property_action_arg ka;
+ const char *sep = strchr(name, '/');
+ if (sep && sep[1]) {
+ char base[128];
+ snprintf(base, sizeof(base), "%.*s", (int)(sep - name), name);
+ prop = m_property_list_find(prop_list, base);
+ ka = (struct m_property_action_arg) {
+ .key = sep + 1,
+ .action = action,
+ .arg = arg,
+ };
+ action = M_PROPERTY_KEY_ACTION;
+ arg = &ka;
+ } else
+ prop = m_property_list_find(prop_list, name);
+ if (!prop)
+ return M_PROPERTY_UNKNOWN;
+ return prop->call(ctx, prop, action, arg);
+}
+
+// (as a hack, log can be NULL on read-only paths)
+int m_property_do(struct mp_log *log, const struct m_property *prop_list,
+ const char *name, int action, void *arg, void *ctx)
+{
+ union m_option_value val = m_option_value_default;
+ int r;
+
+ struct m_option opt = {0};
+ r = do_action(prop_list, name, M_PROPERTY_GET_TYPE, &opt, ctx);
+ if (r <= 0)
+ return r;
+ assert(opt.type);
+
+ switch (action) {
+ case M_PROPERTY_PRINT: {
+ if ((r = do_action(prop_list, name, M_PROPERTY_PRINT, arg, ctx)) >= 0)
+ return r;
+ // Fallback to m_option
+ if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0)
+ return r;
+ char *str = m_option_pretty_print(&opt, &val);
+ m_option_free(&opt, &val);
+ *(char **)arg = str;
+ return str != NULL;
+ }
+ case M_PROPERTY_GET_STRING: {
+ if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0)
+ return r;
+ char *str = m_option_print(&opt, &val);
+ m_option_free(&opt, &val);
+ *(char **)arg = str;
+ return str != NULL;
+ }
+ case M_PROPERTY_SET_STRING: {
+ struct mpv_node node = { .format = MPV_FORMAT_STRING, .u.string = arg };
+ return m_property_do(log, prop_list, name, M_PROPERTY_SET_NODE, &node, ctx);
+ }
+ case M_PROPERTY_MULTIPLY: {
+ return m_property_multiply(log, prop_list, name, *(double *)arg, ctx);
+ }
+ case M_PROPERTY_SWITCH: {
+ if (!log)
+ return M_PROPERTY_ERROR;
+ struct m_property_switch_arg *sarg = arg;
+ if ((r = do_action(prop_list, name, M_PROPERTY_SWITCH, arg, ctx)) !=
+ M_PROPERTY_NOT_IMPLEMENTED)
+ return r;
+ // Fallback to m_option
+ r = m_property_do(log, prop_list, name, M_PROPERTY_GET_CONSTRICTED_TYPE,
+ &opt, ctx);
+ if (r <= 0)
+ return r;
+ assert(opt.type);
+ if (!opt.type->add)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0)
+ return r;
+ opt.type->add(&opt, &val, sarg->inc, sarg->wrap);
+ r = do_action(prop_list, name, M_PROPERTY_SET, &val, ctx);
+ m_option_free(&opt, &val);
+ return r;
+ }
+ case M_PROPERTY_GET_CONSTRICTED_TYPE: {
+ r = do_action(prop_list, name, action, arg, ctx);
+ if (r >= 0 || r == M_PROPERTY_UNAVAILABLE)
+ return r;
+ if ((r = do_action(prop_list, name, M_PROPERTY_GET_TYPE, arg, ctx)) >= 0)
+ return r;
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ }
+ case M_PROPERTY_SET: {
+ return do_action(prop_list, name, M_PROPERTY_SET, arg, ctx);
+ }
+ case M_PROPERTY_GET_NODE: {
+ if ((r = do_action(prop_list, name, M_PROPERTY_GET_NODE, arg, ctx)) !=
+ M_PROPERTY_NOT_IMPLEMENTED)
+ return r;
+ if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0)
+ return r;
+ struct mpv_node *node = arg;
+ int err = m_option_get_node(&opt, NULL, node, &val);
+ if (err == M_OPT_UNKNOWN) {
+ r = M_PROPERTY_NOT_IMPLEMENTED;
+ } else if (err < 0) {
+ r = M_PROPERTY_INVALID_FORMAT;
+ } else {
+ r = M_PROPERTY_OK;
+ }
+ m_option_free(&opt, &val);
+ return r;
+ }
+ case M_PROPERTY_SET_NODE: {
+ if (!log)
+ return M_PROPERTY_ERROR;
+ if ((r = do_action(prop_list, name, M_PROPERTY_SET_NODE, arg, ctx)) !=
+ M_PROPERTY_NOT_IMPLEMENTED)
+ return r;
+ int err = m_option_set_node_or_string(log, &opt, name, &val, arg);
+ if (err == M_OPT_UNKNOWN) {
+ r = M_PROPERTY_NOT_IMPLEMENTED;
+ } else if (err < 0) {
+ r = M_PROPERTY_INVALID_FORMAT;
+ } else {
+ r = do_action(prop_list, name, M_PROPERTY_SET, &val, ctx);
+ }
+ m_option_free(&opt, &val);
+ return r;
+ }
+ default:
+ return do_action(prop_list, name, action, arg, ctx);
+ }
+}
+
+bool m_property_split_path(const char *path, bstr *prefix, char **rem)
+{
+ char *next = strchr(path, '/');
+ if (next) {
+ *prefix = bstr_splice(bstr0(path), 0, next - path);
+ *rem = next + 1;
+ return true;
+ } else {
+ *prefix = bstr0(path);
+ *rem = "";
+ return false;
+ }
+}
+
+// If *action is M_PROPERTY_KEY_ACTION, but the associated path is "", then
+// make this into a top-level action.
+static void m_property_unkey(int *action, void **arg)
+{
+ if (*action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *ka = *arg;
+ if (!ka->key[0]) {
+ *action = ka->action;
+ *arg = ka->arg;
+ }
+ }
+}
+
+static int m_property_do_bstr(const struct m_property *prop_list, bstr name,
+ int action, void *arg, void *ctx)
+{
+ char *name0 = bstrdup0(NULL, name);
+ int ret = m_property_do(NULL, prop_list, name0, action, arg, ctx);
+ talloc_free(name0);
+ return ret;
+}
+
+static void append_str(char **s, int *len, bstr append)
+{
+ MP_TARRAY_GROW(NULL, *s, *len + append.len);
+ if (append.len)
+ memcpy(*s + *len, append.start, append.len);
+ *len = *len + append.len;
+}
+
+static int expand_property(const struct m_property *prop_list, char **ret,
+ int *ret_len, bstr prop, bool silent_error, void *ctx)
+{
+ bool cond_yes = bstr_eatstart0(&prop, "?");
+ bool cond_no = !cond_yes && bstr_eatstart0(&prop, "!");
+ bool test = cond_yes || cond_no;
+ bool raw = bstr_eatstart0(&prop, "=");
+ bstr comp_with = {0};
+ bool comp = test && bstr_split_tok(prop, "==", &prop, &comp_with);
+ if (test && !comp)
+ raw = true;
+ int method = raw ? M_PROPERTY_GET_STRING : M_PROPERTY_PRINT;
+
+ char *s = NULL;
+ int r = m_property_do_bstr(prop_list, prop, method, &s, ctx);
+ bool skip;
+ if (comp) {
+ skip = ((s && bstr_equals0(comp_with, s)) != cond_yes);
+ } else if (test) {
+ skip = (!!s != cond_yes);
+ } else {
+ skip = !!s;
+ char *append = s;
+ if (!s && !silent_error && !raw)
+ append = (r == M_PROPERTY_UNAVAILABLE) ? "(unavailable)" : "(error)";
+ append_str(ret, ret_len, bstr0(append));
+ }
+ talloc_free(s);
+ return skip;
+}
+
+char *m_properties_expand_string(const struct m_property *prop_list,
+ const char *str0, void *ctx)
+{
+ char *ret = NULL;
+ int ret_len = 0;
+ bool skip = false;
+ int level = 0, skip_level = 0;
+ bstr str = bstr0(str0);
+
+ while (str.len) {
+ if (level > 0 && bstr_eatstart0(&str, "}")) {
+ if (skip && level <= skip_level)
+ skip = false;
+ level--;
+ } else if (bstr_startswith0(str, "${") && bstr_find0(str, "}") >= 0) {
+ str = bstr_cut(str, 2);
+ level++;
+
+ // Assume ":" and "}" can't be part of the property name
+ // => if ":" comes before "}", it must be for the fallback
+ int term_pos = bstrcspn(str, ":}");
+ bstr name = bstr_splice(str, 0, term_pos < 0 ? str.len : term_pos);
+ str = bstr_cut(str, term_pos);
+ bool have_fallback = bstr_eatstart0(&str, ":");
+
+ if (!skip) {
+ skip = expand_property(prop_list, &ret, &ret_len, name,
+ have_fallback, ctx);
+ if (skip)
+ skip_level = level;
+ }
+ } else if (level == 0 && bstr_eatstart0(&str, "$>")) {
+ append_str(&ret, &ret_len, str);
+ break;
+ } else {
+ char c;
+
+ // Other combinations, e.g. "$x", are added verbatim
+ if (bstr_eatstart0(&str, "$$")) {
+ c = '$';
+ } else if (bstr_eatstart0(&str, "$}")) {
+ c = '}';
+ } else {
+ c = str.start[0];
+ str = bstr_cut(str, 1);
+ }
+
+ if (!skip)
+ MP_TARRAY_APPEND(NULL, ret, ret_len, c);
+ }
+ }
+
+ MP_TARRAY_APPEND(NULL, ret, ret_len, '\0');
+ return ret;
+}
+
+void m_properties_print_help_list(struct mp_log *log,
+ const struct m_property *list)
+{
+ int count = 0;
+
+ mp_info(log, "Name\n\n");
+ for (int i = 0; list[i].name; i++) {
+ const struct m_property *p = &list[i];
+ mp_info(log, " %s\n", p->name);
+ count++;
+ }
+ mp_info(log, "\nTotal: %d properties\n", count);
+}
+
+int m_property_bool_ro(int action, void* arg, bool var)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(bool *)arg = !!var;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_BOOL};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+int m_property_int_ro(int action, void *arg, int var)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *)arg = var;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_INT};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+int m_property_int64_ro(int action, void* arg, int64_t var)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int64_t *)arg = var;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_INT64};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+int m_property_float_ro(int action, void *arg, float var)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(float *)arg = var;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_FLOAT};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+int m_property_double_ro(int action, void *arg, double var)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(double *)arg = var;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_DOUBLE};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+int m_property_strdup_ro(int action, void* arg, const char *var)
+{
+ if (!var)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char **)arg = talloc_strdup(NULL, var);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+int m_property_read_sub_validate(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ m_property_unkey(&action, &arg);
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ case M_PROPERTY_PRINT:
+ case M_PROPERTY_KEY_ACTION:
+ return M_PROPERTY_VALID;
+ default:
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ };
+}
+
+// This allows you to make a list of values (like from a struct) available
+// as a number of sub-properties. The property list is set up with the current
+// property values on the stack before calling this function.
+// This does not support write access.
+int m_property_read_sub(const struct m_sub_property *props, int action, void *arg)
+{
+ m_property_unkey(&action, &arg);
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ struct mpv_node node;
+ node.format = MPV_FORMAT_NODE_MAP;
+ node.u.list = talloc_zero(NULL, mpv_node_list);
+ mpv_node_list *list = node.u.list;
+ for (int n = 0; props && props[n].name; n++) {
+ const struct m_sub_property *prop = &props[n];
+ if (prop->unavailable)
+ continue;
+ MP_TARRAY_GROW(list, list->values, list->num);
+ MP_TARRAY_GROW(list, list->keys, list->num);
+ mpv_node *val = &list->values[list->num];
+ if (m_option_get_node(&prop->type, list, val, (void*)&prop->value) < 0)
+ {
+ char *s = m_option_print(&prop->type, &prop->value);
+ val->format = MPV_FORMAT_STRING;
+ val->u.string = talloc_steal(list, s);
+ }
+ list->keys[list->num] = (char *)prop->name;
+ list->num++;
+ }
+ *(struct mpv_node *)arg = node;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT: {
+ // Output "something" - what it really should return is not yet decided.
+ // It should probably be something that is easy to consume by slave
+ // mode clients. (M_PROPERTY_PRINT on the other hand can return this
+ // as human readable version just fine).
+ char *res = NULL;
+ for (int n = 0; props && props[n].name; n++) {
+ const struct m_sub_property *prop = &props[n];
+ if (prop->unavailable)
+ continue;
+ char *s = m_option_print(&prop->type, &prop->value);
+ ta_xasprintf_append(&res, "%s=%s\n", prop->name, s);
+ talloc_free(s);
+ }
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ const struct m_sub_property *prop = NULL;
+ for (int n = 0; props && props[n].name; n++) {
+ if (strcmp(props[n].name, ka->key) == 0) {
+ prop = &props[n];
+ break;
+ }
+ }
+ if (!prop)
+ return M_PROPERTY_UNKNOWN;
+ if (prop->unavailable)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (ka->action) {
+ case M_PROPERTY_GET: {
+ memset(ka->arg, 0, prop->type.type->size);
+ m_option_copy(&prop->type, ka->arg, &prop->value);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = prop->type;
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+// Make a list of items available as indexed sub-properties. E.g. you can access
+// item 0 as "property/0", item 1 as "property/1", etc., where each of these
+// properties is redirected to the get_item(0, ...), get_item(1, ...), callback.
+// Additionally, the number of entries is made available as "property/count".
+// action, arg: property access.
+// count: number of items.
+// get_item: callback to access a single item.
+// ctx: userdata passed to get_item.
+int m_property_read_list(int action, void *arg, int count,
+ m_get_item_cb get_item, void *ctx)
+{
+ m_property_unkey(&action, &arg);
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ struct mpv_node node;
+ node.format = MPV_FORMAT_NODE_ARRAY;
+ node.u.list = talloc_zero(NULL, mpv_node_list);
+ node.u.list->num = count;
+ node.u.list->values = talloc_array(node.u.list, mpv_node, count);
+ for (int n = 0; n < count; n++) {
+ struct mpv_node *sub = &node.u.list->values[n];
+ sub->format = MPV_FORMAT_NONE;
+ int r;
+ r = get_item(n, M_PROPERTY_GET_NODE, sub, ctx);
+ if (r == M_PROPERTY_NOT_IMPLEMENTED) {
+ struct m_option opt = {0};
+ r = get_item(n, M_PROPERTY_GET_TYPE, &opt, ctx);
+ if (r != M_PROPERTY_OK)
+ goto err;
+ union m_option_value val = m_option_value_default;
+ r = get_item(n, M_PROPERTY_GET, &val, ctx);
+ if (r != M_PROPERTY_OK)
+ goto err;
+ m_option_get_node(&opt, node.u.list, sub, &val);
+ m_option_free(&opt, &val);
+ err: ;
+ }
+ }
+ *(struct mpv_node *)arg = node;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT: {
+ // See m_property_read_sub() remarks.
+ char *res = NULL;
+ for (int n = 0; n < count; n++) {
+ char *s = NULL;
+ int r = get_item(n, M_PROPERTY_PRINT, &s, ctx);
+ if (r != M_PROPERTY_OK) {
+ talloc_free(res);
+ return r;
+ }
+ ta_xasprintf_append(&res, "%d: %s\n", n, s);
+ talloc_free(s);
+ }
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ if (strcmp(ka->key, "count") == 0) {
+ switch (ka->action) {
+ case M_PROPERTY_GET_TYPE: {
+ struct m_option opt = {.type = CONF_TYPE_INT};
+ *(struct m_option *)ka->arg = opt;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET:
+ *(int *)ka->arg = MPMAX(0, count);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ }
+ // This is expected of the form "123" or "123/rest"
+ char *next = strchr(ka->key, '/');
+ char *end = NULL;
+ const char *key_end = ka->key + strlen(ka->key);
+ long int item = strtol(ka->key, &end, 10);
+ // not a number, trailing characters, etc.
+ if ((end != key_end || ka->key == key_end) && end != next)
+ return M_PROPERTY_UNKNOWN;
+ if (item < 0 || item >= count)
+ return M_PROPERTY_UNKNOWN;
+ if (next) {
+ // Sub-path
+ struct m_property_action_arg n_ka = *ka;
+ n_ka.key = next + 1;
+ return get_item(item, M_PROPERTY_KEY_ACTION, &n_ka, ctx);
+ } else {
+ // Direct query
+ return get_item(item, ka->action, ka->arg, ctx);
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
diff --git a/options/m_property.h b/options/m_property.h
new file mode 100644
index 0000000..0dce246
--- /dev/null
+++ b/options/m_property.h
@@ -0,0 +1,234 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_M_PROPERTY_H
+#define MPLAYER_M_PROPERTY_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "m_option.h"
+
+struct mp_log;
+
+enum mp_property_action {
+ // Get the property type. This defines the fundamental data type read from
+ // or written to the property.
+ // If unimplemented, the m_option entry that defines the property is used.
+ // arg: m_option*
+ M_PROPERTY_GET_TYPE,
+
+ // Get the current value.
+ // arg: pointer to a variable of the type according to the property type
+ M_PROPERTY_GET,
+
+ // Set a new value. The property wrapper will make sure that only valid
+ // values are set (e.g. according to the property type's min/max range).
+ // If unimplemented, the property is read-only.
+ // arg: pointer to a variable of the type according to the property type
+ M_PROPERTY_SET,
+
+ // Get human readable string representing the current value.
+ // If unimplemented, the property wrapper uses the property type as
+ // fallback.
+ // arg: char**
+ M_PROPERTY_PRINT,
+
+ // Like M_PROPERTY_GET_TYPE, but get a type that is compatible to the real
+ // type, but reflect practical limits, such as runtime-available values.
+ // This is mostly used for "UI" related things.
+ // (Example: volume property.)
+ M_PROPERTY_GET_CONSTRICTED_TYPE,
+
+ // Switch the property up/down by a given value.
+ // If unimplemented, the property wrapper uses the property type as
+ // fallback.
+ // arg: struct m_property_switch_arg*
+ M_PROPERTY_SWITCH,
+
+ // Get a string containing a parseable representation.
+ // Can't be overridden by property implementations.
+ // arg: char**
+ M_PROPERTY_GET_STRING,
+
+ // Set a new value from a string. The property wrapper parses this using the
+ // parse function provided by the property type.
+ // Can't be overridden by property implementations.
+ // arg: char*
+ M_PROPERTY_SET_STRING,
+
+ // Set a mpv_node value.
+ // arg: mpv_node*
+ M_PROPERTY_GET_NODE,
+
+ // Get a mpv_node value.
+ // arg: mpv_node*
+ M_PROPERTY_SET_NODE,
+
+ // Multiply numeric property with a factor.
+ // arg: double*
+ M_PROPERTY_MULTIPLY,
+
+ // Pass down an action to a sub-property.
+ // arg: struct m_property_action_arg*
+ M_PROPERTY_KEY_ACTION,
+
+ // Delete a value.
+ // Most properties do not implement this.
+ // arg: (ignored)
+ M_PROPERTY_DELETE,
+};
+
+// Argument for M_PROPERTY_SWITCH
+struct m_property_switch_arg {
+ double inc; // value to add to property, or cycle direction
+ bool wrap; // whether value should wrap around on over/underflow
+};
+
+// Argument for M_PROPERTY_KEY_ACTION
+struct m_property_action_arg {
+ const char* key;
+ int action;
+ void* arg;
+};
+
+enum mp_property_return {
+ // Returned from validator if action should be executed.
+ M_PROPERTY_VALID = 2,
+
+ // Returned on success.
+ M_PROPERTY_OK = 1,
+
+ // Returned on error.
+ M_PROPERTY_ERROR = 0,
+
+ // Returned when the property can't be used, for example video related
+ // properties while playing audio only.
+ M_PROPERTY_UNAVAILABLE = -1,
+
+ // Returned if the requested action is not implemented.
+ M_PROPERTY_NOT_IMPLEMENTED = -2,
+
+ // Returned when asking for a property that doesn't exist.
+ M_PROPERTY_UNKNOWN = -3,
+
+ // When trying to set invalid or incorrectly formatted data.
+ M_PROPERTY_INVALID_FORMAT = -4,
+};
+
+struct m_property {
+ const char *name;
+ // ctx: opaque caller context, which the property might use
+ // prop: pointer to this struct
+ // action: one of enum mp_property_action
+ // arg: specific to the action
+ // returns: one of enum mp_property_return
+ int (*call)(void *ctx, struct m_property *prop, int action, void *arg);
+ void *priv;
+ // Special-case: mark options for which command.c uses the option-bridge
+ bool is_option;
+};
+
+struct m_property *m_property_list_find(const struct m_property *list,
+ const char *name);
+
+// Access a property.
+// action: one of m_property_action
+// ctx: opaque value passed through to property implementation
+// returns: one of mp_property_return
+int m_property_do(struct mp_log *log, const struct m_property* prop_list,
+ const char* property_name, int action, void* arg, void *ctx);
+
+// Given a path of the form "a/b/c", this function will set *prefix to "a",
+// and rem to "b/c", and return true.
+// If there is no '/' in the path, set prefix to path, and rem to "", and
+// return false.
+bool m_property_split_path(const char *path, bstr *prefix, char **rem);
+
+// Print a list of properties.
+void m_properties_print_help_list(struct mp_log *log,
+ const struct m_property *list);
+
+// Expand a property string.
+// This function allows to print strings containing property values.
+// ${NAME} is expanded to the value of property NAME.
+// If NAME starts with '=', use the raw value of the property.
+// ${NAME:STR} expands to the property, or STR if the property is not
+// available.
+// ${?NAME:STR} expands to STR if the property is available.
+// ${!NAME:STR} expands to STR if the property is not available.
+// General syntax: "${" ["?" | "!"] ["="] NAME ":" STR "}"
+// STR is recursively expanded using the same rules.
+// "$$" can be used to escape "$", and "$}" to escape "}".
+// "$>" disables parsing of "$" for the rest of the string.
+char* m_properties_expand_string(const struct m_property *prop_list,
+ const char *str, void *ctx);
+
+// Trivial helpers for implementing properties.
+int m_property_bool_ro(int action, void* arg, bool var);
+int m_property_int_ro(int action, void* arg, int var);
+int m_property_int64_ro(int action, void* arg, int64_t var);
+int m_property_float_ro(int action, void* arg, float var);
+int m_property_double_ro(int action, void* arg, double var);
+int m_property_strdup_ro(int action, void* arg, const char *var);
+
+struct m_sub_property {
+ // Name of the sub-property - this will be prefixed with the parent
+ // property's name.
+ const char *name;
+ // Type of the data stored in the value member. See m_option.
+ struct m_option type;
+ // Data returned by the sub-property. m_property_read_sub() will make a
+ // copy of this if needed. It will never write or free the data.
+ union m_option_value value;
+ // This can be set to true if the property should be hidden.
+ bool unavailable;
+};
+
+// Convenience macros which can be used as part of a sub_property entry.
+#define SUB_PROP_INT(i) \
+ .type = {.type = CONF_TYPE_INT}, .value = {.int_ = (i)}
+#define SUB_PROP_INT64(i) \
+ .type = {.type = CONF_TYPE_INT64}, .value = {.int64 = (i)}
+#define SUB_PROP_STR(s) \
+ .type = {.type = CONF_TYPE_STRING}, .value = {.string = (char *)(s)}
+#define SUB_PROP_FLOAT(f) \
+ .type = {.type = CONF_TYPE_FLOAT}, .value = {.float_ = (f)}
+#define SUB_PROP_DOUBLE(f) \
+ .type = {.type = CONF_TYPE_DOUBLE}, .value = {.double_ = (f)}
+#define SUB_PROP_BOOL(f) \
+ .type = {.type = CONF_TYPE_BOOL}, .value = {.bool_ = (f)}
+#define SUB_PROP_PTS(f) \
+ .type = {.type = &m_option_type_time}, .value = {.double_ = (f)}
+
+int m_property_read_sub_validate(void *ctx, struct m_property *prop,
+ int action, void *arg);
+int m_property_read_sub(const struct m_sub_property *props, int action, void *arg);
+
+
+// Used with m_property_read_list().
+// Get an entry. item is the 0-based index of the item. This behaves like a
+// top-level property request (but you must implement M_PROPERTY_GET_TYPE).
+// item will be in range [0, count), for count see m_property_read_list()
+// action, arg are for property access.
+// ctx is userdata passed to m_property_read_list.
+typedef int (*m_get_item_cb)(int item, int action, void *arg, void *ctx);
+
+int m_property_read_list(int action, void *arg, int count,
+ m_get_item_cb get_item, void *ctx);
+
+#endif /* MPLAYER_M_PROPERTY_H */
diff --git a/options/options.c b/options/options.c
new file mode 100644
index 0000000..7c6ffa5
--- /dev/null
+++ b/options/options.c
@@ -0,0 +1,1097 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_CFG_MPLAYER_H
+#define MPLAYER_CFG_MPLAYER_H
+
+/*
+ * config for cfgparser
+ */
+
+#include <float.h>
+#include <stddef.h>
+#include <sys/types.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#include <dwmapi.h>
+#endif
+
+#include "options.h"
+#include "m_config.h"
+#include "m_option.h"
+#include "common/common.h"
+#include "input/event.h"
+#include "stream/stream.h"
+#include "video/csputils.h"
+#include "video/hwdec.h"
+#include "video/image_writer.h"
+#include "sub/osd.h"
+#include "player/core.h"
+#include "player/command.h"
+#include "stream/stream.h"
+#include "demux/demux.h"
+
+static void print_version(struct mp_log *log)
+{
+ mp_print_version(log, true);
+}
+
+extern const struct m_sub_options tv_params_conf;
+extern const struct m_sub_options stream_bluray_conf;
+extern const struct m_sub_options stream_cdda_conf;
+extern const struct m_sub_options stream_dvb_conf;
+extern const struct m_sub_options stream_lavf_conf;
+extern const struct m_sub_options sws_conf;
+extern const struct m_sub_options zimg_conf;
+extern const struct m_sub_options drm_conf;
+extern const struct m_sub_options demux_rawaudio_conf;
+extern const struct m_sub_options demux_rawvideo_conf;
+extern const struct m_sub_options demux_playlist_conf;
+extern const struct m_sub_options demux_lavf_conf;
+extern const struct m_sub_options demux_mkv_conf;
+extern const struct m_sub_options demux_cue_conf;
+extern const struct m_sub_options vd_lavc_conf;
+extern const struct m_sub_options ad_lavc_conf;
+extern const struct m_sub_options input_config;
+extern const struct m_sub_options encode_config;
+extern const struct m_sub_options ra_ctx_conf;
+extern const struct m_sub_options gl_video_conf;
+extern const struct m_sub_options ao_alsa_conf;
+
+extern const struct m_sub_options demux_conf;
+extern const struct m_sub_options demux_cache_conf;
+
+extern const struct m_obj_list vf_obj_list;
+extern const struct m_obj_list af_obj_list;
+extern const struct m_obj_list vo_obj_list;
+
+extern const struct m_sub_options ao_conf;
+
+extern const struct m_sub_options opengl_conf;
+extern const struct m_sub_options vulkan_conf;
+extern const struct m_sub_options vulkan_display_conf;
+extern const struct m_sub_options spirv_conf;
+extern const struct m_sub_options d3d11_conf;
+extern const struct m_sub_options d3d11va_conf;
+extern const struct m_sub_options angle_conf;
+extern const struct m_sub_options macos_conf;
+extern const struct m_sub_options wayland_conf;
+extern const struct m_sub_options wingl_conf;
+extern const struct m_sub_options vaapi_conf;
+
+static const struct m_sub_options screenshot_conf = {
+ .opts = image_writer_opts,
+ .size = sizeof(struct image_writer_opts),
+ .defaults = &image_writer_opts_defaults,
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct mp_vo_opts
+
+static const m_option_t mp_vo_opt_list[] = {
+ {"vo", OPT_SETTINGSLIST(video_driver_list, &vo_obj_list)},
+ {"taskbar-progress", OPT_BOOL(taskbar_progress)},
+ {"drag-and-drop", OPT_CHOICE(drag_and_drop, {"no", -2}, {"auto", -1},
+ {"replace", DND_REPLACE}, {"append", DND_APPEND})},
+ {"snap-window", OPT_BOOL(snap_window)},
+ {"ontop", OPT_BOOL(ontop)},
+ {"ontop-level", OPT_CHOICE(ontop_level, {"window", -1}, {"system", -2},
+ {"desktop", -3}), M_RANGE(0, INT_MAX)},
+ {"border", OPT_BOOL(border)},
+ {"title-bar", OPT_BOOL(title_bar)},
+ {"on-all-workspaces", OPT_BOOL(all_workspaces)},
+ {"geometry", OPT_GEOMETRY(geometry)},
+ {"autofit", OPT_SIZE_BOX(autofit)},
+ {"autofit-larger", OPT_SIZE_BOX(autofit_larger)},
+ {"autofit-smaller", OPT_SIZE_BOX(autofit_smaller)},
+ {"auto-window-resize", OPT_BOOL(auto_window_resize)},
+ {"window-scale", OPT_DOUBLE(window_scale), M_RANGE(0.001, 100)},
+ {"window-minimized", OPT_BOOL(window_minimized)},
+ {"window-maximized", OPT_BOOL(window_maximized)},
+ {"focus-on-open", OPT_BOOL(focus_on_open)},
+ {"force-render", OPT_BOOL(force_render)},
+ {"force-window-position", OPT_BOOL(force_window_position)},
+ {"x11-name", OPT_STRING(winname)},
+ {"wayland-app-id", OPT_STRING(appid)},
+ {"monitoraspect", OPT_FLOAT(force_monitor_aspect), M_RANGE(0.0, 9.0)},
+ {"monitorpixelaspect", OPT_FLOAT(monitor_pixel_aspect),
+ M_RANGE(1.0/32.0, 32.0)},
+ {"fullscreen", OPT_BOOL(fullscreen)},
+ {"fs", OPT_ALIAS("fullscreen")},
+ {"input-cursor-passthrough", OPT_BOOL(cursor_passthrough)},
+ {"native-keyrepeat", OPT_BOOL(native_keyrepeat)},
+ {"panscan", OPT_FLOAT(panscan), M_RANGE(0.0, 1.0)},
+ {"video-zoom", OPT_FLOAT(zoom), M_RANGE(-20.0, 20.0)},
+ {"video-pan-x", OPT_FLOAT(pan_x)},
+ {"video-pan-y", OPT_FLOAT(pan_y)},
+ {"video-align-x", OPT_FLOAT(align_x), M_RANGE(-1.0, 1.0)},
+ {"video-align-y", OPT_FLOAT(align_y), M_RANGE(-1.0, 1.0)},
+ {"video-scale-x", OPT_FLOAT(scale_x), M_RANGE(0, 10000.0)},
+ {"video-scale-y", OPT_FLOAT(scale_y), M_RANGE(0, 10000.0)},
+ {"video-margin-ratio-left", OPT_FLOAT(margin_x[0]), M_RANGE(0.0, 1.0)},
+ {"video-margin-ratio-right", OPT_FLOAT(margin_x[1]), M_RANGE(0.0, 1.0)},
+ {"video-margin-ratio-top", OPT_FLOAT(margin_y[0]), M_RANGE(0.0, 1.0)},
+ {"video-margin-ratio-bottom", OPT_FLOAT(margin_y[1]), M_RANGE(0.0, 1.0)},
+ {"video-crop", OPT_RECT(video_crop), .flags = UPDATE_IMGPAR},
+ {"video-unscaled", OPT_CHOICE(unscaled,
+ {"no", 0}, {"yes", 1}, {"downscale-big", 2})},
+ {"wid", OPT_INT64(WinID)},
+ {"screen", OPT_CHOICE(screen_id, {"default", -1}), M_RANGE(0, 32)},
+ {"screen-name", OPT_STRING(screen_name)},
+ {"fs-screen", OPT_CHOICE(fsscreen_id, {"all", -2}, {"current", -1}),
+ M_RANGE(0, 32)},
+ {"fs-screen-name", OPT_STRING(fsscreen_name)},
+ {"keepaspect", OPT_BOOL(keepaspect)},
+ {"keepaspect-window", OPT_BOOL(keepaspect_window)},
+ {"hidpi-window-scale", OPT_BOOL(hidpi_window_scale)},
+ {"native-fs", OPT_BOOL(native_fs)},
+ {"display-fps-override", OPT_DOUBLE(display_fps_override),
+ M_RANGE(0, DBL_MAX)},
+ {"video-timing-offset", OPT_DOUBLE(timing_offset), M_RANGE(0.0, 1.0)},
+ {"video-sync", OPT_CHOICE(video_sync,
+ {"audio", VS_DEFAULT},
+ {"display-resample", VS_DISP_RESAMPLE},
+ {"display-resample-vdrop", VS_DISP_RESAMPLE_VDROP},
+ {"display-resample-desync", VS_DISP_RESAMPLE_NONE},
+ {"display-tempo", VS_DISP_TEMPO},
+ {"display-adrop", VS_DISP_ADROP},
+ {"display-vdrop", VS_DISP_VDROP},
+ {"display-desync", VS_DISP_NONE},
+ {"desync", VS_NONE})},
+#if HAVE_X11
+ {"x11-netwm", OPT_CHOICE(x11_netwm, {"auto", 0}, {"no", -1}, {"yes", 1})},
+ {"x11-bypass-compositor", OPT_CHOICE(x11_bypass_compositor,
+ {"no", 0}, {"yes", 1}, {"fs-only", 2}, {"never", 3})},
+ {"x11-present", OPT_CHOICE(x11_present,
+ {"no", 0}, {"auto", 1}, {"yes", 2})},
+ {"x11-wid-title", OPT_BOOL(x11_wid_title)},
+#endif
+#if HAVE_WAYLAND
+ {"wayland-content-type", OPT_CHOICE(content_type, {"auto", -1}, {"none", 0},
+ {"photo", 1}, {"video", 2}, {"game", 3})},
+#endif
+#if HAVE_WIN32_DESKTOP
+// For old MinGW-w64 compatibility
+#define DWMWCP_DEFAULT 0
+#define DWMWCP_DONOTROUND 1
+#define DWMWCP_ROUND 2
+#define DWMWCP_ROUNDSMALL 3
+
+#define DWMSBT_AUTO 0
+#define DWMSBT_NONE 1
+#define DWMSBT_MAINWINDOW 2
+#define DWMSBT_TRANSIENTWINDOW 3
+#define DWMSBT_TABBEDWINDOW 4
+
+ {"backdrop-type", OPT_CHOICE(backdrop_type, {"auto", DWMSBT_AUTO}, {"none", DWMSBT_NONE},
+ {"mica", DWMSBT_MAINWINDOW}, {"acrylic", DWMSBT_TRANSIENTWINDOW}, {"mica-alt", DWMSBT_TABBEDWINDOW})},
+ {"window-affinity", OPT_CHOICE(window_affinity, {"default", WDA_NONE},
+ {"excludefromcapture", WDA_EXCLUDEFROMCAPTURE}, {"monitor", WDA_MONITOR})},
+ {"vo-mmcss-profile", OPT_STRING(mmcss_profile)},
+ {"window-corners", OPT_CHOICE(window_corners,
+ {"default", DWMWCP_DEFAULT},
+ {"donotround", DWMWCP_DONOTROUND},
+ {"round", DWMWCP_ROUND},
+ {"roundsmall", DWMWCP_ROUNDSMALL})},
+#endif
+#if HAVE_EGL_ANDROID
+ {"android-surface-size", OPT_SIZE_BOX(android_surface_size)},
+#endif
+ {"swapchain-depth", OPT_INT(swapchain_depth), M_RANGE(1, VO_MAX_SWAPCHAIN_DEPTH)},
+ {"override-display-fps", OPT_REPLACED("display-fps-override")},
+ {0}
+};
+
+const struct m_sub_options vo_sub_opts = {
+ .opts = mp_vo_opt_list,
+ .size = sizeof(struct mp_vo_opts),
+ .defaults = &(const struct mp_vo_opts){
+ .video_driver_list = NULL,
+ .drag_and_drop = -1,
+ .monitor_pixel_aspect = 1.0,
+ .screen_id = -1,
+ .fsscreen_id = -1,
+ .panscan = 0.0f,
+ .scale_x = 1.0f,
+ .scale_y = 1.0f,
+ .auto_window_resize = true,
+ .keepaspect = true,
+ .keepaspect_window = true,
+ .hidpi_window_scale = true,
+ .native_fs = true,
+ .taskbar_progress = true,
+ .border = true,
+ .title_bar = true,
+ .appid = "mpv",
+ .content_type = -1,
+ .WinID = -1,
+ .window_scale = 1.0,
+ .x11_bypass_compositor = 2,
+ .x11_present = 1,
+ .mmcss_profile = "Playback",
+ .ontop_level = -1,
+ .timing_offset = 0.050,
+ .swapchain_depth = 3,
+ .focus_on_open = true,
+ },
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct mp_sub_filter_opts
+
+const struct m_sub_options mp_sub_filter_opts = {
+ .opts = (const struct m_option[]){
+ {"sub-filter-sdh", OPT_BOOL(sub_filter_SDH)},
+ {"sub-filter-sdh-harder", OPT_BOOL(sub_filter_SDH_harder)},
+ {"sub-filter-regex-enable", OPT_BOOL(rf_enable)},
+ {"sub-filter-regex-plain", OPT_BOOL(rf_plain)},
+ {"sub-filter-regex", OPT_STRINGLIST(rf_items)},
+ {"sub-filter-jsre", OPT_STRINGLIST(jsre_items)},
+ {"sub-filter-regex-warn", OPT_BOOL(rf_warn)},
+ {0}
+ },
+ .size = sizeof(OPT_BASE_STRUCT),
+ .defaults = &(OPT_BASE_STRUCT){
+ .rf_enable = true,
+ },
+ .change_flags = UPDATE_SUB_FILT,
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct mp_subtitle_opts
+
+const struct m_sub_options mp_subtitle_sub_opts = {
+ .opts = (const struct m_option[]){
+ {"sub-delay", OPT_FLOAT(sub_delay)},
+ {"sub-fps", OPT_FLOAT(sub_fps)},
+ {"sub-speed", OPT_FLOAT(sub_speed)},
+ {"sub-visibility", OPT_BOOL(sub_visibility)},
+ {"secondary-sub-visibility", OPT_BOOL(sec_sub_visibility)},
+ {"sub-forced-events-only", OPT_BOOL(sub_forced_events_only)},
+ {"stretch-dvd-subs", OPT_BOOL(stretch_dvd_subs)},
+ {"stretch-image-subs-to-screen", OPT_BOOL(stretch_image_subs)},
+ {"image-subs-video-resolution", OPT_BOOL(image_subs_video_res)},
+ {"sub-fix-timing", OPT_BOOL(sub_fix_timing)},
+ {"sub-stretch-durations", OPT_BOOL(sub_stretch_durations)},
+ {"sub-pos", OPT_FLOAT(sub_pos), M_RANGE(0.0, 150.0)},
+ {"sub-gauss", OPT_FLOAT(sub_gauss), M_RANGE(0.0, 3.0)},
+ {"sub-gray", OPT_BOOL(sub_gray)},
+ {"sub-ass", OPT_BOOL(ass_enabled), .flags = UPDATE_SUB_HARD},
+ {"sub-scale", OPT_FLOAT(sub_scale), M_RANGE(0, 100)},
+ {"sub-ass-line-spacing", OPT_FLOAT(ass_line_spacing),
+ M_RANGE(-1000, 1000)},
+ {"sub-use-margins", OPT_BOOL(sub_use_margins)},
+ {"sub-ass-force-margins", OPT_BOOL(ass_use_margins)},
+ {"sub-ass-vsfilter-aspect-compat", OPT_BOOL(ass_vsfilter_aspect_compat)},
+ {"sub-ass-vsfilter-color-compat", OPT_CHOICE(ass_vsfilter_color_compat,
+ {"no", 0}, {"basic", 1}, {"full", 2}, {"force-601", 3})},
+ {"sub-ass-vsfilter-blur-compat", OPT_BOOL(ass_vsfilter_blur_compat)},
+ {"embeddedfonts", OPT_BOOL(use_embedded_fonts), .flags = UPDATE_SUB_HARD},
+ {"sub-ass-style-overrides", OPT_STRINGLIST(ass_style_override_list),
+ .flags = UPDATE_SUB_HARD},
+ {"sub-ass-styles", OPT_STRING(ass_styles_file),
+ .flags = M_OPT_FILE | UPDATE_SUB_HARD},
+ {"sub-ass-hinting", OPT_CHOICE(ass_hinting,
+ {"none", 0}, {"light", 1}, {"normal", 2}, {"native", 3})},
+ {"sub-ass-shaper", OPT_CHOICE(ass_shaper,
+ {"simple", 0}, {"complex", 1})},
+ {"sub-ass-justify", OPT_BOOL(ass_justify)},
+ {"sub-ass-override", OPT_CHOICE(ass_style_override,
+ {"no", 0}, {"yes", 1}, {"force", 3}, {"scale", 4}, {"strip", 5}),
+ .flags = UPDATE_SUB_HARD},
+ {"sub-scale-by-window", OPT_BOOL(sub_scale_by_window)},
+ {"sub-scale-with-window", OPT_BOOL(sub_scale_with_window)},
+ {"sub-ass-scale-with-window", OPT_BOOL(ass_scale_with_window)},
+ {"sub", OPT_SUBSTRUCT(sub_style, sub_style_conf)},
+ {"sub-clear-on-seek", OPT_BOOL(sub_clear_on_seek)},
+ {"teletext-page", OPT_INT(teletext_page), M_RANGE(1, 999)},
+ {"sub-past-video-end", OPT_BOOL(sub_past_video_end)},
+ {"sub-ass-force-style", OPT_REPLACED("sub-ass-style-overrides")},
+ {0}
+ },
+ .size = sizeof(OPT_BASE_STRUCT),
+ .defaults = &(OPT_BASE_STRUCT){
+ .sub_visibility = true,
+ .sec_sub_visibility = true,
+ .sub_pos = 100,
+ .sub_speed = 1.0,
+ .ass_enabled = true,
+ .sub_scale_by_window = true,
+ .sub_use_margins = true,
+ .sub_scale_with_window = true,
+ .teletext_page = 100,
+ .sub_scale = 1,
+ .ass_vsfilter_aspect_compat = true,
+ .ass_vsfilter_color_compat = 1,
+ .ass_vsfilter_blur_compat = true,
+ .ass_style_override = 1,
+ .ass_shaper = 1,
+ .use_embedded_fonts = true,
+ },
+ .change_flags = UPDATE_OSD,
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct mp_osd_render_opts
+
+const struct m_sub_options mp_osd_render_sub_opts = {
+ .opts = (const struct m_option[]){
+ {"osd-bar-align-x", OPT_FLOAT(osd_bar_align_x), M_RANGE(-1.0, +1.0)},
+ {"osd-bar-align-y", OPT_FLOAT(osd_bar_align_y), M_RANGE(-1.0, +1.0)},
+ {"osd-bar-w", OPT_FLOAT(osd_bar_w), M_RANGE(1, 100)},
+ {"osd-bar-h", OPT_FLOAT(osd_bar_h), M_RANGE(0.1, 50)},
+ {"osd", OPT_SUBSTRUCT(osd_style, osd_style_conf)},
+ {"osd-scale", OPT_FLOAT(osd_scale), M_RANGE(0, 100)},
+ {"osd-scale-by-window", OPT_BOOL(osd_scale_by_window)},
+ {"force-rgba-osd-rendering", OPT_BOOL(force_rgba_osd)},
+ {0}
+ },
+ .size = sizeof(OPT_BASE_STRUCT),
+ .defaults = &(OPT_BASE_STRUCT){
+ .osd_bar_align_y = 0.5,
+ .osd_bar_w = 75.0,
+ .osd_bar_h = 3.125,
+ .osd_scale = 1,
+ .osd_scale_by_window = true,
+ },
+ .change_flags = UPDATE_OSD,
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct cuda_opts
+
+const struct m_sub_options cuda_conf = {
+ .opts = (const struct m_option[]){
+ {"decode-device", OPT_CHOICE(cuda_device, {"auto", -1}),
+ M_RANGE(0, INT_MAX)},
+ {0}
+ },
+ .size = sizeof(struct cuda_opts),
+ .defaults = &(const struct cuda_opts){
+ .cuda_device = -1,
+ },
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct dvd_opts
+
+const struct m_sub_options dvd_conf = {
+ .opts = (const struct m_option[]){
+ {"dvd-device", OPT_STRING(device), .flags = M_OPT_FILE},
+ {"dvd-speed", OPT_INT(speed)},
+ {"dvd-angle", OPT_INT(angle), M_RANGE(1, 99)},
+ {0}
+ },
+ .size = sizeof(struct dvd_opts),
+ .defaults = &(const struct dvd_opts){
+ .angle = 1,
+ },
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct filter_opts
+
+const struct m_sub_options filter_conf = {
+ .opts = (const struct m_option[]){
+ {"deinterlace", OPT_BOOL(deinterlace)},
+ {0}
+ },
+ .size = sizeof(OPT_BASE_STRUCT),
+ .change_flags = UPDATE_IMGPAR,
+};
+
+#undef OPT_BASE_STRUCT
+#define OPT_BASE_STRUCT struct MPOpts
+
+static const m_option_t mp_opts[] = {
+ // handled in command line pre-parser (parse_commandline.c)
+ {"v", &m_option_type_dummy_flag, CONF_NOCFG | M_OPT_NOPROP,
+ .offset = -1},
+ {"playlist", CONF_TYPE_STRING, CONF_NOCFG | M_OPT_FILE, .offset = -1},
+ {"{", &m_option_type_dummy_flag, CONF_NOCFG | M_OPT_NOPROP,
+ .offset = -1},
+ {"}", &m_option_type_dummy_flag, CONF_NOCFG | M_OPT_NOPROP,
+ .offset = -1},
+
+ // handled in m_config.c
+ { "include", CONF_TYPE_STRING, M_OPT_FILE, .offset = -1},
+ { "profile", CONF_TYPE_STRING_LIST, 0, .offset = -1},
+ { "show-profile", CONF_TYPE_STRING, CONF_NOCFG | M_OPT_NOPROP |
+ M_OPT_OPTIONAL_PARAM, .offset = -1},
+ { "list-options", &m_option_type_dummy_flag, CONF_NOCFG | M_OPT_NOPROP,
+ .offset = -1},
+ {"list-properties", OPT_BOOL(property_print_help),
+ .flags = CONF_NOCFG | M_OPT_NOPROP},
+ { "help", CONF_TYPE_STRING, CONF_NOCFG | M_OPT_NOPROP | M_OPT_OPTIONAL_PARAM,
+ .offset = -1},
+ { "h", CONF_TYPE_STRING, CONF_NOCFG | M_OPT_NOPROP | M_OPT_OPTIONAL_PARAM,
+ .offset = -1},
+
+ {"list-protocols", OPT_PRINT(stream_print_proto_list)},
+ {"version", OPT_PRINT(print_version)},
+ {"V", OPT_PRINT(print_version)},
+
+ {"player-operation-mode", OPT_CHOICE(operation_mode,
+ {"cplayer", 0}, {"pseudo-gui", 1}),
+ .flags = M_OPT_PRE_PARSE | M_OPT_NOPROP},
+
+ {"shuffle", OPT_BOOL(shuffle)},
+
+// ------------------------- common options --------------------
+ {"quiet", OPT_BOOL(quiet)},
+ {"really-quiet", OPT_BOOL(msg_really_quiet),
+ .flags = CONF_PRE_PARSE | UPDATE_TERM},
+ {"terminal", OPT_BOOL(use_terminal), .flags = CONF_PRE_PARSE | UPDATE_TERM},
+ {"msg-level", OPT_MSGLEVELS(msg_levels),
+ .flags = CONF_PRE_PARSE | UPDATE_TERM},
+ {"dump-stats", OPT_STRING(dump_stats),
+ .flags = UPDATE_TERM | CONF_PRE_PARSE | M_OPT_FILE},
+ {"msg-color", OPT_BOOL(msg_color), .flags = CONF_PRE_PARSE | UPDATE_TERM},
+ {"log-file", OPT_STRING(log_file),
+ .flags = CONF_PRE_PARSE | M_OPT_FILE | UPDATE_TERM},
+ {"msg-module", OPT_BOOL(msg_module), .flags = UPDATE_TERM},
+ {"msg-time", OPT_BOOL(msg_time), .flags = UPDATE_TERM},
+#if HAVE_WIN32_DESKTOP
+ {"priority", OPT_CHOICE(w32_priority,
+ {"no", 0},
+ {"realtime", REALTIME_PRIORITY_CLASS},
+ {"high", HIGH_PRIORITY_CLASS},
+ {"abovenormal", ABOVE_NORMAL_PRIORITY_CLASS},
+ {"normal", NORMAL_PRIORITY_CLASS},
+ {"belownormal", BELOW_NORMAL_PRIORITY_CLASS},
+ {"idle", IDLE_PRIORITY_CLASS}),
+ .flags = UPDATE_PRIORITY},
+#endif
+ {"config", OPT_BOOL(load_config), .flags = CONF_PRE_PARSE},
+ {"config-dir", OPT_STRING(force_configdir),
+ .flags = CONF_NOCFG | CONF_PRE_PARSE | M_OPT_FILE},
+ {"reset-on-next-file", OPT_STRINGLIST(reset_options)},
+
+#if HAVE_LUA || HAVE_JAVASCRIPT || HAVE_CPLUGINS
+ {"scripts", OPT_PATHLIST(script_files), .flags = M_OPT_FILE},
+ {"script", OPT_CLI_ALIAS("scripts-append")},
+ {"script-opts", OPT_KEYVALUELIST(script_opts)},
+ {"load-scripts", OPT_BOOL(auto_load_scripts)},
+#endif
+#if HAVE_JAVASCRIPT
+ {"js-memory-report", OPT_BOOL(js_memory_report)},
+#endif
+#if HAVE_LUA
+ {"osc", OPT_BOOL(lua_load_osc), .flags = UPDATE_BUILTIN_SCRIPTS},
+ {"ytdl", OPT_BOOL(lua_load_ytdl), .flags = UPDATE_BUILTIN_SCRIPTS},
+ {"ytdl-format", OPT_STRING(lua_ytdl_format)},
+ {"ytdl-raw-options", OPT_KEYVALUELIST(lua_ytdl_raw_options)},
+ {"load-stats-overlay", OPT_BOOL(lua_load_stats),
+ .flags = UPDATE_BUILTIN_SCRIPTS},
+ {"load-osd-console", OPT_BOOL(lua_load_console),
+ .flags = UPDATE_BUILTIN_SCRIPTS},
+ {"load-auto-profiles",
+ OPT_CHOICE(lua_load_auto_profiles, {"no", 0}, {"yes", 1}, {"auto", -1}),
+ .flags = UPDATE_BUILTIN_SCRIPTS},
+#endif
+
+// ------------------------- stream options --------------------
+
+#if HAVE_DVDNAV
+ {"", OPT_SUBSTRUCT(dvd_opts, dvd_conf)},
+#endif
+ {"edition", OPT_CHOICE(edition_id, {"auto", -1}), M_RANGE(0, 8190)},
+#if HAVE_LIBBLURAY
+ {"bluray", OPT_SUBSTRUCT(stream_bluray_opts, stream_bluray_conf)},
+#endif /* HAVE_LIBBLURAY */
+
+// ------------------------- demuxer options --------------------
+
+ {"frames", OPT_CHOICE(play_frames, {"all", -1}), M_RANGE(0, INT_MAX)},
+
+ {"start", OPT_REL_TIME(play_start)},
+ {"end", OPT_REL_TIME(play_end)},
+ {"length", OPT_REL_TIME(play_length)},
+
+ {"play-direction", OPT_CHOICE(play_dir,
+ {"forward", 1}, {"+", 1}, {"backward", -1}, {"-", -1})},
+
+ {"rebase-start-time", OPT_BOOL(rebase_start_time)},
+
+ {"ab-loop-a", OPT_TIME(ab_loop[0]), .flags = M_OPT_ALLOW_NO},
+ {"ab-loop-b", OPT_TIME(ab_loop[1]), .flags = M_OPT_ALLOW_NO},
+ {"ab-loop-count", OPT_CHOICE(ab_loop_count, {"inf", -1}),
+ M_RANGE(0, INT_MAX)},
+
+ {"playlist-start", OPT_CHOICE(playlist_pos, {"auto", -1}, {"no", -1}),
+ M_RANGE(0, INT_MAX)},
+
+ {"pause", OPT_BOOL(pause)},
+ {"keep-open", OPT_CHOICE(keep_open,
+ {"no", 0},
+ {"yes", 1},
+ {"always", 2})},
+ {"keep-open-pause", OPT_BOOL(keep_open_pause)},
+ {"image-display-duration", OPT_DOUBLE(image_display_duration),
+ M_RANGE(0, INFINITY)},
+
+ // select audio/video/subtitle stream
+ // keep in sync with num_ptracks[] and MAX_PTRACKS
+ {"aid", OPT_TRACKCHOICE(stream_id[0][STREAM_AUDIO])},
+ {"vid", OPT_TRACKCHOICE(stream_id[0][STREAM_VIDEO])},
+ {"sid", OPT_TRACKCHOICE(stream_id[0][STREAM_SUB])},
+ {"secondary-sid", OPT_TRACKCHOICE(stream_id[1][STREAM_SUB])},
+ {"sub", OPT_ALIAS("sid")},
+ {"video", OPT_ALIAS("vid")},
+ {"audio", OPT_ALIAS("aid")},
+ {"alang", OPT_STRINGLIST(stream_lang[STREAM_AUDIO])},
+ {"slang", OPT_STRINGLIST(stream_lang[STREAM_SUB])},
+ {"vlang", OPT_STRINGLIST(stream_lang[STREAM_VIDEO])},
+ {"track-auto-selection", OPT_BOOL(stream_auto_sel)},
+ {"subs-with-matching-audio", OPT_BOOL(subs_with_matching_audio)},
+ {"subs-match-os-language", OPT_BOOL(subs_match_os_language)},
+ {"subs-fallback", OPT_CHOICE(subs_fallback, {"no", 0}, {"default", 1}, {"yes", 2})},
+ {"subs-fallback-forced", OPT_CHOICE(subs_fallback_forced, {"no", 0},
+ {"yes", 1}, {"always", 2})},
+
+ {"lavfi-complex", OPT_STRING(lavfi_complex), .flags = UPDATE_LAVFI_COMPLEX},
+
+ {"audio-display", OPT_CHOICE(audio_display, {"no", 0},
+ {"embedded-first", 1}, {"external-first", 2})},
+
+ {"hls-bitrate", OPT_CHOICE(hls_bitrate,
+ {"no", -1}, {"min", 0}, {"max", INT_MAX}), M_RANGE(0, INT_MAX)},
+
+ {"display-tags", OPT_STRINGLIST(display_tags)},
+
+#if HAVE_CDDA
+ {"cdda", OPT_SUBSTRUCT(stream_cdda_opts, stream_cdda_conf)},
+ {"cdrom-device", OPT_REPLACED("cdda-device")},
+#endif
+
+ // demuxer.c - select audio/sub file/demuxer
+ {"demuxer", OPT_STRING(demuxer_name), .help = demuxer_help},
+ {"audio-demuxer", OPT_STRING(audio_demuxer_name), .help = demuxer_help},
+ {"sub-demuxer", OPT_STRING(sub_demuxer_name), .help = demuxer_help},
+ {"demuxer-thread", OPT_BOOL(demuxer_thread)},
+ {"demuxer-termination-timeout", OPT_DOUBLE(demux_termination_timeout)},
+ {"demuxer-cache-wait", OPT_BOOL(demuxer_cache_wait)},
+ {"prefetch-playlist", OPT_BOOL(prefetch_open)},
+ {"cache-pause", OPT_BOOL(cache_pause)},
+ {"cache-pause-initial", OPT_BOOL(cache_pause_initial)},
+ {"cache-pause-wait", OPT_FLOAT(cache_pause_wait), M_RANGE(0, DBL_MAX)},
+
+#if HAVE_DVBIN
+ {"dvbin", OPT_SUBSTRUCT(stream_dvb_opts, stream_dvb_conf)},
+#endif
+ {"", OPT_SUBSTRUCT(stream_lavf_opts, stream_lavf_conf)},
+
+// ------------------------- a-v sync options --------------------
+
+ // set A-V sync correction speed (0=disables it):
+ {"mc", OPT_FLOAT(default_max_pts_correction), M_RANGE(0, 100)},
+
+ {"audio-samplerate", OPT_INT(force_srate), .flags = UPDATE_AUDIO,
+ M_RANGE(0, 16*48000)},
+ {"audio-channels", OPT_CHANNELS(audio_output_channels), .flags = UPDATE_AUDIO},
+ {"audio-format", OPT_AUDIOFORMAT(audio_output_format), .flags = UPDATE_AUDIO},
+ {"speed", OPT_DOUBLE(playback_speed), M_RANGE(0.01, 100.0)},
+
+ {"audio-pitch-correction", OPT_BOOL(pitch_correction)},
+
+ // set a-v distance
+ {"audio-delay", OPT_FLOAT(audio_delay)},
+
+// ------------------------- codec/vfilter options --------------------
+
+ {"af", OPT_SETTINGSLIST(af_settings, &af_obj_list)},
+ {"vf", OPT_SETTINGSLIST(vf_settings, &vf_obj_list)},
+
+ {"", OPT_SUBSTRUCT(filter_opts, filter_conf)},
+
+ {"", OPT_SUBSTRUCT(dec_wrapper, dec_wrapper_conf)},
+ {"", OPT_SUBSTRUCT(vd_lavc_params, vd_lavc_conf)},
+ {"ad-lavc", OPT_SUBSTRUCT(ad_lavc_params, ad_lavc_conf)},
+
+ {"", OPT_SUBSTRUCT(demux_lavf, demux_lavf_conf)},
+ {"demuxer-rawaudio", OPT_SUBSTRUCT(demux_rawaudio, demux_rawaudio_conf)},
+ {"demuxer-rawvideo", OPT_SUBSTRUCT(demux_rawvideo, demux_rawvideo_conf)},
+ {"", OPT_SUBSTRUCT(demux_playlist, demux_playlist_conf)},
+ {"demuxer-mkv", OPT_SUBSTRUCT(demux_mkv, demux_mkv_conf)},
+ {"demuxer-cue", OPT_SUBSTRUCT(demux_cue, demux_cue_conf)},
+
+// ------------------------- subtitles options --------------------
+
+ {"sub-files", OPT_PATHLIST(sub_name), .flags = M_OPT_FILE},
+ {"sub-file", OPT_CLI_ALIAS("sub-files-append")},
+ {"audio-files", OPT_PATHLIST(audio_files), .flags = M_OPT_FILE},
+ {"audio-file", OPT_CLI_ALIAS("audio-files-append")},
+ {"cover-art-files", OPT_PATHLIST(coverart_files), .flags = M_OPT_FILE},
+ {"cover-art-file", OPT_CLI_ALIAS("cover-art-files-append")},
+
+ {"sub-file-paths", OPT_PATHLIST(sub_paths), .flags = M_OPT_FILE},
+ {"audio-file-paths", OPT_PATHLIST(audiofile_paths), .flags = M_OPT_FILE},
+
+ {"external-files", OPT_PATHLIST(external_files), .flags = M_OPT_FILE},
+ {"external-file", OPT_CLI_ALIAS("external-files-append")},
+ {"autoload-files", OPT_BOOL(autoload_files)},
+
+ {"sub-auto", OPT_CHOICE(sub_auto,
+ {"no", -1}, {"exact", 0}, {"fuzzy", 1}, {"all", 2})},
+ {"sub-auto-exts", OPT_STRINGLIST(sub_auto_exts), .flags = UPDATE_SUB_EXTS},
+ {"audio-file-auto", OPT_CHOICE(audiofile_auto,
+ {"no", -1}, {"exact", 0}, {"fuzzy", 1}, {"all", 2})},
+ {"audio-file-auto-exts", OPT_STRINGLIST(audiofile_auto_exts)},
+ {"cover-art-auto", OPT_CHOICE(coverart_auto,
+ {"no", -1}, {"exact", 0}, {"fuzzy", 1}, {"all", 2})},
+ {"cover-art-auto-exts", OPT_STRINGLIST(coverart_auto_exts)},
+ {"cover-art-whitelist", OPT_BOOL(coverart_whitelist)},
+
+ {"", OPT_SUBSTRUCT(subs_rend, mp_subtitle_sub_opts)},
+ {"", OPT_SUBSTRUCT(subs_filt, mp_sub_filter_opts)},
+ {"", OPT_SUBSTRUCT(osd_rend, mp_osd_render_sub_opts)},
+
+ {"osd-bar", OPT_BOOL(osd_bar_visible), .flags = UPDATE_OSD},
+
+//---------------------- libao/libvo options ------------------------
+ {"", OPT_SUBSTRUCT(ao_opts, ao_conf)},
+ {"audio-exclusive", OPT_BOOL(audio_exclusive), .flags = UPDATE_AUDIO},
+ {"audio-fallback-to-null", OPT_BOOL(ao_null_fallback)},
+ {"audio-stream-silence", OPT_BOOL(audio_stream_silence)},
+ {"audio-wait-open", OPT_FLOAT(audio_wait_open), M_RANGE(0, 60)},
+ {"force-window", OPT_CHOICE(force_vo,
+ {"no", 0}, {"yes", 1}, {"immediate", 2})},
+
+ {"volume-max", OPT_FLOAT(softvol_max), M_RANGE(100, 1000)},
+ // values <0 for volume and mute are legacy and ignored
+ {"volume", OPT_FLOAT(softvol_volume), .flags = UPDATE_VOL,
+ M_RANGE(-1, 1000)},
+ {"mute", OPT_CHOICE(softvol_mute,
+ {"no", 0},
+ {"auto", 0},
+ {"yes", 1}),
+ .flags = UPDATE_VOL},
+ {"replaygain", OPT_CHOICE(rgain_mode,
+ {"no", 0},
+ {"track", 1},
+ {"album", 2}),
+ .flags = UPDATE_VOL},
+ {"replaygain-preamp", OPT_FLOAT(rgain_preamp), .flags = UPDATE_VOL,
+ M_RANGE(-150, 150)},
+ {"replaygain-clip", OPT_BOOL(rgain_clip), .flags = UPDATE_VOL},
+ {"replaygain-fallback", OPT_FLOAT(rgain_fallback), .flags = UPDATE_VOL,
+ M_RANGE(-200, 60)},
+ {"gapless-audio", OPT_CHOICE(gapless_audio,
+ {"no", 0},
+ {"yes", 1},
+ {"weak", -1})},
+
+ {"title", OPT_STRING(wintitle)},
+ {"force-media-title", OPT_STRING(media_title)},
+
+ {"cursor-autohide", OPT_CHOICE(cursor_autohide_delay,
+ {"no", -1}, {"always", -2}), M_RANGE(0, 30000)},
+ {"cursor-autohide-fs-only", OPT_BOOL(cursor_autohide_fs)},
+ {"stop-screensaver", OPT_CHOICE(stop_screensaver,
+ {"no", 0},
+ {"yes", 1},
+ {"always", 2}),
+ .flags = UPDATE_SCREENSAVER},
+
+ {"", OPT_SUBSTRUCT(video_equalizer, mp_csp_equalizer_conf)},
+
+ {"use-filedir-conf", OPT_BOOL(use_filedir_conf)},
+ {"osd-level", OPT_CHOICE(osd_level,
+ {"0", 0}, {"1", 1}, {"2", 2}, {"3", 3})},
+ {"osd-on-seek", OPT_CHOICE(osd_on_seek,
+ {"no", 0},
+ {"bar", 1},
+ {"msg", 2},
+ {"msg-bar", 3})},
+ {"osd-duration", OPT_INT(osd_duration), M_RANGE(0, 3600000)},
+ {"osd-fractions", OPT_BOOL(osd_fractions)},
+
+ {"sstep", OPT_DOUBLE(step_sec), M_RANGE(0, DBL_MAX)},
+
+ {"framedrop", OPT_CHOICE(frame_dropping,
+ {"no", 0},
+ {"vo", 1},
+ {"decoder", 2},
+ {"decoder+vo", 3})},
+ {"video-latency-hacks", OPT_BOOL(video_latency_hacks)},
+
+ {"untimed", OPT_BOOL(untimed)},
+
+ {"stream-dump", OPT_STRING(stream_dump), .flags = M_OPT_FILE},
+
+ {"stop-playback-on-init-failure", OPT_BOOL(stop_playback_on_init_failure)},
+
+ {"loop-playlist", OPT_CHOICE(loop_times,
+ {"no", 1},
+ {"inf", -1}, {"yes", -1},
+ {"force", -2}),
+ M_RANGE(1, 10000)},
+ {"loop-file", OPT_CHOICE(loop_file,
+ {"no", 0},
+ {"inf", -1},
+ {"yes", -1}),
+ M_RANGE(0, 10000)},
+ {"loop", OPT_ALIAS("loop-file")},
+
+ {"resume-playback", OPT_BOOL(position_resume)},
+ {"resume-playback-check-mtime", OPT_BOOL(position_check_mtime)},
+ {"save-position-on-quit", OPT_BOOL(position_save_on_quit)},
+ {"write-filename-in-watch-later-config",
+ OPT_BOOL(write_filename_in_watch_later_config)},
+ {"ignore-path-in-watch-later-config",
+ OPT_BOOL(ignore_path_in_watch_later_config)},
+ {"watch-later-dir", OPT_STRING(watch_later_dir),
+ .flags = M_OPT_FILE},
+ {"watch-later-directory", OPT_ALIAS("watch-later-dir")},
+ {"watch-later-options", OPT_STRINGLIST(watch_later_options)},
+
+ {"ordered-chapters", OPT_BOOL(ordered_chapters)},
+ {"ordered-chapters-files", OPT_STRING(ordered_chapters_files),
+ .flags = M_OPT_FILE},
+ {"chapter-merge-threshold", OPT_INT(chapter_merge_threshold),
+ M_RANGE(0, 10000)},
+
+ {"chapter-seek-threshold", OPT_DOUBLE(chapter_seek_threshold)},
+
+ {"chapters-file", OPT_STRING(chapter_file), .flags = M_OPT_FILE},
+
+ {"merge-files", OPT_BOOL(merge_files)},
+
+ // a-v sync stuff:
+ {"initial-audio-sync", OPT_BOOL(initial_audio_sync)},
+ {"video-sync-max-video-change", OPT_DOUBLE(sync_max_video_change),
+ M_RANGE(0, DBL_MAX)},
+ {"video-sync-max-audio-change", OPT_DOUBLE(sync_max_audio_change),
+ M_RANGE(0, 1)},
+ {"video-sync-max-factor", OPT_INT(sync_max_factor), M_RANGE(1, 10)},
+ {"hr-seek", OPT_CHOICE(hr_seek,
+ {"no", -1}, {"absolute", 0}, {"yes", 1}, {"always", 1}, {"default", 2})},
+ {"hr-seek-demuxer-offset", OPT_FLOAT(hr_seek_demuxer_offset)},
+ {"hr-seek-framedrop", OPT_BOOL(hr_seek_framedrop)},
+ {"autosync", OPT_CHOICE(autosync, {"no", -1}), M_RANGE(0, 10000)},
+
+ {"term-osd", OPT_CHOICE(term_osd,
+ {"force", 1}, {"auto", 2}, {"no", 0}), .flags = UPDATE_OSD},
+
+ {"term-osd-bar", OPT_BOOL(term_osd_bar), .flags = UPDATE_OSD},
+ {"term-osd-bar-chars", OPT_STRING(term_osd_bar_chars), .flags = UPDATE_OSD},
+ {"term-remaining-playtime", OPT_BOOL(term_remaining_playtime), .flags = UPDATE_OSD},
+ {"term-title", OPT_STRING(term_title), .flags = UPDATE_OSD},
+
+ {"term-playing-msg", OPT_STRING(playing_msg)},
+ {"osd-playing-msg", OPT_STRING(osd_playing_msg)},
+ {"osd-playing-msg-duration", OPT_INT(osd_playing_msg_duration),
+ M_RANGE(0, 3600000)},
+ {"term-status-msg", OPT_STRING(status_msg), .flags = UPDATE_OSD},
+ {"osd-status-msg", OPT_STRING(osd_status_msg), .flags = UPDATE_OSD},
+ {"osd-msg1", OPT_STRING(osd_msg[0]), .flags = UPDATE_OSD},
+ {"osd-msg2", OPT_STRING(osd_msg[1]), .flags = UPDATE_OSD},
+ {"osd-msg3", OPT_STRING(osd_msg[2]), .flags = UPDATE_OSD},
+
+ {"video-osd", OPT_BOOL(video_osd), .flags = UPDATE_OSD},
+
+ {"idle", OPT_CHOICE(player_idle_mode,
+ {"no", 0}, {"once", 1}, {"yes", 2})},
+
+ {"input-terminal", OPT_BOOL(consolecontrols), .flags = UPDATE_TERM},
+
+ {"input-ipc-server", OPT_STRING(ipc_path), .flags = M_OPT_FILE},
+#if HAVE_POSIX
+ {"input-ipc-client", OPT_STRING(ipc_client)},
+#endif
+
+ {"screenshot", OPT_SUBSTRUCT(screenshot_image_opts, screenshot_conf)},
+ {"screenshot-template", OPT_STRING(screenshot_template)},
+ {"screenshot-dir", OPT_STRING(screenshot_dir),
+ .flags = M_OPT_FILE},
+ {"screenshot-directory", OPT_ALIAS("screenshot-dir")},
+ {"screenshot-sw", OPT_BOOL(screenshot_sw)},
+
+ {"", OPT_SUBSTRUCT(resample_opts, resample_conf)},
+
+ {"", OPT_SUBSTRUCT(input_opts, input_config)},
+
+ {"", OPT_SUBSTRUCT(vo, vo_sub_opts)},
+ {"", OPT_SUBSTRUCT(demux_opts, demux_conf)},
+ {"", OPT_SUBSTRUCT(demux_cache_opts, demux_cache_conf)},
+ {"", OPT_SUBSTRUCT(stream_opts, stream_conf)},
+
+ {"", OPT_SUBSTRUCT(ra_ctx_opts, ra_ctx_conf)},
+ {"", OPT_SUBSTRUCT(gl_video_opts, gl_video_conf)},
+ {"", OPT_SUBSTRUCT(spirv_opts, spirv_conf)},
+
+#if HAVE_GL
+ {"", OPT_SUBSTRUCT(opengl_opts, opengl_conf)},
+#endif
+
+#if HAVE_VULKAN
+ {"", OPT_SUBSTRUCT(vulkan_opts, vulkan_conf)},
+#if HAVE_VK_KHR_DISPLAY
+ {"", OPT_SUBSTRUCT(vulkan_display_opts, vulkan_display_conf)},
+#endif
+#endif
+
+#if HAVE_D3D11
+ {"", OPT_SUBSTRUCT(d3d11_opts, d3d11_conf)},
+#if HAVE_D3D_HWACCEL
+ {"", OPT_SUBSTRUCT(d3d11va_opts, d3d11va_conf)},
+#endif
+#endif
+
+#if HAVE_EGL_ANGLE_WIN32
+ {"", OPT_SUBSTRUCT(angle_opts, angle_conf)},
+#endif
+
+#if HAVE_COCOA
+ {"", OPT_SUBSTRUCT(macos_opts, macos_conf)},
+#endif
+
+#if HAVE_DRM
+ {"", OPT_SUBSTRUCT(drm_opts, drm_conf)},
+#endif
+
+#if HAVE_WAYLAND
+ {"", OPT_SUBSTRUCT(wayland_opts, wayland_conf)},
+#endif
+
+#if HAVE_GL_WIN32
+ {"", OPT_SUBSTRUCT(wingl_opts, wingl_conf)},
+#endif
+
+#if HAVE_CUDA_HWACCEL
+ {"cuda", OPT_SUBSTRUCT(cuda_opts, cuda_conf)},
+#endif
+
+#if HAVE_VAAPI
+ {"vaapi", OPT_SUBSTRUCT(vaapi_opts, vaapi_conf)},
+#endif
+
+ {"sws", OPT_SUBSTRUCT(sws_opts, sws_conf)},
+
+#if HAVE_ZIMG
+ {"zimg", OPT_SUBSTRUCT(zimg_opts, zimg_conf)},
+#endif
+
+ {"", OPT_SUBSTRUCT(encode_opts, encode_config)},
+
+ {"play-dir", OPT_REPLACED("play-direction")},
+ {"sub-forced-only", OPT_REPLACED("sub-forced-events-only")},
+ {0}
+};
+
+static const struct MPOpts mp_default_opts = {
+ .use_terminal = true,
+ .msg_color = true,
+ .softvol_max = 130,
+ .softvol_volume = 100,
+ .gapless_audio = -1,
+ .wintitle = "${?media-title:${media-title}}${!media-title:No file} - mpv",
+ .stop_screensaver = 1,
+ .cursor_autohide_delay = 1000,
+ .video_osd = true,
+ .osd_level = 1,
+ .osd_on_seek = 1,
+ .osd_duration = 1000,
+#if HAVE_LUA
+ .lua_load_osc = true,
+ .lua_load_ytdl = true,
+ .lua_ytdl_format = NULL,
+ .lua_ytdl_raw_options = NULL,
+ .lua_load_stats = true,
+ .lua_load_console = true,
+ .lua_load_auto_profiles = -1,
+#endif
+ .auto_load_scripts = true,
+ .loop_times = 1,
+ .ordered_chapters = true,
+ .chapter_merge_threshold = 100,
+ .chapter_seek_threshold = 5.0,
+ .hr_seek = 2,
+ .hr_seek_framedrop = true,
+ .sync_max_video_change = 1,
+ .sync_max_audio_change = 0.125,
+ .sync_max_factor = 5,
+ .load_config = true,
+ .position_resume = true,
+ .autoload_files = true,
+ .demuxer_thread = true,
+ .demux_termination_timeout = 0.1,
+ .hls_bitrate = INT_MAX,
+ .cache_pause = true,
+ .cache_pause_wait = 1.0,
+ .ab_loop = {MP_NOPTS_VALUE, MP_NOPTS_VALUE},
+ .ab_loop_count = -1,
+ .edition_id = -1,
+ .default_max_pts_correction = -1,
+ .initial_audio_sync = true,
+ .frame_dropping = 1,
+ .term_osd = 2,
+ .term_osd_bar_chars = "[-+-]",
+ .term_remaining_playtime = true,
+ .consolecontrols = true,
+ .playlist_pos = -1,
+ .play_frames = -1,
+ .rebase_start_time = true,
+ .keep_open_pause = true,
+ .image_display_duration = 1.0,
+ .stream_id = { { [STREAM_AUDIO] = -1,
+ [STREAM_VIDEO] = -1,
+ [STREAM_SUB] = -1, },
+ { [STREAM_AUDIO] = -2,
+ [STREAM_VIDEO] = -2,
+ [STREAM_SUB] = -2, }, },
+ .stream_auto_sel = true,
+ .subs_with_matching_audio = true,
+ .subs_match_os_language = true,
+ .subs_fallback = 1,
+ .subs_fallback_forced = 1,
+ .audio_display = 1,
+ .audio_output_format = 0, // AF_FORMAT_UNKNOWN
+ .playback_speed = 1.,
+ .pitch_correction = true,
+ .audiofile_auto = -1,
+ .coverart_whitelist = true,
+ .osd_bar_visible = true,
+ .screenshot_template = "mpv-shot%n",
+ .play_dir = 1,
+
+ .audiofile_auto_exts = (char *[]){
+ "aac",
+ "ac3",
+ "dts",
+ "eac3",
+ "flac",
+ "m4a",
+ "mka",
+ "mp3",
+ "ogg",
+ "opus",
+ "thd",
+ "wav",
+ "wv",
+ NULL
+ },
+
+ .coverart_auto_exts = (char *[]){
+ "avif",
+ "bmp",
+ "gif",
+ "jpeg",
+ "jpg",
+ "jxl",
+ "png",
+ "tif",
+ "tiff",
+ "webp",
+ NULL
+ },
+
+ .sub_auto_exts = (char *[]){
+ "ass",
+ "idx",
+ "lrc",
+ "mks",
+ "pgs",
+ "rt",
+ "sbv",
+ "scc",
+ "smi",
+ "srt",
+ "ssa",
+ "sub",
+ "sup",
+ "utf",
+ "utf-8",
+ "utf8",
+ "vtt",
+ NULL
+ },
+
+ .audio_output_channels = {
+ .set = 1,
+ .auto_safe = 1,
+ },
+
+ .display_tags = (char *[]){
+ "Artist", "Album", "Album_Artist", "Comment", "Composer",
+ "Date", "Description", "Genre", "Performer", "Rating",
+ "Series", "Title", "Track", "icy-title", "service_name",
+ "Uploader", "Channel_URL",
+ NULL
+ },
+
+ .cuda_device = -1,
+
+ .watch_later_options = (char *[]){
+ "start",
+ "speed",
+ "edition",
+ "volume",
+ "mute",
+ "audio-delay",
+ "gamma",
+ "brightness",
+ "contrast",
+ "saturation",
+ "hue",
+ "deinterlace",
+ "vf",
+ "af",
+ "panscan",
+ "aid",
+ "vid",
+ "sid",
+ "sub-delay",
+ "sub-speed",
+ "sub-pos",
+ "sub-visibility",
+ "sub-scale",
+ "sub-use-margins",
+ "sub-ass-force-margins",
+ "sub-ass-vsfilter-aspect-compat",
+ "sub-ass-override",
+ "secondary-sub-visibility",
+ "ab-loop-a",
+ "ab-loop-b",
+ "video-aspect-override",
+ "video-aspect-method",
+ "video-unscaled",
+ "video-pan-x",
+ "video-pan-y",
+ "video-rotate",
+ "video-crop",
+ "video-zoom",
+ "video-scale-x",
+ "video-scale-y",
+ "video-align-x",
+ "video-align-y",
+ NULL
+ },
+};
+
+const struct m_sub_options mp_opt_root = {
+ .opts = mp_opts,
+ .size = sizeof(struct MPOpts),
+ .defaults = &mp_default_opts,
+};
+
+#endif /* MPLAYER_CFG_MPLAYER_H */
diff --git a/options/options.h b/options/options.h
new file mode 100644
index 0000000..aa071b2
--- /dev/null
+++ b/options/options.h
@@ -0,0 +1,406 @@
+#ifndef MPLAYER_OPTIONS_H
+#define MPLAYER_OPTIONS_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include "m_option.h"
+#include "common/common.h"
+
+typedef struct mp_vo_opts {
+ struct m_obj_settings *video_driver_list;
+
+ bool taskbar_progress;
+ bool snap_window;
+ int drag_and_drop;
+ bool ontop;
+ int ontop_level;
+ bool fullscreen;
+ bool border;
+ bool title_bar;
+ bool all_workspaces;
+ bool window_minimized;
+ bool window_maximized;
+ bool focus_on_open;
+
+ int screen_id;
+ char *screen_name;
+ int fsscreen_id;
+ char *fsscreen_name;
+ char *winname;
+ char *appid;
+ int content_type;
+ int x11_netwm;
+ int x11_bypass_compositor;
+ int x11_present;
+ bool x11_wid_title;
+ bool cursor_passthrough;
+ bool native_keyrepeat;
+
+ float panscan;
+ float zoom;
+ float pan_x, pan_y;
+ float align_x, align_y;
+ float scale_x, scale_y;
+ float margin_x[2];
+ float margin_y[2];
+ int unscaled;
+
+ struct m_geometry geometry;
+ struct m_geometry autofit;
+ struct m_geometry autofit_larger;
+ struct m_geometry autofit_smaller;
+ double window_scale;
+
+ bool auto_window_resize;
+ bool keepaspect;
+ bool keepaspect_window;
+ bool hidpi_window_scale;
+ bool native_fs;
+
+ int64_t WinID;
+
+ float force_monitor_aspect;
+ float monitor_pixel_aspect;
+ bool force_render;
+ bool force_window_position;
+
+ int backdrop_type;
+ int window_affinity;
+ char *mmcss_profile;
+ int window_corners;
+
+ double display_fps_override;
+ double timing_offset;
+ int video_sync;
+
+ struct m_geometry android_surface_size;
+
+ int swapchain_depth; // max number of images to render ahead
+
+ struct m_geometry video_crop;
+} mp_vo_opts;
+
+// Subtitle options needed by the subtitle decoders/renderers.
+struct mp_subtitle_opts {
+ bool sub_visibility;
+ bool sec_sub_visibility;
+ float sub_pos;
+ float sub_delay;
+ float sub_fps;
+ float sub_speed;
+ bool sub_forced_events_only;
+ bool stretch_dvd_subs;
+ bool stretch_image_subs;
+ bool image_subs_video_res;
+ bool sub_fix_timing;
+ bool sub_stretch_durations;
+ bool sub_scale_by_window;
+ bool sub_scale_with_window;
+ bool ass_scale_with_window;
+ struct osd_style_opts *sub_style;
+ float sub_scale;
+ float sub_gauss;
+ bool sub_gray;
+ bool ass_enabled;
+ float ass_line_spacing;
+ bool ass_use_margins;
+ bool sub_use_margins;
+ bool ass_vsfilter_aspect_compat;
+ int ass_vsfilter_color_compat;
+ bool ass_vsfilter_blur_compat;
+ bool use_embedded_fonts;
+ char **ass_style_override_list;
+ char *ass_styles_file;
+ int ass_style_override;
+ int ass_hinting;
+ int ass_shaper;
+ bool ass_justify;
+ bool sub_clear_on_seek;
+ int teletext_page;
+ bool sub_past_video_end;
+};
+
+struct mp_sub_filter_opts {
+ bool sub_filter_SDH;
+ bool sub_filter_SDH_harder;
+ bool rf_enable;
+ bool rf_plain;
+ char **rf_items;
+ char **jsre_items;
+ bool rf_warn;
+};
+
+struct mp_osd_render_opts {
+ float osd_bar_align_x;
+ float osd_bar_align_y;
+ float osd_bar_w;
+ float osd_bar_h;
+ float osd_scale;
+ bool osd_scale_by_window;
+ struct osd_style_opts *osd_style;
+ bool force_rgba_osd;
+};
+
+typedef struct MPOpts {
+ bool property_print_help;
+ bool use_terminal;
+ char *dump_stats;
+ int verbose;
+ bool msg_really_quiet;
+ char **msg_levels;
+ bool msg_color;
+ bool msg_module;
+ bool msg_time;
+ char *log_file;
+
+ int operation_mode;
+
+ char **reset_options;
+ char **script_files;
+ char **script_opts;
+ bool js_memory_report;
+ bool lua_load_osc;
+ bool lua_load_ytdl;
+ char *lua_ytdl_format;
+ char **lua_ytdl_raw_options;
+ bool lua_load_stats;
+ bool lua_load_console;
+ int lua_load_auto_profiles;
+
+ bool auto_load_scripts;
+
+ bool audio_exclusive;
+ bool ao_null_fallback;
+ bool audio_stream_silence;
+ float audio_wait_open;
+ int force_vo;
+ float softvol_volume;
+ int rgain_mode;
+ float rgain_preamp; // Set replaygain pre-amplification
+ bool rgain_clip; // Enable/disable clipping prevention
+ float rgain_fallback;
+ int softvol_mute;
+ float softvol_max;
+ int gapless_audio;
+
+ mp_vo_opts *vo;
+ struct ao_opts *ao_opts;
+
+ char *wintitle;
+ char *media_title;
+
+ struct mp_csp_equalizer_opts *video_equalizer;
+
+ int stop_screensaver;
+ int cursor_autohide_delay;
+ bool cursor_autohide_fs;
+
+ struct mp_subtitle_opts *subs_rend;
+ struct mp_sub_filter_opts *subs_filt;
+ struct mp_osd_render_opts *osd_rend;
+
+ int osd_level;
+ int osd_duration;
+ bool osd_fractions;
+ int osd_on_seek;
+ bool video_osd;
+
+ bool untimed;
+ char *stream_dump;
+ bool stop_playback_on_init_failure;
+ int loop_times;
+ int loop_file;
+ bool shuffle;
+ bool ordered_chapters;
+ char *ordered_chapters_files;
+ int chapter_merge_threshold;
+ double chapter_seek_threshold;
+ char *chapter_file;
+ bool merge_files;
+ bool quiet;
+ bool load_config;
+ char *force_configdir;
+ bool use_filedir_conf;
+ int hls_bitrate;
+ int edition_id;
+ bool initial_audio_sync;
+ double sync_max_video_change;
+ double sync_max_audio_change;
+ int sync_max_factor;
+ int hr_seek;
+ float hr_seek_demuxer_offset;
+ bool hr_seek_framedrop;
+ float audio_delay;
+ float default_max_pts_correction;
+ int autosync;
+ int frame_dropping;
+ bool video_latency_hacks;
+ int term_osd;
+ bool term_osd_bar;
+ char *term_osd_bar_chars;
+ bool term_remaining_playtime;
+ char *term_title;
+ char *playing_msg;
+ char *osd_playing_msg;
+ int osd_playing_msg_duration;
+ char *status_msg;
+ char *osd_status_msg;
+ char *osd_msg[3];
+ int player_idle_mode;
+ bool consolecontrols;
+ int playlist_pos;
+ struct m_rel_time play_start;
+ struct m_rel_time play_end;
+ struct m_rel_time play_length;
+ int play_dir;
+ bool rebase_start_time;
+ int play_frames;
+ double ab_loop[2];
+ int ab_loop_count;
+ double step_sec;
+ bool position_resume;
+ bool position_check_mtime;
+ bool position_save_on_quit;
+ bool write_filename_in_watch_later_config;
+ bool ignore_path_in_watch_later_config;
+ char *watch_later_dir;
+ char **watch_later_options;
+ bool pause;
+ int keep_open;
+ bool keep_open_pause;
+ double image_display_duration;
+ char *lavfi_complex;
+ int stream_id[2][STREAM_TYPE_COUNT];
+ char **stream_lang[STREAM_TYPE_COUNT];
+ bool stream_auto_sel;
+ bool subs_with_matching_audio;
+ bool subs_match_os_language;
+ int subs_fallback;
+ int subs_fallback_forced;
+ int audio_display;
+ char **display_tags;
+
+ char **audio_files;
+ char *demuxer_name;
+ bool demuxer_thread;
+ double demux_termination_timeout;
+ bool demuxer_cache_wait;
+ bool prefetch_open;
+ char *audio_demuxer_name;
+ char *sub_demuxer_name;
+
+ bool cache_pause;
+ bool cache_pause_initial;
+ float cache_pause_wait;
+
+ struct image_writer_opts *screenshot_image_opts;
+ char *screenshot_template;
+ char *screenshot_dir;
+ bool screenshot_sw;
+
+ struct m_channels audio_output_channels;
+ int audio_output_format;
+ int force_srate;
+ double playback_speed;
+ bool pitch_correction;
+ struct m_obj_settings *vf_settings;
+ struct m_obj_settings *af_settings;
+ struct filter_opts *filter_opts;
+ struct dec_wrapper_opts *dec_wrapper;
+ char **sub_name;
+ char **sub_paths;
+ char **audiofile_paths;
+ char **coverart_files;
+ char **external_files;
+ bool autoload_files;
+ int sub_auto;
+ char **sub_auto_exts;
+ int audiofile_auto;
+ char **audiofile_auto_exts;
+ int coverart_auto;
+ char **coverart_auto_exts;
+ bool coverart_whitelist;
+ bool osd_bar_visible;
+
+ int w32_priority;
+
+ struct bluray_opts *stream_bluray_opts;
+ struct cdda_opts *stream_cdda_opts;
+ struct dvb_opts *stream_dvb_opts;
+ struct lavf_opts *stream_lavf_opts;
+
+ char *bluray_device;
+
+ struct demux_rawaudio_opts *demux_rawaudio;
+ struct demux_rawvideo_opts *demux_rawvideo;
+ struct demux_playlist_opts *demux_playlist;
+ struct demux_lavf_opts *demux_lavf;
+ struct demux_mkv_opts *demux_mkv;
+ struct demux_cue_opts *demux_cue;
+
+ struct demux_opts *demux_opts;
+ struct demux_cache_opts *demux_cache_opts;
+ struct stream_opts *stream_opts;
+
+ struct vd_lavc_params *vd_lavc_params;
+ struct ad_lavc_params *ad_lavc_params;
+
+ struct input_opts *input_opts;
+
+ // may be NULL if encoding is not compiled-in
+ struct encode_opts *encode_opts;
+
+ char *ipc_path;
+ char *ipc_client;
+
+ struct mp_resample_opts *resample_opts;
+
+ struct ra_ctx_opts *ra_ctx_opts;
+ struct gl_video_opts *gl_video_opts;
+ struct angle_opts *angle_opts;
+ struct opengl_opts *opengl_opts;
+ struct vulkan_opts *vulkan_opts;
+ struct vulkan_display_opts *vulkan_display_opts;
+ struct spirv_opts *spirv_opts;
+ struct d3d11_opts *d3d11_opts;
+ struct d3d11va_opts *d3d11va_opts;
+ struct macos_opts *macos_opts;
+ struct drm_opts *drm_opts;
+ struct wayland_opts *wayland_opts;
+ struct wingl_opts *wingl_opts;
+ struct cuda_opts *cuda_opts;
+ struct dvd_opts *dvd_opts;
+ struct vaapi_opts *vaapi_opts;
+ struct sws_opts *sws_opts;
+ struct zimg_opts *zimg_opts;
+
+ int cuda_device;
+} MPOpts;
+
+struct cuda_opts {
+ int cuda_device;
+};
+
+struct dvd_opts {
+ int angle;
+ int speed;
+ char *device;
+};
+
+struct filter_opts {
+ bool deinterlace;
+};
+
+extern const struct m_sub_options vo_sub_opts;
+extern const struct m_sub_options cuda_conf;
+extern const struct m_sub_options dvd_conf;
+extern const struct m_sub_options mp_subtitle_sub_opts;
+extern const struct m_sub_options mp_sub_filter_opts;
+extern const struct m_sub_options mp_osd_render_sub_opts;
+extern const struct m_sub_options filter_conf;
+extern const struct m_sub_options resample_conf;
+extern const struct m_sub_options stream_conf;
+extern const struct m_sub_options dec_wrapper_conf;
+extern const struct m_sub_options mp_opt_root;
+
+#endif
diff --git a/options/parse_commandline.c b/options/parse_commandline.c
new file mode 100644
index 0000000..93120d3
--- /dev/null
+++ b/options/parse_commandline.c
@@ -0,0 +1,261 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+#include <stdbool.h>
+
+#include "osdep/io.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "m_option.h"
+#include "m_config_frontend.h"
+#include "options.h"
+#include "common/playlist.h"
+#include "parse_commandline.h"
+
+#define GLOBAL 0
+#define LOCAL 1
+
+struct parse_state {
+ struct m_config *config;
+ char **argv;
+ struct mp_log *log; // silent if NULL
+
+ bool no_more_opts;
+ bool error;
+
+ bool is_opt;
+ struct bstr arg;
+ struct bstr param;
+};
+
+// Returns true if more args, false if all parsed or an error occurred.
+static bool split_opt(struct parse_state *p)
+{
+ assert(!p->error);
+
+ if (!p->argv || !p->argv[0])
+ return false;
+
+ p->is_opt = false;
+ p->arg = bstr0(p->argv[0]);
+ p->param = bstr0(NULL);
+
+ p->argv++;
+
+ if (p->no_more_opts || !bstr_startswith0(p->arg, "-") || p->arg.len == 1)
+ return true;
+
+ if (bstrcmp0(p->arg, "--") == 0) {
+ p->no_more_opts = true;
+ return split_opt(p);
+ }
+
+ p->is_opt = true;
+
+ bool new_opt = bstr_eatstart0(&p->arg, "--");
+ if (!new_opt)
+ bstr_eatstart0(&p->arg, "-");
+
+ bool ambiguous = !bstr_split_tok(p->arg, "=", &p->arg, &p->param);
+
+ bool need_param = m_config_option_requires_param(p->config, p->arg) > 0;
+
+ if (ambiguous && need_param) {
+ if (!p->argv[0] || new_opt) {
+ p->error = true;
+ MP_FATAL(p, "Error parsing commandline option %.*s: %s\n",
+ BSTR_P(p->arg), m_option_strerror(M_OPT_MISSING_PARAM));
+ MP_WARN(p, "Make sure you're using e.g. '--%.*s=value' instead "
+ "of '--%.*s value'.\n", BSTR_P(p->arg), BSTR_P(p->arg));
+ return false;
+ }
+ p->param = bstr0(p->argv[0]);
+ p->argv++;
+ }
+
+ return true;
+}
+
+#ifdef __MINGW32__
+static void process_non_option(struct playlist *files, const char *arg)
+{
+ glob_t gg;
+
+ // Glob filenames on Windows (cmd.exe doesn't do this automatically)
+ if (glob(arg, 0, NULL, &gg)) {
+ playlist_add_file(files, arg);
+ } else {
+ for (int i = 0; i < gg.gl_pathc; i++)
+ playlist_add_file(files, gg.gl_pathv[i]);
+
+ globfree(&gg);
+ }
+}
+#else
+static void process_non_option(struct playlist *files, const char *arg)
+{
+ playlist_add_file(files, arg);
+}
+#endif
+
+// returns M_OPT_... error code
+int m_config_parse_mp_command_line(m_config_t *config, struct playlist *files,
+ struct mpv_global *global, char **argv)
+{
+ int ret = M_OPT_UNKNOWN;
+ int mode = 0;
+ struct playlist_entry *local_start = NULL;
+
+ int local_params_count = 0;
+ struct playlist_param *local_params = 0;
+
+ assert(config != NULL);
+
+ mode = GLOBAL;
+
+ struct parse_state p = {config, argv, config->log};
+ while (split_opt(&p)) {
+ if (p.is_opt) {
+ int flags = M_SETOPT_FROM_CMDLINE;
+ if (mode == LOCAL)
+ flags |= M_SETOPT_BACKUP | M_SETOPT_CHECK_ONLY;
+ int r = m_config_set_option_cli(config, p.arg, p.param, flags);
+ if (r == M_OPT_EXIT) {
+ ret = r;
+ goto err_out;
+ } else if (r < 0) {
+ MP_FATAL(config, "Setting commandline option --%.*s=%.*s failed.\n",
+ BSTR_P(p.arg), BSTR_P(p.param));
+ goto err_out;
+ }
+
+ // Handle some special arguments outside option parser.
+
+ if (!bstrcmp0(p.arg, "{")) {
+ if (mode != GLOBAL) {
+ MP_ERR(config, "'--{' can not be nested.\n");
+ goto err_out;
+ }
+ mode = LOCAL;
+ assert(!local_start);
+ local_start = playlist_get_last(files);
+ continue;
+ }
+
+ if (!bstrcmp0(p.arg, "}")) {
+ if (mode != LOCAL) {
+ MP_ERR(config, "Too many closing '--}'.\n");
+ goto err_out;
+ }
+ if (local_params_count) {
+ // The files added between '{' and '}' are the entries from
+ // the entry _after_ local_start, until the end of the list.
+ // If local_start is NULL, the list was empty on '{', and we
+ // want all files in the list.
+ struct playlist_entry *cur = local_start
+ ? playlist_entry_get_rel(local_start, 1)
+ : playlist_get_first(files);
+ if (!cur)
+ MP_WARN(config, "Ignored options!\n");
+ while (cur) {
+ playlist_entry_add_params(cur, local_params,
+ local_params_count);
+ cur = playlist_entry_get_rel(cur, 1);
+ }
+ }
+ local_params_count = 0;
+ mode = GLOBAL;
+ m_config_restore_backups(config);
+ local_start = NULL;
+ continue;
+ }
+
+ if (bstrcmp0(p.arg, "playlist") == 0) {
+ // append the playlist to the local args
+ char *param0 = bstrdup0(NULL, p.param);
+ struct playlist *pl = playlist_parse_file(param0, NULL, global);
+ if (!pl) {
+ MP_FATAL(config, "Error reading playlist '%.*s'\n",
+ BSTR_P(p.param));
+ talloc_free(param0);
+ goto err_out;
+ }
+ playlist_transfer_entries(files, pl);
+ playlist_populate_playlist_path(files, param0);
+ talloc_free(param0);
+ talloc_free(pl);
+ continue;
+ }
+
+ if (mode == LOCAL) {
+ MP_TARRAY_APPEND(NULL, local_params, local_params_count,
+ (struct playlist_param) {p.arg, p.param});
+ }
+ } else {
+ // filename
+ void *tmp = talloc_new(NULL);
+ char *file0 = bstrdup0(tmp, p.arg);
+ process_non_option(files, file0);
+ talloc_free(tmp);
+ }
+ }
+
+ if (p.error)
+ goto err_out;
+
+ if (mode != GLOBAL) {
+ MP_ERR(config, "Missing closing --} on command line.\n");
+ goto err_out;
+ }
+
+ ret = 0; // success
+
+err_out:
+ talloc_free(local_params);
+ m_config_restore_backups(config);
+ return ret;
+}
+
+/* Parse some command line options early before main parsing.
+ * --no-config prevents reading configuration files (otherwise done before
+ * command line parsing), and --really-quiet suppresses messages printed
+ * during normal options parsing.
+ */
+void m_config_preparse_command_line(m_config_t *config, struct mpv_global *global,
+ int *verbose, char **argv)
+{
+ struct parse_state p = {config, argv, mp_null_log};
+ while (split_opt(&p)) {
+ if (p.is_opt) {
+ // Ignore non-pre-parse options. They will be set later.
+ // Option parsing errors will be handled later as well.
+ int flags = M_SETOPT_FROM_CMDLINE | M_SETOPT_PRE_PARSE_ONLY;
+ m_config_set_option_cli(config, p.arg, p.param, flags);
+ if (bstrcmp0(p.arg, "v") == 0)
+ (*verbose)++;
+ }
+ }
+
+ for (int n = 0; n < config->num_opts; n++)
+ config->opts[n].warning_was_printed = false;
+}
diff --git a/options/parse_commandline.h b/options/parse_commandline.h
new file mode 100644
index 0000000..1509fc9
--- /dev/null
+++ b/options/parse_commandline.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_PARSER_MPCMD_H
+#define MPLAYER_PARSER_MPCMD_H
+
+#include <stdbool.h>
+
+struct playlist;
+struct m_config;
+struct mpv_global;
+
+int m_config_parse_mp_command_line(m_config_t *config, struct playlist *files,
+ struct mpv_global *global, char **argv);
+void m_config_preparse_command_line(m_config_t *config, struct mpv_global *global,
+ int *verbose, char **argv);
+
+#endif /* MPLAYER_PARSER_MPCMD_H */
diff --git a/options/parse_configfile.c b/options/parse_configfile.c
new file mode 100644
index 0000000..edd6be9
--- /dev/null
+++ b/options/parse_configfile.c
@@ -0,0 +1,178 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "osdep/io.h"
+
+#include "parse_configfile.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "m_option.h"
+#include "m_config.h"
+#include "stream/stream.h"
+
+// Skip whitespace and comments (assuming there are no line breaks)
+static bool skip_ws(bstr *s)
+{
+ *s = bstr_lstrip(*s);
+ if (bstr_startswith0(*s, "#"))
+ s->len = 0;
+ return s->len;
+}
+
+int m_config_parse(m_config_t *config, const char *location, bstr data,
+ char *initial_section, int flags)
+{
+ m_profile_t *profile = m_config_add_profile(config, initial_section);
+ void *tmp = talloc_new(NULL);
+ int line_no = 0;
+ int errors = 0;
+
+ bstr_eatstart0(&data, "\xEF\xBB\xBF"); // skip BOM
+
+ while (data.len) {
+ talloc_free_children(tmp);
+ bool ok = false;
+
+ line_no++;
+ char loc[512];
+ snprintf(loc, sizeof(loc), "%s:%d:", location, line_no);
+
+ bstr line = bstr_strip_linebreaks(bstr_getline(data, &data));
+ if (!skip_ws(&line))
+ continue;
+
+ // Profile declaration
+ if (bstr_eatstart0(&line, "[")) {
+ bstr profilename;
+ if (!bstr_split_tok(line, "]", &profilename, &line)) {
+ MP_ERR(config, "%s missing closing ]\n", loc);
+ goto error;
+ }
+ if (skip_ws(&line)) {
+ MP_ERR(config, "%s unparsable extra characters: '%.*s'\n",
+ loc, BSTR_P(line));
+ goto error;
+ }
+ profile = m_config_add_profile(config, bstrto0(tmp, profilename));
+ continue;
+ }
+
+ bstr_eatstart0(&line, "--");
+
+ bstr option = line;
+ while (line.len && (mp_isalnum(line.start[0]) || line.start[0] == '_' ||
+ line.start[0] == '-'))
+ line = bstr_cut(line, 1);
+ option.len = option.len - line.len;
+ skip_ws(&line);
+
+ bstr value = {0};
+ if (bstr_eatstart0(&line, "=")) {
+ skip_ws(&line);
+ if (line.len && (line.start[0] == '"' || line.start[0] == '\'')) {
+ // Simple quoting, like "value"
+ char term[2] = {line.start[0], 0};
+ line = bstr_cut(line, 1);
+ if (!bstr_split_tok(line, term, &value, &line)) {
+ MP_ERR(config, "%s unterminated quote\n", loc);
+ goto error;
+ }
+ } else if (bstr_eatstart0(&line, "%")) {
+ // Quoting with length, like %5%value
+ bstr rest;
+ long long len = bstrtoll(line, &rest, 10);
+ if (rest.len == line.len || !bstr_eatstart0(&rest, "%") ||
+ len > rest.len)
+ {
+ MP_ERR(config, "%s fixed-length quoting expected - put "
+ "\"quotes\" around the option value if you did not "
+ "intend to use this, but your option value starts "
+ "with '%%'\n", loc);
+ goto error;
+ }
+ value = bstr_splice(rest, 0, len);
+ line = bstr_cut(rest, len);
+ } else {
+ // No quoting; take everything until the comment or end of line
+ int end = bstrchr(line, '#');
+ value = bstr_strip(end < 0 ? line : bstr_splice(line, 0, end));
+ line.len = 0;
+ }
+ }
+ if (skip_ws(&line)) {
+ MP_ERR(config, "%s unparsable extra characters: '%.*s'\n",
+ loc, BSTR_P(line));
+ goto error;
+ }
+
+ int res = m_config_set_profile_option(config, profile, option, value);
+ if (res < 0) {
+ MP_ERR(config, "%s setting option %.*s='%.*s' failed.\n",
+ loc, BSTR_P(option), BSTR_P(value));
+ goto error;
+ }
+
+ ok = true;
+ error:
+ if (!ok)
+ errors++;
+ if (errors > 16) {
+ MP_ERR(config, "%s: too many errors, stopping.\n", location);
+ break;
+ }
+ }
+
+ if (config->recursion_depth == 0)
+ m_config_finish_default_profile(config, flags);
+
+ talloc_free(tmp);
+ return 1;
+}
+
+// Load options and profiles from a config file.
+// conffile: path to the config file
+// initial_section: default section where to add normal options
+// flags: M_SETOPT_* bits
+// returns: 1 on success, -1 on error, 0 if file not accessible.
+int m_config_parse_config_file(m_config_t *config, struct mpv_global *global,
+ const char *conffile, char *initial_section,
+ int flags)
+{
+ flags = flags | M_SETOPT_FROM_CONFIG_FILE;
+
+ MP_VERBOSE(config, "Reading config file %s\n", conffile);
+
+ struct stream *s = stream_create(conffile, STREAM_READ | STREAM_ORIGIN_DIRECT,
+ NULL, global);
+ if (!s)
+ return 0;
+ bstr data = stream_read_complete(s, s, 1000000000);
+ if (!data.start)
+ return 0;
+
+ int r = m_config_parse(config, conffile, data, initial_section, flags);
+ talloc_free(data.start);
+ free_stream(s);
+ return r;
+}
diff --git a/options/parse_configfile.h b/options/parse_configfile.h
new file mode 100644
index 0000000..3622fc8
--- /dev/null
+++ b/options/parse_configfile.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_PARSER_CFG_H
+#define MPLAYER_PARSER_CFG_H
+
+#include "m_config_frontend.h"
+
+int m_config_parse_config_file(m_config_t* config, struct mpv_global *global,
+ const char *conffile, char *initial_section,
+ int flags);
+
+int m_config_parse(m_config_t *config, const char *location, bstr data,
+ char *initial_section, int flags);
+
+#endif /* MPLAYER_PARSER_CFG_H */
diff --git a/options/path.c b/options/path.c
new file mode 100644
index 0000000..52dc113
--- /dev/null
+++ b/options/path.c
@@ -0,0 +1,410 @@
+/*
+ * This file is part of mpv.
+ *
+ * Get path to config dir/file.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "mpv_talloc.h"
+#include "osdep/io.h"
+#include "osdep/path.h"
+#include "misc/ctype.h"
+
+// In order of decreasing priority: the first has highest priority.
+static const mp_get_platform_path_cb path_resolvers[] = {
+#if HAVE_COCOA
+ mp_get_platform_path_osx,
+#endif
+#if HAVE_DARWIN
+ mp_get_platform_path_darwin,
+#elif !defined(_WIN32) || defined(__CYGWIN__)
+ mp_get_platform_path_unix,
+#endif
+#if HAVE_UWP
+ mp_get_platform_path_uwp,
+#elif defined(_WIN32)
+ mp_get_platform_path_win,
+#endif
+};
+
+// from highest (most preferred) to lowest priority
+static const char *const config_dirs[] = {
+ "home",
+ "old_home",
+ "osxbundle",
+ "exe_dir",
+ "global",
+};
+
+// Return a platform specific path using a path type as defined in osdep/path.h.
+// Keep in mind that the only way to free the return value is freeing talloc_ctx
+// (or its children), as this function can return a statically allocated string.
+static const char *mp_get_platform_path(void *talloc_ctx,
+ struct mpv_global *global,
+ const char *type)
+{
+ assert(talloc_ctx);
+
+ if (global->configdir) {
+ // force all others to NULL, only first returns the path
+ for (int n = 0; n < MP_ARRAY_SIZE(config_dirs); n++) {
+ if (strcmp(config_dirs[n], type) == 0)
+ return (n == 0 && global->configdir[0]) ? global->configdir : NULL;
+ }
+ }
+
+ // Return the native config path if the platform doesn't support the
+ // type we are trying to fetch.
+ const char *fallback_type = NULL;
+ if (!strcmp(type, "cache") || !strcmp(type, "state"))
+ fallback_type = "home";
+
+ for (int n = 0; n < MP_ARRAY_SIZE(path_resolvers); n++) {
+ const char *path = path_resolvers[n](talloc_ctx, type);
+ if (path && path[0])
+ return path;
+ }
+
+ if (fallback_type) {
+ assert(strcmp(fallback_type, type) != 0);
+ return mp_get_platform_path(talloc_ctx, global, fallback_type);
+ }
+ return NULL;
+}
+
+void mp_init_paths(struct mpv_global *global, struct MPOpts *opts)
+{
+ TA_FREEP(&global->configdir);
+
+ const char *force_configdir = getenv("MPV_HOME");
+ if (opts->force_configdir && opts->force_configdir[0])
+ force_configdir = opts->force_configdir;
+ if (!opts->load_config)
+ force_configdir = "";
+
+ global->configdir = talloc_strdup(global, force_configdir);
+}
+
+char *mp_find_user_file(void *talloc_ctx, struct mpv_global *global,
+ const char *type, const char *filename)
+{
+ void *tmp = talloc_new(NULL);
+ char *res = (char *)mp_get_platform_path(tmp, global, type);
+ if (res)
+ res = mp_path_join(talloc_ctx, res, filename);
+ talloc_free(tmp);
+ MP_DBG(global, "%s path: '%s' -> '%s'\n", type, filename, res ? res : "-");
+ return res;
+}
+
+static char **mp_find_all_config_files_limited(void *talloc_ctx,
+ struct mpv_global *global,
+ int max_files,
+ const char *filename)
+{
+ char **ret = talloc_array(talloc_ctx, char*, 2); // 2 preallocated
+ int num_ret = 0;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(config_dirs); i++) {
+ const char *dir = mp_get_platform_path(ret, global, config_dirs[i]);
+ bstr s = bstr0(filename);
+ while (dir && num_ret < max_files && s.len) {
+ bstr fn;
+ bstr_split_tok(s, "|", &fn, &s);
+
+ char *file = mp_path_join_bstr(ret, bstr0(dir), fn);
+ if (mp_path_exists(file)) {
+ MP_DBG(global, "config path: '%.*s' -> '%s'\n",
+ BSTR_P(fn), file);
+ MP_TARRAY_APPEND(NULL, ret, num_ret, file);
+ } else {
+ MP_DBG(global, "config path: '%.*s' -/-> '%s'\n",
+ BSTR_P(fn), file);
+ }
+ }
+ }
+
+ MP_TARRAY_GROW(NULL, ret, num_ret);
+ ret[num_ret] = NULL;
+
+ for (int n = 0; n < num_ret / 2; n++)
+ MPSWAP(char*, ret[n], ret[num_ret - n - 1]);
+ return ret;
+}
+
+char **mp_find_all_config_files(void *talloc_ctx, struct mpv_global *global,
+ const char *filename)
+{
+ return mp_find_all_config_files_limited(talloc_ctx, global, 64, filename);
+}
+
+char *mp_find_config_file(void *talloc_ctx, struct mpv_global *global,
+ const char *filename)
+{
+ char **l = mp_find_all_config_files_limited(talloc_ctx, global, 1, filename);
+ char *r = l && l[0] ? talloc_steal(talloc_ctx, l[0]) : NULL;
+ talloc_free(l);
+ return r;
+}
+
+char *mp_get_user_path(void *talloc_ctx, struct mpv_global *global,
+ const char *path)
+{
+ if (!path)
+ return NULL;
+ char *res = NULL;
+ bstr bpath = bstr0(path);
+ if (bstr_eatstart0(&bpath, "~")) {
+ // parse to "~" <prefix> "/" <rest>
+ bstr prefix, rest;
+ if (bstr_split_tok(bpath, "/", &prefix, &rest)) {
+ const char *rest0 = rest.start; // ok in this case
+ if (bstr_equals0(prefix, "~")) {
+ res = mp_find_config_file(talloc_ctx, global, rest0);
+ if (!res) {
+ void *tmp = talloc_new(NULL);
+ const char *p = mp_get_platform_path(tmp, global, "home");
+ res = mp_path_join_bstr(talloc_ctx, bstr0(p), rest);
+ talloc_free(tmp);
+ }
+ } else if (bstr_equals0(prefix, "")) {
+ char *home = getenv("HOME");
+ if (!home)
+ home = getenv("USERPROFILE");
+ res = mp_path_join_bstr(talloc_ctx, bstr0(home), rest);
+ } else if (bstr_eatstart0(&prefix, "~")) {
+ void *tmp = talloc_new(NULL);
+ char type[80];
+ snprintf(type, sizeof(type), "%.*s", BSTR_P(prefix));
+ const char *p = mp_get_platform_path(tmp, global, type);
+ res = mp_path_join_bstr(talloc_ctx, bstr0(p), rest);
+ talloc_free(tmp);
+ }
+ }
+ }
+ if (!res)
+ res = talloc_strdup(talloc_ctx, path);
+ MP_DBG(global, "user path: '%s' -> '%s'\n", path, res);
+ return res;
+}
+
+char *mp_basename(const char *path)
+{
+ char *s;
+
+#if HAVE_DOS_PATHS
+ if (!mp_is_url(bstr0(path))) {
+ s = strrchr(path, '\\');
+ if (s)
+ path = s + 1;
+ s = strrchr(path, ':');
+ if (s)
+ path = s + 1;
+ }
+#endif
+ s = strrchr(path, '/');
+ return s ? s + 1 : (char *)path;
+}
+
+struct bstr mp_dirname(const char *path)
+{
+ struct bstr ret = {
+ (uint8_t *)path, mp_basename(path) - path
+ };
+ if (ret.len == 0)
+ return bstr0(".");
+ return ret;
+}
+
+
+#if HAVE_DOS_PATHS
+static const char mp_path_separators[] = "\\/";
+#else
+static const char mp_path_separators[] = "/";
+#endif
+
+// Mutates path and removes a trailing '/' (or '\' on Windows)
+void mp_path_strip_trailing_separator(char *path)
+{
+ size_t len = strlen(path);
+ if (len > 0 && strchr(mp_path_separators, path[len - 1]))
+ path[len - 1] = '\0';
+}
+
+char *mp_splitext(const char *path, bstr *root)
+{
+ assert(path);
+ int skip = (*path == '.'); // skip leading dot for "hidden" unix files
+ const char *split = strrchr(path + skip, '.');
+ if (!split || !split[1] || strchr(split, '/'))
+ return NULL;
+ if (root)
+ *root = (bstr){(char *)path, split - path};
+ return (char *)split + 1;
+}
+
+bool mp_path_is_absolute(struct bstr path)
+{
+ if (path.len && strchr(mp_path_separators, path.start[0]))
+ return true;
+
+#if HAVE_DOS_PATHS
+ // Note: "X:filename" is a path relative to the current working directory
+ // of drive X, and thus is not an absolute path. It needs to be
+ // followed by \ or /.
+ if (path.len >= 3 && path.start[1] == ':' &&
+ strchr(mp_path_separators, path.start[2]))
+ return true;
+#endif
+
+ return false;
+}
+
+char *mp_path_join_bstr(void *talloc_ctx, struct bstr p1, struct bstr p2)
+{
+ if (p1.len == 0)
+ return bstrdup0(talloc_ctx, p2);
+ if (p2.len == 0)
+ return bstrdup0(talloc_ctx, p1);
+
+ if (mp_path_is_absolute(p2))
+ return bstrdup0(talloc_ctx, p2);
+
+ bool have_separator = strchr(mp_path_separators, p1.start[p1.len - 1]);
+#if HAVE_DOS_PATHS
+ // "X:" only => path relative to "X:" current working directory.
+ if (p1.len == 2 && p1.start[1] == ':')
+ have_separator = true;
+#endif
+
+ return talloc_asprintf(talloc_ctx, "%.*s%s%.*s", BSTR_P(p1),
+ have_separator ? "" : "/", BSTR_P(p2));
+}
+
+char *mp_path_join(void *talloc_ctx, const char *p1, const char *p2)
+{
+ return mp_path_join_bstr(talloc_ctx, bstr0(p1), bstr0(p2));
+}
+
+char *mp_getcwd(void *talloc_ctx)
+{
+ char *e_wd = getenv("PWD");
+ if (e_wd)
+ return talloc_strdup(talloc_ctx, e_wd);
+
+ char *wd = talloc_array(talloc_ctx, char, 20);
+ while (getcwd(wd, talloc_get_size(wd)) == NULL) {
+ if (errno != ERANGE) {
+ talloc_free(wd);
+ return NULL;
+ }
+ wd = talloc_realloc(talloc_ctx, wd, char, talloc_get_size(wd) * 2);
+ }
+ return wd;
+}
+
+char *mp_normalize_path(void *talloc_ctx, const char *path)
+{
+ if (mp_is_url(bstr0(path)))
+ return talloc_strdup(talloc_ctx, path);
+
+ return mp_path_join(talloc_ctx, mp_getcwd(talloc_ctx), path);
+}
+
+bool mp_path_exists(const char *path)
+{
+ struct stat st;
+ return path && stat(path, &st) == 0;
+}
+
+bool mp_path_isdir(const char *path)
+{
+ struct stat st;
+ return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
+}
+
+// Return false if it's considered a normal local filesystem path.
+bool mp_is_url(bstr path)
+{
+ int proto = bstr_find0(path, "://");
+ if (proto < 1)
+ return false;
+ // Per RFC3986, the first character of the protocol must be alphabetic.
+ // The rest must be alphanumeric plus -, + and .
+ for (int i = 0; i < proto; i++) {
+ unsigned char c = path.start[i];
+ if ((i == 0 && !mp_isalpha(c)) ||
+ (!mp_isalnum(c) && c != '.' && c != '-' && c != '+'))
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Return the protocol part of path, e.g. "http" if path is "http://...".
+// On success, out_url (if not NULL) is set to the part after the "://".
+bstr mp_split_proto(bstr path, bstr *out_url)
+{
+ if (!mp_is_url(path))
+ return (bstr){0};
+ bstr r;
+ bstr_split_tok(path, "://", &r, out_url ? out_url : &(bstr){0});
+ return r;
+}
+
+void mp_mkdirp(const char *dir)
+{
+ char *path = talloc_strdup(NULL, dir);
+ char *cdir = path + 1;
+
+ while (cdir) {
+ cdir = strchr(cdir, '/');
+ if (cdir)
+ *cdir = 0;
+
+ mkdir(path, 0700);
+
+ if (cdir)
+ *cdir++ = '/';
+ }
+
+ talloc_free(path);
+}
+
+void mp_mk_user_dir(struct mpv_global *global, const char *type, char *subdir)
+{
+ char *dir = mp_find_user_file(NULL, global, type, subdir);
+ if (dir)
+ mp_mkdirp(dir);
+ talloc_free(dir);
+}
diff --git a/options/path.h b/options/path.h
new file mode 100644
index 0000000..7ec8f7b
--- /dev/null
+++ b/options/path.h
@@ -0,0 +1,98 @@
+/*
+ * Get path to config dir/file.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_PATH_H
+#define MPLAYER_PATH_H
+
+#include <stdbool.h>
+#include "misc/bstr.h"
+
+struct mpv_global;
+struct MPOpts;
+
+void mp_init_paths(struct mpv_global *global, struct MPOpts *opts);
+
+// Search for the input filename in several paths. These include user and global
+// config locations by default. Some platforms may implement additional platform
+// related lookups (i.e.: OSX inside an application bundle).
+char *mp_find_config_file(void *talloc_ctx, struct mpv_global *global,
+ const char *filename);
+
+// Search for local writable user files within a specific kind of user dir
+// as documented in osdep/path.h. This returns a result even if the file does
+// not exist. Calling it with filename="" is equivalent to retrieving the path
+// to the dir.
+char *mp_find_user_file(void *talloc_ctx, struct mpv_global *global,
+ const char *type, const char *filename);
+
+// Find all instances of the given config file. Paths are returned in order
+// from lowest to highest priority. filename can contain multiple names
+// separated with '|', with the first having highest priority.
+char **mp_find_all_config_files(void *talloc_ctx, struct mpv_global *global,
+ const char *filename);
+
+// Normally returns a talloc_strdup'ed copy of the path, except for special
+// paths starting with '~'. Used to allow the user explicitly reference a
+// file from the user's home or mpv config directory.
+char *mp_get_user_path(void *talloc_ctx, struct mpv_global *global,
+ const char *path);
+
+// Return pointer to filename part of path
+
+char *mp_basename(const char *path);
+
+/* Return file extension, excluding the '.'. If root is not NULL, set it to the
+ * part of the path without extension. So: path == root + "." + extension
+ * Don't consider it a file extension if the only '.' is the first character.
+ * Return NULL if no extension and don't set *root in this case.
+ */
+char *mp_splitext(const char *path, bstr *root);
+
+/* Return struct bstr referencing directory part of path, or if that
+ * would be empty, ".".
+ */
+struct bstr mp_dirname(const char *path);
+
+void mp_path_strip_trailing_separator(char *path);
+
+/* Join two path components and return a newly allocated string
+ * for the result. '/' is inserted between the components if needed.
+ * If p2 is an absolute path then the value of p1 is ignored.
+ */
+char *mp_path_join(void *talloc_ctx, const char *p1, const char *p2);
+char *mp_path_join_bstr(void *talloc_ctx, struct bstr p1, struct bstr p2);
+
+// Return whether the path is absolute.
+bool mp_path_is_absolute(struct bstr path);
+
+char *mp_getcwd(void *talloc_ctx);
+
+char *mp_normalize_path(void *talloc_ctx, const char *path);
+
+bool mp_path_exists(const char *path);
+bool mp_path_isdir(const char *path);
+
+bool mp_is_url(bstr path);
+
+bstr mp_split_proto(bstr path, bstr *out_url);
+
+void mp_mkdirp(const char *dir);
+void mp_mk_user_dir(struct mpv_global *global, const char *type, char *subdir);
+
+#endif /* MPLAYER_PATH_H */
diff --git a/osdep/android/strnlen.c b/osdep/android/strnlen.c
new file mode 100644
index 0000000..c8c9d3d
--- /dev/null
+++ b/osdep/android/strnlen.c
@@ -0,0 +1,40 @@
+/*-
+ * Copyright (c) 2009 David Schultz <das@FreeBSD.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stddef.h>
+#include "osdep/android/strnlen.h"
+
+size_t
+freebsd_strnlen(const char *s, size_t maxlen)
+{
+ size_t len;
+
+ for (len = 0; len < maxlen; len++, s++) {
+ if (!*s)
+ break;
+ }
+ return (len);
+}
diff --git a/osdep/android/strnlen.h b/osdep/android/strnlen.h
new file mode 100644
index 0000000..c1f3391
--- /dev/null
+++ b/osdep/android/strnlen.h
@@ -0,0 +1,33 @@
+/*-
+ * Copyright (c) 2009 David Schultz <das@FreeBSD.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef MP_OSDEP_ANDROID_STRNLEN
+#define MP_OSDEP_ANDROID_STRNLEN
+
+size_t
+freebsd_strnlen(const char *s, size_t maxlen);
+
+#endif
diff --git a/osdep/apple_utils.c b/osdep/apple_utils.c
new file mode 100644
index 0000000..02cdfaa
--- /dev/null
+++ b/osdep/apple_utils.c
@@ -0,0 +1,39 @@
+/*
+ * Apple-specific utility functions
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "apple_utils.h"
+
+#include "mpv_talloc.h"
+
+CFStringRef cfstr_from_cstr(const char *str)
+{
+ return CFStringCreateWithCString(NULL, str, kCFStringEncodingUTF8);
+}
+
+char *cfstr_get_cstr(const CFStringRef cfstr)
+{
+ if (!cfstr)
+ return NULL;
+ CFIndex size =
+ CFStringGetMaximumSizeForEncoding(
+ CFStringGetLength(cfstr), kCFStringEncodingUTF8) + 1;
+ char *buffer = talloc_zero_size(NULL, size);
+ CFStringGetCString(cfstr, buffer, size, kCFStringEncodingUTF8);
+ return buffer;
+}
diff --git a/osdep/apple_utils.h b/osdep/apple_utils.h
new file mode 100644
index 0000000..166937e
--- /dev/null
+++ b/osdep/apple_utils.h
@@ -0,0 +1,28 @@
+/*
+ * Apple-specific utility functions
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_APPLE_UTILS
+#define MPV_APPLE_UTILS
+
+#include <CoreFoundation/CoreFoundation.h>
+
+CFStringRef cfstr_from_cstr(const char *str);
+char *cfstr_get_cstr(const CFStringRef cfstr);
+
+#endif /* MPV_APPLE_UTILS */
diff --git a/osdep/compiler.h b/osdep/compiler.h
new file mode 100644
index 0000000..f565897
--- /dev/null
+++ b/osdep/compiler.h
@@ -0,0 +1,30 @@
+#ifndef MPV_COMPILER_H
+#define MPV_COMPILER_H
+
+#define MP_EXPAND_ARGS(...) __VA_ARGS__
+
+#ifdef __GNUC__
+#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format(printf, a1, a2)))
+#define MP_NORETURN __attribute__((noreturn))
+#define MP_FALLTHROUGH __attribute__((fallthrough))
+#define MP_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+#else
+#define PRINTF_ATTRIBUTE(a1, a2)
+#define MP_NORETURN
+#define MP_FALLTHROUGH do {} while (0)
+#define MP_WARN_UNUSED_RESULT
+#endif
+
+// Broken crap with __USE_MINGW_ANSI_STDIO
+#if defined(__MINGW32__) && defined(__GNUC__) && !defined(__clang__)
+#undef PRINTF_ATTRIBUTE
+#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format (gnu_printf, a1, a2)))
+#endif
+
+#ifdef __GNUC__
+#define MP_ASSERT_UNREACHABLE() (assert(!"unreachable"), __builtin_unreachable())
+#else
+#define MP_ASSERT_UNREACHABLE() (assert(!"unreachable"), abort())
+#endif
+
+#endif
diff --git a/osdep/endian.h b/osdep/endian.h
new file mode 100644
index 0000000..c6d1376
--- /dev/null
+++ b/osdep/endian.h
@@ -0,0 +1,37 @@
+#ifndef MP_ENDIAN_H_
+#define MP_ENDIAN_H_
+
+#include <sys/types.h>
+
+#if !defined(BYTE_ORDER)
+
+#if defined(__BYTE_ORDER)
+#define BYTE_ORDER __BYTE_ORDER
+#define LITTLE_ENDIAN __LITTLE_ENDIAN
+#define BIG_ENDIAN __BIG_ENDIAN
+#elif defined(__DARWIN_BYTE_ORDER)
+#define BYTE_ORDER __DARWIN_BYTE_ORDER
+#define LITTLE_ENDIAN __DARWIN_LITTLE_ENDIAN
+#define BIG_ENDIAN __DARWIN_BIG_ENDIAN
+#else
+#include <libavutil/bswap.h>
+#if AV_HAVE_BIGENDIAN
+#define BYTE_ORDER 1234
+#define LITTLE_ENDIAN 4321
+#define BIG_ENDIAN 1234
+#else
+#define BYTE_ORDER 1234
+#define LITTLE_ENDIAN 1234
+#define BIG_ENDIAN 4321
+#endif
+#endif
+
+#endif /* !defined(BYTE_ORDER) */
+
+#if BYTE_ORDER == BIG_ENDIAN
+#define MP_SELECT_LE_BE(LE, BE) BE
+#else
+#define MP_SELECT_LE_BE(LE, BE) LE
+#endif
+
+#endif
diff --git a/osdep/getpid.h b/osdep/getpid.h
new file mode 100644
index 0000000..ace5e29
--- /dev/null
+++ b/osdep/getpid.h
@@ -0,0 +1,29 @@
+/*
+ * getpid wrapper
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef _WIN32
+#include <windows.h>
+#define mp_getpid() GetCurrentProcessId()
+#else // POSIX
+#include <sys/types.h>
+#include <unistd.h>
+#define mp_getpid() getpid()
+#endif
diff --git a/osdep/glob-win.c b/osdep/glob-win.c
new file mode 100644
index 0000000..08fd90f
--- /dev/null
+++ b/osdep/glob-win.c
@@ -0,0 +1,162 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <stdbool.h>
+#include <string.h>
+#include "osdep/io.h"
+#include "mpv_talloc.h"
+
+#if HAVE_UWP
+// Missing from MinGW headers.
+WINBASEAPI HANDLE WINAPI FindFirstFileExW(LPCWSTR lpFileName,
+ FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData,
+ FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags);
+#endif
+
+static wchar_t *talloc_wcsdup(void *ctx, const wchar_t *wcs)
+{
+ size_t len = (wcslen(wcs) + 1) * sizeof(wchar_t);
+ return talloc_memdup(ctx, (void*)wcs, len);
+}
+
+static int compare_wcscoll(const void *v1, const void *v2)
+{
+ wchar_t * const* p1 = v1;
+ wchar_t * const* p2 = v2;
+ return wcscoll(*p1, *p2);
+}
+
+static bool exists(const char *filename)
+{
+ wchar_t *wfilename = mp_from_utf8(NULL, filename);
+ bool result = GetFileAttributesW(wfilename) != INVALID_FILE_ATTRIBUTES;
+ talloc_free(wfilename);
+ return result;
+}
+
+int mp_glob(const char *restrict pattern, int flags,
+ int (*errfunc)(const char*, int), mp_glob_t *restrict pglob)
+{
+ // This glob implementation never calls errfunc and doesn't understand any
+ // flags. These features are currently unused in mpv, however if new code
+ // were to use these them, it would probably break on Windows.
+
+ unsigned dirlen = 0;
+ bool wildcards = false;
+
+ // Check for drive relative paths eg. "C:*.flac"
+ if (pattern[0] != '\0' && pattern[1] == ':')
+ dirlen = 2;
+
+ // Split the directory and filename. All files returned by FindFirstFile
+ // will be in this directory. Also check the filename for wildcards.
+ for (unsigned i = 0; pattern[i]; i ++) {
+ if (pattern[i] == '?' || pattern[i] == '*')
+ wildcards = true;
+
+ if (pattern[i] == '\\' || pattern[i] == '/') {
+ dirlen = i + 1;
+ wildcards = false;
+ }
+ }
+
+ // FindFirstFile is unreliable with certain input (it returns weird results
+ // with paths like "." and "..", and presumably others.) If there are no
+ // wildcards in the filename, don't call it, just check if the file exists.
+ // The CRT globbing code does this too.
+ if (!wildcards) {
+ if (!exists(pattern)) {
+ pglob->gl_pathc = 0;
+ return GLOB_NOMATCH;
+ }
+
+ pglob->ctx = talloc_new(NULL);
+ pglob->gl_pathc = 1;
+ pglob->gl_pathv = talloc_array_ptrtype(pglob->ctx, pglob->gl_pathv, 2);
+ pglob->gl_pathv[0] = talloc_strdup(pglob->ctx, pattern);
+ pglob->gl_pathv[1] = NULL;
+ return 0;
+ }
+
+ wchar_t *wpattern = mp_from_utf8(NULL, pattern);
+ WIN32_FIND_DATAW data;
+ HANDLE find = FindFirstFileExW(wpattern, FindExInfoBasic, &data, FindExSearchNameMatch, NULL, 0);
+ talloc_free(wpattern);
+
+ // Assume an error means there were no matches. mpv doesn't check for
+ // glob() errors, so this should be fine for now.
+ if (find == INVALID_HANDLE_VALUE) {
+ pglob->gl_pathc = 0;
+ return GLOB_NOMATCH;
+ }
+
+ size_t pathc = 0;
+ void *tmp = talloc_new(NULL);
+ wchar_t **wnamev = NULL;
+
+ // Read a list of filenames. Unlike glob(), FindFirstFile doesn't return
+ // the full path, since all files are relative to the directory specified
+ // in the pattern.
+ do {
+ if (!wcscmp(data.cFileName, L".") || !wcscmp(data.cFileName, L".."))
+ continue;
+
+ wchar_t *wname = talloc_wcsdup(tmp, data.cFileName);
+ MP_TARRAY_APPEND(tmp, wnamev, pathc, wname);
+ } while (FindNextFileW(find, &data));
+ FindClose(find);
+
+ if (!wnamev) {
+ talloc_free(tmp);
+ pglob->gl_pathc = 0;
+ return GLOB_NOMATCH;
+ }
+
+ // POSIX glob() is supposed to sort paths according to LC_COLLATE.
+ // FindFirstFile just returns paths in the order they are read from the
+ // directory, so sort them manually with wcscoll.
+ qsort(wnamev, pathc, sizeof(wchar_t*), compare_wcscoll);
+
+ pglob->ctx = talloc_new(NULL);
+ pglob->gl_pathc = pathc;
+ pglob->gl_pathv = talloc_array_ptrtype(pglob->ctx, pglob->gl_pathv,
+ pathc + 1);
+
+ // Now convert all filenames to UTF-8 (they had to be in UTF-16 for
+ // sorting) and prepend the directory
+ for (unsigned i = 0; i < pathc; i ++) {
+ int namelen = WideCharToMultiByte(CP_UTF8, 0, wnamev[i], -1, NULL, 0,
+ NULL, NULL);
+ char *path = talloc_array(pglob->ctx, char, namelen + dirlen);
+
+ memcpy(path, pattern, dirlen);
+ WideCharToMultiByte(CP_UTF8, 0, wnamev[i], -1, path + dirlen,
+ namelen, NULL, NULL);
+ pglob->gl_pathv[i] = path;
+ }
+
+ // gl_pathv must be null terminated
+ pglob->gl_pathv[pathc] = NULL;
+ talloc_free(tmp);
+ return 0;
+}
+
+void mp_globfree(mp_glob_t *pglob)
+{
+ talloc_free(pglob->ctx);
+}
diff --git a/osdep/io.c b/osdep/io.c
new file mode 100644
index 0000000..bdf79f8
--- /dev/null
+++ b/osdep/io.c
@@ -0,0 +1,904 @@
+/*
+ * unicode/utf-8 I/O helpers and wrappers for Windows
+ *
+ * Contains parts based on libav code (http://libav.org).
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <limits.h>
+#include <unistd.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "misc/random.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+
+#if HAVE_UWP
+// Missing from MinGW headers.
+#include <windows.h>
+WINBASEAPI UINT WINAPI GetTempFileNameW(LPCWSTR lpPathName, LPCWSTR lpPrefixString,
+ UINT uUnique, LPWSTR lpTempFileName);
+WINBASEAPI DWORD WINAPI GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer);
+WINBASEAPI DWORD WINAPI GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength,
+ LPWSTR lpBuffer, LPWSTR *lpFilePart);
+#endif
+
+// Set the CLOEXEC flag on the given fd.
+// On error, false is returned (and errno set).
+bool mp_set_cloexec(int fd)
+{
+#if defined(F_SETFD)
+ if (fd >= 0) {
+ int flags = fcntl(fd, F_GETFD);
+ if (flags == -1)
+ return false;
+ if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1)
+ return false;
+ }
+#endif
+ return true;
+}
+
+#ifdef __MINGW32__
+int mp_make_cloexec_pipe(int pipes[2])
+{
+ pipes[0] = pipes[1] = -1;
+ return -1;
+}
+#else
+int mp_make_cloexec_pipe(int pipes[2])
+{
+ if (pipe(pipes) != 0) {
+ pipes[0] = pipes[1] = -1;
+ return -1;
+ }
+
+ for (int i = 0; i < 2; i++)
+ mp_set_cloexec(pipes[i]);
+ return 0;
+}
+#endif
+
+#ifdef __MINGW32__
+int mp_make_wakeup_pipe(int pipes[2])
+{
+ return mp_make_cloexec_pipe(pipes);
+}
+#else
+// create a pipe, and set it to non-blocking (and also set FD_CLOEXEC)
+int mp_make_wakeup_pipe(int pipes[2])
+{
+ if (mp_make_cloexec_pipe(pipes) < 0)
+ return -1;
+
+ for (int i = 0; i < 2; i++) {
+ int val = fcntl(pipes[i], F_GETFL) | O_NONBLOCK;
+ fcntl(pipes[i], F_SETFL, val);
+ }
+ return 0;
+}
+#endif
+
+void mp_flush_wakeup_pipe(int pipe_end)
+{
+#ifndef __MINGW32__
+ char buf[100];
+ (void)read(pipe_end, buf, sizeof(buf));
+#endif
+}
+
+#ifdef _WIN32
+
+#include <windows.h>
+#include <wchar.h>
+#include <stdio.h>
+#include <stddef.h>
+
+//copied and modified from libav
+//http://git.libav.org/?p=libav.git;a=blob;f=libavformat/os_support.c;h=a0fcd6c9ba2be4b0dbcc476f6c53587345cc1152;hb=HEADl30
+
+wchar_t *mp_from_utf8(void *talloc_ctx, const char *s)
+{
+ int count = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
+ if (count <= 0)
+ abort();
+ wchar_t *ret = talloc_array(talloc_ctx, wchar_t, count);
+ MultiByteToWideChar(CP_UTF8, 0, s, -1, ret, count);
+ return ret;
+}
+
+char *mp_to_utf8(void *talloc_ctx, const wchar_t *s)
+{
+ int count = WideCharToMultiByte(CP_UTF8, 0, s, -1, NULL, 0, NULL, NULL);
+ if (count <= 0)
+ abort();
+ char *ret = talloc_array(talloc_ctx, char, count);
+ WideCharToMultiByte(CP_UTF8, 0, s, -1, ret, count, NULL, NULL);
+ return ret;
+}
+
+#endif // _WIN32
+
+#ifdef __MINGW32__
+
+#include <io.h>
+#include <fcntl.h>
+#include "osdep/threads.h"
+
+static void set_errno_from_lasterror(void)
+{
+ // This just handles the error codes expected from CreateFile at the moment
+ switch (GetLastError()) {
+ case ERROR_FILE_NOT_FOUND:
+ errno = ENOENT;
+ break;
+ case ERROR_SHARING_VIOLATION:
+ case ERROR_ACCESS_DENIED:
+ errno = EACCES;
+ break;
+ case ERROR_FILE_EXISTS:
+ case ERROR_ALREADY_EXISTS:
+ errno = EEXIST;
+ break;
+ case ERROR_PIPE_BUSY:
+ errno = EAGAIN;
+ break;
+ default:
+ errno = EINVAL;
+ break;
+ }
+}
+
+static time_t filetime_to_unix_time(int64_t wintime)
+{
+ static const int64_t hns_per_second = 10000000ll;
+ static const int64_t win_to_unix_epoch = 11644473600ll;
+ return wintime / hns_per_second - win_to_unix_epoch;
+}
+
+static bool get_file_ids_win8(HANDLE h, dev_t *dev, ino_t *ino)
+{
+ FILE_ID_INFO ii;
+ if (!GetFileInformationByHandleEx(h, FileIdInfo, &ii, sizeof(ii)))
+ return false;
+ *dev = ii.VolumeSerialNumber;
+ // The definition of FILE_ID_128 differs between mingw-w64 and the Windows
+ // SDK, but we can ignore that by just memcpying it. This will also
+ // truncate the file ID on 32-bit Windows, which doesn't support __int128.
+ // 128-bit file IDs are only used for ReFS, so that should be okay.
+ assert(sizeof(*ino) <= sizeof(ii.FileId));
+ memcpy(ino, &ii.FileId, sizeof(*ino));
+ return true;
+}
+
+#if HAVE_UWP
+static bool get_file_ids(HANDLE h, dev_t *dev, ino_t *ino)
+{
+ return false;
+}
+#else
+static bool get_file_ids(HANDLE h, dev_t *dev, ino_t *ino)
+{
+ // GetFileInformationByHandle works on FAT partitions and Windows 7, but
+ // doesn't work in UWP and can produce non-unique IDs on ReFS
+ BY_HANDLE_FILE_INFORMATION bhfi;
+ if (!GetFileInformationByHandle(h, &bhfi))
+ return false;
+ *dev = bhfi.dwVolumeSerialNumber;
+ *ino = ((ino_t)bhfi.nFileIndexHigh << 32) | bhfi.nFileIndexLow;
+ return true;
+}
+#endif
+
+// Like fstat(), but with a Windows HANDLE
+static int hstat(HANDLE h, struct mp_stat *buf)
+{
+ // Handle special (or unknown) file types first
+ switch (GetFileType(h) & ~FILE_TYPE_REMOTE) {
+ case FILE_TYPE_PIPE:
+ *buf = (struct mp_stat){ .st_nlink = 1, .st_mode = _S_IFIFO | 0644 };
+ return 0;
+ case FILE_TYPE_CHAR: // character device
+ *buf = (struct mp_stat){ .st_nlink = 1, .st_mode = _S_IFCHR | 0644 };
+ return 0;
+ case FILE_TYPE_UNKNOWN:
+ errno = EBADF;
+ return -1;
+ }
+
+ struct mp_stat st = { 0 };
+
+ FILE_BASIC_INFO bi;
+ if (!GetFileInformationByHandleEx(h, FileBasicInfo, &bi, sizeof(bi))) {
+ errno = EBADF;
+ return -1;
+ }
+ st.st_atime = filetime_to_unix_time(bi.LastAccessTime.QuadPart);
+ st.st_mtime = filetime_to_unix_time(bi.LastWriteTime.QuadPart);
+ st.st_ctime = filetime_to_unix_time(bi.ChangeTime.QuadPart);
+
+ FILE_STANDARD_INFO si;
+ if (!GetFileInformationByHandleEx(h, FileStandardInfo, &si, sizeof(si))) {
+ errno = EBADF;
+ return -1;
+ }
+ st.st_nlink = si.NumberOfLinks;
+
+ // Here we pretend Windows has POSIX permissions by pretending all
+ // directories are 755 and regular files are 644
+ if (si.Directory) {
+ st.st_mode |= _S_IFDIR | 0755;
+ } else {
+ st.st_mode |= _S_IFREG | 0644;
+ st.st_size = si.EndOfFile.QuadPart;
+ }
+
+ if (!get_file_ids_win8(h, &st.st_dev, &st.st_ino)) {
+ // Fall back to the Windows 7 method (also used for FAT in Win8)
+ if (!get_file_ids(h, &st.st_dev, &st.st_ino)) {
+ errno = EBADF;
+ return -1;
+ }
+ }
+
+ *buf = st;
+ return 0;
+}
+
+int mp_stat(const char *path, struct mp_stat *buf)
+{
+ wchar_t *wpath = mp_from_utf8(NULL, path);
+ HANDLE h = CreateFileW(wpath, FILE_READ_ATTRIBUTES,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | SECURITY_SQOS_PRESENT |
+ SECURITY_IDENTIFICATION, NULL);
+ talloc_free(wpath);
+ if (h == INVALID_HANDLE_VALUE) {
+ set_errno_from_lasterror();
+ return -1;
+ }
+
+ int ret = hstat(h, buf);
+ CloseHandle(h);
+ return ret;
+}
+
+int mp_fstat(int fd, struct mp_stat *buf)
+{
+ HANDLE h = (HANDLE)_get_osfhandle(fd);
+ if (h == INVALID_HANDLE_VALUE) {
+ errno = EBADF;
+ return -1;
+ }
+ // Use mpv's hstat() function rather than MSVCRT's fstat() because mpv's
+ // supports directories and device/inode numbers.
+ return hstat(h, buf);
+}
+
+#if HAVE_UWP
+static int mp_vfprintf(FILE *stream, const char *format, va_list args)
+{
+ return vfprintf(stream, format, args);
+}
+#else
+static int mp_check_console(HANDLE wstream)
+{
+ if (wstream != INVALID_HANDLE_VALUE) {
+ unsigned int filetype = GetFileType(wstream);
+
+ if (!((filetype == FILE_TYPE_UNKNOWN) &&
+ (GetLastError() != ERROR_SUCCESS)))
+ {
+ filetype &= ~(FILE_TYPE_REMOTE);
+
+ if (filetype == FILE_TYPE_CHAR) {
+ DWORD ConsoleMode;
+ int ret = GetConsoleMode(wstream, &ConsoleMode);
+
+ if (!(!ret && (GetLastError() == ERROR_INVALID_HANDLE))) {
+ // This seems to be a console
+ return 1;
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int mp_vfprintf(FILE *stream, const char *format, va_list args)
+{
+ int done = 0;
+
+ HANDLE wstream = INVALID_HANDLE_VALUE;
+
+ if (stream == stdout || stream == stderr) {
+ wstream = GetStdHandle(stream == stdout ?
+ STD_OUTPUT_HANDLE : STD_ERROR_HANDLE);
+ }
+
+ if (mp_check_console(wstream)) {
+ size_t len = vsnprintf(NULL, 0, format, args) + 1;
+ char *buf = talloc_array(NULL, char, len);
+
+ if (buf) {
+ done = vsnprintf(buf, len, format, args);
+ mp_write_console_ansi(wstream, buf);
+ }
+ talloc_free(buf);
+ } else {
+ done = vfprintf(stream, format, args);
+ }
+
+ return done;
+}
+#endif
+
+int mp_fprintf(FILE *stream, const char *format, ...)
+{
+ int res;
+ va_list args;
+ va_start(args, format);
+ res = mp_vfprintf(stream, format, args);
+ va_end(args);
+ return res;
+}
+
+int mp_printf(const char *format, ...)
+{
+ int res;
+ va_list args;
+ va_start(args, format);
+ res = mp_vfprintf(stdout, format, args);
+ va_end(args);
+ return res;
+}
+
+int mp_open(const char *filename, int oflag, ...)
+{
+ // Always use all share modes, which is useful for opening files that are
+ // open in other processes, and also more POSIX-like
+ static const DWORD share =
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
+ // Setting FILE_APPEND_DATA and avoiding GENERIC_WRITE/FILE_WRITE_DATA
+ // will make the file handle use atomic append behavior
+ static const DWORD append =
+ FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA;
+
+ DWORD access = 0;
+ DWORD disposition = 0;
+ DWORD flags = 0;
+
+ switch (oflag & (_O_RDONLY | _O_RDWR | _O_WRONLY | _O_APPEND)) {
+ case _O_RDONLY:
+ access = GENERIC_READ;
+ flags |= FILE_FLAG_BACKUP_SEMANTICS; // For opening directories
+ break;
+ case _O_RDWR:
+ access = GENERIC_READ | GENERIC_WRITE;
+ break;
+ case _O_RDWR | _O_APPEND:
+ case _O_RDONLY | _O_APPEND:
+ access = GENERIC_READ | append;
+ break;
+ case _O_WRONLY:
+ access = GENERIC_WRITE;
+ break;
+ case _O_WRONLY | _O_APPEND:
+ access = append;
+ break;
+ default:
+ errno = EINVAL;
+ return -1;
+ }
+
+ switch (oflag & (_O_CREAT | _O_EXCL | _O_TRUNC)) {
+ case 0:
+ case _O_EXCL: // Like MSVCRT, ignore invalid use of _O_EXCL
+ disposition = OPEN_EXISTING;
+ break;
+ case _O_TRUNC:
+ case _O_TRUNC | _O_EXCL:
+ disposition = TRUNCATE_EXISTING;
+ break;
+ case _O_CREAT:
+ disposition = OPEN_ALWAYS;
+ flags |= FILE_ATTRIBUTE_NORMAL;
+ break;
+ case _O_CREAT | _O_TRUNC:
+ disposition = CREATE_ALWAYS;
+ break;
+ case _O_CREAT | _O_EXCL:
+ case _O_CREAT | _O_EXCL | _O_TRUNC:
+ disposition = CREATE_NEW;
+ flags |= FILE_ATTRIBUTE_NORMAL;
+ break;
+ }
+
+ // Opening a named pipe as a file can allow the pipe server to impersonate
+ // mpv's process, which could be a security issue. Set SQOS flags, so pipe
+ // servers can only identify the mpv process, not impersonate it.
+ if (disposition != CREATE_NEW)
+ flags |= SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION;
+
+ // Keep the same semantics for some MSVCRT-specific flags
+ if (oflag & _O_TEMPORARY) {
+ flags |= FILE_FLAG_DELETE_ON_CLOSE;
+ access |= DELETE;
+ }
+ if (oflag & _O_SHORT_LIVED)
+ flags |= FILE_ATTRIBUTE_TEMPORARY;
+ if (oflag & _O_SEQUENTIAL) {
+ flags |= FILE_FLAG_SEQUENTIAL_SCAN;
+ } else if (oflag & _O_RANDOM) {
+ flags |= FILE_FLAG_RANDOM_ACCESS;
+ }
+
+ // Open the Windows file handle
+ wchar_t *wpath = mp_from_utf8(NULL, filename);
+ HANDLE h = CreateFileW(wpath, access, share, NULL, disposition, flags, NULL);
+ talloc_free(wpath);
+ if (h == INVALID_HANDLE_VALUE) {
+ set_errno_from_lasterror();
+ return -1;
+ }
+
+ // Map the Windows file handle to a CRT file descriptor. Note: MSVCRT only
+ // cares about the following oflags.
+ oflag &= _O_APPEND | _O_RDONLY | _O_RDWR | _O_WRONLY;
+ oflag |= _O_NOINHERIT; // We never create inheritable handles
+ int fd = _open_osfhandle((intptr_t)h, oflag);
+ if (fd < 0) {
+ CloseHandle(h);
+ return -1;
+ }
+
+ return fd;
+}
+
+int mp_creat(const char *filename, int mode)
+{
+ return mp_open(filename, _O_CREAT | _O_WRONLY | _O_TRUNC, mode);
+}
+
+int mp_rename(const char *oldpath, const char *newpath)
+{
+ wchar_t *woldpath = mp_from_utf8(NULL, oldpath),
+ *wnewpath = mp_from_utf8(NULL, newpath);
+ BOOL ok = MoveFileExW(woldpath, wnewpath, MOVEFILE_REPLACE_EXISTING);
+ talloc_free(woldpath);
+ talloc_free(wnewpath);
+ if (!ok) {
+ set_errno_from_lasterror();
+ return -1;
+ }
+ return 0;
+}
+
+FILE *mp_fopen(const char *filename, const char *mode)
+{
+ if (!mode[0]) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ int rwmode;
+ int oflags = 0;
+ switch (mode[0]) {
+ case 'r':
+ rwmode = _O_RDONLY;
+ break;
+ case 'w':
+ rwmode = _O_WRONLY;
+ oflags |= _O_CREAT | _O_TRUNC;
+ break;
+ case 'a':
+ rwmode = _O_WRONLY;
+ oflags |= _O_CREAT | _O_APPEND;
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ // Parse extra mode flags
+ for (const char *pos = mode + 1; *pos; pos++) {
+ switch (*pos) {
+ case '+': rwmode = _O_RDWR; break;
+ case 'x': oflags |= _O_EXCL; break;
+ // Ignore unknown flags (glibc does too)
+ default: break;
+ }
+ }
+
+ // Open a CRT file descriptor
+ int fd = mp_open(filename, rwmode | oflags);
+ if (fd < 0)
+ return NULL;
+
+ // Add 'b' to the mode so the CRT knows the file is opened in binary mode
+ char bmode[] = { mode[0], 'b', rwmode == _O_RDWR ? '+' : '\0', '\0' };
+ FILE *fp = fdopen(fd, bmode);
+ if (!fp) {
+ close(fd);
+ return NULL;
+ }
+
+ return fp;
+}
+
+// Windows' MAX_PATH/PATH_MAX/FILENAME_MAX is fixed to 260, but this limit
+// applies to unicode paths encoded with wchar_t (2 bytes on Windows). The UTF-8
+// version could end up bigger in memory. In the worst case each wchar_t is
+// encoded to 3 bytes in UTF-8, so in the worst case we have:
+// wcslen(wpath) * 3 <= strlen(utf8path)
+// Thus we need MP_PATH_MAX as the UTF-8/char version of PATH_MAX.
+// Also make sure there's free space for the terminating \0.
+// (For codepoints encoded as UTF-16 surrogate pairs, UTF-8 has the same length.)
+#define MP_PATH_MAX (FILENAME_MAX * 3 + 1)
+
+struct mp_dir {
+ DIR crap; // must be first member
+ _WDIR *wdir;
+ union {
+ struct dirent dirent;
+ // dirent has space only for FILENAME_MAX bytes. _wdirent has space for
+ // FILENAME_MAX wchar_t, which might end up bigger as UTF-8 in some
+ // cases. Guarantee we can always hold _wdirent.d_name converted to
+ // UTF-8 (see MP_PATH_MAX).
+ // This works because dirent.d_name is the last member of dirent.
+ char space[MP_PATH_MAX];
+ };
+};
+
+DIR* mp_opendir(const char *path)
+{
+ wchar_t *wpath = mp_from_utf8(NULL, path);
+ _WDIR *wdir = _wopendir(wpath);
+ talloc_free(wpath);
+ if (!wdir)
+ return NULL;
+ struct mp_dir *mpdir = talloc(NULL, struct mp_dir);
+ // DIR is supposed to be opaque, but unfortunately the MinGW headers still
+ // define it. Make sure nobody tries to use it.
+ memset(&mpdir->crap, 0xCD, sizeof(mpdir->crap));
+ mpdir->wdir = wdir;
+ return (DIR*)mpdir;
+}
+
+struct dirent* mp_readdir(DIR *dir)
+{
+ struct mp_dir *mpdir = (struct mp_dir*)dir;
+ struct _wdirent *wdirent = _wreaddir(mpdir->wdir);
+ if (!wdirent)
+ return NULL;
+ size_t buffersize = sizeof(mpdir->space) - offsetof(struct dirent, d_name);
+ WideCharToMultiByte(CP_UTF8, 0, wdirent->d_name, -1, mpdir->dirent.d_name,
+ buffersize, NULL, NULL);
+ mpdir->dirent.d_ino = 0;
+ mpdir->dirent.d_reclen = 0;
+ mpdir->dirent.d_namlen = strlen(mpdir->dirent.d_name);
+ return &mpdir->dirent;
+}
+
+int mp_closedir(DIR *dir)
+{
+ struct mp_dir *mpdir = (struct mp_dir*)dir;
+ int res = _wclosedir(mpdir->wdir);
+ talloc_free(mpdir);
+ return res;
+}
+
+int mp_mkdir(const char *path, int mode)
+{
+ wchar_t *wpath = mp_from_utf8(NULL, path);
+ int res = _wmkdir(wpath);
+ talloc_free(wpath);
+ return res;
+}
+
+char *mp_win32_getcwd(char *buf, size_t size)
+{
+ if (size >= SIZE_MAX / 3 - 1) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ size_t wbuffer = size * 3 + 1;
+ wchar_t *wres = talloc_array(NULL, wchar_t, wbuffer);
+ DWORD wlen = GetFullPathNameW(L".", wbuffer, wres, NULL);
+ if (wlen >= wbuffer || wlen == 0) {
+ talloc_free(wres);
+ errno = wlen ? ERANGE : ENOENT;
+ return NULL;
+ }
+ char *t = mp_to_utf8(NULL, wres);
+ talloc_free(wres);
+ size_t st = strlen(t);
+ if (st >= size) {
+ talloc_free(t);
+ errno = ERANGE;
+ return NULL;
+ }
+ memcpy(buf, t, st + 1);
+ talloc_free(t);
+ return buf;
+}
+
+static char **utf8_environ;
+static void *utf8_environ_ctx;
+
+static void free_env(void)
+{
+ talloc_free(utf8_environ_ctx);
+ utf8_environ_ctx = NULL;
+ utf8_environ = NULL;
+}
+
+// Note: UNIX getenv() returns static strings, and we try to do the same. Since
+// using putenv() is not multithreading safe, we don't expect env vars to change
+// at runtime, and converting/allocating them in advance is ok.
+static void init_getenv(void)
+{
+#if !HAVE_UWP
+ wchar_t *wenv = GetEnvironmentStringsW();
+ if (!wenv)
+ return;
+ utf8_environ_ctx = talloc_new(NULL);
+ int num_env = 0;
+ while (1) {
+ size_t len = wcslen(wenv);
+ if (!len)
+ break;
+ char *s = mp_to_utf8(utf8_environ_ctx, wenv);
+ MP_TARRAY_APPEND(utf8_environ_ctx, utf8_environ, num_env, s);
+ wenv += len + 1;
+ }
+ MP_TARRAY_APPEND(utf8_environ_ctx, utf8_environ, num_env, NULL);
+ // Avoid showing up in leak detectors etc.
+ atexit(free_env);
+#endif
+}
+
+char *mp_getenv(const char *name)
+{
+ static mp_once once_init_getenv = MP_STATIC_ONCE_INITIALIZER;
+ mp_exec_once(&once_init_getenv, init_getenv);
+ // Copied from musl, http://git.musl-libc.org/cgit/musl/tree/COPYRIGHT
+ // Copyright © 2005-2013 Rich Felker, standard MIT license
+ int i;
+ size_t l = strlen(name);
+ if (!utf8_environ || !*name || strchr(name, '=')) return NULL;
+ for (i=0; utf8_environ[i] && (strncmp(name, utf8_environ[i], l)
+ || utf8_environ[i][l] != '='); i++) {}
+ if (utf8_environ[i]) return utf8_environ[i] + l+1;
+ return NULL;
+}
+
+char ***mp_penviron(void)
+{
+ mp_getenv(""); // ensure init
+ return &utf8_environ; // `environ' should be an l-value
+}
+
+off_t mp_lseek(int fd, off_t offset, int whence)
+{
+ HANDLE h = (HANDLE)_get_osfhandle(fd);
+ if (h != INVALID_HANDLE_VALUE && GetFileType(h) != FILE_TYPE_DISK) {
+ errno = ESPIPE;
+ return (off_t)-1;
+ }
+ return _lseeki64(fd, offset, whence);
+}
+
+_Thread_local
+static struct {
+ DWORD errcode;
+ char *errstring;
+} mp_dl_result = {
+ .errcode = 0,
+ .errstring = NULL
+};
+
+static void mp_dl_free(void)
+{
+ if (mp_dl_result.errstring != NULL) {
+ talloc_free(mp_dl_result.errstring);
+ }
+}
+
+static void mp_dl_init(void)
+{
+ atexit(mp_dl_free);
+}
+
+void *mp_dlopen(const char *filename, int flag)
+{
+ wchar_t *wfilename = mp_from_utf8(NULL, filename);
+ HMODULE lib = LoadLibraryW(wfilename);
+ talloc_free(wfilename);
+ mp_dl_result.errcode = GetLastError();
+ return (void *)lib;
+}
+
+void *mp_dlsym(void *handle, const char *symbol)
+{
+ FARPROC addr = GetProcAddress((HMODULE)handle, symbol);
+ mp_dl_result.errcode = GetLastError();
+ return (void *)addr;
+}
+
+char *mp_dlerror(void)
+{
+ static mp_once once_init_dlerror = MP_STATIC_ONCE_INITIALIZER;
+ mp_exec_once(&once_init_dlerror, mp_dl_init);
+ mp_dl_free();
+
+ if (mp_dl_result.errcode == 0)
+ return NULL;
+
+ // convert error code to a string message
+ LPWSTR werrstring = NULL;
+ FormatMessageW(
+ FORMAT_MESSAGE_FROM_SYSTEM
+ | FORMAT_MESSAGE_IGNORE_INSERTS
+ | FORMAT_MESSAGE_ALLOCATE_BUFFER,
+ NULL,
+ mp_dl_result.errcode,
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
+ (LPWSTR) &werrstring,
+ 0,
+ NULL);
+ mp_dl_result.errcode = 0;
+
+ if (werrstring) {
+ mp_dl_result.errstring = mp_to_utf8(NULL, werrstring);
+ LocalFree(werrstring);
+ }
+
+ return mp_dl_result.errstring == NULL
+ ? "unknown error"
+ : mp_dl_result.errstring;
+}
+
+#if HAVE_UWP
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
+{
+ errno = ENOSYS;
+ return MAP_FAILED;
+}
+
+int munmap(void *addr, size_t length)
+{
+ errno = ENOSYS;
+ return -1;
+}
+
+int msync(void *addr, size_t length, int flags)
+{
+ errno = ENOSYS;
+ return -1;
+}
+#else
+// Limited mmap() wrapper, inspired by:
+// http://code.google.com/p/mman-win32/source/browse/trunk/mman.c
+
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
+{
+ assert(addr == NULL); // not implemented
+ assert(flags == MAP_SHARED); // not implemented
+
+ HANDLE osf = (HANDLE)_get_osfhandle(fd);
+ if (!osf) {
+ errno = EBADF;
+ return MAP_FAILED;
+ }
+
+ DWORD protect = 0;
+ DWORD access = 0;
+ if (prot & PROT_WRITE) {
+ protect = PAGE_READWRITE;
+ access = FILE_MAP_WRITE;
+ } else if (prot & PROT_READ) {
+ protect = PAGE_READONLY;
+ access = FILE_MAP_READ;
+ }
+
+ DWORD l_low = (uint32_t)length;
+ DWORD l_high = ((uint64_t)length) >> 32;
+ HANDLE map = CreateFileMapping(osf, NULL, protect, l_high, l_low, NULL);
+
+ if (!map) {
+ errno = EACCES; // something random
+ return MAP_FAILED;
+ }
+
+ DWORD o_low = (uint32_t)offset;
+ DWORD o_high = ((uint64_t)offset) >> 32;
+ void *p = MapViewOfFile(map, access, o_high, o_low, length);
+
+ CloseHandle(map);
+
+ if (!p) {
+ errno = EINVAL;
+ return MAP_FAILED;
+ }
+ return p;
+}
+
+int munmap(void *addr, size_t length)
+{
+ UnmapViewOfFile(addr);
+ return 0;
+}
+
+int msync(void *addr, size_t length, int flags)
+{
+ FlushViewOfFile(addr, length);
+ return 0;
+}
+#endif
+
+locale_t newlocale(int category, const char *locale, locale_t base)
+{
+ return (locale_t)1;
+}
+
+locale_t uselocale(locale_t locobj)
+{
+ return (locale_t)1;
+}
+
+void freelocale(locale_t locobj)
+{
+}
+
+#endif // __MINGW32__
+
+int mp_mkostemps(char *template, int suffixlen, int flags)
+{
+ size_t len = strlen(template);
+ char *t = len >= 6 + suffixlen ? &template[len - (6 + suffixlen)] : NULL;
+ if (!t || strncmp(t, "XXXXXX", 6) != 0) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ for (size_t fuckshit = 0; fuckshit < UINT32_MAX; fuckshit++) {
+ // Using a random value may make it require fewer iterations (even if
+ // not truly random; just a counter would be sufficient).
+ size_t fuckmess = mp_rand_next();
+ char crap[7] = "";
+ snprintf(crap, sizeof(crap), "%06zx", fuckmess);
+ memcpy(t, crap, 6);
+
+ int res = open(template, O_RDWR | O_CREAT | O_EXCL | flags, 0600);
+ if (res >= 0 || errno != EEXIST)
+ return res;
+ }
+
+ errno = EEXIST;
+ return -1;
+}
diff --git a/osdep/io.h b/osdep/io.h
new file mode 100644
index 0000000..db711fb
--- /dev/null
+++ b/osdep/io.h
@@ -0,0 +1,232 @@
+/*
+ * unicode/utf-8 I/O helpers and wrappers for Windows
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_OSDEP_IO
+#define MPLAYER_OSDEP_IO
+
+#include "config.h"
+#include <stdbool.h>
+#include <stdint.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <locale.h>
+
+#if HAVE_GLOB_POSIX
+#include <glob.h>
+#endif
+
+#if HAVE_ANDROID
+# include <unistd.h>
+# include <stdio.h>
+
+// replace lseek with the 64bit variant
+#ifdef lseek
+# undef lseek
+#endif
+#define lseek(f,p,w) lseek64((f), (p), (w))
+
+// replace possible fseeko with a
+// lseek64 based solution.
+#ifdef fseeko
+# undef fseeko
+#endif
+static inline int mp_fseeko(FILE* fp, off64_t offset, int whence) {
+ int ret = -1;
+ if ((ret = fflush(fp)) != 0) {
+ return ret;
+ }
+
+ return lseek64(fileno(fp), offset, whence) >= 0 ? 0 : -1;
+}
+#define fseeko(f,p,w) mp_fseeko((f), (p), (w))
+
+#endif // HAVE_ANDROID
+
+#ifndef O_BINARY
+#define O_BINARY 0
+#endif
+
+// This is in POSIX.1-2008, but support outside of Linux is scarce.
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+#ifndef FD_CLOEXEC
+#define FD_CLOEXEC 0
+#endif
+
+bool mp_set_cloexec(int fd);
+int mp_make_cloexec_pipe(int pipes[2]);
+int mp_make_wakeup_pipe(int pipes[2]);
+void mp_flush_wakeup_pipe(int pipe_end);
+
+#ifdef _WIN32
+#include <wchar.h>
+wchar_t *mp_from_utf8(void *talloc_ctx, const char *s);
+char *mp_to_utf8(void *talloc_ctx, const wchar_t *s);
+#endif
+
+#ifdef __CYGWIN__
+#include <io.h>
+#endif
+
+#ifdef __MINGW32__
+
+#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+int mp_printf(const char *format, ...);
+int mp_fprintf(FILE *stream, const char *format, ...);
+int mp_open(const char *filename, int oflag, ...);
+int mp_creat(const char *filename, int mode);
+int mp_rename(const char *oldpath, const char *newpath);
+FILE *mp_fopen(const char *filename, const char *mode);
+DIR *mp_opendir(const char *path);
+struct dirent *mp_readdir(DIR *dir);
+int mp_closedir(DIR *dir);
+int mp_mkdir(const char *path, int mode);
+char *mp_win32_getcwd(char *buf, size_t size);
+char *mp_getenv(const char *name);
+
+#ifdef environ /* mingw defines it as _environ */
+#undef environ
+#endif
+#define environ (*mp_penviron()) /* ensure initialization and l-value */
+char ***mp_penviron(void);
+
+off_t mp_lseek(int fd, off_t offset, int whence);
+void *mp_dlopen(const char *filename, int flag);
+void *mp_dlsym(void *handle, const char *symbol);
+char *mp_dlerror(void);
+
+// mp_stat types. MSVCRT's dev_t and ino_t are way too short to be unique.
+typedef uint64_t mp_dev_t_;
+#ifdef _WIN64
+typedef unsigned __int128 mp_ino_t_;
+#else
+// 32-bit Windows doesn't have a __int128-type, which means ReFS file IDs will
+// be truncated and might collide. This is probably not a problem because ReFS
+// is not available in consumer versions of Windows.
+typedef uint64_t mp_ino_t_;
+#endif
+#define dev_t mp_dev_t_
+#define ino_t mp_ino_t_
+
+// mp_stat uses a different structure to MSVCRT, with 64-bit inodes
+struct mp_stat {
+ dev_t st_dev;
+ ino_t st_ino;
+ unsigned short st_mode;
+ unsigned int st_nlink;
+ short st_uid;
+ short st_gid;
+ dev_t st_rdev;
+ int64_t st_size;
+ time_t st_atime;
+ time_t st_mtime;
+ time_t st_ctime;
+};
+
+int mp_stat(const char *path, struct mp_stat *buf);
+int mp_fstat(int fd, struct mp_stat *buf);
+
+typedef struct {
+ size_t gl_pathc;
+ char **gl_pathv;
+ size_t gl_offs;
+ void *ctx;
+} mp_glob_t;
+
+// glob-win.c
+int mp_glob(const char *restrict pattern, int flags,
+ int (*errfunc)(const char*, int), mp_glob_t *restrict pglob);
+void mp_globfree(mp_glob_t *pglob);
+
+#define printf(...) mp_printf(__VA_ARGS__)
+#define fprintf(...) mp_fprintf(__VA_ARGS__)
+#define open(...) mp_open(__VA_ARGS__)
+#define creat(...) mp_creat(__VA_ARGS__)
+#define rename(...) mp_rename(__VA_ARGS__)
+#define fopen(...) mp_fopen(__VA_ARGS__)
+#define opendir(...) mp_opendir(__VA_ARGS__)
+#define readdir(...) mp_readdir(__VA_ARGS__)
+#define closedir(...) mp_closedir(__VA_ARGS__)
+#define mkdir(...) mp_mkdir(__VA_ARGS__)
+#define getcwd(...) mp_win32_getcwd(__VA_ARGS__)
+#define getenv(...) mp_getenv(__VA_ARGS__)
+
+#undef lseek
+#define lseek(...) mp_lseek(__VA_ARGS__)
+
+#define RTLD_NOW 0
+#define RTLD_LOCAL 0
+#define dlopen(fn,fg) mp_dlopen((fn), (fg))
+#define dlsym(h,s) mp_dlsym((h), (s))
+#define dlerror mp_dlerror
+
+// Affects both "stat()" and "struct stat".
+#undef stat
+#define stat mp_stat
+
+#undef fstat
+#define fstat(...) mp_fstat(__VA_ARGS__)
+
+#define utime(...) _utime(__VA_ARGS__)
+#define utimbuf _utimbuf
+
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
+int munmap(void *addr, size_t length);
+int msync(void *addr, size_t length, int flags);
+#define PROT_READ 1
+#define PROT_WRITE 2
+#define MAP_SHARED 1
+#define MAP_FAILED ((void *)-1)
+#define MS_ASYNC 1
+#define MS_SYNC 2
+#define MS_INVALIDATE 4
+
+#ifndef GLOB_NOMATCH
+#define GLOB_NOMATCH 3
+#endif
+
+#define glob_t mp_glob_t
+#define glob(...) mp_glob(__VA_ARGS__)
+#define globfree(...) mp_globfree(__VA_ARGS__)
+
+// These are stubs since there is not anything that helps with this on Windows.
+#define locale_t int
+#define LC_CTYPE_MASK 0
+locale_t newlocale(int, const char *, locale_t);
+locale_t uselocale(locale_t);
+void freelocale(locale_t);
+
+#else /* __MINGW32__ */
+
+#include <sys/mman.h>
+
+extern char **environ;
+
+#endif /* __MINGW32__ */
+
+int mp_mkostemps(char *template, int suffixlen, int flags);
+
+#endif
diff --git a/osdep/language-apple.c b/osdep/language-apple.c
new file mode 100644
index 0000000..dc83fb5
--- /dev/null
+++ b/osdep/language-apple.c
@@ -0,0 +1,45 @@
+/*
+ * User language lookup for Apple platforms
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc/language.h"
+
+#include "apple_utils.h"
+#include "mpv_talloc.h"
+
+char **mp_get_user_langs(void)
+{
+ CFArrayRef arr = CFLocaleCopyPreferredLanguages();
+ if (!arr)
+ return NULL;
+ CFIndex count = CFArrayGetCount(arr);
+ if (!count)
+ return NULL;
+
+ char **ret = talloc_array_ptrtype(NULL, ret, count + 1);
+
+ for (CFIndex i = 0; i < count; i++) {
+ CFStringRef cfstr = CFArrayGetValueAtIndex(arr, i);
+ ret[i] = talloc_steal(ret, cfstr_get_cstr(cfstr));
+ }
+
+ ret[count] = NULL;
+
+ CFRelease(arr);
+ return ret;
+}
diff --git a/osdep/language-posix.c b/osdep/language-posix.c
new file mode 100644
index 0000000..8fd68c6
--- /dev/null
+++ b/osdep/language-posix.c
@@ -0,0 +1,72 @@
+/*
+ * User language lookup for generic POSIX platforms
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc/language.h"
+#include "mpv_talloc.h"
+
+#include <stddef.h>
+
+char **mp_get_user_langs(void)
+{
+ static const char *const list[] = {
+ "LC_ALL",
+ "LC_MESSAGES",
+ "LANG",
+ NULL
+ };
+
+ size_t nb = 0;
+ char **ret = NULL;
+ bool has_c = false;
+
+ // Prefer anything we get from LANGUAGE first
+ for (const char *langList = getenv("LANGUAGE"); langList && *langList;) {
+ size_t len = strcspn(langList, ":");
+ MP_TARRAY_GROW(NULL, ret, nb);
+ ret[nb++] = talloc_strndup(ret, langList, len);
+ langList += len;
+ while (*langList == ':')
+ langList++;
+ }
+
+ // Then, the language components of other relevant locale env vars
+ for (int i = 0; list[i]; i++) {
+ const char *envval = getenv(list[i]);
+ if (envval && *envval) {
+ size_t len = strcspn(envval, ".@");
+ if (!strncmp("C", envval, len)) {
+ has_c = true;
+ continue;
+ }
+
+ MP_TARRAY_GROW(NULL, ret, nb);
+ ret[nb++] = talloc_strndup(ret, envval, len);
+ }
+ }
+
+ if (has_c && !nb) {
+ MP_TARRAY_GROW(NULL, ret, nb);
+ ret[nb++] = talloc_strdup(ret, "en");
+ }
+
+ // Null-terminate the list
+ MP_TARRAY_APPEND(NULL, ret, nb, NULL);
+
+ return ret;
+}
diff --git a/osdep/language-win.c b/osdep/language-win.c
new file mode 100644
index 0000000..7d8e7fe
--- /dev/null
+++ b/osdep/language-win.c
@@ -0,0 +1,65 @@
+/*
+ * User language lookup for win32
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "misc/language.h"
+#include "mpv_talloc.h"
+#include "osdep/io.h"
+
+#include <windows.h>
+
+char **mp_get_user_langs(void)
+{
+ size_t nb = 0;
+ char **ret = NULL;
+ ULONG got_count = 0;
+ ULONG got_size = 0;
+ if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, NULL, &got_size) ||
+ got_size == 0)
+ return NULL;
+
+ wchar_t *buf = talloc_array(NULL, wchar_t, got_size);
+
+ if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, buf, &got_size) ||
+ got_size == 0)
+ goto cleanup;
+
+ for (ULONG pos = 0; buf[pos]; pos += wcslen(buf + pos) + 1) {
+ ret = talloc_realloc(NULL, ret, char*, (nb + 2));
+ ret[nb++] = mp_to_utf8(ret, buf + pos);
+ }
+ ret[nb] = NULL;
+
+ if (!GetSystemPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, NULL, &got_size))
+ goto cleanup;
+
+ buf = talloc_realloc(NULL, buf, wchar_t, got_size);
+
+ if (!GetSystemPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, buf, &got_size))
+ goto cleanup;
+
+ for (ULONG pos = 0; buf[pos]; pos += wcslen(buf + pos) + 1) {
+ ret = talloc_realloc(NULL, ret, char*, (nb + 2));
+ ret[nb++] = mp_to_utf8(ret, buf + pos);
+ }
+ ret[nb] = NULL;
+
+cleanup:
+ talloc_free(buf);
+ return ret;
+}
diff --git a/osdep/macOS_swift_bridge.h b/osdep/macOS_swift_bridge.h
new file mode 100644
index 0000000..9407b6f
--- /dev/null
+++ b/osdep/macOS_swift_bridge.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// including frameworks here again doesn't make sense, but otherwise the swift
+// compiler doesn't include the needed headers in our generated header file
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include "player/client.h"
+#include "video/out/libmpv.h"
+#include "libmpv/render_gl.h"
+
+#include "options/m_config.h"
+#include "player/core.h"
+#include "input/input.h"
+#include "video/out/win_state.h"
+
+#include "osdep/macosx_application_objc.h"
+#include "osdep/macosx_events_objc.h"
+
+
+// complex macros won't get imported to Swift so we have to reassign them
+static int SWIFT_MBTN_LEFT = MP_MBTN_LEFT;
+static int SWIFT_MBTN_MID = MP_MBTN_MID;
+static int SWIFT_MBTN_RIGHT = MP_MBTN_RIGHT;
+static int SWIFT_WHEEL_UP = MP_WHEEL_UP;
+static int SWIFT_WHEEL_DOWN = MP_WHEEL_DOWN;
+static int SWIFT_WHEEL_LEFT = MP_WHEEL_LEFT;
+static int SWIFT_WHEEL_RIGHT = MP_WHEEL_RIGHT;
+static int SWIFT_MBTN_BACK = MP_MBTN_BACK;
+static int SWIFT_MBTN_FORWARD = MP_MBTN_FORWARD;
+static int SWIFT_MBTN9 = MP_MBTN9;
+
+static int SWIFT_KEY_MOUSE_LEAVE = MP_KEY_MOUSE_LEAVE;
+static int SWIFT_KEY_MOUSE_ENTER = MP_KEY_MOUSE_ENTER;
+
+// only used from Swift files and therefore seen as unused by the c compiler
+static void SWIFT_TARRAY_STRING_APPEND(void *t, char ***a, int *i, char *s) __attribute__ ((unused));
+
+static void SWIFT_TARRAY_STRING_APPEND(void *t, char ***a, int *i, char *s)
+{
+ MP_TARRAY_APPEND(t, *a, *i, s);
+}
diff --git a/osdep/macos/libmpv_helper.swift b/osdep/macos/libmpv_helper.swift
new file mode 100644
index 0000000..8b1c697
--- /dev/null
+++ b/osdep/macos/libmpv_helper.swift
@@ -0,0 +1,250 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+import OpenGL.GL
+import OpenGL.GL3
+
+let glDummy: @convention(c) () -> Void = {}
+
+class LibmpvHelper {
+ var log: LogHelper
+ var mpvHandle: OpaquePointer?
+ var mpvRenderContext: OpaquePointer?
+ var macOptsPtr: UnsafeMutableRawPointer?
+ var macOpts: macos_opts = macos_opts()
+ var fbo: GLint = 1
+ let deinitLock = NSLock()
+
+ init(_ mpv: OpaquePointer, _ mpLog: OpaquePointer?) {
+ mpvHandle = mpv
+ log = LogHelper(mpLog)
+
+ guard let app = NSApp as? Application,
+ let ptr = mp_get_config_group(nil,
+ mp_client_get_global(mpvHandle),
+ app.getMacOSConf()) else
+ {
+ log.sendError("macOS config group couldn't be retrieved'")
+ exit(1)
+ }
+ macOptsPtr = ptr
+ macOpts = UnsafeMutablePointer<macos_opts>(OpaquePointer(ptr)).pointee
+ }
+
+ func initRender() {
+ let advanced: CInt = 1
+ let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
+ let pAddress = mpv_opengl_init_params(get_proc_address: getProcAddress,
+ get_proc_address_ctx: nil)
+
+ MPVHelper.withUnsafeMutableRawPointers([pAddress, advanced]) { (pointers: [UnsafeMutableRawPointer?]) in
+ var params: [mpv_render_param] = [
+ mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
+ mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, data: pointers[0]),
+ mpv_render_param(type: MPV_RENDER_PARAM_ADVANCED_CONTROL, data: pointers[1]),
+ mpv_render_param()
+ ]
+
+ if (mpv_render_context_create(&mpvRenderContext, mpvHandle, &params) < 0) {
+ log.sendError("Render context init has failed.")
+ exit(1)
+ }
+ }
+
+ }
+
+ let getProcAddress: (@convention(c) (UnsafeMutableRawPointer?, UnsafePointer<Int8>?)
+ -> UnsafeMutableRawPointer?) =
+ {
+ (ctx: UnsafeMutableRawPointer?, name: UnsafePointer<Int8>?)
+ -> UnsafeMutableRawPointer? in
+ let symbol: CFString = CFStringCreateWithCString(
+ kCFAllocatorDefault, name, kCFStringEncodingASCII)
+ let identifier = CFBundleGetBundleWithIdentifier("com.apple.opengl" as CFString)
+ let addr = CFBundleGetFunctionPointerForName(identifier, symbol)
+
+ if symbol as String == "glFlush" {
+ return unsafeBitCast(glDummy, to: UnsafeMutableRawPointer.self)
+ }
+
+ return addr
+ }
+
+ func setRenderUpdateCallback(_ callback: @escaping mpv_render_update_fn, context object: AnyObject) {
+ if mpvRenderContext == nil {
+ log.sendWarning("Init mpv render context first.")
+ } else {
+ mpv_render_context_set_update_callback(mpvRenderContext, callback, MPVHelper.bridge(obj: object))
+ }
+ }
+
+ func setRenderControlCallback(_ callback: @escaping mp_render_cb_control_fn, context object: AnyObject) {
+ if mpvRenderContext == nil {
+ log.sendWarning("Init mpv render context first.")
+ } else {
+ mp_render_context_set_control_callback(mpvRenderContext, callback, MPVHelper.bridge(obj: object))
+ }
+ }
+
+ func reportRenderFlip() {
+ if mpvRenderContext == nil { return }
+ mpv_render_context_report_swap(mpvRenderContext)
+ }
+
+ func isRenderUpdateFrame() -> Bool {
+ deinitLock.lock()
+ if mpvRenderContext == nil {
+ deinitLock.unlock()
+ return false
+ }
+ let flags: UInt64 = mpv_render_context_update(mpvRenderContext)
+ deinitLock.unlock()
+ return flags & UInt64(MPV_RENDER_UPDATE_FRAME.rawValue) > 0
+ }
+
+ func drawRender(_ surface: NSSize, _ depth: GLint, _ ctx: CGLContextObj, skip: Bool = false) {
+ deinitLock.lock()
+ if mpvRenderContext != nil {
+ var i: GLint = 0
+ let flip: CInt = 1
+ let skip: CInt = skip ? 1 : 0
+ let ditherDepth = depth
+ glGetIntegerv(GLenum(GL_DRAW_FRAMEBUFFER_BINDING), &i)
+ // CAOpenGLLayer has ownership of FBO zero yet can return it to us,
+ // so only utilize a newly received FBO ID if it is nonzero.
+ fbo = i != 0 ? i : fbo
+
+ let data = mpv_opengl_fbo(fbo: Int32(fbo),
+ w: Int32(surface.width),
+ h: Int32(surface.height),
+ internal_format: 0)
+
+ MPVHelper.withUnsafeMutableRawPointers([data, flip, ditherDepth, skip]) { (pointers: [UnsafeMutableRawPointer?]) in
+ var params: [mpv_render_param] = [
+ mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: pointers[0]),
+ mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: pointers[1]),
+ mpv_render_param(type: MPV_RENDER_PARAM_DEPTH, data: pointers[2]),
+ mpv_render_param(type: MPV_RENDER_PARAM_SKIP_RENDERING, data: pointers[3]),
+ mpv_render_param()
+ ]
+ mpv_render_context_render(mpvRenderContext, &params);
+ }
+ } else {
+ glClearColor(0, 0, 0, 1)
+ glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
+ }
+
+ if !skip { CGLFlushDrawable(ctx) }
+
+ deinitLock.unlock()
+ }
+
+ func setRenderICCProfile(_ profile: NSColorSpace) {
+ if mpvRenderContext == nil { return }
+ guard var iccData = profile.iccProfileData else {
+ log.sendWarning("Invalid ICC profile data.")
+ return
+ }
+ iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
+ guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return }
+
+ let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
+ let iccBstr = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count))
+ var icc = mpv_byte_array(data: iccBstr.start, size: iccBstr.len)
+ withUnsafeMutableBytes(of: &icc) { (ptr: UnsafeMutableRawBufferPointer) in
+ let params = mpv_render_param(type: MPV_RENDER_PARAM_ICC_PROFILE, data: ptr.baseAddress)
+ mpv_render_context_set_parameter(mpvRenderContext, params)
+ }
+ }
+ }
+
+ func setRenderLux(_ lux: Int) {
+ if mpvRenderContext == nil { return }
+ var light = lux
+ withUnsafeMutableBytes(of: &light) { (ptr: UnsafeMutableRawBufferPointer) in
+ let params = mpv_render_param(type: MPV_RENDER_PARAM_AMBIENT_LIGHT, data: ptr.baseAddress)
+ mpv_render_context_set_parameter(mpvRenderContext, params)
+ }
+ }
+
+ func commandAsync(_ cmd: [String?], id: UInt64 = 1) {
+ if mpvHandle == nil { return }
+ var mCmd = cmd
+ mCmd.append(nil)
+ var cargs = mCmd.map { $0.flatMap { UnsafePointer<Int8>(strdup($0)) } }
+ mpv_command_async(mpvHandle, id, &cargs)
+ for ptr in cargs { free(UnsafeMutablePointer(mutating: ptr)) }
+ }
+
+ // Unsafe function when called while using the render API
+ func command(_ cmd: String) {
+ if mpvHandle == nil { return }
+ mpv_command_string(mpvHandle, cmd)
+ }
+
+ func getBoolProperty(_ name: String) -> Bool {
+ if mpvHandle == nil { return false }
+ var value = Int32()
+ mpv_get_property(mpvHandle, name, MPV_FORMAT_FLAG, &value)
+ return value > 0
+ }
+
+ func getIntProperty(_ name: String) -> Int {
+ if mpvHandle == nil { return 0 }
+ var value = Int64()
+ mpv_get_property(mpvHandle, name, MPV_FORMAT_INT64, &value)
+ return Int(value)
+ }
+
+ func getStringProperty(_ name: String) -> String? {
+ guard let mpv = mpvHandle else { return nil }
+ guard let value = mpv_get_property_string(mpv, name) else { return nil }
+ let str = String(cString: value)
+ mpv_free(value)
+ return str
+ }
+
+ func deinitRender() {
+ mpv_render_context_set_update_callback(mpvRenderContext, nil, nil)
+ mp_render_context_set_control_callback(mpvRenderContext, nil, nil)
+ deinitLock.lock()
+ mpv_render_context_free(mpvRenderContext)
+ mpvRenderContext = nil
+ deinitLock.unlock()
+ }
+
+ func deinitMPV(_ destroy: Bool = false) {
+ if destroy {
+ mpv_destroy(mpvHandle)
+ }
+ ta_free(macOptsPtr)
+ macOptsPtr = nil
+ mpvHandle = nil
+ }
+
+ // *(char **) MPV_FORMAT_STRING on mpv_event_property
+ class func mpvStringArrayToString(_ obj: UnsafeMutableRawPointer) -> String? {
+ let cstr = UnsafeMutablePointer<UnsafeMutablePointer<Int8>>(OpaquePointer(obj))
+ return String(cString: cstr[0])
+ }
+
+ // MPV_FORMAT_FLAG
+ class func mpvFlagToBool(_ obj: UnsafeMutableRawPointer) -> Bool? {
+ return UnsafePointer<Bool>(OpaquePointer(obj))?.pointee
+ }
+}
diff --git a/osdep/macos/log_helper.swift b/osdep/macos/log_helper.swift
new file mode 100644
index 0000000..9464075
--- /dev/null
+++ b/osdep/macos/log_helper.swift
@@ -0,0 +1,47 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class LogHelper: NSObject {
+ var log: OpaquePointer?
+
+ init(_ log: OpaquePointer?) {
+ self.log = log
+ }
+
+ func sendVerbose(_ msg: String) {
+ send(message: msg, type: MSGL_V)
+ }
+
+ func sendInfo(_ msg: String) {
+ send(message: msg, type: MSGL_INFO)
+ }
+
+ func sendWarning(_ msg: String) {
+ send(message: msg, type: MSGL_WARN)
+ }
+
+ func sendError(_ msg: String) {
+ send(message: msg, type: MSGL_ERR)
+ }
+
+ func send(message msg: String, type t: Int) {
+ let args: [CVarArg] = [ (msg as NSString).utf8String ?? "NO MESSAGE"]
+ mp_msg_va(log, Int32(t), "%s\n", getVaList(args))
+ }
+}
diff --git a/osdep/macos/mpv_helper.swift b/osdep/macos/mpv_helper.swift
new file mode 100644
index 0000000..3b2a716
--- /dev/null
+++ b/osdep/macos/mpv_helper.swift
@@ -0,0 +1,156 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+typealias swift_wakeup_cb_fn = (@convention(c) (UnsafeMutableRawPointer?) -> Void)?
+
+class MPVHelper {
+ var log: LogHelper
+ var vo: UnsafeMutablePointer<vo>
+ var optsCachePtr: UnsafeMutablePointer<m_config_cache>
+ var optsPtr: UnsafeMutablePointer<mp_vo_opts>
+ var macOptsCachePtr: UnsafeMutablePointer<m_config_cache>
+ var macOptsPtr: UnsafeMutablePointer<macos_opts>
+
+ // these computed properties return a local copy of the struct accessed:
+ // - don't use if you rely on the pointers
+ // - only for reading
+ var vout: vo { get { return vo.pointee } }
+ var optsCache: m_config_cache { get { return optsCachePtr.pointee } }
+ var opts: mp_vo_opts { get { return optsPtr.pointee } }
+ var macOptsCache: m_config_cache { get { return macOptsCachePtr.pointee } }
+ var macOpts: macos_opts { get { return macOptsPtr.pointee } }
+
+ var input: OpaquePointer { get { return vout.input_ctx } }
+
+ init(_ vo: UnsafeMutablePointer<vo>, _ log: LogHelper) {
+ self.vo = vo
+ self.log = log
+
+ guard let app = NSApp as? Application,
+ let cache = m_config_cache_alloc(vo, vo.pointee.global, app.getVoSubConf()) else
+ {
+ log.sendError("NSApp couldn't be retrieved")
+ exit(1)
+ }
+
+ optsCachePtr = cache
+ optsPtr = UnsafeMutablePointer<mp_vo_opts>(OpaquePointer(cache.pointee.opts))
+
+ guard let macCache = m_config_cache_alloc(vo,
+ vo.pointee.global,
+ app.getMacOSConf()) else
+ {
+ // will never be hit, mp_get_config_group asserts for invalid groups
+ exit(1)
+ }
+ macOptsCachePtr = macCache
+ macOptsPtr = UnsafeMutablePointer<macos_opts>(OpaquePointer(macCache.pointee.opts))
+ }
+
+ func canBeDraggedAt(_ pos: NSPoint) -> Bool {
+ let canDrag = !mp_input_test_dragging(input, Int32(pos.x), Int32(pos.y))
+ return canDrag
+ }
+
+ func mouseEnabled() -> Bool {
+ return mp_input_mouse_enabled(input)
+ }
+
+ func setMousePosition(_ pos: NSPoint) {
+ mp_input_set_mouse_pos(input, Int32(pos.x), Int32(pos.y))
+ }
+
+ func putAxis(_ mpkey: Int32, delta: Double) {
+ mp_input_put_wheel(input, mpkey, delta)
+ }
+
+ func nextChangedOption(property: inout UnsafeMutableRawPointer?) -> Bool {
+ return m_config_cache_get_next_changed(optsCachePtr, &property)
+ }
+
+ func setOption(fullscreen: Bool) {
+ optsPtr.pointee.fullscreen = fullscreen
+ _ = withUnsafeMutableBytes(of: &optsPtr.pointee.fullscreen) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(optsCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setOption(minimized: Bool) {
+ optsPtr.pointee.window_minimized = minimized
+ _ = withUnsafeMutableBytes(of: &optsPtr.pointee.window_minimized) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(optsCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setOption(maximized: Bool) {
+ optsPtr.pointee.window_maximized = maximized
+ _ = withUnsafeMutableBytes(of: &optsPtr.pointee.window_maximized) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(optsCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setMacOptionCallback(_ callback: swift_wakeup_cb_fn, context object: AnyObject) {
+ m_config_cache_set_wakeup_cb(macOptsCachePtr, callback, MPVHelper.bridge(obj: object))
+ }
+
+ func nextChangedMacOption(property: inout UnsafeMutableRawPointer?) -> Bool {
+ return m_config_cache_get_next_changed(macOptsCachePtr, &property)
+ }
+
+ func command(_ cmd: String) {
+ let cCmd = UnsafePointer<Int8>(strdup(cmd))
+ let mpvCmd = mp_input_parse_cmd(input, bstr0(cCmd), "")
+ mp_input_queue_cmd(input, mpvCmd)
+ free(UnsafeMutablePointer(mutating: cCmd))
+ }
+
+ // (__bridge void*)
+ class func bridge<T: AnyObject>(obj: T) -> UnsafeMutableRawPointer {
+ return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque())
+ }
+
+ // (__bridge T*)
+ class func bridge<T: AnyObject>(ptr: UnsafeRawPointer) -> T {
+ return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue()
+ }
+
+ class func withUnsafeMutableRawPointers(_ arguments: [Any],
+ pointers: [UnsafeMutableRawPointer?] = [],
+ closure: (_ pointers: [UnsafeMutableRawPointer?]) -> Void) {
+ if arguments.count > 0 {
+ let args = Array(arguments.dropFirst(1))
+ var newPtrs = pointers
+ var firstArg = arguments.first
+ withUnsafeMutableBytes(of: &firstArg) { (ptr: UnsafeMutableRawBufferPointer) in
+ newPtrs.append(ptr.baseAddress)
+ withUnsafeMutableRawPointers(args, pointers: newPtrs, closure: closure)
+ }
+
+ return
+ }
+
+ closure(pointers)
+ }
+
+ class func getPointer<T>(_ value: inout T) -> UnsafeMutableRawPointer? {
+ return withUnsafeMutableBytes(of: &value) { (ptr: UnsafeMutableRawBufferPointer) in
+ ptr.baseAddress
+ }
+ }
+}
diff --git a/osdep/macos/precise_timer.swift b/osdep/macos/precise_timer.swift
new file mode 100644
index 0000000..f4ad3bb
--- /dev/null
+++ b/osdep/macos/precise_timer.swift
@@ -0,0 +1,153 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+struct Timing {
+ let time: UInt64
+ let closure: () -> ()
+}
+
+class PreciseTimer {
+ unowned var common: Common
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ let nanoPerSecond: Double = 1e+9
+ let machToNano: Double = {
+ var timebase: mach_timebase_info = mach_timebase_info()
+ mach_timebase_info(&timebase)
+ return Double(timebase.numer) / Double(timebase.denom)
+ }()
+
+ let condition = NSCondition()
+ var events: [Timing] = []
+ var isRunning: Bool = true
+ var isHighPrecision: Bool = false
+
+ var thread: pthread_t!
+ var threadPort: thread_port_t = thread_port_t()
+ let policyFlavor = thread_policy_flavor_t(THREAD_TIME_CONSTRAINT_POLICY)
+ let policyCount = MemoryLayout<thread_time_constraint_policy>.size /
+ MemoryLayout<integer_t>.size
+ var typeNumber: mach_msg_type_number_t {
+ return mach_msg_type_number_t(policyCount)
+ }
+ var threadAttr: pthread_attr_t = {
+ var attr = pthread_attr_t()
+ var param = sched_param()
+ pthread_attr_init(&attr)
+ param.sched_priority = sched_get_priority_max(SCHED_FIFO)
+ pthread_attr_setschedparam(&attr, &param)
+ pthread_attr_setschedpolicy(&attr, SCHED_FIFO)
+ return attr
+ }()
+
+ init?(common com: Common) {
+ common = com
+
+ pthread_create(&thread, &threadAttr, entryC, MPVHelper.bridge(obj: self))
+ if thread == nil {
+ common.log.sendWarning("Couldn't create pthread for high precision timer")
+ return nil
+ }
+
+ threadPort = pthread_mach_thread_np(thread)
+ }
+
+ func updatePolicy(periodSeconds: Double = 1 / 60.0) {
+ let period = periodSeconds * nanoPerSecond / machToNano
+ var policy = thread_time_constraint_policy(
+ period: UInt32(period),
+ computation: UInt32(0.75 * period),
+ constraint: UInt32(0.85 * period),
+ preemptible: 1
+ )
+
+ let success = withUnsafeMutablePointer(to: &policy) {
+ $0.withMemoryRebound(to: integer_t.self, capacity: policyCount) {
+ thread_policy_set(threadPort, policyFlavor, $0, typeNumber)
+ }
+ }
+
+ isHighPrecision = success == KERN_SUCCESS
+ if !isHighPrecision {
+ common.log.sendWarning("Couldn't create a high precision timer")
+ }
+ }
+
+ func terminate() {
+ condition.lock()
+ isRunning = false
+ condition.signal()
+ condition.unlock()
+ pthread_kill(thread, SIGALRM)
+ pthread_join(thread, nil)
+ }
+
+ func scheduleAt(time: UInt64, closure: @escaping () -> ()) {
+ condition.lock()
+ let firstEventTime = events.first?.time ?? 0
+ let lastEventTime = events.last?.time ?? 0
+ events.append(Timing(time: time, closure: closure))
+
+ if lastEventTime > time {
+ events.sort{ $0.time < $1.time }
+ }
+
+ condition.signal()
+ condition.unlock()
+
+ if firstEventTime > time {
+ pthread_kill(thread, SIGALRM)
+ }
+ }
+
+ let threadSignal: @convention(c) (Int32) -> () = { (sig: Int32) in }
+
+ let entryC: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? = { (ptr: UnsafeMutableRawPointer) in
+ let ptimer: PreciseTimer = MPVHelper.bridge(ptr: ptr)
+ ptimer.entry()
+ return nil
+ }
+
+ func entry() {
+ signal(SIGALRM, threadSignal)
+
+ while isRunning {
+ condition.lock()
+ while events.count == 0 && isRunning {
+ condition.wait()
+ }
+
+ if !isRunning { break }
+
+ guard let event = events.first else {
+ continue
+ }
+ condition.unlock()
+
+ mach_wait_until(event.time)
+
+ condition.lock()
+ if events.first?.time == event.time && isRunning {
+ event.closure()
+ events.removeFirst()
+ }
+ condition.unlock()
+ }
+ }
+}
diff --git a/osdep/macos/remote_command_center.swift b/osdep/macos/remote_command_center.swift
new file mode 100644
index 0000000..6fb2229
--- /dev/null
+++ b/osdep/macos/remote_command_center.swift
@@ -0,0 +1,191 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import MediaPlayer
+
+class RemoteCommandCenter: NSObject {
+ enum KeyType {
+ case normal
+ case repeatable
+ }
+
+ var config: [MPRemoteCommand:[String:Any]] = [
+ MPRemoteCommandCenter.shared().pauseCommand: [
+ "mpKey": MP_KEY_PAUSEONLY,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().playCommand: [
+ "mpKey": MP_KEY_PLAYONLY,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().stopCommand: [
+ "mpKey": MP_KEY_STOP,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().nextTrackCommand: [
+ "mpKey": MP_KEY_NEXT,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().previousTrackCommand: [
+ "mpKey": MP_KEY_PREV,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().togglePlayPauseCommand: [
+ "mpKey": MP_KEY_PLAY,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().seekForwardCommand: [
+ "mpKey": MP_KEY_FORWARD,
+ "keyType": KeyType.repeatable,
+ "state": MP_KEY_STATE_UP
+ ],
+ MPRemoteCommandCenter.shared().seekBackwardCommand: [
+ "mpKey": MP_KEY_REWIND,
+ "keyType": KeyType.repeatable,
+ "state": MP_KEY_STATE_UP
+ ],
+ ]
+
+ var nowPlayingInfo: [String: Any] = [
+ MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue),
+ MPNowPlayingInfoPropertyDefaultPlaybackRate: NSNumber(value: 1),
+ MPNowPlayingInfoPropertyPlaybackProgress: NSNumber(value: 0.0),
+ MPMediaItemPropertyPlaybackDuration: NSNumber(value: 0),
+ MPMediaItemPropertyTitle: "mpv",
+ MPMediaItemPropertyAlbumTitle: "mpv",
+ MPMediaItemPropertyArtist: "mpv",
+ ]
+
+ let disabledCommands: [MPRemoteCommand] = [
+ MPRemoteCommandCenter.shared().changePlaybackRateCommand,
+ MPRemoteCommandCenter.shared().changeRepeatModeCommand,
+ MPRemoteCommandCenter.shared().changeShuffleModeCommand,
+ MPRemoteCommandCenter.shared().skipForwardCommand,
+ MPRemoteCommandCenter.shared().skipBackwardCommand,
+ MPRemoteCommandCenter.shared().changePlaybackPositionCommand,
+ MPRemoteCommandCenter.shared().enableLanguageOptionCommand,
+ MPRemoteCommandCenter.shared().disableLanguageOptionCommand,
+ MPRemoteCommandCenter.shared().ratingCommand,
+ MPRemoteCommandCenter.shared().likeCommand,
+ MPRemoteCommandCenter.shared().dislikeCommand,
+ MPRemoteCommandCenter.shared().bookmarkCommand,
+ ]
+
+ var mpInfoCenter: MPNowPlayingInfoCenter { get { return MPNowPlayingInfoCenter.default() } }
+ var isPaused: Bool = false { didSet { updatePlaybackState() } }
+
+ @objc override init() {
+ super.init()
+
+ for cmd in disabledCommands {
+ cmd.isEnabled = false
+ }
+ }
+
+ @objc func start() {
+ for (cmd, _) in config {
+ cmd.isEnabled = true
+ cmd.addTarget { [unowned self] event in
+ return self.cmdHandler(event)
+ }
+ }
+
+ if let app = NSApp as? Application, let icon = app.getMPVIcon() {
+ let albumArt = MPMediaItemArtwork(boundsSize: icon.size) { _ in
+ return icon
+ }
+ nowPlayingInfo[MPMediaItemPropertyArtwork] = albumArt
+ }
+
+ mpInfoCenter.nowPlayingInfo = nowPlayingInfo
+ mpInfoCenter.playbackState = .playing
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(self.makeCurrent),
+ name: NSApplication.willBecomeActiveNotification,
+ object: nil
+ )
+ }
+
+ @objc func stop() {
+ for (cmd, _) in config {
+ cmd.isEnabled = false
+ cmd.removeTarget(nil)
+ }
+
+ mpInfoCenter.nowPlayingInfo = nil
+ mpInfoCenter.playbackState = .unknown
+ }
+
+ @objc func makeCurrent(notification: NSNotification) {
+ mpInfoCenter.playbackState = .paused
+ mpInfoCenter.playbackState = .playing
+ updatePlaybackState()
+ }
+
+ func updatePlaybackState() {
+ mpInfoCenter.playbackState = isPaused ? .paused : .playing
+ }
+
+ func cmdHandler(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
+ guard let cmdConfig = config[event.command],
+ let mpKey = cmdConfig["mpKey"] as? Int32,
+ let keyType = cmdConfig["keyType"] as? KeyType else
+ {
+ return .commandFailed
+ }
+
+ var state = cmdConfig["state"] as? UInt32 ?? 0
+
+ if let currentState = cmdConfig["state"] as? UInt32, keyType == .repeatable {
+ state = MP_KEY_STATE_DOWN
+ config[event.command]?["state"] = MP_KEY_STATE_DOWN
+ if currentState == MP_KEY_STATE_DOWN {
+ state = MP_KEY_STATE_UP
+ config[event.command]?["state"] = MP_KEY_STATE_UP
+ }
+ }
+
+ EventsResponder.sharedInstance().handleMPKey(mpKey, withMask: Int32(state))
+
+ return .success
+ }
+
+ @objc func processEvent(_ event: UnsafeMutablePointer<mpv_event>) {
+ switch event.pointee.event_id {
+ case MPV_EVENT_PROPERTY_CHANGE:
+ handlePropertyChange(event)
+ default:
+ break
+ }
+ }
+
+ func handlePropertyChange(_ event: UnsafeMutablePointer<mpv_event>) {
+ let pData = OpaquePointer(event.pointee.data)
+ guard let property = UnsafePointer<mpv_event_property>(pData)?.pointee else {
+ return
+ }
+
+ switch String(cString: property.name) {
+ case "pause" where property.format == MPV_FORMAT_FLAG:
+ isPaused = LibmpvHelper.mpvFlagToBool(property.data) ?? false
+ default:
+ break
+ }
+ }
+}
diff --git a/osdep/macos/swift_compat.swift b/osdep/macos/swift_compat.swift
new file mode 100644
index 0000000..83059da
--- /dev/null
+++ b/osdep/macos/swift_compat.swift
@@ -0,0 +1,36 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#if !swift(>=5.0)
+extension Data {
+ mutating func withUnsafeMutableBytes<Type>(_ body: (UnsafeMutableRawBufferPointer) throws -> Type) rethrows -> Type {
+ let dataCount = count
+ return try withUnsafeMutableBytes { (ptr: UnsafeMutablePointer<UInt8>) throws -> Type in
+ try body(UnsafeMutableRawBufferPointer(start: ptr, count: dataCount))
+ }
+ }
+}
+#endif
+
+#if !swift(>=4.2)
+extension NSDraggingInfo {
+ var draggingPasteboard: NSPasteboard {
+ get { return draggingPasteboard() }
+ }
+}
+#endif
diff --git a/osdep/macos/swift_extensions.swift b/osdep/macos/swift_extensions.swift
new file mode 100644
index 0000000..127c568
--- /dev/null
+++ b/osdep/macos/swift_extensions.swift
@@ -0,0 +1,58 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+extension NSDeviceDescriptionKey {
+ static let screenNumber = NSDeviceDescriptionKey("NSScreenNumber")
+}
+
+extension NSScreen {
+
+ public var displayID: CGDirectDisplayID {
+ get {
+ return deviceDescription[.screenNumber] as? CGDirectDisplayID ?? 0
+ }
+ }
+}
+
+extension NSColor {
+
+ convenience init(hex: String) {
+ let int = Int(hex.dropFirst(), radix: 16) ?? 0
+ let alpha = CGFloat((int >> 24) & 0x000000FF)/255
+ let red = CGFloat((int >> 16) & 0x000000FF)/255
+ let green = CGFloat((int >> 8) & 0x000000FF)/255
+ let blue = CGFloat((int) & 0x000000FF)/255
+
+ self.init(calibratedRed: red, green: green, blue: blue, alpha: alpha)
+ }
+}
+
+extension Bool {
+
+ init(_ int32: Int32) {
+ self.init(int32 != 0)
+ }
+}
+
+extension Int32 {
+
+ init(_ bool: Bool) {
+ self.init(bool ? 1 : 0)
+ }
+}
diff --git a/osdep/macosx_application.h b/osdep/macosx_application.h
new file mode 100644
index 0000000..753b9f0
--- /dev/null
+++ b/osdep/macosx_application.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_MACOSX_APPLICATION
+#define MPV_MACOSX_APPLICATION
+
+#include "osdep/macosx_menubar.h"
+#include "options/m_option.h"
+
+enum {
+ FRAME_VISIBLE = 0,
+ FRAME_WHOLE,
+};
+
+enum {
+ RENDER_TIMER_CALLBACK = 0,
+ RENDER_TIMER_PRECISE,
+ RENDER_TIMER_SYSTEM,
+};
+
+struct macos_opts {
+ int macos_title_bar_style;
+ int macos_title_bar_appearance;
+ int macos_title_bar_material;
+ struct m_color macos_title_bar_color;
+ int macos_fs_animation_duration;
+ bool macos_force_dedicated_gpu;
+ int macos_app_activation_policy;
+ int macos_geometry_calculation;
+ int macos_render_timer;
+ int cocoa_cb_sw_renderer;
+ bool cocoa_cb_10bit_context;
+};
+
+// multithreaded wrapper for mpv_main
+int cocoa_main(int argc, char *argv[]);
+void cocoa_register_menu_item_action(MPMenuKey key, void* action);
+
+extern const struct m_sub_options macos_conf;
+
+#endif /* MPV_MACOSX_APPLICATION */
diff --git a/osdep/macosx_application.m b/osdep/macosx_application.m
new file mode 100644
index 0000000..73503ad
--- /dev/null
+++ b/osdep/macosx_application.m
@@ -0,0 +1,375 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include "config.h"
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "player/client.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#import "osdep/macosx_application_objc.h"
+#import "osdep/macosx_events_objc.h"
+#include "osdep/threads.h"
+#include "osdep/main-fn.h"
+
+#if HAVE_MACOS_TOUCHBAR
+#import "osdep/macosx_touchbar.h"
+#endif
+#if HAVE_MACOS_COCOA_CB
+#include "osdep/macOS_swift.h"
+#endif
+
+#define MPV_PROTOCOL @"mpv://"
+
+#define OPT_BASE_STRUCT struct macos_opts
+const struct m_sub_options macos_conf = {
+ .opts = (const struct m_option[]) {
+ {"macos-title-bar-appearance", OPT_CHOICE(macos_title_bar_appearance,
+ {"auto", 0}, {"aqua", 1}, {"darkAqua", 2},
+ {"vibrantLight", 3}, {"vibrantDark", 4},
+ {"aquaHighContrast", 5}, {"darkAquaHighContrast", 6},
+ {"vibrantLightHighContrast", 7},
+ {"vibrantDarkHighContrast", 8})},
+ {"macos-title-bar-material", OPT_CHOICE(macos_title_bar_material,
+ {"titlebar", 0}, {"selection", 1}, {"menu", 2},
+ {"popover", 3}, {"sidebar", 4}, {"headerView", 5},
+ {"sheet", 6}, {"windowBackground", 7}, {"hudWindow", 8},
+ {"fullScreen", 9}, {"toolTip", 10}, {"contentBackground", 11},
+ {"underWindowBackground", 12}, {"underPageBackground", 13},
+ {"dark", 14}, {"light", 15}, {"mediumLight", 16},
+ {"ultraDark", 17})},
+ {"macos-title-bar-color", OPT_COLOR(macos_title_bar_color)},
+ {"macos-fs-animation-duration",
+ OPT_CHOICE(macos_fs_animation_duration, {"default", -1}),
+ M_RANGE(0, 1000)},
+ {"macos-force-dedicated-gpu", OPT_BOOL(macos_force_dedicated_gpu)},
+ {"macos-app-activation-policy", OPT_CHOICE(macos_app_activation_policy,
+ {"regular", 0}, {"accessory", 1}, {"prohibited", 2})},
+ {"macos-geometry-calculation", OPT_CHOICE(macos_geometry_calculation,
+ {"visible", FRAME_VISIBLE}, {"whole", FRAME_WHOLE})},
+ {"macos-render-timer", OPT_CHOICE(macos_render_timer,
+ {"callback", RENDER_TIMER_CALLBACK}, {"precise", RENDER_TIMER_PRECISE},
+ {"system", RENDER_TIMER_SYSTEM})},
+ {"cocoa-cb-sw-renderer", OPT_CHOICE(cocoa_cb_sw_renderer,
+ {"auto", -1}, {"no", 0}, {"yes", 1})},
+ {"cocoa-cb-10bit-context", OPT_BOOL(cocoa_cb_10bit_context)},
+ {0}
+ },
+ .size = sizeof(struct macos_opts),
+ .defaults = &(const struct macos_opts){
+ .macos_title_bar_color = {0, 0, 0, 0},
+ .macos_fs_animation_duration = -1,
+ .cocoa_cb_sw_renderer = -1,
+ .cocoa_cb_10bit_context = true
+ },
+};
+
+// Whether the NSApplication singleton was created. If this is false, we are
+// running in libmpv mode, and cocoa_main() was never called.
+static bool application_instantiated;
+
+static mp_thread playback_thread_id;
+
+@interface Application ()
+{
+ EventsResponder *_eventsResponder;
+}
+
+@end
+
+static Application *mpv_shared_app(void)
+{
+ return (Application *)[Application sharedApplication];
+}
+
+static void terminate_cocoa_application(void)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [NSApp hide:NSApp];
+ [NSApp terminate:NSApp];
+ });
+}
+
+@implementation Application
+@synthesize menuBar = _menu_bar;
+@synthesize openCount = _open_count;
+@synthesize cocoaCB = _cocoa_cb;
+
+- (void)sendEvent:(NSEvent *)event
+{
+ if ([self modalWindow] || ![_eventsResponder processKeyEvent:event])
+ [super sendEvent:event];
+ [_eventsResponder wakeup];
+}
+
+- (id)init
+{
+ if (self = [super init]) {
+ _eventsResponder = [EventsResponder sharedInstance];
+
+ NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager];
+ [em setEventHandler:self
+ andSelector:@selector(getUrl:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager];
+ [em removeEventHandlerForEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+ [em removeEventHandlerForEventClass:kCoreEventClass
+ andEventID:kAEQuitApplication];
+ [super dealloc];
+}
+
+static const char macosx_icon[] =
+#include "TOOLS/osxbundle/icon.icns.inc"
+;
+
+- (NSImage *)getMPVIcon
+{
+ // The C string contains a trailing null, so we strip it away
+ NSData *icon_data = [NSData dataWithBytesNoCopy:(void *)macosx_icon
+ length:sizeof(macosx_icon) - 1
+ freeWhenDone:NO];
+ return [[NSImage alloc] initWithData:icon_data];
+}
+
+#if HAVE_MACOS_TOUCHBAR
+- (NSTouchBar *)makeTouchBar
+{
+ TouchBar *tBar = [[TouchBar alloc] init];
+ [tBar setApp:self];
+ tBar.delegate = tBar;
+ tBar.customizationIdentifier = customID;
+ tBar.defaultItemIdentifiers = @[play, previousItem, nextItem, seekBar];
+ tBar.customizationAllowedItemIdentifiers = @[play, seekBar, previousItem,
+ nextItem, previousChapter, nextChapter, cycleAudio, cycleSubtitle,
+ currentPosition, timeLeft];
+ return tBar;
+}
+#endif
+
+- (void)processEvent:(struct mpv_event *)event
+{
+#if HAVE_MACOS_TOUCHBAR
+ [(TouchBar *)self.touchBar processEvent:event];
+#endif
+ if (_cocoa_cb) {
+ [_cocoa_cb processEvent:event];
+ }
+}
+
+- (void)setMpvHandle:(struct mpv_handle *)ctx
+{
+#if HAVE_MACOS_COCOA_CB
+ [NSApp setCocoaCB:[[CocoaCB alloc] init:ctx]];
+#endif
+}
+
+- (const struct m_sub_options *)getMacOSConf
+{
+ return &macos_conf;
+}
+
+- (const struct m_sub_options *)getVoSubConf
+{
+ return &vo_sub_opts;
+}
+
+- (void)queueCommand:(char *)cmd
+{
+ [_eventsResponder queueCommand:cmd];
+}
+
+- (void)stopMPV:(char *)cmd
+{
+ if (![_eventsResponder queueCommand:cmd])
+ terminate_cocoa_application();
+}
+
+- (void)applicationWillFinishLaunching:(NSNotification *)notification
+{
+ NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager];
+ [em setEventHandler:self
+ andSelector:@selector(handleQuitEvent:withReplyEvent:)
+ forEventClass:kCoreEventClass
+ andEventID:kAEQuitApplication];
+}
+
+- (void)handleQuitEvent:(NSAppleEventDescriptor *)event
+ withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ [self stopMPV:"quit"];
+}
+
+- (void)getUrl:(NSAppleEventDescriptor *)event
+ withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ NSString *url =
+ [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
+
+ url = [url stringByReplacingOccurrencesOfString:MPV_PROTOCOL
+ withString:@""
+ options:NSAnchoredSearch
+ range:NSMakeRange(0, [MPV_PROTOCOL length])];
+
+ url = [url stringByRemovingPercentEncoding];
+ [_eventsResponder handleFilesArray:@[url]];
+}
+
+- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
+{
+ if (mpv_shared_app().openCount > 0) {
+ mpv_shared_app().openCount--;
+ return;
+ }
+ [self openFiles:filenames];
+}
+
+- (void)openFiles:(NSArray *)filenames
+{
+ SEL cmpsel = @selector(localizedStandardCompare:);
+ NSArray *files = [filenames sortedArrayUsingSelector:cmpsel];
+ [_eventsResponder handleFilesArray:files];
+}
+@end
+
+struct playback_thread_ctx {
+ int *argc;
+ char ***argv;
+};
+
+static void cocoa_run_runloop(void)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [NSApp run];
+ [pool drain];
+}
+
+static MP_THREAD_VOID playback_thread(void *ctx_obj)
+{
+ mp_thread_set_name("core/playback");
+ @autoreleasepool {
+ struct playback_thread_ctx *ctx = (struct playback_thread_ctx*) ctx_obj;
+ int r = mpv_main(*ctx->argc, *ctx->argv);
+ terminate_cocoa_application();
+ // normally never reached - unless the cocoa mainloop hasn't started yet
+ exit(r);
+ }
+}
+
+void cocoa_register_menu_item_action(MPMenuKey key, void* action)
+{
+ if (application_instantiated)
+ [[NSApp menuBar] registerSelector:(SEL)action forKey:key];
+}
+
+static void init_cocoa_application(bool regular)
+{
+ NSApp = mpv_shared_app();
+ [NSApp setDelegate:NSApp];
+ [NSApp setMenuBar:[[MenuBar alloc] init]];
+
+ // Will be set to Regular from cocoa_common during UI creation so that we
+ // don't create an icon when playing audio only files.
+ [NSApp setActivationPolicy: regular ?
+ NSApplicationActivationPolicyRegular :
+ NSApplicationActivationPolicyAccessory];
+
+ atexit_b(^{
+ // Because activation policy has just been set to behave like a real
+ // application, that policy must be reset on exit to prevent, among
+ // other things, the menubar created here from remaining on screen.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
+ });
+ });
+}
+
+static bool bundle_started_from_finder()
+{
+ NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"];
+ return [bundle isEqual:@"true"];
+}
+
+static bool is_psn_argument(char *arg_to_check)
+{
+ NSString *arg = [NSString stringWithUTF8String:arg_to_check];
+ return [arg hasPrefix:@"-psn_"];
+}
+
+static void setup_bundle(int *argc, char *argv[])
+{
+ if (*argc > 1 && is_psn_argument(argv[1])) {
+ *argc = 1;
+ argv[1] = NULL;
+ }
+
+ NSDictionary *env = [[NSProcessInfo processInfo] environment];
+ NSString *path_bundle = [env objectForKey:@"PATH"];
+ NSString *path_new = [NSString stringWithFormat:@"%@:%@:%@:%@:%@",
+ path_bundle,
+ @"/usr/local/bin",
+ @"/usr/local/sbin",
+ @"/opt/local/bin",
+ @"/opt/local/sbin"];
+ setenv("PATH", [path_new UTF8String], 1);
+}
+
+int cocoa_main(int argc, char *argv[])
+{
+ @autoreleasepool {
+ application_instantiated = true;
+ [[EventsResponder sharedInstance] setIsApplication:YES];
+
+ struct playback_thread_ctx ctx = {0};
+ ctx.argc = &argc;
+ ctx.argv = &argv;
+
+ if (bundle_started_from_finder()) {
+ setup_bundle(&argc, argv);
+ init_cocoa_application(true);
+ } else {
+ for (int i = 1; i < argc; i++)
+ if (argv[i][0] != '-')
+ mpv_shared_app().openCount++;
+ init_cocoa_application(false);
+ }
+
+ mp_thread_create(&playback_thread_id, playback_thread, &ctx);
+ [[EventsResponder sharedInstance] waitForInputContext];
+ cocoa_run_runloop();
+
+ // This should never be reached: cocoa_run_runloop blocks until the
+ // process is quit
+ fprintf(stderr, "There was either a problem "
+ "initializing Cocoa or the Runloop was stopped unexpectedly. "
+ "Please report this issues to a developer.\n");
+ mp_thread_join(playback_thread_id);
+ return 1;
+ }
+}
diff --git a/osdep/macosx_application_objc.h b/osdep/macosx_application_objc.h
new file mode 100644
index 0000000..11959a8
--- /dev/null
+++ b/osdep/macosx_application_objc.h
@@ -0,0 +1,40 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#include "osdep/macosx_application.h"
+#import "osdep/macosx_menubar_objc.h"
+
+@class CocoaCB;
+struct mpv_event;
+struct mpv_handle;
+
+@interface Application : NSApplication
+
+- (NSImage *)getMPVIcon;
+- (void)processEvent:(struct mpv_event *)event;
+- (void)queueCommand:(char *)cmd;
+- (void)stopMPV:(char *)cmd;
+- (void)openFiles:(NSArray *)filenames;
+- (void)setMpvHandle:(struct mpv_handle *)ctx;
+- (const struct m_sub_options *)getMacOSConf;
+- (const struct m_sub_options *)getVoSubConf;
+
+@property(nonatomic, retain) MenuBar *menuBar;
+@property(nonatomic, assign) size_t openCount;
+@property(nonatomic, retain) CocoaCB *cocoaCB;
+@end
diff --git a/osdep/macosx_events.h b/osdep/macosx_events.h
new file mode 100644
index 0000000..9188c8b
--- /dev/null
+++ b/osdep/macosx_events.h
@@ -0,0 +1,36 @@
+/*
+ * Cocoa Application Event Handling
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MACOSX_EVENTS_H
+#define MACOSX_EVENTS_H
+#include "input/keycodes.h"
+
+struct input_ctx;
+struct mpv_handle;
+
+void cocoa_put_key(int keycode);
+void cocoa_put_key_with_modifiers(int keycode, int modifiers);
+
+void cocoa_init_media_keys(void);
+void cocoa_uninit_media_keys(void);
+
+void cocoa_set_input_context(struct input_ctx *input_context);
+void cocoa_set_mpv_handle(struct mpv_handle *ctx);
+
+#endif
diff --git a/osdep/macosx_events.m b/osdep/macosx_events.m
new file mode 100644
index 0000000..627077a
--- /dev/null
+++ b/osdep/macosx_events.m
@@ -0,0 +1,408 @@
+/*
+ * Cocoa Application Event Handling
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Carbon header is included but Carbon is NOT linked to mpv's binary. This
+// file only needs this include to use the keycode definitions in keymap.
+#import <Carbon/Carbon.h>
+
+// Media keys definitions
+#import <IOKit/hidsystem/ev_keymap.h>
+#import <Cocoa/Cocoa.h>
+
+#include "mpv_talloc.h"
+#include "input/event.h"
+#include "input/input.h"
+#include "player/client.h"
+#include "input/keycodes.h"
+// doesn't make much sense, but needed to access keymap functionality
+#include "video/out/vo.h"
+
+#import "osdep/macosx_events_objc.h"
+#import "osdep/macosx_application_objc.h"
+
+#include "config.h"
+
+#if HAVE_MACOS_COCOA_CB
+#include "osdep/macOS_swift.h"
+#endif
+
+@interface EventsResponder ()
+{
+ struct input_ctx *_inputContext;
+ struct mpv_handle *_ctx;
+ BOOL _is_application;
+ NSCondition *_input_lock;
+}
+
+- (NSEvent *)handleKey:(NSEvent *)event;
+- (BOOL)setMpvHandle:(struct mpv_handle *)ctx;
+- (void)readEvents;
+- (void)startMediaKeys;
+- (void)stopMediaKeys;
+- (int)mapKeyModifiers:(int)cocoaModifiers;
+- (int)keyModifierMask:(NSEvent *)event;
+@end
+
+
+#define NSLeftAlternateKeyMask (0x000020 | NSEventModifierFlagOption)
+#define NSRightAlternateKeyMask (0x000040 | NSEventModifierFlagOption)
+
+static bool LeftAltPressed(int mask)
+{
+ return (mask & NSLeftAlternateKeyMask) == NSLeftAlternateKeyMask;
+}
+
+static bool RightAltPressed(int mask)
+{
+ return (mask & NSRightAlternateKeyMask) == NSRightAlternateKeyMask;
+}
+
+static const struct mp_keymap keymap[] = {
+ // special keys
+ {kVK_Return, MP_KEY_ENTER}, {kVK_Escape, MP_KEY_ESC},
+ {kVK_Delete, MP_KEY_BACKSPACE}, {kVK_Option, MP_KEY_BACKSPACE},
+ {kVK_Control, MP_KEY_BACKSPACE}, {kVK_Shift, MP_KEY_BACKSPACE},
+ {kVK_Tab, MP_KEY_TAB},
+
+ // cursor keys
+ {kVK_UpArrow, MP_KEY_UP}, {kVK_DownArrow, MP_KEY_DOWN},
+ {kVK_LeftArrow, MP_KEY_LEFT}, {kVK_RightArrow, MP_KEY_RIGHT},
+
+ // navigation block
+ {kVK_Help, MP_KEY_INSERT}, {kVK_ForwardDelete, MP_KEY_DELETE},
+ {kVK_Home, MP_KEY_HOME}, {kVK_End, MP_KEY_END},
+ {kVK_PageUp, MP_KEY_PAGE_UP}, {kVK_PageDown, MP_KEY_PAGE_DOWN},
+
+ // F-keys
+ {kVK_F1, MP_KEY_F + 1}, {kVK_F2, MP_KEY_F + 2}, {kVK_F3, MP_KEY_F + 3},
+ {kVK_F4, MP_KEY_F + 4}, {kVK_F5, MP_KEY_F + 5}, {kVK_F6, MP_KEY_F + 6},
+ {kVK_F7, MP_KEY_F + 7}, {kVK_F8, MP_KEY_F + 8}, {kVK_F9, MP_KEY_F + 9},
+ {kVK_F10, MP_KEY_F + 10}, {kVK_F11, MP_KEY_F + 11}, {kVK_F12, MP_KEY_F + 12},
+ {kVK_F13, MP_KEY_F + 13}, {kVK_F14, MP_KEY_F + 14}, {kVK_F15, MP_KEY_F + 15},
+ {kVK_F16, MP_KEY_F + 16}, {kVK_F17, MP_KEY_F + 17}, {kVK_F18, MP_KEY_F + 18},
+ {kVK_F19, MP_KEY_F + 19}, {kVK_F20, MP_KEY_F + 20},
+
+ // numpad
+ {kVK_ANSI_KeypadPlus, '+'}, {kVK_ANSI_KeypadMinus, '-'},
+ {kVK_ANSI_KeypadMultiply, '*'}, {kVK_ANSI_KeypadDivide, '/'},
+ {kVK_ANSI_KeypadEnter, MP_KEY_KPENTER},
+ {kVK_ANSI_KeypadDecimal, MP_KEY_KPDEC},
+ {kVK_ANSI_Keypad0, MP_KEY_KP0}, {kVK_ANSI_Keypad1, MP_KEY_KP1},
+ {kVK_ANSI_Keypad2, MP_KEY_KP2}, {kVK_ANSI_Keypad3, MP_KEY_KP3},
+ {kVK_ANSI_Keypad4, MP_KEY_KP4}, {kVK_ANSI_Keypad5, MP_KEY_KP5},
+ {kVK_ANSI_Keypad6, MP_KEY_KP6}, {kVK_ANSI_Keypad7, MP_KEY_KP7},
+ {kVK_ANSI_Keypad8, MP_KEY_KP8}, {kVK_ANSI_Keypad9, MP_KEY_KP9},
+
+ {0, 0}
+};
+
+static int convert_key(unsigned key, unsigned charcode)
+{
+ int mpkey = lookup_keymap_table(keymap, key);
+ if (mpkey)
+ return mpkey;
+ return charcode;
+}
+
+void cocoa_init_media_keys(void)
+{
+ [[EventsResponder sharedInstance] startMediaKeys];
+}
+
+void cocoa_uninit_media_keys(void)
+{
+ [[EventsResponder sharedInstance] stopMediaKeys];
+}
+
+void cocoa_put_key(int keycode)
+{
+ [[EventsResponder sharedInstance] putKey:keycode];
+}
+
+void cocoa_put_key_with_modifiers(int keycode, int modifiers)
+{
+ keycode |= [[EventsResponder sharedInstance] mapKeyModifiers:modifiers];
+ cocoa_put_key(keycode);
+}
+
+void cocoa_set_input_context(struct input_ctx *input_context)
+{
+ [[EventsResponder sharedInstance] setInputContext:input_context];
+}
+
+static void wakeup(void *context)
+{
+ [[EventsResponder sharedInstance] readEvents];
+}
+
+void cocoa_set_mpv_handle(struct mpv_handle *ctx)
+{
+ if ([[EventsResponder sharedInstance] setMpvHandle:ctx]) {
+ mpv_observe_property(ctx, 0, "duration", MPV_FORMAT_DOUBLE);
+ mpv_observe_property(ctx, 0, "time-pos", MPV_FORMAT_DOUBLE);
+ mpv_observe_property(ctx, 0, "pause", MPV_FORMAT_FLAG);
+ mpv_set_wakeup_callback(ctx, wakeup, NULL);
+ }
+}
+
+@implementation EventsResponder
+
+@synthesize remoteCommandCenter = _remoteCommandCenter;
+
++ (EventsResponder *)sharedInstance
+{
+ static EventsResponder *responder = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ responder = [EventsResponder new];
+ });
+ return responder;
+}
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ _input_lock = [NSCondition new];
+ }
+ return self;
+}
+
+- (void)waitForInputContext
+{
+ [_input_lock lock];
+ while (!_inputContext)
+ [_input_lock wait];
+ [_input_lock unlock];
+}
+
+- (void)setInputContext:(struct input_ctx *)ctx
+{
+ [_input_lock lock];
+ _inputContext = ctx;
+ [_input_lock signal];
+ [_input_lock unlock];
+}
+
+- (void)wakeup
+{
+ [_input_lock lock];
+ if (_inputContext)
+ mp_input_wakeup(_inputContext);
+ [_input_lock unlock];
+}
+
+- (bool)queueCommand:(char *)cmd
+{
+ bool r = false;
+ [_input_lock lock];
+ if (_inputContext) {
+ mp_cmd_t *cmdt = mp_input_parse_cmd(_inputContext, bstr0(cmd), "");
+ mp_input_queue_cmd(_inputContext, cmdt);
+ r = true;
+ }
+ [_input_lock unlock];
+ return r;
+}
+
+- (void)putKey:(int)keycode
+{
+ [_input_lock lock];
+ if (_inputContext)
+ mp_input_put_key(_inputContext, keycode);
+ [_input_lock unlock];
+}
+
+- (BOOL)useAltGr
+{
+ BOOL r = YES;
+ [_input_lock lock];
+ if (_inputContext)
+ r = mp_input_use_alt_gr(_inputContext);
+ [_input_lock unlock];
+ return r;
+}
+
+- (void)setIsApplication:(BOOL)isApplication
+{
+ _is_application = isApplication;
+}
+
+- (BOOL)setMpvHandle:(struct mpv_handle *)ctx
+{
+ if (_is_application) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ _ctx = ctx;
+ [NSApp setMpvHandle:ctx];
+ });
+ return YES;
+ } else {
+ mpv_destroy(ctx);
+ return NO;
+ }
+}
+
+- (void)readEvents
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ while (_ctx) {
+ mpv_event *event = mpv_wait_event(_ctx, 0);
+ if (event->event_id == MPV_EVENT_NONE)
+ break;
+ [self processEvent:event];
+ }
+ });
+}
+
+-(void)processEvent:(struct mpv_event *)event
+{
+ if(_is_application) {
+ [NSApp processEvent:event];
+ }
+
+ if (_remoteCommandCenter) {
+ [_remoteCommandCenter processEvent:event];
+ }
+
+ switch (event->event_id) {
+ case MPV_EVENT_SHUTDOWN: {
+#if HAVE_MACOS_COCOA_CB
+ if ([(Application *)NSApp cocoaCB].isShuttingDown) {
+ _ctx = nil;
+ return;
+ }
+#endif
+ mpv_destroy(_ctx);
+ _ctx = nil;
+ break;
+ }
+ }
+}
+
+- (void)startMediaKeys
+{
+#if HAVE_MACOS_MEDIA_PLAYER
+ if (_remoteCommandCenter == nil) {
+ _remoteCommandCenter = [[RemoteCommandCenter alloc] init];
+ }
+#endif
+
+ [_remoteCommandCenter start];
+}
+
+- (void)stopMediaKeys
+{
+ [_remoteCommandCenter stop];
+}
+
+- (int)mapKeyModifiers:(int)cocoaModifiers
+{
+ int mask = 0;
+ if (cocoaModifiers & NSEventModifierFlagShift)
+ mask |= MP_KEY_MODIFIER_SHIFT;
+ if (cocoaModifiers & NSEventModifierFlagControl)
+ mask |= MP_KEY_MODIFIER_CTRL;
+ if (LeftAltPressed(cocoaModifiers) ||
+ (RightAltPressed(cocoaModifiers) && ![self useAltGr]))
+ mask |= MP_KEY_MODIFIER_ALT;
+ if (cocoaModifiers & NSEventModifierFlagCommand)
+ mask |= MP_KEY_MODIFIER_META;
+ return mask;
+}
+
+- (int)mapTypeModifiers:(NSEventType)type
+{
+ NSDictionary *map = @{
+ @(NSEventTypeKeyDown) : @(MP_KEY_STATE_DOWN),
+ @(NSEventTypeKeyUp) : @(MP_KEY_STATE_UP),
+ };
+ return [map[@(type)] intValue];
+}
+
+- (int)keyModifierMask:(NSEvent *)event
+{
+ return [self mapKeyModifiers:[event modifierFlags]] |
+ [self mapTypeModifiers:[event type]];
+}
+
+-(BOOL)handleMPKey:(int)key withMask:(int)mask
+{
+ if (key > 0) {
+ cocoa_put_key(key | mask);
+ if (mask & MP_KEY_STATE_UP)
+ cocoa_put_key(MP_INPUT_RELEASE_ALL);
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+- (NSEvent*)handleKey:(NSEvent *)event
+{
+ if ([event isARepeat]) return nil;
+
+ NSString *chars;
+
+ if ([self useAltGr] && RightAltPressed([event modifierFlags])) {
+ chars = [event characters];
+ } else {
+ chars = [event charactersIgnoringModifiers];
+ }
+
+ struct bstr t = bstr0([chars UTF8String]);
+ int key = convert_key([event keyCode], bstr_decode_utf8(t, &t));
+
+ if (key > -1)
+ [self handleMPKey:key withMask:[self keyModifierMask:event]];
+
+ return nil;
+}
+
+- (bool)processKeyEvent:(NSEvent *)event
+{
+ if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp){
+ if (![[NSApp mainMenu] performKeyEquivalent:event])
+ [self handleKey:event];
+ return true;
+ }
+ return false;
+}
+
+- (void)handleFilesArray:(NSArray *)files
+{
+ enum mp_dnd_action action = [NSEvent modifierFlags] &
+ NSEventModifierFlagShift ? DND_APPEND : DND_REPLACE;
+
+ size_t num_files = [files count];
+ char **files_utf8 = talloc_array(NULL, char*, num_files);
+ [files enumerateObjectsUsingBlock:^(NSString *p, NSUInteger i, BOOL *_){
+ if ([p hasPrefix:@"file:///.file/id="])
+ p = [[NSURL URLWithString:p] path];
+ char *filename = (char *)[p UTF8String];
+ size_t bytes = [p lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ files_utf8[i] = talloc_memdup(files_utf8, filename, bytes + 1);
+ }];
+ [_input_lock lock];
+ if (_inputContext)
+ mp_event_drop_files(_inputContext, num_files, files_utf8, action);
+ [_input_lock unlock];
+ talloc_free(files_utf8);
+}
+
+@end
diff --git a/osdep/macosx_events_objc.h b/osdep/macosx_events_objc.h
new file mode 100644
index 0000000..9394fe7
--- /dev/null
+++ b/osdep/macosx_events_objc.h
@@ -0,0 +1,45 @@
+/*
+ * Cocoa Application Event Handling
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#include "osdep/macosx_events.h"
+
+@class RemoteCommandCenter;
+struct input_ctx;
+
+@interface EventsResponder : NSObject
+
++ (EventsResponder *)sharedInstance;
+- (void)setInputContext:(struct input_ctx *)ctx;
+- (void)setIsApplication:(BOOL)isApplication;
+
+/// Blocks until inputContext is present.
+- (void)waitForInputContext;
+- (void)wakeup;
+- (void)putKey:(int)keycode;
+- (void)handleFilesArray:(NSArray *)files;
+
+- (bool)queueCommand:(char *)cmd;
+- (bool)processKeyEvent:(NSEvent *)event;
+
+- (BOOL)handleMPKey:(int)key withMask:(int)mask;
+
+@property(nonatomic, retain) RemoteCommandCenter *remoteCommandCenter;
+
+@end
diff --git a/osdep/macosx_menubar.h b/osdep/macosx_menubar.h
new file mode 100644
index 0000000..509083d
--- /dev/null
+++ b/osdep/macosx_menubar.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_MACOSX_MENU
+#define MPV_MACOSX_MENU
+
+// Menu Keys identifying menu items
+typedef enum {
+ MPM_H_SIZE,
+ MPM_N_SIZE,
+ MPM_D_SIZE,
+ MPM_MINIMIZE,
+ MPM_ZOOM,
+} MPMenuKey;
+
+#endif /* MPV_MACOSX_MENU */
diff --git a/osdep/macosx_menubar.m b/osdep/macosx_menubar.m
new file mode 100644
index 0000000..5c6cd47
--- /dev/null
+++ b/osdep/macosx_menubar.m
@@ -0,0 +1,853 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "common/common.h"
+
+#import "macosx_menubar_objc.h"
+#import "osdep/macosx_application_objc.h"
+
+@implementation MenuBar
+{
+ NSArray *menuTree;
+}
+
+- (id)init
+{
+ if (self = [super init]) {
+ NSUserDefaults *userDefaults =[NSUserDefaults standardUserDefaults];
+ [userDefaults setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
+ [userDefaults setBool:YES forKey:@"NSDisabledDictationMenuItem"];
+ [userDefaults setBool:YES forKey:@"NSDisabledCharacterPaletteMenuItem"];
+ [NSWindow setAllowsAutomaticWindowTabbing: NO];
+
+ menuTree = @[
+ @{
+ @"name": @"Apple",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"About mpv",
+ @"action" : @"about",
+ @"key" : @"",
+ @"target" : self
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Preferences…",
+ @"action" : @"preferences:",
+ @"key" : @",",
+ @"target" : self,
+ @"file" : @"mpv.conf",
+ @"alertTitle1": @"No Application found to open your config file.",
+ @"alertText1" : @"Please open the mpv.conf file with "
+ "your preferred text editor in the now "
+ "open folder to edit your config.",
+ @"alertTitle2": @"No config file found.",
+ @"alertText2" : @"Please create a mpv.conf file with your "
+ "preferred text editor in the now open folder.",
+ @"alertTitle3": @"No config path or file found.",
+ @"alertText3" : @"Please create the following path ~/.config/mpv/ "
+ "and a mpv.conf file within with your preferred "
+ "text editor."
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Keyboard Shortcuts Config…",
+ @"action" : @"preferences:",
+ @"key" : @"",
+ @"target" : self,
+ @"file" : @"input.conf",
+ @"alertTitle1": @"No Application found to open your config file.",
+ @"alertText1" : @"Please open the input.conf file with "
+ "your preferred text editor in the now "
+ "open folder to edit your config.",
+ @"alertTitle2": @"No config file found.",
+ @"alertText2" : @"Please create a input.conf file with your "
+ "preferred text editor in the now open folder.",
+ @"alertTitle3": @"No config path or file found.",
+ @"alertText3" : @"Please create the following path ~/.config/mpv/ "
+ "and a input.conf file within with your preferred "
+ "text editor."
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Services",
+ @"key" : @"",
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Hide mpv",
+ @"action" : @"hide:",
+ @"key" : @"h",
+ @"target" : NSApp
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Hide Others",
+ @"action" : @"hideOtherApplications:",
+ @"key" : @"h",
+ @"modifiers" : [NSNumber numberWithUnsignedInteger:
+ NSEventModifierFlagCommand |
+ NSEventModifierFlagOption],
+ @"target" : NSApp
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show All",
+ @"action" : @"unhideAllApplications:",
+ @"key" : @"",
+ @"target" : NSApp
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Quit and Remember Position",
+ @"action" : @"quit:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"quit-watch-later"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Quit mpv",
+ @"action" : @"quit:",
+ @"key" : @"q",
+ @"target" : self,
+ @"cmd" : @"quit"
+ }]
+ ]
+ },
+ @{
+ @"name": @"File",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Open File…",
+ @"action" : @"openFile",
+ @"key" : @"o",
+ @"target" : self
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Open URL…",
+ @"action" : @"openURL",
+ @"key" : @"O",
+ @"target" : self
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Open Playlist…",
+ @"action" : @"openPlaylist",
+ @"key" : @"",
+ @"target" : self
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Close",
+ @"action" : @"performClose:",
+ @"key" : @"w"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Save Screenshot",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"async screenshot"
+ }]
+ ]
+ },
+ @{
+ @"name": @"Edit",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Undo",
+ @"action" : @"undo:",
+ @"key" : @"z"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Redo",
+ @"action" : @"redo:",
+ @"key" : @"Z"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Cut",
+ @"action" : @"cut:",
+ @"key" : @"x"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Copy",
+ @"action" : @"copy:",
+ @"key" : @"c"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Paste",
+ @"action" : @"paste:",
+ @"key" : @"v"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Select All",
+ @"action" : @"selectAll:",
+ @"key" : @"a"
+ }]
+ ]
+ },
+ @{
+ @"name": @"View",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Fullscreen",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle fullscreen"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Float on Top",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle ontop"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Visibility on All Workspaces",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle on-all-workspaces"
+ }],
+#if HAVE_MACOS_TOUCHBAR
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Customize Touch Bar…",
+ @"action" : @"toggleTouchBarCustomizationPalette:",
+ @"key" : @"",
+ @"target" : NSApp
+ }]
+#endif
+ ]
+ },
+ @{
+ @"name": @"Video",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Zoom Out",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add panscan -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Zoom In",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add panscan 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Zoom",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set panscan 0"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 4:3",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"4:3\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 16:9",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"16:9\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 1.85:1",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"1.85:1\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 2.35:1",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"2.35:1\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Aspect Ratio",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"-1\""
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Rotate Left",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values video-rotate 0 270 180 90"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Rotate Right",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values video-rotate 90 180 270 0"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Rotation",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-rotate 0"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Half Size",
+ @"key" : @"0",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_H_SIZE]
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Normal Size",
+ @"key" : @"1",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_N_SIZE]
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Double Size",
+ @"key" : @"2",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_D_SIZE]
+ }]
+ ]
+ },
+ @{
+ @"name": @"Audio",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next Audio Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle audio"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous Audio Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle audio down"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Mute",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle mute"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Play Audio Later",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add audio-delay 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Play Audio Earlier",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add audio-delay -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Audio Delay",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set audio-delay 0.0 "
+ }]
+ ]
+ },
+ @{
+ @"name": @"Subtitle",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next Subtitle Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle sub"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous Subtitle Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle sub down"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Force Style",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values sub-ass-override \"force\" \"no\""
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Display Subtitles Later",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add sub-delay 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Display Subtitles Earlier",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add sub-delay -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Subtitle Delay",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set sub-delay 0.0"
+ }]
+ ]
+ },
+ @{
+ @"name": @"Playback",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Pause",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle pause"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Increase Speed",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add speed 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Decrease Speed",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add speed -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Speed",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set speed 1.0"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show Playlist",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"script-message osc-playlist"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show Chapters",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"script-message osc-chapterlist"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show Tracks",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"script-message osc-tracklist"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next File",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"playlist-next"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous File",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"playlist-prev"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Loop File",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values loop-file \"inf\" \"no\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Loop Playlist",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values loop-playlist \"inf\" \"no\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Shuffle",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"playlist-shuffle"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next Chapter",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add chapter 1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous Chapter",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add chapter -1"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Step Forward",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"frame-step"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Step Backward",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"frame-back-step"
+ }]
+ ]
+ },
+ @{
+ @"name": @"Window",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Minimize",
+ @"key" : @"m",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_MINIMIZE]
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Zoom",
+ @"key" : @"z",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_ZOOM]
+ }]
+ ]
+ },
+ @{
+ @"name": @"Help",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"mpv Website…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://mpv.io"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"mpv on github…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Online Manual…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://mpv.io/manual/master/"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Online Wiki…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/wiki"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Release Notes…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/blob/master/RELEASE_NOTES"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Keyboard Shortcuts…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/blob/master/etc/input.conf"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Report Issue…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/issues/new/choose"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show log File…",
+ @"action" : @"showFile:",
+ @"key" : @"",
+ @"target" : self,
+ @"file" : @"~/Library/Logs/mpv.log",
+ @"alertTitle" : @"No log File found.",
+ @"alertText" : @"You deactivated logging for the Bundle."
+ }]
+ ]
+ }
+ ];
+
+ [NSApp setMainMenu:[self mainMenu]];
+ }
+
+ return self;
+}
+
+- (NSMenu *)mainMenu
+{
+ NSMenu *mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
+ [NSApp setServicesMenu:[[NSMenu alloc] init]];
+ NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"];
+
+ for(id mMenu in menuTree) {
+ NSMenu *menu = [[NSMenu alloc] initWithTitle:mMenu[@"name"]];
+ NSMenuItem *mItem = [mainMenu addItemWithTitle:mMenu[@"name"]
+ action:nil
+ keyEquivalent:@""];
+ [mainMenu setSubmenu:menu forItem:mItem];
+
+ for(id subMenu in mMenu[@"menu"]) {
+ NSString *name = subMenu[@"name"];
+ NSString *action = subMenu[@"action"];
+
+#if HAVE_MACOS_TOUCHBAR
+ if ([action isEqual:@"toggleTouchBarCustomizationPalette:"]) {
+ continue;
+ }
+#endif
+
+ if ([name isEqual:@"Show log File…"] && ![bundle isEqual:@"true"]) {
+ continue;
+ }
+
+ if ([name isEqual:@"separator"]) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ } else {
+ NSMenuItem *iItem = [menu addItemWithTitle:name
+ action:NSSelectorFromString(action)
+ keyEquivalent:subMenu[@"key"]];
+ [iItem setTarget:subMenu[@"target"]];
+ [subMenu setObject:iItem forKey:@"menuItem"];
+
+ NSNumber *m = subMenu[@"modifiers"];
+ if (m) {
+ [iItem setKeyEquivalentModifierMask:m.unsignedIntegerValue];
+ }
+
+ if ([subMenu[@"name"] isEqual:@"Services"]) {
+ iItem.submenu = [NSApp servicesMenu];
+ }
+ }
+ }
+ }
+
+ return mainMenu;
+}
+
+- (void)about
+{
+ NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
+ @"mpv", @"ApplicationName",
+ [(Application *)NSApp getMPVIcon], @"ApplicationIcon",
+ [NSString stringWithUTF8String:mpv_copyright], @"Copyright",
+ [NSString stringWithUTF8String:mpv_version], @"ApplicationVersion",
+ nil];
+ [NSApp orderFrontStandardAboutPanelWithOptions:options];
+}
+
+- (void)preferences:(NSMenuItem *)menuItem
+{
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSMutableDictionary *mItemDict = [self getDictFromMenuItem:menuItem];
+ NSArray *configPaths = @[
+ [NSString stringWithFormat:@"%@/.mpv/", NSHomeDirectory()],
+ [NSString stringWithFormat:@"%@/.config/mpv/", NSHomeDirectory()]];
+
+ for (id path in configPaths) {
+ NSString *fileP = [path stringByAppendingString:mItemDict[@"file"]];
+ if ([fileManager fileExistsAtPath:fileP]){
+ if ([workspace openFile:fileP])
+ return;
+ [workspace openFile:path];
+ [self alertWithTitle:mItemDict[@"alertTitle1"]
+ andText:mItemDict[@"alertText1"]];
+ return;
+ }
+ if ([workspace openFile:path]) {
+ [self alertWithTitle:mItemDict[@"alertTitle2"]
+ andText:mItemDict[@"alertText2"]];
+ return;
+ }
+ }
+
+ [self alertWithTitle:mItemDict[@"alertTitle3"]
+ andText:mItemDict[@"alertText3"]];
+}
+
+- (void)quit:(NSMenuItem *)menuItem
+{
+ NSString *cmd = [self getDictFromMenuItem:menuItem][@"cmd"];
+ [(Application *)NSApp stopMPV:(char *)[cmd UTF8String]];
+}
+
+- (void)openFile
+{
+ NSOpenPanel *panel = [[NSOpenPanel alloc] init];
+ [panel setCanChooseDirectories:YES];
+ [panel setAllowsMultipleSelection:YES];
+
+ if ([panel runModal] == NSModalResponseOK){
+ NSMutableArray *fileArray = [[NSMutableArray alloc] init];
+ for (id url in [panel URLs])
+ [fileArray addObject:[url path]];
+ [(Application *)NSApp openFiles:fileArray];
+ }
+}
+
+- (void)openPlaylist
+{
+ NSOpenPanel *panel = [[NSOpenPanel alloc] init];
+
+ if ([panel runModal] == NSModalResponseOK){
+ NSString *pl = [NSString stringWithFormat:@"loadlist \"%@\"",
+ [panel URLs][0].path];
+ [(Application *)NSApp queueCommand:(char *)[pl UTF8String]];
+ }
+}
+
+- (void)openURL
+{
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:@"Open URL"];
+ [alert addButtonWithTitle:@"Ok"];
+ [alert addButtonWithTitle:@"Cancel"];
+ [alert setIcon:[(Application *)NSApp getMPVIcon]];
+
+ NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)];
+ [input setPlaceholderString:@"URL"];
+ [alert setAccessoryView:input];
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [input becomeFirstResponder];
+ });
+
+ if ([alert runModal] == NSAlertFirstButtonReturn && [input stringValue].length > 0) {
+ NSArray *url = [NSArray arrayWithObjects:[input stringValue], nil];
+ [(Application *)NSApp openFiles:url];
+ }
+}
+
+- (void)cmd:(NSMenuItem *)menuItem
+{
+ NSString *cmd = [self getDictFromMenuItem:menuItem][@"cmd"];
+ [(Application *)NSApp queueCommand:(char *)[cmd UTF8String]];
+}
+
+- (void)url:(NSMenuItem *)menuItem
+{
+ NSString *url = [self getDictFromMenuItem:menuItem][@"url"];
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
+}
+
+- (void)showFile:(NSMenuItem *)menuItem
+{
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSMutableDictionary *mItemDict = [self getDictFromMenuItem:menuItem];
+ NSString *file = [mItemDict[@"file"] stringByExpandingTildeInPath];
+
+ if ([fileManager fileExistsAtPath:file]){
+ NSURL *url = [NSURL fileURLWithPath:file];
+ NSArray *urlArray = [NSArray arrayWithObjects:url, nil];
+
+ [workspace activateFileViewerSelectingURLs:urlArray];
+ return;
+ }
+
+ [self alertWithTitle:mItemDict[@"alertTitle"]
+ andText:mItemDict[@"alertText"]];
+}
+
+- (void)alertWithTitle:(NSString *)title andText:(NSString *)text
+{
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:title];
+ [alert setInformativeText:text];
+ [alert addButtonWithTitle:@"Ok"];
+ [alert setIcon:[(Application *)NSApp getMPVIcon]];
+ [alert runModal];
+}
+
+- (NSMutableDictionary *)getDictFromMenuItem:(NSMenuItem *)menuItem
+{
+ for(id mMenu in menuTree) {
+ for(id subMenu in mMenu[@"menu"]) {
+ if([subMenu[@"menuItem"] isEqual:menuItem])
+ return subMenu;
+ }
+ }
+
+ return nil;
+}
+
+- (void)registerSelector:(SEL)action forKey:(MPMenuKey)key
+{
+ for(id mMenu in menuTree) {
+ for(id subMenu in mMenu[@"menu"]) {
+ if([subMenu[@"cmdSpecial"] isEqual:[NSNumber numberWithInt:key]]) {
+ [subMenu[@"menuItem"] setAction:action];
+ return;
+ }
+ }
+ }
+}
+
+@end
diff --git a/osdep/macosx_menubar_objc.h b/osdep/macosx_menubar_objc.h
new file mode 100644
index 0000000..072fef8
--- /dev/null
+++ b/osdep/macosx_menubar_objc.h
@@ -0,0 +1,25 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#include "osdep/macosx_menubar.h"
+
+@interface MenuBar : NSObject
+
+- (void)registerSelector:(SEL)action forKey:(MPMenuKey)key;
+
+@end
diff --git a/osdep/macosx_touchbar.h b/osdep/macosx_touchbar.h
new file mode 100644
index 0000000..a03b68c
--- /dev/null
+++ b/osdep/macosx_touchbar.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#import "osdep/macosx_application_objc.h"
+
+#define BASE_ID @"io.mpv.touchbar"
+static NSTouchBarCustomizationIdentifier customID = BASE_ID;
+static NSTouchBarItemIdentifier seekBar = BASE_ID ".seekbar";
+static NSTouchBarItemIdentifier play = BASE_ID ".play";
+static NSTouchBarItemIdentifier nextItem = BASE_ID ".nextItem";
+static NSTouchBarItemIdentifier previousItem = BASE_ID ".previousItem";
+static NSTouchBarItemIdentifier nextChapter = BASE_ID ".nextChapter";
+static NSTouchBarItemIdentifier previousChapter = BASE_ID ".previousChapter";
+static NSTouchBarItemIdentifier cycleAudio = BASE_ID ".cycleAudio";
+static NSTouchBarItemIdentifier cycleSubtitle = BASE_ID ".cycleSubtitle";
+static NSTouchBarItemIdentifier currentPosition = BASE_ID ".currentPosition";
+static NSTouchBarItemIdentifier timeLeft = BASE_ID ".timeLeft";
+
+struct mpv_event;
+
+@interface TouchBar : NSTouchBar <NSTouchBarDelegate>
+
+-(void)processEvent:(struct mpv_event *)event;
+
+@property(nonatomic, retain) Application *app;
+@property(nonatomic, retain) NSDictionary *touchbarItems;
+@property(nonatomic, assign) double duration;
+@property(nonatomic, assign) double position;
+@property(nonatomic, assign) int pause;
+
+@end
diff --git a/osdep/macosx_touchbar.m b/osdep/macosx_touchbar.m
new file mode 100644
index 0000000..ccce8f7
--- /dev/null
+++ b/osdep/macosx_touchbar.m
@@ -0,0 +1,334 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "player/client.h"
+#import "macosx_touchbar.h"
+
+@implementation TouchBar
+
+@synthesize app = _app;
+@synthesize touchbarItems = _touchbar_items;
+@synthesize duration = _duration;
+@synthesize position = _position;
+@synthesize pause = _pause;
+
+- (id)init
+{
+ if (self = [super init]) {
+ self.touchbarItems = @{
+ seekBar: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"slider",
+ @"name": @"Seek Bar",
+ @"cmd": @"seek %f absolute-percent"
+ }],
+ play: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Play Button",
+ @"cmd": @"cycle pause",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarPauseTemplate],
+ @"imageAlt": [NSImage imageNamed:NSImageNameTouchBarPlayTemplate]
+ }],
+ previousItem: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Previous Playlist Item",
+ @"cmd": @"playlist-prev",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarGoBackTemplate]
+ }],
+ nextItem: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Next Playlist Item",
+ @"cmd": @"playlist-next",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarGoForwardTemplate]
+ }],
+ previousChapter: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Previous Chapter",
+ @"cmd": @"add chapter -1",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarSkipBackTemplate]
+ }],
+ nextChapter: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Next Chapter",
+ @"cmd": @"add chapter 1",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarSkipAheadTemplate]
+ }],
+ cycleAudio: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Cycle Audio",
+ @"cmd": @"cycle audio",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarAudioInputTemplate]
+ }],
+ cycleSubtitle: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Cycle Subtitle",
+ @"cmd": @"cycle sub",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarComposeTemplate]
+ }],
+ currentPosition: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"text",
+ @"name": @"Current Position"
+ }],
+ timeLeft: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"text",
+ @"name": @"Time Left"
+ }]
+ };
+
+ [self addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ }
+ return self;
+}
+
+- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier
+{
+ if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"slider"]) {
+ NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
+ NSSlider *slider = [NSSlider sliderWithTarget:self action:@selector(seekbarChanged:)];
+ slider.minValue = 0.0f;
+ slider.maxValue = 100.0f;
+ tbItem.view = slider;
+ tbItem.customizationLabel = self.touchbarItems[identifier][@"name"];
+ [self.touchbarItems[identifier] setObject:slider forKey:@"view"];
+ [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"];
+ [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ return tbItem;
+ } else if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"button"]) {
+ NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
+ NSImage *tbImage = self.touchbarItems[identifier][@"image"];
+ NSButton *tbButton = [NSButton buttonWithImage:tbImage target:self action:@selector(buttonAction:)];
+ tbItem.view = tbButton;
+ tbItem.customizationLabel = self.touchbarItems[identifier][@"name"];
+ [self.touchbarItems[identifier] setObject:tbButton forKey:@"view"];
+ [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"];
+ [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ return tbItem;
+ } else if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"text"]) {
+ NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
+ NSTextField *tbText = [NSTextField labelWithString:@"0:00"];
+ tbText.alignment = NSTextAlignmentCenter;
+ tbItem.view = tbText;
+ tbItem.customizationLabel = self.touchbarItems[identifier][@"name"];
+ [self.touchbarItems[identifier] setObject:tbText forKey:@"view"];
+ [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"];
+ [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ return tbItem;
+ }
+
+ return nil;
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey,id> *)change
+ context:(void *)context {
+ if ([keyPath isEqualToString:@"visible"]) {
+ NSNumber *visible = [object valueForKey:@"visible"];
+ if (visible.boolValue) {
+ [self updateTouchBarTimeItems];
+ [self updatePlayButton];
+ }
+ }
+}
+
+- (void)updateTouchBarTimeItems
+{
+ if (!self.isVisible)
+ return;
+
+ [self updateSlider];
+ [self updateTimeLeft];
+ [self updateCurrentPosition];
+}
+
+- (void)updateSlider
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[seekBar][@"tbItem"];
+ if (!tbItem.visible)
+ return;
+
+ NSSlider *seekSlider = self.touchbarItems[seekBar][@"view"];
+
+ if (self.duration <= 0) {
+ seekSlider.enabled = NO;
+ seekSlider.doubleValue = 0;
+ } else {
+ seekSlider.enabled = YES;
+ if (!seekSlider.highlighted)
+ seekSlider.doubleValue = (self.position/self.duration)*100;
+ }
+}
+
+- (void)updateTimeLeft
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[timeLeft][@"tbItem"];
+ if (!tbItem.visible)
+ return;
+
+ NSTextField *timeLeftItem = self.touchbarItems[timeLeft][@"view"];
+
+ [self removeConstraintForIdentifier:timeLeft];
+ if (self.duration <= 0) {
+ timeLeftItem.stringValue = @"";
+ } else {
+ int left = (int)(floor(self.duration)-floor(self.position));
+ NSString *leftFormat = [self formatTime:left];
+ NSString *durFormat = [self formatTime:self.duration];
+ timeLeftItem.stringValue = [NSString stringWithFormat:@"-%@", leftFormat];
+ [self applyConstraintFromString:[NSString stringWithFormat:@"-%@", durFormat]
+ forIdentifier:timeLeft];
+ }
+}
+
+- (void)updateCurrentPosition
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[currentPosition][@"tbItem"];
+ if (!tbItem.visible)
+ return;
+
+ NSTextField *curPosItem = self.touchbarItems[currentPosition][@"view"];
+ NSString *posFormat = [self formatTime:(int)floor(self.position)];
+ curPosItem.stringValue = posFormat;
+
+ [self removeConstraintForIdentifier:currentPosition];
+ if (self.duration <= 0) {
+ [self applyConstraintFromString:[self formatTime:self.position]
+ forIdentifier:currentPosition];
+ } else {
+ NSString *durFormat = [self formatTime:self.duration];
+ [self applyConstraintFromString:durFormat forIdentifier:currentPosition];
+ }
+}
+
+- (void)updatePlayButton
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[play][@"tbItem"];
+ if (!self.isVisible || !tbItem.visible)
+ return;
+
+ NSButton *playButton = self.touchbarItems[play][@"view"];
+ if (self.pause) {
+ playButton.image = self.touchbarItems[play][@"imageAlt"];
+ } else {
+ playButton.image = self.touchbarItems[play][@"image"];
+ }
+}
+
+- (void)buttonAction:(NSButton *)sender
+{
+ NSString *identifier = [self getIdentifierFromView:sender];
+ [self.app queueCommand:(char *)[self.touchbarItems[identifier][@"cmd"] UTF8String]];
+}
+
+- (void)seekbarChanged:(NSSlider *)slider
+{
+ NSString *identifier = [self getIdentifierFromView:slider];
+ NSString *seek = [NSString stringWithFormat:
+ self.touchbarItems[identifier][@"cmd"], slider.doubleValue];
+ [self.app queueCommand:(char *)[seek UTF8String]];
+}
+
+- (NSString *)formatTime:(int)time
+{
+ int seconds = time % 60;
+ int minutes = (time / 60) % 60;
+ int hours = time / (60 * 60);
+
+ NSString *stime = hours > 0 ? [NSString stringWithFormat:@"%d:", hours] : @"";
+ stime = (stime.length > 0 || minutes > 9) ?
+ [NSString stringWithFormat:@"%@%02d:", stime, minutes] :
+ [NSString stringWithFormat:@"%d:", minutes];
+ stime = [NSString stringWithFormat:@"%@%02d", stime, seconds];
+
+ return stime;
+}
+
+- (void)removeConstraintForIdentifier:(NSTouchBarItemIdentifier)identifier
+{
+ NSTextField *field = self.touchbarItems[identifier][@"view"];
+ [field removeConstraint:self.touchbarItems[identifier][@"constrain"]];
+}
+
+- (void)applyConstraintFromString:(NSString *)string
+ forIdentifier:(NSTouchBarItemIdentifier)identifier
+{
+ NSTextField *field = self.touchbarItems[identifier][@"view"];
+ if (field) {
+ NSString *fString = [[string componentsSeparatedByCharactersInSet:
+ [NSCharacterSet decimalDigitCharacterSet]] componentsJoinedByString:@"0"];
+ NSTextField *textField = [NSTextField labelWithString:fString];
+ NSSize size = [textField frame].size;
+
+ NSLayoutConstraint *con =
+ [NSLayoutConstraint constraintWithItem:field
+ attribute:NSLayoutAttributeWidth
+ relatedBy:NSLayoutRelationEqual
+ toItem:nil
+ attribute:NSLayoutAttributeNotAnAttribute
+ multiplier:1.0
+ constant:(int)ceil(size.width*1.1)];
+ [field addConstraint:con];
+ [self.touchbarItems[identifier] setObject:con forKey:@"constrain"];
+ }
+}
+
+- (NSString *)getIdentifierFromView:(id)view
+{
+ NSString *identifier;
+ for (identifier in self.touchbarItems)
+ if([self.touchbarItems[identifier][@"view"] isEqual:view])
+ break;
+ return identifier;
+}
+
+- (void)processEvent:(struct mpv_event *)event
+{
+ switch (event->event_id) {
+ case MPV_EVENT_END_FILE: {
+ self.position = 0;
+ self.duration = 0;
+ break;
+ }
+ case MPV_EVENT_PROPERTY_CHANGE: {
+ [self handlePropertyChange:(mpv_event_property *)event->data];
+ break;
+ }
+ }
+}
+
+- (void)handlePropertyChange:(struct mpv_event_property *)property
+{
+ NSString *name = [NSString stringWithUTF8String:property->name];
+ mpv_format format = property->format;
+
+ if ([name isEqualToString:@"time-pos"] && format == MPV_FORMAT_DOUBLE) {
+ double newPosition = *(double *)property->data;
+ newPosition = newPosition < 0 ? 0 : newPosition;
+ if ((int)(floor(newPosition) - floor(self.position)) != 0) {
+ self.position = newPosition;
+ [self updateTouchBarTimeItems];
+ }
+ } else if ([name isEqualToString:@"duration"] && format == MPV_FORMAT_DOUBLE) {
+ self.duration = *(double *)property->data;
+ [self updateTouchBarTimeItems];
+ } else if ([name isEqualToString:@"pause"] && format == MPV_FORMAT_FLAG) {
+ self.pause = *(int *)property->data;
+ [self updatePlayButton];
+ }
+}
+
+@end
diff --git a/osdep/main-fn-cocoa.c b/osdep/main-fn-cocoa.c
new file mode 100644
index 0000000..eeed127
--- /dev/null
+++ b/osdep/main-fn-cocoa.c
@@ -0,0 +1,10 @@
+#include "osdep/macosx_application.h"
+
+// This is needed because Cocoa absolutely requires creating the NSApplication
+// singleton and running it in the "main" thread. It is apparently not
+// possible to do this on a separate thread at all. It is not known how
+// Apple managed this colossal fuckup.
+int main(int argc, char *argv[])
+{
+ return cocoa_main(argc, argv);
+}
diff --git a/osdep/main-fn-unix.c b/osdep/main-fn-unix.c
new file mode 100644
index 0000000..c30c4a9
--- /dev/null
+++ b/osdep/main-fn-unix.c
@@ -0,0 +1,6 @@
+#include "main-fn.h"
+
+int main(int argc, char *argv[])
+{
+ return mpv_main(argc, argv);
+}
diff --git a/osdep/main-fn-win.c b/osdep/main-fn-win.c
new file mode 100644
index 0000000..16ea80b
--- /dev/null
+++ b/osdep/main-fn-win.c
@@ -0,0 +1,93 @@
+#include <windows.h>
+
+#ifndef BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE
+#define BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE (0x0001)
+#endif
+
+#include "common/common.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+#include "osdep/main-fn.h"
+
+#ifndef HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION
+
+#define HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION 1
+enum { HeapOptimizeResources = 3 };
+
+struct HEAP_OPTIMIZE_RESOURCES_INFORMATION {
+ DWORD Version;
+ DWORD Flags;
+};
+
+#endif
+
+static bool is_valid_handle(HANDLE h)
+{
+ return h != INVALID_HANDLE_VALUE && h != NULL &&
+ GetFileType(h) != FILE_TYPE_UNKNOWN;
+}
+
+static bool has_redirected_stdio(void)
+{
+ return is_valid_handle(GetStdHandle(STD_INPUT_HANDLE)) ||
+ is_valid_handle(GetStdHandle(STD_OUTPUT_HANDLE)) ||
+ is_valid_handle(GetStdHandle(STD_ERROR_HANDLE));
+}
+
+static void microsoft_nonsense(void)
+{
+ // stop Windows from showing all kinds of annoying error dialogs
+ SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
+
+ // Enable heap corruption detection
+ HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);
+
+ // Allow heap cache optimization and memory decommit
+ struct HEAP_OPTIMIZE_RESOURCES_INFORMATION heap_info = {
+ .Version = HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION
+ };
+ HeapSetInformation(NULL, HeapOptimizeResources, &heap_info,
+ sizeof(heap_info));
+
+ // Always use safe search paths for DLLs and other files, ie. never use the
+ // current directory
+ SetDllDirectoryW(L"");
+ SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE |
+ BASE_SEARCH_PATH_PERMANENT);
+}
+
+int main(int argc_, char **argv_)
+{
+ microsoft_nonsense();
+
+ // If started from the console wrapper (see osdep/win32-console-wrapper.c),
+ // attach to the console and set up the standard IO handles
+ bool has_console = terminal_try_attach();
+
+ // If mpv is started from Explorer, the Run dialog or the Start Menu, it
+ // will have no console and no standard IO handles. In this case, the user
+ // is expecting mpv to show some UI, so enable the pseudo-GUI profile.
+ bool gui = !has_console && !has_redirected_stdio();
+
+ int argc = 0;
+ wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
+
+ int argv_len = 0;
+ char **argv_u8 = NULL;
+
+ // Build mpv's UTF-8 argv, and add the pseudo-GUI profile if necessary
+ if (argc > 0 && argv[0])
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len, mp_to_utf8(argv_u8, argv[0]));
+ if (gui) {
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len,
+ "--player-operation-mode=pseudo-gui");
+ }
+ for (int i = 1; i < argc; i++)
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len, mp_to_utf8(argv_u8, argv[i]));
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len, NULL);
+
+ int ret = mpv_main(argv_len - 1, argv_u8);
+
+ talloc_free(argv_u8);
+ return ret;
+}
diff --git a/osdep/main-fn.h b/osdep/main-fn.h
new file mode 100644
index 0000000..8f20308
--- /dev/null
+++ b/osdep/main-fn.h
@@ -0,0 +1 @@
+int mpv_main(int argc, char *argv[]);
diff --git a/osdep/meson.build b/osdep/meson.build
new file mode 100644
index 0000000..21baafc
--- /dev/null
+++ b/osdep/meson.build
@@ -0,0 +1,51 @@
+# custom swift targets
+bridge = join_paths(source_root, 'osdep/macOS_swift_bridge.h')
+header = join_paths(build_root, 'osdep/macOS_swift.h')
+module = join_paths(build_root, 'osdep/macOS_swift.swiftmodule')
+target = join_paths(build_root, 'osdep/macOS_swift.o')
+
+swift_flags = ['-frontend', '-c', '-sdk', macos_sdk_path,
+ '-enable-objc-interop', '-emit-objc-header', '-parse-as-library']
+
+if swift_ver.version_compare('>=6.0')
+ swift_flags += ['-swift-version', '5']
+endif
+
+if get_option('debug')
+ swift_flags += '-g'
+endif
+
+if get_option('optimization') != '0'
+ swift_flags += '-O'
+endif
+
+extra_flags = get_option('swift-flags').split()
+swift_flags += extra_flags
+
+swift_compile = [swift_prog, swift_flags, '-module-name', 'macOS_swift',
+ '-emit-module-path', '@OUTPUT0@', '-import-objc-header', bridge,
+ '-emit-objc-header-path', '@OUTPUT1@', '-o', '@OUTPUT2@',
+ '@INPUT@', '-I.', '-I' + source_root,
+ '-I' + libplacebo.get_variable('includedir',
+ default_value: source_root / 'subprojects' / 'libplacebo' / 'src' / 'include')]
+
+swift_targets = custom_target('swift_targets',
+ input: swift_sources,
+ output: ['macOS_swift.swiftmodule', 'macOS_swift.h', 'macOS_swift.o'],
+ command: swift_compile,
+)
+sources += swift_targets
+
+swift_lib_dir_py = find_program(join_paths(tools_directory, 'macos-swift-lib-directory.py'))
+swift_lib_dir = run_command(swift_lib_dir_py, swift_prog.full_path(), check: true).stdout()
+message('Detected Swift library directory: ' + swift_lib_dir)
+
+# linker flags
+swift_link_flags = ['-L' + swift_lib_dir, '-Xlinker', '-rpath',
+ '-Xlinker', swift_lib_dir, '-rdynamic', '-Xlinker',
+ '-add_ast_path', '-Xlinker', module]
+if swift_ver.version_compare('>=5.0')
+ swift_link_flags += ['-Xlinker', '-rpath', '-Xlinker',
+ '/usr/lib/swift', '-L/usr/lib/swift']
+endif
+add_project_link_arguments(swift_link_flags, language: ['c', 'objc'])
diff --git a/osdep/mpv.exe.manifest b/osdep/mpv.exe.manifest
new file mode 100644
index 0000000..32dd80b
--- /dev/null
+++ b/osdep/mpv.exe.manifest
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <assemblyIdentity
+ version="0.0.9.0"
+ processorArchitecture="*"
+ name="mpv"
+ type="win32"
+ />
+ <description>mpv - The Movie Player</description>
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings>
+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
+ <activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
+ </windowsSettings>
+ </application>
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel
+ level="asInvoker"
+ uiAccess="false"
+ />
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ <!-- Windows 8.1 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <!-- Windows 8 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <!-- Windows 7 -->
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ <!-- Windows Vista -->
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/osdep/mpv.rc b/osdep/mpv.rc
new file mode 100644
index 0000000..7deb785
--- /dev/null
+++ b/osdep/mpv.rc
@@ -0,0 +1,50 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <winver.h>
+#include "version.h"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 2, 0, 0, 0
+ PRODUCTVERSION 2, 0, 0, 0
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+ FILEFLAGS 0
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0
+ {
+ BLOCK "StringFileInfo" {
+ BLOCK "000004b0" {
+ VALUE "Comments", "mpv is distributed under the terms of the GNU General Public License Version 2 or later."
+ VALUE "CompanyName", "mpv"
+ VALUE "FileDescription", "mpv"
+ VALUE "FileVersion", VERSION
+ VALUE "LegalCopyright", MPVCOPYRIGHT
+ VALUE "OriginalFilename", "mpv.exe"
+ VALUE "ProductName", "mpv"
+ VALUE "ProductVersion", VERSION
+ }
+ }
+ BLOCK "VarFileInfo" {
+ VALUE "Translation", 0, 1200
+ }
+ }
+
+IDI_ICON1 ICON DISCARDABLE "etc/mpv-icon.ico"
+
+// for some reason RT_MANIFEST does not work
+1 24 "mpv.exe.manifest"
diff --git a/osdep/path-darwin.c b/osdep/path-darwin.c
new file mode 100644
index 0000000..9c7fcda
--- /dev/null
+++ b/osdep/path-darwin.c
@@ -0,0 +1,77 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "options/path.h"
+#include "osdep/threads.h"
+#include "path.h"
+
+#include "config.h"
+
+static mp_once path_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+static char mpv_home[512];
+static char old_home[512];
+static char mpv_cache[512];
+static char old_cache[512];
+
+static void path_init(void)
+{
+ char *home = getenv("HOME");
+ char *xdg_config = getenv("XDG_CONFIG_HOME");
+
+ if (xdg_config && xdg_config[0]) {
+ snprintf(mpv_home, sizeof(mpv_home), "%s/mpv", xdg_config);
+ } else if (home && home[0]) {
+ snprintf(mpv_home, sizeof(mpv_home), "%s/.config/mpv", home);
+ }
+
+ // Maintain compatibility with old ~/.mpv
+ if (home && home[0]) {
+ snprintf(old_home, sizeof(old_home), "%s/.mpv", home);
+ snprintf(old_cache, sizeof(old_cache), "%s/.mpv/cache", home);
+ }
+
+ if (home && home[0])
+ snprintf(mpv_cache, sizeof(mpv_cache), "%s/Library/Caches/io.mpv", home);
+
+ // If the old ~/.mpv exists, and the XDG config dir doesn't, use the old
+ // config dir only.
+ if (mp_path_exists(old_home) && !mp_path_exists(mpv_home)) {
+ snprintf(mpv_home, sizeof(mpv_home), "%s", old_home);
+ snprintf(mpv_cache, sizeof(mpv_cache), "%s", old_cache);
+ old_home[0] = '\0';
+ old_cache[0] = '\0';
+ }
+}
+
+const char *mp_get_platform_path_darwin(void *talloc_ctx, const char *type)
+{
+ mp_exec_once(&path_init_once, path_init);
+ if (strcmp(type, "home") == 0)
+ return mpv_home;
+ if (strcmp(type, "old_home") == 0)
+ return old_home;
+ if (strcmp(type, "cache") == 0)
+ return mpv_cache;
+ if (strcmp(type, "global") == 0)
+ return MPV_CONFDIR;
+ if (strcmp(type, "desktop") == 0)
+ return getenv("HOME");
+ return NULL;
+}
diff --git a/osdep/path-macosx.m b/osdep/path-macosx.m
new file mode 100644
index 0000000..8a5a704
--- /dev/null
+++ b/osdep/path-macosx.m
@@ -0,0 +1,34 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Foundation/Foundation.h>
+#include "options/path.h"
+#include "osdep/path.h"
+
+const char *mp_get_platform_path_osx(void *talloc_ctx, const char *type)
+{
+ if (strcmp(type, "osxbundle") == 0 && getenv("MPVBUNDLE")) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSString *path = [[NSBundle mainBundle] resourcePath];
+ char *res = talloc_strdup(talloc_ctx, [path UTF8String]);
+ [pool release];
+ return res;
+ }
+ if (strcmp(type, "desktop") == 0 && getenv("HOME"))
+ return mp_path_join(talloc_ctx, getenv("HOME"), "Desktop");
+ return NULL;
+}
diff --git a/osdep/path-unix.c b/osdep/path-unix.c
new file mode 100644
index 0000000..eae8b60
--- /dev/null
+++ b/osdep/path-unix.c
@@ -0,0 +1,100 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "options/path.h"
+#include "osdep/threads.h"
+#include "path.h"
+
+#include "config.h"
+
+static mp_once path_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+#define CONF_MAX 512
+static char mpv_home[CONF_MAX];
+static char old_home[CONF_MAX];
+static char mpv_cache[CONF_MAX];
+static char old_cache[CONF_MAX];
+static char mpv_state[CONF_MAX];
+#define MKPATH(BUF, ...) (snprintf((BUF), CONF_MAX, __VA_ARGS__) >= CONF_MAX)
+
+static void path_init(void)
+{
+ char *home = getenv("HOME");
+ char *xdg_cache = getenv("XDG_CACHE_HOME");
+ char *xdg_config = getenv("XDG_CONFIG_HOME");
+ char *xdg_state = getenv("XDG_STATE_HOME");
+
+ bool err = false;
+ if (xdg_config && xdg_config[0]) {
+ err = err || MKPATH(mpv_home, "%s/mpv", xdg_config);
+ } else if (home && home[0]) {
+ err = err || MKPATH(mpv_home, "%s/.config/mpv", home);
+ }
+
+ // Maintain compatibility with old ~/.mpv
+ if (home && home[0]) {
+ err = err || MKPATH(old_home, "%s/.mpv", home);
+ err = err || MKPATH(old_cache, "%s/.mpv/cache", home);
+ }
+
+ if (xdg_cache && xdg_cache[0]) {
+ err = err || MKPATH(mpv_cache, "%s/mpv", xdg_cache);
+ } else if (home && home[0]) {
+ err = err || MKPATH(mpv_cache, "%s/.cache/mpv", home);
+ }
+
+ if (xdg_state && xdg_state[0]) {
+ err = err || MKPATH(mpv_state, "%s/mpv", xdg_state);
+ } else if (home && home[0]) {
+ err = err || MKPATH(mpv_state, "%s/.local/state/mpv", home);
+ }
+
+ // If the old ~/.mpv exists, and the XDG config dir doesn't, use the old
+ // config dir only. Also do not use any other XDG directories.
+ if (mp_path_exists(old_home) && !mp_path_exists(mpv_home)) {
+ err = err || MKPATH(mpv_home, "%s", old_home);
+ err = err || MKPATH(mpv_cache, "%s", old_cache);
+ err = err || MKPATH(mpv_state, "%s", old_home);
+ old_home[0] = '\0';
+ old_cache[0] = '\0';
+ }
+
+ if (err) {
+ fprintf(stderr, "Config dir exceeds %d bytes\n", CONF_MAX);
+ abort();
+ }
+}
+
+const char *mp_get_platform_path_unix(void *talloc_ctx, const char *type)
+{
+ mp_exec_once(&path_init_once, path_init);
+ if (strcmp(type, "home") == 0)
+ return mpv_home;
+ if (strcmp(type, "old_home") == 0)
+ return old_home;
+ if (strcmp(type, "cache") == 0)
+ return mpv_cache;
+ if (strcmp(type, "state") == 0)
+ return mpv_state;
+ if (strcmp(type, "global") == 0)
+ return MPV_CONFDIR;
+ if (strcmp(type, "desktop") == 0)
+ return getenv("HOME");
+ return NULL;
+}
diff --git a/osdep/path-uwp.c b/osdep/path-uwp.c
new file mode 100644
index 0000000..7eafb03
--- /dev/null
+++ b/osdep/path-uwp.c
@@ -0,0 +1,35 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+
+#include "osdep/path.h"
+#include "osdep/io.h"
+#include "options/path.h"
+
+// Missing from MinGW headers.
+WINBASEAPI DWORD WINAPI GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer);
+
+const char *mp_get_platform_path_uwp(void *talloc_ctx, const char *type)
+{
+ if (strcmp(type, "home") == 0) {
+ wchar_t homeDir[_MAX_PATH];
+ if (GetCurrentDirectoryW(_MAX_PATH, homeDir) != 0)
+ return mp_to_utf8(talloc_ctx, homeDir);
+ }
+ return NULL;
+}
diff --git a/osdep/path-win.c b/osdep/path-win.c
new file mode 100644
index 0000000..bddf5a5
--- /dev/null
+++ b/osdep/path-win.c
@@ -0,0 +1,113 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <shlobj.h>
+#include <knownfolders.h>
+
+#include "options/path.h"
+#include "osdep/io.h"
+#include "osdep/path.h"
+#include "osdep/threads.h"
+
+// Warning: do not use PATH_MAX. Cygwin messed it up.
+
+static mp_once path_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+static char *portable_path;
+
+static char *mp_get_win_exe_dir(void *talloc_ctx)
+{
+ wchar_t w_exedir[MAX_PATH + 1] = {0};
+
+ int len = (int)GetModuleFileNameW(NULL, w_exedir, MAX_PATH);
+ int imax = 0;
+ for (int i = 0; i < len; i++) {
+ if (w_exedir[i] == '\\') {
+ w_exedir[i] = '/';
+ imax = i;
+ }
+ }
+
+ w_exedir[imax] = '\0';
+
+ return mp_to_utf8(talloc_ctx, w_exedir);
+}
+
+static char *mp_get_win_exe_subdir(void *ta_ctx, const char *name)
+{
+ return talloc_asprintf(ta_ctx, "%s/%s", mp_get_win_exe_dir(ta_ctx), name);
+}
+
+static char *mp_get_win_shell_dir(void *talloc_ctx, REFKNOWNFOLDERID folder)
+{
+ wchar_t *w_appdir = NULL;
+
+ if (FAILED(SHGetKnownFolderPath(folder, KF_FLAG_CREATE, NULL, &w_appdir)))
+ return NULL;
+
+ char *appdir = mp_to_utf8(talloc_ctx, w_appdir);
+ CoTaskMemFree(w_appdir);
+ return appdir;
+}
+
+static char *mp_get_win_app_dir(void *talloc_ctx)
+{
+ char *path = mp_get_win_shell_dir(talloc_ctx, &FOLDERID_RoamingAppData);
+ return path ? mp_path_join(talloc_ctx, path, "mpv") : NULL;
+}
+
+static char *mp_get_win_local_app_dir(void *talloc_ctx)
+{
+ char *path = mp_get_win_shell_dir(talloc_ctx, &FOLDERID_LocalAppData);
+ return path ? mp_path_join(talloc_ctx, path, "mpv") : NULL;
+}
+
+static void path_init(void)
+{
+ void *tmp = talloc_new(NULL);
+ char *path = mp_get_win_exe_subdir(tmp, "portable_config");
+ if (path && mp_path_exists(path))
+ portable_path = talloc_strdup(NULL, path);
+ talloc_free(tmp);
+}
+
+const char *mp_get_platform_path_win(void *talloc_ctx, const char *type)
+{
+ mp_exec_once(&path_init_once, path_init);
+ if (portable_path) {
+ if (strcmp(type, "home") == 0)
+ return portable_path;
+ if (strcmp(type, "cache") == 0)
+ return mp_path_join(talloc_ctx, portable_path, "cache");
+ } else {
+ if (strcmp(type, "home") == 0)
+ return mp_get_win_app_dir(talloc_ctx);
+ if (strcmp(type, "cache") == 0)
+ return mp_path_join(talloc_ctx, mp_get_win_local_app_dir(talloc_ctx), "cache");
+ if (strcmp(type, "state") == 0)
+ return mp_get_win_local_app_dir(talloc_ctx);
+ if (strcmp(type, "exe_dir") == 0)
+ return mp_get_win_exe_dir(talloc_ctx);
+ // Not really true, but serves as a way to return a lowest-priority dir.
+ if (strcmp(type, "global") == 0)
+ return mp_get_win_exe_subdir(talloc_ctx, "mpv");
+ }
+ if (strcmp(type, "desktop") == 0)
+ return mp_get_win_shell_dir(talloc_ctx, &FOLDERID_Desktop);
+ return NULL;
+}
diff --git a/osdep/path.h b/osdep/path.h
new file mode 100644
index 0000000..2c00ea5
--- /dev/null
+++ b/osdep/path.h
@@ -0,0 +1,32 @@
+#ifndef OSDEP_PATH_H
+#define OSDEP_PATH_H
+
+// Return a platform-specific path, identified by the type parameter. If the
+// return value is allocated, talloc_ctx is used as talloc parent context.
+//
+// The following type values are defined:
+// "home" the native mpv-specific user config dir
+// "old_home" same as "home", but lesser priority (compatibility)
+// "osxbundle" OSX bundle resource path
+// "global" the least priority, global config file location
+// "desktop" path to desktop contents
+//
+// These additional types are also defined. However, they are not necessarily
+// implemented on every platform. Unlike some other type values that are
+// platform specific (like "osxbundle"), the value of "home" is returned
+// instead if these types are not explicitly defined.
+// "cache" the native mpv-specific user cache dir
+// "state" the native mpv-specific user state dir
+//
+// It is allowed to return a static string, so the caller must set talloc_ctx
+// to something other than NULL to avoid memory leaks.
+typedef const char *(*mp_get_platform_path_cb)(void *talloc_ctx, const char *type);
+
+// Conforming to mp_get_platform_path_cb.
+const char *mp_get_platform_path_darwin(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_uwp(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_win(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_osx(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_unix(void *talloc_ctx, const char *type);
+
+#endif
diff --git a/osdep/poll_wrapper.c b/osdep/poll_wrapper.c
new file mode 100644
index 0000000..3fe039b
--- /dev/null
+++ b/osdep/poll_wrapper.c
@@ -0,0 +1,89 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <poll.h>
+#include <sys/select.h>
+#include <stdio.h>
+
+#include "common/common.h"
+#include "config.h"
+#include "poll_wrapper.h"
+#include "timer.h"
+
+
+int mp_poll(struct pollfd *fds, int nfds, int64_t timeout_ns)
+{
+#if HAVE_PPOLL
+ struct timespec ts;
+ ts.tv_sec = timeout_ns / MP_TIME_S_TO_NS(1);
+ ts.tv_nsec = timeout_ns % MP_TIME_S_TO_NS(1);
+ struct timespec *tsp = timeout_ns >= 0 ? &ts : NULL;
+ return ppoll(fds, nfds, tsp, NULL);
+#endif
+ // Round-up to 1ms for short timeouts (100us, 1000us]
+ if (timeout_ns > MP_TIME_US_TO_NS(100))
+ timeout_ns = MPMAX(timeout_ns, MP_TIME_MS_TO_NS(1));
+ if (timeout_ns > 0)
+ timeout_ns /= MP_TIME_MS_TO_NS(1);
+ return poll(fds, nfds, timeout_ns);
+}
+
+// poll shim that supports device files on macOS.
+int polldev(struct pollfd fds[], nfds_t nfds, int timeout)
+{
+#ifdef __APPLE__
+ int maxfd = 0;
+ fd_set readfds, writefds;
+ FD_ZERO(&readfds);
+ FD_ZERO(&writefds);
+ for (size_t i = 0; i < nfds; ++i) {
+ struct pollfd *fd = &fds[i];
+ if (fd->fd > maxfd) {
+ maxfd = fd->fd;
+ }
+ if ((fd->events & POLLIN)) {
+ FD_SET(fd->fd, &readfds);
+ }
+ if ((fd->events & POLLOUT)) {
+ FD_SET(fd->fd, &writefds);
+ }
+ }
+ struct timeval _timeout = {
+ .tv_sec = timeout / 1000,
+ .tv_usec = (timeout % 1000) * 1000
+ };
+ int n = select(maxfd + 1, &readfds, &writefds, NULL,
+ timeout != -1 ? &_timeout : NULL);
+ if (n < 0) {
+ return n;
+ }
+ for (size_t i = 0; i < nfds; ++i) {
+ struct pollfd *fd = &fds[i];
+ fd->revents = 0;
+ if (FD_ISSET(fd->fd, &readfds)) {
+ fd->revents |= POLLIN;
+ }
+ if (FD_ISSET(fd->fd, &writefds)) {
+ fd->revents |= POLLOUT;
+ }
+ }
+ return n;
+#else
+ return poll(fds, nfds, timeout);
+#endif
+}
diff --git a/osdep/poll_wrapper.h b/osdep/poll_wrapper.h
new file mode 100644
index 0000000..b359ed3
--- /dev/null
+++ b/osdep/poll_wrapper.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <poll.h>
+#include <stdint.h>
+
+// Behaves like poll(3) but works for device files on macOS.
+// Only supports POLLIN and POLLOUT.
+int polldev(struct pollfd fds[], nfds_t nfds, int timeout);
+
+// Generic polling wrapper. It will try and use higher resolution
+// polling (ppoll) if available.
+int mp_poll(struct pollfd *fds, int nfds, int64_t timeout_ns);
diff --git a/osdep/semaphore.h b/osdep/semaphore.h
new file mode 100644
index 0000000..40cf383
--- /dev/null
+++ b/osdep/semaphore.h
@@ -0,0 +1,37 @@
+#ifndef MP_SEMAPHORE_H_
+#define MP_SEMAPHORE_H_
+
+#include <sys/types.h>
+#include <semaphore.h>
+
+// OSX provides non-working empty stubs, so we emulate them.
+// This should be AS-safe, but cancellation issues were ignored.
+// sem_getvalue() is not provided.
+// sem_post() won't always correctly return an error on overflow.
+// Process-shared semantics are not provided.
+
+#ifdef __APPLE__
+
+#define MP_SEMAPHORE_EMULATION
+
+#include "osdep/threads.h"
+
+#define MP_SEM_VALUE_MAX 4096
+
+typedef struct {
+ int wakeup_pipe[2];
+ mp_mutex lock;
+ // protected by lock
+ unsigned int count;
+} mp_sem_t;
+
+int mp_sem_init(mp_sem_t *sem, int pshared, unsigned int value);
+int mp_sem_wait(mp_sem_t *sem);
+int mp_sem_trywait(mp_sem_t *sem);
+int mp_sem_timedwait(mp_sem_t *sem, int64_t until);
+int mp_sem_post(mp_sem_t *sem);
+int mp_sem_destroy(mp_sem_t *sem);
+
+#endif
+
+#endif
diff --git a/osdep/semaphore_osx.c b/osdep/semaphore_osx.c
new file mode 100644
index 0000000..bfb4d57
--- /dev/null
+++ b/osdep/semaphore_osx.c
@@ -0,0 +1,117 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "osdep/semaphore.h"
+
+#ifdef MP_SEMAPHORE_EMULATION
+
+#include <unistd.h>
+#include <poll.h>
+#include <limits.h>
+#include <sys/time.h>
+#include <errno.h>
+
+#include <common/common.h>
+#include "io.h"
+#include "timer.h"
+
+int mp_sem_init(mp_sem_t *sem, int pshared, unsigned int value)
+{
+ if (pshared) {
+ errno = ENOSYS;
+ return -1;
+ }
+ if (value > INT_MAX) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (mp_make_wakeup_pipe(sem->wakeup_pipe) < 0)
+ return -1;
+ sem->count = 0;
+ mp_mutex_init(&sem->lock);
+ return 0;
+}
+
+int mp_sem_wait(mp_sem_t *sem)
+{
+ return mp_sem_timedwait(sem, -1);
+}
+
+int mp_sem_trywait(mp_sem_t *sem)
+{
+ int r = -1;
+ mp_mutex_lock(&sem->lock);
+ if (sem->count == 0) {
+ char buf[1024];
+ ssize_t s = read(sem->wakeup_pipe[0], buf, sizeof(buf));
+ if (s > 0 && s <= INT_MAX - sem->count) // can't handle overflows correctly
+ sem->count += s;
+ }
+ if (sem->count > 0) {
+ sem->count -= 1;
+ r = 0;
+ }
+ mp_mutex_unlock(&sem->lock);
+ if (r < 0)
+ errno = EAGAIN;
+ return r;
+}
+
+int mp_sem_timedwait(mp_sem_t *sem, int64_t until)
+{
+ while (1) {
+ if (!mp_sem_trywait(sem))
+ return 0;
+
+ int timeout = 0;
+ if (until == -1) {
+ timeout = -1;
+ } else if (until >= 0) {
+ timeout = (until - mp_time_ns()) / MP_TIME_MS_TO_NS(1);
+ timeout = MPCLAMP(timeout, 0, INT_MAX);
+ } else {
+ assert(false && "Invalid mp_time value!");
+ }
+
+ struct pollfd fd = {.fd = sem->wakeup_pipe[0], .events = POLLIN};
+ int r = poll(&fd, 1, timeout);
+ if (r < 0)
+ return -1;
+ if (r == 0) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ }
+}
+
+int mp_sem_post(mp_sem_t *sem)
+{
+ if (write(sem->wakeup_pipe[1], &(char){0}, 1) == 1)
+ return 0;
+ // Actually we can't handle overflow fully correctly, because we can't
+ // check sem->count atomically, while still being AS-safe.
+ errno = EOVERFLOW;
+ return -1;
+}
+
+int mp_sem_destroy(mp_sem_t *sem)
+{
+ close(sem->wakeup_pipe[0]);
+ close(sem->wakeup_pipe[1]);
+ mp_mutex_destroy(&sem->lock);
+ return 0;
+}
+
+#endif
diff --git a/osdep/strnlen.h b/osdep/strnlen.h
new file mode 100644
index 0000000..e66932a
--- /dev/null
+++ b/osdep/strnlen.h
@@ -0,0 +1,31 @@
+/*
+ * strnlen wrapper
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_OSDEP_STRNLEN
+#define MP_OSDEP_STRNLEN
+
+#include "config.h"
+
+#if HAVE_ANDROID
+// strnlen is broken on current android ndk, see https://code.google.com/p/android/issues/detail?id=74741
+#include "osdep/android/strnlen.h"
+#define strnlen freebsd_strnlen
+#endif
+
+#endif
diff --git a/osdep/subprocess-dummy.c b/osdep/subprocess-dummy.c
new file mode 100644
index 0000000..df74538
--- /dev/null
+++ b/osdep/subprocess-dummy.c
@@ -0,0 +1,7 @@
+#include "subprocess.h"
+
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res)
+{
+ *res = (struct mp_subprocess_result){.error = MP_SUBPROCESS_EUNSUPPORTED};
+}
diff --git a/osdep/subprocess-posix.c b/osdep/subprocess-posix.c
new file mode 100644
index 0000000..0656ec5
--- /dev/null
+++ b/osdep/subprocess-posix.c
@@ -0,0 +1,345 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <poll.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+
+#include "osdep/subprocess.h"
+
+#include "common/common.h"
+#include "misc/thread_tools.h"
+#include "osdep/io.h"
+#include "stream/stream.h"
+
+extern char **environ;
+
+#ifdef SIGRTMAX
+#define SIGNAL_MAX SIGRTMAX
+#else
+#define SIGNAL_MAX 32
+#endif
+
+#define SAFE_CLOSE(fd) do { if ((fd) >= 0) close((fd)); (fd) = -1; } while (0)
+
+// Async-signal-safe execvpe(). POSIX does not list it as async-signal-safe
+// (POSIX is such a joke), so do it manually. While in theory the searching is
+// apparently implementation dependent and not exposed (because POSIX is a
+// joke?), the expected rules are still relatively simple.
+// Doesn't set errno correctly.
+// Somewhat inspired by musl's src/process/execvp.c.
+static int as_execvpe(const char *path, const char *file, char *const argv[],
+ char *const envp[])
+{
+ if (strchr(file, '/') || !file[0])
+ return execve(file, argv, envp);
+
+ size_t flen = strlen(file);
+ while (path && path[0]) {
+ size_t plen = strcspn(path, ":");
+ // Ignore paths that are too long.
+ char fn[PATH_MAX];
+ if (plen + 1 + flen + 1 < sizeof(fn)) {
+ memcpy(fn, path, plen);
+ fn[plen] = '/';
+ memcpy(fn + plen + 1, file, flen + 1);
+ execve(fn, argv, envp);
+ if (errno != EACCES && errno != ENOENT && errno != ENOTDIR)
+ break;
+ }
+ path += plen + (path[plen] == ':' ? 1 : 0);
+ }
+ return -1;
+}
+
+// In the child process, resets the signal mask to defaults. Also clears any
+// signal handlers first so nothing funny happens.
+static void reset_signals_child(void)
+{
+ struct sigaction sa = { 0 };
+ sigset_t sigmask;
+ sa.sa_handler = SIG_DFL;
+ sigemptyset(&sigmask);
+
+ for (int nr = 1; nr < SIGNAL_MAX; nr++)
+ sigaction(nr, &sa, NULL);
+ sigprocmask(SIG_SETMASK, &sigmask, NULL);
+}
+
+// Returns 0 on any error, valid PID on success.
+// This function must be async-signal-safe, as it may be called from a fork().
+static pid_t spawn_process(const char *path, struct mp_subprocess_opts *opts,
+ int src_fds[])
+{
+ int p[2] = {-1, -1};
+ pid_t fres = 0;
+ sigset_t sigmask, oldmask;
+ sigfillset(&sigmask);
+ pthread_sigmask(SIG_BLOCK, &sigmask, &oldmask);
+
+ // We setup a communication pipe to signal failure. Since the child calls
+ // exec() and becomes the calling process, we don't know if or when the
+ // child process successfully ran exec() just from the PID.
+ // Use a CLOEXEC pipe to detect whether exec() was used. Obviously it will
+ // be closed if exec() succeeds, and an error is written if not.
+ // There are also some things further below in the code that need CLOEXEC.
+ if (mp_make_cloexec_pipe(p) < 0)
+ goto done;
+ // Check whether CLOEXEC is really set. Important for correct operation.
+ int p_flags = fcntl(p[0], F_GETFD);
+ if (p_flags == -1 || !FD_CLOEXEC || !(p_flags & FD_CLOEXEC))
+ goto done; // require CLOEXEC; unknown if fallback would be worth it
+
+ fres = fork();
+ if (fres < 0) {
+ fres = 0;
+ goto done;
+ }
+ if (fres == 0) {
+ // child
+ reset_signals_child();
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (src_fds[n] == opts->fds[n].fd) {
+ int flags = fcntl(opts->fds[n].fd, F_GETFD);
+ if (flags == -1)
+ goto child_failed;
+ flags &= ~(unsigned)FD_CLOEXEC;
+ if (fcntl(opts->fds[n].fd, F_SETFD, flags) == -1)
+ goto child_failed;
+ } else if (dup2(src_fds[n], opts->fds[n].fd) < 0) {
+ goto child_failed;
+ }
+ }
+
+ as_execvpe(path, opts->exe, opts->args, opts->env ? opts->env : environ);
+
+ child_failed:
+ (void)write(p[1], &(char){1}, 1); // shouldn't be able to fail
+ _exit(1);
+ }
+
+ SAFE_CLOSE(p[1]);
+
+ int r;
+ do {
+ r = read(p[0], &(char){0}, 1);
+ } while (r < 0 && errno == EINTR);
+
+ // If exec()ing child failed, collect it immediately.
+ if (r != 0) {
+ while (waitpid(fres, &(int){0}, 0) < 0 && errno == EINTR) {}
+ fres = 0;
+ }
+
+done:
+ pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
+ SAFE_CLOSE(p[0]);
+ SAFE_CLOSE(p[1]);
+
+ return fres;
+}
+
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res)
+{
+ int status = -1;
+ int comm_pipe[MP_SUBPROCESS_MAX_FDS][2];
+ int src_fds[MP_SUBPROCESS_MAX_FDS];
+ int devnull = -1;
+ pid_t pid = 0;
+ bool spawned = false;
+ bool killed_by_us = false;
+ int cancel_fd = -1;
+ char *path = getenv("PATH");
+ if (!path)
+ path = ""; // failure, who cares
+
+ *res = (struct mp_subprocess_result){0};
+
+ for (int n = 0; n < opts->num_fds; n++)
+ comm_pipe[n][0] = comm_pipe[n][1] = -1;
+
+ if (opts->cancel) {
+ cancel_fd = mp_cancel_get_fd(opts->cancel);
+ if (cancel_fd < 0)
+ goto done;
+ }
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ assert(!(opts->fds[n].on_read && opts->fds[n].on_write));
+
+ if (opts->fds[n].on_read && mp_make_cloexec_pipe(comm_pipe[n]) < 0)
+ goto done;
+
+ if (opts->fds[n].on_write || opts->fds[n].write_buf) {
+ assert(opts->fds[n].on_write && opts->fds[n].write_buf);
+ if (mp_make_cloexec_pipe(comm_pipe[n]) < 0)
+ goto done;
+ MPSWAP(int, comm_pipe[n][0], comm_pipe[n][1]);
+
+ struct sigaction sa = {.sa_handler = SIG_IGN, .sa_flags = SA_RESTART};
+ sigfillset(&sa.sa_mask);
+ sigaction(SIGPIPE, &sa, NULL);
+ }
+ }
+
+ devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ if (devnull < 0)
+ goto done;
+
+ // redirect FDs
+ for (int n = 0; n < opts->num_fds; n++) {
+ int src_fd = devnull;
+ if (comm_pipe[n][1] >= 0)
+ src_fd = comm_pipe[n][1];
+ if (opts->fds[n].src_fd >= 0)
+ src_fd = opts->fds[n].src_fd;
+ src_fds[n] = src_fd;
+ }
+
+ if (opts->detach) {
+ // If we run it detached, we fork a child to start the process; then
+ // it exits immediately, letting PID 1 inherit it. So we don't need
+ // anything else to collect these child PIDs.
+ sigset_t sigmask, oldmask;
+ sigfillset(&sigmask);
+ pthread_sigmask(SIG_BLOCK, &sigmask, &oldmask);
+ pid_t fres = fork();
+ if (fres < 0)
+ goto done;
+ if (fres == 0) {
+ // child
+ setsid();
+ if (!spawn_process(path, opts, src_fds))
+ _exit(1);
+ _exit(0);
+ }
+ pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
+ int child_status = 0;
+ while (waitpid(fres, &child_status, 0) < 0 && errno == EINTR) {}
+ if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0)
+ goto done;
+ } else {
+ pid = spawn_process(path, opts, src_fds);
+ if (!pid)
+ goto done;
+ }
+
+ spawned = true;
+
+ for (int n = 0; n < opts->num_fds; n++)
+ SAFE_CLOSE(comm_pipe[n][1]);
+ SAFE_CLOSE(devnull);
+
+ while (1) {
+ struct pollfd fds[MP_SUBPROCESS_MAX_FDS + 1];
+ int map_fds[MP_SUBPROCESS_MAX_FDS + 1];
+ int num_fds = 0;
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (comm_pipe[n][0] >= 0) {
+ map_fds[num_fds] = n;
+ fds[num_fds++] = (struct pollfd){
+ .events = opts->fds[n].on_read ? POLLIN : POLLOUT,
+ .fd = comm_pipe[n][0],
+ };
+ }
+ }
+ if (!num_fds)
+ break;
+ if (cancel_fd >= 0) {
+ map_fds[num_fds] = -1;
+ fds[num_fds++] = (struct pollfd){.events = POLLIN, .fd = cancel_fd};
+ }
+
+ if (poll(fds, num_fds, -1) < 0 && errno != EINTR)
+ break;
+
+ for (int idx = 0; idx < num_fds; idx++) {
+ if (fds[idx].revents) {
+ int n = map_fds[idx];
+ if (n < 0) {
+ // cancel_fd
+ if (pid)
+ kill(pid, SIGKILL);
+ killed_by_us = true;
+ break;
+ }
+ struct mp_subprocess_fd *fd = &opts->fds[n];
+ if (fd->on_read) {
+ char buf[4096];
+ ssize_t r = read(comm_pipe[n][0], buf, sizeof(buf));
+ if (r < 0 && errno == EINTR)
+ continue;
+ fd->on_read(fd->on_read_ctx, buf, MPMAX(r, 0));
+ if (r <= 0)
+ SAFE_CLOSE(comm_pipe[n][0]);
+ } else if (fd->on_write) {
+ if (!fd->write_buf->len) {
+ fd->on_write(fd->on_write_ctx);
+ if (!fd->write_buf->len) {
+ SAFE_CLOSE(comm_pipe[n][0]);
+ continue;
+ }
+ }
+ ssize_t r = write(comm_pipe[n][0], fd->write_buf->start,
+ fd->write_buf->len);
+ if (r < 0 && errno == EINTR)
+ continue;
+ if (r < 0) {
+ // Let's not signal an error for now - caller can check
+ // whether all buffer was written.
+ SAFE_CLOSE(comm_pipe[n][0]);
+ continue;
+ }
+ *fd->write_buf = bstr_cut(*fd->write_buf, r);
+ }
+ }
+ }
+ }
+
+ // Note: it can happen that a child process closes the pipe, but does not
+ // terminate yet. In this case, we would have to run waitpid() in
+ // a separate thread and use pthread_cancel(), or use other weird
+ // and laborious tricks in order to react to mp_cancel.
+ // So this isn't handled yet.
+ if (pid)
+ while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
+
+done:
+ for (int n = 0; n < opts->num_fds; n++) {
+ SAFE_CLOSE(comm_pipe[n][0]);
+ SAFE_CLOSE(comm_pipe[n][1]);
+ }
+ SAFE_CLOSE(devnull);
+
+ if (!spawned || (pid && WIFEXITED(status) && WEXITSTATUS(status) == 127)) {
+ res->error = MP_SUBPROCESS_EINIT;
+ } else if (pid && WIFEXITED(status)) {
+ res->exit_status = WEXITSTATUS(status);
+ } else if (spawned && opts->detach) {
+ // ok
+ } else if (killed_by_us) {
+ res->error = MP_SUBPROCESS_EKILLED_BY_US;
+ } else {
+ res->error = MP_SUBPROCESS_EGENERIC;
+ }
+}
diff --git a/osdep/subprocess-win.c b/osdep/subprocess-win.c
new file mode 100644
index 0000000..5413b24
--- /dev/null
+++ b/osdep/subprocess-win.c
@@ -0,0 +1,516 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <string.h>
+
+#include "osdep/subprocess.h"
+
+#include "osdep/io.h"
+#include "osdep/windows_utils.h"
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "stream/stream.h"
+#include "misc/bstr.h"
+#include "misc/thread_tools.h"
+
+// Internal CRT FD flags
+#define FOPEN (0x01)
+#define FPIPE (0x08)
+#define FDEV (0x40)
+
+static void write_arg(bstr *cmdline, char *arg)
+{
+ // Empty args must be represented as an empty quoted string
+ if (!arg[0]) {
+ bstr_xappend(NULL, cmdline, bstr0("\"\""));
+ return;
+ }
+
+ // If the string doesn't have characters that need to be escaped, it's best
+ // to leave it alone for the sake of Windows programs that don't process
+ // quoted args correctly.
+ if (!strpbrk(arg, " \t\"")) {
+ bstr_xappend(NULL, cmdline, bstr0(arg));
+ return;
+ }
+
+ // If there are characters that need to be escaped, write a quoted string
+ bstr_xappend(NULL, cmdline, bstr0("\""));
+
+ // Escape the argument. To match the behavior of CommandLineToArgvW,
+ // backslashes are only escaped if they appear before a quote or the end of
+ // the string.
+ int num_slashes = 0;
+ for (int pos = 0; arg[pos]; pos++) {
+ switch (arg[pos]) {
+ case '\\':
+ // Count consecutive backslashes
+ num_slashes++;
+ break;
+ case '"':
+ // Write the argument up to the point before the quote
+ bstr_xappend(NULL, cmdline, (struct bstr){arg, pos});
+ arg += pos;
+ pos = 0;
+
+ // Double backslashes preceding the quote
+ for (int i = 0; i < num_slashes; i++)
+ bstr_xappend(NULL, cmdline, bstr0("\\"));
+ num_slashes = 0;
+
+ // Escape the quote itself
+ bstr_xappend(NULL, cmdline, bstr0("\\"));
+ break;
+ default:
+ num_slashes = 0;
+ }
+ }
+
+ // Write the rest of the argument
+ bstr_xappend(NULL, cmdline, bstr0(arg));
+
+ // Double backslashes at the end of the argument
+ for (int i = 0; i < num_slashes; i++)
+ bstr_xappend(NULL, cmdline, bstr0("\\"));
+
+ bstr_xappend(NULL, cmdline, bstr0("\""));
+}
+
+// Convert an array of arguments to a properly escaped command-line string
+static wchar_t *write_cmdline(void *ctx, char *argv0, char **args)
+{
+ // argv0 should always be quoted. Otherwise, arguments may be interpreted as
+ // part of the program name. Also, it can't contain escape sequences.
+ bstr cmdline = {0};
+ bstr_xappend_asprintf(NULL, &cmdline, "\"%s\"", argv0);
+
+ if (args) {
+ for (int i = 0; args[i]; i++) {
+ bstr_xappend(NULL, &cmdline, bstr0(" "));
+ write_arg(&cmdline, args[i]);
+ }
+ }
+
+ wchar_t *wcmdline = mp_from_utf8(ctx, cmdline.start);
+ talloc_free(cmdline.start);
+ return wcmdline;
+}
+
+static void delete_handle_list(void *p)
+{
+ LPPROC_THREAD_ATTRIBUTE_LIST list = p;
+ DeleteProcThreadAttributeList(list);
+}
+
+// Create a PROC_THREAD_ATTRIBUTE_LIST that specifies exactly which handles are
+// inherited by the subprocess
+static LPPROC_THREAD_ATTRIBUTE_LIST create_handle_list(void *ctx,
+ HANDLE *handles, int num)
+{
+ // Get required attribute list size
+ SIZE_T size = 0;
+ if (!InitializeProcThreadAttributeList(NULL, 1, 0, &size)) {
+ if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ return NULL;
+ }
+
+ // Allocate attribute list
+ LPPROC_THREAD_ATTRIBUTE_LIST list = talloc_size(ctx, size);
+ if (!InitializeProcThreadAttributeList(list, 1, 0, &size))
+ goto error;
+ talloc_set_destructor(list, delete_handle_list);
+
+ if (!UpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ handles, num * sizeof(HANDLE), NULL, NULL))
+ goto error;
+
+ return list;
+error:
+ talloc_free(list);
+ return NULL;
+}
+
+// Helper method similar to sparse_poll, skips NULL handles
+static int sparse_wait(HANDLE *handles, unsigned num_handles)
+{
+ unsigned w_num_handles = 0;
+ HANDLE w_handles[MP_SUBPROCESS_MAX_FDS + 2];
+ int map[MP_SUBPROCESS_MAX_FDS + 2];
+ if (num_handles > MP_ARRAY_SIZE(w_handles))
+ return -1;
+
+ for (unsigned i = 0; i < num_handles; i++) {
+ if (!handles[i])
+ continue;
+
+ w_handles[w_num_handles] = handles[i];
+ map[w_num_handles] = i;
+ w_num_handles++;
+ }
+
+ if (w_num_handles == 0)
+ return -1;
+ DWORD i = WaitForMultipleObjects(w_num_handles, w_handles, FALSE, INFINITE);
+ i -= WAIT_OBJECT_0;
+
+ if (i >= w_num_handles)
+ return -1;
+ return map[i];
+}
+
+// Wrapper for ReadFile that treats ERROR_IO_PENDING as success
+static int async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol)
+{
+ if (!ReadFile(file, buf, size, NULL, ol))
+ return (GetLastError() == ERROR_IO_PENDING) ? 0 : -1;
+ return 0;
+}
+
+static bool is_valid_handle(HANDLE h)
+{
+ // _get_osfhandle can return -2 "when the file descriptor is not associated
+ // with a stream"
+ return h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2;
+}
+
+static wchar_t *convert_environ(void *ctx, char **env)
+{
+ // Environment size in wchar_ts, including the trailing NUL
+ size_t env_size = 1;
+
+ for (int i = 0; env[i]; i++) {
+ int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1, NULL, 0);
+ if (count <= 0)
+ abort();
+ env_size += count;
+ }
+
+ wchar_t *ret = talloc_array(ctx, wchar_t, env_size);
+ size_t pos = 0;
+
+ for (int i = 0; env[i]; i++) {
+ int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1,
+ ret + pos, env_size - pos);
+ if (count <= 0)
+ abort();
+ pos += count;
+ }
+
+ return ret;
+}
+
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res)
+{
+ wchar_t *tmp = talloc_new(NULL);
+ DWORD r;
+
+ HANDLE share_hndls[MP_SUBPROCESS_MAX_FDS] = {0};
+ int share_hndl_count = 0;
+ HANDLE wait_hndls[MP_SUBPROCESS_MAX_FDS + 2] = {0};
+ int wait_hndl_count = 0;
+
+ struct {
+ HANDLE handle;
+ bool handle_close;
+ char crt_flags;
+
+ HANDLE read;
+ OVERLAPPED read_ol;
+ char *read_buf;
+ } fd_data[MP_SUBPROCESS_MAX_FDS] = {0};
+
+ // The maximum target FD is limited because FDs have to fit in two sparse
+ // arrays in STARTUPINFO.lpReserved2, which has a maximum size of 65535
+ // bytes. The first four bytes are the handle count, followed by one byte
+ // per handle for flags, and an intptr_t per handle for the HANDLE itself.
+ static const int crt_fd_max = (65535 - sizeof(int)) / (1 + sizeof(intptr_t));
+ int crt_fd_count = 0;
+
+ // If the function exits before CreateProcess, there was an init error
+ *res = (struct mp_subprocess_result){ .error = MP_SUBPROCESS_EINIT };
+
+ STARTUPINFOEXW si = {
+ .StartupInfo = {
+ .cb = sizeof si,
+ .dwFlags = STARTF_USESTDHANDLES | STARTF_FORCEOFFFEEDBACK,
+ },
+ };
+
+ PROCESS_INFORMATION pi = {0};
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (opts->fds[n].fd >= crt_fd_max) {
+ // Target FD is too big to fit in the CRT FD array
+ res->error = MP_SUBPROCESS_EUNSUPPORTED;
+ goto done;
+ }
+
+ if (opts->fds[n].fd >= crt_fd_count)
+ crt_fd_count = opts->fds[n].fd + 1;
+
+ if (opts->fds[n].src_fd >= 0) {
+ HANDLE src_handle = (HANDLE)_get_osfhandle(opts->fds[n].src_fd);
+
+ // Invalid handles are just ignored. This is because sometimes the
+ // standard handles are invalid in Windows, like in GUI processes.
+ // In this case mp_subprocess2 callers should still be able to
+ // blindly forward the standard FDs.
+ if (!is_valid_handle(src_handle))
+ continue;
+
+ DWORD type = GetFileType(src_handle);
+ bool is_console_handle = false;
+ switch (type & 0xff) {
+ case FILE_TYPE_DISK:
+ fd_data[n].crt_flags = FOPEN;
+ break;
+ case FILE_TYPE_CHAR:
+ fd_data[n].crt_flags = FOPEN | FDEV;
+ is_console_handle = GetConsoleMode(src_handle, &(DWORD){0});
+ break;
+ case FILE_TYPE_PIPE:
+ fd_data[n].crt_flags = FOPEN | FPIPE;
+ break;
+ case FILE_TYPE_UNKNOWN:
+ continue;
+ }
+
+ if (is_console_handle) {
+ // Some Windows versions have bugs when duplicating console
+ // handles, or when adding console handles to the CreateProcess
+ // handle list, so just use the handle directly for now. Console
+ // handles treat inheritance weirdly, so this should still work.
+ fd_data[n].handle = src_handle;
+ } else {
+ // Instead of making the source handle inheritable, just
+ // duplicate it to an inheritable handle
+ if (!DuplicateHandle(GetCurrentProcess(), src_handle,
+ GetCurrentProcess(), &fd_data[n].handle, 0,
+ TRUE, DUPLICATE_SAME_ACCESS))
+ goto done;
+ fd_data[n].handle_close = true;
+
+ share_hndls[share_hndl_count++] = fd_data[n].handle;
+ }
+
+ } else if (opts->fds[n].on_read && !opts->detach) {
+ fd_data[n].read_ol.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
+ if (!fd_data[n].read_ol.hEvent)
+ goto done;
+
+ struct w32_create_anon_pipe_opts o = {
+ .server_flags = PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
+ .client_inheritable = true,
+ };
+ if (!mp_w32_create_anon_pipe(&fd_data[n].read, &fd_data[n].handle, &o))
+ goto done;
+ fd_data[n].handle_close = true;
+
+ wait_hndls[n] = fd_data[n].read_ol.hEvent;
+ wait_hndl_count++;
+
+ fd_data[n].crt_flags = FOPEN | FPIPE;
+ fd_data[n].read_buf = talloc_size(tmp, 4096);
+
+ share_hndls[share_hndl_count++] = fd_data[n].handle;
+
+ } else {
+ DWORD access;
+ if (opts->fds[n].fd == 0) {
+ access = FILE_GENERIC_READ;
+ } else if (opts->fds[n].fd <= 2) {
+ access = FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES;
+ } else {
+ access = FILE_GENERIC_READ | FILE_GENERIC_WRITE;
+ }
+
+ SECURITY_ATTRIBUTES sa = {
+ .nLength = sizeof sa,
+ .bInheritHandle = TRUE,
+ };
+ fd_data[n].crt_flags = FOPEN | FDEV;
+ fd_data[n].handle = CreateFileW(L"NUL", access,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ &sa, OPEN_EXISTING, 0, NULL);
+ fd_data[n].handle_close = true;
+ }
+
+ switch (opts->fds[n].fd) {
+ case 0:
+ si.StartupInfo.hStdInput = fd_data[n].handle;
+ break;
+ case 1:
+ si.StartupInfo.hStdOutput = fd_data[n].handle;
+ break;
+ case 2:
+ si.StartupInfo.hStdError = fd_data[n].handle;
+ break;
+ }
+ }
+
+ // Convert the UTF-8 environment into a UTF-16 Windows environment block
+ wchar_t *env = NULL;
+ if (opts->env)
+ env = convert_environ(tmp, opts->env);
+
+ // Convert the args array to a UTF-16 Windows command-line string
+ char **args = opts->args && opts->args[0] ? &opts->args[1] : 0;
+ wchar_t *cmdline = write_cmdline(tmp, opts->exe, args);
+
+ // Get pointers to the arrays in lpReserved2. This is an undocumented data
+ // structure used by MSVCRT (and other frameworks and runtimes) to emulate
+ // FD inheritance. The format is unofficially documented here:
+ // https://www.catch22.net/tuts/undocumented-createprocess
+ si.StartupInfo.cbReserved2 = sizeof(int) + crt_fd_count * (1 + sizeof(intptr_t));
+ si.StartupInfo.lpReserved2 = talloc_size(tmp, si.StartupInfo.cbReserved2);
+ char *crt_buf_flags = si.StartupInfo.lpReserved2 + sizeof(int);
+ char *crt_buf_hndls = crt_buf_flags + crt_fd_count;
+
+ memcpy(si.StartupInfo.lpReserved2, &crt_fd_count, sizeof(int));
+
+ // Fill the handle array with INVALID_HANDLE_VALUE, for unassigned handles
+ for (int n = 0; n < crt_fd_count; n++) {
+ HANDLE h = INVALID_HANDLE_VALUE;
+ memcpy(crt_buf_hndls + n * sizeof(intptr_t), &h, sizeof(intptr_t));
+ }
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ crt_buf_flags[opts->fds[n].fd] = fd_data[n].crt_flags;
+ memcpy(crt_buf_hndls + opts->fds[n].fd * sizeof(intptr_t),
+ &fd_data[n].handle, sizeof(intptr_t));
+ }
+
+ DWORD flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
+
+ // Specify which handles are inherited by the subprocess. If this isn't
+ // specified, the subprocess inherits all inheritable handles, which could
+ // include handles created by other threads. See:
+ // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/16/10248328.aspx
+ si.lpAttributeList = create_handle_list(tmp, share_hndls, share_hndl_count);
+
+ // If we have a console, the subprocess will automatically attach to it so
+ // it can receive Ctrl+C events. If we don't have a console, prevent the
+ // subprocess from creating its own console window by specifying
+ // CREATE_NO_WINDOW. GetConsoleCP() can be used to reliably determine if we
+ // have a console or not (Cygwin uses it too.)
+ if (!GetConsoleCP())
+ flags |= CREATE_NO_WINDOW;
+
+ if (!CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, flags, env, NULL,
+ &si.StartupInfo, &pi))
+ goto done;
+ talloc_free(cmdline);
+ talloc_free(env);
+ talloc_free(si.StartupInfo.lpReserved2);
+ talloc_free(si.lpAttributeList);
+ CloseHandle(pi.hThread);
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle))
+ CloseHandle(fd_data[n].handle);
+ fd_data[n].handle = NULL;
+
+ if (fd_data[n].read) {
+ // Do the first read operation on each pipe
+ if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096,
+ &fd_data[n].read_ol))
+ {
+ CloseHandle(fd_data[n].read);
+ wait_hndls[n] = fd_data[n].read = NULL;
+ wait_hndl_count--;
+ }
+ }
+ }
+
+ if (opts->detach) {
+ res->error = MP_SUBPROCESS_OK;
+ goto done;
+ }
+
+ res->error = MP_SUBPROCESS_EGENERIC;
+
+ wait_hndls[MP_SUBPROCESS_MAX_FDS] = pi.hProcess;
+ wait_hndl_count++;
+
+ if (opts->cancel)
+ wait_hndls[MP_SUBPROCESS_MAX_FDS + 1] = mp_cancel_get_event(opts->cancel);
+
+ DWORD exit_code;
+ while (wait_hndl_count) {
+ int n = sparse_wait(wait_hndls, MP_ARRAY_SIZE(wait_hndls));
+
+ if (n >= 0 && n < MP_SUBPROCESS_MAX_FDS) {
+ // Complete the read operation on the pipe
+ if (!GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE)) {
+ CloseHandle(fd_data[n].read);
+ wait_hndls[n] = fd_data[n].read = NULL;
+ wait_hndl_count--;
+ } else {
+ opts->fds[n].on_read(opts->fds[n].on_read_ctx,
+ fd_data[n].read_buf, r);
+
+ // Begin the next read operation on the pipe
+ if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096,
+ &fd_data[n].read_ol))
+ {
+ CloseHandle(fd_data[n].read);
+ wait_hndls[n] = fd_data[n].read = NULL;
+ wait_hndl_count--;
+ }
+ }
+
+ } else if (n == MP_SUBPROCESS_MAX_FDS) { // pi.hProcess
+ GetExitCodeProcess(pi.hProcess, &exit_code);
+ res->exit_status = exit_code;
+
+ CloseHandle(pi.hProcess);
+ wait_hndls[n] = pi.hProcess = NULL;
+ wait_hndl_count--;
+
+ } else if (n == MP_SUBPROCESS_MAX_FDS + 1) { // opts.cancel
+ if (pi.hProcess) {
+ TerminateProcess(pi.hProcess, 1);
+ res->error = MP_SUBPROCESS_EKILLED_BY_US;
+ goto done;
+ }
+ } else {
+ goto done;
+ }
+ }
+
+ res->error = MP_SUBPROCESS_OK;
+
+done:
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (is_valid_handle(fd_data[n].read)) {
+ // Cancel any pending I/O (if the process was killed)
+ CancelIo(fd_data[n].read);
+ GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE);
+ CloseHandle(fd_data[n].read);
+ }
+ if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle))
+ CloseHandle(fd_data[n].handle);
+ if (fd_data[n].read_ol.hEvent)
+ CloseHandle(fd_data[n].read_ol.hEvent);
+ }
+ if (pi.hProcess)
+ CloseHandle(pi.hProcess);
+ talloc_free(tmp);
+}
diff --git a/osdep/subprocess.c b/osdep/subprocess.c
new file mode 100644
index 0000000..75cd124
--- /dev/null
+++ b/osdep/subprocess.c
@@ -0,0 +1,39 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+
+#include "subprocess.h"
+
+void mp_devnull(void *ctx, char *data, size_t size)
+{
+}
+
+const char *mp_subprocess_err_str(int num)
+{
+ // Note: these are visible to the public client API
+ switch (num) {
+ case MP_SUBPROCESS_OK: return "success";
+ case MP_SUBPROCESS_EKILLED_BY_US: return "killed";
+ case MP_SUBPROCESS_EINIT: return "init";
+ case MP_SUBPROCESS_EUNSUPPORTED: return "unsupported";
+ case MP_SUBPROCESS_EGENERIC: // fall through
+ default: return "unknown";
+ }
+}
diff --git a/osdep/subprocess.h b/osdep/subprocess.h
new file mode 100644
index 0000000..4bf2dc3
--- /dev/null
+++ b/osdep/subprocess.h
@@ -0,0 +1,88 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_SUBPROCESS_H_
+#define MP_SUBPROCESS_H_
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "misc/bstr.h"
+
+struct mp_cancel;
+
+// Incrementally called with data that was read. Buffer valid only during call.
+// size==0 means EOF.
+typedef void (*subprocess_read_cb)(void *ctx, char *data, size_t size);
+// Incrementally called to refill *mp_subprocess_fd.write_buf, whenever write_buf
+// has length 0 and the pipe is writable. While writing, *write_buf is adjusted
+// to contain only the not yet written data.
+// Not filling the buffer means EOF.
+typedef void (*subprocess_write_cb)(void *ctx);
+
+void mp_devnull(void *ctx, char *data, size_t size);
+
+#define MP_SUBPROCESS_MAX_FDS 10
+
+struct mp_subprocess_fd {
+ int fd; // target FD
+
+ // Only one of on_read or src_fd can be set. If none are set, use /dev/null.
+ // Note: "neutral" initialization requires setting src_fd=-1.
+ subprocess_read_cb on_read; // if not NULL, serve reads
+ void *on_read_ctx; // for on_read(on_read_ctx, ...)
+ subprocess_write_cb on_write; // if not NULL, serve writes
+ void *on_write_ctx; // for on_write(on_write_ctx, ...)
+ bstr *write_buf; // must be !=NULL if on_write is set
+ int src_fd; // if >=0, dup this FD to target FD
+};
+
+struct mp_subprocess_opts {
+ char *exe; // binary to execute (never non-NULL)
+ char **args; // argument list (NULL for none, otherwise NULL-terminated)
+ char **env; // if !NULL, set this as environment variable block
+ // Complete set of FDs passed down. All others are supposed to be closed.
+ struct mp_subprocess_fd fds[MP_SUBPROCESS_MAX_FDS];
+ int num_fds;
+ struct mp_cancel *cancel; // if !NULL, asynchronous process abort (kills it)
+ bool detach; // if true, do not wait for process to end
+};
+
+struct mp_subprocess_result {
+ int error; // one of MP_SUBPROCESS_* (>0 on error)
+ // NB: if WIFEXITED applies, error==0, and this is WEXITSTATUS
+ // on win32, this can use the full 32 bit
+ // if started with detach==true, this is always 0
+ uint32_t exit_status; // if error==0==MP_SUBPROCESS_OK, 0 otherwise
+};
+
+// Subprocess error values.
+#define MP_SUBPROCESS_OK 0 // no error
+#define MP_SUBPROCESS_EGENERIC -1 // unknown error
+#define MP_SUBPROCESS_EKILLED_BY_US -2 // mp_cancel was triggered
+#define MP_SUBPROCESS_EINIT -3 // error during initialization
+#define MP_SUBPROCESS_EUNSUPPORTED -4 // API not supported
+
+// Turn MP_SUBPROCESS_* values into a static string. Never returns NULL.
+const char *mp_subprocess_err_str(int num);
+
+// Caller must set *opts.
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res);
+
+#endif
diff --git a/osdep/terminal-dummy.c b/osdep/terminal-dummy.c
new file mode 100644
index 0000000..4b33d78
--- /dev/null
+++ b/osdep/terminal-dummy.c
@@ -0,0 +1,35 @@
+#include "terminal.h"
+
+void terminal_init(void)
+{
+}
+
+void terminal_setup_getch(struct input_ctx *ictx)
+{
+}
+
+void terminal_uninit(void)
+{
+}
+
+bool terminal_in_background(void)
+{
+ return false;
+}
+
+void terminal_get_size(int *w, int *h)
+{
+}
+
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height)
+{
+}
+
+void mp_write_console_ansi(void *wstream, char *buf)
+{
+}
+
+bool terminal_try_attach(void)
+{
+ return false;
+}
diff --git a/osdep/terminal-unix.c b/osdep/terminal-unix.c
new file mode 100644
index 0000000..d5b8fe3
--- /dev/null
+++ b/osdep/terminal-unix.c
@@ -0,0 +1,573 @@
+/*
+ * Based on GyS-TermIO v2.0 (for GySmail v3) (copyright (C) 1999 A'rpi/ESP-team)
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <signal.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <assert.h>
+
+#include <termios.h>
+#include <unistd.h>
+
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/poll_wrapper.h"
+
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "misc/ctype.h"
+#include "terminal.h"
+
+// Timeout in ms after which the (normally ambiguous) ESC key is detected.
+#define ESC_TIMEOUT 100
+
+// Timeout in ms after which the poll for input is aborted. The FG/BG state is
+// tested before every wait, and a positive value allows reactivating input
+// processing when mpv is brought to the foreground while it was running in the
+// background. In such a situation, an infinite timeout (-1) will keep mpv
+// waiting for input without realizing the terminal state changed - and thus
+// buffer all keypresses until ENTER is pressed.
+#define INPUT_TIMEOUT 1000
+
+static struct termios tio_orig;
+
+static int tty_in = -1, tty_out = -1;
+
+struct key_entry {
+ const char *seq;
+ int mpkey;
+ // If this is not NULL, then if seq is matched as unique prefix, the
+ // existing sequence is replaced by the following string. Matching
+ // continues normally, and mpkey is or-ed into the final result.
+ const char *replace;
+};
+
+static const struct key_entry keys[] = {
+ {"\010", MP_KEY_BS},
+ {"\011", MP_KEY_TAB},
+ {"\012", MP_KEY_ENTER},
+ {"\177", MP_KEY_BS},
+
+ {"\033[1~", MP_KEY_HOME},
+ {"\033[2~", MP_KEY_INS},
+ {"\033[3~", MP_KEY_DEL},
+ {"\033[4~", MP_KEY_END},
+ {"\033[5~", MP_KEY_PGUP},
+ {"\033[6~", MP_KEY_PGDWN},
+ {"\033[7~", MP_KEY_HOME},
+ {"\033[8~", MP_KEY_END},
+
+ {"\033[11~", MP_KEY_F+1},
+ {"\033[12~", MP_KEY_F+2},
+ {"\033[13~", MP_KEY_F+3},
+ {"\033[14~", MP_KEY_F+4},
+ {"\033[15~", MP_KEY_F+5},
+ {"\033[17~", MP_KEY_F+6},
+ {"\033[18~", MP_KEY_F+7},
+ {"\033[19~", MP_KEY_F+8},
+ {"\033[20~", MP_KEY_F+9},
+ {"\033[21~", MP_KEY_F+10},
+ {"\033[23~", MP_KEY_F+11},
+ {"\033[24~", MP_KEY_F+12},
+
+ {"\033OA", MP_KEY_UP},
+ {"\033OB", MP_KEY_DOWN},
+ {"\033OC", MP_KEY_RIGHT},
+ {"\033OD", MP_KEY_LEFT},
+ {"\033[A", MP_KEY_UP},
+ {"\033[B", MP_KEY_DOWN},
+ {"\033[C", MP_KEY_RIGHT},
+ {"\033[D", MP_KEY_LEFT},
+ {"\033[E", MP_KEY_KP5},
+ {"\033[F", MP_KEY_END},
+ {"\033[H", MP_KEY_HOME},
+
+ {"\033[[A", MP_KEY_F+1},
+ {"\033[[B", MP_KEY_F+2},
+ {"\033[[C", MP_KEY_F+3},
+ {"\033[[D", MP_KEY_F+4},
+ {"\033[[E", MP_KEY_F+5},
+
+ {"\033OE", MP_KEY_KP5}, // mintty?
+ {"\033OM", MP_KEY_KPENTER},
+ {"\033OP", MP_KEY_F+1},
+ {"\033OQ", MP_KEY_F+2},
+ {"\033OR", MP_KEY_F+3},
+ {"\033OS", MP_KEY_F+4},
+
+ {"\033Oa", MP_KEY_UP | MP_KEY_MODIFIER_CTRL}, // urxvt
+ {"\033Ob", MP_KEY_DOWN | MP_KEY_MODIFIER_CTRL},
+ {"\033Oc", MP_KEY_RIGHT | MP_KEY_MODIFIER_CTRL},
+ {"\033Od", MP_KEY_LEFT | MP_KEY_MODIFIER_CTRL},
+ {"\033Oj", '*'}, // also keypad, but we don't have separate codes for them
+ {"\033Ok", '+'},
+ {"\033Om", '-'},
+ {"\033On", MP_KEY_KPDEC},
+ {"\033Oo", '/'},
+ {"\033Op", MP_KEY_KP0},
+ {"\033Oq", MP_KEY_KP1},
+ {"\033Or", MP_KEY_KP2},
+ {"\033Os", MP_KEY_KP3},
+ {"\033Ot", MP_KEY_KP4},
+ {"\033Ou", MP_KEY_KP5},
+ {"\033Ov", MP_KEY_KP6},
+ {"\033Ow", MP_KEY_KP7},
+ {"\033Ox", MP_KEY_KP8},
+ {"\033Oy", MP_KEY_KP9},
+
+ {"\033[a", MP_KEY_UP | MP_KEY_MODIFIER_SHIFT}, // urxvt
+ {"\033[b", MP_KEY_DOWN | MP_KEY_MODIFIER_SHIFT},
+ {"\033[c", MP_KEY_RIGHT | MP_KEY_MODIFIER_SHIFT},
+ {"\033[d", MP_KEY_LEFT | MP_KEY_MODIFIER_SHIFT},
+ {"\033[2^", MP_KEY_INS | MP_KEY_MODIFIER_CTRL},
+ {"\033[3^", MP_KEY_DEL | MP_KEY_MODIFIER_CTRL},
+ {"\033[5^", MP_KEY_PGUP | MP_KEY_MODIFIER_CTRL},
+ {"\033[6^", MP_KEY_PGDWN | MP_KEY_MODIFIER_CTRL},
+ {"\033[7^", MP_KEY_HOME | MP_KEY_MODIFIER_CTRL},
+ {"\033[8^", MP_KEY_END | MP_KEY_MODIFIER_CTRL},
+
+ {"\033[1;2", MP_KEY_MODIFIER_SHIFT, .replace = "\033["}, // xterm
+ {"\033[1;3", MP_KEY_MODIFIER_ALT, .replace = "\033["},
+ {"\033[1;5", MP_KEY_MODIFIER_CTRL, .replace = "\033["},
+ {"\033[1;4", MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_SHIFT, .replace = "\033["},
+ {"\033[1;6", MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_SHIFT, .replace = "\033["},
+ {"\033[1;7", MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_ALT, .replace = "\033["},
+ {"\033[1;8",
+ MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_SHIFT,
+ .replace = "\033["},
+
+ {"\033[29~", MP_KEY_MENU},
+ {"\033[Z", MP_KEY_TAB | MP_KEY_MODIFIER_SHIFT},
+
+ {0}
+};
+
+#define BUF_LEN 256
+
+struct termbuf {
+ unsigned char b[BUF_LEN];
+ int len;
+ int mods;
+};
+
+static void skip_buf(struct termbuf *b, unsigned int count)
+{
+ assert(count <= b->len);
+
+ memmove(&b->b[0], &b->b[count], b->len - count);
+ b->len -= count;
+ b->mods = 0;
+}
+
+static struct termbuf buf;
+
+static void process_input(struct input_ctx *input_ctx, bool timeout)
+{
+ while (buf.len) {
+ // Lone ESC is ambiguous, so accept it only after a timeout.
+ if (timeout &&
+ ((buf.len == 1 && buf.b[0] == '\033') ||
+ (buf.len > 1 && buf.b[0] == '\033' && buf.b[1] == '\033')))
+ {
+ mp_input_put_key(input_ctx, MP_KEY_ESC);
+ skip_buf(&buf, 1);
+ }
+
+ int utf8_len = bstr_parse_utf8_code_length(buf.b[0]);
+ if (utf8_len > 1) {
+ if (buf.len < utf8_len)
+ goto read_more;
+
+ mp_input_put_key_utf8(input_ctx, buf.mods, (bstr){buf.b, utf8_len});
+ skip_buf(&buf, utf8_len);
+ continue;
+ }
+
+ const struct key_entry *match = NULL; // may be a partial match
+ for (int n = 0; keys[n].seq; n++) {
+ const struct key_entry *e = &keys[n];
+ if (memcmp(e->seq, buf.b, MPMIN(buf.len, strlen(e->seq))) == 0) {
+ if (match)
+ goto read_more; /* need more bytes to disambiguate */
+ match = e;
+ }
+ }
+
+ if (!match) { // normal or unknown key
+ int mods = 0;
+ if (buf.b[0] == '\033') {
+ if (buf.len > 1 && buf.b[1] == '[') {
+ // unknown CSI sequence. wait till it completes
+ for (int i = 2; i < buf.len; i++) {
+ if (buf.b[i] >= 0x40 && buf.b[i] <= 0x7E) {
+ skip_buf(&buf, i+1);
+ continue; // complete - throw it away
+ }
+ }
+ goto read_more; // not yet complete
+ }
+ // non-CSI esc sequence
+ skip_buf(&buf, 1);
+ if (buf.len > 0 && buf.b[0] > 0 && buf.b[0] < 127) {
+ // meta+normal key
+ mods |= MP_KEY_MODIFIER_ALT;
+ } else {
+ // Throw it away. Typically, this will be a complete,
+ // unsupported sequence, and dropping this will skip it.
+ skip_buf(&buf, buf.len);
+ continue;
+ }
+ }
+ unsigned char c = buf.b[0];
+ skip_buf(&buf, 1);
+ if (c < 32) {
+ // 1..26 is ^A..^Z, and 27..31 is ^3..^7
+ c = c <= 26 ? (c + 'a' - 1) : (c + '3' - 27);
+ mods |= MP_KEY_MODIFIER_CTRL;
+ }
+ mp_input_put_key(input_ctx, c | mods);
+ continue;
+ }
+
+ int seq_len = strlen(match->seq);
+ if (seq_len > buf.len)
+ goto read_more; /* partial match */
+
+ if (match->replace) {
+ int rep = strlen(match->replace);
+ assert(rep <= seq_len);
+ memcpy(buf.b, match->replace, rep);
+ memmove(buf.b + rep, buf.b + seq_len, buf.len - seq_len);
+ buf.len = rep + buf.len - seq_len;
+ buf.mods |= match->mpkey;
+ continue;
+ }
+
+ mp_input_put_key(input_ctx, buf.mods | match->mpkey);
+ skip_buf(&buf, seq_len);
+ }
+
+read_more: ; /* need more bytes */
+}
+
+static int getch2_active = 0;
+static int getch2_enabled = 0;
+static bool read_terminal;
+
+static void enable_kx(bool enable)
+{
+ // This check is actually always true, as enable_kx calls are all guarded
+ // by read_terminal, which is true only if both stdin and stdout are a
+ // tty. Note that stderr being redirected away has no influence over mpv's
+ // I/O handling except for disabling the terminal OSD, and thus stderr
+ // shouldn't be relied on here either.
+ if (isatty(tty_out)) {
+ char *cmd = enable ? "\033=" : "\033>";
+ (void)write(tty_out, cmd, strlen(cmd));
+ }
+}
+
+static void do_activate_getch2(void)
+{
+ if (getch2_active || !read_terminal)
+ return;
+
+ enable_kx(true);
+
+ struct termios tio_new;
+ tcgetattr(tty_in,&tio_new);
+
+ tio_new.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
+ tio_new.c_cc[VMIN] = 1;
+ tio_new.c_cc[VTIME] = 0;
+ tcsetattr(tty_in,TCSANOW,&tio_new);
+
+ getch2_active = 1;
+}
+
+static void do_deactivate_getch2(void)
+{
+ if (!getch2_active)
+ return;
+
+ enable_kx(false);
+ tcsetattr(tty_in, TCSANOW, &tio_orig);
+
+ getch2_active = 0;
+}
+
+// sigaction wrapper
+static int setsigaction(int signo, void (*handler) (int),
+ int flags, bool do_mask)
+{
+ struct sigaction sa;
+ sa.sa_handler = handler;
+
+ if(do_mask)
+ sigfillset(&sa.sa_mask);
+ else
+ sigemptyset(&sa.sa_mask);
+
+ sa.sa_flags = flags | SA_RESTART;
+ return sigaction(signo, &sa, NULL);
+}
+
+static void getch2_poll(void)
+{
+ if (!getch2_enabled)
+ return;
+
+ // check if stdin is in the foreground process group
+ int newstatus = (tcgetpgrp(tty_in) == getpgrp());
+
+ // and activate getch2 if it is, deactivate otherwise
+ if (newstatus)
+ do_activate_getch2();
+ else
+ do_deactivate_getch2();
+}
+
+static mp_thread input_thread;
+static struct input_ctx *input_ctx;
+static int death_pipe[2] = {-1, -1};
+enum { PIPE_STOP, PIPE_CONT };
+static int stop_cont_pipe[2] = {-1, -1};
+
+static void stop_sighandler(int signum)
+{
+ int saved_errno = errno;
+ (void)write(stop_cont_pipe[1], &(char){PIPE_STOP}, 1);
+ errno = saved_errno;
+
+ // note: for this signal, we use SA_RESETHAND but do NOT mask signals
+ // so this will invoke the default handler
+ raise(SIGTSTP);
+}
+
+static void continue_sighandler(int signum)
+{
+ int saved_errno = errno;
+ // SA_RESETHAND has reset SIGTSTP, so we need to restore it here
+ setsigaction(SIGTSTP, stop_sighandler, SA_RESETHAND, false);
+
+ (void)write(stop_cont_pipe[1], &(char){PIPE_CONT}, 1);
+ errno = saved_errno;
+}
+
+static void safe_close(int *p)
+{
+ if (*p >= 0)
+ close(*p);
+ *p = -1;
+}
+
+static void close_sig_pipes(void)
+{
+ for (int n = 0; n < 2; n++) {
+ safe_close(&death_pipe[n]);
+ safe_close(&stop_cont_pipe[n]);
+ }
+}
+
+static void close_tty(void)
+{
+ if (tty_in >= 0 && tty_in != STDIN_FILENO)
+ close(tty_in);
+
+ tty_in = tty_out = -1;
+}
+
+static void quit_request_sighandler(int signum)
+{
+ int saved_errno = errno;
+ (void)write(death_pipe[1], &(char){1}, 1);
+ errno = saved_errno;
+}
+
+static MP_THREAD_VOID terminal_thread(void *ptr)
+{
+ mp_thread_set_name("terminal/input");
+ bool stdin_ok = read_terminal; // if false, we still wait for SIGTERM
+ while (1) {
+ getch2_poll();
+ struct pollfd fds[3] = {
+ { .events = POLLIN, .fd = death_pipe[0] },
+ { .events = POLLIN, .fd = stop_cont_pipe[0] },
+ { .events = POLLIN, .fd = tty_in }
+ };
+ /*
+ * if the process isn't in foreground process group, then on macos
+ * polldev() doesn't rest and gets into 100% cpu usage (see issue #11795)
+ * with read() returning EIO. but we shouldn't quit on EIO either since
+ * the process might be foregrounded later.
+ *
+ * so just avoid poll-ing tty_in when we know the process is not in the
+ * foreground. there's a small race window, but the timeout will take
+ * care of it so it's fine.
+ */
+ bool is_fg = tcgetpgrp(tty_in) == getpgrp();
+ int r = polldev(fds, stdin_ok && is_fg ? 3 : 2, buf.len ? ESC_TIMEOUT : INPUT_TIMEOUT);
+ if (fds[0].revents) {
+ do_deactivate_getch2();
+ break;
+ }
+ if (fds[1].revents & POLLIN) {
+ int8_t c = -1;
+ (void)read(stop_cont_pipe[0], &c, 1);
+ if (c == PIPE_STOP)
+ do_deactivate_getch2();
+ else if (c == PIPE_CONT)
+ getch2_poll();
+ }
+ if (fds[2].revents) {
+ int retval = read(tty_in, &buf.b[buf.len], BUF_LEN - buf.len);
+ if (!retval || (retval == -1 && errno != EINTR && errno != EAGAIN && errno != EIO))
+ break; // EOF/closed
+ if (retval > 0) {
+ buf.len += retval;
+ process_input(input_ctx, false);
+ }
+ }
+ if (r == 0)
+ process_input(input_ctx, true);
+ }
+ char c;
+ bool quit = read(death_pipe[0], &c, 1) == 1 && c == 1;
+ // Important if we received SIGTERM, rather than regular quit.
+ if (quit) {
+ struct mp_cmd *cmd = mp_input_parse_cmd(input_ctx, bstr0("quit 4"), "");
+ if (cmd)
+ mp_input_queue_cmd(input_ctx, cmd);
+ }
+ MP_THREAD_RETURN();
+}
+
+void terminal_setup_getch(struct input_ctx *ictx)
+{
+ if (!getch2_enabled || input_ctx)
+ return;
+
+ if (mp_make_wakeup_pipe(death_pipe) < 0)
+ return;
+ if (mp_make_wakeup_pipe(stop_cont_pipe) < 0) {
+ close_sig_pipes();
+ return;
+ }
+
+ // Disable reading from the terminal even if stdout is not a tty, to make
+ // mpv ... | less
+ // do the right thing.
+ read_terminal = isatty(tty_in) && isatty(STDOUT_FILENO);
+
+ input_ctx = ictx;
+
+ if (mp_thread_create(&input_thread, terminal_thread, NULL)) {
+ input_ctx = NULL;
+ close_sig_pipes();
+ close_tty();
+ return;
+ }
+
+ setsigaction(SIGINT, quit_request_sighandler, SA_RESETHAND, false);
+ setsigaction(SIGQUIT, quit_request_sighandler, SA_RESETHAND, false);
+ setsigaction(SIGTERM, quit_request_sighandler, SA_RESETHAND, false);
+}
+
+void terminal_uninit(void)
+{
+ if (!getch2_enabled)
+ return;
+
+ // restore signals
+ setsigaction(SIGCONT, SIG_DFL, 0, false);
+ setsigaction(SIGTSTP, SIG_DFL, 0, false);
+ setsigaction(SIGINT, SIG_DFL, 0, false);
+ setsigaction(SIGQUIT, SIG_DFL, 0, false);
+ setsigaction(SIGTERM, SIG_DFL, 0, false);
+ setsigaction(SIGTTIN, SIG_DFL, 0, false);
+ setsigaction(SIGTTOU, SIG_DFL, 0, false);
+
+ if (input_ctx) {
+ (void)write(death_pipe[1], &(char){0}, 1);
+ mp_thread_join(input_thread);
+ close_sig_pipes();
+ input_ctx = NULL;
+ }
+
+ do_deactivate_getch2();
+ close_tty();
+
+ getch2_enabled = 0;
+ read_terminal = false;
+}
+
+bool terminal_in_background(void)
+{
+ return read_terminal && tcgetpgrp(STDERR_FILENO) != getpgrp();
+}
+
+void terminal_get_size(int *w, int *h)
+{
+ struct winsize ws;
+ if (ioctl(tty_in, TIOCGWINSZ, &ws) < 0 || !ws.ws_row || !ws.ws_col)
+ return;
+
+ *w = ws.ws_col;
+ *h = ws.ws_row;
+}
+
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height)
+{
+ struct winsize ws;
+ if (ioctl(tty_in, TIOCGWINSZ, &ws) < 0 || !ws.ws_row || !ws.ws_col
+ || !ws.ws_xpixel || !ws.ws_ypixel)
+ return;
+
+ *rows = ws.ws_row;
+ *cols = ws.ws_col;
+ *px_width = ws.ws_xpixel;
+ *px_height = ws.ws_ypixel;
+}
+
+void terminal_init(void)
+{
+ assert(!getch2_enabled);
+ getch2_enabled = 1;
+
+ tty_in = tty_out = open("/dev/tty", O_RDWR | O_CLOEXEC);
+ if (tty_in < 0) {
+ tty_in = STDIN_FILENO;
+ tty_out = STDOUT_FILENO;
+ }
+
+ tcgetattr(tty_in, &tio_orig);
+
+ // handlers to fix terminal settings
+ setsigaction(SIGCONT, continue_sighandler, 0, true);
+ setsigaction(SIGTSTP, stop_sighandler, SA_RESETHAND, false);
+ setsigaction(SIGTTIN, SIG_IGN, 0, true);
+ setsigaction(SIGTTOU, SIG_IGN, 0, true);
+
+ getch2_poll();
+}
diff --git a/osdep/terminal-win.c b/osdep/terminal-win.c
new file mode 100644
index 0000000..8f3410c
--- /dev/null
+++ b/osdep/terminal-win.c
@@ -0,0 +1,425 @@
+/* Windows TermIO
+ *
+ * copyright (C) 2003 Sascha Sommer
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <windows.h>
+#include <io.h>
+#include <assert.h>
+#include "common/common.h"
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "terminal.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/w32_keyboard.h"
+
+// https://docs.microsoft.com/en-us/windows/console/setconsolemode
+// These values are effective on Windows 10 build 16257 (August 2017) or later
+#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
+ #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
+#endif
+#ifndef DISABLE_NEWLINE_AUTO_RETURN
+ #define DISABLE_NEWLINE_AUTO_RETURN 0x0008
+#endif
+
+// Note: the DISABLE_NEWLINE_AUTO_RETURN docs say it enables delayed-wrap, but
+// it's wrong. It does only what its names suggests - and we want it unset:
+// https://github.com/microsoft/terminal/issues/4126#issuecomment-571418661
+static void attempt_native_out_vt(HANDLE hOut, DWORD basemode)
+{
+ DWORD vtmode = basemode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+ vtmode &= ~DISABLE_NEWLINE_AUTO_RETURN;
+ if (!SetConsoleMode(hOut, vtmode))
+ SetConsoleMode(hOut, basemode);
+}
+
+static bool is_native_out_vt(HANDLE hOut)
+{
+ DWORD cmode;
+ return GetConsoleMode(hOut, &cmode) &&
+ (cmode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) &&
+ !(cmode & DISABLE_NEWLINE_AUTO_RETURN);
+}
+
+#define hSTDOUT GetStdHandle(STD_OUTPUT_HANDLE)
+#define hSTDERR GetStdHandle(STD_ERROR_HANDLE)
+
+#define FOREGROUND_ALL (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
+#define BACKGROUND_ALL (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE)
+
+static short stdoutAttrs = 0; // copied from the screen buffer on init
+static const unsigned char ansi2win32[8] = {
+ 0,
+ FOREGROUND_RED,
+ FOREGROUND_GREEN,
+ FOREGROUND_GREEN | FOREGROUND_RED,
+ FOREGROUND_BLUE,
+ FOREGROUND_BLUE | FOREGROUND_RED,
+ FOREGROUND_BLUE | FOREGROUND_GREEN,
+ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED,
+};
+static const unsigned char ansi2win32bg[8] = {
+ 0,
+ BACKGROUND_RED,
+ BACKGROUND_GREEN,
+ BACKGROUND_GREEN | BACKGROUND_RED,
+ BACKGROUND_BLUE,
+ BACKGROUND_BLUE | BACKGROUND_RED,
+ BACKGROUND_BLUE | BACKGROUND_GREEN,
+ BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED,
+};
+
+static bool running;
+static HANDLE death;
+static mp_thread input_thread;
+static struct input_ctx *input_ctx;
+
+void terminal_get_size(int *w, int *h)
+{
+ CONSOLE_SCREEN_BUFFER_INFO cinfo;
+ HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (GetConsoleScreenBufferInfo(hOut, &cinfo)) {
+ *w = cinfo.dwMaximumWindowSize.X - (is_native_out_vt(hOut) ? 0 : 1);
+ *h = cinfo.dwMaximumWindowSize.Y;
+ }
+}
+
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height)
+{
+}
+
+static bool has_input_events(HANDLE h)
+{
+ DWORD num_events;
+ if (!GetNumberOfConsoleInputEvents(h, &num_events))
+ return false;
+ return !!num_events;
+}
+
+static void read_input(HANDLE in)
+{
+ // Process any input events in the buffer
+ while (has_input_events(in)) {
+ INPUT_RECORD event;
+ if (!ReadConsoleInputW(in, &event, 1, &(DWORD){0}))
+ break;
+
+ // Only key-down events are interesting to us
+ if (event.EventType != KEY_EVENT)
+ continue;
+ KEY_EVENT_RECORD *record = &event.Event.KeyEvent;
+ if (!record->bKeyDown)
+ continue;
+
+ UINT vkey = record->wVirtualKeyCode;
+ bool ext = record->dwControlKeyState & ENHANCED_KEY;
+
+ int mods = 0;
+ if (record->dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED))
+ mods |= MP_KEY_MODIFIER_ALT;
+ if (record->dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED))
+ mods |= MP_KEY_MODIFIER_CTRL;
+ if (record->dwControlKeyState & SHIFT_PRESSED)
+ mods |= MP_KEY_MODIFIER_SHIFT;
+
+ int mpkey = mp_w32_vkey_to_mpkey(vkey, ext);
+ if (mpkey) {
+ mp_input_put_key(input_ctx, mpkey | mods);
+ } else {
+ // Only characters should be remaining
+ int c = record->uChar.UnicodeChar;
+ // The ctrl key always produces control characters in the console.
+ // Shift them back up to regular characters.
+ if (c > 0 && c < 0x20 && (mods & MP_KEY_MODIFIER_CTRL))
+ c += (mods & MP_KEY_MODIFIER_SHIFT) ? 0x40 : 0x60;
+ if (c >= 0x20)
+ mp_input_put_key(input_ctx, c | mods);
+ }
+ }
+}
+
+static MP_THREAD_VOID input_thread_fn(void *ptr)
+{
+ mp_thread_set_name("terminal/input");
+ HANDLE in = ptr;
+ HANDLE stuff[2] = {in, death};
+ while (1) {
+ DWORD r = WaitForMultipleObjects(2, stuff, FALSE, INFINITE);
+ if (r != WAIT_OBJECT_0)
+ break;
+ read_input(in);
+ }
+ MP_THREAD_RETURN();
+}
+
+void terminal_setup_getch(struct input_ctx *ictx)
+{
+ if (running)
+ return;
+
+ HANDLE in = GetStdHandle(STD_INPUT_HANDLE);
+ if (GetNumberOfConsoleInputEvents(in, &(DWORD){0})) {
+ input_ctx = ictx;
+ death = CreateEventW(NULL, TRUE, FALSE, NULL);
+ if (!death)
+ return;
+ if (mp_thread_create(&input_thread, input_thread_fn, in)) {
+ CloseHandle(death);
+ return;
+ }
+ running = true;
+ }
+}
+
+void terminal_uninit(void)
+{
+ if (running) {
+ SetEvent(death);
+ mp_thread_join(input_thread);
+ input_ctx = NULL;
+ running = false;
+ }
+}
+
+bool terminal_in_background(void)
+{
+ return false;
+}
+
+void mp_write_console_ansi(HANDLE wstream, char *buf)
+{
+ wchar_t *wbuf = mp_from_utf8(NULL, buf);
+ wchar_t *pos = wbuf;
+
+ while (*pos) {
+ if (is_native_out_vt(wstream)) {
+ WriteConsoleW(wstream, pos, wcslen(pos), NULL, NULL);
+ break;
+ }
+ wchar_t *next = wcschr(pos, '\033');
+ if (!next) {
+ WriteConsoleW(wstream, pos, wcslen(pos), NULL, NULL);
+ break;
+ }
+ next[0] = '\0';
+ WriteConsoleW(wstream, pos, wcslen(pos), NULL, NULL);
+ if (next[1] == '[') {
+ // CSI - Control Sequence Introducer
+ next += 2;
+
+ // CSI codes generally follow this syntax:
+ // "\033[" [ <i> (';' <i> )* ] <c>
+ // where <i> are integers, and <c> a single char command code.
+ // Also see: http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes
+ int params[16]; // 'm' might be unlimited; ignore that
+ int num_params = 0;
+ while (num_params < MP_ARRAY_SIZE(params)) {
+ wchar_t *end = next;
+ long p = wcstol(next, &end, 10);
+ if (end == next)
+ break;
+ next = end;
+ params[num_params++] = p;
+ if (next[0] != ';' || !next[0])
+ break;
+ next += 1;
+ }
+ wchar_t code = next[0];
+ if (code)
+ next += 1;
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ GetConsoleScreenBufferInfo(wstream, &info);
+ switch (code) {
+ case 'K': { // erase to end of line
+ COORD at = info.dwCursorPosition;
+ int len = info.dwSize.X - at.X;
+ FillConsoleOutputCharacterW(wstream, ' ', len, at, &(DWORD){0});
+ SetConsoleCursorPosition(wstream, at);
+ break;
+ }
+ case 'A': { // cursor up
+ info.dwCursorPosition.Y -= 1;
+ SetConsoleCursorPosition(wstream, info.dwCursorPosition);
+ break;
+ }
+ case 'm': { // "SGR"
+ short attr = info.wAttributes;
+ if (num_params == 0) // reset
+ params[num_params++] = 0;
+
+ // we don't emulate italic, reverse/underline don't always work
+ for (int n = 0; n < num_params; n++) {
+ int p = params[n];
+ if (p == 0) {
+ attr = stdoutAttrs;
+ } else if (p == 1) {
+ attr |= FOREGROUND_INTENSITY;
+ } else if (p == 22) {
+ attr &= ~FOREGROUND_INTENSITY;
+ } else if (p == 4) {
+ attr |= COMMON_LVB_UNDERSCORE;
+ } else if (p == 24) {
+ attr &= ~COMMON_LVB_UNDERSCORE;
+ } else if (p == 7) {
+ attr |= COMMON_LVB_REVERSE_VIDEO;
+ } else if (p == 27) {
+ attr &= ~COMMON_LVB_REVERSE_VIDEO;
+ } else if (p >= 30 && p <= 37) {
+ attr &= ~FOREGROUND_ALL;
+ attr |= ansi2win32[p - 30];
+ } else if (p == 39) {
+ attr &= ~FOREGROUND_ALL;
+ attr |= stdoutAttrs & FOREGROUND_ALL;
+ } else if (p >= 40 && p <= 47) {
+ attr &= ~BACKGROUND_ALL;
+ attr |= ansi2win32bg[p - 40];
+ } else if (p == 49) {
+ attr &= ~BACKGROUND_ALL;
+ attr |= stdoutAttrs & BACKGROUND_ALL;
+ } else if (p == 38 || p == 48) { // ignore and skip sub-values
+ // 256 colors: <38/48>;5;N true colors: <38/48>;2;R;G;B
+ if (n+1 < num_params) {
+ n += params[n+1] == 5 ? 2
+ : params[n+1] == 2 ? 4
+ : num_params; /* unrecognized -> the rest */
+ }
+ }
+ }
+
+ if (attr != info.wAttributes)
+ SetConsoleTextAttribute(wstream, attr);
+ break;
+ }
+ }
+ } else if (next[1] == ']') {
+ // OSC - Operating System Commands
+ next += 2;
+
+ // OSC sequences generally follow this syntax:
+ // "\033]" <command> ST
+ // Where <command> is a string command
+ wchar_t *cmd = next;
+ while (next[0]) {
+ // BEL can be used instead of ST in xterm
+ if (next[0] == '\007' || next[0] == 0x9c) {
+ next[0] = '\0';
+ next += 1;
+ break;
+ }
+ if (next[0] == '\033' && next[1] == '\\') {
+ next[0] = '\0';
+ next += 2;
+ break;
+ }
+ next += 1;
+ }
+
+ // Handle xterm-style OSC commands
+ if (cmd[0] && cmd[1] == ';') {
+ wchar_t code = cmd[0];
+ wchar_t *param = cmd + 2;
+
+ switch (code) {
+ case '0': // Change Icon Name and Window Title
+ case '2': // Change Window Title
+ SetConsoleTitleW(param);
+ break;
+ }
+ }
+ } else {
+ WriteConsoleW(wstream, L"\033", 1, NULL, NULL);
+ }
+ pos = next;
+ }
+
+ talloc_free(wbuf);
+}
+
+static bool is_a_console(HANDLE h)
+{
+ return GetConsoleMode(h, &(DWORD){0});
+}
+
+static void reopen_console_handle(DWORD std, int fd, FILE *stream)
+{
+ HANDLE handle = GetStdHandle(std);
+ if (is_a_console(handle)) {
+ if (fd == 0) {
+ freopen("CONIN$", "rt", stream);
+ } else {
+ freopen("CONOUT$", "wt", stream);
+ }
+ setvbuf(stream, NULL, _IONBF, 0);
+
+ // Set the low-level FD to the new handle value, since mp_subprocess2
+ // callers might rely on low-level FDs being set. Note, with this
+ // method, fileno(stdin) != STDIN_FILENO, but that shouldn't matter.
+ int unbound_fd = -1;
+ if (fd == 0) {
+ unbound_fd = _open_osfhandle((intptr_t)handle, _O_RDONLY);
+ } else {
+ unbound_fd = _open_osfhandle((intptr_t)handle, _O_WRONLY);
+ }
+ // dup2 will duplicate the underlying handle. Don't close unbound_fd,
+ // since that will close the original handle.
+ dup2(unbound_fd, fd);
+ }
+}
+
+bool terminal_try_attach(void)
+{
+ // mpv.exe is a flagged as a GUI application, but it acts as a console
+ // application when started from the console wrapper (see
+ // osdep/win32-console-wrapper.c). The console wrapper sets
+ // _started_from_console=yes, so check that variable before trying to
+ // attach to the console.
+ wchar_t console_env[4] = { 0 };
+ if (!GetEnvironmentVariableW(L"_started_from_console", console_env, 4))
+ return false;
+ if (wcsncmp(console_env, L"yes", 4))
+ return false;
+ SetEnvironmentVariableW(L"_started_from_console", NULL);
+
+ if (!AttachConsole(ATTACH_PARENT_PROCESS))
+ return false;
+
+ // We have a console window. Redirect input/output streams to that console's
+ // low-level handles, so things that use stdio work later on.
+ reopen_console_handle(STD_INPUT_HANDLE, STDIN_FILENO, stdin);
+ reopen_console_handle(STD_OUTPUT_HANDLE, STDOUT_FILENO, stdout);
+ reopen_console_handle(STD_ERROR_HANDLE, STDERR_FILENO, stderr);
+
+ return true;
+}
+
+void terminal_init(void)
+{
+ CONSOLE_SCREEN_BUFFER_INFO cinfo;
+ DWORD cmode = 0;
+ GetConsoleMode(hSTDOUT, &cmode);
+ cmode |= (ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT);
+ attempt_native_out_vt(hSTDOUT, cmode);
+ attempt_native_out_vt(hSTDERR, cmode);
+ GetConsoleScreenBufferInfo(hSTDOUT, &cinfo);
+ stdoutAttrs = cinfo.wAttributes;
+}
diff --git a/osdep/terminal.h b/osdep/terminal.h
new file mode 100644
index 0000000..5383a17
--- /dev/null
+++ b/osdep/terminal.h
@@ -0,0 +1,60 @@
+/*
+ * Based on GyS-TermIO v2.0 (for GySmail v3) (copyright (C) 1999 A'rpi/ESP-team)
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_GETCH2_H
+#define MPLAYER_GETCH2_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#define TERM_ESC_GOTO_YX "\033[%d;%df"
+#define TERM_ESC_HIDE_CURSOR "\033[?25l"
+#define TERM_ESC_RESTORE_CURSOR "\033[?25h"
+
+#define TERM_ESC_CLEAR_SCREEN "\033[2J"
+#define TERM_ESC_ALT_SCREEN "\033[?1049h"
+#define TERM_ESC_NORMAL_SCREEN "\033[?1049l"
+
+struct input_ctx;
+
+/* Global initialization for terminal output. */
+void terminal_init(void);
+
+/* Setup ictx to read keys from the terminal */
+void terminal_setup_getch(struct input_ctx *ictx);
+
+/* Undo terminal_init(), and also terminal_setup_getch() */
+void terminal_uninit(void);
+
+/* Return whether the process has been backgrounded. */
+bool terminal_in_background(void);
+
+/* Get terminal-size in columns/rows. */
+void terminal_get_size(int *w, int *h);
+
+/* Get terminal-size in columns/rows and width/height in pixels. */
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height);
+
+// Windows only.
+void mp_write_console_ansi(void *wstream, char *buf);
+
+/* Windows-only function to attach to the parent process's console */
+bool terminal_try_attach(void);
+
+#endif /* MPLAYER_GETCH2_H */
diff --git a/osdep/threads-posix.c b/osdep/threads-posix.c
new file mode 100644
index 0000000..0b09a7c
--- /dev/null
+++ b/osdep/threads-posix.c
@@ -0,0 +1,64 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include "common/common.h"
+#include "config.h"
+#include "threads.h"
+#include "timer.h"
+
+#if HAVE_BSD_THREAD_NAME
+#include <pthread_np.h>
+#endif
+
+int mp_ptwrap_check(const char *file, int line, int res)
+{
+ if (res && res != ETIMEDOUT) {
+ fprintf(stderr, "%s:%d: internal error: pthread result %d (%s)\n",
+ file, line, res, mp_strerror(res));
+ abort();
+ }
+ return res;
+}
+
+int mp_ptwrap_mutex_init(const char *file, int line, pthread_mutex_t *m,
+ const pthread_mutexattr_t *attr)
+{
+ pthread_mutexattr_t m_attr;
+ if (!attr) {
+ attr = &m_attr;
+ pthread_mutexattr_init(&m_attr);
+ // Force normal mutexes to error checking.
+ pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_ERRORCHECK);
+ }
+ int res = mp_ptwrap_check(file, line, (pthread_mutex_init)(m, attr));
+ if (attr == &m_attr)
+ pthread_mutexattr_destroy(&m_attr);
+ return res;
+}
+
+int mp_ptwrap_mutex_trylock(const char *file, int line, pthread_mutex_t *m)
+{
+ int res = (pthread_mutex_trylock)(m);
+
+ if (res != EBUSY)
+ mp_ptwrap_check(file, line, res);
+
+ return res;
+}
diff --git a/osdep/threads-posix.h b/osdep/threads-posix.h
new file mode 100644
index 0000000..482e4a8
--- /dev/null
+++ b/osdep/threads-posix.h
@@ -0,0 +1,247 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdio.h>
+
+#include "common/common.h"
+#include "config.h"
+#include "osdep/compiler.h"
+#include "timer.h"
+
+int mp_ptwrap_check(const char *file, int line, int res);
+int mp_ptwrap_mutex_init(const char *file, int line, pthread_mutex_t *m,
+ const pthread_mutexattr_t *attr);
+int mp_ptwrap_mutex_trylock(const char *file, int line, pthread_mutex_t *m);
+
+#if HAVE_PTHREAD_DEBUG
+
+// pthread debugging wrappers. Technically, this is undefined behavior, because
+// you are not supposed to define any symbols that clash with reserved names.
+// Other than that, they should be fine.
+
+// Note: mpv normally never checks pthread error return values of certain
+// functions that should never fail. It does so because these cases would
+// be undefined behavior anyway (such as double-frees etc.). However,
+// since there are no good pthread debugging tools, these wrappers are
+// provided for the sake of debugging. They crash on unexpected errors.
+//
+// Technically, pthread_cond/mutex_init() can fail with ENOMEM. We don't
+// really respect this for normal/recursive mutex types, as due to the
+// existence of static initializers, no sane implementation could actually
+// require allocating memory.
+
+#define MP_PTWRAP(fn, ...) \
+ mp_ptwrap_check(__FILE__, __LINE__, (fn)(__VA_ARGS__))
+
+// ISO C defines that all standard functions can be macros, except undef'ing
+// them is allowed and must make the "real" definitions available. (Whatever.)
+#undef pthread_cond_init
+#undef pthread_cond_destroy
+#undef pthread_cond_broadcast
+#undef pthread_cond_signal
+#undef pthread_cond_wait
+#undef pthread_cond_timedwait
+#undef pthread_detach
+#undef pthread_join
+#undef pthread_mutex_destroy
+#undef pthread_mutex_lock
+#undef pthread_mutex_trylock
+#undef pthread_mutex_unlock
+
+#define pthread_cond_init(...) MP_PTWRAP(pthread_cond_init, __VA_ARGS__)
+#define pthread_cond_destroy(...) MP_PTWRAP(pthread_cond_destroy, __VA_ARGS__)
+#define pthread_cond_broadcast(...) MP_PTWRAP(pthread_cond_broadcast, __VA_ARGS__)
+#define pthread_cond_signal(...) MP_PTWRAP(pthread_cond_signal, __VA_ARGS__)
+#define pthread_cond_wait(...) MP_PTWRAP(pthread_cond_wait, __VA_ARGS__)
+#define pthread_cond_timedwait(...) MP_PTWRAP(pthread_cond_timedwait, __VA_ARGS__)
+#define pthread_detach(...) MP_PTWRAP(pthread_detach, __VA_ARGS__)
+#define pthread_join(...) MP_PTWRAP(pthread_join, __VA_ARGS__)
+#define pthread_mutex_destroy(...) MP_PTWRAP(pthread_mutex_destroy, __VA_ARGS__)
+#define pthread_mutex_lock(...) MP_PTWRAP(pthread_mutex_lock, __VA_ARGS__)
+#define pthread_mutex_unlock(...) MP_PTWRAP(pthread_mutex_unlock, __VA_ARGS__)
+
+#define pthread_mutex_init(...) \
+ mp_ptwrap_mutex_init(__FILE__, __LINE__, __VA_ARGS__)
+
+#define pthread_mutex_trylock(...) \
+ mp_ptwrap_mutex_trylock(__FILE__, __LINE__, __VA_ARGS__)
+
+#endif
+
+typedef struct {
+ pthread_cond_t cond;
+ clockid_t clk_id;
+} mp_cond;
+
+typedef pthread_mutex_t mp_mutex;
+typedef pthread_mutex_t mp_static_mutex;
+typedef pthread_once_t mp_once;
+typedef pthread_t mp_thread_id;
+typedef pthread_t mp_thread;
+
+#define MP_STATIC_COND_INITIALIZER (mp_cond){ .cond = PTHREAD_COND_INITIALIZER, .clk_id = CLOCK_REALTIME }
+#define MP_STATIC_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER
+#define MP_STATIC_ONCE_INITIALIZER PTHREAD_ONCE_INIT
+
+static inline int mp_mutex_init_type_internal(mp_mutex *mutex, enum mp_mutex_type mtype)
+{
+ int mutex_type;
+ switch (mtype) {
+ case MP_MUTEX_RECURSIVE:
+ mutex_type = PTHREAD_MUTEX_RECURSIVE;
+ break;
+ case MP_MUTEX_NORMAL:
+ default:
+#ifndef NDEBUG
+ mutex_type = PTHREAD_MUTEX_ERRORCHECK;
+#else
+ mutex_type = PTHREAD_MUTEX_DEFAULT;
+#endif
+ break;
+ }
+
+ int ret = 0;
+ pthread_mutexattr_t attr;
+ ret = pthread_mutexattr_init(&attr);
+ if (ret != 0)
+ return ret;
+
+ pthread_mutexattr_settype(&attr, mutex_type);
+ ret = pthread_mutex_init(mutex, &attr);
+ pthread_mutexattr_destroy(&attr);
+ assert(!ret);
+ return ret;
+}
+
+#define mp_mutex_destroy pthread_mutex_destroy
+#define mp_mutex_lock pthread_mutex_lock
+#define mp_mutex_trylock pthread_mutex_trylock
+#define mp_mutex_unlock pthread_mutex_unlock
+
+static inline int mp_cond_init(mp_cond *cond)
+{
+ assert(cond);
+
+ int ret = 0;
+ pthread_condattr_t attr;
+ ret = pthread_condattr_init(&attr);
+ if (ret)
+ return ret;
+
+ cond->clk_id = CLOCK_REALTIME;
+#if HAVE_PTHREAD_CONDATTR_SETCLOCK
+ if (!pthread_condattr_setclock(&attr, CLOCK_MONOTONIC))
+ cond->clk_id = CLOCK_MONOTONIC;
+#endif
+
+ ret = pthread_cond_init(&cond->cond, &attr);
+ pthread_condattr_destroy(&attr);
+ return ret;
+}
+
+static inline int mp_cond_destroy(mp_cond *cond)
+{
+ assert(cond);
+ return pthread_cond_destroy(&cond->cond);
+}
+
+static inline int mp_cond_broadcast(mp_cond *cond)
+{
+ assert(cond);
+ return pthread_cond_broadcast(&cond->cond);
+}
+
+static inline int mp_cond_signal(mp_cond *cond)
+{
+ assert(cond);
+ return pthread_cond_signal(&cond->cond);
+}
+
+static inline int mp_cond_wait(mp_cond *cond, mp_mutex *mutex)
+{
+ assert(cond);
+ return pthread_cond_wait(&cond->cond, mutex);
+}
+
+static inline int mp_cond_timedwait(mp_cond *cond, mp_mutex *mutex, int64_t timeout)
+{
+ assert(cond);
+
+ timeout = MPMAX(0, timeout);
+ // consider anything above 1000 days as infinity
+ if (timeout > MP_TIME_S_TO_NS(1000 * 24 * 60 * 60))
+ return pthread_cond_wait(&cond->cond, mutex);
+
+ struct timespec ts;
+ clock_gettime(cond->clk_id, &ts);
+ ts.tv_sec += timeout / MP_TIME_S_TO_NS(1);
+ ts.tv_nsec += timeout % MP_TIME_S_TO_NS(1);
+ if (ts.tv_nsec >= MP_TIME_S_TO_NS(1)) {
+ ts.tv_nsec -= MP_TIME_S_TO_NS(1);
+ ts.tv_sec++;
+ }
+
+ return pthread_cond_timedwait(&cond->cond, mutex, &ts);
+}
+
+static inline int mp_cond_timedwait_until(mp_cond *cond, mp_mutex *mutex, int64_t until)
+{
+ return mp_cond_timedwait(cond, mutex, until - mp_time_ns());
+}
+
+#define mp_exec_once pthread_once
+
+#define MP_THREAD_VOID void *
+#define MP_THREAD_RETURN() return NULL
+
+#define mp_thread_create(t, f, a) pthread_create(t, NULL, f, a)
+#define mp_thread_join(t) pthread_join(t, NULL)
+#define mp_thread_join_id(t) pthread_join(t, NULL)
+#define mp_thread_detach pthread_detach
+#define mp_thread_current_id pthread_self
+#define mp_thread_id_equal(a, b) ((a) == (b))
+#define mp_thread_get_id(thread) (thread)
+
+static inline void mp_thread_set_name(const char *name)
+{
+#if HAVE_GLIBC_THREAD_NAME
+ if (pthread_setname_np(pthread_self(), name) == ERANGE) {
+ char tname[16] = {0}; // glibc-checked kernel limit
+ strncpy(tname, name, sizeof(tname) - 1);
+ pthread_setname_np(pthread_self(), tname);
+ }
+#elif HAVE_BSD_THREAD_NAME
+ pthread_set_name_np(pthread_self(), name);
+#elif HAVE_OSX_THREAD_NAME
+ pthread_setname_np(name);
+#endif
+}
+
+static inline int64_t mp_thread_cpu_time_ns(mp_thread_id thread)
+{
+#if defined(_POSIX_TIMERS) && _POSIX_TIMERS > 0 && defined(_POSIX_THREAD_CPUTIME)
+ clockid_t id;
+ struct timespec ts;
+ if (pthread_getcpuclockid(thread, &id) == 0 && clock_gettime(id, &ts) == 0)
+ return MP_TIME_S_TO_NS(ts.tv_sec) + ts.tv_nsec;
+#endif
+ return 0;
+}
diff --git a/osdep/threads-win32.h b/osdep/threads-win32.h
new file mode 100644
index 0000000..dbce353
--- /dev/null
+++ b/osdep/threads-win32.h
@@ -0,0 +1,224 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <errno.h>
+#include <process.h>
+#include <windows.h>
+
+#include "common/common.h"
+#include "timer.h"
+
+typedef struct {
+ char use_cs;
+ union {
+ CRITICAL_SECTION cs;
+ SRWLOCK srw;
+ };
+} mp_mutex;
+
+typedef CONDITION_VARIABLE mp_cond;
+typedef INIT_ONCE mp_once;
+typedef mp_mutex mp_static_mutex;
+typedef HANDLE mp_thread;
+typedef DWORD mp_thread_id;
+
+#define MP_STATIC_COND_INITIALIZER CONDITION_VARIABLE_INIT
+#define MP_STATIC_MUTEX_INITIALIZER (mp_mutex){ .srw = SRWLOCK_INIT }
+#define MP_STATIC_ONCE_INITIALIZER INIT_ONCE_STATIC_INIT
+
+static inline int mp_mutex_init_type_internal(mp_mutex *mutex, enum mp_mutex_type mtype)
+{
+ mutex->use_cs = mtype == MP_MUTEX_RECURSIVE;
+ if (mutex->use_cs)
+ return !InitializeCriticalSectionEx(&mutex->cs, 0, 0);
+ InitializeSRWLock(&mutex->srw);
+ return 0;
+}
+
+static inline int mp_mutex_destroy(mp_mutex *mutex)
+{
+ if (mutex->use_cs)
+ DeleteCriticalSection(&mutex->cs);
+ return 0;
+}
+
+static inline int mp_mutex_lock(mp_mutex *mutex)
+{
+ if (mutex->use_cs) {
+ EnterCriticalSection(&mutex->cs);
+ } else {
+ AcquireSRWLockExclusive(&mutex->srw);
+ }
+ return 0;
+}
+
+static inline int mp_mutex_trylock(mp_mutex *mutex)
+{
+ if (mutex->use_cs)
+ return !TryEnterCriticalSection(&mutex->cs);
+ return !TryAcquireSRWLockExclusive(&mutex->srw);
+}
+
+static inline int mp_mutex_unlock(mp_mutex *mutex)
+{
+ if (mutex->use_cs) {
+ LeaveCriticalSection(&mutex->cs);
+ } else {
+ ReleaseSRWLockExclusive(&mutex->srw);
+ }
+ return 0;
+}
+
+static inline int mp_cond_init(mp_cond *cond)
+{
+ InitializeConditionVariable(cond);
+ return 0;
+}
+
+static inline int mp_cond_destroy(mp_cond *cond)
+{
+ // condition variables are not destroyed
+ (void) cond;
+ return 0;
+}
+
+static inline int mp_cond_broadcast(mp_cond *cond)
+{
+ WakeAllConditionVariable(cond);
+ return 0;
+}
+
+static inline int mp_cond_signal(mp_cond *cond)
+{
+ WakeConditionVariable(cond);
+ return 0;
+}
+
+static inline int mp_cond_timedwait(mp_cond *cond, mp_mutex *mutex, int64_t timeout)
+{
+ timeout = MPCLAMP(timeout, 0, MP_TIME_MS_TO_NS(INFINITE)) / MP_TIME_MS_TO_NS(1);
+
+ int ret = 0;
+ int hrt = mp_start_hires_timers(timeout);
+ BOOL bRet;
+
+ if (mutex->use_cs) {
+ bRet = SleepConditionVariableCS(cond, &mutex->cs, timeout);
+ } else {
+ bRet = SleepConditionVariableSRW(cond, &mutex->srw, timeout, 0);
+ }
+ if (bRet == FALSE)
+ ret = GetLastError() == ERROR_TIMEOUT ? ETIMEDOUT : EINVAL;
+
+ mp_end_hires_timers(hrt);
+ return ret;
+}
+
+static inline int mp_cond_wait(mp_cond *cond, mp_mutex *mutex)
+{
+ return mp_cond_timedwait(cond, mutex, MP_TIME_MS_TO_NS(INFINITE));
+}
+
+static inline int mp_cond_timedwait_until(mp_cond *cond, mp_mutex *mutex, int64_t until)
+{
+ return mp_cond_timedwait(cond, mutex, until - mp_time_ns());
+}
+
+static inline int mp_exec_once(mp_once *once, void (*init_routine)(void))
+{
+ BOOL pending;
+
+ if (!InitOnceBeginInitialize(once, 0, &pending, NULL))
+ abort();
+
+ if (pending) {
+ init_routine();
+ InitOnceComplete(once, 0, NULL);
+ }
+
+ return 0;
+}
+
+#define MP_THREAD_VOID unsigned __stdcall
+#define MP_THREAD_RETURN() return 0
+
+static inline int mp_thread_create(mp_thread *thread,
+ MP_THREAD_VOID (*fun)(void *),
+ void *__restrict arg)
+{
+ *thread = (HANDLE) _beginthreadex(NULL, 0, fun, arg, 0, NULL);
+ return *thread ? 0 : -1;
+}
+
+static inline int mp_thread_join(mp_thread thread)
+{
+ DWORD ret = WaitForSingleObject(thread, INFINITE);
+ if (ret != WAIT_OBJECT_0)
+ return ret == WAIT_ABANDONED ? EINVAL : EDEADLK;
+ CloseHandle(thread);
+ return 0;
+}
+
+static inline int mp_thread_join_id(mp_thread_id id)
+{
+ mp_thread thread = OpenThread(SYNCHRONIZE, FALSE, id);
+ if (!thread)
+ return ESRCH;
+ int ret = mp_thread_join(thread);
+ if (ret)
+ CloseHandle(thread);
+ return ret;
+}
+
+static inline int mp_thread_detach(mp_thread thread)
+{
+ return CloseHandle(thread) ? 0 : EINVAL;
+}
+
+#define mp_thread_current_id GetCurrentThreadId
+#define mp_thread_id_equal(a, b) ((a) == (b))
+#define mp_thread_get_id(thread) GetThreadId(thread)
+
+// declared in io.h, which we don't want to pull in everywhere
+wchar_t *mp_from_utf8(void *talloc_ctx, const char *s);
+static inline void mp_thread_set_name(const char *name)
+{
+ HRESULT (WINAPI *pSetThreadDescription)(HANDLE, PCWSTR);
+#if !HAVE_UWP
+ HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
+ if (!kernel32)
+ return;
+ pSetThreadDescription = (void *) GetProcAddress(kernel32, "SetThreadDescription");
+ if (!pSetThreadDescription)
+ return;
+#else
+ WINBASEAPI HRESULT WINAPI
+ SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription);
+ pSetThreadDescription = &SetThreadDescription;
+#endif
+ wchar_t *wname = mp_from_utf8(NULL, name);
+ pSetThreadDescription(GetCurrentThread(), wname);
+ talloc_free(wname);
+}
+
+static inline int64_t mp_thread_cpu_time_ns(mp_thread_id thread)
+{
+ (void) thread;
+ return 0;
+}
diff --git a/osdep/threads.h b/osdep/threads.h
new file mode 100644
index 0000000..b6d950e
--- /dev/null
+++ b/osdep/threads.h
@@ -0,0 +1,23 @@
+#ifndef MP_OSDEP_THREADS_H_
+#define MP_OSDEP_THREADS_H_
+
+#include "config.h"
+
+enum mp_mutex_type {
+ MP_MUTEX_NORMAL = 0,
+ MP_MUTEX_RECURSIVE,
+};
+
+#define mp_mutex_init(mutex) \
+ mp_mutex_init_type(mutex, MP_MUTEX_NORMAL)
+
+#define mp_mutex_init_type(mutex, mtype) \
+ mp_mutex_init_type_internal(mutex, mtype)
+
+#if HAVE_WIN32_THREADS
+#include "threads-win32.h"
+#else
+#include "threads-posix.h"
+#endif
+
+#endif
diff --git a/osdep/timer-darwin.c b/osdep/timer-darwin.c
new file mode 100644
index 0000000..bb8a9b4
--- /dev/null
+++ b/osdep/timer-darwin.c
@@ -0,0 +1,48 @@
+/*
+ * Precise timer routines using Mach timing
+ *
+ * Copyright (c) 2003-2004, Dan Villiom Podlaski Christiansen
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ */
+
+#include <unistd.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+#include <math.h>
+#include <sys/time.h>
+#include <mach/mach_time.h>
+
+#include "common/msg.h"
+#include "timer.h"
+
+static double timebase_ratio_ns;
+
+void mp_sleep_ns(int64_t ns)
+{
+ uint64_t deadline = ns / timebase_ratio_ns + mach_absolute_time();
+ mach_wait_until(deadline);
+}
+
+uint64_t mp_raw_time_ns(void)
+{
+ return mach_absolute_time() * timebase_ratio_ns;
+}
+
+void mp_raw_time_init(void)
+{
+ struct mach_timebase_info timebase;
+
+ mach_timebase_info(&timebase);
+ timebase_ratio_ns = (double)timebase.numer / (double)timebase.denom;
+}
diff --git a/osdep/timer-linux.c b/osdep/timer-linux.c
new file mode 100644
index 0000000..559a496
--- /dev/null
+++ b/osdep/timer-linux.c
@@ -0,0 +1,64 @@
+/*
+ * precise timer routines for Linux/UNIX
+ * copyright (C) LGB & A'rpi/ASTRAL
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "common/common.h"
+#include "timer.h"
+
+static clockid_t clk_id;
+
+void mp_sleep_ns(int64_t ns)
+{
+ if (ns < 0)
+ return;
+ struct timespec ts;
+ ts.tv_sec = ns / MP_TIME_S_TO_NS(1);
+ ts.tv_nsec = ns % MP_TIME_S_TO_NS(1);
+ nanosleep(&ts, NULL);
+}
+
+uint64_t mp_raw_time_ns(void)
+{
+ struct timespec tp = {0};
+ clock_gettime(clk_id, &tp);
+ return MP_TIME_S_TO_NS(tp.tv_sec) + tp.tv_nsec;
+}
+
+void mp_raw_time_init(void)
+{
+ static const clockid_t clock_ids[] = {
+#ifdef CLOCK_MONOTONIC_RAW
+ CLOCK_MONOTONIC_RAW,
+#endif
+ CLOCK_MONOTONIC,
+ };
+
+ struct timespec tp;
+ for (int i = 0; i < MP_ARRAY_SIZE(clock_ids); ++i) {
+ clk_id = clock_ids[i];
+ if (!clock_gettime(clk_id, &tp))
+ return;
+ }
+ fputs("No clock source available!\n", stderr);
+ abort();
+}
diff --git a/osdep/timer-win32.c b/osdep/timer-win32.c
new file mode 100644
index 0000000..7867b5a
--- /dev/null
+++ b/osdep/timer-win32.c
@@ -0,0 +1,141 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <sys/time.h>
+#include <mmsystem.h>
+#include <stdlib.h>
+#include <versionhelpers.h>
+
+#include "timer.h"
+
+#include "config.h"
+
+static LARGE_INTEGER perf_freq;
+
+// ms values
+static int hires_max = 50;
+static int hires_res = 1;
+
+int mp_start_hires_timers(int wait_ms)
+{
+#if !HAVE_UWP
+ // policy: request hires_res ms resolution if wait < hires_max ms
+ if (wait_ms > 0 && wait_ms <= hires_max &&
+ timeBeginPeriod(hires_res) == TIMERR_NOERROR)
+ {
+ return hires_res;
+ }
+#endif
+ return 0;
+}
+
+void mp_end_hires_timers(int res_ms)
+{
+#if !HAVE_UWP
+ if (res_ms > 0)
+ timeEndPeriod(res_ms);
+#endif
+}
+
+void mp_sleep_ns(int64_t ns)
+{
+ if (ns < 0)
+ return;
+
+ int hrt = mp_start_hires_timers(ns < 1e6 ? 1 : ns / 1e6);
+
+#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
+#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x2
+#endif
+
+ HANDLE timer = CreateWaitableTimerEx(NULL, NULL,
+ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
+ TIMER_ALL_ACCESS);
+
+ // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION is supported in Windows 10 1803+,
+ // retry without it.
+ if (!timer)
+ timer = CreateWaitableTimerEx(NULL, NULL, 0, TIMER_ALL_ACCESS);
+
+ if (!timer)
+ goto end;
+
+ // Time is expected in 100 nanosecond intervals.
+ // Negative values indicate relative time.
+ LARGE_INTEGER time = (LARGE_INTEGER){ .QuadPart = -(ns / 100) };
+ if (!SetWaitableTimer(timer, &time, 0, NULL, NULL, 0))
+ goto end;
+
+ if (WaitForSingleObject(timer, INFINITE) != WAIT_OBJECT_0)
+ goto end;
+
+end:
+ if (timer)
+ CloseHandle(timer);
+ mp_end_hires_timers(hrt);
+}
+
+uint64_t mp_raw_time_ns(void)
+{
+ LARGE_INTEGER perf_count;
+ QueryPerformanceCounter(&perf_count);
+
+ // Convert QPC units (1/perf_freq seconds) to nanoseconds. This will work
+ // without overflow because the QPC value is guaranteed not to roll-over
+ // within 100 years, so perf_freq must be less than 2.9*10^9.
+ return perf_count.QuadPart / perf_freq.QuadPart * UINT64_C(1000000000) +
+ perf_count.QuadPart % perf_freq.QuadPart * UINT64_C(1000000000) / perf_freq.QuadPart;
+}
+
+void mp_raw_time_init(void)
+{
+ QueryPerformanceFrequency(&perf_freq);
+
+#if !HAVE_UWP
+ // allow (undocumented) control of all the High Res Timers parameters,
+ // for easier experimentation and diagnostic of bug reports.
+ const char *v;
+
+ // 1..1000 ms max timetout for hires (used in "perwait" mode)
+ if ((v = getenv("MPV_HRT_MAX"))) {
+ int hmax = atoi(v);
+ if (hmax >= 1 && hmax <= 1000)
+ hires_max = hmax;
+ }
+
+ // 1..15 ms hires resolution (not used in "never" mode)
+ if ((v = getenv("MPV_HRT_RES"))) {
+ int res = atoi(v);
+ if (res >= 1 && res <= 15)
+ hires_res = res;
+ }
+
+ // "always"/"never"/"perwait" (or "auto" - same as unset)
+ if (!(v = getenv("MPV_HRT")) || !strcmp(v, "auto"))
+ v = IsWindows10OrGreater() ? "perwait" : "always";
+
+ if (!strcmp(v, "perwait")) {
+ // no-op, already per-wait
+ } else if (!strcmp(v, "never")) {
+ hires_max = 0;
+ } else { // "always" or unknown value
+ hires_max = 0;
+ timeBeginPeriod(hires_res);
+ }
+#endif
+}
diff --git a/osdep/timer.c b/osdep/timer.c
new file mode 100644
index 0000000..d0a8a92
--- /dev/null
+++ b/osdep/timer.c
@@ -0,0 +1,67 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/random.h"
+#include "threads.h"
+#include "timer.h"
+
+static uint64_t raw_time_offset;
+static mp_once timer_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+static void do_timer_init(void)
+{
+ mp_raw_time_init();
+ mp_rand_seed(mp_raw_time_ns());
+ raw_time_offset = mp_raw_time_ns();
+ assert(raw_time_offset > 0);
+}
+
+void mp_time_init(void)
+{
+ mp_exec_once(&timer_init_once, do_timer_init);
+}
+
+int64_t mp_time_ns(void)
+{
+ return mp_raw_time_ns() - raw_time_offset;
+}
+
+double mp_time_sec(void)
+{
+ return mp_time_ns() / 1e9;
+}
+
+int64_t mp_time_ns_add(int64_t time_ns, double timeout_sec)
+{
+ assert(time_ns > 0); // mp_time_ns() returns strictly positive values
+ double t = MPCLAMP(timeout_sec * 1e9, -0x1p63, 0x1p63);
+ int64_t ti = t == 0x1p63 ? INT64_MAX : (int64_t)t;
+ if (ti > INT64_MAX - time_ns)
+ return INT64_MAX;
+ if (ti <= -time_ns)
+ return 1;
+ return time_ns + ti;
+}
diff --git a/osdep/timer.h b/osdep/timer.h
new file mode 100644
index 0000000..3a925ca
--- /dev/null
+++ b/osdep/timer.h
@@ -0,0 +1,63 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_TIMER_H
+#define MPLAYER_TIMER_H
+
+#include <inttypes.h>
+
+// Initialize timer, must be called at least once at start.
+void mp_time_init(void);
+
+// Return time in nanoseconds. Never wraps. Never returns 0 or negative values.
+int64_t mp_time_ns(void);
+
+// Return time in seconds. Can have down to 1 nanosecond resolution, but will
+// be much worse when casted to float.
+double mp_time_sec(void);
+
+// Provided by OS specific functions (timer-linux.c)
+void mp_raw_time_init(void);
+// ensure this doesn't return 0
+uint64_t mp_raw_time_ns(void);
+
+// Sleep in nanoseconds.
+void mp_sleep_ns(int64_t ns);
+
+#ifdef _WIN32
+// returns: timer resolution in ms if needed and started successfully, else 0
+int mp_start_hires_timers(int wait_ms);
+
+// call unconditionally with the return value of mp_start_hires_timers
+void mp_end_hires_timers(int resolution_ms);
+#endif /* _WIN32 */
+
+// Converts time units to nanoseconds (int64_t)
+#define MP_TIME_S_TO_NS(s) ((s) * INT64_C(1000000000))
+#define MP_TIME_MS_TO_NS(ms) ((ms) * INT64_C(1000000))
+#define MP_TIME_US_TO_NS(us) ((us) * INT64_C(1000))
+
+// Converts nanoseconds to specified time unit (double)
+#define MP_TIME_NS_TO_S(ns) ((ns) / (double)1000000000)
+#define MP_TIME_NS_TO_MS(ns) ((ns) / (double)1000000)
+#define MP_TIME_NS_TO_US(ns) ((ns) / (double)1000)
+
+// Add a time in seconds to the given time in nanoseconds, and return it.
+// Takes care of possible overflows. Never returns a negative or 0 time.
+int64_t mp_time_ns_add(int64_t time_ns, double timeout_sec);
+
+#endif /* MPLAYER_TIMER_H */
diff --git a/osdep/w32_keyboard.c b/osdep/w32_keyboard.c
new file mode 100644
index 0000000..52221e6
--- /dev/null
+++ b/osdep/w32_keyboard.c
@@ -0,0 +1,123 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include "osdep/w32_keyboard.h"
+#include "input/keycodes.h"
+
+struct keymap {
+ int from;
+ int to;
+};
+
+static const struct keymap vk_map_ext[] = {
+ // cursor keys
+ {VK_LEFT, MP_KEY_LEFT}, {VK_UP, MP_KEY_UP}, {VK_RIGHT, MP_KEY_RIGHT},
+ {VK_DOWN, MP_KEY_DOWN},
+
+ // navigation block
+ {VK_INSERT, MP_KEY_INSERT}, {VK_DELETE, MP_KEY_DELETE},
+ {VK_HOME, MP_KEY_HOME}, {VK_END, MP_KEY_END}, {VK_PRIOR, MP_KEY_PAGE_UP},
+ {VK_NEXT, MP_KEY_PAGE_DOWN},
+
+ // numpad independent of numlock
+ {VK_RETURN, MP_KEY_KPENTER},
+
+ {0, 0}
+};
+
+static const struct keymap vk_map[] = {
+ // special keys
+ {VK_ESCAPE, MP_KEY_ESC}, {VK_BACK, MP_KEY_BS}, {VK_TAB, MP_KEY_TAB},
+ {VK_RETURN, MP_KEY_ENTER}, {VK_PAUSE, MP_KEY_PAUSE},
+ {VK_SLEEP, MP_KEY_SLEEP}, {VK_SNAPSHOT, MP_KEY_PRINT},
+ {VK_APPS, MP_KEY_MENU},
+
+ // F-keys
+ {VK_F1, MP_KEY_F+1}, {VK_F2, MP_KEY_F+2}, {VK_F3, MP_KEY_F+3},
+ {VK_F4, MP_KEY_F+4}, {VK_F5, MP_KEY_F+5}, {VK_F6, MP_KEY_F+6},
+ {VK_F7, MP_KEY_F+7}, {VK_F8, MP_KEY_F+8}, {VK_F9, MP_KEY_F+9},
+ {VK_F10, MP_KEY_F+10}, {VK_F11, MP_KEY_F+11}, {VK_F12, MP_KEY_F+12},
+ {VK_F13, MP_KEY_F+13}, {VK_F14, MP_KEY_F+14}, {VK_F15, MP_KEY_F+15},
+ {VK_F16, MP_KEY_F+16}, {VK_F17, MP_KEY_F+17}, {VK_F18, MP_KEY_F+18},
+ {VK_F19, MP_KEY_F+19}, {VK_F20, MP_KEY_F+20}, {VK_F21, MP_KEY_F+21},
+ {VK_F22, MP_KEY_F+22}, {VK_F23, MP_KEY_F+23}, {VK_F24, MP_KEY_F+24},
+
+ // numpad with numlock
+ {VK_NUMPAD0, MP_KEY_KP0}, {VK_NUMPAD1, MP_KEY_KP1},
+ {VK_NUMPAD2, MP_KEY_KP2}, {VK_NUMPAD3, MP_KEY_KP3},
+ {VK_NUMPAD4, MP_KEY_KP4}, {VK_NUMPAD5, MP_KEY_KP5},
+ {VK_NUMPAD6, MP_KEY_KP6}, {VK_NUMPAD7, MP_KEY_KP7},
+ {VK_NUMPAD8, MP_KEY_KP8}, {VK_NUMPAD9, MP_KEY_KP9},
+ {VK_DECIMAL, MP_KEY_KPDEC},
+
+ // numpad without numlock
+ {VK_INSERT, MP_KEY_KPINS}, {VK_END, MP_KEY_KPEND}, {VK_DOWN, MP_KEY_KPDOWN},
+ {VK_NEXT, MP_KEY_KPPGDOWN}, {VK_LEFT, MP_KEY_KPLEFT}, {VK_CLEAR, MP_KEY_KP5},
+ {VK_RIGHT, MP_KEY_KPRIGHT}, {VK_HOME, MP_KEY_KPHOME}, {VK_UP, MP_KEY_KPUP},
+ {VK_PRIOR, MP_KEY_KPPGUP}, {VK_DELETE, MP_KEY_KPDEL},
+
+ {0, 0}
+};
+
+static const struct keymap appcmd_map[] = {
+ {APPCOMMAND_MEDIA_NEXTTRACK, MP_KEY_NEXT},
+ {APPCOMMAND_MEDIA_PREVIOUSTRACK, MP_KEY_PREV},
+ {APPCOMMAND_MEDIA_STOP, MP_KEY_STOP},
+ {APPCOMMAND_MEDIA_PLAY_PAUSE, MP_KEY_PLAYPAUSE},
+ {APPCOMMAND_MEDIA_PLAY, MP_KEY_PLAY},
+ {APPCOMMAND_MEDIA_PAUSE, MP_KEY_PAUSE},
+ {APPCOMMAND_MEDIA_RECORD, MP_KEY_RECORD},
+ {APPCOMMAND_MEDIA_FAST_FORWARD, MP_KEY_FORWARD},
+ {APPCOMMAND_MEDIA_REWIND, MP_KEY_REWIND},
+ {APPCOMMAND_MEDIA_CHANNEL_UP, MP_KEY_CHANNEL_UP},
+ {APPCOMMAND_MEDIA_CHANNEL_DOWN, MP_KEY_CHANNEL_DOWN},
+ {APPCOMMAND_VOLUME_MUTE, MP_KEY_MUTE},
+ {APPCOMMAND_VOLUME_DOWN, MP_KEY_VOLUME_DOWN},
+ {APPCOMMAND_VOLUME_UP, MP_KEY_VOLUME_UP},
+ {APPCOMMAND_BROWSER_HOME, MP_KEY_HOMEPAGE},
+ {APPCOMMAND_LAUNCH_MAIL, MP_KEY_MAIL},
+ {APPCOMMAND_BROWSER_FAVORITES, MP_KEY_FAVORITES},
+ {APPCOMMAND_BROWSER_SEARCH, MP_KEY_SEARCH},
+ {0, 0}
+};
+
+static int lookup_keymap(const struct keymap *map, int key)
+{
+ while (map->from && map->from != key) map++;
+ return map->to;
+}
+
+int mp_w32_vkey_to_mpkey(UINT vkey, bool extended)
+{
+ // The extended flag is set for the navigation cluster and the arrow keys,
+ // so it can be used to differentiate between them and the numpad. The
+ // numpad enter key also has this flag set.
+ int mpkey = lookup_keymap(extended ? vk_map_ext : vk_map, vkey);
+
+ // If we got the extended flag for a key we don't recognize, search the
+ // normal keymap before giving up
+ if (extended && !mpkey)
+ mpkey = lookup_keymap(vk_map, vkey);
+
+ return mpkey;
+}
+
+int mp_w32_appcmd_to_mpkey(UINT appcmd)
+{
+ return lookup_keymap(appcmd_map, appcmd);
+}
diff --git a/osdep/w32_keyboard.h b/osdep/w32_keyboard.h
new file mode 100644
index 0000000..b06cdee
--- /dev/null
+++ b/osdep/w32_keyboard.h
@@ -0,0 +1,29 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_W32_KEYBOARD
+#define MP_W32_KEYBOARD
+
+#include <stdbool.h>
+
+/* Convert a Windows virtual key code to an mpv key */
+int mp_w32_vkey_to_mpkey(UINT vkey, bool extended);
+
+/* Convert a WM_APPCOMMAND value to an mpv key */
+int mp_w32_appcmd_to_mpkey(UINT appcmd);
+
+#endif
diff --git a/osdep/win32-console-wrapper.c b/osdep/win32-console-wrapper.c
new file mode 100644
index 0000000..4e74dac
--- /dev/null
+++ b/osdep/win32-console-wrapper.c
@@ -0,0 +1,89 @@
+/*
+ * conredir, a hack to get working console IO with Windows GUI applications
+ *
+ * Copyright (c) 2013, Martin Herkt
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <windows.h>
+
+int wmain(int argc, wchar_t **argv, wchar_t **envp);
+
+static void cr_perror(const wchar_t *prefix)
+{
+ wchar_t *error;
+
+ FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, GetLastError(),
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (LPWSTR)&error, 0, NULL);
+
+ fwprintf(stderr, L"%s: %s", prefix, error);
+ LocalFree(error);
+}
+
+static int cr_runproc(wchar_t *name, wchar_t *cmdline)
+{
+ STARTUPINFOW si;
+ STARTUPINFOW our_si;
+ PROCESS_INFORMATION pi;
+ DWORD retval = 1;
+
+ ZeroMemory(&si, sizeof(si));
+ si.cb = sizeof(si);
+ si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+ si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+ si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+ si.dwFlags |= STARTF_USESTDHANDLES;
+
+ // Copy the list of inherited CRT file descriptors to the new process
+ our_si.cb = sizeof(our_si);
+ GetStartupInfoW(&our_si);
+ si.lpReserved2 = our_si.lpReserved2;
+ si.cbReserved2 = our_si.cbReserved2;
+
+ ZeroMemory(&pi, sizeof(pi));
+
+ if (!CreateProcessW(name, cmdline, NULL, NULL, TRUE, 0,
+ NULL, NULL, &si, &pi)) {
+
+ cr_perror(L"CreateProcess");
+ } else {
+ WaitForSingleObject(pi.hProcess, INFINITE);
+ GetExitCodeProcess(pi.hProcess, &retval);
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ }
+
+ return (int)retval;
+}
+
+int wmain(int argc, wchar_t **argv, wchar_t **envp)
+{
+ wchar_t *cmd;
+ wchar_t exe[MAX_PATH];
+
+ cmd = GetCommandLineW();
+ GetModuleFileNameW(NULL, exe, MAX_PATH);
+ wcscpy(wcsrchr(exe, '.') + 1, L"exe");
+
+ // Set an environment variable so the child process can tell whether it
+ // was started from this wrapper and attach to the console accordingly
+ SetEnvironmentVariableW(L"_started_from_console", L"yes");
+
+ return cr_runproc(exe, cmd);
+}
diff --git a/osdep/windows_utils.c b/osdep/windows_utils.c
new file mode 100644
index 0000000..8cedf93
--- /dev/null
+++ b/osdep/windows_utils.c
@@ -0,0 +1,229 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <stdatomic.h>
+#include <stdio.h>
+
+#include <windows.h>
+#include <errors.h>
+#include <audioclient.h>
+#include <d3d9.h>
+#include <dxgi1_2.h>
+
+#include "common/common.h"
+#include "windows_utils.h"
+
+char *mp_GUID_to_str_buf(char *buf, size_t buf_size, const GUID *guid)
+{
+ snprintf(buf, buf_size,
+ "{%8.8x-%4.4x-%4.4x-%2.2x%2.2x-%2.2x%2.2x%2.2x%2.2x%2.2x%2.2x}",
+ (unsigned) guid->Data1, guid->Data2, guid->Data3,
+ guid->Data4[0], guid->Data4[1],
+ guid->Data4[2], guid->Data4[3],
+ guid->Data4[4], guid->Data4[5],
+ guid->Data4[6], guid->Data4[7]);
+ return buf;
+}
+
+static char *hresult_to_str(const HRESULT hr)
+{
+#define E(x) case x : return # x ;
+ switch (hr) {
+ E(S_OK)
+ E(S_FALSE)
+ E(E_FAIL)
+ E(E_OUTOFMEMORY)
+ E(E_POINTER)
+ E(E_HANDLE)
+ E(E_NOTIMPL)
+ E(E_INVALIDARG)
+ E(E_PROP_ID_UNSUPPORTED)
+ E(E_NOINTERFACE)
+ E(REGDB_E_IIDNOTREG)
+ E(CO_E_NOTINITIALIZED)
+ E(AUDCLNT_E_NOT_INITIALIZED)
+ E(AUDCLNT_E_ALREADY_INITIALIZED)
+ E(AUDCLNT_E_WRONG_ENDPOINT_TYPE)
+ E(AUDCLNT_E_DEVICE_INVALIDATED)
+ E(AUDCLNT_E_NOT_STOPPED)
+ E(AUDCLNT_E_BUFFER_TOO_LARGE)
+ E(AUDCLNT_E_OUT_OF_ORDER)
+ E(AUDCLNT_E_UNSUPPORTED_FORMAT)
+ E(AUDCLNT_E_INVALID_SIZE)
+ E(AUDCLNT_E_DEVICE_IN_USE)
+ E(AUDCLNT_E_BUFFER_OPERATION_PENDING)
+ E(AUDCLNT_E_THREAD_NOT_REGISTERED)
+ E(AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)
+ E(AUDCLNT_E_ENDPOINT_CREATE_FAILED)
+ E(AUDCLNT_E_SERVICE_NOT_RUNNING)
+ E(AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED)
+ E(AUDCLNT_E_EXCLUSIVE_MODE_ONLY)
+ E(AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL)
+ E(AUDCLNT_E_EVENTHANDLE_NOT_SET)
+ E(AUDCLNT_E_INCORRECT_BUFFER_SIZE)
+ E(AUDCLNT_E_BUFFER_SIZE_ERROR)
+ E(AUDCLNT_E_CPUUSAGE_EXCEEDED)
+ E(AUDCLNT_E_BUFFER_ERROR)
+ E(AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED)
+ E(AUDCLNT_E_INVALID_DEVICE_PERIOD)
+ E(AUDCLNT_E_INVALID_STREAM_FLAG)
+ E(AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE)
+ E(AUDCLNT_E_RESOURCES_INVALIDATED)
+ E(AUDCLNT_S_BUFFER_EMPTY)
+ E(AUDCLNT_S_THREAD_ALREADY_REGISTERED)
+ E(AUDCLNT_S_POSITION_STALLED)
+ E(D3DERR_WRONGTEXTUREFORMAT)
+ E(D3DERR_UNSUPPORTEDCOLOROPERATION)
+ E(D3DERR_UNSUPPORTEDCOLORARG)
+ E(D3DERR_UNSUPPORTEDALPHAOPERATION)
+ E(D3DERR_UNSUPPORTEDALPHAARG)
+ E(D3DERR_TOOMANYOPERATIONS)
+ E(D3DERR_CONFLICTINGTEXTUREFILTER)
+ E(D3DERR_UNSUPPORTEDFACTORVALUE)
+ E(D3DERR_CONFLICTINGRENDERSTATE)
+ E(D3DERR_UNSUPPORTEDTEXTUREFILTER)
+ E(D3DERR_CONFLICTINGTEXTUREPALETTE)
+ E(D3DERR_DRIVERINTERNALERROR)
+ E(D3DERR_NOTFOUND)
+ E(D3DERR_MOREDATA)
+ E(D3DERR_DEVICELOST)
+ E(D3DERR_DEVICENOTRESET)
+ E(D3DERR_NOTAVAILABLE)
+ E(D3DERR_OUTOFVIDEOMEMORY)
+ E(D3DERR_INVALIDDEVICE)
+ E(D3DERR_INVALIDCALL)
+ E(D3DERR_DRIVERINVALIDCALL)
+ E(D3DERR_WASSTILLDRAWING)
+ E(D3DOK_NOAUTOGEN)
+ E(D3DERR_DEVICEREMOVED)
+ E(D3DERR_DEVICEHUNG)
+ E(S_NOT_RESIDENT)
+ E(S_RESIDENT_IN_SHARED_MEMORY)
+ E(S_PRESENT_MODE_CHANGED)
+ E(S_PRESENT_OCCLUDED)
+ E(D3DERR_UNSUPPORTEDOVERLAY)
+ E(D3DERR_UNSUPPORTEDOVERLAYFORMAT)
+ E(D3DERR_CANNOTPROTECTCONTENT)
+ E(D3DERR_UNSUPPORTEDCRYPTO)
+ E(D3DERR_PRESENT_STATISTICS_DISJOINT)
+ E(DXGI_ERROR_DEVICE_HUNG)
+ E(DXGI_ERROR_DEVICE_REMOVED)
+ E(DXGI_ERROR_DEVICE_RESET)
+ E(DXGI_ERROR_DRIVER_INTERNAL_ERROR)
+ E(DXGI_ERROR_INVALID_CALL)
+ E(DXGI_ERROR_WAS_STILL_DRAWING)
+ E(DXGI_STATUS_OCCLUDED)
+ default:
+ return "<Unknown>";
+ }
+#undef E
+}
+
+static char *fmtmsg_buf(char *buf, size_t buf_size, DWORD errorID)
+{
+ DWORD n = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, errorID, 0, buf, buf_size, NULL);
+ if (!n && GetLastError() == ERROR_MORE_DATA) {
+ snprintf(buf, buf_size,
+ "<Insufficient buffer size (%zd) for error message>",
+ buf_size);
+ } else {
+ if (n > 0 && buf[n-1] == '\n')
+ buf[n-1] = '\0';
+ if (n > 1 && buf[n-2] == '\r')
+ buf[n-2] = '\0';
+ }
+ return buf;
+}
+#define fmtmsg(hr) fmtmsg_buf((char[243]){0}, 243, (hr))
+
+char *mp_HRESULT_to_str_buf(char *buf, size_t buf_size, HRESULT hr)
+{
+ char* msg = fmtmsg(hr);
+ msg = msg[0] ? msg : hresult_to_str(hr);
+ snprintf(buf, buf_size, "%s (0x%"PRIx32")", msg, (uint32_t)hr);
+ return buf;
+}
+
+bool mp_w32_create_anon_pipe(HANDLE *server, HANDLE *client,
+ struct w32_create_anon_pipe_opts *opts)
+{
+ static atomic_ulong counter = 0;
+
+ // Generate pipe name
+ unsigned long id = atomic_fetch_add(&counter, 1);
+ unsigned pid = GetCurrentProcessId();
+ wchar_t buf[36];
+ swprintf(buf, MP_ARRAY_SIZE(buf), L"\\\\.\\pipe\\mpv-anon-%08x-%08lx",
+ pid, id);
+
+ DWORD client_access = 0;
+ DWORD out_buffer = opts->out_buf_size;
+ DWORD in_buffer = opts->in_buf_size;
+
+ if (opts->server_flags & PIPE_ACCESS_INBOUND) {
+ client_access |= FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES;
+ if (!in_buffer)
+ in_buffer = 4096;
+ }
+ if (opts->server_flags & PIPE_ACCESS_OUTBOUND) {
+ client_access |= FILE_GENERIC_READ | FILE_WRITE_ATTRIBUTES;
+ if (!out_buffer)
+ out_buffer = 4096;
+ }
+
+ SECURITY_ATTRIBUTES inherit_sa = {
+ .nLength = sizeof inherit_sa,
+ .bInheritHandle = TRUE,
+ };
+
+ // The function for creating anonymous pipes (CreatePipe) can't create
+ // overlapped pipes, so instead, use a named pipe with a unique name
+ *server = CreateNamedPipeW(buf,
+ opts->server_flags | FILE_FLAG_FIRST_PIPE_INSTANCE,
+ opts->server_mode | PIPE_REJECT_REMOTE_CLIENTS,
+ 1, out_buffer, in_buffer, 0,
+ opts->server_inheritable ? &inherit_sa : NULL);
+ if (*server == INVALID_HANDLE_VALUE)
+ goto error;
+
+ // Open the write end of the pipe as a synchronous handle
+ *client = CreateFileW(buf, client_access, 0,
+ opts->client_inheritable ? &inherit_sa : NULL,
+ OPEN_EXISTING,
+ opts->client_flags | SECURITY_SQOS_PRESENT |
+ SECURITY_ANONYMOUS, NULL);
+ if (*client == INVALID_HANDLE_VALUE) {
+ CloseHandle(*server);
+ goto error;
+ }
+
+ if (opts->client_mode) {
+ if (!SetNamedPipeHandleState(*client, &opts->client_mode, NULL, NULL)) {
+ CloseHandle(*server);
+ CloseHandle(*client);
+ goto error;
+ }
+ }
+
+ return true;
+error:
+ *server = *client = INVALID_HANDLE_VALUE;
+ return false;
+}
diff --git a/osdep/windows_utils.h b/osdep/windows_utils.h
new file mode 100644
index 0000000..a8a5e94
--- /dev/null
+++ b/osdep/windows_utils.h
@@ -0,0 +1,49 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_WINDOWS_UTILS_H_
+#define MP_WINDOWS_UTILS_H_
+
+#include <windows.h>
+#include <stdbool.h>
+
+// Conditionally release a COM interface and set the pointer to NULL
+#define SAFE_RELEASE(u) \
+ do { if ((u) != NULL) (u)->lpVtbl->Release(u); (u) = NULL; } while(0)
+
+char *mp_GUID_to_str_buf(char *buf, size_t buf_size, const GUID *guid);
+#define mp_GUID_to_str(guid) mp_GUID_to_str_buf((char[40]){0}, 40, (guid))
+char *mp_HRESULT_to_str_buf(char *buf, size_t buf_size, HRESULT hr);
+#define mp_HRESULT_to_str(hr) mp_HRESULT_to_str_buf((char[256]){0}, 256, (hr))
+#define mp_LastError_to_str() mp_HRESULT_to_str(HRESULT_FROM_WIN32(GetLastError()))
+
+struct w32_create_anon_pipe_opts {
+ DWORD server_flags;
+ DWORD server_mode;
+ bool server_inheritable;
+ DWORD out_buf_size;
+ DWORD in_buf_size;
+
+ DWORD client_flags;
+ DWORD client_mode;
+ bool client_inheritable;
+};
+
+bool mp_w32_create_anon_pipe(HANDLE *server, HANDLE *client,
+ struct w32_create_anon_pipe_opts *opts);
+
+#endif
diff --git a/player/audio.c b/player/audio.c
new file mode 100644
index 0000000..ca17d33
--- /dev/null
+++ b/player/audio.c
@@ -0,0 +1,985 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "common/encode.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "osdep/timer.h"
+
+#include "audio/format.h"
+#include "audio/out/ao.h"
+#include "demux/demux.h"
+#include "filters/f_async_queue.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+
+#include "core.h"
+#include "command.h"
+
+enum {
+ AD_OK = 0,
+ AD_EOF = -2,
+ AD_WAIT = -4,
+};
+
+static void ao_process(struct mp_filter *f);
+
+static void update_speed_filters(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c)
+ return;
+
+ double speed = mpctx->opts->playback_speed;
+ double resample = mpctx->speed_factor_a;
+ double drop = 1.0;
+
+ if (!mpctx->opts->pitch_correction) {
+ resample *= speed;
+ speed = 1.0;
+ }
+
+ if (mpctx->display_sync_active) {
+ switch (mpctx->video_out->opts->video_sync) {
+ case VS_DISP_ADROP:
+ drop *= speed * resample;
+ resample = speed = 1.0;
+ break;
+ case VS_DISP_TEMPO:
+ speed = mpctx->audio_speed;
+ resample = 1.0;
+ break;
+ }
+ }
+
+ mp_output_chain_set_audio_speed(ao_c->filter, speed, resample, drop);
+}
+
+static int recreate_audio_filters(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ assert(ao_c);
+
+ if (!mp_output_chain_update_filters(ao_c->filter, mpctx->opts->af_settings))
+ goto fail;
+
+ update_speed_filters(mpctx);
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+
+ return 0;
+
+fail:
+ MP_ERR(mpctx, "Audio filter initialized failed!\n");
+ return -1;
+}
+
+int reinit_audio_filters(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c)
+ return 0;
+
+ double delay = mp_output_get_measured_total_delay(ao_c->filter);
+
+ if (recreate_audio_filters(mpctx) < 0)
+ return -1;
+
+ double ndelay = mp_output_get_measured_total_delay(ao_c->filter);
+
+ // Only force refresh if the amount of dropped buffered data is going to
+ // cause "issues" for the A/V sync logic.
+ if (mpctx->audio_status == STATUS_PLAYING && delay - ndelay >= 0.2)
+ issue_refresh_seek(mpctx, MPSEEK_EXACT);
+ return 1;
+}
+
+static double db_gain(double db)
+{
+ return pow(10.0, db/20.0);
+}
+
+static float compute_replaygain(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ float rgain = 1.0;
+
+ struct replaygain_data *rg = NULL;
+ struct track *track = mpctx->current_track[0][STREAM_AUDIO];
+ if (track)
+ rg = track->stream->codec->replaygain_data;
+ if (opts->rgain_mode && rg) {
+ MP_VERBOSE(mpctx, "Replaygain: Track=%f/%f Album=%f/%f\n",
+ rg->track_gain, rg->track_peak,
+ rg->album_gain, rg->album_peak);
+
+ float gain, peak;
+ if (opts->rgain_mode == 1) {
+ gain = rg->track_gain;
+ peak = rg->track_peak;
+ } else {
+ gain = rg->album_gain;
+ peak = rg->album_peak;
+ }
+
+ gain += opts->rgain_preamp;
+ rgain = db_gain(gain);
+
+ MP_VERBOSE(mpctx, "Applying replay-gain: %f\n", rgain);
+
+ if (!opts->rgain_clip) { // clipping prevention
+ rgain = MPMIN(rgain, 1.0 / peak);
+ MP_VERBOSE(mpctx, "...with clipping prevention: %f\n", rgain);
+ }
+ } else if (opts->rgain_fallback) {
+ rgain = db_gain(opts->rgain_fallback);
+ MP_VERBOSE(mpctx, "Applying fallback gain: %f\n", rgain);
+ }
+
+ return rgain;
+}
+
+// Called when opts->softvol_volume or opts->softvol_mute were changed.
+void audio_update_volume(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c || !ao_c->ao)
+ return;
+
+ float gain = MPMAX(opts->softvol_volume / 100.0, 0);
+ gain = pow(gain, 3);
+ gain *= compute_replaygain(mpctx);
+ if (opts->softvol_mute == 1)
+ gain = 0.0;
+
+ ao_set_gain(ao_c->ao, gain);
+}
+
+// Call this if opts->playback_speed or mpctx->speed_factor_* change.
+void update_playback_speed(struct MPContext *mpctx)
+{
+ mpctx->audio_speed = mpctx->opts->playback_speed * mpctx->speed_factor_a;
+ mpctx->video_speed = mpctx->opts->playback_speed * mpctx->speed_factor_v;
+
+ update_speed_filters(mpctx);
+}
+
+static bool has_video_track(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain && mpctx->vo_chain->is_coverart)
+ return false;
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type == STREAM_VIDEO && !track->attached_picture && !track->image)
+ return true;
+ }
+
+ return false;
+}
+
+static void ao_chain_reset_state(struct ao_chain *ao_c)
+{
+ ao_c->last_out_pts = MP_NOPTS_VALUE;
+ ao_c->out_eof = false;
+ ao_c->start_pts_known = false;
+ ao_c->start_pts = MP_NOPTS_VALUE;
+ ao_c->untimed_throttle = false;
+ ao_c->underrun = false;
+}
+
+void reset_audio_state(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain) {
+ ao_chain_reset_state(mpctx->ao_chain);
+ struct track *t = mpctx->ao_chain->track;
+ if (t && t->dec)
+ mp_decoder_wrapper_set_play_dir(t->dec, mpctx->play_dir);
+ }
+ mpctx->audio_status = mpctx->ao_chain ? STATUS_SYNCING : STATUS_EOF;
+ mpctx->delay = 0;
+ mpctx->logged_async_diff = -1;
+}
+
+void uninit_audio_out(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (ao_c) {
+ ao_c->ao_queue = NULL;
+ TA_FREEP(&ao_c->queue_filter);
+ ao_c->ao = NULL;
+ }
+ if (mpctx->ao) {
+ // Note: with gapless_audio, stop_play is not correctly set
+ if ((mpctx->opts->gapless_audio || mpctx->stop_play == AT_END_OF_FILE) &&
+ ao_is_playing(mpctx->ao) && !get_internal_paused(mpctx))
+ {
+ MP_VERBOSE(mpctx, "draining left over audio\n");
+ ao_drain(mpctx->ao);
+ }
+ ao_uninit(mpctx->ao);
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+ }
+ mpctx->ao = NULL;
+ TA_FREEP(&mpctx->ao_filter_fmt);
+}
+
+static void ao_chain_uninit(struct ao_chain *ao_c)
+{
+ struct track *track = ao_c->track;
+ if (track) {
+ assert(track->ao_c == ao_c);
+ track->ao_c = NULL;
+ if (ao_c->dec_src)
+ assert(track->dec->f->pins[0] == ao_c->dec_src);
+ talloc_free(track->dec->f);
+ track->dec = NULL;
+ }
+
+ if (ao_c->filter_src)
+ mp_pin_disconnect(ao_c->filter_src);
+
+ talloc_free(ao_c->filter->f);
+ talloc_free(ao_c->ao_filter);
+ talloc_free(ao_c);
+}
+
+void uninit_audio_chain(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain) {
+ ao_chain_uninit(mpctx->ao_chain);
+ mpctx->ao_chain = NULL;
+
+ mpctx->audio_status = STATUS_EOF;
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+ }
+}
+
+static char *audio_config_to_str_buf(char *buf, size_t buf_sz, int rate,
+ int format, struct mp_chmap channels)
+{
+ char ch[128];
+ mp_chmap_to_str_buf(ch, sizeof(ch), &channels);
+ char *hr_ch = mp_chmap_to_str_hr(&channels);
+ if (strcmp(hr_ch, ch) != 0)
+ mp_snprintf_cat(ch, sizeof(ch), " (%s)", hr_ch);
+ snprintf(buf, buf_sz, "%dHz %s %dch %s", rate,
+ ch, channels.num, af_fmt_to_str(format));
+ return buf;
+}
+
+// Decide whether on a format change, we should reinit the AO.
+static bool keep_weak_gapless_format(struct mp_aframe *old, struct mp_aframe* new)
+{
+ bool res = false;
+ struct mp_aframe *new_mod = mp_aframe_new_ref(new);
+ MP_HANDLE_OOM(new_mod);
+
+ // If the sample formats are compatible (== libswresample generally can
+ // convert them), keep the AO. On other changes, recreate it.
+
+ int old_fmt = mp_aframe_get_format(old);
+ int new_fmt = mp_aframe_get_format(new);
+
+ if (af_format_conversion_score(old_fmt, new_fmt) == INT_MIN)
+ goto done; // completely incompatible formats
+
+ if (!mp_aframe_set_format(new_mod, old_fmt))
+ goto done;
+
+ res = mp_aframe_config_equals(old, new_mod);
+
+done:
+ talloc_free(new_mod);
+ return res;
+}
+
+static void ao_chain_set_ao(struct ao_chain *ao_c, struct ao *ao)
+{
+ if (ao_c->ao != ao) {
+ assert(!ao_c->ao);
+ ao_c->ao = ao;
+ ao_c->ao_queue = ao_get_queue(ao_c->ao);
+ ao_c->queue_filter = mp_async_queue_create_filter(ao_c->ao_filter,
+ MP_PIN_IN, ao_c->ao_queue);
+ mp_async_queue_set_notifier(ao_c->queue_filter, ao_c->ao_filter);
+ // Make sure filtering never stops with frames stuck in access filter.
+ mp_filter_set_high_priority(ao_c->queue_filter, true);
+ audio_update_volume(ao_c->mpctx);
+ }
+
+ if (ao_c->filter->ao_needs_update)
+ mp_output_chain_set_ao(ao_c->filter, ao_c->ao);
+
+ mp_filter_wakeup(ao_c->ao_filter);
+}
+
+static int reinit_audio_filters_and_output(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ assert(ao_c);
+ struct track *track = ao_c->track;
+
+ assert(ao_c->filter->ao_needs_update);
+
+ // The "ideal" filter output format
+ struct mp_aframe *out_fmt = mp_aframe_new_ref(ao_c->filter->output_aformat);
+ MP_HANDLE_OOM(out_fmt);
+
+ if (!mp_aframe_config_is_valid(out_fmt)) {
+ talloc_free(out_fmt);
+ goto init_error;
+ }
+
+ if (af_fmt_is_pcm(mp_aframe_get_format(out_fmt))) {
+ if (opts->force_srate)
+ mp_aframe_set_rate(out_fmt, opts->force_srate);
+ if (opts->audio_output_format)
+ mp_aframe_set_format(out_fmt, opts->audio_output_format);
+ if (opts->audio_output_channels.num_chmaps == 1)
+ mp_aframe_set_chmap(out_fmt, &opts->audio_output_channels.chmaps[0]);
+ }
+
+ // Weak gapless audio: if the filter output format is the same as the
+ // previous one, keep the AO and don't reinit anything.
+ // Strong gapless: always keep the AO
+ if ((mpctx->ao_filter_fmt && mpctx->ao && opts->gapless_audio < 0 &&
+ keep_weak_gapless_format(mpctx->ao_filter_fmt, out_fmt)) ||
+ (mpctx->ao && opts->gapless_audio > 0))
+ {
+ ao_chain_set_ao(ao_c, mpctx->ao);
+ talloc_free(out_fmt);
+ return 0;
+ }
+
+ // Wait until all played.
+ if (mpctx->ao && ao_is_playing(mpctx->ao)) {
+ talloc_free(out_fmt);
+ return 0;
+ }
+ // Format change during syncing. Force playback start early, then wait.
+ if (ao_c->ao_queue && mp_async_queue_get_frames(ao_c->ao_queue) &&
+ mpctx->audio_status == STATUS_SYNCING)
+ {
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ talloc_free(out_fmt);
+ return 0;
+ }
+ if (mpctx->audio_status == STATUS_READY) {
+ talloc_free(out_fmt);
+ return 0;
+ }
+
+ uninit_audio_out(mpctx);
+
+ int out_rate = mp_aframe_get_rate(out_fmt);
+ int out_format = mp_aframe_get_format(out_fmt);
+ struct mp_chmap out_channels = {0};
+ mp_aframe_get_chmap(out_fmt, &out_channels);
+
+ int ao_flags = 0;
+ bool spdif_fallback = af_fmt_is_spdif(out_format) &&
+ ao_c->spdif_passthrough;
+
+ if (opts->ao_null_fallback && !spdif_fallback)
+ ao_flags |= AO_INIT_NULL_FALLBACK;
+
+ if (opts->audio_stream_silence)
+ ao_flags |= AO_INIT_STREAM_SILENCE;
+
+ if (opts->audio_exclusive)
+ ao_flags |= AO_INIT_EXCLUSIVE;
+
+ if (af_fmt_is_pcm(out_format)) {
+ if (!opts->audio_output_channels.set ||
+ opts->audio_output_channels.auto_safe)
+ ao_flags |= AO_INIT_SAFE_MULTICHANNEL_ONLY;
+
+ mp_chmap_sel_list(&out_channels,
+ opts->audio_output_channels.chmaps,
+ opts->audio_output_channels.num_chmaps);
+ }
+
+ if (!has_video_track(mpctx))
+ ao_flags |= AO_INIT_MEDIA_ROLE_MUSIC;
+
+ mpctx->ao_filter_fmt = out_fmt;
+
+ mpctx->ao = ao_init_best(mpctx->global, ao_flags, mp_wakeup_core_cb,
+ mpctx, mpctx->encode_lavc_ctx, out_rate,
+ out_format, out_channels);
+
+ int ao_rate = 0;
+ int ao_format = 0;
+ struct mp_chmap ao_channels = {0};
+ if (mpctx->ao)
+ ao_get_format(mpctx->ao, &ao_rate, &ao_format, &ao_channels);
+
+ // Verify passthrough format was not changed.
+ if (mpctx->ao && af_fmt_is_spdif(out_format)) {
+ if (out_rate != ao_rate || out_format != ao_format ||
+ !mp_chmap_equals(&out_channels, &ao_channels))
+ {
+ MP_ERR(mpctx, "Passthrough format unsupported.\n");
+ ao_uninit(mpctx->ao);
+ mpctx->ao = NULL;
+ }
+ }
+
+ if (!mpctx->ao) {
+ // If spdif was used, try to fallback to PCM.
+ if (spdif_fallback && ao_c->track && ao_c->track->dec) {
+ MP_VERBOSE(mpctx, "Falling back to PCM output.\n");
+ ao_c->spdif_passthrough = false;
+ ao_c->spdif_failed = true;
+ mp_decoder_wrapper_set_spdif_flag(ao_c->track->dec, false);
+ if (!mp_decoder_wrapper_reinit(ao_c->track->dec))
+ goto init_error;
+ reset_audio_state(mpctx);
+ mp_output_chain_reset_harder(ao_c->filter);
+ mp_wakeup_core(mpctx); // reinit with new format next time
+ return 0;
+ }
+
+ MP_ERR(mpctx, "Could not open/initialize audio device -> no sound.\n");
+ mpctx->error_playing = MPV_ERROR_AO_INIT_FAILED;
+ goto init_error;
+ }
+
+ char tmp[192];
+ MP_INFO(mpctx, "AO: [%s] %s\n", ao_get_name(mpctx->ao),
+ audio_config_to_str_buf(tmp, sizeof(tmp), ao_rate, ao_format,
+ ao_channels));
+ MP_VERBOSE(mpctx, "AO: Description: %s\n", ao_get_description(mpctx->ao));
+ update_window_title(mpctx, true);
+
+ ao_c->ao_resume_time =
+ opts->audio_wait_open > 0 ? mp_time_sec() + opts->audio_wait_open : 0;
+
+ bool eof = mpctx->audio_status == STATUS_EOF;
+ ao_set_paused(mpctx->ao, get_internal_paused(mpctx), eof);
+
+ ao_chain_set_ao(ao_c, mpctx->ao);
+
+ audio_update_volume(mpctx);
+
+ // Almost nonsensical hack to deal with certain format change scenarios.
+ if (mpctx->audio_status == STATUS_PLAYING)
+ ao_start(mpctx->ao);
+
+ mp_wakeup_core(mpctx);
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+
+ return 0;
+
+init_error:
+ uninit_audio_chain(mpctx);
+ uninit_audio_out(mpctx);
+ error_on_track(mpctx, track);
+ return -1;
+}
+
+int init_audio_decoder(struct MPContext *mpctx, struct track *track)
+{
+ assert(!track->dec);
+ if (!track->stream)
+ goto init_error;
+
+ track->dec = mp_decoder_wrapper_create(mpctx->filter_root, track->stream);
+ if (!track->dec)
+ goto init_error;
+
+ if (track->ao_c)
+ mp_decoder_wrapper_set_spdif_flag(track->dec, true);
+
+ if (!mp_decoder_wrapper_reinit(track->dec))
+ goto init_error;
+
+ return 1;
+
+init_error:
+ if (track->sink)
+ mp_pin_disconnect(track->sink);
+ track->sink = NULL;
+ error_on_track(mpctx, track);
+ return 0;
+}
+
+void reinit_audio_chain(struct MPContext *mpctx)
+{
+ struct track *track = NULL;
+ track = mpctx->current_track[0][STREAM_AUDIO];
+ if (!track || !track->stream) {
+ if (!mpctx->encode_lavc_ctx)
+ uninit_audio_out(mpctx);
+ error_on_track(mpctx, track);
+ return;
+ }
+ reinit_audio_chain_src(mpctx, track);
+}
+
+static const struct mp_filter_info ao_filter = {
+ .name = "ao",
+ .process = ao_process,
+};
+
+// (track=NULL creates a blank chain, used for lavfi-complex)
+void reinit_audio_chain_src(struct MPContext *mpctx, struct track *track)
+{
+ assert(!mpctx->ao_chain);
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+
+ struct ao_chain *ao_c = talloc_zero(NULL, struct ao_chain);
+ mpctx->ao_chain = ao_c;
+ ao_c->mpctx = mpctx;
+ ao_c->log = mpctx->log;
+ ao_c->filter =
+ mp_output_chain_create(mpctx->filter_root, MP_OUTPUT_CHAIN_AUDIO);
+ ao_c->spdif_passthrough = true;
+ ao_c->last_out_pts = MP_NOPTS_VALUE;
+ ao_c->delay = mpctx->opts->audio_delay;
+
+ ao_c->ao_filter = mp_filter_create(mpctx->filter_root, &ao_filter);
+ if (!ao_c->filter || !ao_c->ao_filter)
+ goto init_error;
+ ao_c->ao_filter->priv = ao_c;
+
+ mp_filter_add_pin(ao_c->ao_filter, MP_PIN_IN, "in");
+ mp_pin_connect(ao_c->ao_filter->pins[0], ao_c->filter->f->pins[1]);
+
+ if (track) {
+ ao_c->track = track;
+ track->ao_c = ao_c;
+ if (!init_audio_decoder(mpctx, track))
+ goto init_error;
+ ao_c->dec_src = track->dec->f->pins[0];
+ mp_pin_connect(ao_c->filter->f->pins[0], ao_c->dec_src);
+ }
+
+ reset_audio_state(mpctx);
+
+ if (recreate_audio_filters(mpctx) < 0)
+ goto init_error;
+
+ if (mpctx->ao)
+ audio_update_volume(mpctx);
+
+ mp_wakeup_core(mpctx);
+ return;
+
+init_error:
+ uninit_audio_chain(mpctx);
+ uninit_audio_out(mpctx);
+ error_on_track(mpctx, track);
+}
+
+// Return pts value corresponding to the start point of audio written to the
+// ao queue so far.
+double written_audio_pts(struct MPContext *mpctx)
+{
+ return mpctx->ao_chain ? mpctx->ao_chain->last_out_pts : MP_NOPTS_VALUE;
+}
+
+// Return pts value corresponding to currently playing audio.
+double playing_audio_pts(struct MPContext *mpctx)
+{
+ double pts = written_audio_pts(mpctx);
+ if (pts == MP_NOPTS_VALUE || !mpctx->ao)
+ return pts;
+ return pts - mpctx->audio_speed * ao_get_delay(mpctx->ao);
+}
+
+// This garbage is needed for untimed AOs. These consume audio infinitely fast,
+// so try keeping approximate A/V sync by blocking audio transfer as needed.
+static void update_throttle(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ bool new_throttle = mpctx->audio_status == STATUS_PLAYING &&
+ mpctx->delay > 0 && ao_c && ao_c->ao &&
+ ao_untimed(ao_c->ao) &&
+ mpctx->video_status != STATUS_EOF;
+ if (ao_c && new_throttle != ao_c->untimed_throttle) {
+ ao_c->untimed_throttle = new_throttle;
+ mp_wakeup_core(mpctx);
+ mp_filter_wakeup(ao_c->ao_filter);
+ }
+}
+
+static void ao_process(struct mp_filter *f)
+{
+ struct ao_chain *ao_c = f->priv;
+ struct MPContext *mpctx = ao_c->mpctx;
+
+ if (!ao_c->queue_filter) {
+ // This will eventually lead to the creation of the AO + queue, due
+ // to how f_output_chain and AO management works.
+ mp_pin_out_request_data(f->ppins[0]);
+ // Check for EOF with no data case, which is a mess because everything
+ // hates us.
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (frame.type == MP_FRAME_EOF) {
+ MP_VERBOSE(mpctx, "got EOF with no data before it\n");
+ ao_c->out_eof = true;
+ mpctx->audio_status = STATUS_DRAINING;
+ mp_wakeup_core(mpctx);
+ } else if (frame.type) {
+ mp_pin_out_unread(f->ppins[0], frame);
+ }
+ return;
+ }
+
+ // Due to mp_async_queue_set_notifier() this function is called when the
+ // queue becomes full. This affects state changes in the normal playloop,
+ // so wake it up. But avoid redundant wakeups during normal playback.
+ if (mpctx->audio_status != STATUS_PLAYING &&
+ mp_async_queue_is_full(ao_c->ao_queue))
+ mp_wakeup_core(mpctx);
+
+ if (mpctx->audio_status == STATUS_SYNCING && !ao_c->start_pts_known)
+ return;
+
+ if (ao_c->untimed_throttle)
+ return;
+
+ if (!mp_pin_can_transfer_data(ao_c->queue_filter->pins[0], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (frame.type == MP_FRAME_AUDIO) {
+ struct mp_aframe *af = frame.data;
+
+ double endpts = get_play_end_pts(mpctx);
+ if (endpts != MP_NOPTS_VALUE) {
+ endpts *= mpctx->play_dir;
+ // Avoid decoding and discarding the entire rest of the file.
+ if (mp_aframe_get_pts(af) >= endpts) {
+ mp_pin_out_unread(f->ppins[0], frame);
+ if (!ao_c->out_eof) {
+ ao_c->out_eof = true;
+ mp_pin_in_write(ao_c->queue_filter->pins[0], MP_EOF_FRAME);
+ }
+ return;
+ }
+ }
+ double startpts = mpctx->audio_status == STATUS_SYNCING ?
+ ao_c->start_pts : MP_NOPTS_VALUE;
+ mp_aframe_clip_timestamps(af, startpts, endpts);
+
+ int samples = mp_aframe_get_size(af);
+ if (!samples) {
+ mp_filter_internal_mark_progress(f);
+ mp_frame_unref(&frame);
+ return;
+ }
+
+ ao_c->out_eof = false;
+
+ if (mpctx->audio_status == STATUS_DRAINING ||
+ mpctx->audio_status == STATUS_EOF)
+ {
+ // If a new frame comes decoder/filter EOF, we should preferably
+ // call get_sync_pts() again, which (at least in obscure situations)
+ // may require us to wait a while until the sync PTS is known. Our
+ // code sucks and can't deal with that, so jump through a hoop to
+ // get things done in the correct order.
+ mp_pin_out_unread(f->ppins[0], frame);
+ ao_c->start_pts_known = false;
+ mpctx->audio_status = STATUS_SYNCING;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "new audio frame after EOF\n");
+ return;
+ }
+
+ mpctx->shown_aframes += samples;
+ double real_samplerate = mp_aframe_get_rate(af) / mpctx->audio_speed;
+ if (mpctx->video_status != STATUS_EOF)
+ mpctx->delay += samples / real_samplerate;
+ ao_c->last_out_pts = mp_aframe_end_pts(af);
+ update_throttle(mpctx);
+
+ // Gapless case: the AO is still playing from previous file. It makes
+ // no sense to wait, and in fact the "full queue" event we're waiting
+ // for may never happen, so start immediately.
+ // If the new audio starts "later" (big video sync offset), transfer
+ // of data is stopped somewhere else.
+ if (mpctx->audio_status == STATUS_SYNCING && ao_is_playing(ao_c->ao)) {
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "previous audio still playing; continuing\n");
+ }
+
+ mp_pin_in_write(ao_c->queue_filter->pins[0], frame);
+ } else if (frame.type == MP_FRAME_EOF) {
+ MP_VERBOSE(mpctx, "audio filter EOF\n");
+
+ ao_c->out_eof = true;
+ mp_wakeup_core(mpctx);
+
+ mp_pin_in_write(ao_c->queue_filter->pins[0], frame);
+ mp_filter_internal_mark_progress(f);
+ } else {
+ mp_frame_unref(&frame);
+ }
+}
+
+void reload_audio_output(struct MPContext *mpctx)
+{
+ if (!mpctx->ao)
+ return;
+
+ ao_reset(mpctx->ao);
+ uninit_audio_out(mpctx);
+ reinit_audio_filters(mpctx); // mostly to issue refresh seek
+
+ struct ao_chain *ao_c = mpctx->ao_chain;
+
+ if (ao_c) {
+ reset_audio_state(mpctx);
+ mp_output_chain_reset_harder(ao_c->filter);
+ }
+
+ // Whether we can use spdif might have changed. If we failed to use spdif
+ // in the previous initialization, try it with spdif again (we'll fallback
+ // to PCM again if necessary).
+ if (ao_c && ao_c->track) {
+ struct mp_decoder_wrapper *dec = ao_c->track->dec;
+ if (dec && ao_c->spdif_failed) {
+ ao_c->spdif_passthrough = true;
+ ao_c->spdif_failed = false;
+ mp_decoder_wrapper_set_spdif_flag(ao_c->track->dec, true);
+ if (!mp_decoder_wrapper_reinit(dec)) {
+ MP_ERR(mpctx, "Error reinitializing audio.\n");
+ error_on_track(mpctx, ao_c->track);
+ }
+ }
+ }
+
+ mp_wakeup_core(mpctx);
+}
+
+// Returns audio start pts for seeking or video sync.
+// Returns false if PTS is not known yet.
+static bool get_sync_pts(struct MPContext *mpctx, double *pts)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ *pts = MP_NOPTS_VALUE;
+
+ if (!opts->initial_audio_sync)
+ return true;
+
+ bool sync_to_video = mpctx->vo_chain && mpctx->video_status != STATUS_EOF &&
+ !mpctx->vo_chain->is_sparse;
+
+ if (sync_to_video) {
+ if (mpctx->video_status < STATUS_READY)
+ return false; // wait until we know a video PTS
+ if (mpctx->video_pts != MP_NOPTS_VALUE)
+ *pts = mpctx->video_pts - opts->audio_delay;
+ } else if (mpctx->hrseek_active) {
+ *pts = mpctx->hrseek_pts;
+ } else {
+ // If audio-only is enabled mid-stream during playback, sync accordingly.
+ *pts = mpctx->playback_pts;
+ }
+
+ return true;
+}
+
+// Look whether audio can be started yet - if audio has to start some time
+// after video.
+// Caller needs to ensure mpctx->restart_complete is OK
+void audio_start_ao(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c || !ao_c->ao || mpctx->audio_status != STATUS_READY)
+ return;
+ double pts = MP_NOPTS_VALUE;
+ if (!get_sync_pts(mpctx, &pts))
+ return;
+ double apts = playing_audio_pts(mpctx); // (basically including mpctx->delay)
+ if (pts != MP_NOPTS_VALUE && apts != MP_NOPTS_VALUE && pts < apts &&
+ mpctx->video_status != STATUS_EOF)
+ {
+ double diff = (apts - pts) / mpctx->opts->playback_speed;
+ if (!get_internal_paused(mpctx))
+ mp_set_timeout(mpctx, diff);
+ if (mpctx->logged_async_diff != diff) {
+ MP_VERBOSE(mpctx, "delaying audio start %f vs. %f, diff=%f\n",
+ apts, pts, diff);
+ mpctx->logged_async_diff = diff;
+ }
+ return;
+ }
+
+ MP_VERBOSE(mpctx, "starting audio playback\n");
+ ao_start(ao_c->ao);
+ mpctx->audio_status = STATUS_PLAYING;
+ if (ao_c->out_eof) {
+ mpctx->audio_status = STATUS_DRAINING;
+ MP_VERBOSE(mpctx, "audio draining\n");
+ }
+ ao_c->underrun = false;
+ mpctx->logged_async_diff = -1;
+ mp_wakeup_core(mpctx);
+}
+
+void fill_audio_out_buffers(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->ao && ao_query_and_reset_events(mpctx->ao, AO_EVENT_RELOAD))
+ reload_audio_output(mpctx);
+
+ if (mpctx->ao && ao_query_and_reset_events(mpctx->ao,
+ AO_EVENT_INITIAL_UNBLOCK))
+ ao_unblock(mpctx->ao);
+
+ update_throttle(mpctx);
+
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c)
+ return;
+
+ if (ao_c->filter->failed_output_conversion) {
+ error_on_track(mpctx, ao_c->track);
+ return;
+ }
+
+ if (ao_c->filter->ao_needs_update) {
+ if (reinit_audio_filters_and_output(mpctx) < 0)
+ return;
+ }
+
+ if (mpctx->vo_chain && ao_c->track && ao_c->track->dec &&
+ mp_decoder_wrapper_get_pts_reset(ao_c->track->dec))
+ {
+ MP_WARN(mpctx, "Reset playback due to audio timestamp reset.\n");
+ reset_playback_state(mpctx);
+ mp_wakeup_core(mpctx);
+ }
+
+ if (mpctx->audio_status == STATUS_SYNCING) {
+ double pts;
+ bool ok = get_sync_pts(mpctx, &pts);
+
+ // If the AO is still playing from the previous file (due to gapless),
+ // but if video is active, this may not work if audio starts later than
+ // video, and gapless has no advantages anyway. So block doing anything
+ // until the old audio is fully played.
+ // (Buggy if AO underruns.)
+ if (mpctx->ao && ao_is_playing(mpctx->ao) &&
+ mpctx->video_status != STATUS_EOF) {
+ MP_VERBOSE(mpctx, "blocked, waiting for old audio to play\n");
+ ok = false;
+ }
+
+ if (ao_c->start_pts_known != ok || ao_c->start_pts != pts) {
+ ao_c->start_pts_known = ok;
+ ao_c->start_pts = pts;
+ mp_filter_wakeup(ao_c->ao_filter);
+ }
+
+ if (ao_c->ao && mp_async_queue_is_full(ao_c->ao_queue)) {
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "audio ready\n");
+ } else if (ao_c->out_eof) {
+ // Force playback start early.
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "audio ready (and EOF)\n");
+ }
+ }
+
+ if (ao_c->ao && !ao_is_playing(ao_c->ao) && !ao_c->underrun &&
+ (mpctx->audio_status == STATUS_PLAYING ||
+ mpctx->audio_status == STATUS_DRAINING))
+ {
+ // Should be playing, but somehow isn't.
+
+ if (ao_c->out_eof && !mp_async_queue_get_frames(ao_c->ao_queue)) {
+ MP_VERBOSE(mpctx, "AO signaled EOF (while in state %s)\n",
+ mp_status_str(mpctx->audio_status));
+ mpctx->audio_status = STATUS_EOF;
+ mp_wakeup_core(mpctx);
+ // stops untimed AOs, stops pull AOs from streaming silence
+ ao_reset(ao_c->ao);
+ } else {
+ if (!ao_c->ao_underrun) {
+ MP_WARN(mpctx, "Audio device underrun detected.\n");
+ ao_c->ao_underrun = true;
+ mp_wakeup_core(mpctx);
+ ao_c->underrun = true;
+ }
+
+ // Wait until buffers are filled before recovering underrun.
+ if (ao_c->out_eof || mp_async_queue_is_full(ao_c->ao_queue)) {
+ MP_VERBOSE(mpctx, "restarting audio after underrun\n");
+ ao_start(mpctx->ao_chain->ao);
+ ao_c->ao_underrun = false;
+ ao_c->underrun = false;
+ mp_wakeup_core(mpctx);
+ }
+ }
+ }
+
+ if (mpctx->audio_status == STATUS_PLAYING && ao_c->out_eof) {
+ mpctx->audio_status = STATUS_DRAINING;
+ MP_VERBOSE(mpctx, "audio draining\n");
+ mp_wakeup_core(mpctx);
+ }
+
+ if (mpctx->audio_status == STATUS_DRAINING) {
+ // Wait until the AO has played all queued data. In the gapless case,
+ // we trigger EOF immediately, and let it play asynchronously.
+ if (!ao_c->ao || (!ao_is_playing(ao_c->ao) ||
+ (opts->gapless_audio && !ao_untimed(ao_c->ao))))
+ {
+ MP_VERBOSE(mpctx, "audio EOF reached\n");
+ mpctx->audio_status = STATUS_EOF;
+ mp_wakeup_core(mpctx);
+ }
+ }
+
+ if (mpctx->restart_complete)
+ audio_start_ao(mpctx); // in case it got delayed
+}
+
+// Drop data queued for output, or which the AO is currently outputting.
+void clear_audio_output_buffers(struct MPContext *mpctx)
+{
+ if (mpctx->ao)
+ ao_reset(mpctx->ao);
+}
diff --git a/player/client.c b/player/client.c
new file mode 100644
index 0000000..b35f20a
--- /dev/null
+++ b/player/client.c
@@ -0,0 +1,2248 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/global.h"
+#include "input/input.h"
+#include "input/cmd.h"
+#include "misc/ctype.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "misc/rendezvous.h"
+#include "misc/thread_tools.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/m_property.h"
+#include "options/path.h"
+#include "options/parse_configfile.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "osdep/io.h"
+#include "stream/stream.h"
+
+#include "command.h"
+#include "core.h"
+#include "client.h"
+
+/*
+ * Locking hierarchy:
+ *
+ * MPContext > mp_client_api.lock > mpv_handle.lock > * > mpv_handle.wakeup_lock
+ *
+ * MPContext strictly speaking has no locks, and instead is implicitly managed
+ * by MPContext.dispatch, which basically stops the playback thread at defined
+ * points in order to let clients access it in a synchronized manner. Since
+ * MPContext code accesses the client API, it's on top of the lock hierarchy.
+ *
+ */
+
+struct mp_client_api {
+ struct MPContext *mpctx;
+
+ mp_mutex lock;
+
+ // -- protected by lock
+
+ struct mpv_handle **clients;
+ int num_clients;
+ bool shutting_down; // do not allow new clients
+ bool have_terminator; // a client took over the role of destroying the core
+ bool terminate_core_thread; // make libmpv core thread exit
+ // This is incremented whenever the clients[] array above changes. This is
+ // used to safely unlock mp_client_api.lock while iterating the list of
+ // clients.
+ uint64_t clients_list_change_ts;
+ int64_t id_alloc;
+
+ struct mp_custom_protocol *custom_protocols;
+ int num_custom_protocols;
+
+ struct mpv_render_context *render_context;
+};
+
+struct observe_property {
+ // -- immutable
+ struct mpv_handle *owner;
+ char *name;
+ int id; // ==mp_get_property_id(name)
+ uint64_t event_mask; // ==mp_get_property_event_mask(name)
+ int64_t reply_id;
+ mpv_format format;
+ const struct m_option *type;
+ // -- protected by owner->lock
+ size_t refcount;
+ uint64_t change_ts; // logical timestamp incremented on each change
+ uint64_t value_ts; // logical timestamp for value contents
+ bool value_valid;
+ union m_option_value value;
+ uint64_t value_ret_ts; // logical timestamp of value returned to user
+ union m_option_value value_ret;
+ bool waiting_for_hook; // flag for draining old property changes on a hook
+};
+
+struct mpv_handle {
+ // -- immutable
+ char name[MAX_CLIENT_NAME];
+ struct mp_log *log;
+ struct MPContext *mpctx;
+ struct mp_client_api *clients;
+ int64_t id;
+
+ // -- not thread-safe
+ struct mpv_event *cur_event;
+ struct mpv_event_property cur_property_event;
+ struct observe_property *cur_property;
+
+ mp_mutex lock;
+
+ mp_mutex wakeup_lock;
+ mp_cond wakeup;
+
+ // -- protected by wakeup_lock
+ bool need_wakeup;
+ void (*wakeup_cb)(void *d);
+ void *wakeup_cb_ctx;
+ int wakeup_pipe[2];
+
+ // -- protected by lock
+
+ uint64_t event_mask;
+ bool queued_wakeup;
+
+ mpv_event *events; // ringbuffer of max_events entries
+ int max_events; // allocated number of entries in events
+ int first_event; // events[first_event] is the first readable event
+ int num_events; // number of readable events
+ int reserved_events; // number of entries reserved for replies
+ size_t async_counter; // pending other async events
+ bool choked; // recovering from queue overflow
+ bool destroying; // pending destruction; no API accesses allowed
+ bool hook_pending; // hook events are returned after draining properties
+
+ struct observe_property **properties;
+ int num_properties;
+ bool has_pending_properties; // (maybe) new property events (producer side)
+ bool new_property_events; // new property events (consumer side)
+ int cur_property_index; // round-robin for property events (consumer side)
+ uint64_t property_event_masks; // or-ed together event masks of all properties
+ // This is incremented whenever the properties[] array above changes. This
+ // is used to safely unlock mpv_handle.lock while reading a property. If
+ // the counter didn't change between unlock and relock, then it will assume
+ // the array did not change.
+ uint64_t properties_change_ts;
+
+ bool fuzzy_initialized; // see scripting.c wait_loaded()
+ bool is_weak; // can not keep core alive on its own
+ struct mp_log_buffer *messages;
+ int messages_level;
+};
+
+static bool gen_log_message_event(struct mpv_handle *ctx);
+static bool gen_property_change_event(struct mpv_handle *ctx);
+static void notify_property_events(struct mpv_handle *ctx, int event);
+
+// Must be called with prop->owner->lock held.
+static void prop_unref(struct observe_property *prop)
+{
+ if (!prop)
+ return;
+
+ assert(prop->refcount > 0);
+ prop->refcount -= 1;
+ if (!prop->refcount)
+ talloc_free(prop);
+}
+
+void mp_clients_init(struct MPContext *mpctx)
+{
+ mpctx->clients = talloc_ptrtype(NULL, mpctx->clients);
+ *mpctx->clients = (struct mp_client_api) {
+ .mpctx = mpctx,
+ };
+ mpctx->global->client_api = mpctx->clients;
+ mp_mutex_init(&mpctx->clients->lock);
+}
+
+void mp_clients_destroy(struct MPContext *mpctx)
+{
+ if (!mpctx->clients)
+ return;
+ assert(mpctx->clients->num_clients == 0);
+
+ // The API user is supposed to call mpv_render_context_free(). It's simply
+ // not allowed not to do this.
+ if (mpctx->clients->render_context) {
+ MP_FATAL(mpctx, "Broken API use: mpv_render_context_free() not called.\n");
+ abort();
+ }
+
+ mp_mutex_destroy(&mpctx->clients->lock);
+ talloc_free(mpctx->clients);
+ mpctx->clients = NULL;
+}
+
+// Test for "fuzzy" initialization of all clients. That is, all clients have
+// at least called mpv_wait_event() at least once since creation (or exited).
+bool mp_clients_all_initialized(struct MPContext *mpctx)
+{
+ bool all_ok = true;
+ mp_mutex_lock(&mpctx->clients->lock);
+ for (int n = 0; n < mpctx->clients->num_clients; n++) {
+ struct mpv_handle *ctx = mpctx->clients->clients[n];
+ mp_mutex_lock(&ctx->lock);
+ all_ok &= ctx->fuzzy_initialized;
+ mp_mutex_unlock(&ctx->lock);
+ }
+ mp_mutex_unlock(&mpctx->clients->lock);
+ return all_ok;
+}
+
+static struct mpv_handle *find_client_id(struct mp_client_api *clients, int64_t id)
+{
+ for (int n = 0; n < clients->num_clients; n++) {
+ if (clients->clients[n]->id == id)
+ return clients->clients[n];
+ }
+ return NULL;
+}
+
+static struct mpv_handle *find_client(struct mp_client_api *clients,
+ const char *name)
+{
+ if (name[0] == '@') {
+ char *end;
+ errno = 0;
+ long long int id = strtoll(name + 1, &end, 10);
+ if (errno || end[0])
+ return NULL;
+ return find_client_id(clients, id);
+ }
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ if (strcmp(clients->clients[n]->name, name) == 0)
+ return clients->clients[n];
+ }
+
+ return NULL;
+}
+
+bool mp_client_id_exists(struct MPContext *mpctx, int64_t id)
+{
+ mp_mutex_lock(&mpctx->clients->lock);
+ bool r = find_client_id(mpctx->clients, id);
+ mp_mutex_unlock(&mpctx->clients->lock);
+ return r;
+}
+
+struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name)
+{
+ mp_mutex_lock(&clients->lock);
+
+ char nname[MAX_CLIENT_NAME];
+ for (int n = 1; n < 1000; n++) {
+ if (!name)
+ name = "client";
+ snprintf(nname, sizeof(nname) - 3, "%s", name); // - space for number
+ for (int i = 0; nname[i]; i++)
+ nname[i] = mp_isalnum(nname[i]) ? nname[i] : '_';
+ if (n > 1)
+ mp_snprintf_cat(nname, sizeof(nname), "%d", n);
+ if (!find_client(clients, nname))
+ break;
+ nname[0] = '\0';
+ }
+
+ if (!nname[0] || clients->shutting_down) {
+ mp_mutex_unlock(&clients->lock);
+ return NULL;
+ }
+
+ int num_events = 1000;
+
+ struct mpv_handle *client = talloc_ptrtype(NULL, client);
+ *client = (struct mpv_handle){
+ .log = mp_log_new(client, clients->mpctx->log, nname),
+ .mpctx = clients->mpctx,
+ .clients = clients,
+ .id = ++(clients->id_alloc),
+ .cur_event = talloc_zero(client, struct mpv_event),
+ .events = talloc_array(client, mpv_event, num_events),
+ .max_events = num_events,
+ .event_mask = (1ULL << INTERNAL_EVENT_BASE) - 1, // exclude internal events
+ .wakeup_pipe = {-1, -1},
+ };
+ mp_mutex_init(&client->lock);
+ mp_mutex_init(&client->wakeup_lock);
+ mp_cond_init(&client->wakeup);
+
+ snprintf(client->name, sizeof(client->name), "%s", nname);
+
+ clients->clients_list_change_ts += 1;
+ MP_TARRAY_APPEND(clients, clients->clients, clients->num_clients, client);
+
+ if (clients->num_clients == 1 && !clients->mpctx->is_cli)
+ client->fuzzy_initialized = true;
+
+ mp_mutex_unlock(&clients->lock);
+
+ mpv_request_event(client, MPV_EVENT_TICK, 0);
+
+ return client;
+}
+
+void mp_client_set_weak(struct mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->lock);
+ ctx->is_weak = true;
+ mp_mutex_unlock(&ctx->lock);
+}
+
+const char *mpv_client_name(mpv_handle *ctx)
+{
+ return ctx->name;
+}
+
+int64_t mpv_client_id(mpv_handle *ctx)
+{
+ return ctx->id;
+}
+
+struct mp_log *mp_client_get_log(struct mpv_handle *ctx)
+{
+ return ctx->log;
+}
+
+struct mpv_global *mp_client_get_global(struct mpv_handle *ctx)
+{
+ return ctx->mpctx->global;
+}
+
+static void wakeup_client(struct mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->wakeup_lock);
+ if (!ctx->need_wakeup) {
+ ctx->need_wakeup = true;
+ mp_cond_broadcast(&ctx->wakeup);
+ if (ctx->wakeup_cb)
+ ctx->wakeup_cb(ctx->wakeup_cb_ctx);
+ if (ctx->wakeup_pipe[0] != -1)
+ (void)write(ctx->wakeup_pipe[1], &(char){0}, 1);
+ }
+ mp_mutex_unlock(&ctx->wakeup_lock);
+}
+
+// Note: the caller has to deal with sporadic wakeups.
+static int wait_wakeup(struct mpv_handle *ctx, int64_t end)
+{
+ int r = 0;
+ mp_mutex_unlock(&ctx->lock);
+ mp_mutex_lock(&ctx->wakeup_lock);
+ if (!ctx->need_wakeup)
+ r = mp_cond_timedwait_until(&ctx->wakeup, &ctx->wakeup_lock, end);
+ if (r == 0)
+ ctx->need_wakeup = false;
+ mp_mutex_unlock(&ctx->wakeup_lock);
+ mp_mutex_lock(&ctx->lock);
+ return r;
+}
+
+void mpv_set_wakeup_callback(mpv_handle *ctx, void (*cb)(void *d), void *d)
+{
+ mp_mutex_lock(&ctx->wakeup_lock);
+ ctx->wakeup_cb = cb;
+ ctx->wakeup_cb_ctx = d;
+ if (ctx->wakeup_cb)
+ ctx->wakeup_cb(ctx->wakeup_cb_ctx);
+ mp_mutex_unlock(&ctx->wakeup_lock);
+}
+
+static void lock_core(mpv_handle *ctx)
+{
+ mp_dispatch_lock(ctx->mpctx->dispatch);
+}
+
+static void unlock_core(mpv_handle *ctx)
+{
+ mp_dispatch_unlock(ctx->mpctx->dispatch);
+}
+
+void mpv_wait_async_requests(mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->lock);
+ while (ctx->reserved_events || ctx->async_counter)
+ wait_wakeup(ctx, INT64_MAX);
+ mp_mutex_unlock(&ctx->lock);
+}
+
+// Send abort signal to all matching work items.
+// If type==0, destroy all of the matching ctx.
+// If ctx==0, destroy all.
+static void abort_async(struct MPContext *mpctx, mpv_handle *ctx,
+ int type, uint64_t id)
+{
+ mp_mutex_lock(&mpctx->abort_lock);
+
+ // Destroy all => ensure any newly appearing work is aborted immediately.
+ if (ctx == NULL)
+ mpctx->abort_all = true;
+
+ for (int n = 0; n < mpctx->num_abort_list; n++) {
+ struct mp_abort_entry *abort = mpctx->abort_list[n];
+ if (!ctx || (abort->client == ctx && (!type ||
+ (abort->client_work_type == type && abort->client_work_id == id))))
+ {
+ mp_abort_trigger_locked(mpctx, abort);
+ }
+ }
+
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+static void get_thread_id(void *ptr)
+{
+ *(mp_thread_id *)ptr = mp_thread_current_id();
+}
+
+static void mp_destroy_client(mpv_handle *ctx, bool terminate)
+{
+ if (!ctx)
+ return;
+
+ struct MPContext *mpctx = ctx->mpctx;
+ struct mp_client_api *clients = ctx->clients;
+
+ MP_DBG(ctx, "Exiting...\n");
+
+ if (terminate)
+ mpv_command(ctx, (const char*[]){"quit", NULL});
+
+ mp_mutex_lock(&ctx->lock);
+
+ ctx->destroying = true;
+
+ for (int n = 0; n < ctx->num_properties; n++)
+ prop_unref(ctx->properties[n]);
+ ctx->num_properties = 0;
+ ctx->properties_change_ts += 1;
+
+ prop_unref(ctx->cur_property);
+ ctx->cur_property = NULL;
+
+ mp_mutex_unlock(&ctx->lock);
+
+ abort_async(mpctx, ctx, 0, 0);
+
+ // reserved_events equals the number of asynchronous requests that weren't
+ // yet replied. In order to avoid that trying to reply to a removed client
+ // causes a crash, block until all asynchronous requests were served.
+ mpv_wait_async_requests(ctx);
+
+ osd_set_external_remove_owner(mpctx->osd, ctx);
+ mp_input_remove_sections_by_owner(mpctx->input, ctx->name);
+
+ mp_mutex_lock(&clients->lock);
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ if (clients->clients[n] == ctx) {
+ clients->clients_list_change_ts += 1;
+ MP_TARRAY_REMOVE_AT(clients->clients, clients->num_clients, n);
+ while (ctx->num_events) {
+ talloc_free(ctx->events[ctx->first_event].data);
+ ctx->first_event = (ctx->first_event + 1) % ctx->max_events;
+ ctx->num_events--;
+ }
+ mp_msg_log_buffer_destroy(ctx->messages);
+ mp_cond_destroy(&ctx->wakeup);
+ mp_mutex_destroy(&ctx->wakeup_lock);
+ mp_mutex_destroy(&ctx->lock);
+ if (ctx->wakeup_pipe[0] != -1) {
+ close(ctx->wakeup_pipe[0]);
+ close(ctx->wakeup_pipe[1]);
+ }
+ talloc_free(ctx);
+ ctx = NULL;
+ break;
+ }
+ }
+ assert(!ctx);
+
+ if (mpctx->is_cli) {
+ terminate = false;
+ } else {
+ // If the last strong mpv_handle got destroyed, destroy the core.
+ bool has_strong_ref = false;
+ for (int n = 0; n < clients->num_clients; n++)
+ has_strong_ref |= !clients->clients[n]->is_weak;
+ if (!has_strong_ref)
+ terminate = true;
+
+ // Reserve the right to destroy mpctx for us.
+ if (clients->have_terminator)
+ terminate = false;
+ clients->have_terminator |= terminate;
+ }
+
+ // mp_shutdown_clients() sleeps to avoid wasting CPU.
+ // mp_hook_test_completion() also relies on this a bit.
+ mp_wakeup_core(mpctx);
+
+ mp_mutex_unlock(&clients->lock);
+
+ // Note that even if num_clients==0, having set have_terminator keeps mpctx
+ // and the core thread alive.
+ if (terminate) {
+ // Make sure the core stops playing files etc. Being able to lock the
+ // dispatch queue requires that the core thread is still active.
+ mp_dispatch_lock(mpctx->dispatch);
+ mpctx->stop_play = PT_QUIT;
+ mp_dispatch_unlock(mpctx->dispatch);
+
+ mp_thread_id playthread;
+ mp_dispatch_run(mpctx->dispatch, get_thread_id, &playthread);
+
+ // Ask the core thread to stop.
+ mp_mutex_lock(&clients->lock);
+ clients->terminate_core_thread = true;
+ mp_mutex_unlock(&clients->lock);
+ mp_wakeup_core(mpctx);
+
+ // Blocking wait for all clients and core thread to terminate.
+ mp_thread_join_id(playthread);
+
+ mp_destroy(mpctx);
+ }
+}
+
+void mpv_destroy(mpv_handle *ctx)
+{
+ mp_destroy_client(ctx, false);
+}
+
+void mpv_terminate_destroy(mpv_handle *ctx)
+{
+ mp_destroy_client(ctx, true);
+}
+
+// Can be called on the core thread only. Idempotent.
+// Also happens to take care of shutting down any async work.
+void mp_shutdown_clients(struct MPContext *mpctx)
+{
+ struct mp_client_api *clients = mpctx->clients;
+
+ // Forcefully abort async work after 2 seconds of waiting.
+ double abort_time = mp_time_sec() + 2;
+
+ mp_mutex_lock(&clients->lock);
+
+ // Prevent that new clients can appear.
+ clients->shutting_down = true;
+
+ // Wait until we can terminate.
+ while (clients->num_clients || mpctx->outstanding_async ||
+ !(mpctx->is_cli || clients->terminate_core_thread))
+ {
+ mp_mutex_unlock(&clients->lock);
+
+ double left = abort_time - mp_time_sec();
+ if (left >= 0) {
+ mp_set_timeout(mpctx, left);
+ } else {
+ // Forcefully abort any ongoing async work. This is quite rude and
+ // probably not what everyone wants, so it happens only after a
+ // timeout.
+ abort_async(mpctx, NULL, 0, 0);
+ }
+
+ mp_client_broadcast_event(mpctx, MPV_EVENT_SHUTDOWN, NULL);
+ mp_wait_events(mpctx);
+
+ mp_mutex_lock(&clients->lock);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+}
+
+bool mp_is_shutting_down(struct MPContext *mpctx)
+{
+ struct mp_client_api *clients = mpctx->clients;
+ mp_mutex_lock(&clients->lock);
+ bool res = clients->shutting_down;
+ mp_mutex_unlock(&clients->lock);
+ return res;
+}
+
+static MP_THREAD_VOID core_thread(void *p)
+{
+ struct MPContext *mpctx = p;
+
+ mp_thread_set_name("core");
+
+ while (!mpctx->initialized && mpctx->stop_play != PT_QUIT)
+ mp_idle(mpctx);
+
+ if (mpctx->initialized)
+ mp_play_files(mpctx);
+
+ // This actually waits until all clients are gone before actually
+ // destroying mpctx. Actual destruction is done by whatever destroys
+ // the last mpv_handle.
+ mp_shutdown_clients(mpctx);
+
+ MP_THREAD_RETURN();
+}
+
+mpv_handle *mpv_create(void)
+{
+ struct MPContext *mpctx = mp_create();
+ if (!mpctx)
+ return NULL;
+
+ m_config_set_profile(mpctx->mconfig, "libmpv", 0);
+
+ mpv_handle *ctx = mp_new_client(mpctx->clients, "main");
+ if (!ctx) {
+ mp_destroy(mpctx);
+ return NULL;
+ }
+
+ mp_thread thread;
+ if (mp_thread_create(&thread, core_thread, mpctx) != 0) {
+ ctx->clients->have_terminator = true; // avoid blocking
+ mpv_terminate_destroy(ctx);
+ mp_destroy(mpctx);
+ return NULL;
+ }
+
+ return ctx;
+}
+
+mpv_handle *mpv_create_client(mpv_handle *ctx, const char *name)
+{
+ if (!ctx)
+ return mpv_create();
+ mpv_handle *new = mp_new_client(ctx->mpctx->clients, name);
+ if (new)
+ mpv_wait_event(new, 0); // set fuzzy_initialized
+ return new;
+}
+
+mpv_handle *mpv_create_weak_client(mpv_handle *ctx, const char *name)
+{
+ mpv_handle *new = mpv_create_client(ctx, name);
+ if (new)
+ mp_client_set_weak(new);
+ return new;
+}
+
+int mpv_initialize(mpv_handle *ctx)
+{
+ lock_core(ctx);
+ int res = mp_initialize(ctx->mpctx, NULL) ? MPV_ERROR_INVALID_PARAMETER : 0;
+ mp_wakeup_core(ctx->mpctx);
+ unlock_core(ctx);
+ return res;
+}
+
+// set ev->data to a new copy of the original data
+// (done only for message types that are broadcast)
+static void dup_event_data(struct mpv_event *ev)
+{
+ switch (ev->event_id) {
+ case MPV_EVENT_CLIENT_MESSAGE: {
+ struct mpv_event_client_message *src = ev->data;
+ struct mpv_event_client_message *msg =
+ talloc_zero(NULL, struct mpv_event_client_message);
+ for (int n = 0; n < src->num_args; n++) {
+ MP_TARRAY_APPEND(msg, msg->args, msg->num_args,
+ talloc_strdup(msg, src->args[n]));
+ }
+ ev->data = msg;
+ break;
+ }
+ case MPV_EVENT_START_FILE:
+ ev->data = talloc_memdup(NULL, ev->data, sizeof(mpv_event_start_file));
+ break;
+ case MPV_EVENT_END_FILE:
+ ev->data = talloc_memdup(NULL, ev->data, sizeof(mpv_event_end_file));
+ break;
+ default:
+ // Doesn't use events with memory allocation.
+ if (ev->data)
+ abort();
+ }
+}
+
+// Reserve an entry in the ring buffer. This can be used to guarantee that the
+// reply can be made, even if the buffer becomes congested _after_ sending
+// the request.
+// Returns an error code if the buffer is full.
+static int reserve_reply(struct mpv_handle *ctx)
+{
+ int res = MPV_ERROR_EVENT_QUEUE_FULL;
+ mp_mutex_lock(&ctx->lock);
+ if (ctx->reserved_events + ctx->num_events < ctx->max_events && !ctx->choked)
+ {
+ ctx->reserved_events++;
+ res = 0;
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return res;
+}
+
+static int append_event(struct mpv_handle *ctx, struct mpv_event event, bool copy)
+{
+ if (ctx->num_events + ctx->reserved_events >= ctx->max_events)
+ return -1;
+ if (copy)
+ dup_event_data(&event);
+ ctx->events[(ctx->first_event + ctx->num_events) % ctx->max_events] = event;
+ ctx->num_events++;
+ wakeup_client(ctx);
+ if (event.event_id == MPV_EVENT_SHUTDOWN)
+ ctx->event_mask &= ctx->event_mask & ~(1ULL << MPV_EVENT_SHUTDOWN);
+ return 0;
+}
+
+static int send_event(struct mpv_handle *ctx, struct mpv_event *event, bool copy)
+{
+ mp_mutex_lock(&ctx->lock);
+ uint64_t mask = 1ULL << event->event_id;
+ if (ctx->property_event_masks & mask)
+ notify_property_events(ctx, event->event_id);
+ int r;
+ if (!(ctx->event_mask & mask)) {
+ r = 0;
+ } else if (ctx->choked) {
+ r = -1;
+ } else {
+ r = append_event(ctx, *event, copy);
+ if (r < 0) {
+ MP_ERR(ctx, "Too many events queued.\n");
+ ctx->choked = true;
+ }
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return r;
+}
+
+// Send a reply; the reply must have been previously reserved with
+// reserve_reply (otherwise, use send_event()).
+static void send_reply(struct mpv_handle *ctx, uint64_t userdata,
+ struct mpv_event *event)
+{
+ event->reply_userdata = userdata;
+ mp_mutex_lock(&ctx->lock);
+ // If this fails, reserve_reply() probably wasn't called.
+ assert(ctx->reserved_events > 0);
+ ctx->reserved_events--;
+ if (append_event(ctx, *event, false) < 0)
+ MP_ASSERT_UNREACHABLE();
+ mp_mutex_unlock(&ctx->lock);
+}
+
+void mp_client_broadcast_event(struct MPContext *mpctx, int event, void *data)
+{
+ struct mp_client_api *clients = mpctx->clients;
+
+ mp_mutex_lock(&clients->lock);
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ struct mpv_event event_data = {
+ .event_id = event,
+ .data = data,
+ };
+ send_event(clients->clients[n], &event_data, true);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+}
+
+// Like mp_client_broadcast_event(), but can be called from any thread.
+// Avoid using this.
+void mp_client_broadcast_event_external(struct mp_client_api *api, int event,
+ void *data)
+{
+ struct MPContext *mpctx = api->mpctx;
+
+ mp_client_broadcast_event(mpctx, event, data);
+ mp_wakeup_core(mpctx);
+}
+
+// If client_name == NULL, then broadcast and free the event.
+int mp_client_send_event(struct MPContext *mpctx, const char *client_name,
+ uint64_t reply_userdata, int event, void *data)
+{
+ if (!client_name) {
+ mp_client_broadcast_event(mpctx, event, data);
+ talloc_free(data);
+ return 0;
+ }
+
+ struct mp_client_api *clients = mpctx->clients;
+ int r = 0;
+
+ struct mpv_event event_data = {
+ .event_id = event,
+ .data = data,
+ .reply_userdata = reply_userdata,
+ };
+
+ mp_mutex_lock(&clients->lock);
+
+ struct mpv_handle *ctx = find_client(clients, client_name);
+ if (ctx) {
+ r = send_event(ctx, &event_data, false);
+ } else {
+ r = -1;
+ talloc_free(data);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+
+ return r;
+}
+
+int mp_client_send_event_dup(struct MPContext *mpctx, const char *client_name,
+ int event, void *data)
+{
+ if (!client_name) {
+ mp_client_broadcast_event(mpctx, event, data);
+ return 0;
+ }
+
+ struct mpv_event event_data = {
+ .event_id = event,
+ .data = data,
+ };
+
+ dup_event_data(&event_data);
+ return mp_client_send_event(mpctx, client_name, 0, event, event_data.data);
+}
+
+const static bool deprecated_events[] = {
+ [MPV_EVENT_IDLE] = true,
+ [MPV_EVENT_TICK] = true,
+};
+
+int mpv_request_event(mpv_handle *ctx, mpv_event_id event, int enable)
+{
+ if (!mpv_event_name(event) || enable < 0 || enable > 1)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (event == MPV_EVENT_SHUTDOWN && !enable)
+ return MPV_ERROR_INVALID_PARAMETER;
+ assert(event < (int)INTERNAL_EVENT_BASE); // excluded above; they have no name
+ mp_mutex_lock(&ctx->lock);
+ uint64_t bit = 1ULL << event;
+ ctx->event_mask = enable ? ctx->event_mask | bit : ctx->event_mask & ~bit;
+ if (enable && event < MP_ARRAY_SIZE(deprecated_events) &&
+ deprecated_events[event])
+ {
+ MP_WARN(ctx, "The '%s' event is deprecated and will be removed.\n",
+ mpv_event_name(event));
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return 0;
+}
+
+// Set waiting_for_hook==true for all possibly pending properties.
+static void set_wait_for_hook_flags(mpv_handle *ctx)
+{
+ for (int n = 0; n < ctx->num_properties; n++) {
+ struct observe_property *prop = ctx->properties[n];
+
+ if (prop->value_ret_ts != prop->change_ts)
+ prop->waiting_for_hook = true;
+ }
+}
+
+// Return whether any property still has waiting_for_hook set.
+static bool check_for_for_hook_flags(mpv_handle *ctx)
+{
+ for (int n = 0; n < ctx->num_properties; n++) {
+ if (ctx->properties[n]->waiting_for_hook)
+ return true;
+ }
+ return false;
+}
+
+mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout)
+{
+ mpv_event *event = ctx->cur_event;
+
+ mp_mutex_lock(&ctx->lock);
+
+ if (!ctx->fuzzy_initialized)
+ mp_wakeup_core(ctx->clients->mpctx);
+ ctx->fuzzy_initialized = true;
+
+ if (timeout < 0)
+ timeout = 1e20;
+
+ int64_t deadline = mp_time_ns_add(mp_time_ns(), timeout);
+
+ *event = (mpv_event){0};
+ talloc_free_children(event);
+
+ while (1) {
+ if (ctx->queued_wakeup)
+ deadline = 0;
+ // Recover from overflow.
+ if (ctx->choked && !ctx->num_events) {
+ ctx->choked = false;
+ event->event_id = MPV_EVENT_QUEUE_OVERFLOW;
+ break;
+ }
+ struct mpv_event *ev =
+ ctx->num_events ? &ctx->events[ctx->first_event] : NULL;
+ if (ev && ev->event_id == MPV_EVENT_HOOK) {
+ // Give old property notifications priority over hooks. This is a
+ // guarantee given to clients to simplify their logic. New property
+ // changes after this are treated normally, so
+ if (!ctx->hook_pending) {
+ ctx->hook_pending = true;
+ set_wait_for_hook_flags(ctx);
+ }
+ if (check_for_for_hook_flags(ctx)) {
+ ev = NULL; // delay
+ } else {
+ ctx->hook_pending = false;
+ }
+ }
+ if (ev) {
+ *event = *ev;
+ ctx->first_event = (ctx->first_event + 1) % ctx->max_events;
+ ctx->num_events--;
+ talloc_steal(event, event->data);
+ break;
+ }
+ // If there's a changed property, generate change event (never queued).
+ if (gen_property_change_event(ctx))
+ break;
+ // Pop item from message queue, and return as event.
+ if (gen_log_message_event(ctx))
+ break;
+ int r = wait_wakeup(ctx, deadline);
+ if (r == ETIMEDOUT)
+ break;
+ }
+ ctx->queued_wakeup = false;
+
+ mp_mutex_unlock(&ctx->lock);
+
+ return event;
+}
+
+void mpv_wakeup(mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->lock);
+ ctx->queued_wakeup = true;
+ wakeup_client(ctx);
+ mp_mutex_unlock(&ctx->lock);
+}
+
+// map client API types to internal types
+static const struct m_option type_conv[] = {
+ [MPV_FORMAT_STRING] = { .type = CONF_TYPE_STRING },
+ [MPV_FORMAT_FLAG] = { .type = CONF_TYPE_FLAG },
+ [MPV_FORMAT_INT64] = { .type = CONF_TYPE_INT64 },
+ [MPV_FORMAT_DOUBLE] = { .type = CONF_TYPE_DOUBLE },
+ [MPV_FORMAT_NODE] = { .type = CONF_TYPE_NODE },
+};
+
+static const struct m_option *get_mp_type(mpv_format format)
+{
+ if ((unsigned)format >= MP_ARRAY_SIZE(type_conv))
+ return NULL;
+ if (!type_conv[format].type)
+ return NULL;
+ return &type_conv[format];
+}
+
+// for read requests - MPV_FORMAT_OSD_STRING special handling
+static const struct m_option *get_mp_type_get(mpv_format format)
+{
+ if (format == MPV_FORMAT_OSD_STRING)
+ format = MPV_FORMAT_STRING; // it's string data, just other semantics
+ return get_mp_type(format);
+}
+
+// move src->dst, and do implicit conversion if possible (conversions to or
+// from strings are handled otherwise)
+static bool conv_node_to_format(void *dst, mpv_format dst_fmt, mpv_node *src)
+{
+ if (dst_fmt == src->format) {
+ const struct m_option *type = get_mp_type(dst_fmt);
+ memcpy(dst, &src->u, type->type->size);
+ return true;
+ }
+ if (dst_fmt == MPV_FORMAT_DOUBLE && src->format == MPV_FORMAT_INT64) {
+ *(double *)dst = src->u.int64;
+ return true;
+ }
+ if (dst_fmt == MPV_FORMAT_INT64 && src->format == MPV_FORMAT_DOUBLE) {
+ if (src->u.double_ > (double)INT64_MIN &&
+ src->u.double_ < (double)INT64_MAX)
+ {
+ *(int64_t *)dst = src->u.double_;
+ return true;
+ }
+ }
+ return false;
+}
+
+void mpv_free_node_contents(mpv_node *node)
+{
+ static const struct m_option type = { .type = CONF_TYPE_NODE };
+ m_option_free(&type, node);
+}
+
+int mpv_set_option(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data)
+{
+ const struct m_option *type = get_mp_type(format);
+ if (!type)
+ return MPV_ERROR_OPTION_FORMAT;
+ struct mpv_node tmp;
+ if (format != MPV_FORMAT_NODE) {
+ tmp.format = format;
+ memcpy(&tmp.u, data, type->type->size);
+ data = &tmp;
+ }
+ lock_core(ctx);
+ int err = m_config_set_option_node(ctx->mpctx->mconfig, bstr0(name), data, 0);
+ unlock_core(ctx);
+ switch (err) {
+ case M_OPT_MISSING_PARAM:
+ case M_OPT_INVALID:
+ return MPV_ERROR_OPTION_ERROR;
+ case M_OPT_OUT_OF_RANGE:
+ return MPV_ERROR_OPTION_FORMAT;
+ case M_OPT_UNKNOWN:
+ return MPV_ERROR_OPTION_NOT_FOUND;
+ default:
+ if (err >= 0)
+ return 0;
+ return MPV_ERROR_OPTION_ERROR;
+ }
+}
+
+int mpv_set_option_string(mpv_handle *ctx, const char *name, const char *data)
+{
+ return mpv_set_option(ctx, name, MPV_FORMAT_STRING, &data);
+}
+
+// Run a command in the playback thread.
+static void run_locked(mpv_handle *ctx, void (*fn)(void *fn_data), void *fn_data)
+{
+ mp_dispatch_lock(ctx->mpctx->dispatch);
+ fn(fn_data);
+ mp_dispatch_unlock(ctx->mpctx->dispatch);
+}
+
+// Run a command asynchronously. It's the responsibility of the caller to
+// actually send the reply. This helper merely saves a small part of the
+// required boilerplate to do so.
+// fn: callback to execute the request
+// fn_data: opaque caller-defined argument for fn. This will be automatically
+// freed with talloc_free(fn_data).
+static int run_async(mpv_handle *ctx, void (*fn)(void *fn_data), void *fn_data)
+{
+ int err = reserve_reply(ctx);
+ if (err < 0) {
+ talloc_free(fn_data);
+ return err;
+ }
+ mp_dispatch_enqueue(ctx->mpctx->dispatch, fn, fn_data);
+ return 0;
+}
+
+struct cmd_request {
+ struct MPContext *mpctx;
+ struct mp_cmd *cmd;
+ int status;
+ struct mpv_node *res;
+ struct mp_waiter completion;
+};
+
+static void cmd_complete(struct mp_cmd_ctx *cmd)
+{
+ struct cmd_request *req = cmd->on_completion_priv;
+
+ req->status = cmd->success ? 0 : MPV_ERROR_COMMAND;
+ if (req->res) {
+ *req->res = cmd->result;
+ cmd->result = (mpv_node){0};
+ }
+
+ // Unblock the waiting thread (especially for async commands).
+ mp_waiter_wakeup(&req->completion, 0);
+}
+
+static int run_client_command(mpv_handle *ctx, struct mp_cmd *cmd, mpv_node *res)
+{
+ if (!cmd)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (!ctx->mpctx->initialized) {
+ talloc_free(cmd);
+ return MPV_ERROR_UNINITIALIZED;
+ }
+
+ cmd->sender = ctx->name;
+
+ struct cmd_request req = {
+ .mpctx = ctx->mpctx,
+ .cmd = cmd,
+ .res = res,
+ .completion = MP_WAITER_INITIALIZER,
+ };
+
+ bool async = cmd->flags & MP_ASYNC_CMD;
+
+ lock_core(ctx);
+ if (async) {
+ run_command(ctx->mpctx, cmd, NULL, NULL, NULL);
+ } else {
+ struct mp_abort_entry *abort = NULL;
+ if (cmd->def->can_abort) {
+ abort = talloc_zero(NULL, struct mp_abort_entry);
+ abort->client = ctx;
+ }
+ run_command(ctx->mpctx, cmd, abort, cmd_complete, &req);
+ }
+ unlock_core(ctx);
+
+ if (!async)
+ mp_waiter_wait(&req.completion);
+
+ return req.status;
+}
+
+int mpv_command(mpv_handle *ctx, const char **args)
+{
+ return run_client_command(ctx, mp_input_parse_cmd_strv(ctx->log, args), NULL);
+}
+
+int mpv_command_node(mpv_handle *ctx, mpv_node *args, mpv_node *result)
+{
+ struct mpv_node rn = {.format = MPV_FORMAT_NONE};
+ int r = run_client_command(ctx, mp_input_parse_cmd_node(ctx->log, args), &rn);
+ if (result && r >= 0)
+ *result = rn;
+ return r;
+}
+
+int mpv_command_ret(mpv_handle *ctx, const char **args, mpv_node *result)
+{
+ struct mpv_node rn = {.format = MPV_FORMAT_NONE};
+ int r = run_client_command(ctx, mp_input_parse_cmd_strv(ctx->log, args), &rn);
+ if (result && r >= 0)
+ *result = rn;
+ return r;
+}
+
+int mpv_command_string(mpv_handle *ctx, const char *args)
+{
+ return run_client_command(ctx,
+ mp_input_parse_cmd(ctx->mpctx->input, bstr0((char*)args), ctx->name), NULL);
+}
+
+struct async_cmd_request {
+ struct MPContext *mpctx;
+ struct mp_cmd *cmd;
+ struct mpv_handle *reply_ctx;
+ uint64_t userdata;
+};
+
+static void async_cmd_complete(struct mp_cmd_ctx *cmd)
+{
+ struct async_cmd_request *req = cmd->on_completion_priv;
+
+ struct mpv_event_command *data = talloc_zero(NULL, struct mpv_event_command);
+ data->result = cmd->result;
+ cmd->result = (mpv_node){0};
+ talloc_steal(data, node_get_alloc(&data->result));
+
+ struct mpv_event reply = {
+ .event_id = MPV_EVENT_COMMAND_REPLY,
+ .data = data,
+ .error = cmd->success ? 0 : MPV_ERROR_COMMAND,
+ };
+ send_reply(req->reply_ctx, req->userdata, &reply);
+
+ talloc_free(req);
+}
+
+static void async_cmd_fn(void *data)
+{
+ struct async_cmd_request *req = data;
+
+ struct mp_cmd *cmd = req->cmd;
+ ta_set_parent(cmd, NULL);
+ req->cmd = NULL;
+
+ struct mp_abort_entry *abort = NULL;
+ if (cmd->def->can_abort) {
+ abort = talloc_zero(NULL, struct mp_abort_entry);
+ abort->client = req->reply_ctx;
+ abort->client_work_type = MPV_EVENT_COMMAND_REPLY;
+ abort->client_work_id = req->userdata;
+ }
+
+ // This will synchronously or asynchronously call cmd_complete (depending
+ // on the command).
+ run_command(req->mpctx, cmd, abort, async_cmd_complete, req);
+}
+
+static int run_async_cmd(mpv_handle *ctx, uint64_t ud, struct mp_cmd *cmd)
+{
+ if (!cmd)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (!ctx->mpctx->initialized) {
+ talloc_free(cmd);
+ return MPV_ERROR_UNINITIALIZED;
+ }
+
+ cmd->sender = ctx->name;
+
+ struct async_cmd_request *req = talloc_ptrtype(NULL, req);
+ *req = (struct async_cmd_request){
+ .mpctx = ctx->mpctx,
+ .cmd = talloc_steal(req, cmd),
+ .reply_ctx = ctx,
+ .userdata = ud,
+ };
+ return run_async(ctx, async_cmd_fn, req);
+}
+
+int mpv_command_async(mpv_handle *ctx, uint64_t ud, const char **args)
+{
+ return run_async_cmd(ctx, ud, mp_input_parse_cmd_strv(ctx->log, args));
+}
+
+int mpv_command_node_async(mpv_handle *ctx, uint64_t ud, mpv_node *args)
+{
+ return run_async_cmd(ctx, ud, mp_input_parse_cmd_node(ctx->log, args));
+}
+
+void mpv_abort_async_command(mpv_handle *ctx, uint64_t reply_userdata)
+{
+ abort_async(ctx->mpctx, ctx, MPV_EVENT_COMMAND_REPLY, reply_userdata);
+}
+
+static int translate_property_error(int errc)
+{
+ switch (errc) {
+ case M_PROPERTY_OK: return 0;
+ case M_PROPERTY_ERROR: return MPV_ERROR_PROPERTY_ERROR;
+ case M_PROPERTY_UNAVAILABLE: return MPV_ERROR_PROPERTY_UNAVAILABLE;
+ case M_PROPERTY_NOT_IMPLEMENTED: return MPV_ERROR_PROPERTY_ERROR;
+ case M_PROPERTY_UNKNOWN: return MPV_ERROR_PROPERTY_NOT_FOUND;
+ case M_PROPERTY_INVALID_FORMAT: return MPV_ERROR_PROPERTY_FORMAT;
+ // shouldn't happen
+ default: return MPV_ERROR_PROPERTY_ERROR;
+ }
+}
+
+struct setproperty_request {
+ struct MPContext *mpctx;
+ const char *name;
+ int format;
+ void *data;
+ int status;
+ struct mpv_handle *reply_ctx;
+ uint64_t userdata;
+};
+
+static void setproperty_fn(void *arg)
+{
+ struct setproperty_request *req = arg;
+ const struct m_option *type = get_mp_type(req->format);
+
+ struct mpv_node *node;
+ struct mpv_node tmp;
+ if (req->format == MPV_FORMAT_NODE) {
+ node = req->data;
+ } else {
+ tmp.format = req->format;
+ memcpy(&tmp.u, req->data, type->type->size);
+ node = &tmp;
+ }
+
+ int err = mp_property_do(req->name, M_PROPERTY_SET_NODE, node, req->mpctx);
+
+ req->status = translate_property_error(err);
+
+ if (req->reply_ctx) {
+ struct mpv_event reply = {
+ .event_id = MPV_EVENT_SET_PROPERTY_REPLY,
+ .error = req->status,
+ };
+ send_reply(req->reply_ctx, req->userdata, &reply);
+ talloc_free(req);
+ }
+}
+
+int mpv_set_property(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data)
+{
+ if (!ctx->mpctx->initialized) {
+ int r = mpv_set_option(ctx, name, format, data);
+ if (r == MPV_ERROR_OPTION_NOT_FOUND &&
+ mp_get_property_id(ctx->mpctx, name) >= 0)
+ return MPV_ERROR_PROPERTY_UNAVAILABLE;
+ switch (r) {
+ case MPV_ERROR_SUCCESS: return MPV_ERROR_SUCCESS;
+ case MPV_ERROR_OPTION_FORMAT: return MPV_ERROR_PROPERTY_FORMAT;
+ case MPV_ERROR_OPTION_NOT_FOUND: return MPV_ERROR_PROPERTY_NOT_FOUND;
+ default: return MPV_ERROR_PROPERTY_ERROR;
+ }
+ }
+ if (!get_mp_type(format))
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct setproperty_request req = {
+ .mpctx = ctx->mpctx,
+ .name = name,
+ .format = format,
+ .data = data,
+ };
+ run_locked(ctx, setproperty_fn, &req);
+ return req.status;
+}
+
+int mpv_del_property(mpv_handle *ctx, const char *name)
+{
+ const char* args[] = { "del", name, NULL };
+ return mpv_command(ctx, args);
+}
+
+int mpv_set_property_string(mpv_handle *ctx, const char *name, const char *data)
+{
+ return mpv_set_property(ctx, name, MPV_FORMAT_STRING, &data);
+}
+
+static void free_prop_set_req(void *ptr)
+{
+ struct setproperty_request *req = ptr;
+ const struct m_option *type = get_mp_type(req->format);
+ m_option_free(type, req->data);
+}
+
+int mpv_set_property_async(mpv_handle *ctx, uint64_t ud, const char *name,
+ mpv_format format, void *data)
+{
+ const struct m_option *type = get_mp_type(format);
+ if (!ctx->mpctx->initialized)
+ return MPV_ERROR_UNINITIALIZED;
+ if (!type)
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct setproperty_request *req = talloc_ptrtype(NULL, req);
+ *req = (struct setproperty_request){
+ .mpctx = ctx->mpctx,
+ .name = talloc_strdup(req, name),
+ .format = format,
+ .data = talloc_zero_size(req, type->type->size),
+ .reply_ctx = ctx,
+ .userdata = ud,
+ };
+
+ m_option_copy(type, req->data, data);
+ talloc_set_destructor(req, free_prop_set_req);
+
+ return run_async(ctx, setproperty_fn, req);
+}
+
+struct getproperty_request {
+ struct MPContext *mpctx;
+ const char *name;
+ mpv_format format;
+ void *data;
+ int status;
+ struct mpv_handle *reply_ctx;
+ uint64_t userdata;
+};
+
+static void free_prop_data(void *ptr)
+{
+ struct mpv_event_property *prop = ptr;
+ const struct m_option *type = get_mp_type_get(prop->format);
+ m_option_free(type, prop->data);
+}
+
+static void getproperty_fn(void *arg)
+{
+ struct getproperty_request *req = arg;
+ const struct m_option *type = get_mp_type_get(req->format);
+
+ union m_option_value xdata = m_option_value_default;
+ void *data = req->data ? req->data : &xdata;
+
+ int err = -1;
+ switch (req->format) {
+ case MPV_FORMAT_OSD_STRING:
+ err = mp_property_do(req->name, M_PROPERTY_PRINT, data, req->mpctx);
+ break;
+ case MPV_FORMAT_STRING: {
+ char *s = NULL;
+ err = mp_property_do(req->name, M_PROPERTY_GET_STRING, &s, req->mpctx);
+ if (err == M_PROPERTY_OK)
+ *(char **)data = s;
+ break;
+ }
+ case MPV_FORMAT_NODE:
+ case MPV_FORMAT_FLAG:
+ case MPV_FORMAT_INT64:
+ case MPV_FORMAT_DOUBLE: {
+ struct mpv_node node = {{0}};
+ err = mp_property_do(req->name, M_PROPERTY_GET_NODE, &node, req->mpctx);
+ if (err == M_PROPERTY_NOT_IMPLEMENTED) {
+ // Go through explicit string conversion. Same reasoning as on the
+ // GET code path.
+ char *s = NULL;
+ err = mp_property_do(req->name, M_PROPERTY_GET_STRING, &s,
+ req->mpctx);
+ if (err != M_PROPERTY_OK)
+ break;
+ node.format = MPV_FORMAT_STRING;
+ node.u.string = s;
+ } else if (err <= 0)
+ break;
+ if (req->format == MPV_FORMAT_NODE) {
+ *(struct mpv_node *)data = node;
+ } else {
+ if (!conv_node_to_format(data, req->format, &node)) {
+ err = M_PROPERTY_INVALID_FORMAT;
+ mpv_free_node_contents(&node);
+ }
+ }
+ break;
+ }
+ default:
+ abort();
+ }
+
+ req->status = translate_property_error(err);
+
+ if (req->reply_ctx) {
+ struct mpv_event_property *prop = talloc_ptrtype(NULL, prop);
+ *prop = (struct mpv_event_property){
+ .name = talloc_steal(prop, (char *)req->name),
+ .format = req->format,
+ .data = talloc_size(prop, type->type->size),
+ };
+ // move data
+ memcpy(prop->data, &xdata, type->type->size);
+ talloc_set_destructor(prop, free_prop_data);
+ struct mpv_event reply = {
+ .event_id = MPV_EVENT_GET_PROPERTY_REPLY,
+ .data = prop,
+ .error = req->status,
+ };
+ send_reply(req->reply_ctx, req->userdata, &reply);
+ talloc_free(req);
+ }
+}
+
+int mpv_get_property(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data)
+{
+ if (!ctx->mpctx->initialized)
+ return MPV_ERROR_UNINITIALIZED;
+ if (!data)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (!get_mp_type_get(format))
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct getproperty_request req = {
+ .mpctx = ctx->mpctx,
+ .name = name,
+ .format = format,
+ .data = data,
+ };
+ run_locked(ctx, getproperty_fn, &req);
+ return req.status;
+}
+
+char *mpv_get_property_string(mpv_handle *ctx, const char *name)
+{
+ char *str = NULL;
+ mpv_get_property(ctx, name, MPV_FORMAT_STRING, &str);
+ return str;
+}
+
+char *mpv_get_property_osd_string(mpv_handle *ctx, const char *name)
+{
+ char *str = NULL;
+ mpv_get_property(ctx, name, MPV_FORMAT_OSD_STRING, &str);
+ return str;
+}
+
+int mpv_get_property_async(mpv_handle *ctx, uint64_t ud, const char *name,
+ mpv_format format)
+{
+ if (!ctx->mpctx->initialized)
+ return MPV_ERROR_UNINITIALIZED;
+ if (!get_mp_type_get(format))
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct getproperty_request *req = talloc_ptrtype(NULL, req);
+ *req = (struct getproperty_request){
+ .mpctx = ctx->mpctx,
+ .name = talloc_strdup(req, name),
+ .format = format,
+ .reply_ctx = ctx,
+ .userdata = ud,
+ };
+ return run_async(ctx, getproperty_fn, req);
+}
+
+static void property_free(void *p)
+{
+ struct observe_property *prop = p;
+
+ assert(prop->refcount == 0);
+
+ if (prop->type) {
+ m_option_free(prop->type, &prop->value);
+ m_option_free(prop->type, &prop->value_ret);
+ }
+}
+
+int mpv_observe_property(mpv_handle *ctx, uint64_t userdata,
+ const char *name, mpv_format format)
+{
+ const struct m_option *type = get_mp_type_get(format);
+ if (format != MPV_FORMAT_NONE && !type)
+ return MPV_ERROR_PROPERTY_FORMAT;
+ // Explicitly disallow this, because it would require a special code path.
+ if (format == MPV_FORMAT_OSD_STRING)
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ mp_mutex_lock(&ctx->lock);
+ assert(!ctx->destroying);
+ struct observe_property *prop = talloc_ptrtype(ctx, prop);
+ talloc_set_destructor(prop, property_free);
+ *prop = (struct observe_property){
+ .owner = ctx,
+ .name = talloc_strdup(prop, name),
+ .id = mp_get_property_id(ctx->mpctx, name),
+ .event_mask = mp_get_property_event_mask(name),
+ .reply_id = userdata,
+ .format = format,
+ .type = type,
+ .change_ts = 1, // force initial event
+ .refcount = 1,
+ .value = m_option_value_default,
+ .value_ret = m_option_value_default,
+ };
+ ctx->properties_change_ts += 1;
+ MP_TARRAY_APPEND(ctx, ctx->properties, ctx->num_properties, prop);
+ ctx->property_event_masks |= prop->event_mask;
+ ctx->new_property_events = true;
+ ctx->cur_property_index = 0;
+ ctx->has_pending_properties = true;
+ mp_mutex_unlock(&ctx->lock);
+ mp_wakeup_core(ctx->mpctx);
+ return 0;
+}
+
+int mpv_unobserve_property(mpv_handle *ctx, uint64_t userdata)
+{
+ mp_mutex_lock(&ctx->lock);
+ int count = 0;
+ for (int n = ctx->num_properties - 1; n >= 0; n--) {
+ struct observe_property *prop = ctx->properties[n];
+ // Perform actual removal of the property lazily to avoid creating
+ // dangling pointers and such.
+ if (prop->reply_id == userdata) {
+ prop_unref(prop);
+ ctx->properties_change_ts += 1;
+ MP_TARRAY_REMOVE_AT(ctx->properties, ctx->num_properties, n);
+ ctx->cur_property_index = 0;
+ count++;
+ }
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return count;
+}
+
+static bool property_shared_prefix(const char *a0, const char *b0)
+{
+ bstr a = bstr0(a0);
+ bstr b = bstr0(b0);
+
+ // Treat options and properties as equivalent.
+ bstr_eatstart0(&a, "options/");
+ bstr_eatstart0(&b, "options/");
+
+ // Compare the potentially-common portion
+ if (memcmp(a.start, b.start, MPMIN(a.len, b.len)))
+ return false;
+
+ // If lengths were equal, we're done
+ if (a.len == b.len)
+ return true;
+
+ // Check for a slash in the first non-common byte of the longer string
+ if (a.len > b.len)
+ return a.start[b.len] == '/';
+ else
+ return b.start[a.len] == '/';
+}
+
+// Broadcast that a property has changed.
+void mp_client_property_change(struct MPContext *mpctx, const char *name)
+{
+ struct mp_client_api *clients = mpctx->clients;
+ int id = mp_get_property_id(mpctx, name);
+ bool any_pending = false;
+
+ mp_mutex_lock(&clients->lock);
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ struct mpv_handle *client = clients->clients[n];
+ mp_mutex_lock(&client->lock);
+ for (int i = 0; i < client->num_properties; i++) {
+ if (client->properties[i]->id == id &&
+ property_shared_prefix(name, client->properties[i]->name)) {
+ client->properties[i]->change_ts += 1;
+ client->has_pending_properties = true;
+ any_pending = true;
+ }
+ }
+ mp_mutex_unlock(&client->lock);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+
+ // If we're inside mp_dispatch_queue_process(), this will cause the playloop
+ // to be re-run (to get mp_client_send_property_changes() called). If we're
+ // inside the normal playloop, this does nothing, but the latter function
+ // will be called at the end of the playloop anyway.
+ if (any_pending)
+ mp_dispatch_adjust_timeout(mpctx->dispatch, 0);
+}
+
+// Mark properties as changed in reaction to specific events.
+// Called with ctx->lock held.
+static void notify_property_events(struct mpv_handle *ctx, int event)
+{
+ uint64_t mask = 1ULL << event;
+ for (int i = 0; i < ctx->num_properties; i++) {
+ if (ctx->properties[i]->event_mask & mask) {
+ ctx->properties[i]->change_ts += 1;
+ ctx->has_pending_properties = true;
+ }
+ }
+
+ // Same as in mp_client_property_change().
+ if (ctx->has_pending_properties)
+ mp_dispatch_adjust_timeout(ctx->mpctx->dispatch, 0);
+}
+
+// Call with ctx->lock held (only). May temporarily drop the lock.
+static void send_client_property_changes(struct mpv_handle *ctx)
+{
+ uint64_t cur_ts = ctx->properties_change_ts;
+
+ ctx->has_pending_properties = false;
+
+ for (int n = 0; n < ctx->num_properties; n++) {
+ struct observe_property *prop = ctx->properties[n];
+
+ if (prop->value_ts == prop->change_ts)
+ continue;
+
+ bool changed = false;
+ if (prop->format) {
+ const struct m_option *type = prop->type;
+ union m_option_value val = m_option_value_default;
+ struct getproperty_request req = {
+ .mpctx = ctx->mpctx,
+ .name = prop->name,
+ .format = prop->format,
+ .data = &val,
+ };
+
+ // Temporarily unlock and read the property. The very important
+ // thing is that property getters can do whatever they want, _and_
+ // that they may wait on the client API user thread (if vo_libmpv
+ // or similar things are involved).
+ prop->refcount += 1; // keep prop alive (esp. prop->name)
+ ctx->async_counter += 1; // keep ctx alive
+ mp_mutex_unlock(&ctx->lock);
+ getproperty_fn(&req);
+ mp_mutex_lock(&ctx->lock);
+ ctx->async_counter -= 1;
+ prop_unref(prop);
+
+ // Set if observed properties was changed or something similar
+ // => start over, retry next time.
+ if (cur_ts != ctx->properties_change_ts || ctx->destroying) {
+ m_option_free(type, &val);
+ mp_wakeup_core(ctx->mpctx);
+ ctx->has_pending_properties = true;
+ break;
+ }
+ assert(prop->refcount > 0);
+
+ bool val_valid = req.status >= 0;
+ changed = prop->value_valid != val_valid;
+ if (prop->value_valid && val_valid)
+ changed = !equal_mpv_value(&prop->value, &val, prop->format);
+ if (prop->value_ts == 0)
+ changed = true; // initial event
+
+ prop->value_valid = val_valid;
+ if (changed && val_valid) {
+ // move val to prop->value
+ m_option_free(type, &prop->value);
+ memcpy(&prop->value, &val, type->type->size);
+ memset(&val, 0, type->type->size);
+ }
+
+ m_option_free(prop->type, &val);
+ } else {
+ changed = true;
+ }
+
+ if (prop->waiting_for_hook)
+ ctx->new_property_events = true; // make sure to wakeup
+
+ // Avoid retriggering the change event if the property didn't change,
+ // and the previous value was actually returned to the client.
+ if (!changed && prop->value_ret_ts == prop->value_ts) {
+ prop->value_ret_ts = prop->change_ts; // no change => no event
+ prop->waiting_for_hook = false;
+ } else {
+ ctx->new_property_events = true;
+ }
+
+ prop->value_ts = prop->change_ts;
+ }
+
+ if (ctx->destroying || ctx->new_property_events)
+ wakeup_client(ctx);
+}
+
+void mp_client_send_property_changes(struct MPContext *mpctx)
+{
+ struct mp_client_api *clients = mpctx->clients;
+
+ mp_mutex_lock(&clients->lock);
+ uint64_t cur_ts = clients->clients_list_change_ts;
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ struct mpv_handle *ctx = clients->clients[n];
+
+ mp_mutex_lock(&ctx->lock);
+ if (!ctx->has_pending_properties || ctx->destroying) {
+ mp_mutex_unlock(&ctx->lock);
+ continue;
+ }
+ // Keep ctx->lock locked (unlock order does not matter).
+ mp_mutex_unlock(&clients->lock);
+ send_client_property_changes(ctx);
+ mp_mutex_unlock(&ctx->lock);
+ mp_mutex_lock(&clients->lock);
+ if (cur_ts != clients->clients_list_change_ts) {
+ // List changed; need to start over. Do it in the next iteration.
+ mp_wakeup_core(mpctx);
+ break;
+ }
+ }
+
+ mp_mutex_unlock(&clients->lock);
+}
+
+// Set ctx->cur_event to a generated property change event, if there is any
+// outstanding property.
+static bool gen_property_change_event(struct mpv_handle *ctx)
+{
+ if (!ctx->mpctx->initialized)
+ return false;
+
+ while (1) {
+ if (ctx->cur_property_index >= ctx->num_properties) {
+ ctx->new_property_events &= ctx->num_properties > 0;
+ if (!ctx->new_property_events)
+ break;
+ ctx->new_property_events = false;
+ ctx->cur_property_index = 0;
+ }
+
+ struct observe_property *prop = ctx->properties[ctx->cur_property_index++];
+
+ if (prop->value_ts == prop->change_ts && // not a stale value?
+ prop->value_ret_ts != prop->value_ts) // other value than last time?
+ {
+ prop->value_ret_ts = prop->value_ts;
+ prop->waiting_for_hook = false;
+ prop_unref(ctx->cur_property);
+ ctx->cur_property = prop;
+ prop->refcount += 1;
+
+ if (prop->value_valid)
+ m_option_copy(prop->type, &prop->value_ret, &prop->value);
+
+ ctx->cur_property_event = (struct mpv_event_property){
+ .name = prop->name,
+ .format = prop->value_valid ? prop->format : 0,
+ .data = prop->value_valid ? &prop->value_ret : NULL,
+ };
+ *ctx->cur_event = (struct mpv_event){
+ .event_id = MPV_EVENT_PROPERTY_CHANGE,
+ .reply_userdata = prop->reply_id,
+ .data = &ctx->cur_property_event,
+ };
+ return true;
+ }
+ }
+
+ return false;
+}
+
+int mpv_hook_add(mpv_handle *ctx, uint64_t reply_userdata,
+ const char *name, int priority)
+{
+ lock_core(ctx);
+ mp_hook_add(ctx->mpctx, ctx->name, ctx->id, name, reply_userdata, priority);
+ unlock_core(ctx);
+ return 0;
+}
+
+int mpv_hook_continue(mpv_handle *ctx, uint64_t id)
+{
+ lock_core(ctx);
+ int r = mp_hook_continue(ctx->mpctx, ctx->id, id);
+ unlock_core(ctx);
+ return r;
+}
+
+int mpv_load_config_file(mpv_handle *ctx, const char *filename)
+{
+ lock_core(ctx);
+ int r = m_config_parse_config_file(ctx->mpctx->mconfig, ctx->mpctx->global, filename, NULL, 0);
+ unlock_core(ctx);
+ if (r == 0)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (r < 0)
+ return MPV_ERROR_OPTION_ERROR;
+ return 0;
+}
+
+static void msg_wakeup(void *p)
+{
+ mpv_handle *ctx = p;
+ wakeup_client(ctx);
+}
+
+// Undocumented: if min_level starts with "silent:", then log messages are not
+// returned to the API user, but are stored until logging is enabled normally
+// again by calling this without "silent:". (Using a different level will
+// flush it, though.)
+int mpv_request_log_messages(mpv_handle *ctx, const char *min_level)
+{
+ bstr blevel = bstr0(min_level);
+ bool silent = bstr_eatstart0(&blevel, "silent:");
+
+ int level = -1;
+ for (int n = 0; n < MSGL_MAX + 1; n++) {
+ if (mp_log_levels[n] && bstr_equals0(blevel, mp_log_levels[n])) {
+ level = n;
+ break;
+ }
+ }
+ if (bstr_equals0(blevel, "terminal-default"))
+ level = MP_LOG_BUFFER_MSGL_TERM;
+
+ if (level < 0 && strcmp(min_level, "no") != 0)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ mp_mutex_lock(&ctx->lock);
+ if (level < 0 || level != ctx->messages_level) {
+ mp_msg_log_buffer_destroy(ctx->messages);
+ ctx->messages = NULL;
+ }
+ if (level >= 0) {
+ if (!ctx->messages) {
+ int size = level >= MSGL_V ? 10000 : 1000;
+ ctx->messages = mp_msg_log_buffer_new(ctx->mpctx->global, size,
+ level, msg_wakeup, ctx);
+ ctx->messages_level = level;
+ }
+ mp_msg_log_buffer_set_silent(ctx->messages, silent);
+ }
+ wakeup_client(ctx);
+ mp_mutex_unlock(&ctx->lock);
+ return 0;
+}
+
+// Set ctx->cur_event to a generated log message event, if any available.
+static bool gen_log_message_event(struct mpv_handle *ctx)
+{
+ if (ctx->messages) {
+ struct mp_log_buffer_entry *msg =
+ mp_msg_log_buffer_read(ctx->messages);
+ if (msg) {
+ struct mpv_event_log_message *cmsg =
+ talloc_ptrtype(ctx->cur_event, cmsg);
+ talloc_steal(cmsg, msg);
+ *cmsg = (struct mpv_event_log_message){
+ .prefix = msg->prefix,
+ .level = mp_log_levels[msg->level],
+ .log_level = mp_mpv_log_levels[msg->level],
+ .text = msg->text,
+ };
+ *ctx->cur_event = (struct mpv_event){
+ .event_id = MPV_EVENT_LOG_MESSAGE,
+ .data = cmsg,
+ };
+ return true;
+ }
+ }
+ return false;
+}
+
+int mpv_get_wakeup_pipe(mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->wakeup_lock);
+ if (ctx->wakeup_pipe[0] == -1) {
+ if (mp_make_wakeup_pipe(ctx->wakeup_pipe) >= 0)
+ (void)write(ctx->wakeup_pipe[1], &(char){0}, 1);
+ }
+ int fd = ctx->wakeup_pipe[0];
+ mp_mutex_unlock(&ctx->wakeup_lock);
+ return fd;
+}
+
+unsigned long mpv_client_api_version(void)
+{
+ return MPV_CLIENT_API_VERSION;
+}
+
+int mpv_event_to_node(mpv_node *dst, mpv_event *event)
+{
+ *dst = (mpv_node){0};
+
+ node_init(dst, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_string(dst, "event", mpv_event_name(event->event_id));
+
+ if (event->error < 0)
+ node_map_add_string(dst, "error", mpv_error_string(event->error));
+
+ if (event->reply_userdata)
+ node_map_add_int64(dst, "id", event->reply_userdata);
+
+ switch (event->event_id) {
+
+ case MPV_EVENT_START_FILE: {
+ mpv_event_start_file *esf = event->data;
+
+ node_map_add_int64(dst, "playlist_entry_id", esf->playlist_entry_id);
+ break;
+ }
+
+ case MPV_EVENT_END_FILE: {
+ mpv_event_end_file *eef = event->data;
+
+ const char *reason;
+ switch (eef->reason) {
+ case MPV_END_FILE_REASON_EOF: reason = "eof"; break;
+ case MPV_END_FILE_REASON_STOP: reason = "stop"; break;
+ case MPV_END_FILE_REASON_QUIT: reason = "quit"; break;
+ case MPV_END_FILE_REASON_ERROR: reason = "error"; break;
+ case MPV_END_FILE_REASON_REDIRECT: reason = "redirect"; break;
+ default:
+ reason = "unknown";
+ }
+ node_map_add_string(dst, "reason", reason);
+
+ node_map_add_int64(dst, "playlist_entry_id", eef->playlist_entry_id);
+
+ if (eef->playlist_insert_id) {
+ node_map_add_int64(dst, "playlist_insert_id", eef->playlist_insert_id);
+ node_map_add_int64(dst, "playlist_insert_num_entries",
+ eef->playlist_insert_num_entries);
+ }
+
+ if (eef->reason == MPV_END_FILE_REASON_ERROR)
+ node_map_add_string(dst, "file_error", mpv_error_string(eef->error));
+ break;
+ }
+
+ case MPV_EVENT_LOG_MESSAGE: {
+ mpv_event_log_message *msg = event->data;
+
+ node_map_add_string(dst, "prefix", msg->prefix);
+ node_map_add_string(dst, "level", msg->level);
+ node_map_add_string(dst, "text", msg->text);
+ break;
+ }
+
+ case MPV_EVENT_CLIENT_MESSAGE: {
+ mpv_event_client_message *msg = event->data;
+
+ struct mpv_node *args = node_map_add(dst, "args", MPV_FORMAT_NODE_ARRAY);
+ for (int n = 0; n < msg->num_args; n++) {
+ struct mpv_node *sn = node_array_add(args, MPV_FORMAT_NONE);
+ sn->format = MPV_FORMAT_STRING;
+ sn->u.string = (char *)msg->args[n];
+ }
+ break;
+ }
+
+ case MPV_EVENT_PROPERTY_CHANGE: {
+ mpv_event_property *prop = event->data;
+
+ node_map_add_string(dst, "name", prop->name);
+
+ switch (prop->format) {
+ case MPV_FORMAT_NODE:
+ *node_map_add(dst, "data", MPV_FORMAT_NONE) =
+ *(struct mpv_node *)prop->data;
+ break;
+ case MPV_FORMAT_DOUBLE:
+ node_map_add_double(dst, "data", *(double *)prop->data);
+ break;
+ case MPV_FORMAT_FLAG:
+ node_map_add_flag(dst, "data", *(int *)prop->data);
+ break;
+ case MPV_FORMAT_STRING:
+ node_map_add_string(dst, "data", *(char **)prop->data);
+ break;
+ default: ;
+ }
+ break;
+ }
+
+ case MPV_EVENT_COMMAND_REPLY: {
+ mpv_event_command *cmd = event->data;
+
+ *node_map_add(dst, "result", MPV_FORMAT_NONE) = cmd->result;
+ break;
+ }
+
+ case MPV_EVENT_HOOK: {
+ mpv_event_hook *hook = event->data;
+
+ node_map_add_int64(dst, "hook_id", hook->id);
+ break;
+ }
+
+ }
+ return 0;
+}
+
+static const char *const err_table[] = {
+ [-MPV_ERROR_SUCCESS] = "success",
+ [-MPV_ERROR_EVENT_QUEUE_FULL] = "event queue full",
+ [-MPV_ERROR_NOMEM] = "memory allocation failed",
+ [-MPV_ERROR_UNINITIALIZED] = "core not uninitialized",
+ [-MPV_ERROR_INVALID_PARAMETER] = "invalid parameter",
+ [-MPV_ERROR_OPTION_NOT_FOUND] = "option not found",
+ [-MPV_ERROR_OPTION_FORMAT] = "unsupported format for accessing option",
+ [-MPV_ERROR_OPTION_ERROR] = "error setting option",
+ [-MPV_ERROR_PROPERTY_NOT_FOUND] = "property not found",
+ [-MPV_ERROR_PROPERTY_FORMAT] = "unsupported format for accessing property",
+ [-MPV_ERROR_PROPERTY_UNAVAILABLE] = "property unavailable",
+ [-MPV_ERROR_PROPERTY_ERROR] = "error accessing property",
+ [-MPV_ERROR_COMMAND] = "error running command",
+ [-MPV_ERROR_LOADING_FAILED] = "loading failed",
+ [-MPV_ERROR_AO_INIT_FAILED] = "audio output initialization failed",
+ [-MPV_ERROR_VO_INIT_FAILED] = "video output initialization failed",
+ [-MPV_ERROR_NOTHING_TO_PLAY] = "no audio or video data played",
+ [-MPV_ERROR_UNKNOWN_FORMAT] = "unrecognized file format",
+ [-MPV_ERROR_UNSUPPORTED] = "not supported",
+ [-MPV_ERROR_NOT_IMPLEMENTED] = "operation not implemented",
+ [-MPV_ERROR_GENERIC] = "something happened",
+};
+
+const char *mpv_error_string(int error)
+{
+ error = -error;
+ if (error < 0)
+ error = 0;
+ const char *name = NULL;
+ if (error < MP_ARRAY_SIZE(err_table))
+ name = err_table[error];
+ return name ? name : "unknown error";
+}
+
+static const char *const event_table[] = {
+ [MPV_EVENT_NONE] = "none",
+ [MPV_EVENT_SHUTDOWN] = "shutdown",
+ [MPV_EVENT_LOG_MESSAGE] = "log-message",
+ [MPV_EVENT_GET_PROPERTY_REPLY] = "get-property-reply",
+ [MPV_EVENT_SET_PROPERTY_REPLY] = "set-property-reply",
+ [MPV_EVENT_COMMAND_REPLY] = "command-reply",
+ [MPV_EVENT_START_FILE] = "start-file",
+ [MPV_EVENT_END_FILE] = "end-file",
+ [MPV_EVENT_FILE_LOADED] = "file-loaded",
+ [MPV_EVENT_IDLE] = "idle",
+ [MPV_EVENT_TICK] = "tick",
+ [MPV_EVENT_CLIENT_MESSAGE] = "client-message",
+ [MPV_EVENT_VIDEO_RECONFIG] = "video-reconfig",
+ [MPV_EVENT_AUDIO_RECONFIG] = "audio-reconfig",
+ [MPV_EVENT_SEEK] = "seek",
+ [MPV_EVENT_PLAYBACK_RESTART] = "playback-restart",
+ [MPV_EVENT_PROPERTY_CHANGE] = "property-change",
+ [MPV_EVENT_QUEUE_OVERFLOW] = "event-queue-overflow",
+ [MPV_EVENT_HOOK] = "hook",
+};
+
+const char *mpv_event_name(mpv_event_id event)
+{
+ if ((unsigned)event >= MP_ARRAY_SIZE(event_table))
+ return NULL;
+ return event_table[event];
+}
+
+void mpv_free(void *data)
+{
+ talloc_free(data);
+}
+
+int64_t mpv_get_time_ns(mpv_handle *ctx)
+{
+ return mp_time_ns();
+}
+
+int64_t mpv_get_time_us(mpv_handle *ctx)
+{
+ return mp_time_ns() / 1000;
+}
+
+#include "video/out/libmpv.h"
+
+static void do_kill(void *ptr)
+{
+ struct MPContext *mpctx = ptr;
+
+ struct track *track = mpctx->vo_chain ? mpctx->vo_chain->track : NULL;
+ uninit_video_out(mpctx);
+ if (track) {
+ mpctx->error_playing = MPV_ERROR_VO_INIT_FAILED;
+ error_on_track(mpctx, track);
+ }
+}
+
+// Used by vo_libmpv to (a)synchronously uninitialize video.
+void kill_video_async(struct mp_client_api *client_api)
+{
+ struct MPContext *mpctx = client_api->mpctx;
+ mp_dispatch_enqueue(mpctx->dispatch, do_kill, mpctx);
+}
+
+// Used by vo_libmpv to set the current render context.
+bool mp_set_main_render_context(struct mp_client_api *client_api,
+ struct mpv_render_context *ctx, bool active)
+{
+ assert(ctx);
+
+ mp_mutex_lock(&client_api->lock);
+ bool is_set = !!client_api->render_context;
+ bool is_same = client_api->render_context == ctx;
+ // Can set if it doesn't remove another existing ctx.
+ bool res = is_same || !is_set;
+ if (res)
+ client_api->render_context = active ? ctx : NULL;
+ mp_mutex_unlock(&client_api->lock);
+ return res;
+}
+
+// Used by vo_libmpv. Relies on guarantees by mp_render_context_acquire().
+struct mpv_render_context *
+mp_client_api_acquire_render_context(struct mp_client_api *ca)
+{
+ struct mpv_render_context *res = NULL;
+ mp_mutex_lock(&ca->lock);
+ if (ca->render_context && mp_render_context_acquire(ca->render_context))
+ res = ca->render_context;
+ mp_mutex_unlock(&ca->lock);
+ return res;
+}
+
+// stream_cb
+
+struct mp_custom_protocol {
+ char *protocol;
+ void *user_data;
+ mpv_stream_cb_open_ro_fn open_fn;
+};
+
+int mpv_stream_cb_add_ro(mpv_handle *ctx, const char *protocol, void *user_data,
+ mpv_stream_cb_open_ro_fn open_fn)
+{
+ if (!open_fn)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ struct mp_client_api *clients = ctx->clients;
+ int r = 0;
+ mp_mutex_lock(&clients->lock);
+ for (int n = 0; n < clients->num_custom_protocols; n++) {
+ struct mp_custom_protocol *proto = &clients->custom_protocols[n];
+ if (strcmp(proto->protocol, protocol) == 0) {
+ r = MPV_ERROR_INVALID_PARAMETER;
+ break;
+ }
+ }
+ if (stream_has_proto(protocol))
+ r = MPV_ERROR_INVALID_PARAMETER;
+ if (r >= 0) {
+ struct mp_custom_protocol proto = {
+ .protocol = talloc_strdup(clients, protocol),
+ .user_data = user_data,
+ .open_fn = open_fn,
+ };
+ MP_TARRAY_APPEND(clients, clients->custom_protocols,
+ clients->num_custom_protocols, proto);
+ }
+ mp_mutex_unlock(&clients->lock);
+ return r;
+}
+
+bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol,
+ void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn)
+{
+ struct mp_client_api *clients = g->client_api;
+ bool found = false;
+ mp_mutex_lock(&clients->lock);
+ for (int n = 0; n < clients->num_custom_protocols; n++) {
+ struct mp_custom_protocol *proto = &clients->custom_protocols[n];
+ if (strcmp(proto->protocol, protocol) == 0) {
+ *out_user_data = proto->user_data;
+ *out_fn = proto->open_fn;
+ found = true;
+ break;
+ }
+ }
+ mp_mutex_unlock(&clients->lock);
+ return found;
+}
diff --git a/player/client.h b/player/client.h
new file mode 100644
index 0000000..ddc568e
--- /dev/null
+++ b/player/client.h
@@ -0,0 +1,58 @@
+#ifndef MP_CLIENT_H_
+#define MP_CLIENT_H_
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "libmpv/client.h"
+#include "libmpv/stream_cb.h"
+#include "misc/bstr.h"
+
+struct MPContext;
+struct mpv_handle;
+struct mp_client_api;
+struct mp_log;
+struct mpv_global;
+
+// Includes space for \0
+#define MAX_CLIENT_NAME 64
+
+void mp_clients_init(struct MPContext *mpctx);
+void mp_clients_destroy(struct MPContext *mpctx);
+void mp_shutdown_clients(struct MPContext *mpctx);
+bool mp_is_shutting_down(struct MPContext *mpctx);
+bool mp_clients_all_initialized(struct MPContext *mpctx);
+
+bool mp_client_id_exists(struct MPContext *mpctx, int64_t id);
+void mp_client_broadcast_event(struct MPContext *mpctx, int event, void *data);
+int mp_client_send_event(struct MPContext *mpctx, const char *client_name,
+ uint64_t reply_userdata, int event, void *data);
+int mp_client_send_event_dup(struct MPContext *mpctx, const char *client_name,
+ int event, void *data);
+void mp_client_property_change(struct MPContext *mpctx, const char *name);
+void mp_client_send_property_changes(struct MPContext *mpctx);
+
+struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name);
+void mp_client_set_weak(struct mpv_handle *ctx);
+struct mp_log *mp_client_get_log(struct mpv_handle *ctx);
+struct mpv_global *mp_client_get_global(struct mpv_handle *ctx);
+
+void mp_client_broadcast_event_external(struct mp_client_api *api, int event,
+ void *data);
+
+// m_option.c
+void *node_get_alloc(struct mpv_node *node);
+
+// for vo_libmpv.c
+struct osd_state;
+struct mpv_render_context;
+bool mp_set_main_render_context(struct mp_client_api *client_api,
+ struct mpv_render_context *ctx, bool active);
+struct mpv_render_context *
+mp_client_api_acquire_render_context(struct mp_client_api *ca);
+void kill_video_async(struct mp_client_api *client_api);
+
+bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol,
+ void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn);
+
+#endif
diff --git a/player/command.c b/player/command.c
new file mode 100644
index 0000000..8bff0cd
--- /dev/null
+++ b/player/command.c
@@ -0,0 +1,7149 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <time.h>
+#include <math.h>
+#include <sys/types.h>
+
+#include <ass/ass.h>
+#include <libavutil/avstring.h>
+#include <libavutil/common.h>
+
+#include "mpv_talloc.h"
+#include "client.h"
+#include "external_files.h"
+#include "common/av_common.h"
+#include "common/codecs.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "filters/f_decoder_wrapper.h"
+#include "command.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "common/common.h"
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "stream/stream.h"
+#include "demux/demux.h"
+#include "demux/stheader.h"
+#include "common/playlist.h"
+#include "sub/dec_sub.h"
+#include "sub/osd.h"
+#include "sub/sd.h"
+#include "options/m_option.h"
+#include "options/m_property.h"
+#include "options/m_config_frontend.h"
+#include "osdep/getpid.h"
+#include "video/out/vo.h"
+#include "video/csputils.h"
+#include "video/hwdec.h"
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "audio/out/ao.h"
+#include "video/out/bitmap_packer.h"
+#include "options/path.h"
+#include "screenshot.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "misc/thread_pool.h"
+#include "misc/thread_tools.h"
+
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+
+#include "core.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+struct command_ctx {
+ // All properties, terminated with a {0} item.
+ struct m_property *properties;
+
+ double last_seek_time;
+ double last_seek_pts;
+ double marked_pts;
+ bool marked_permanent;
+
+ char **warned_deprecated;
+ int num_warned_deprecated;
+
+ struct overlay *overlays;
+ int num_overlays;
+ // One of these is in use by the OSD; the other one exists so that the
+ // bitmap list can be manipulated without additional synchronization.
+ struct sub_bitmaps overlay_osd[2];
+ int overlay_osd_current;
+ struct bitmap_packer *overlay_packer;
+
+ struct hook_handler **hooks;
+ int num_hooks;
+ int64_t hook_seq; // for hook_handler.seq
+
+ struct ao_hotplug *hotplug;
+
+ struct mp_cmd_ctx *cache_dump_cmd; // in progress cache dumping
+
+ char **script_props;
+ mpv_node udata;
+
+ double cached_window_scale;
+ bool shared_script_warning;
+};
+
+static const struct m_option script_props_type = {
+ .type = &m_option_type_keyvalue_list
+};
+
+static const struct m_option udata_type = {
+ .type = CONF_TYPE_NODE
+};
+
+struct overlay {
+ struct mp_image *source;
+ int x, y;
+};
+
+struct hook_handler {
+ char *client; // client mpv_handle name (for logging)
+ int64_t client_id; // client mpv_handle ID
+ char *type; // kind of hook, e.g. "on_load"
+ uint64_t user_id; // user-chosen ID
+ int priority; // priority for global hook order
+ int64_t seq; // unique ID, != 0, also for fixed order on equal priorities
+ bool active; // hook is currently in progress (only 1 at a time for now)
+};
+
+// U+279C HEAVY ROUND-TIPPED RIGHTWARDS ARROW
+// U+00A0 NO-BREAK SPACE
+#define ARROW_SP "\342\236\234\302\240"
+
+const char list_current[] = OSD_ASS_0 ARROW_SP OSD_ASS_1;
+const char list_normal[] = OSD_ASS_0 "{\\alpha&HFF}" ARROW_SP "{\\r}" OSD_ASS_1;
+
+static int edit_filters(struct MPContext *mpctx, struct mp_log *log,
+ enum stream_type mediatype,
+ const char *cmd, const char *arg);
+static int set_filters(struct MPContext *mpctx, enum stream_type mediatype,
+ struct m_obj_settings *new_chain);
+
+static bool is_property_set(int action, void *val);
+
+static void hook_remove(struct MPContext *mpctx, struct hook_handler *h)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ for (int n = 0; n < cmd->num_hooks; n++) {
+ if (cmd->hooks[n] == h) {
+ talloc_free(cmd->hooks[n]);
+ MP_TARRAY_REMOVE_AT(cmd->hooks, cmd->num_hooks, n);
+ return;
+ }
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+bool mp_hook_test_completion(struct MPContext *mpctx, char *type)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ for (int n = 0; n < cmd->num_hooks; n++) {
+ struct hook_handler *h = cmd->hooks[n];
+ if (h->active && strcmp(h->type, type) == 0) {
+ if (!mp_client_id_exists(mpctx, h->client_id)) {
+ MP_WARN(mpctx, "client removed during hook handling\n");
+ hook_remove(mpctx, h);
+ break;
+ }
+ return false;
+ }
+ }
+ return true;
+}
+
+static int invoke_hook_handler(struct MPContext *mpctx, struct hook_handler *h)
+{
+ MP_VERBOSE(mpctx, "Running hook: %s/%s\n", h->client, h->type);
+ h->active = true;
+
+ uint64_t reply_id = 0;
+ mpv_event_hook *m = talloc_ptrtype(NULL, m);
+ *m = (mpv_event_hook){
+ .name = talloc_strdup(m, h->type),
+ .id = h->seq,
+ },
+ reply_id = h->user_id;
+ char *name = mp_tprintf(22, "@%"PRIi64, h->client_id);
+ int r = mp_client_send_event(mpctx, name, reply_id, MPV_EVENT_HOOK, m);
+ if (r < 0) {
+ MP_WARN(mpctx, "Sending hook command failed. Removing hook.\n");
+ hook_remove(mpctx, h);
+ mp_wakeup_core(mpctx); // repeat next iteration to finish
+ }
+ return r;
+}
+
+static int run_next_hook_handler(struct MPContext *mpctx, char *type, int index)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ for (int n = index; n < cmd->num_hooks; n++) {
+ struct hook_handler *h = cmd->hooks[n];
+ if (strcmp(h->type, type) == 0)
+ return invoke_hook_handler(mpctx, h);
+ }
+
+ mp_wakeup_core(mpctx); // finished hook
+ return 0;
+}
+
+// Start processing script/client API hooks. This is asynchronous, and the
+// caller needs to use mp_hook_test_completion() to check whether they're done.
+void mp_hook_start(struct MPContext *mpctx, char *type)
+{
+ while (run_next_hook_handler(mpctx, type, 0) < 0) {
+ // We can repeat this until all broken clients have been removed, and
+ // hook processing is successfully started.
+ }
+}
+
+int mp_hook_continue(struct MPContext *mpctx, int64_t client_id, uint64_t id)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ for (int n = 0; n < cmd->num_hooks; n++) {
+ struct hook_handler *h = cmd->hooks[n];
+ if (h->client_id == client_id && h->seq == id) {
+ if (!h->active)
+ break;
+ h->active = false;
+ return run_next_hook_handler(mpctx, h->type, n + 1);
+ }
+ }
+
+ MP_ERR(mpctx, "invalid hook API usage\n");
+ return MPV_ERROR_INVALID_PARAMETER;
+}
+
+static int compare_hook(const void *pa, const void *pb)
+{
+ struct hook_handler **h1 = (void *)pa;
+ struct hook_handler **h2 = (void *)pb;
+ if ((*h1)->priority != (*h2)->priority)
+ return (*h1)->priority - (*h2)->priority;
+ return (*h1)->seq - (*h2)->seq;
+}
+
+void mp_hook_add(struct MPContext *mpctx, char *client, int64_t client_id,
+ const char *name, uint64_t user_id, int pri)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct hook_handler *h = talloc_ptrtype(cmd, h);
+ int64_t seq = ++cmd->hook_seq;
+ *h = (struct hook_handler){
+ .client = talloc_strdup(h, client),
+ .client_id = client_id,
+ .type = talloc_strdup(h, name),
+ .user_id = user_id,
+ .priority = pri,
+ .seq = seq,
+ };
+ MP_TARRAY_APPEND(cmd, cmd->hooks, cmd->num_hooks, h);
+ qsort(cmd->hooks, cmd->num_hooks, sizeof(cmd->hooks[0]), compare_hook);
+}
+
+// Call before a seek, in order to allow revert-seek to undo the seek.
+void mark_seek(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ double now = mp_time_sec();
+ if (now > cmd->last_seek_time + 2.0 || cmd->last_seek_pts == MP_NOPTS_VALUE)
+ cmd->last_seek_pts = get_current_time(mpctx);
+ cmd->last_seek_time = now;
+}
+
+static char *skip_n_lines(char *text, int lines)
+{
+ while (text && lines > 0) {
+ char *next = strchr(text, '\n');
+ text = next ? next + 1 : NULL;
+ lines--;
+ }
+ return text;
+}
+
+static int count_lines(char *text)
+{
+ int count = 0;
+ while (text) {
+ char *next = strchr(text, '\n');
+ if (!next || (next[0] == '\n' && !next[1]))
+ break;
+ text = next + 1;
+ count++;
+ }
+ return count;
+}
+
+// Given a huge string separated by new lines, attempts to cut off text above
+// the current line to keep the line visible, and below to keep rendering
+// performance up. pos gives the current line (0 for the first line).
+// "text" might be returned as is, or it can be freed and a new allocation is
+// returned.
+// This is only a heuristic - we can't deal with line breaking.
+static char *cut_osd_list(struct MPContext *mpctx, char *text, int pos)
+{
+ int screen_h, font_h;
+ osd_get_text_size(mpctx->osd, &screen_h, &font_h);
+ int max_lines = screen_h / MPMAX(font_h, 1) - 1;
+
+ if (!text || max_lines < 5)
+ return text;
+
+ int count = count_lines(text);
+ if (count <= max_lines)
+ return text;
+
+ char *new = talloc_strdup(NULL, "");
+
+ int start = MPMAX(pos - max_lines / 2, 0);
+ if (start == 1)
+ start = 0; // avoid weird transition when pad_h becomes visible
+ int pad_h = start > 0;
+
+ int space = max_lines - pad_h - 1;
+ int pad_t = count - start > space;
+ if (!pad_t)
+ start = count - space;
+
+ if (pad_h) {
+ new = talloc_asprintf_append_buffer(new, "\342\206\221 (%d hidden items)\n",
+ start);
+ }
+
+ char *head = skip_n_lines(text, start);
+ if (!head) {
+ talloc_free(new);
+ return text;
+ }
+
+ int lines_shown = max_lines - pad_h - pad_t;
+ char *tail = skip_n_lines(head, lines_shown);
+ new = talloc_asprintf_append_buffer(new, "%.*s",
+ (int)(tail ? tail - head : strlen(head)), head);
+ if (pad_t) {
+ new = talloc_asprintf_append_buffer(new, "\342\206\223 (%d hidden items)\n",
+ count - start - lines_shown + 1);
+ }
+
+ talloc_free(text);
+ return new;
+}
+
+static char *format_delay(double time)
+{
+ return talloc_asprintf(NULL, "%d ms", (int)lrint(time * 1000));
+}
+
+// Property-option bridge. (Maps the property to the option with the same name.)
+static int mp_property_generic_option(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct m_config_option *opt =
+ m_config_get_co(mpctx->mconfig, bstr0(prop->name));
+
+ if (!opt)
+ return M_PROPERTY_UNKNOWN;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = *(opt->opt);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ if (!opt->data)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ m_option_copy(opt->opt, arg, opt->data);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ if (m_config_set_option_raw(mpctx->mconfig, opt, arg, 0) < 0)
+ return M_PROPERTY_ERROR;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Playback speed (RW)
+static int mp_property_playback_speed(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ double speed = mpctx->opts->playback_speed;
+ *(char **)arg = talloc_asprintf(NULL, "%.2f", speed);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_av_speed_correction(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ char *type = prop->priv;
+ double val = 0;
+ switch (type[0]) {
+ case 'a': val = mpctx->speed_factor_a; break;
+ case 'v': val = mpctx->speed_factor_v; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_asprintf(NULL, "%+.3g%%", (val - 1) * 100);
+ return M_PROPERTY_OK;
+ }
+
+ return m_property_double_ro(action, arg, val);
+}
+
+static int mp_property_display_sync_active(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, mpctx->display_sync_active);
+}
+
+static int mp_property_pid(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ // 32 bit on linux/windows - which C99 `int' is not guaranteed to hold
+ return m_property_int64_ro(action, arg, mp_getpid());
+}
+
+/// filename with path (RO)
+static int mp_property_path(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(action, arg, mpctx->filename);
+}
+
+static int mp_property_filename(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ char *filename = talloc_strdup(NULL, mpctx->filename);
+ if (mp_is_url(bstr0(filename)))
+ mp_url_unescape_inplace(filename);
+ char *f = (char *)mp_basename(filename);
+ if (!f[0])
+ f = filename;
+ if (action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *ka = arg;
+ if (strcmp(ka->key, "no-ext") == 0) {
+ action = ka->action;
+ arg = ka->arg;
+ bstr root;
+ if (mp_splitext(f, &root))
+ f = bstrto0(filename, root);
+ }
+ }
+ int r = m_property_strdup_ro(action, arg, f);
+ talloc_free(filename);
+ return r;
+}
+
+static int mp_property_stream_open_filename(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->stream_open_filename || !mpctx->playing)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_SET: {
+ if (mpctx->demuxer)
+ return M_PROPERTY_ERROR;
+ mpctx->stream_open_filename =
+ talloc_strdup(mpctx->stream_open_filename, *(char **)arg);
+ mp_notify_property(mpctx, prop->name);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ case M_PROPERTY_GET:
+ return m_property_strdup_ro(action, arg, mpctx->stream_open_filename);
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_file_size(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int64_t size = mpctx->demuxer->filesize;
+ if (size < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = format_file_size(size);
+ return M_PROPERTY_OK;
+ }
+ return m_property_int64_ro(action, arg, size);
+}
+
+static const char *find_non_filename_media_title(MPContext *mpctx)
+{
+ const char *name = mpctx->opts->media_title;
+ if (name && name[0])
+ return name;
+ if (mpctx->demuxer) {
+ name = mp_tags_get_str(mpctx->demuxer->metadata, "service_name");
+ if (name && name[0])
+ return name;
+ name = mp_tags_get_str(mpctx->demuxer->metadata, "title");
+ if (name && name[0])
+ return name;
+ name = mp_tags_get_str(mpctx->demuxer->metadata, "icy-title");
+ if (name && name[0])
+ return name;
+ }
+ if (mpctx->playing && mpctx->playing->title)
+ return mpctx->playing->title;
+ return NULL;
+}
+
+static int mp_property_media_title(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ const char *name = find_non_filename_media_title(mpctx);
+ if (name && name[0])
+ return m_property_strdup_ro(action, arg, name);
+ return mp_property_filename(ctx, prop, action, arg);
+}
+
+static int mp_property_stream_path(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer || !mpctx->demuxer->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(action, arg, mpctx->demuxer->filename);
+}
+
+/// Demuxer name (RO)
+static int mp_property_demuxer(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(action, arg, demuxer->desc->name);
+}
+
+static int mp_property_file_format(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ const char *name = demuxer->filetype ? demuxer->filetype : demuxer->desc->name;
+ return m_property_strdup_ro(action, arg, name);
+}
+
+static int mp_property_stream_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer || demuxer->filepos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(action, arg, demuxer->filepos);
+}
+
+/// Stream end offset (RO)
+static int mp_property_stream_end(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return mp_property_file_size(ctx, prop, action, arg);
+}
+
+// Does some magic to handle "<name>/full" as time formatted with milliseconds.
+// Assumes prop is the type of the actual property.
+static int property_time(int action, void *arg, double time)
+{
+ if (time == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ const struct m_option time_type = {.type = CONF_TYPE_TIME};
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(double *)arg = time;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = time_type;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+
+ if (strcmp(ka->key, "full") != 0)
+ return M_PROPERTY_UNKNOWN;
+
+ switch (ka->action) {
+ case M_PROPERTY_GET:
+ *(double *)ka->arg = time;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ *(char **)ka->arg = mp_format_time(time, true);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = time_type;
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_duration(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ double len = get_time_length(mpctx);
+
+ if (len < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(action, arg, len);
+}
+
+static int mp_property_avsync(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->ao_chain || !mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+ if (action == M_PROPERTY_PRINT) {
+ // Truncate anything < 1e-4 to avoid switching to scientific notation
+ if (fabs(mpctx->last_av_difference) < 1e-4) {
+ *(char **)arg = talloc_strdup(NULL, "0");
+ } else {
+ *(char **)arg = talloc_asprintf(NULL, "%+.2g", mpctx->last_av_difference);
+ }
+ return M_PROPERTY_OK;
+ }
+ return m_property_double_ro(action, arg, mpctx->last_av_difference);
+}
+
+static int mp_property_total_avsync_change(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->ao_chain || !mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+ if (mpctx->total_avsync_change == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, mpctx->total_avsync_change);
+}
+
+static int mp_property_frame_drop_dec(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_decoder_wrapper *dec = mpctx->vo_chain && mpctx->vo_chain->track
+ ? mpctx->vo_chain->track->dec : NULL;
+ if (!dec)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg,
+ mp_decoder_wrapper_get_frames_dropped(dec));
+}
+
+static int mp_property_mistimed_frame_count(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain || !mpctx->display_sync_active)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, mpctx->mistimed_frames_total);
+}
+
+static int mp_property_vsync_ratio(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain || !mpctx->display_sync_active)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int vsyncs = 0, frames = 0;
+ for (int n = 0; n < mpctx->num_past_frames; n++) {
+ int vsync = mpctx->past_frames[n].num_vsyncs;
+ if (vsync < 0)
+ break;
+ vsyncs += vsync;
+ frames += 1;
+ }
+
+ if (!frames)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, vsyncs / (double)frames);
+}
+
+static int mp_property_frame_drop_vo(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, vo_get_drop_count(mpctx->video_out));
+}
+
+static int mp_property_vo_delayed_frame_count(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, vo_get_delayed_count(mpctx->video_out));
+}
+
+/// Current position in percent (RW)
+static int mp_property_percent_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ double pos = *(double *)arg;
+ queue_seek(mpctx, MPSEEK_FACTOR, pos / 100.0, MPSEEK_DEFAULT, 0);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ double pos = get_current_pos_ratio(mpctx, false) * 100.0;
+ if (pos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ *(double *)arg = pos;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_DOUBLE,
+ .min = 0,
+ .max = 100,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ int pos = get_percent_pos(mpctx);
+ if (pos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ *(char **)arg = talloc_asprintf(NULL, "%d", pos);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_time_start(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ // minor backwards-compat.
+ return property_time(action, arg, 0);
+}
+
+/// Current position in seconds (RW)
+static int mp_property_time_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, *(double *)arg, MPSEEK_DEFAULT, 0);
+ return M_PROPERTY_OK;
+ }
+ return property_time(action, arg, get_current_time(mpctx));
+}
+
+/// Current audio pts in seconds (R)
+static int mp_property_audio_pts(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized || mpctx->audio_status < STATUS_PLAYING ||
+ mpctx->audio_status >= STATUS_EOF)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(action, arg, playing_audio_pts(mpctx));
+}
+
+static bool time_remaining(MPContext *mpctx, double *remaining)
+{
+ double len = get_time_length(mpctx);
+ double playback = get_playback_time(mpctx);
+
+ if (playback == MP_NOPTS_VALUE || len <= 0)
+ return false;
+
+ *remaining = len - playback;
+
+ return len >= 0;
+}
+
+static int mp_property_remaining(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ double remaining;
+ if (!time_remaining(ctx, &remaining))
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(action, arg, remaining);
+}
+
+static int mp_property_playtime_remaining(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ double remaining;
+ if (!time_remaining(mpctx, &remaining))
+ return M_PROPERTY_UNAVAILABLE;
+
+ double speed = mpctx->video_speed;
+ return property_time(action, arg, remaining / speed);
+}
+
+static int mp_property_playback_time(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, *(double *)arg, MPSEEK_DEFAULT, 0);
+ return M_PROPERTY_OK;
+ }
+ return property_time(action, arg, get_playback_time(mpctx));
+}
+
+/// Current chapter (RW)
+static int mp_property_chapter(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int chapter = get_current_chapter(mpctx);
+ int num = get_chapter_count(mpctx);
+ if (chapter < -1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *) arg = chapter;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_INT,
+ .min = -1,
+ .max = num - 1,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ *(char **) arg = chapter_display_name(mpctx, chapter);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SWITCH:
+ case M_PROPERTY_SET: ;
+ mark_seek(mpctx);
+ int step_all;
+ if (action == M_PROPERTY_SWITCH) {
+ struct m_property_switch_arg *sarg = arg;
+ step_all = lrint(sarg->inc);
+ // Check threshold for relative backward seeks
+ if (mpctx->opts->chapter_seek_threshold >= 0 && step_all < 0) {
+ double current_chapter_start =
+ chapter_start_time(mpctx, chapter);
+ // If we are far enough into a chapter, seek back to the
+ // beginning of current chapter instead of previous one
+ if (current_chapter_start != MP_NOPTS_VALUE &&
+ get_current_time(mpctx) - current_chapter_start >
+ mpctx->opts->chapter_seek_threshold)
+ {
+ step_all++;
+ }
+ }
+ } else // Absolute set
+ step_all = *(int *)arg - chapter;
+ chapter += step_all;
+ if (chapter < 0) // avoid using -1 if first chapter starts at 0
+ chapter = (chapter_start_time(mpctx, 0) <= 0) ? 0 : -1;
+ if (chapter >= num && step_all > 0) {
+ if (mpctx->opts->keep_open) {
+ seek_to_last_frame(mpctx);
+ } else {
+ // semi-broken file; ignore for user convenience
+ if (action == M_PROPERTY_SWITCH && num < 2)
+ return M_PROPERTY_UNAVAILABLE;
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mp_wakeup_core(mpctx);
+ }
+ } else {
+ double pts = chapter_start_time(mpctx, chapter);
+ if (pts != MP_NOPTS_VALUE) {
+ queue_seek(mpctx, MPSEEK_CHAPTER, pts, MPSEEK_DEFAULT, 0);
+ mpctx->last_chapter_seek = chapter;
+ mpctx->last_chapter_flag = true;
+ }
+ }
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_chapter_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+ char *name = chapter_name(mpctx, item);
+ double time = chapter_start_time(mpctx, item);
+ struct m_sub_property props[] = {
+ {"title", SUB_PROP_STR(name)},
+ {"time", {.type = CONF_TYPE_TIME}, {.time = time}},
+ {0}
+ };
+
+ int r = m_property_read_sub(props, action, arg);
+ return r;
+}
+
+static int parse_node_chapters(struct MPContext *mpctx,
+ struct mpv_node *given_chapters)
+{
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (given_chapters->format != MPV_FORMAT_NODE_ARRAY)
+ return M_PROPERTY_ERROR;
+
+ double len = get_time_length(mpctx);
+
+ talloc_free(mpctx->chapters);
+ mpctx->num_chapters = 0;
+ mpctx->chapters = talloc_array(NULL, struct demux_chapter, 0);
+
+ for (int n = 0; n < given_chapters->u.list->num; n++) {
+ struct mpv_node *chapter_data = &given_chapters->u.list->values[n];
+
+ if (chapter_data->format != MPV_FORMAT_NODE_MAP)
+ continue;
+
+ mpv_node_list *chapter_data_elements = chapter_data->u.list;
+
+ double time = -1;
+ char *title = 0;
+
+ for (int e = 0; e < chapter_data_elements->num; e++) {
+ struct mpv_node *chapter_data_element =
+ &chapter_data_elements->values[e];
+ char *key = chapter_data_elements->keys[e];
+ switch (chapter_data_element->format) {
+ case MPV_FORMAT_INT64:
+ if (strcmp(key, "time") == 0)
+ time = (double)chapter_data_element->u.int64;
+ break;
+ case MPV_FORMAT_DOUBLE:
+ if (strcmp(key, "time") == 0)
+ time = chapter_data_element->u.double_;
+ break;
+ case MPV_FORMAT_STRING:
+ if (strcmp(key, "title") == 0)
+ title = chapter_data_element->u.string;
+ break;
+ }
+ }
+
+ if (time >= 0 && time < len) {
+ struct demux_chapter new = {
+ .pts = time,
+ .metadata = talloc_zero(mpctx->chapters, struct mp_tags),
+ };
+ if (title)
+ mp_tags_set_str(new.metadata, "title", title);
+ MP_TARRAY_APPEND(NULL, mpctx->chapters, mpctx->num_chapters, new);
+ }
+ }
+
+ mp_notify(mpctx, MP_EVENT_CHAPTER_CHANGE, NULL);
+ mp_notify_property(mpctx, "chapter-list");
+
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_list_chapters(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int count = get_chapter_count(mpctx);
+ switch (action) {
+ case M_PROPERTY_PRINT: {
+ int cur = mpctx->playback_initialized ? get_current_chapter(mpctx) : -1;
+ char *res = NULL;
+ int n;
+
+ if (count < 1) {
+ res = talloc_asprintf_append(res, "No chapters.");
+ }
+
+ for (n = 0; n < count; n++) {
+ char *name = chapter_display_name(mpctx, n);
+ double t = chapter_start_time(mpctx, n);
+ char* time = mp_format_time(t, false);
+ res = talloc_asprintf_append(res, "%s", time);
+ talloc_free(time);
+ const char *m = n == cur ? list_current : list_normal;
+ res = talloc_asprintf_append(res, " %s%s\n", m, name);
+ talloc_free(name);
+ }
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ struct mpv_node *given_chapters = arg;
+ return parse_node_chapters(mpctx, given_chapters);
+ }
+ }
+ return m_property_read_list(action, arg, count, get_chapter_entry, mpctx);
+}
+
+static int mp_property_current_edition(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer || demuxer->num_editions <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(action, arg, demuxer->edition);
+}
+
+static int mp_property_edition(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ char *name = NULL;
+
+ if (!demuxer)
+ return mp_property_generic_option(mpctx, prop, action, arg);
+
+ int ed = demuxer->edition;
+
+ if (demuxer->num_editions <= 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET_CONSTRICTED_TYPE: {
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_INT,
+ .min = 0,
+ .max = demuxer->num_editions - 1,
+ };
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT: {
+ if (ed < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ name = mp_tags_get_str(demuxer->editions[ed].metadata, "title");
+ if (name) {
+ *(char **) arg = talloc_strdup(NULL, name);
+ } else {
+ *(char **) arg = talloc_asprintf(NULL, "%d", ed + 1);
+ }
+ return M_PROPERTY_OK;
+ }
+ default:
+ return mp_property_generic_option(mpctx, prop, action, arg);
+ }
+}
+
+static int get_edition_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+
+ struct demuxer *demuxer = mpctx->demuxer;
+ struct demux_edition *ed = &demuxer->editions[item];
+
+ char *title = mp_tags_get_str(ed->metadata, "title");
+
+ struct m_sub_property props[] = {
+ {"id", SUB_PROP_INT(item)},
+ {"title", SUB_PROP_STR(title),
+ .unavailable = !title},
+ {"default", SUB_PROP_BOOL(ed->default_edition)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int property_list_editions(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_PRINT) {
+ char *res = NULL;
+
+ struct demux_edition *editions = demuxer->editions;
+ int num_editions = demuxer->num_editions;
+ int current = demuxer->edition;
+
+ if (!num_editions)
+ res = talloc_asprintf_append(res, "No editions.");
+
+ for (int n = 0; n < num_editions; n++) {
+ struct demux_edition *ed = &editions[n];
+
+ res = talloc_strdup_append(res, n == current ? list_current
+ : list_normal);
+ res = talloc_asprintf_append(res, "%d: ", n);
+ char *title = mp_tags_get_str(ed->metadata, "title");
+ if (!title)
+ title = "unnamed";
+ res = talloc_asprintf_append(res, "'%s'\n", title);
+ }
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ return m_property_read_list(action, arg, demuxer->num_editions,
+ get_edition_entry, mpctx);
+}
+
+/// Number of chapters in file
+static int mp_property_chapters(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ int count = get_chapter_count(mpctx);
+ return m_property_int_ro(action, arg, count);
+}
+
+static int mp_property_editions(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ if (demuxer->num_editions <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(action, arg, demuxer->num_editions);
+}
+
+static int get_tag_entry(int item, int action, void *arg, void *ctx)
+{
+ struct mp_tags *tags = ctx;
+
+ struct m_sub_property props[] = {
+ {"key", SUB_PROP_STR(tags->keys[item])},
+ {"value", SUB_PROP_STR(tags->values[item])},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+// tags can be NULL for M_PROPERTY_GET_TYPE. (In all other cases, tags must be
+// provided, even for M_PROPERTY_KEY_ACTION GET_TYPE sub-actions.)
+static int tag_property(int action, void *arg, struct mp_tags *tags)
+{
+ switch (action) {
+ case M_PROPERTY_GET_NODE: // same as GET, because type==mpv_node
+ case M_PROPERTY_GET: {
+ mpv_node_list *list = talloc_zero(NULL, mpv_node_list);
+ mpv_node node = {
+ .format = MPV_FORMAT_NODE_MAP,
+ .u.list = list,
+ };
+ list->num = tags->num_keys;
+ list->values = talloc_array(list, mpv_node, list->num);
+ list->keys = talloc_array(list, char*, list->num);
+ for (int n = 0; n < tags->num_keys; n++) {
+ list->keys[n] = talloc_strdup(list, tags->keys[n]);
+ list->values[n] = (struct mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = talloc_strdup(list, tags->values[n]),
+ };
+ }
+ *(mpv_node*)arg = node;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE: {
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT: {
+ char *res = NULL;
+ for (int n = 0; n < tags->num_keys; n++) {
+ res = talloc_asprintf_append_buffer(res, "%s: %s\n",
+ tags->keys[n], tags->values[n]);
+ }
+ if (!res)
+ res = talloc_strdup(NULL, "(empty)");
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+ if (bstr_equals0(key, "list")) {
+ struct m_property_action_arg nka = *ka;
+ nka.key = rem;
+ return m_property_read_list(action, &nka, tags->num_keys,
+ get_tag_entry, tags);
+ }
+ // Direct access without this prefix is allowed for compatibility.
+ bstr k = bstr0(ka->key);
+ bstr_eatstart0(&k, "by-key/");
+ char *meta = mp_tags_get_bstr(tags, k);
+ if (!meta)
+ return M_PROPERTY_UNKNOWN;
+ switch (ka->action) {
+ case M_PROPERTY_GET:
+ *(char **)ka->arg = talloc_strdup(NULL, meta);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = (struct m_option){
+ .type = CONF_TYPE_STRING,
+ };
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Demuxer meta data
+static int mp_property_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return tag_property(action, arg, demuxer->metadata);
+}
+
+static int mp_property_filtered_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->filtered_tags)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return tag_property(action, arg, mpctx->filtered_tags);
+}
+
+static int mp_property_chapter_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int chapter = get_current_chapter(mpctx);
+ if (chapter < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return tag_property(action, arg, mpctx->chapters[chapter].metadata);
+}
+
+static int mp_property_filter_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ const char *type = prop->priv;
+
+ if (action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+ struct mp_tags *metadata = NULL;
+ struct mp_output_chain *chain = NULL;
+ if (strcmp(type, "vf") == 0) {
+ chain = mpctx->vo_chain ? mpctx->vo_chain->filter : NULL;
+ } else if (strcmp(type, "af") == 0) {
+ chain = mpctx->ao_chain ? mpctx->ao_chain->filter : NULL;
+ }
+ if (!chain)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (ka->action != M_PROPERTY_GET_TYPE) {
+ struct mp_filter_command cmd = {
+ .type = MP_FILTER_COMMAND_GET_META,
+ .res = &metadata,
+ };
+ mp_output_chain_command(chain, mp_tprintf(80, "%.*s", BSTR_P(key)),
+ &cmd);
+
+ if (!metadata)
+ return M_PROPERTY_ERROR;
+ }
+
+ int res;
+ if (strlen(rem)) {
+ struct m_property_action_arg next_ka = *ka;
+ next_ka.key = rem;
+ res = tag_property(M_PROPERTY_KEY_ACTION, &next_ka, metadata);
+ } else {
+ res = tag_property(ka->action, ka->arg, metadata);
+ }
+ talloc_free(metadata);
+ return res;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_core_idle(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, !mpctx->playback_active);
+}
+
+static int mp_property_idle(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, mpctx->stop_play == PT_STOP);
+}
+
+static int mp_property_window_id(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ int64_t wid;
+ if (!vo || vo_control(vo, VOCTRL_GET_WINDOW_ID, &wid) <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(action, arg, wid);
+}
+
+static int mp_property_eof_reached(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ bool eof = mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF;
+ return m_property_bool_ro(action, arg, eof);
+}
+
+static int mp_property_seeking(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, !mpctx->restart_complete);
+}
+
+static int mp_property_playback_abort(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, !mpctx->playing || mpctx->stop_play);
+}
+
+static int mp_property_cache_speed(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ uint64_t val = s.bytes_per_second;
+
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_strdup_append(format_file_size(val), "/s");
+ return M_PROPERTY_OK;
+ }
+ return m_property_int64_ro(action, arg, val);
+}
+
+static int mp_property_demuxer_cache_duration(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ if (s.ts_duration < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, s.ts_duration);
+}
+
+static int mp_property_demuxer_cache_time(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ if (s.ts_end == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, s.ts_end);
+}
+
+static int mp_property_demuxer_cache_idle(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ return m_property_bool_ro(action, arg, s.idle);
+}
+
+static int mp_property_demuxer_cache_state(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_GET_TYPE) {
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ }
+ if (action != M_PROPERTY_GET)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ struct mpv_node *r = (struct mpv_node *)arg;
+ node_init(r, MPV_FORMAT_NODE_MAP, NULL);
+
+ if (s.ts_end != MP_NOPTS_VALUE)
+ node_map_add_double(r, "cache-end", s.ts_end);
+
+ if (s.ts_reader != MP_NOPTS_VALUE)
+ node_map_add_double(r, "reader-pts", s.ts_reader);
+
+ if (s.ts_duration >= 0)
+ node_map_add_double(r, "cache-duration", s.ts_duration);
+
+ node_map_add_flag(r, "eof", s.eof);
+ node_map_add_flag(r, "underrun", s.underrun);
+ node_map_add_flag(r, "idle", s.idle);
+ node_map_add_int64(r, "total-bytes", s.total_bytes);
+ node_map_add_int64(r, "fw-bytes", s.fw_bytes);
+ if (s.file_cache_bytes >= 0)
+ node_map_add_int64(r, "file-cache-bytes", s.file_cache_bytes);
+ if (s.bytes_per_second > 0)
+ node_map_add_int64(r, "raw-input-rate", s.bytes_per_second);
+ if (s.seeking != MP_NOPTS_VALUE)
+ node_map_add_double(r, "debug-seeking", s.seeking);
+ node_map_add_int64(r, "debug-low-level-seeks", s.low_level_seeks);
+ node_map_add_int64(r, "debug-byte-level-seeks", s.byte_level_seeks);
+ if (s.ts_last != MP_NOPTS_VALUE)
+ node_map_add_double(r, "debug-ts-last", s.ts_last);
+
+ node_map_add_flag(r, "bof-cached", s.bof_cached);
+ node_map_add_flag(r, "eof-cached", s.eof_cached);
+
+ struct mpv_node *ranges =
+ node_map_add(r, "seekable-ranges", MPV_FORMAT_NODE_ARRAY);
+ for (int n = s.num_seek_ranges - 1; n >= 0; n--) {
+ struct demux_seek_range *range = &s.seek_ranges[n];
+ struct mpv_node *sub = node_array_add(ranges, MPV_FORMAT_NODE_MAP);
+ node_map_add_double(sub, "start", range->start);
+ node_map_add_double(sub, "end", range->end);
+ }
+
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_demuxer_start_time(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, mpctx->demuxer->start_time);
+}
+
+static int mp_property_paused_for_cache(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, mpctx->paused_for_cache);
+}
+
+static int mp_property_cache_buffering(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int state = get_cache_buffering_percentage(mpctx);
+ if (state < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(action, arg, state);
+}
+
+static int mp_property_demuxer_is_network(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_bool_ro(action, arg, mpctx->demuxer->is_network);
+}
+
+
+static int mp_property_clock(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ char outstr[6];
+ time_t t = time(NULL);
+ struct tm *tmp = localtime(&t);
+
+ if ((tmp != NULL) && (strftime(outstr, sizeof(outstr), "%H:%M", tmp) == 5))
+ return m_property_strdup_ro(action, arg, outstr);
+ return M_PROPERTY_UNAVAILABLE;
+}
+
+static int mp_property_seekable(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, mpctx->demuxer->seekable);
+}
+
+static int mp_property_partially_seekable(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, mpctx->demuxer->partially_seekable);
+}
+
+static int mp_property_mixer_active(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, !!mpctx->ao);
+}
+
+/// Volume (RW)
+static int mp_property_volume(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+
+ switch (action) {
+ case M_PROPERTY_GET_CONSTRICTED_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_FLOAT,
+ .min = 0,
+ .max = opts->softvol_max,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ *(char **)arg = talloc_asprintf(NULL, "%i", (int)opts->softvol_volume);
+ return M_PROPERTY_OK;
+ }
+
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_ao_volume(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct ao *ao = mpctx->ao;
+ if (!ao)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ float vol = *(float *)arg;
+ if (ao_control(ao, AOCONTROL_SET_VOLUME, &vol) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ if (ao_control(ao, AOCONTROL_GET_VOLUME, arg) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_FLOAT,
+ .min = 0,
+ .max = 100,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ float vol = 0;
+ if (ao_control(ao, AOCONTROL_GET_VOLUME, &vol) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ *(char **)arg = talloc_asprintf(NULL, "%.f", vol);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_ao_mute(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct ao *ao = mpctx->ao;
+ if (!ao)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ bool value = *(int *)arg;
+ if (ao_control(ao, AOCONTROL_SET_MUTE, &value) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ bool value = false;
+ if (ao_control(ao, AOCONTROL_GET_MUTE, &value) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ *(int *)arg = value;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_BOOL};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_device_entry(int item, int action, void *arg, void *ctx)
+{
+ struct ao_device_list *list = ctx;
+ struct ao_device_desc *entry = &list->devices[item];
+
+ struct m_sub_property props[] = {
+ {"name", SUB_PROP_STR(entry->name)},
+ {"description", SUB_PROP_STR(entry->desc)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static void create_hotplug(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ if (!cmd->hotplug) {
+ cmd->hotplug = ao_hotplug_create(mpctx->global, mp_wakeup_core_cb,
+ mpctx);
+ }
+}
+
+static int mp_property_audio_device(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ if (action == M_PROPERTY_PRINT) {
+ create_hotplug(mpctx);
+
+ char *name = NULL;
+ if (mp_property_generic_option(mpctx, prop, M_PROPERTY_GET, &name) < 1)
+ name = NULL;
+
+ struct ao_device_list *list = ao_hotplug_get_device_list(cmd->hotplug, mpctx->ao);
+ for (int n = 0; n < list->num_devices; n++) {
+ struct ao_device_desc *dev = &list->devices[n];
+ if (dev->name && name && strcmp(dev->name, name) == 0) {
+ *(char **)arg = talloc_strdup(NULL, dev->desc ? dev->desc : "?");
+ talloc_free(name);
+ return M_PROPERTY_OK;
+ }
+ }
+
+ talloc_free(name);
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_audio_devices(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ create_hotplug(mpctx);
+
+ struct ao_device_list *list = ao_hotplug_get_device_list(cmd->hotplug, mpctx->ao);
+ return m_property_read_list(action, arg, list->num_devices,
+ get_device_entry, list);
+}
+
+static int mp_property_ao(void *ctx, struct m_property *p, int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_strdup_ro(action, arg,
+ mpctx->ao ? ao_get_name(mpctx->ao) : NULL);
+}
+
+/// Audio delay (RW)
+static int mp_property_audio_delay(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = format_delay(mpctx->opts->audio_delay);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+/// Audio codec tag (RO)
+static int mp_property_audio_codec_name(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_AUDIO];
+ const char *c = track && track->stream ? track->stream->codec->codec : NULL;
+ return m_property_strdup_ro(action, arg, c);
+}
+
+/// Audio codec name (RO)
+static int mp_property_audio_codec(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_AUDIO];
+ char desc[256] = "";
+ if (track && track->dec)
+ mp_decoder_wrapper_get_desc(track->dec, desc, sizeof(desc));
+ return m_property_strdup_ro(action, arg, desc[0] ? desc : NULL);
+}
+
+static int property_audiofmt(struct mp_aframe *fmt, int action, void *arg)
+{
+ if (!fmt || !mp_aframe_config_is_valid(fmt))
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct mp_chmap chmap = {0};
+ mp_aframe_get_chmap(fmt, &chmap);
+
+ struct m_sub_property props[] = {
+ {"samplerate", SUB_PROP_INT(mp_aframe_get_rate(fmt))},
+ {"channel-count", SUB_PROP_INT(chmap.num)},
+ {"channels", SUB_PROP_STR(mp_chmap_to_str(&chmap))},
+ {"hr-channels", SUB_PROP_STR(mp_chmap_to_str_hr(&chmap))},
+ {"format", SUB_PROP_STR(af_fmt_to_str(mp_aframe_get_format(fmt)))},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_audio_params(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return property_audiofmt(mpctx->ao_chain ?
+ mpctx->ao_chain->filter->input_aformat : NULL, action, arg);
+}
+
+static int mp_property_audio_out_params(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_aframe *frame = NULL;
+ if (mpctx->ao) {
+ frame = mp_aframe_create();
+ int samplerate;
+ int format;
+ struct mp_chmap channels;
+ ao_get_format(mpctx->ao, &samplerate, &format, &channels);
+ mp_aframe_set_rate(frame, samplerate);
+ mp_aframe_set_format(frame, format);
+ mp_aframe_set_chmap(frame, &channels);
+ }
+ int r = property_audiofmt(frame, action, arg);
+ talloc_free(frame);
+ return r;
+}
+
+static struct track* track_next(struct MPContext *mpctx, enum stream_type type,
+ int direction, struct track *track)
+{
+ assert(direction == -1 || direction == +1);
+ struct track *prev = NULL, *next = NULL;
+ bool seen = track == NULL;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *cur = mpctx->tracks[n];
+ if (cur->type == type) {
+ if (cur == track) {
+ seen = true;
+ } else if (!cur->selected) {
+ if (seen && !next) {
+ next = cur;
+ }
+ if (!seen || !track) {
+ prev = cur;
+ }
+ }
+ }
+ }
+ return direction > 0 ? next : prev;
+}
+
+static int property_switch_track(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ const int *def = prop->priv;
+ int order = def[0];
+ enum stream_type type = def[1];
+
+ struct track *track = mpctx->current_track[order][type];
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ if (mpctx->playback_initialized) {
+ *(int *)arg = track ? track->user_tid : -2;
+ } else {
+ *(int *)arg = mpctx->opts->stream_id[order][type];
+ }
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ if (track) {
+ char *lang = track->lang;
+ if (!lang && type != STREAM_VIDEO) {
+ lang = "unknown";
+ } else if (!lang) {
+ lang = "";
+ }
+
+ if (track->title) {
+ *(char **)arg = talloc_asprintf(NULL, "(%d) %s (\"%s\")",
+ track->user_tid, lang, track->title);
+ } else {
+ *(char **)arg = talloc_asprintf(NULL, "(%d) %s",
+ track->user_tid, lang);
+ }
+ } else {
+ const char *msg = "no";
+ if (!mpctx->playback_initialized &&
+ mpctx->opts->stream_id[order][type] == -1)
+ msg = "auto";
+ *(char **) arg = talloc_strdup(NULL, msg);
+ }
+ return M_PROPERTY_OK;
+
+ case M_PROPERTY_SWITCH: {
+ if (mpctx->playback_initialized) {
+ struct m_property_switch_arg *sarg = arg;
+ do {
+ track = track_next(mpctx, type, sarg->inc >= 0 ? +1 : -1, track);
+ mp_switch_track_n(mpctx, order, type, track, FLAG_MARK_SELECTION);
+ } while (mpctx->current_track[order][type] != track);
+ print_track_list(mpctx, "Track switched:");
+ } else {
+ // Simply cycle between "no" and "auto". It's possible that this does
+ // not always do what the user means, but keep the complexity low.
+ mark_track_selection(mpctx, order, type,
+ mpctx->opts->stream_id[order][type] == -1 ? -2 : -1);
+ }
+ return M_PROPERTY_OK;
+ }
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int track_channels(struct track *track)
+{
+ return track->stream ? track->stream->codec->channels.num : 0;
+}
+
+static int get_track_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+ struct track *track = mpctx->tracks[item];
+
+ struct mp_codec_params p =
+ track->stream ? *track->stream->codec : (struct mp_codec_params){0};
+
+ char decoder_desc[256] = {0};
+ if (track->dec)
+ mp_decoder_wrapper_get_desc(track->dec, decoder_desc, sizeof(decoder_desc));
+
+ bool has_rg = track->stream && track->stream->codec->replaygain_data;
+ struct replaygain_data rg = has_rg ? *track->stream->codec->replaygain_data
+ : (struct replaygain_data){0};
+
+ double par = 0.0;
+ if (p.par_h)
+ par = p.par_w / (double) p.par_h;
+
+ int order = -1;
+ if (track->selected) {
+ for (int i = 0; i < num_ptracks[track->type]; i++) {
+ if (mpctx->current_track[i][track->type] == track) {
+ order = i;
+ break;
+ }
+ }
+ }
+
+ bool has_crop = mp_rect_w(p.crop) > 0 && mp_rect_h(p.crop) > 0;
+ struct m_sub_property props[] = {
+ {"id", SUB_PROP_INT(track->user_tid)},
+ {"type", SUB_PROP_STR(stream_type_name(track->type)),
+ .unavailable = !stream_type_name(track->type)},
+ {"src-id", SUB_PROP_INT(track->demuxer_id),
+ .unavailable = track->demuxer_id == -1},
+ {"title", SUB_PROP_STR(track->title),
+ .unavailable = !track->title},
+ {"lang", SUB_PROP_STR(track->lang),
+ .unavailable = !track->lang},
+ {"audio-channels", SUB_PROP_INT(track_channels(track)),
+ .unavailable = track_channels(track) <= 0},
+ {"image", SUB_PROP_BOOL(track->image)},
+ {"albumart", SUB_PROP_BOOL(track->attached_picture)},
+ {"default", SUB_PROP_BOOL(track->default_track)},
+ {"forced", SUB_PROP_BOOL(track->forced_track)},
+ {"dependent", SUB_PROP_BOOL(track->dependent_track)},
+ {"visual-impaired", SUB_PROP_BOOL(track->visual_impaired_track)},
+ {"hearing-impaired", SUB_PROP_BOOL(track->hearing_impaired_track)},
+ {"external", SUB_PROP_BOOL(track->is_external)},
+ {"selected", SUB_PROP_BOOL(track->selected)},
+ {"main-selection", SUB_PROP_INT(order), .unavailable = order < 0},
+ {"external-filename", SUB_PROP_STR(track->external_filename),
+ .unavailable = !track->external_filename},
+ {"ff-index", SUB_PROP_INT(track->ff_index)},
+ {"hls-bitrate", SUB_PROP_INT(track->hls_bitrate),
+ .unavailable = !track->hls_bitrate},
+ {"program-id", SUB_PROP_INT(track->program_id),
+ .unavailable = track->program_id < 0},
+ {"decoder-desc", SUB_PROP_STR(decoder_desc),
+ .unavailable = !decoder_desc[0]},
+ {"codec", SUB_PROP_STR(p.codec),
+ .unavailable = !p.codec},
+ {"demux-w", SUB_PROP_INT(p.disp_w), .unavailable = !p.disp_w},
+ {"demux-h", SUB_PROP_INT(p.disp_h), .unavailable = !p.disp_h},
+ {"demux-crop-x",SUB_PROP_INT(p.crop.x0), .unavailable = !has_crop},
+ {"demux-crop-y",SUB_PROP_INT(p.crop.y0), .unavailable = !has_crop},
+ {"demux-crop-w",SUB_PROP_INT(mp_rect_w(p.crop)), .unavailable = !has_crop},
+ {"demux-crop-h",SUB_PROP_INT(mp_rect_h(p.crop)), .unavailable = !has_crop},
+ {"demux-channel-count", SUB_PROP_INT(p.channels.num),
+ .unavailable = !p.channels.num},
+ {"demux-channels", SUB_PROP_STR(mp_chmap_to_str(&p.channels)),
+ .unavailable = !p.channels.num},
+ {"demux-samplerate", SUB_PROP_INT(p.samplerate),
+ .unavailable = !p.samplerate},
+ {"demux-fps", SUB_PROP_DOUBLE(p.fps), .unavailable = p.fps <= 0},
+ {"demux-bitrate", SUB_PROP_INT(p.bitrate), .unavailable = p.bitrate <= 0},
+ {"demux-rotation", SUB_PROP_INT(p.rotate), .unavailable = p.rotate <= 0},
+ {"demux-par", SUB_PROP_DOUBLE(par), .unavailable = par <= 0},
+ {"replaygain-track-peak", SUB_PROP_FLOAT(rg.track_peak),
+ .unavailable = !has_rg},
+ {"replaygain-track-gain", SUB_PROP_FLOAT(rg.track_gain),
+ .unavailable = !has_rg},
+ {"replaygain-album-peak", SUB_PROP_FLOAT(rg.album_peak),
+ .unavailable = !has_rg},
+ {"replaygain-album-gain", SUB_PROP_FLOAT(rg.album_gain),
+ .unavailable = !has_rg},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static const char *track_type_name(enum stream_type t)
+{
+ switch (t) {
+ case STREAM_VIDEO: return "Video";
+ case STREAM_AUDIO: return "Audio";
+ case STREAM_SUB: return "Sub";
+ }
+ return NULL;
+}
+
+static int property_list_tracks(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ char *res = NULL;
+
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type != type)
+ continue;
+
+ res = talloc_asprintf_append(res, "%s: ",
+ track_type_name(track->type));
+ res = talloc_strdup_append(res,
+ track->selected ? list_current : list_normal);
+ res = talloc_asprintf_append(res, "(%d) ", track->user_tid);
+ if (track->title)
+ res = talloc_asprintf_append(res, "'%s' ", track->title);
+ if (track->lang)
+ res = talloc_asprintf_append(res, "(%s) ", track->lang);
+ if (track->is_external)
+ res = talloc_asprintf_append(res, "(external) ");
+ res = talloc_asprintf_append(res, "\n");
+ }
+
+ res = talloc_asprintf_append(res, "\n");
+ }
+
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (demuxer && demuxer->num_editions > 1)
+ res = talloc_asprintf_append(res, "\nEdition: %d of %d\n",
+ demuxer->edition + 1,
+ demuxer->num_editions);
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ return m_property_read_list(action, arg, mpctx->num_tracks,
+ get_track_entry, mpctx);
+}
+
+static int property_current_tracks(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+
+ if (action != M_PROPERTY_KEY_ACTION)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int type = -1;
+ int order = 0;
+
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+
+ if (bstr_equals0(key, "video")) {
+ type = STREAM_VIDEO;
+ } else if (bstr_equals0(key, "audio")) {
+ type = STREAM_AUDIO;
+ } else if (bstr_equals0(key, "sub")) {
+ type = STREAM_SUB;
+ } else if (bstr_equals0(key, "sub2")) {
+ type = STREAM_SUB;
+ order = 1;
+ }
+
+ if (type < 0)
+ return M_PROPERTY_UNKNOWN;
+
+ struct track *t = mpctx->current_track[order][type];
+
+ if (!t && mpctx->lavfi) {
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ if (mpctx->tracks[n]->type == type && mpctx->tracks[n]->selected) {
+ t = mpctx->tracks[n];
+ break;
+ }
+ }
+ }
+
+ if (!t)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int index = -1;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ if (mpctx->tracks[n] == t) {
+ index = n;
+ break;
+ }
+ }
+ assert(index >= 0);
+
+ char *name = mp_tprintf(80, "track-list/%d/%s", index, rem);
+ return mp_property_do(name, ka->action, ka->arg, ctx);
+}
+
+static int mp_property_hwdec_current(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_decoder_wrapper *dec = track ? track->dec : NULL;
+
+ if (!dec)
+ return M_PROPERTY_UNAVAILABLE;
+
+ char *current = NULL;
+ mp_decoder_wrapper_control(dec, VDCTRL_GET_HWDEC, &current);
+ if (!current || !current[0])
+ current = "no";
+ return m_property_strdup_ro(action, arg, current);
+}
+
+static int mp_property_hwdec_interop(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->video_out || !mpctx->video_out->hwdec_devs)
+ return M_PROPERTY_UNAVAILABLE;
+
+ char *names = hwdec_devices_get_names(mpctx->video_out->hwdec_devs);
+ int res = m_property_strdup_ro(action, arg, names);
+ talloc_free(names);
+ return res;
+}
+
+static int get_frame_count(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return -1;
+ if (!mpctx->vo_chain)
+ return -1;
+ double len = get_time_length(mpctx);
+ double fps = mpctx->vo_chain->filter->container_fps;
+ if (len < 0 || fps <= 0)
+ return 0;
+
+ return len * fps;
+}
+
+static int mp_property_frame_number(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int frames = get_frame_count(mpctx);
+ if (frames < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg,
+ lrint(get_current_pos_ratio(mpctx, false) * frames));
+}
+
+static int mp_property_frame_count(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int frames = get_frame_count(mpctx);
+ if (frames < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, frames);
+}
+
+/// Video codec tag (RO)
+static int mp_property_video_format(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ const char *c = track && track->stream ? track->stream->codec->codec : NULL;
+ return m_property_strdup_ro(action, arg, c);
+}
+
+/// Video codec name (RO)
+static int mp_property_video_codec(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ char desc[256] = "";
+ if (track && track->dec)
+ mp_decoder_wrapper_get_desc(track->dec, desc, sizeof(desc));
+ return m_property_strdup_ro(action, arg, desc[0] ? desc : NULL);
+}
+
+static const char *get_aspect_ratio_name(double ratio)
+{
+ // Depending on cropping/mastering exact ratio may differ.
+#define RATIO_THRESH 0.025
+#define RATIO_CASE(ref, name) \
+ if (fabs(ratio - (ref)) < RATIO_THRESH) \
+ return name; \
+
+ // https://en.wikipedia.org/wiki/Aspect_ratio_(image)
+ RATIO_CASE(9.0 / 16.0, "Vertical")
+ RATIO_CASE(1.0, "Square");
+ RATIO_CASE(19.0 / 16.0, "Movietone Ratio");
+ RATIO_CASE(5.0 / 4.0, "5:4");
+ RATIO_CASE(4.0 / 3.0, "4:3");
+ RATIO_CASE(11.0 / 8.0, "Academy Ratio");
+ RATIO_CASE(1.43, "IMAX Ratio");
+ RATIO_CASE(3.0 / 2.0, "VistaVision Ratio");
+ RATIO_CASE(16.0 / 10.0, "16:10");
+ RATIO_CASE(5.0 / 3.0, "35mm Widescreen Ratio");
+ RATIO_CASE(16.0 / 9.0, "16:9");
+ RATIO_CASE(7.0 / 4.0, "Early 35mm Widescreen Ratio");
+ RATIO_CASE(1.85, "Academy Flat");
+ RATIO_CASE(256.0 / 135.0, "SMPTE/DCI Ratio");
+ RATIO_CASE(2.0, "Univisium");
+ RATIO_CASE(2.208, "70mm film");
+ RATIO_CASE(2.35, "Scope");
+ RATIO_CASE(2.39, "Panavision");
+ RATIO_CASE(2.55, "Original CinemaScope");
+ RATIO_CASE(2.59, "Full-frame Cinerama");
+ RATIO_CASE(24.0 / 9.0, "Full-frame Super 16mm");
+ RATIO_CASE(2.76, "Ultra Panavision 70");
+ RATIO_CASE(32.0 / 9.0, "32:9");
+ RATIO_CASE(3.6, "Ultra-WideScreen 3.6");
+ RATIO_CASE(4.0, "Polyvision");
+ RATIO_CASE(12.0, "Circle-Vision 360°");
+
+ return NULL;
+
+#undef RATIO_THRESH
+#undef RATIO_CASE
+}
+
+static int property_imgparams(struct mp_image_params p, int action, void *arg)
+{
+ if (!p.imgfmt)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int d_w, d_h;
+ mp_image_params_get_dsize(&p, &d_w, &d_h);
+
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p.imgfmt);
+ int bpp = 0;
+ for (int i = 0; i < desc.num_planes; i++)
+ bpp += desc.bpp[i] >> (desc.xs[i] + desc.ys[i]);
+
+ // Alpha type is not supported by FFmpeg, so MP_ALPHA_AUTO may mean alpha
+ // is of an unknown type, or simply not present. Normalize to AUTO=no alpha.
+ if (!!(desc.flags & MP_IMGFLAG_ALPHA) != (p.alpha != MP_ALPHA_AUTO)) {
+ p.alpha =
+ (desc.flags & MP_IMGFLAG_ALPHA) ? MP_ALPHA_STRAIGHT : MP_ALPHA_AUTO;
+ }
+
+ const struct pl_hdr_metadata *hdr = &p.color.hdr;
+ bool has_cie_y = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_CIE_Y);
+ bool has_hdr10 = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_HDR10);
+ bool has_hdr10plus = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_HDR10PLUS);
+
+ bool has_crop = mp_rect_w(p.crop) > 0 && mp_rect_h(p.crop) > 0;
+ const char *aspect_name = get_aspect_ratio_name(d_w / (double)d_h);
+ const char *sar_name = get_aspect_ratio_name(p.w / (double)p.h);
+ struct m_sub_property props[] = {
+ {"pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p.imgfmt))},
+ {"hw-pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p.hw_subfmt)),
+ .unavailable = !p.hw_subfmt},
+ {"average-bpp", SUB_PROP_INT(bpp),
+ .unavailable = !bpp},
+ {"w", SUB_PROP_INT(p.w)},
+ {"h", SUB_PROP_INT(p.h)},
+ {"dw", SUB_PROP_INT(d_w)},
+ {"dh", SUB_PROP_INT(d_h)},
+ {"crop-x", SUB_PROP_INT(p.crop.x0), .unavailable = !has_crop},
+ {"crop-y", SUB_PROP_INT(p.crop.y0), .unavailable = !has_crop},
+ {"crop-w", SUB_PROP_INT(mp_rect_w(p.crop)), .unavailable = !has_crop},
+ {"crop-h", SUB_PROP_INT(mp_rect_h(p.crop)), .unavailable = !has_crop},
+ {"aspect", SUB_PROP_FLOAT(d_w / (double)d_h)},
+ {"aspect-name", SUB_PROP_STR(aspect_name), .unavailable = !aspect_name},
+ {"par", SUB_PROP_FLOAT(p.p_w / (double)p.p_h)},
+ {"sar", SUB_PROP_FLOAT(p.w / (double)p.h)},
+ {"sar-name", SUB_PROP_STR(sar_name), .unavailable = !sar_name},
+ {"colormatrix",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_names, p.color.space))},
+ {"colorlevels",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_levels_names, p.color.levels))},
+ {"primaries",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_prim_names, p.color.primaries))},
+ {"gamma",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_trc_names, p.color.gamma))},
+ {"sig-peak", SUB_PROP_FLOAT(p.color.hdr.max_luma / MP_REF_WHITE)},
+ {"light",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p.color.light))},
+ {"chroma-location",
+ SUB_PROP_STR(m_opt_choice_str(mp_chroma_names, p.chroma_location))},
+ {"stereo-in",
+ SUB_PROP_STR(m_opt_choice_str(mp_stereo3d_names, p.stereo3d))},
+ {"rotate", SUB_PROP_INT(p.rotate)},
+ {"alpha",
+ SUB_PROP_STR(m_opt_choice_str(mp_alpha_names, p.alpha)),
+ // avoid using "auto" for "no", so just make it unavailable
+ .unavailable = p.alpha == MP_ALPHA_AUTO},
+ {"min-luma", SUB_PROP_FLOAT(hdr->min_luma), .unavailable = !has_hdr10},
+ {"max-luma", SUB_PROP_FLOAT(hdr->max_luma), .unavailable = !has_hdr10},
+ {"max-cll", SUB_PROP_FLOAT(hdr->max_cll), .unavailable = !has_hdr10},
+ {"max-fall", SUB_PROP_FLOAT(hdr->max_fall), .unavailable = !has_hdr10},
+ {"scene-max-r", SUB_PROP_FLOAT(hdr->scene_max[0]), .unavailable = !has_hdr10plus},
+ {"scene-max-g", SUB_PROP_FLOAT(hdr->scene_max[1]), .unavailable = !has_hdr10plus},
+ {"scene-max-b", SUB_PROP_FLOAT(hdr->scene_max[2]), .unavailable = !has_hdr10plus},
+ {"scene-avg", SUB_PROP_FLOAT(hdr->scene_avg), .unavailable = !has_hdr10plus},
+ {"max-pq-y", SUB_PROP_FLOAT(hdr->max_pq_y), .unavailable = !has_cie_y},
+ {"avg-pq-y", SUB_PROP_FLOAT(hdr->avg_pq_y), .unavailable = !has_cie_y},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static struct mp_image_params get_video_out_params(struct MPContext *mpctx)
+{
+ if (!mpctx->vo_chain)
+ return (struct mp_image_params){0};
+
+ struct mp_image_params o_params = mpctx->vo_chain->filter->output_params;
+ if (mpctx->video_out) {
+ struct m_geometry *gm = &mpctx->video_out->opts->video_crop;
+ if (gm->xy_valid || (gm->wh_valid && (gm->w > 0 || gm->h > 0)))
+ {
+ m_rect_apply(&o_params.crop, o_params.w, o_params.h, gm);
+ }
+ }
+
+ return o_params;
+}
+
+static int mp_property_vo_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ return property_imgparams(vo_get_current_params(vo), action, arg);
+}
+
+static int mp_property_dec_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_image_params p = {0};
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ if (!vo_c || !vo_c->track)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ mp_decoder_wrapper_get_video_dec_params(vo_c->track->dec, &p);
+ if (!p.imgfmt)
+ return M_PROPERTY_UNAVAILABLE;
+ return property_imgparams(p, action, arg);
+}
+
+static int mp_property_vd_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ if (!vo_c)
+ return M_PROPERTY_UNAVAILABLE;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_codec_params *c =
+ track && track->stream ? track->stream->codec : NULL;
+ if (vo_c->filter->input_params.imgfmt) {
+ return property_imgparams(vo_c->filter->input_params, action, arg);
+ } else if (c && c->disp_w && c->disp_h) {
+ // Simplistic fallback for stupid scripts querying "width"/"height"
+ // before the first frame is decoded.
+ struct m_sub_property props[] = {
+ {"w", SUB_PROP_INT(c->disp_w)},
+ {"h", SUB_PROP_INT(c->disp_h)},
+ {0}
+ };
+ return m_property_read_sub(props, action, arg);
+ }
+ return M_PROPERTY_UNAVAILABLE;
+}
+
+static int mp_property_video_frame_info(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ struct mp_image *f = vo_get_current_frame(mpctx->video_out);
+ if (!f)
+ return M_PROPERTY_UNAVAILABLE;
+
+ const char *pict_types[] = {0, "I", "P", "B"};
+ const char *pict_type = f->pict_type >= 1 && f->pict_type <= 3
+ ? pict_types[f->pict_type] : NULL;
+
+ struct m_sub_property props[] = {
+ {"picture-type", SUB_PROP_STR(pict_type), .unavailable = !pict_type},
+ {"interlaced", SUB_PROP_BOOL(!!(f->fields & MP_IMGFIELD_INTERLACED))},
+ {"tff", SUB_PROP_BOOL(!!(f->fields & MP_IMGFIELD_TOP_FIRST))},
+ {"repeat", SUB_PROP_BOOL(!!(f->fields & MP_IMGFIELD_REPEAT_FIRST))},
+ {0}
+ };
+
+ talloc_free(f);
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_current_window_scale(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct mp_image_params params = get_video_out_params(mpctx);
+ int vid_w, vid_h;
+ mp_image_params_get_dsize(&params, &vid_w, &vid_h);
+ if (vid_w < 1 || vid_h < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (params.rotate % 180 == 90 && (vo->driver->caps & VO_CAP_ROTATE90))
+ MPSWAP(int, vid_w, vid_h);
+
+ if (vo->monitor_par < 1) {
+ vid_h = MPCLAMP(vid_h / vo->monitor_par, 1, 16000);
+ } else {
+ vid_w = MPCLAMP(vid_w * vo->monitor_par, 1, 16000);
+ }
+
+ if (action == M_PROPERTY_SET) {
+ // Also called by update_window_scale as a NULL property.
+ double scale = *(double *)arg;
+ int s[2] = {vid_w * scale, vid_h * scale};
+ if (s[0] <= 0 || s[1] <= 0)
+ return M_PROPERTY_INVALID_FORMAT;
+ vo_control(vo, VOCTRL_SET_UNFS_WINDOW_SIZE, s);
+ return M_PROPERTY_OK;
+ }
+
+ int s[2];
+ if (vo_control(vo, VOCTRL_GET_UNFS_WINDOW_SIZE, s) <= 0 ||
+ s[0] < 1 || s[1] < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ double xs = (double)s[0] / vid_w;
+ double ys = (double)s[1] / vid_h;
+ return m_property_double_ro(action, arg, (xs + ys) / 2);
+}
+
+static void update_window_scale(struct MPContext *mpctx)
+{
+ double scale = mpctx->opts->vo->window_scale;
+ mp_property_current_window_scale(mpctx, (struct m_property *)NULL,
+ M_PROPERTY_SET, (void*)&scale);
+}
+
+static int mp_property_display_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ double fps = mpctx->video_out ? vo_get_display_fps(mpctx->video_out) : 0;
+ switch (action) {
+ case M_PROPERTY_GET:
+ if (fps <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, fps);
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_DOUBLE};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_estimated_display_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ double interval = vo_get_estimated_vsync_interval(vo) / 1e9;
+ if (interval <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, 1.0 / interval);
+}
+
+static int mp_property_vsync_jitter(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ double stddev = vo_get_estimated_vsync_jitter(vo);
+ if (stddev < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, stddev);
+}
+
+static int mp_property_display_resolution(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ int res[2];
+ if (vo_control(vo, VOCTRL_GET_DISPLAY_RES, &res) <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ if (strcmp(prop->name, "display-width") == 0) {
+ return m_property_int_ro(action, arg, res[0]);
+ } else {
+ return m_property_int_ro(action, arg, res[1]);
+ }
+}
+
+static int mp_property_hidpi_scale(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ if (!cmd->cached_window_scale) {
+ double scale = 0;
+ if (vo_control(vo, VOCTRL_GET_HIDPI_SCALE, &scale) < 1 || !scale)
+ scale = -1;
+ cmd->cached_window_scale = scale;
+ }
+ if (cmd->cached_window_scale < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, cmd->cached_window_scale);
+}
+
+static int mp_property_focused(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ bool focused;
+ if (vo_control(vo, VOCTRL_GET_FOCUSED, &focused) < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_bool_ro(action, arg, focused);
+}
+
+static int mp_property_display_names(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ char** display_names;
+ if (vo_control(vo, VOCTRL_GET_DISPLAY_NAMES, &display_names) < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ *(char ***)arg = display_names;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_vo_configured(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg,
+ mpctx->video_out && mpctx->video_out->config_ok);
+}
+
+static void get_frame_perf(struct mpv_node *node, struct mp_frame_perf *perf)
+{
+ for (int i = 0; i < perf->count; i++) {
+ struct mp_pass_perf *data = &perf->perf[i];
+ struct mpv_node *pass = node_array_add(node, MPV_FORMAT_NODE_MAP);
+
+ node_map_add_string(pass, "desc", perf->desc[i]);
+ node_map_add(pass, "last", MPV_FORMAT_INT64)->u.int64 = data->last;
+ node_map_add(pass, "avg", MPV_FORMAT_INT64)->u.int64 = data->avg;
+ node_map_add(pass, "peak", MPV_FORMAT_INT64)->u.int64 = data->peak;
+ node_map_add(pass, "count", MPV_FORMAT_INT64)->u.int64 = data->count;
+ struct mpv_node *samples = node_map_add(pass, "samples", MPV_FORMAT_NODE_ARRAY);
+ for (int n = 0; n < data->count; n++)
+ node_array_add(samples, MPV_FORMAT_INT64)->u.int64 = data->samples[n];
+ }
+}
+
+static char *asprint_perf(char *res, struct mp_frame_perf *perf)
+{
+ for (int i = 0; i < perf->count; i++) {
+ struct mp_pass_perf *pass = &perf->perf[i];
+ res = talloc_asprintf_append(res,
+ "- %s: last %dus avg %dus peak %dus\n", perf->desc[i],
+ (int)pass->last/1000, (int)pass->avg/1000, (int)pass->peak/1000);
+ }
+
+ return res;
+}
+
+static int mp_property_vo_passes(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+
+ // Return early, to avoid having to go through a completely unnecessary VOCTRL
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ case M_PROPERTY_GET:
+ break;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ default:
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ }
+
+ struct voctrl_performance_data *data = talloc_ptrtype(NULL, data);
+ if (vo_control(mpctx->video_out, VOCTRL_PERFORMANCE_DATA, data) <= 0) {
+ talloc_free(data);
+ return M_PROPERTY_UNAVAILABLE;
+ }
+
+ switch (action) {
+ case M_PROPERTY_PRINT: {
+ char *res = NULL;
+ res = talloc_asprintf_append(res, "fresh:\n");
+ res = asprint_perf(res, &data->fresh);
+ res = talloc_asprintf_append(res, "\nredraw:\n");
+ res = asprint_perf(res, &data->redraw);
+ *(char **)arg = res;
+ break;
+ }
+
+ case M_PROPERTY_GET: {
+ struct mpv_node node;
+ node_init(&node, MPV_FORMAT_NODE_MAP, NULL);
+ struct mpv_node *fresh = node_map_add(&node, "fresh", MPV_FORMAT_NODE_ARRAY);
+ struct mpv_node *redraw = node_map_add(&node, "redraw", MPV_FORMAT_NODE_ARRAY);
+ get_frame_perf(fresh, &data->fresh);
+ get_frame_perf(redraw, &data->redraw);
+ *(struct mpv_node *)arg = node;
+ break;
+ }
+ }
+
+ talloc_free(data);
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_perf_info(void *ctx, struct m_property *p, int action,
+ void *arg)
+{
+ MPContext *mpctx = ctx;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ stats_global_query(mpctx->global, (struct mpv_node *)arg);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_vo(void *ctx, struct m_property *p, int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_strdup_ro(action, arg,
+ mpctx->video_out ? mpctx->video_out->driver->name : NULL);
+}
+
+static int mp_property_osd_dim(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_osd_res vo_res = osd_get_vo_res(mpctx->osd);
+
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ vo_res = (struct mp_osd_res){0};
+
+ double aspect = 1.0 * vo_res.w / MPMAX(vo_res.h, 1) /
+ (vo_res.display_par ? vo_res.display_par : 1);
+
+ struct m_sub_property props[] = {
+ {"w", SUB_PROP_INT(vo_res.w)},
+ {"h", SUB_PROP_INT(vo_res.h)},
+ {"par", SUB_PROP_DOUBLE(vo_res.display_par)},
+ {"aspect", SUB_PROP_DOUBLE(aspect)},
+ {"mt", SUB_PROP_INT(vo_res.mt)},
+ {"mb", SUB_PROP_INT(vo_res.mb)},
+ {"ml", SUB_PROP_INT(vo_res.ml)},
+ {"mr", SUB_PROP_INT(vo_res.mr)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_osd_sym(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ char temp[20];
+ get_current_osd_sym(mpctx, temp, sizeof(temp));
+ return m_property_strdup_ro(action, arg, temp);
+}
+
+static int mp_property_osd_ass(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct m_sub_property props[] = {
+ {"0", SUB_PROP_STR(OSD_ASS_0)},
+ {"1", SUB_PROP_STR(OSD_ASS_1)},
+ {0}
+ };
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_mouse_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+
+ case M_PROPERTY_GET: {
+ struct mpv_node node;
+ int x, y, hover;
+ mp_input_get_mouse_pos(mpctx->input, &x, &y, &hover);
+
+ node_init(&node, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(&node, "x", x);
+ node_map_add_int64(&node, "y", y);
+ node_map_add_flag(&node, "hover", hover);
+ *(struct mpv_node *)arg = node;
+
+ return M_PROPERTY_OK;
+ }
+ }
+
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Video fps (RO)
+static int mp_property_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ float fps = mpctx->vo_chain ? mpctx->vo_chain->filter->container_fps : 0;
+ if (fps < 0.1 || !isfinite(fps))
+ return M_PROPERTY_UNAVAILABLE;;
+ return m_property_float_ro(action, arg, fps);
+}
+
+static int mp_property_vf_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+ double avg = calc_average_frame_duration(mpctx);
+ if (avg <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, 1.0 / avg);
+}
+
+#define doubles_equal(x, y) (fabs((x) - (y)) <= 0.001)
+
+static int mp_property_video_aspect_override(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ double aspect_ratio;
+ mp_property_generic_option(mpctx, prop, M_PROPERTY_GET, &aspect_ratio);
+
+ if (doubles_equal(aspect_ratio, 2.35 / 1.0))
+ *(char **)arg = talloc_asprintf(NULL, "2.35:1");
+ else if (doubles_equal(aspect_ratio, 16.0 / 9.0))
+ *(char **)arg = talloc_asprintf(NULL, "16:9");
+ else if (doubles_equal(aspect_ratio, 16.0 / 10.0))
+ *(char **)arg = talloc_asprintf(NULL, "16:10");
+ else if (doubles_equal(aspect_ratio, 4.0 / 3.0))
+ *(char **)arg = talloc_asprintf(NULL, "4:3");
+ else if (doubles_equal(aspect_ratio, -1.0))
+ *(char **)arg = talloc_asprintf(NULL, "Original");
+ else
+ *(char **)arg = talloc_asprintf(NULL, "%.3f", aspect_ratio);
+
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+/// Subtitle delay (RW)
+static int mp_property_sub_delay(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **)arg = format_delay(opts->subs_rend->sub_delay);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+/// Subtitle speed (RW)
+static int mp_property_sub_speed(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg =
+ talloc_asprintf(NULL, "%4.1f%%", 100 * opts->subs_rend->sub_speed);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_sub_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_asprintf(NULL, "%4.2f%%/100", opts->subs_rend->sub_pos);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_sub_ass_extradata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ if (!sub)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *data = sub_ass_get_extradata(sub);
+ if (!data)
+ return M_PROPERTY_UNAVAILABLE;
+ *(char **)arg = data;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg, int sub_index)
+{
+ int type = *(int *)prop->priv;
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[sub_index][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ double pts = mpctx->playback_pts;
+ if (!sub || pts == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *text = sub_get_text(sub, pts, type);
+ if (!text)
+ text = talloc_strdup(NULL, "");
+ *(char **)arg = text;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return get_sub_text(ctx, prop, action, arg, 0);
+}
+
+static int mp_property_secondary_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return get_sub_text(ctx, prop, action, arg, 1);
+}
+
+static struct sd_times get_times(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+ MPContext *mpctx = ctx;
+ int track_ind = *(int *)prop->priv;
+ struct track *track = mpctx->current_track[track_ind][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ double pts = mpctx->playback_pts;
+ if (!sub || pts == MP_NOPTS_VALUE)
+ return res;
+ return sub_get_times(sub, pts);
+}
+
+static int mp_property_sub_start(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ double start = get_times(ctx, prop, action, arg).start;
+ if (start == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, start);
+}
+
+
+static int mp_property_sub_end(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ double end = get_times(ctx, prop, action, arg).end;
+ if (end == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, end);
+}
+
+static int mp_property_playlist_current_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct playlist *pl = mpctx->playlist;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ *(int *)arg = playlist_entry_to_index(pl, pl->current);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ pl->current = playlist_entry_from_index(pl, *(int *)arg);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_INT};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_playlist_playing_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct playlist *pl = mpctx->playlist;
+ return m_property_int_ro(action, arg,
+ playlist_entry_to_index(pl, mpctx->playing));
+}
+
+static int mp_property_playlist_pos_x(void *ctx, struct m_property *prop,
+ int action, void *arg, int base)
+{
+ MPContext *mpctx = ctx;
+ struct playlist *pl = mpctx->playlist;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ int pos = playlist_entry_to_index(pl, pl->current);
+ *(int *)arg = pos < 0 ? -1 : pos + base;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ int pos = *(int *)arg - base;
+ if (pos >= 0 && playlist_entry_to_index(pl, pl->current) == pos)
+ return M_PROPERTY_OK;
+ mp_set_playlist_entry(mpctx, playlist_entry_from_index(pl, pos));
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_INT};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_CONSTRICTED_TYPE: {
+ struct m_option opt = {
+ .type = CONF_TYPE_INT,
+ .min = base,
+ .max = playlist_entry_count(pl) - 1 + base,
+ };
+ *(struct m_option *)arg = opt;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_playlist_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return mp_property_playlist_pos_x(ctx, prop, action, arg, 0);
+}
+
+static int mp_property_playlist_pos_1(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return mp_property_playlist_pos_x(ctx, prop, action, arg, 1);
+}
+
+static int get_playlist_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+
+ struct playlist_entry *e = playlist_entry_from_index(mpctx->playlist, item);
+ if (!e)
+ return M_PROPERTY_ERROR;
+
+ bool current = mpctx->playlist->current == e;
+ bool playing = mpctx->playing == e;
+ struct m_sub_property props[] = {
+ {"filename", SUB_PROP_STR(e->filename)},
+ {"current", SUB_PROP_BOOL(1), .unavailable = !current},
+ {"playing", SUB_PROP_BOOL(1), .unavailable = !playing},
+ {"title", SUB_PROP_STR(e->title), .unavailable = !e->title},
+ {"id", SUB_PROP_INT64(e->id)},
+ {"playlist-path", SUB_PROP_STR(e->playlist_path), .unavailable = !e->playlist_path},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_playlist_path(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playlist->current)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct playlist_entry *e = mpctx->playlist->current;
+ return m_property_strdup_ro(action, arg, e->playlist_path);
+}
+
+static int mp_property_playlist(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ struct playlist *pl = mpctx->playlist;
+ char *res = talloc_strdup(NULL, "");
+
+ for (int n = 0; n < pl->num_entries; n++) {
+ struct playlist_entry *e = pl->entries[n];
+ char *p = e->title;
+ if (!p) {
+ p = e->filename;
+ if (!mp_is_url(bstr0(p))) {
+ char *s = mp_basename(e->filename);
+ if (s[0])
+ p = s;
+ }
+ }
+ const char *m = pl->current == e ? list_current : list_normal;
+ res = talloc_asprintf_append(res, "%s%s\n", m, p);
+ }
+
+ *(char **)arg =
+ cut_osd_list(mpctx, res, playlist_entry_to_index(pl, pl->current));
+ return M_PROPERTY_OK;
+ }
+
+ return m_property_read_list(action, arg, playlist_entry_count(mpctx->playlist),
+ get_playlist_entry, mpctx);
+}
+
+static char *print_obj_osd_list(struct m_obj_settings *list)
+{
+ char *res = NULL;
+ for (int n = 0; list && list[n].name; n++) {
+ res = talloc_asprintf_append(res, "%s [", list[n].name);
+ for (int i = 0; list[n].attribs && list[n].attribs[i]; i += 2) {
+ res = talloc_asprintf_append(res, "%s%s=%s", i > 0 ? " " : "",
+ list[n].attribs[i],
+ list[n].attribs[i + 1]);
+ }
+ res = talloc_asprintf_append(res, "]");
+ if (!list[n].enabled)
+ res = talloc_strdup_append(res, " (disabled)");
+ res = talloc_strdup_append(res, "\n");
+ }
+ if (!res)
+ res = talloc_strdup(NULL, "(empty)");
+ return res;
+}
+
+static int property_filter(struct m_property *prop, int action, void *arg,
+ MPContext *mpctx, enum stream_type mt)
+{
+ if (action == M_PROPERTY_PRINT) {
+ struct m_config_option *opt = m_config_get_co(mpctx->mconfig,
+ bstr0(prop->name));
+ *(char **)arg = print_obj_osd_list(*(struct m_obj_settings **)opt->data);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_vf(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return property_filter(prop, action, arg, ctx, STREAM_VIDEO);
+}
+
+static int mp_property_af(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return property_filter(prop, action, arg, ctx, STREAM_AUDIO);
+}
+
+static int mp_property_ab_loop(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_KEY_ACTION) {
+ double val;
+ if (mp_property_generic_option(mpctx, prop, M_PROPERTY_GET, &val) < 1)
+ return M_PROPERTY_ERROR;
+
+ return property_time(action, arg, val);
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_packet_bitrate(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int type = (uintptr_t)prop->priv & ~0x100;
+ bool old = (uintptr_t)prop->priv & 0x100;
+
+ struct demuxer *demuxer = NULL;
+ if (mpctx->current_track[0][type])
+ demuxer = mpctx->current_track[0][type]->demuxer;
+ if (!demuxer)
+ demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ double r[STREAM_TYPE_COUNT];
+ demux_get_bitrate_stats(demuxer, r);
+ if (r[type] < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ // r[type] is in bytes/second -> bits
+ double rate = r[type] * 8;
+
+ // Same story, but used kilobits for some reason.
+ if (old)
+ return m_property_int64_ro(action, arg, llrint(rate / 1000.0));
+
+ if (action == M_PROPERTY_PRINT) {
+ rate /= 1000;
+ if (rate < 1000) {
+ *(char **)arg = talloc_asprintf(NULL, "%d kbps", (int)rate);
+ } else {
+ *(char **)arg = talloc_asprintf(NULL, "%.3f mbps", rate / 1000.0);
+ }
+ return M_PROPERTY_OK;
+ }
+ return m_property_int64_ro(action, arg, llrint(rate));
+}
+
+static int mp_property_cwd(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *cwd = mp_getcwd(NULL);
+ if (!cwd)
+ return M_PROPERTY_ERROR;
+ *(char **)arg = cwd;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_protocols(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char ***)arg = stream_get_proto_list();
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_keylist(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char ***)arg = mp_get_key_list();
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_decoder_entry(int item, int action, void *arg, void *ctx)
+{
+ struct mp_decoder_list *codecs = ctx;
+ struct mp_decoder_entry *c = &codecs->entries[item];
+
+ struct m_sub_property props[] = {
+ {"codec", SUB_PROP_STR(c->codec)},
+ {"driver" , SUB_PROP_STR(c->decoder)},
+ {"description", SUB_PROP_STR(c->desc)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_decoders(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct mp_decoder_list *codecs = talloc_zero(NULL, struct mp_decoder_list);
+ struct mp_decoder_list *v = talloc_steal(codecs, video_decoder_list());
+ struct mp_decoder_list *a = talloc_steal(codecs, audio_decoder_list());
+ mp_append_decoders(codecs, v);
+ mp_append_decoders(codecs, a);
+ int r = m_property_read_list(action, arg, codecs->num_entries,
+ get_decoder_entry, codecs);
+ talloc_free(codecs);
+ return r;
+}
+
+static int mp_property_encoders(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct mp_decoder_list *codecs = talloc_zero(NULL, struct mp_decoder_list);
+ mp_add_lavc_encoders(codecs);
+ int r = m_property_read_list(action, arg, codecs->num_entries,
+ get_decoder_entry, codecs);
+ talloc_free(codecs);
+ return r;
+}
+
+static int mp_property_lavf_demuxers(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char ***)arg = mp_get_lavf_demuxers();
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_version(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, mpv_version);
+}
+
+static int mp_property_configuration(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, CONFIGURATION);
+}
+
+static int mp_property_ffmpeg(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, av_version_info());
+}
+
+static int mp_property_libass_version(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_int64_ro(action, arg, ass_library_version());
+}
+
+static int mp_property_platform(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, PLATFORM);
+}
+
+static int mp_property_alias(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ const char *real_property = prop->priv;
+ return mp_property_do(real_property, action, arg, ctx);
+}
+
+static int mp_property_deprecated_alias(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ const char *real_property = prop->priv;
+ for (int n = 0; n < cmd->num_warned_deprecated; n++) {
+ if (strcmp(cmd->warned_deprecated[n], prop->name) == 0)
+ goto done;
+ }
+ MP_WARN(mpctx, "Warning: property '%s' was replaced with '%s' and "
+ "might be removed in the future.\n", prop->name, real_property);
+ MP_TARRAY_APPEND(cmd, cmd->warned_deprecated, cmd->num_warned_deprecated,
+ (char *)prop->name);
+
+done:
+ return mp_property_do(real_property, action, arg, ctx);
+}
+
+static int access_options(struct m_property_action_arg *ka, bool local,
+ MPContext *mpctx)
+{
+ struct m_config_option *opt = m_config_get_co(mpctx->mconfig,
+ bstr0(ka->key));
+ if (!opt)
+ return M_PROPERTY_UNKNOWN;
+ if (!opt->data)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (ka->action) {
+ case M_PROPERTY_GET:
+ m_option_copy(opt->opt, ka->arg, opt->data);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET: {
+ if (local && !mpctx->playing)
+ return M_PROPERTY_ERROR;
+ int flags = local ? M_SETOPT_BACKUP : 0;
+ int r = m_config_set_option_raw(mpctx->mconfig, opt, ka->arg, flags);
+ mp_wakeup_core(mpctx);
+ return r < 0 ? M_PROPERTY_ERROR : M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = *opt->opt;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int access_option_list(int action, void *arg, bool local, MPContext *mpctx)
+{
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(char ***)arg = m_config_list_options(NULL, mpctx->mconfig);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_KEY_ACTION:
+ return access_options(arg, local, mpctx);
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_options(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return access_option_list(action, arg, false, mpctx);
+}
+
+static int mp_property_local_options(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return access_option_list(action, arg, true, mpctx);
+}
+
+static int mp_property_option_info(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ switch (action) {
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+ struct m_config_option *co = m_config_get_co(mpctx->mconfig, key);
+ if (!co)
+ return M_PROPERTY_UNKNOWN;
+ const struct m_option *opt = co->opt;
+
+ union m_option_value def = m_option_value_default;
+ const void *def_ptr = m_config_get_co_default(mpctx->mconfig, co);
+ if (def_ptr && opt->type->size > 0)
+ memcpy(&def, def_ptr, opt->type->size);
+
+ bool has_minmax = opt->min < opt->max &&
+ (opt->type->flags & M_OPT_TYPE_USES_RANGE);
+ char **choices = NULL;
+
+ if (opt->type == &m_option_type_choice) {
+ const struct m_opt_choice_alternatives *alt = opt->priv;
+ int num = 0;
+ for ( ; alt->name; alt++)
+ MP_TARRAY_APPEND(NULL, choices, num, alt->name);
+ MP_TARRAY_APPEND(NULL, choices, num, NULL);
+ }
+ if (opt->type == &m_option_type_obj_settings_list) {
+ const struct m_obj_list *objs = opt->priv;
+ int num = 0;
+ for (int n = 0; ; n++) {
+ struct m_obj_desc desc = {0};
+ if (!objs->get_desc(&desc, n))
+ break;
+ MP_TARRAY_APPEND(NULL, choices, num, (char *)desc.name);
+ }
+ MP_TARRAY_APPEND(NULL, choices, num, NULL);
+ }
+
+ struct m_sub_property props[] = {
+ {"name", SUB_PROP_STR(co->name)},
+ {"type", SUB_PROP_STR(opt->type->name)},
+ {"set-from-commandline", SUB_PROP_BOOL(co->is_set_from_cmdline)},
+ {"set-locally", SUB_PROP_BOOL(co->is_set_locally)},
+ {"default-value", *opt, def},
+ {"min", SUB_PROP_DOUBLE(opt->min),
+ .unavailable = !(has_minmax && opt->min != DBL_MIN)},
+ {"max", SUB_PROP_DOUBLE(opt->max),
+ .unavailable = !(has_minmax && opt->max != DBL_MAX)},
+ {"choices", .type = {.type = CONF_TYPE_STRING_LIST},
+ .value = {.string_list = choices}, .unavailable = !choices},
+ {0}
+ };
+
+ struct m_property_action_arg next_ka = *ka;
+ next_ka.key = rem;
+ int r = m_property_read_sub(props, M_PROPERTY_KEY_ACTION, &next_ka);
+ talloc_free(choices);
+ return r;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_list(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ char **list = NULL;
+ int num = 0;
+ for (int n = 0; cmd->properties[n].name; n++) {
+ MP_TARRAY_APPEND(NULL, list, num,
+ talloc_strdup(NULL, cmd->properties[n].name));
+ }
+ MP_TARRAY_APPEND(NULL, list, num, NULL);
+ *(char ***)arg = list;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_profile_list(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ *(struct mpv_node *)arg = m_config_get_profiles(mpctx->mconfig);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_commands(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ struct mpv_node *root = arg;
+ node_init(root, MPV_FORMAT_NODE_ARRAY, NULL);
+
+ for (int n = 0; mp_cmds[n].name; n++) {
+ const struct mp_cmd_def *cmd = &mp_cmds[n];
+ struct mpv_node *entry = node_array_add(root, MPV_FORMAT_NODE_MAP);
+
+ node_map_add_string(entry, "name", cmd->name);
+
+ struct mpv_node *args =
+ node_map_add(entry, "args", MPV_FORMAT_NODE_ARRAY);
+ for (int i = 0; i < MP_CMD_DEF_MAX_ARGS; i++) {
+ const struct m_option *a = &cmd->args[i];
+ if (!a->type)
+ break;
+ struct mpv_node *ae = node_array_add(args, MPV_FORMAT_NODE_MAP);
+ node_map_add_string(ae, "name", a->name);
+ node_map_add_string(ae, "type", a->type->name);
+ node_map_add_flag(ae, "optional", a->flags & MP_CMD_OPT_ARG);
+ }
+
+ node_map_add_flag(entry, "vararg", cmd->vararg);
+ }
+
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_bindings(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ *(struct mpv_node *)arg = mp_input_get_bindings(mpctx->input);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_script_props(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ if (!cmd->shared_script_warning) {
+ MP_WARN(mpctx, "The shared-script-properties property is deprecated and will "
+ "be removed in the future. Use the user-data property instead.\n");
+ cmd->shared_script_warning = true;
+ }
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = script_props_type;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ m_option_copy(&script_props_type, arg, &cmd->script_props);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ m_option_copy(&script_props_type, &cmd->script_props, arg);
+ mp_notify_property(mpctx, prop->name);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int do_list_udata(int item, int action, void *arg, void *ctx);
+
+struct udata_ctx {
+ MPContext *mpctx;
+ const char *path;
+ mpv_node *node;
+ void *ta_parent;
+};
+
+static int do_op_udata(struct udata_ctx* ctx, int action, void *arg)
+{
+ MPContext *mpctx = ctx->mpctx;
+ mpv_node *node = ctx->node;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = udata_type;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ case M_PROPERTY_GET_NODE: // same as GET, because type==mpv_node
+ assert(node);
+ m_option_copy(&udata_type, arg, node);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ char *str = m_option_pretty_print(&udata_type, node);
+ *(char **)arg = str;
+ return str != NULL;
+ }
+ case M_PROPERTY_SET:
+ case M_PROPERTY_SET_NODE:
+ assert(node);
+ m_option_copy(&udata_type, node, arg);
+ talloc_steal(ctx->ta_parent, node_get_alloc(node));
+ mp_notify_property(mpctx, ctx->path);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_KEY_ACTION: {
+ assert(node);
+
+ // If we're operating on an array, sub-object access is handled by m_property_read_list
+ if (node->format == MPV_FORMAT_NODE_ARRAY)
+ return m_property_read_list(action, arg, node->u.list->num, &do_list_udata, ctx);
+
+ // Sub-objects only make sense for arrays and maps
+ if (node->format != MPV_FORMAT_NODE_MAP)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ struct m_property_action_arg *act = arg;
+
+ // See if the next layer down will also be a sub-object access
+ bstr key;
+ char *rem;
+ bool has_split = m_property_split_path(act->key, &key, &rem);
+
+ if (!has_split && act->action == M_PROPERTY_DELETE) {
+ // Find the object we're looking for
+ int i;
+ for (i = 0; i < node->u.list->num; i++) {
+ if (bstr_equals0(key, node->u.list->keys[i]))
+ break;
+ }
+
+ // Return if it didn't exist
+ if (i == node->u.list->num)
+ return M_PROPERTY_UNKNOWN;
+
+ // Delete the item
+ m_option_free(&udata_type, &node->u.list->values[i]);
+ talloc_free(node->u.list->keys[i]);
+
+ // Shift the remaining items back
+ for (i++; i < node->u.list->num; i++) {
+ node->u.list->values[i - 1] = node->u.list->values[i];
+ node->u.list->keys[i - 1] = node->u.list->keys[i];
+ }
+
+ // And decrement the count
+ node->u.list->num--;
+
+ return M_PROPERTY_OK;
+ }
+
+ // Look up the next level down
+ mpv_node *cnode = node_map_bget(node, key);
+
+ if (!cnode) {
+ switch (act->action) {
+ case M_PROPERTY_SET:
+ case M_PROPERTY_SET_NODE: {
+ // If we're doing a set, and the key doesn't exist, create it.
+ // If we're recursing another layer down, make it an empty map;
+ // otherwise, make it NONE, since we'll be overwriting it at the next level.
+ cnode = node_map_badd(node, key, has_split ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NONE);
+ if (!cnode)
+ return M_PROPERTY_ERROR;
+ break;
+ case M_PROPERTY_GET_TYPE:
+ // Nonexistent keys have type NODE, so they can be overwritten
+ *(struct m_option *)act->arg = udata_type;
+ return M_PROPERTY_OK;
+ default:
+ // We can't perform any other options on nonexistent keys
+ return M_PROPERTY_UNKNOWN;
+ }
+ }
+ }
+
+ struct udata_ctx nctx = *ctx;
+ nctx.node = cnode;
+ nctx.ta_parent = node_get_alloc(node);
+
+ // If we're going down another level, set up a new key-action.
+ if (has_split) {
+ struct m_property_action_arg sub_act = {
+ .key = rem,
+ .action = act->action,
+ .arg = act->arg,
+ };
+
+ return do_op_udata(&nctx, M_PROPERTY_KEY_ACTION, &sub_act);
+ } else {
+ return do_op_udata(&nctx, act->action, act->arg);
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int do_list_udata(int item, int action, void *arg, void *ctx)
+{
+ struct udata_ctx nctx = *(struct udata_ctx*)ctx;
+ nctx.node = &nctx.node->u.list->values[item];
+ nctx.ta_parent = &nctx.node->u.list;
+
+ return do_op_udata(&nctx, action, arg);
+}
+
+static int mp_property_udata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ // The root of udata is a shared map; don't allow overwriting
+ // or deleting the whole thing
+ if (action == M_PROPERTY_SET || action == M_PROPERTY_SET_NODE ||
+ action == M_PROPERTY_DELETE)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ char *path = NULL;
+ if (action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *act = arg;
+ if (act->action == M_PROPERTY_SET || act->action == M_PROPERTY_SET_NODE)
+ path = talloc_asprintf(NULL, "%s/%s", prop->name, act->key);
+ }
+
+ struct MPContext *mpctx = ctx;
+ struct udata_ctx nctx = {
+ .mpctx = mpctx,
+ .path = path,
+ .node = &mpctx->command_ctx->udata,
+ .ta_parent = &mpctx->command_ctx,
+ };
+
+ int ret = do_op_udata(&nctx, action, arg);
+
+ talloc_free(path);
+
+ return ret;
+}
+
+// Redirect a property name to another
+#define M_PROPERTY_ALIAS(name, real_property) \
+ {(name), mp_property_alias, .priv = (real_property)}
+
+#define M_PROPERTY_DEPRECATED_ALIAS(name, real_property) \
+ {(name), mp_property_deprecated_alias, .priv = (real_property)}
+
+// Base list of properties. This does not include option-mapped properties.
+static const struct m_property mp_properties_base[] = {
+ // General
+ {"pid", mp_property_pid},
+ {"speed", mp_property_playback_speed},
+ {"audio-speed-correction", mp_property_av_speed_correction, .priv = "a"},
+ {"video-speed-correction", mp_property_av_speed_correction, .priv = "v"},
+ {"display-sync-active", mp_property_display_sync_active},
+ {"filename", mp_property_filename},
+ {"stream-open-filename", mp_property_stream_open_filename},
+ {"file-size", mp_property_file_size},
+ {"path", mp_property_path},
+ {"media-title", mp_property_media_title},
+ {"stream-path", mp_property_stream_path},
+ {"current-demuxer", mp_property_demuxer},
+ {"file-format", mp_property_file_format},
+ {"stream-pos", mp_property_stream_pos},
+ {"stream-end", mp_property_stream_end},
+ {"duration", mp_property_duration},
+ {"avsync", mp_property_avsync},
+ {"total-avsync-change", mp_property_total_avsync_change},
+ {"mistimed-frame-count", mp_property_mistimed_frame_count},
+ {"vsync-ratio", mp_property_vsync_ratio},
+ {"display-width", mp_property_display_resolution},
+ {"display-height", mp_property_display_resolution},
+ {"decoder-frame-drop-count", mp_property_frame_drop_dec},
+ {"frame-drop-count", mp_property_frame_drop_vo},
+ {"vo-delayed-frame-count", mp_property_vo_delayed_frame_count},
+ {"percent-pos", mp_property_percent_pos},
+ {"time-start", mp_property_time_start},
+ {"time-pos", mp_property_time_pos},
+ {"time-remaining", mp_property_remaining},
+ {"audio-pts", mp_property_audio_pts},
+ {"playtime-remaining", mp_property_playtime_remaining},
+ {"playback-time", mp_property_playback_time},
+ {"chapter", mp_property_chapter},
+ {"edition", mp_property_edition},
+ {"current-edition", mp_property_current_edition},
+ {"chapters", mp_property_chapters},
+ {"editions", mp_property_editions},
+ {"metadata", mp_property_metadata},
+ {"filtered-metadata", mp_property_filtered_metadata},
+ {"chapter-metadata", mp_property_chapter_metadata},
+ {"vf-metadata", mp_property_filter_metadata, .priv = "vf"},
+ {"af-metadata", mp_property_filter_metadata, .priv = "af"},
+ {"core-idle", mp_property_core_idle},
+ {"eof-reached", mp_property_eof_reached},
+ {"seeking", mp_property_seeking},
+ {"playback-abort", mp_property_playback_abort},
+ {"cache-speed", mp_property_cache_speed},
+ {"demuxer-cache-duration", mp_property_demuxer_cache_duration},
+ {"demuxer-cache-time", mp_property_demuxer_cache_time},
+ {"demuxer-cache-idle", mp_property_demuxer_cache_idle},
+ {"demuxer-start-time", mp_property_demuxer_start_time},
+ {"demuxer-cache-state", mp_property_demuxer_cache_state},
+ {"cache-buffering-state", mp_property_cache_buffering},
+ {"paused-for-cache", mp_property_paused_for_cache},
+ {"demuxer-via-network", mp_property_demuxer_is_network},
+ {"clock", mp_property_clock},
+ {"seekable", mp_property_seekable},
+ {"partially-seekable", mp_property_partially_seekable},
+ {"idle-active", mp_property_idle},
+ {"window-id", mp_property_window_id},
+
+ {"chapter-list", mp_property_list_chapters},
+ {"track-list", property_list_tracks},
+ {"current-tracks", property_current_tracks},
+ {"edition-list", property_list_editions},
+
+ {"playlist", mp_property_playlist},
+ {"playlist-path", mp_property_playlist_path},
+ {"playlist-pos", mp_property_playlist_pos},
+ {"playlist-pos-1", mp_property_playlist_pos_1},
+ {"playlist-current-pos", mp_property_playlist_current_pos},
+ {"playlist-playing-pos", mp_property_playlist_playing_pos},
+ M_PROPERTY_ALIAS("playlist-count", "playlist/count"),
+
+ // Audio
+ {"mixer-active", mp_property_mixer_active},
+ {"volume", mp_property_volume},
+ {"ao-volume", mp_property_ao_volume},
+ {"ao-mute", mp_property_ao_mute},
+ {"audio-delay", mp_property_audio_delay},
+ {"audio-codec-name", mp_property_audio_codec_name},
+ {"audio-codec", mp_property_audio_codec},
+ {"audio-params", mp_property_audio_params},
+ {"audio-out-params", mp_property_audio_out_params},
+ {"aid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_AUDIO}},
+ {"audio-device", mp_property_audio_device},
+ {"audio-device-list", mp_property_audio_devices},
+ {"current-ao", mp_property_ao},
+
+ // Video
+ {"video-out-params", mp_property_vo_imgparams},
+ {"video-dec-params", mp_property_dec_imgparams},
+ {"video-params", mp_property_vd_imgparams},
+ {"video-format", mp_property_video_format},
+ {"video-frame-info", mp_property_video_frame_info},
+ {"video-codec", mp_property_video_codec},
+ M_PROPERTY_ALIAS("dwidth", "video-out-params/dw"),
+ M_PROPERTY_ALIAS("dheight", "video-out-params/dh"),
+ M_PROPERTY_ALIAS("width", "video-params/w"),
+ M_PROPERTY_ALIAS("height", "video-params/h"),
+ {"current-window-scale", mp_property_current_window_scale},
+ {"vo-configured", mp_property_vo_configured},
+ {"vo-passes", mp_property_vo_passes},
+ {"perf-info", mp_property_perf_info},
+ {"current-vo", mp_property_vo},
+ {"container-fps", mp_property_fps},
+ {"estimated-vf-fps", mp_property_vf_fps},
+ {"video-aspect-override", mp_property_video_aspect_override},
+ {"vid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_VIDEO}},
+ {"hwdec-current", mp_property_hwdec_current},
+ {"hwdec-interop", mp_property_hwdec_interop},
+
+ {"estimated-frame-count", mp_property_frame_count},
+ {"estimated-frame-number", mp_property_frame_number},
+
+ {"osd-dimensions", mp_property_osd_dim},
+ M_PROPERTY_ALIAS("osd-width", "osd-dimensions/w"),
+ M_PROPERTY_ALIAS("osd-height", "osd-dimensions/h"),
+ M_PROPERTY_ALIAS("osd-par", "osd-dimensions/par"),
+
+ {"osd-sym-cc", mp_property_osd_sym},
+ {"osd-ass-cc", mp_property_osd_ass},
+
+ {"mouse-pos", mp_property_mouse_pos},
+
+ // Subs
+ {"sid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_SUB}},
+ {"secondary-sid", property_switch_track,
+ .priv = (void *)(const int[]){1, STREAM_SUB}},
+ {"sub-delay", mp_property_sub_delay},
+ {"sub-speed", mp_property_sub_speed},
+ {"sub-pos", mp_property_sub_pos},
+ {"sub-ass-extradata", mp_property_sub_ass_extradata},
+ {"sub-text", mp_property_sub_text,
+ .priv = (void *)&(const int){SD_TEXT_TYPE_PLAIN}},
+ {"secondary-sub-text", mp_property_secondary_sub_text,
+ .priv = (void *)&(const int){SD_TEXT_TYPE_PLAIN}},
+ {"sub-text-ass", mp_property_sub_text,
+ .priv = (void *)&(const int){SD_TEXT_TYPE_ASS}},
+ {"sub-start", mp_property_sub_start,
+ .priv = (void *)&(const int){0}},
+ {"secondary-sub-start", mp_property_sub_start,
+ .priv = (void *)&(const int){1}},
+ {"sub-end", mp_property_sub_end,
+ .priv = (void *)&(const int){0}},
+ {"secondary-sub-end", mp_property_sub_end,
+ .priv = (void *)&(const int){1}},
+
+ {"vf", mp_property_vf},
+ {"af", mp_property_af},
+
+ {"ab-loop-a", mp_property_ab_loop},
+ {"ab-loop-b", mp_property_ab_loop},
+
+#define PROPERTY_BITRATE(name, old, type) \
+ {name, mp_property_packet_bitrate, (void *)(uintptr_t)((type)|(old?0x100:0))}
+ PROPERTY_BITRATE("packet-video-bitrate", true, STREAM_VIDEO),
+ PROPERTY_BITRATE("packet-audio-bitrate", true, STREAM_AUDIO),
+ PROPERTY_BITRATE("packet-sub-bitrate", true, STREAM_SUB),
+
+ PROPERTY_BITRATE("video-bitrate", false, STREAM_VIDEO),
+ PROPERTY_BITRATE("audio-bitrate", false, STREAM_AUDIO),
+ PROPERTY_BITRATE("sub-bitrate", false, STREAM_SUB),
+
+ {"focused", mp_property_focused},
+ {"display-names", mp_property_display_names},
+ {"display-fps", mp_property_display_fps},
+ {"estimated-display-fps", mp_property_estimated_display_fps},
+ {"vsync-jitter", mp_property_vsync_jitter},
+ {"display-hidpi-scale", mp_property_hidpi_scale},
+
+ {"working-directory", mp_property_cwd},
+
+ {"protocol-list", mp_property_protocols},
+ {"decoder-list", mp_property_decoders},
+ {"encoder-list", mp_property_encoders},
+ {"demuxer-lavf-list", mp_property_lavf_demuxers},
+ {"input-key-list", mp_property_keylist},
+
+ {"mpv-version", mp_property_version},
+ {"mpv-configuration", mp_property_configuration},
+ {"ffmpeg-version", mp_property_ffmpeg},
+ {"libass-version", mp_property_libass_version},
+ {"platform", mp_property_platform},
+
+ {"options", mp_property_options},
+ {"file-local-options", mp_property_local_options},
+ {"option-info", mp_property_option_info},
+ {"property-list", mp_property_list},
+ {"profile-list", mp_profile_list},
+ {"command-list", mp_property_commands},
+ {"input-bindings", mp_property_bindings},
+
+ {"shared-script-properties", mp_property_script_props},
+ {"user-data", mp_property_udata},
+
+ M_PROPERTY_ALIAS("video", "vid"),
+ M_PROPERTY_ALIAS("audio", "aid"),
+ M_PROPERTY_ALIAS("sub", "sid"),
+
+ // compatibility
+ M_PROPERTY_ALIAS("colormatrix", "video-params/colormatrix"),
+ M_PROPERTY_ALIAS("colormatrix-input-range", "video-params/colorlevels"),
+ M_PROPERTY_ALIAS("colormatrix-primaries", "video-params/primaries"),
+ M_PROPERTY_ALIAS("colormatrix-gamma", "video-params/gamma"),
+
+ M_PROPERTY_DEPRECATED_ALIAS("sub-forced-only-cur", "sub-forced-events-only"),
+};
+
+// Each entry describes which properties an event (possibly) changes.
+#define E(x, ...) [x] = (const char*const[]){__VA_ARGS__, NULL}
+static const char *const *const mp_event_property_change[] = {
+ E(MPV_EVENT_START_FILE, "*"),
+ E(MPV_EVENT_END_FILE, "*"),
+ E(MPV_EVENT_FILE_LOADED, "*"),
+ E(MP_EVENT_CHANGE_ALL, "*"),
+ E(MP_EVENT_TRACKS_CHANGED, "track-list", "current-tracks"),
+ E(MP_EVENT_TRACK_SWITCHED, "track-list", "current-tracks"),
+ E(MPV_EVENT_IDLE, "*"),
+ E(MPV_EVENT_TICK, "time-pos", "audio-pts", "stream-pos", "avsync",
+ "percent-pos", "time-remaining", "playtime-remaining", "playback-time",
+ "estimated-vf-fps", "total-avsync-change", "audio-speed-correction",
+ "video-speed-correction", "vo-delayed-frame-count", "mistimed-frame-count",
+ "vsync-ratio", "estimated-display-fps", "vsync-jitter", "sub-text",
+ "secondary-sub-text", "audio-bitrate", "video-bitrate", "sub-bitrate",
+ "decoder-frame-drop-count", "frame-drop-count", "video-frame-info",
+ "vf-metadata", "af-metadata", "sub-start", "sub-end", "secondary-sub-start",
+ "secondary-sub-end", "video-out-params", "video-dec-params", "video-params"),
+ E(MP_EVENT_DURATION_UPDATE, "duration"),
+ E(MPV_EVENT_VIDEO_RECONFIG, "video-out-params", "video-params",
+ "video-format", "video-codec", "video-bitrate", "dwidth", "dheight",
+ "width", "height", "container-fps", "aspect", "aspect-name", "vo-configured", "current-vo",
+ "video-dec-params", "osd-dimensions",
+ "hwdec", "hwdec-current", "hwdec-interop"),
+ E(MPV_EVENT_AUDIO_RECONFIG, "audio-format", "audio-codec", "audio-bitrate",
+ "samplerate", "channels", "audio", "volume", "mute",
+ "current-ao", "audio-codec-name", "audio-params",
+ "audio-out-params", "volume-max", "mixer-active"),
+ E(MPV_EVENT_SEEK, "seeking", "core-idle", "eof-reached"),
+ E(MPV_EVENT_PLAYBACK_RESTART, "seeking", "core-idle", "eof-reached"),
+ E(MP_EVENT_METADATA_UPDATE, "metadata", "filtered-metadata", "media-title"),
+ E(MP_EVENT_CHAPTER_CHANGE, "chapter", "chapter-metadata"),
+ E(MP_EVENT_CACHE_UPDATE,
+ "demuxer-cache-duration", "demuxer-cache-idle", "paused-for-cache",
+ "demuxer-cache-time", "cache-buffering-state", "cache-speed",
+ "demuxer-cache-state"),
+ E(MP_EVENT_WIN_RESIZE, "current-window-scale", "osd-width", "osd-height",
+ "osd-par", "osd-dimensions"),
+ E(MP_EVENT_WIN_STATE, "display-names", "display-fps", "display-width",
+ "display-height"),
+ E(MP_EVENT_WIN_STATE2, "display-hidpi-scale"),
+ E(MP_EVENT_FOCUS, "focused"),
+ E(MP_EVENT_CHANGE_PLAYLIST, "playlist", "playlist-pos", "playlist-pos-1",
+ "playlist-count", "playlist/count", "playlist-current-pos",
+ "playlist-playing-pos"),
+ E(MP_EVENT_INPUT_PROCESSED, "mouse-pos"),
+ E(MP_EVENT_CORE_IDLE, "core-idle", "eof-reached"),
+};
+#undef E
+
+// If there is no prefix, return length+1 (avoids matching full name as prefix).
+static int prefix_len(const char *p)
+{
+ const char *end = strchr(p, '/');
+ return end ? end - p : strlen(p) + 1;
+}
+
+static bool match_property(const char *a, const char *b)
+{
+ if (strcmp(a, "*") == 0)
+ return true;
+ // Give options and properties the same ID each, so change notifications
+ // work both way.
+ if (strncmp(a, "options/", 8) == 0)
+ a += 8;
+ if (strncmp(b, "options/", 8) == 0)
+ b += 8;
+ int len_a = prefix_len(a);
+ int len_b = prefix_len(b);
+ return strncmp(a, b, MPMIN(len_a, len_b)) == 0;
+}
+
+// Return a bitset of events which change the property.
+uint64_t mp_get_property_event_mask(const char *name)
+{
+ uint64_t mask = 0;
+ for (int n = 0; n < MP_ARRAY_SIZE(mp_event_property_change); n++) {
+ const char *const *const list = mp_event_property_change[n];
+ for (int i = 0; list && list[i]; i++) {
+ if (match_property(list[i], name))
+ mask |= 1ULL << n;
+ }
+ }
+ return mask;
+}
+
+// Return an ID for the property. It might not be unique, but is good enough
+// for property change handling. Return -1 if property unknown.
+int mp_get_property_id(struct MPContext *mpctx, const char *name)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ for (int n = 0; ctx->properties[n].name; n++) {
+ if (match_property(ctx->properties[n].name, name))
+ return n;
+ }
+ return -1;
+}
+
+static bool is_property_set(int action, void *val)
+{
+ switch (action) {
+ case M_PROPERTY_SET:
+ case M_PROPERTY_SWITCH:
+ case M_PROPERTY_SET_STRING:
+ case M_PROPERTY_SET_NODE:
+ case M_PROPERTY_MULTIPLY:
+ return true;
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *key = val;
+ return is_property_set(key->action, key->arg);
+ }
+ default:
+ return false;
+ }
+}
+
+int mp_property_do(const char *name, int action, void *val,
+ struct MPContext *ctx)
+{
+ struct command_ctx *cmd = ctx->command_ctx;
+ int r = m_property_do(ctx->log, cmd->properties, name, action, val, ctx);
+
+ if (mp_msg_test(ctx->log, MSGL_V) && is_property_set(action, val)) {
+ struct m_option ot = {0};
+ void *data = val;
+ switch (action) {
+ case M_PROPERTY_SET_NODE:
+ ot.type = &m_option_type_node;
+ break;
+ case M_PROPERTY_SET_STRING:
+ ot.type = &m_option_type_string;
+ data = &val;
+ break;
+ }
+ char *t = ot.type ? m_option_print(&ot, data) : NULL;
+ MP_VERBOSE(ctx, "Set property: %s%s%s -> %d\n",
+ name, t ? "=" : "", t ? t : "", r);
+ talloc_free(t);
+ }
+ return r;
+}
+
+char *mp_property_expand_string(struct MPContext *mpctx, const char *str)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ return m_properties_expand_string(ctx->properties, str, mpctx);
+}
+
+// Before expanding properties, parse C-style escapes like "\n"
+char *mp_property_expand_escaped_string(struct MPContext *mpctx, const char *str)
+{
+ void *tmp = talloc_new(NULL);
+ bstr strb = bstr0(str);
+ bstr dst = {0};
+ while (strb.len) {
+ if (!mp_append_escaped_string(tmp, &dst, &strb)) {
+ talloc_free(tmp);
+ return talloc_strdup(NULL, "(broken escape sequences)");
+ }
+ // pass " through literally
+ if (!bstr_eatstart0(&strb, "\""))
+ break;
+ bstr_xappend(tmp, &dst, bstr0("\""));
+ }
+ char *r = mp_property_expand_string(mpctx, dst.start);
+ talloc_free(tmp);
+ return r;
+}
+
+void property_print_help(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ m_properties_print_help_list(mpctx->log, ctx->properties);
+}
+
+/* List of default ways to show a property on OSD.
+ *
+ * If osd_progbar is set, a bar showing the current position between min/max
+ * values of the property is shown. In this case osd_msg is only used for
+ * terminal output if there is no video; it'll be a label shown together with
+ * percentage.
+ */
+static const struct property_osd_display {
+ // property name
+ const char *name;
+ // name used on OSD
+ const char *osd_name;
+ // progressbar type
+ int osd_progbar;
+ // Needs special ways to display the new value (seeks are delayed)
+ int seek_msg, seek_bar;
+ // Show a marker thing on OSD bar. Ignored if osd_progbar==0.
+ float marker;
+ // Free-form message (if NULL, osd_name or the property name is used)
+ const char *msg;
+} property_osd_display[] = {
+ // general
+ {"loop-playlist", "Loop"},
+ {"loop-file", "Loop current file"},
+ {"chapter",
+ .seek_msg = OSD_SEEK_INFO_CHAPTER_TEXT,
+ .seek_bar = OSD_SEEK_INFO_BAR},
+ {"hr-seek", "hr-seek"},
+ {"speed", "Speed"},
+ {"clock", "Clock"},
+ {"edition", "Edition"},
+ // audio
+ {"volume", "Volume",
+ .msg = "Volume: ${?volume:${volume}% ${?mute==yes:(Muted)}}${!volume:${volume}}",
+ .osd_progbar = OSD_VOLUME, .marker = 100},
+ {"ao-volume", "AO Volume",
+ .msg = "AO Volume: ${?ao-volume:${ao-volume}% ${?ao-mute==yes:(Muted)}}${!ao-volume:${ao-volume}}",
+ .osd_progbar = OSD_VOLUME, .marker = 100},
+ {"mute", "Mute"},
+ {"ao-mute", "AO Mute"},
+ {"audio-delay", "A-V delay"},
+ {"audio", "Audio"},
+ // video
+ {"panscan", "Panscan", .osd_progbar = OSD_PANSCAN},
+ {"taskbar-progress", "Progress in taskbar"},
+ {"snap-window", "Snap to screen edges"},
+ {"ontop", "Stay on top"},
+ {"on-all-workspaces", "Visibility on all workspaces"},
+ {"border", "Border"},
+ {"framedrop", "Framedrop"},
+ {"deinterlace", "Deinterlace"},
+ {"gamma", "Gamma", .osd_progbar = OSD_BRIGHTNESS },
+ {"brightness", "Brightness", .osd_progbar = OSD_BRIGHTNESS},
+ {"contrast", "Contrast", .osd_progbar = OSD_CONTRAST},
+ {"saturation", "Saturation", .osd_progbar = OSD_SATURATION},
+ {"hue", "Hue", .osd_progbar = OSD_HUE},
+ {"angle", "Angle"},
+ // subs
+ {"sub", "Subtitles"},
+ {"secondary-sid", "Secondary subtitles"},
+ {"sub-pos", "Sub position"},
+ {"sub-delay", "Sub delay"},
+ {"sub-speed", "Sub speed"},
+ {"sub-visibility",
+ .msg = "Subtitles ${!sub-visibility==yes:hidden}"
+ "${?sub-visibility==yes:visible${?sub==no: (but no subtitles selected)}}"},
+ {"secondary-sub-visibility",
+ .msg = "Secondary Subtitles ${!secondary-sub-visibility==yes:hidden}"
+ "${?secondary-sub-visibility==yes:visible${?secondary-sid==no: (but no secondary subtitles selected)}}"},
+ {"sub-forced-events-only", "Forced sub only"},
+ {"sub-scale", "Sub Scale"},
+ {"sub-ass-vsfilter-aspect-compat", "Subtitle VSFilter aspect compat"},
+ {"sub-ass-override", "ASS subtitle style override"},
+ {"vf", "Video filters", .msg = "Video filters:\n${vf}"},
+ {"af", "Audio filters", .msg = "Audio filters:\n${af}"},
+ {"ab-loop-a", "A-B loop start"},
+ {"ab-loop-b", .msg = "A-B loop: ${ab-loop-a} - ${ab-loop-b}"
+ "${?=ab-loop-count==0: (disabled)}"},
+ {"audio-device", "Audio device"},
+ {"hwdec", .msg = "Hardware decoding: ${hwdec-current}"},
+ {"video-aspect-override", "Aspect ratio override"},
+ // By default, don't display the following properties on OSD
+ {"pause", NULL},
+ {"fullscreen", NULL},
+ {"window-minimized", NULL},
+ {"window-maximized", NULL},
+ {0}
+};
+
+static void show_property_osd(MPContext *mpctx, const char *name, int osd_mode)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct property_osd_display disp = {.name = name, .osd_name = name};
+
+ if (!osd_mode)
+ return;
+
+ // look for the command
+ for (const struct property_osd_display *p = property_osd_display; p->name; p++)
+ {
+ if (!strcmp(p->name, name)) {
+ disp = *p;
+ break;
+ }
+ }
+
+ if (osd_mode == MP_ON_OSD_AUTO) {
+ osd_mode =
+ ((disp.msg || disp.osd_name || disp.seek_msg) ? MP_ON_OSD_MSG : 0) |
+ ((disp.osd_progbar || disp.seek_bar) ? MP_ON_OSD_BAR : 0);
+ }
+
+ if (!disp.osd_progbar)
+ disp.osd_progbar = ' ';
+
+ if (!disp.osd_name)
+ disp.osd_name = name;
+
+ if (disp.seek_msg || disp.seek_bar) {
+ mpctx->add_osd_seek_info |=
+ (osd_mode & MP_ON_OSD_MSG ? disp.seek_msg : 0) |
+ (osd_mode & MP_ON_OSD_BAR ? disp.seek_bar : 0);
+ return;
+ }
+
+ struct m_option prop = {0};
+ mp_property_do(name, M_PROPERTY_GET_CONSTRICTED_TYPE, &prop, mpctx);
+ if ((osd_mode & MP_ON_OSD_BAR)) {
+ if (prop.type == CONF_TYPE_INT && prop.min < prop.max) {
+ int n = prop.min;
+ if (disp.osd_progbar)
+ n = disp.marker;
+ int i;
+ if (mp_property_do(name, M_PROPERTY_GET, &i, mpctx) > 0)
+ set_osd_bar(mpctx, disp.osd_progbar, prop.min, prop.max, n, i);
+ } else if (prop.type == CONF_TYPE_FLOAT && prop.min < prop.max) {
+ float n = prop.min;
+ if (disp.osd_progbar)
+ n = disp.marker;
+ float f;
+ if (mp_property_do(name, M_PROPERTY_GET, &f, mpctx) > 0)
+ set_osd_bar(mpctx, disp.osd_progbar, prop.min, prop.max, n, f);
+ }
+ }
+
+ if (osd_mode & MP_ON_OSD_MSG) {
+ void *tmp = talloc_new(NULL);
+
+ const char *msg = disp.msg;
+ if (!msg)
+ msg = talloc_asprintf(tmp, "%s: ${%s}", disp.osd_name, name);
+
+ char *osd_msg = talloc_steal(tmp, mp_property_expand_string(mpctx, msg));
+
+ if (osd_msg && osd_msg[0])
+ set_osd_msg(mpctx, 1, opts->osd_duration, "%s", osd_msg);
+
+ talloc_free(tmp);
+ }
+}
+
+static bool reinit_filters(MPContext *mpctx, enum stream_type mediatype)
+{
+ switch (mediatype) {
+ case STREAM_VIDEO:
+ return reinit_video_filters(mpctx) >= 0;
+ case STREAM_AUDIO:
+ return reinit_audio_filters(mpctx) >= 0;
+ }
+ return false;
+}
+
+static const char *const filter_opt[STREAM_TYPE_COUNT] = {
+ [STREAM_VIDEO] = "vf",
+ [STREAM_AUDIO] = "af",
+};
+
+static int set_filters(struct MPContext *mpctx, enum stream_type mediatype,
+ struct m_obj_settings *new_chain)
+{
+ bstr option = bstr0(filter_opt[mediatype]);
+ struct m_config_option *co = m_config_get_co(mpctx->mconfig, option);
+ if (!co)
+ return -1;
+
+ struct m_obj_settings **list = co->data;
+ struct m_obj_settings *old_settings = *list;
+ *list = NULL;
+ m_option_copy(co->opt, list, &new_chain);
+
+ bool success = reinit_filters(mpctx, mediatype);
+
+ if (success) {
+ m_option_free(co->opt, &old_settings);
+ m_config_notify_change_opt_ptr(mpctx->mconfig, list);
+ } else {
+ m_option_free(co->opt, list);
+ *list = old_settings;
+ }
+
+ return success ? 0 : -1;
+}
+
+static int edit_filters(struct MPContext *mpctx, struct mp_log *log,
+ enum stream_type mediatype,
+ const char *cmd, const char *arg)
+{
+ bstr option = bstr0(filter_opt[mediatype]);
+ struct m_config_option *co = m_config_get_co(mpctx->mconfig, option);
+ if (!co)
+ return -1;
+
+ // The option parser is used to modify the filter list itself.
+ char optname[20];
+ snprintf(optname, sizeof(optname), "%.*s-%s", BSTR_P(option), cmd);
+
+ struct m_obj_settings *new_chain = NULL;
+ m_option_copy(co->opt, &new_chain, co->data);
+
+ int r = m_option_parse(log, co->opt, bstr0(optname), bstr0(arg), &new_chain);
+ if (r >= 0)
+ r = set_filters(mpctx, mediatype, new_chain);
+
+ m_option_free(co->opt, &new_chain);
+
+ return r >= 0 ? 0 : -1;
+}
+
+static int edit_filters_osd(struct MPContext *mpctx, enum stream_type mediatype,
+ const char *cmd, const char *arg, bool on_osd)
+{
+ int r = edit_filters(mpctx, mpctx->log, mediatype, cmd, arg);
+ if (on_osd) {
+ if (r >= 0) {
+ const char *prop = filter_opt[mediatype];
+ show_property_osd(mpctx, prop, MP_ON_OSD_MSG);
+ } else {
+ set_osd_msg(mpctx, 1, mpctx->opts->osd_duration,
+ "Changing filters failed!");
+ }
+ }
+ return r;
+}
+
+static void recreate_overlays(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ int overlay_next = !cmd->overlay_osd_current;
+ struct sub_bitmaps *new = &cmd->overlay_osd[overlay_next];
+ new->format = SUBBITMAP_BGRA;
+ new->change_id = 1;
+
+ bool valid = false;
+
+ new->num_parts = 0;
+ for (int n = 0; n < cmd->num_overlays; n++) {
+ struct overlay *o = &cmd->overlays[n];
+ if (o->source) {
+ struct mp_image *s = o->source;
+ struct sub_bitmap b = {
+ .bitmap = s->planes[0],
+ .stride = s->stride[0],
+ .w = s->w, .dw = s->w,
+ .h = s->h, .dh = s->h,
+ .x = o->x,
+ .y = o->y,
+ };
+ MP_TARRAY_APPEND(cmd, new->parts, new->num_parts, b);
+ }
+ }
+
+ if (!cmd->overlay_packer)
+ cmd->overlay_packer = talloc_zero(cmd, struct bitmap_packer);
+
+ cmd->overlay_packer->padding = 1; // assume bilinear scaling
+ packer_set_size(cmd->overlay_packer, new->num_parts);
+
+ for (int n = 0; n < new->num_parts; n++)
+ cmd->overlay_packer->in[n] = (struct pos){new->parts[n].w, new->parts[n].h};
+
+ if (packer_pack(cmd->overlay_packer) < 0 || new->num_parts == 0)
+ goto done;
+
+ struct pos bb[2];
+ packer_get_bb(cmd->overlay_packer, bb);
+
+ new->packed_w = bb[1].x;
+ new->packed_h = bb[1].y;
+
+ if (!new->packed || new->packed->w < new->packed_w ||
+ new->packed->h < new->packed_h)
+ {
+ talloc_free(new->packed);
+ new->packed = mp_image_alloc(IMGFMT_BGRA, cmd->overlay_packer->w,
+ cmd->overlay_packer->h);
+ if (!new->packed)
+ goto done;
+ }
+
+ if (!mp_image_make_writeable(new->packed))
+ goto done;
+
+ // clear padding
+ mp_image_clear(new->packed, 0, 0, new->packed->w, new->packed->h);
+
+ for (int n = 0; n < new->num_parts; n++) {
+ struct sub_bitmap *b = &new->parts[n];
+ struct pos pos = cmd->overlay_packer->result[n];
+
+ int stride = new->packed->stride[0];
+ void *pdata = (uint8_t *)new->packed->planes[0] + pos.y * stride + pos.x * 4;
+ memcpy_pic(pdata, b->bitmap, b->w * 4, b->h, stride, b->stride);
+
+ b->bitmap = pdata;
+ b->stride = stride;
+
+ b->src_x = pos.x;
+ b->src_y = pos.y;
+ }
+
+ valid = true;
+done:
+ if (!valid) {
+ new->format = SUBBITMAP_EMPTY;
+ new->num_parts = 0;
+ }
+
+ osd_set_external2(mpctx->osd, new);
+ mp_wakeup_core(mpctx);
+ cmd->overlay_osd_current = overlay_next;
+}
+
+// Set overlay with the given ID to the contents as described by "new".
+static void replace_overlay(struct MPContext *mpctx, int id, struct overlay *new)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ assert(id >= 0);
+ if (id >= cmd->num_overlays) {
+ MP_TARRAY_GROW(cmd, cmd->overlays, id);
+ while (cmd->num_overlays <= id)
+ cmd->overlays[cmd->num_overlays++] = (struct overlay){0};
+ }
+
+ struct overlay *ptr = &cmd->overlays[id];
+
+ talloc_free(ptr->source);
+ *ptr = *new;
+
+ recreate_overlays(mpctx);
+}
+
+static void cmd_overlay_add(void *pcmd)
+{
+ struct mp_cmd_ctx *cmd = pcmd;
+ struct MPContext *mpctx = cmd->mpctx;
+ int id = cmd->args[0].v.i, x = cmd->args[1].v.i, y = cmd->args[2].v.i;
+ char *file = cmd->args[3].v.s;
+ int offset = cmd->args[4].v.i;
+ char *fmt = cmd->args[5].v.s;
+ int w = cmd->args[6].v.i, h = cmd->args[7].v.i, stride = cmd->args[8].v.i;
+
+ if (strcmp(fmt, "bgra") != 0) {
+ MP_ERR(mpctx, "overlay-add: unsupported OSD format '%s'\n", fmt);
+ goto error;
+ }
+ if (id < 0 || id >= 64) { // arbitrary upper limit
+ MP_ERR(mpctx, "overlay-add: invalid id %d\n", id);
+ goto error;
+ }
+ if (w <= 0 || h <= 0 || stride < w * 4 || (stride % 4)) {
+ MP_ERR(mpctx, "overlay-add: inconsistent parameters\n");
+ goto error;
+ }
+ struct overlay overlay = {
+ .source = mp_image_alloc(IMGFMT_BGRA, w, h),
+ .x = x,
+ .y = y,
+ };
+ if (!overlay.source)
+ goto error;
+ int fd = -1;
+ bool close_fd = true;
+ void *p = NULL;
+ if (file[0] == '@') {
+ char *end;
+ fd = strtol(&file[1], &end, 10);
+ if (!file[1] || end[0])
+ fd = -1;
+ close_fd = false;
+ } else if (file[0] == '&') {
+ char *end;
+ unsigned long long addr = strtoull(&file[1], &end, 0);
+ if (!file[1] || end[0])
+ addr = 0;
+ p = (void *)(uintptr_t)addr;
+ } else {
+ fd = open(file, O_RDONLY | O_BINARY | O_CLOEXEC);
+ }
+ int map_size = 0;
+ if (fd >= 0) {
+ map_size = offset + h * stride;
+ void *m = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (close_fd)
+ close(fd);
+ if (m && m != MAP_FAILED)
+ p = m;
+ }
+ if (!p) {
+ MP_ERR(mpctx, "overlay-add: could not open or map '%s'\n", file);
+ talloc_free(overlay.source);
+ goto error;
+ }
+ memcpy_pic(overlay.source->planes[0], (char *)p + offset, w * 4, h,
+ overlay.source->stride[0], stride);
+ if (map_size)
+ munmap(p, map_size);
+
+ replace_overlay(mpctx, id, &overlay);
+ return;
+error:
+ cmd->success = false;
+}
+
+static void cmd_overlay_remove(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct command_ctx *cmdctx = mpctx->command_ctx;
+ int id = cmd->args[0].v.i;
+ if (id >= 0 && id < cmdctx->num_overlays)
+ replace_overlay(mpctx, id, &(struct overlay){0});
+}
+
+static void overlay_uninit(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ if (!mpctx->osd)
+ return;
+ for (int id = 0; id < cmd->num_overlays; id++)
+ replace_overlay(mpctx, id, &(struct overlay){0});
+ osd_set_external2(mpctx->osd, NULL);
+ for (int n = 0; n < 2; n++)
+ mp_image_unrefp(&cmd->overlay_osd[n].packed);
+}
+
+static void cmd_osd_overlay(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ double rc[4] = {0};
+
+ struct osd_external_ass ov = {
+ .owner = cmd->cmd->sender,
+ .id = cmd->args[0].v.i64,
+ .format = cmd->args[1].v.i,
+ .data = cmd->args[2].v.s,
+ .res_x = cmd->args[3].v.i,
+ .res_y = cmd->args[4].v.i,
+ .z = cmd->args[5].v.i,
+ .hidden = cmd->args[6].v.b,
+ .out_rc = cmd->args[7].v.b ? rc : NULL,
+ };
+
+ osd_set_external(mpctx->osd, &ov);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+
+ // (An empty rc uses INFINITY, avoid in JSON, just leave it unset.)
+ if (rc[0] < rc[2] && rc[1] < rc[3]) {
+ node_map_add_double(res, "x0", rc[0]);
+ node_map_add_double(res, "y0", rc[1]);
+ node_map_add_double(res, "x1", rc[2]);
+ node_map_add_double(res, "y1", rc[3]);
+ }
+
+ mp_wakeup_core(mpctx);
+}
+
+static struct track *find_track_with_url(struct MPContext *mpctx, int type,
+ const char *url)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track && track->type == type && track->is_external &&
+ strcmp(track->external_filename, url) == 0)
+ return track;
+ }
+ return NULL;
+}
+
+// Whether this property should react to key events generated by auto-repeat.
+static bool check_property_autorepeat(char *property, struct MPContext *mpctx)
+{
+ struct m_option prop = {0};
+ if (mp_property_do(property, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0)
+ return true;
+
+ // This is a heuristic at best.
+ if (prop.type->flags & M_OPT_TYPE_CHOICE)
+ return false;
+
+ return true;
+}
+
+// Whether changes to this property (add/cycle cmds) benefit from cmd->scale
+static bool check_property_scalable(char *property, struct MPContext *mpctx)
+{
+ struct m_option prop = {0};
+ if (mp_property_do(property, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0)
+ return true;
+
+ // These properties are backed by a floating-point number
+ return prop.type == &m_option_type_float ||
+ prop.type == &m_option_type_double ||
+ prop.type == &m_option_type_time ||
+ prop.type == &m_option_type_aspect;
+}
+
+static void show_property_status(struct mp_cmd_ctx *cmd, const char *name, int r)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ struct MPOpts *opts = mpctx->opts;
+ int osd_duration = opts->osd_duration;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ if (r == M_PROPERTY_OK || r == M_PROPERTY_UNAVAILABLE) {
+ show_property_osd(mpctx, name, cmd->on_osd);
+ if (r == M_PROPERTY_UNAVAILABLE)
+ cmd->success = false;
+ } else if (r == M_PROPERTY_UNKNOWN) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown property: '%s'", name);
+ cmd->success = false;
+ } else if (r <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Failed to set property '%s'",
+ name);
+ cmd->success = false;
+ }
+}
+
+static void change_property_cmd(struct mp_cmd_ctx *cmd,
+ const char *name, int action, void *arg)
+{
+ int r = mp_property_do(name, action, arg, cmd->mpctx);
+ show_property_status(cmd, name, r);
+}
+
+static void cmd_cycle_values(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int first = 0, dir = 1;
+
+ if (strcmp(cmd->args[first].v.s, "!reverse") == 0) {
+ first += 1;
+ dir = -1;
+ }
+
+ const char *name = cmd->args[first].v.s;
+ first += 1;
+
+ if (first >= cmd->num_args) {
+ MP_ERR(mpctx, "cycle-values command does not have any value arguments.\n");
+ cmd->success = false;
+ return;
+ }
+
+ struct m_option prop = {0};
+ int r = mp_property_do(name, M_PROPERTY_GET_TYPE, &prop, mpctx);
+ if (r <= 0) {
+ show_property_status(cmd, name, r);
+ return;
+ }
+
+ union m_option_value curval = m_option_value_default;
+ r = mp_property_do(name, M_PROPERTY_GET, &curval, mpctx);
+ if (r <= 0) {
+ show_property_status(cmd, name, r);
+ return;
+ }
+
+ int current = -1;
+ for (int n = first; n < cmd->num_args; n++) {
+ union m_option_value val = m_option_value_default;
+ if (m_option_parse(mpctx->log, &prop, bstr0(name),
+ bstr0(cmd->args[n].v.s), &val) < 0)
+ continue;
+
+ if (m_option_equal(&prop, &curval, &val))
+ current = n;
+
+ m_option_free(&prop, &val);
+
+ if (current >= 0)
+ break;
+ }
+
+ m_option_free(&prop, &curval);
+
+ if (current >= 0) {
+ current += dir;
+ if (current < first)
+ current = cmd->num_args - 1;
+ if (current >= cmd->num_args)
+ current = first;
+ } else {
+ MP_VERBOSE(mpctx, "Current value not found. Picking default.\n");
+ current = dir > 0 ? first : cmd->num_args - 1;
+ }
+
+ change_property_cmd(cmd, name, M_PROPERTY_SET_STRING, cmd->args[current].v.s);
+}
+
+struct cmd_list_ctx {
+ struct MPContext *mpctx;
+
+ // actual list command
+ struct mp_cmd_ctx *parent;
+
+ bool current_valid;
+ mp_thread_id current_tid;
+ bool completed_recursive;
+
+ // list of sub commands yet to run
+ struct mp_cmd **sub;
+ int num_sub;
+};
+
+static void continue_cmd_list(struct cmd_list_ctx *list);
+
+static void on_cmd_list_sub_completion(struct mp_cmd_ctx *cmd)
+{
+ struct cmd_list_ctx *list = cmd->on_completion_priv;
+
+ if (list->current_valid && mp_thread_id_equal(list->current_tid, mp_thread_current_id())) {
+ list->completed_recursive = true;
+ } else {
+ continue_cmd_list(list);
+ }
+}
+
+static void continue_cmd_list(struct cmd_list_ctx *list)
+{
+ while (list->parent->args[0].v.p) {
+ struct mp_cmd *sub = list->parent->args[0].v.p;
+ list->parent->args[0].v.p = sub->queue_next;
+
+ ta_set_parent(sub, NULL);
+
+ if (sub->flags & MP_ASYNC_CMD) {
+ // We run it "detached" (fire & forget)
+ run_command(list->mpctx, sub, NULL, NULL, NULL);
+ } else {
+ // Run the next command once this one completes.
+
+ list->completed_recursive = false;
+ list->current_valid = true;
+ list->current_tid = mp_thread_current_id();
+
+ run_command(list->mpctx, sub, NULL, on_cmd_list_sub_completion, list);
+
+ list->current_valid = false;
+
+ // run_command() either recursively calls the completion function,
+ // or lets the command continue run in the background. If it was
+ // completed recursively, we can just continue our loop. Otherwise
+ // the completion handler will invoke this loop again elsewhere.
+ // We could unconditionally call continue_cmd_list() in the handler
+ // instead, but then stack depth would grow with list length.
+ if (!list->completed_recursive)
+ return;
+ }
+ }
+
+ mp_cmd_ctx_complete(list->parent);
+ talloc_free(list);
+}
+
+static void cmd_list(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ cmd->completed = false;
+
+ struct cmd_list_ctx *list = talloc_zero(NULL, struct cmd_list_ctx);
+ list->mpctx = cmd->mpctx;
+ list->parent = p;
+
+ continue_cmd_list(list);
+}
+
+const struct mp_cmd_def mp_cmd_list = { "list", cmd_list, .exec_async = true };
+
+// Signal that the command is complete now. This also deallocates cmd.
+// You must call this function in a state where the core is locked for the
+// current thread (e.g. from the main thread, or from within mp_dispatch_lock()).
+// Completion means the command is finished, even if it errored or never ran.
+// Keep in mind that calling this can execute further user command that can
+// change arbitrary state (due to cmd_list).
+void mp_cmd_ctx_complete(struct mp_cmd_ctx *cmd)
+{
+ cmd->completed = true;
+ if (!cmd->success)
+ mpv_free_node_contents(&cmd->result);
+ if (cmd->on_completion)
+ cmd->on_completion(cmd);
+ if (cmd->abort)
+ mp_abort_remove(cmd->mpctx, cmd->abort);
+ mpv_free_node_contents(&cmd->result);
+ talloc_free(cmd);
+}
+
+static void run_command_on_worker_thread(void *p)
+{
+ struct mp_cmd_ctx *ctx = p;
+ struct MPContext *mpctx = ctx->mpctx;
+
+ mp_core_lock(mpctx);
+
+ bool exec_async = ctx->cmd->def->exec_async;
+ ctx->cmd->def->handler(ctx);
+ if (!exec_async)
+ mp_cmd_ctx_complete(ctx);
+
+ mpctx->outstanding_async -= 1;
+ if (!mpctx->outstanding_async && mp_is_shutting_down(mpctx))
+ mp_wakeup_core(mpctx);
+
+ mp_core_unlock(mpctx);
+}
+
+// Run the given command. Upon command completion, on_completion is called. This
+// can happen within the function, or for async commands, some time after the
+// function returns (the caller is supposed to be able to handle both cases). In
+// both cases, the callback will be called while the core is locked (i.e. you
+// can access the core freely).
+// If abort is non-NULL, then the caller creates the abort object. It must have
+// been allocated with talloc. run_command() will register/unregister/destroy
+// it. Must not be set if cmd->def->can_abort==false.
+// on_completion_priv is copied to mp_cmd_ctx.on_completion_priv and can be
+// accessed from the completion callback.
+// The completion callback is invoked exactly once. If it's NULL, it's ignored.
+// Ownership of cmd goes to the caller.
+void run_command(struct MPContext *mpctx, struct mp_cmd *cmd,
+ struct mp_abort_entry *abort,
+ void (*on_completion)(struct mp_cmd_ctx *cmd),
+ void *on_completion_priv)
+{
+ struct mp_cmd_ctx *ctx = talloc(NULL, struct mp_cmd_ctx);
+ *ctx = (struct mp_cmd_ctx){
+ .mpctx = mpctx,
+ .cmd = talloc_steal(ctx, cmd),
+ .args = cmd->args,
+ .num_args = cmd->nargs,
+ .priv = cmd->def->priv,
+ .abort = talloc_steal(ctx, abort),
+ .success = true,
+ .completed = true,
+ .on_completion = on_completion,
+ .on_completion_priv = on_completion_priv,
+ };
+
+ if (!ctx->abort && cmd->def->can_abort)
+ ctx->abort = talloc_zero(ctx, struct mp_abort_entry);
+
+ assert(cmd->def->can_abort == !!ctx->abort);
+
+ if (ctx->abort) {
+ ctx->abort->coupled_to_playback |= cmd->def->abort_on_playback_end;
+ mp_abort_add(mpctx, ctx->abort);
+ }
+
+ struct MPOpts *opts = mpctx->opts;
+ ctx->on_osd = cmd->flags & MP_ON_OSD_FLAGS;
+ bool auto_osd = ctx->on_osd == MP_ON_OSD_AUTO;
+ ctx->msg_osd = auto_osd || (ctx->on_osd & MP_ON_OSD_MSG);
+ ctx->bar_osd = auto_osd || (ctx->on_osd & MP_ON_OSD_BAR);
+ ctx->seek_msg_osd = auto_osd ? opts->osd_on_seek & 2 : ctx->msg_osd;
+ ctx->seek_bar_osd = auto_osd ? opts->osd_on_seek & 1 : ctx->bar_osd;
+
+ bool noise = cmd->def->is_noisy || cmd->mouse_move;
+ mp_cmd_dump(mpctx->log, noise ? MSGL_TRACE : MSGL_DEBUG, "Run command:", cmd);
+
+ if (cmd->flags & MP_EXPAND_PROPERTIES) {
+ for (int n = 0; n < cmd->nargs; n++) {
+ if (cmd->args[n].type->type == CONF_TYPE_STRING) {
+ char *s = mp_property_expand_string(mpctx, cmd->args[n].v.s);
+ if (!s) {
+ ctx->success = false;
+ mp_cmd_ctx_complete(ctx);
+ return;
+ }
+ talloc_free(cmd->args[n].v.s);
+ cmd->args[n].v.s = s;
+ }
+ }
+ }
+
+ if (cmd->def->spawn_thread) {
+ mpctx->outstanding_async += 1; // prevent that core disappears
+ if (!mp_thread_pool_queue(mpctx->thread_pool,
+ run_command_on_worker_thread, ctx))
+ {
+ mpctx->outstanding_async -= 1;
+ ctx->success = false;
+ mp_cmd_ctx_complete(ctx);
+ }
+ } else {
+ bool exec_async = cmd->def->exec_async;
+ cmd->def->handler(ctx);
+ if (!exec_async)
+ mp_cmd_ctx_complete(ctx);
+ }
+}
+
+// When a command shows a message. status is the level (e.g. MSGL_INFO), and
+// msg+vararg is as in printf (don't include a trailing "\n").
+void mp_cmd_msg(struct mp_cmd_ctx *cmd, int status, const char *msg, ...)
+{
+ va_list ap;
+ char *s;
+
+ va_start(ap, msg);
+ s = talloc_vasprintf(NULL, msg, ap);
+ va_end(ap);
+
+ MP_MSG(cmd->mpctx, status, "%s\n", s);
+ if (cmd->msg_osd && status <= MSGL_INFO)
+ set_osd_msg(cmd->mpctx, 1, cmd->mpctx->opts->osd_duration, "%s", s);
+
+ talloc_free(s);
+}
+
+static void cmd_seek(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ double v = cmd->args[0].v.d * cmd->cmd->scale;
+ int abs = cmd->args[1].v.i & 3;
+ enum seek_precision precision = MPSEEK_DEFAULT;
+ switch (((cmd->args[2].v.i | cmd->args[1].v.i) >> 3) & 3) {
+ case 1: precision = MPSEEK_KEYFRAME; break;
+ case 2: precision = MPSEEK_EXACT; break;
+ }
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ mark_seek(mpctx);
+ switch (abs) {
+ case 0: { // Relative seek
+ queue_seek(mpctx, MPSEEK_RELATIVE, v, precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, (v > 0) ? OSD_FFW : OSD_REW);
+ break;
+ }
+ case 1: { // Absolute seek by percentage
+ double ratio = v / 100.0;
+ double cur_pos = get_current_pos_ratio(mpctx, false);
+ queue_seek(mpctx, MPSEEK_FACTOR, ratio, precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, cur_pos < ratio ? OSD_FFW : OSD_REW);
+ break;
+ }
+ case 2: { // Absolute seek to a timestamp in seconds
+ if (v < 0) {
+ // Seek from end
+ double len = get_time_length(mpctx);
+ if (len < 0) {
+ cmd->success = false;
+ return;
+ }
+ v = MPMAX(0, len + v);
+ }
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, v, precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx,
+ v > get_current_time(mpctx) ? OSD_FFW : OSD_REW);
+ break;
+ }
+ case 3: { // Relative seek by percentage
+ queue_seek(mpctx, MPSEEK_FACTOR,
+ get_current_pos_ratio(mpctx, false) + v / 100.0,
+ precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, v > 0 ? OSD_FFW : OSD_REW);
+ break;
+ }}
+ if (cmd->seek_bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (cmd->seek_msg_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+}
+
+static void cmd_revert_seek(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct command_ctx *cmdctx = mpctx->command_ctx;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ double oldpts = cmdctx->last_seek_pts;
+ if (cmdctx->marked_pts != MP_NOPTS_VALUE)
+ oldpts = cmdctx->marked_pts;
+ if (cmd->args[0].v.i & 3) {
+ cmdctx->marked_pts = get_current_time(mpctx);
+ cmdctx->marked_permanent = cmd->args[0].v.i & 1;
+ } else if (oldpts != MP_NOPTS_VALUE) {
+ if (!cmdctx->marked_permanent) {
+ cmdctx->marked_pts = MP_NOPTS_VALUE;
+ cmdctx->last_seek_pts = get_current_time(mpctx);
+ }
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, oldpts, MPSEEK_EXACT,
+ MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, OSD_REW);
+ if (cmd->seek_bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (cmd->seek_msg_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+ } else {
+ cmd->success = false;
+ }
+}
+
+static void cmd_set(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ change_property_cmd(cmd, cmd->args[0].v.s,
+ M_PROPERTY_SET_STRING, cmd->args[1].v.s);
+}
+
+static void cmd_del(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ const char *name = cmd->args[0].v.s;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+ int osd_duration = mpctx->opts->osd_duration;
+
+ int r = mp_property_do(name, M_PROPERTY_DELETE, NULL, mpctx);
+
+ if (r == M_PROPERTY_OK) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Deleted property: '%s'", name);
+ cmd->success = true;
+ } else if (r == M_PROPERTY_UNKNOWN) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown property: '%s'", name);
+ cmd->success = false;
+ } else if (r <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Failed to set property '%s'",
+ name);
+ cmd->success = false;
+ }
+}
+
+static void cmd_change_list(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char *name = cmd->args[0].v.s;
+ char *op = cmd->args[1].v.s;
+ char *value = cmd->args[2].v.s;
+ int osd_duration = mpctx->opts->osd_duration;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ struct m_option prop = {0};
+ if (mp_property_do(name, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown option: '%s'", name);
+ cmd->success = false;
+ return;
+ }
+
+ const struct m_option_type *type = prop.type;
+ bool found = false;
+ for (int i = 0; type->actions && type->actions[i].name; i++) {
+ const struct m_option_action *action = &type->actions[i];
+ if (strcmp(action->name, op) == 0)
+ found = true;
+ }
+ if (!found) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown action: '%s'", op);
+ cmd->success = false;
+ return;
+ }
+
+ union m_option_value val = m_option_value_default;
+ if (mp_property_do(name, M_PROPERTY_GET, &val, mpctx) <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Could not read: '%s'", name);
+ cmd->success = false;
+ return;
+ }
+
+ char *optname = mp_tprintf(80, "%s-%s", name, op); // the dirty truth
+ int r = m_option_parse(mpctx->log, &prop, bstr0(optname), bstr0(value), &val);
+ if (r >= 0 && mp_property_do(name, M_PROPERTY_SET, &val, mpctx) <= 0)
+ r = -1;
+ m_option_free(&prop, &val);
+ if (r < 0) {
+ set_osd_msg(mpctx, osdl, osd_duration,
+ "Failed setting option: '%s'", name);
+ cmd->success = false;
+ return;
+ }
+
+ show_property_osd(mpctx, name, cmd->on_osd);
+}
+
+static void cmd_add_cycle(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ bool is_cycle = !!cmd->priv;
+
+ char *property = cmd->args[0].v.s;
+ if (cmd->cmd->repeated && !check_property_autorepeat(property, mpctx) &&
+ !(cmd->cmd->flags & MP_ALLOW_REPEAT) /* "repeatable" prefix */ )
+ {
+ MP_VERBOSE(mpctx, "Dropping command '%s' from auto-repeated key.\n",
+ cmd->cmd->original);
+ return;
+ }
+
+ double scale = 1;
+ int scale_units = cmd->cmd->scale_units;
+ if (check_property_scalable(property, mpctx)) {
+ scale = cmd->cmd->scale;
+ scale_units = 1;
+ }
+
+ for (int i = 0; i < scale_units; i++) {
+ struct m_property_switch_arg s = {
+ .inc = cmd->args[1].v.d * scale,
+ .wrap = is_cycle,
+ };
+ change_property_cmd(cmd, property, M_PROPERTY_SWITCH, &s);
+ if (!cmd->success)
+ return;
+ }
+}
+
+static void cmd_multiply(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ change_property_cmd(cmd, cmd->args[0].v.s,
+ M_PROPERTY_MULTIPLY, &cmd->args[1].v.d);
+}
+
+static void cmd_frame_step(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ if (cmd->cmd->is_up_down) {
+ if (cmd->cmd->is_up) {
+ if (mpctx->step_frames < 1)
+ set_pause_state(mpctx, true);
+ } else {
+ if (cmd->cmd->repeated) {
+ set_pause_state(mpctx, false);
+ } else {
+ add_step_frame(mpctx, 1);
+ }
+ }
+ } else {
+ add_step_frame(mpctx, 1);
+ }
+}
+
+static void cmd_frame_back_step(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ add_step_frame(mpctx, -1);
+}
+
+static void cmd_quit(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ bool write_watch_later = *(bool *)cmd->priv;
+ if (write_watch_later || mpctx->opts->position_save_on_quit)
+ mp_write_watch_later_conf(mpctx);
+ mpctx->stop_play = PT_QUIT;
+ mpctx->quit_custom_rc = cmd->args[0].v.i;
+ mpctx->has_quit_custom_rc = true;
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_playlist_next_prev(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int dir = *(int *)cmd->priv;
+ int force = cmd->args[0].v.i;
+
+ struct playlist_entry *e = mp_next_file(mpctx, dir, force);
+ if (!e && !force) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_set_playlist_entry(mpctx, e);
+ if (cmd->on_osd & MP_ON_OSD_MSG)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_CURRENT_FILE;
+}
+
+static void cmd_playlist_next_prev_playlist(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int direction = *(int *)cmd->priv;
+
+ struct playlist_entry *entry =
+ playlist_get_first_in_next_playlist(mpctx->playlist, direction);
+
+ if (!entry && mpctx->opts->loop_times != 1 && mpctx->playlist->current) {
+ entry = direction > 0 ? playlist_get_first(mpctx->playlist)
+ : playlist_get_last(mpctx->playlist);
+
+ if (entry && entry->playlist_path &&
+ mpctx->playlist->current->playlist_path &&
+ strcmp(entry->playlist_path,
+ mpctx->playlist->current->playlist_path) == 0)
+ entry = NULL;
+
+ if (direction > 0 && entry && mpctx->opts->loop_times > 1) {
+ mpctx->opts->loop_times--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig,
+ &mpctx->opts->loop_times);
+ }
+
+ if (direction < 0)
+ entry = playlist_get_first_in_same_playlist(
+ entry, mpctx->playlist->current->playlist_path);
+ }
+
+ if (!entry) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_set_playlist_entry(mpctx, entry);
+ if (cmd->on_osd & MP_ON_OSD_MSG)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_CURRENT_FILE;
+}
+
+static void cmd_playlist_play_index(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct playlist *pl = mpctx->playlist;
+ int pos = cmd->args[0].v.i;
+
+ if (pos == -2)
+ pos = playlist_entry_to_index(pl, pl->current);
+
+ mp_set_playlist_entry(mpctx, playlist_entry_from_index(pl, pos));
+ if (cmd->on_osd & MP_ON_OSD_MSG)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_CURRENT_FILE;
+}
+
+static void cmd_sub_step_seek(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ bool step = *(bool *)cmd->priv;
+ int track_ind = cmd->args[1].v.i;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ struct track *track = mpctx->current_track[track_ind][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ double refpts = get_current_time(mpctx);
+ if (sub && refpts != MP_NOPTS_VALUE) {
+ double a[2];
+ a[0] = refpts;
+ a[1] = cmd->args[0].v.i;
+ if (sub_control(sub, SD_CTRL_SUB_STEP, a) > 0) {
+ if (step) {
+ mpctx->opts->subs_rend->sub_delay -= a[0] - refpts;
+ m_config_notify_change_opt_ptr_notify(mpctx->mconfig,
+ &mpctx->opts->subs_rend->sub_delay);
+ show_property_osd(mpctx, "sub-delay", cmd->on_osd);
+ } else {
+ // We can easily seek/step to the wrong subtitle line (because
+ // video frame PTS and sub PTS rarely match exactly). Add an
+ // arbitrary forward offset as a workaround.
+ a[0] += SUB_SEEK_OFFSET;
+ mark_seek(mpctx);
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, a[0], MPSEEK_EXACT,
+ MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, (a[0] > refpts) ? OSD_FFW : OSD_REW);
+ if (cmd->seek_bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (cmd->seek_msg_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+ }
+ }
+ }
+}
+
+static void cmd_print_text(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ MP_INFO(mpctx, "%s\n", cmd->args[0].v.s);
+}
+
+static void cmd_show_text(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int osd_duration = mpctx->opts->osd_duration;
+
+ // if no argument supplied use default osd_duration, else <arg> ms.
+ set_osd_msg(mpctx, cmd->args[2].v.i,
+ (cmd->args[1].v.i < 0 ? osd_duration : cmd->args[1].v.i),
+ "%s", cmd->args[0].v.s);
+}
+
+static void cmd_expand_text(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ cmd->result = (mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = mp_property_expand_string(mpctx, cmd->args[0].v.s)
+ };
+}
+
+static void cmd_expand_path(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ cmd->result = (mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = mp_get_user_path(NULL, mpctx->global, cmd->args[0].v.s)
+ };
+}
+
+static void cmd_loadfile(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char *filename = cmd->args[0].v.s;
+ int append = cmd->args[1].v.i;
+
+ if (!append)
+ playlist_clear(mpctx->playlist);
+
+ struct playlist_entry *entry = playlist_entry_new(filename);
+ if (cmd->args[2].v.str_list) {
+ char **pairs = cmd->args[2].v.str_list;
+ for (int i = 0; pairs[i] && pairs[i + 1]; i += 2)
+ playlist_entry_add_param(entry, bstr0(pairs[i]), bstr0(pairs[i + 1]));
+ }
+ playlist_add(mpctx->playlist, entry);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "playlist_entry_id", entry->id);
+
+ if (!append || (append == 2 && !mpctx->playlist->current)) {
+ if (mpctx->opts->position_save_on_quit) // requested in issue #1148
+ mp_write_watch_later_conf(mpctx);
+ mp_set_playlist_entry(mpctx, entry);
+ }
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_loadlist(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char *filename = cmd->args[0].v.s;
+ int append = cmd->args[1].v.i;
+
+ struct playlist *pl = playlist_parse_file(filename, cmd->abort->cancel,
+ mpctx->global);
+ if (pl) {
+ prepare_playlist(mpctx, pl);
+ struct playlist_entry *new = pl->current;
+ if (!append)
+ playlist_clear(mpctx->playlist);
+ struct playlist_entry *first = playlist_entry_from_index(pl, 0);
+ int num_entries = pl->num_entries;
+ playlist_append_entries(mpctx->playlist, pl);
+ talloc_free(pl);
+
+ if (!new)
+ new = playlist_get_first(mpctx->playlist);
+
+ if ((!append || (append == 2 && !mpctx->playlist->current)) && new)
+ mp_set_playlist_entry(mpctx, new);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ if (num_entries) {
+ node_map_add_int64(res, "playlist_entry_id", first->id);
+ node_map_add_int64(res, "num_entries", num_entries);
+ }
+
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+ } else {
+ MP_ERR(mpctx, "Unable to load playlist %s.\n", filename);
+ cmd->success = false;
+ }
+}
+
+static void cmd_playlist_clear(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ // Supposed to clear the playlist, except the currently played item.
+ if (mpctx->playlist->current_was_replaced)
+ mpctx->playlist->current = NULL;
+ playlist_clear_except_current(mpctx->playlist);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_playlist_remove(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ struct playlist_entry *e = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[0].v.i);
+ if (cmd->args[0].v.i < 0)
+ e = mpctx->playlist->current;
+ if (!e) {
+ cmd->success = false;
+ return;
+ }
+
+ // Can't play a removed entry
+ if (mpctx->playlist->current == e && !mpctx->stop_play)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ playlist_remove(mpctx->playlist, e);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_playlist_move(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ struct playlist_entry *e1 = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[0].v.i);
+ struct playlist_entry *e2 = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[1].v.i);
+ if (!e1) {
+ cmd->success = false;
+ return;
+ }
+
+ playlist_move(mpctx->playlist, e1, e2);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+}
+
+static void cmd_playlist_shuffle(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ playlist_shuffle(mpctx->playlist);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+}
+
+static void cmd_playlist_unshuffle(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ playlist_unshuffle(mpctx->playlist);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+}
+
+static void cmd_stop(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int flags = cmd->args[0].v.i;
+
+ if (!(flags & 1))
+ playlist_clear(mpctx->playlist);
+
+ if (mpctx->opts->player_idle_mode < 2 &&
+ mpctx->opts->position_save_on_quit)
+ {
+ mp_write_watch_later_conf(mpctx);
+ }
+
+ if (mpctx->stop_play != PT_QUIT)
+ mpctx->stop_play = PT_STOP;
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_show_progress(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mpctx->add_osd_seek_info |=
+ (cmd->msg_osd ? OSD_SEEK_INFO_TEXT : 0) |
+ (cmd->bar_osd ? OSD_SEEK_INFO_BAR : 0);
+
+ // If we got neither (i.e. no-osd) force both like osd-auto.
+ if (!mpctx->add_osd_seek_info)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT | OSD_SEEK_INFO_BAR;
+ mpctx->osd_force_update = true;
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_track_add(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+ bool is_albumart = type == STREAM_VIDEO &&
+ cmd->args[4].v.b;
+
+ if (mpctx->stop_play) {
+ cmd->success = false;
+ return;
+ }
+
+ if (cmd->args[1].v.i == 2) {
+ struct track *t = find_track_with_url(mpctx, type, cmd->args[0].v.s);
+ if (t) {
+ if (mpctx->playback_initialized) {
+ mp_switch_track(mpctx, t->type, t, FLAG_MARK_SELECTION);
+ print_track_list(mpctx, "Track switched:");
+ } else {
+ mark_track_selection(mpctx, 0, t->type, t->user_tid);
+ }
+ return;
+ }
+ }
+ int first = mp_add_external_file(mpctx, cmd->args[0].v.s, type,
+ cmd->abort->cancel, is_albumart);
+ if (first < 0) {
+ cmd->success = false;
+ return;
+ }
+
+ for (int n = first; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ if (cmd->args[1].v.i == 1) {
+ t->no_default = true;
+ } else if (n == first) {
+ if (mpctx->playback_initialized) {
+ mp_switch_track(mpctx, t->type, t, FLAG_MARK_SELECTION);
+ } else {
+ mark_track_selection(mpctx, 0, t->type, t->user_tid);
+ }
+ }
+ char *title = cmd->args[2].v.s;
+ if (title && title[0])
+ t->title = talloc_strdup(t, title);
+ char *lang = cmd->args[3].v.s;
+ if (lang && lang[0])
+ t->lang = talloc_strdup(t, lang);
+ }
+
+ if (mpctx->playback_initialized)
+ print_track_list(mpctx, "Track added:");
+}
+
+static void cmd_track_remove(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+
+ struct track *t = mp_track_by_tid(mpctx, type, cmd->args[0].v.i);
+ if (!t) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_remove_track(mpctx, t);
+ if (mpctx->playback_initialized)
+ print_track_list(mpctx, "Track removed:");
+}
+
+static void cmd_track_reload(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+
+ if (!mpctx->playback_initialized) {
+ MP_ERR(mpctx, "Cannot reload while not initialized.\n");
+ cmd->success = false;
+ return;
+ }
+
+ struct track *t = mp_track_by_tid(mpctx, type, cmd->args[0].v.i);
+ int nt_num = -1;
+
+ if (t && t->is_external && t->external_filename) {
+ char *filename = talloc_strdup(NULL, t->external_filename);
+ bool is_albumart = t->attached_picture;
+ mp_remove_track(mpctx, t);
+ nt_num = mp_add_external_file(mpctx, filename, type, cmd->abort->cancel,
+ is_albumart);
+ talloc_free(filename);
+ }
+
+ if (nt_num < 0) {
+ cmd->success = false;
+ return;
+ }
+
+ struct track *nt = mpctx->tracks[nt_num];
+ mp_switch_track(mpctx, nt->type, nt, 0);
+ print_track_list(mpctx, "Reloaded:");
+}
+
+static void cmd_rescan_external_files(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (mpctx->stop_play) {
+ cmd->success = false;
+ return;
+ }
+
+ autoload_external_files(mpctx, cmd->abort->cancel);
+ if (!cmd->args[0].v.i && mpctx->playback_initialized) {
+ // somewhat fuzzy and not ideal
+ struct track *a = select_default_track(mpctx, 0, STREAM_AUDIO);
+ if (a && a->is_external)
+ mp_switch_track(mpctx, STREAM_AUDIO, a, 0);
+ struct track *s = select_default_track(mpctx, 0, STREAM_SUB);
+ if (s && s->is_external)
+ mp_switch_track(mpctx, STREAM_SUB, s, 0);
+
+ print_track_list(mpctx, "Track list:");
+ }
+}
+
+static void cmd_run(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char **args = talloc_zero_array(NULL, char *, cmd->num_args + 1);
+ for (int n = 0; n < cmd->num_args; n++)
+ args[n] = cmd->args[n].v.s;
+ mp_msg_flush_status_line(mpctx->log);
+ struct mp_subprocess_opts opts = {
+ .exe = args[0],
+ .args = args,
+ .fds = { {0, .src_fd = 0}, {1, .src_fd = 1}, {2, .src_fd = 2} },
+ .num_fds = 3,
+ .detach = true,
+ };
+ struct mp_subprocess_result res;
+ mp_subprocess2(&opts, &res);
+ if (res.error < 0) {
+ mp_err(mpctx->log, "Starting subprocess failed: %s\n",
+ mp_subprocess_err_str(res.error));
+ }
+ talloc_free(args);
+}
+
+struct subprocess_fd_ctx {
+ struct mp_log *log;
+ void* talloc_ctx;
+ int64_t max_size;
+ int msgl;
+ bool capture;
+ bstr output;
+};
+
+static void subprocess_read(void *p, char *data, size_t size)
+{
+ struct subprocess_fd_ctx *ctx = p;
+ if (ctx->capture) {
+ if (ctx->output.len < ctx->max_size)
+ bstr_xappend(ctx->talloc_ctx, &ctx->output, (bstr){data, size});
+ } else {
+ mp_msg(ctx->log, ctx->msgl, "%.*s", (int)size, data);
+ }
+}
+
+static void subprocess_write(void *p)
+{
+ // Unused; we write a full buffer.
+}
+
+static void cmd_subprocess(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char **args = cmd->args[0].v.str_list;
+ bool playback_only = cmd->args[1].v.b;
+ bool detach = cmd->args[5].v.b;
+ char **env = cmd->args[6].v.str_list;
+ bstr stdin_data = bstr0(cmd->args[7].v.s);
+ bool passthrough_stdin = cmd->args[8].v.b;
+
+ if (env && !env[0])
+ env = NULL; // do not actually set an empty environment
+
+ if (!args || !args[0]) {
+ MP_ERR(mpctx, "program name missing\n");
+ cmd->success = false;
+ return;
+ }
+
+ if (stdin_data.len && passthrough_stdin) {
+ MP_ERR(mpctx, "both stdin_data and passthrough_stdin set\n");
+ cmd->success = false;
+ return;
+ }
+
+ void *tmp = talloc_new(NULL);
+
+ struct mp_log *fdlog = mp_log_new(tmp, mpctx->log, cmd->cmd->sender);
+ struct subprocess_fd_ctx fdctx[3];
+ for (int fd = 0; fd < 3; fd++) {
+ fdctx[fd] = (struct subprocess_fd_ctx) {
+ .log = fdlog,
+ .talloc_ctx = tmp,
+ .max_size = cmd->args[2].v.i,
+ .msgl = fd == 2 ? MSGL_ERR : MSGL_INFO,
+ };
+ }
+ fdctx[1].capture = cmd->args[3].v.b;
+ fdctx[2].capture = cmd->args[4].v.b;
+
+ mp_mutex_lock(&mpctx->abort_lock);
+ cmd->abort->coupled_to_playback = playback_only;
+ mp_abort_recheck_locked(mpctx, cmd->abort);
+ mp_mutex_unlock(&mpctx->abort_lock);
+
+ mp_core_unlock(mpctx);
+
+ struct mp_subprocess_opts opts = {
+ .exe = args[0],
+ .args = args,
+ .env = env,
+ .cancel = cmd->abort->cancel,
+ .detach = detach,
+ .fds = {
+ {
+ .fd = 0, // stdin
+ .src_fd = passthrough_stdin ? 0 : -1,
+ },
+ },
+ .num_fds = 1,
+ };
+
+ // stdout, stderr
+ for (int fd = 1; fd < 3; fd++) {
+ bool capture = fdctx[fd].capture || !detach;
+ opts.fds[opts.num_fds++] = (struct mp_subprocess_fd){
+ .fd = fd,
+ .src_fd = capture ? -1 : fd,
+ .on_read = capture ? subprocess_read : NULL,
+ .on_read_ctx = &fdctx[fd],
+ };
+ }
+ // stdin
+ if (stdin_data.len) {
+ opts.fds[0] = (struct mp_subprocess_fd){
+ .fd = 0,
+ .src_fd = -1,
+ .on_write = subprocess_write,
+ .on_write_ctx = &fdctx[0],
+ .write_buf = &stdin_data,
+ };
+ }
+
+ struct mp_subprocess_result sres;
+ mp_subprocess2(&opts, &sres);
+ int status = sres.exit_status;
+ char *error = NULL;
+ if (sres.error < 0) {
+ error = (char *)mp_subprocess_err_str(sres.error);
+ status = sres.error;
+ }
+
+ mp_core_lock(mpctx);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "status", status);
+ node_map_add_flag(res, "killed_by_us", status == MP_SUBPROCESS_EKILLED_BY_US);
+ node_map_add_string(res, "error_string", error ? error : "");
+ const char *sname[] = {NULL, "stdout", "stderr"};
+ for (int fd = 1; fd < 3; fd++) {
+ if (!fdctx[fd].capture)
+ continue;
+ struct mpv_byte_array *ba =
+ node_map_add(res, sname[fd], MPV_FORMAT_BYTE_ARRAY)->u.ba;
+ *ba = (struct mpv_byte_array){
+ .data = talloc_steal(ba, fdctx[fd].output.start),
+ .size = fdctx[fd].output.len,
+ };
+ }
+
+ talloc_free(tmp);
+}
+
+static void cmd_enable_input_section(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ mp_input_enable_section(mpctx->input, cmd->args[0].v.s, cmd->args[1].v.i);
+}
+
+static void cmd_disable_input_section(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ mp_input_disable_section(mpctx->input, cmd->args[0].v.s);
+}
+
+static void cmd_define_input_section(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ mp_input_define_section(mpctx->input, cmd->args[0].v.s, "<api>",
+ cmd->args[1].v.s, !cmd->args[2].v.i,
+ cmd->cmd->sender);
+}
+
+static void cmd_ab_loop(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int osd_duration = mpctx->opts->osd_duration;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ double now = get_current_time(mpctx);
+ if (mpctx->opts->ab_loop[0] == MP_NOPTS_VALUE) {
+ mp_property_do("ab-loop-a", M_PROPERTY_SET, &now, mpctx);
+ show_property_osd(mpctx, "ab-loop-a", cmd->on_osd);
+ } else if (mpctx->opts->ab_loop[1] == MP_NOPTS_VALUE) {
+ mp_property_do("ab-loop-b", M_PROPERTY_SET, &now, mpctx);
+ show_property_osd(mpctx, "ab-loop-b", cmd->on_osd);
+ } else {
+ now = MP_NOPTS_VALUE;
+ mp_property_do("ab-loop-a", M_PROPERTY_SET, &now, mpctx);
+ mp_property_do("ab-loop-b", M_PROPERTY_SET, &now, mpctx);
+ set_osd_msg(mpctx, osdl, osd_duration, "Clear A-B loop");
+ }
+}
+
+static void cmd_align_cache_ab(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (!mpctx->demuxer)
+ return;
+
+ double a = demux_probe_cache_dump_target(mpctx->demuxer,
+ mpctx->opts->ab_loop[0], false);
+ double b = demux_probe_cache_dump_target(mpctx->demuxer,
+ mpctx->opts->ab_loop[1], true);
+
+ mp_property_do("ab-loop-a", M_PROPERTY_SET, &a, mpctx);
+ mp_property_do("ab-loop-b", M_PROPERTY_SET, &b, mpctx);
+
+ // Happens to cover both properties.
+ show_property_osd(mpctx, "ab-loop-b", cmd->on_osd);
+}
+
+static void cmd_drop_buffers(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ reset_playback_state(mpctx);
+
+ if (mpctx->demuxer)
+ demux_flush(mpctx->demuxer);
+}
+
+static void cmd_ao_reload(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ reload_audio_output(mpctx);
+}
+
+static void cmd_filter(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+ cmd->success = edit_filters_osd(mpctx, type, cmd->args[0].v.s,
+ cmd->args[1].v.s, cmd->msg_osd) >= 0;
+}
+
+static void cmd_filter_command(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+
+ struct mp_output_chain *chain = NULL;
+ if (type == STREAM_VIDEO)
+ chain = mpctx->vo_chain ? mpctx->vo_chain->filter : NULL;
+ if (type == STREAM_AUDIO)
+ chain = mpctx->ao_chain ? mpctx->ao_chain->filter : NULL;
+ if (!chain) {
+ cmd->success = false;
+ return;
+ }
+ struct mp_filter_command filter_cmd = {
+ .type = MP_FILTER_COMMAND_TEXT,
+ .target = cmd->args[3].v.s,
+ .cmd = cmd->args[1].v.s,
+ .arg = cmd->args[2].v.s,
+ };
+ cmd->success = mp_output_chain_command(chain, cmd->args[0].v.s, &filter_cmd);
+}
+
+static void cmd_script_binding(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct mp_cmd *incmd = cmd->cmd;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mpv_event_client_message event = {0};
+ char *name = cmd->args[0].v.s;
+ if (!name || !name[0]) {
+ cmd->success = false;
+ return;
+ }
+
+ char *sep = strchr(name, '/');
+ char *target = NULL;
+ char space[MAX_CLIENT_NAME];
+ if (sep) {
+ snprintf(space, sizeof(space), "%.*s", (int)(sep - name), name);
+ target = space;
+ name = sep + 1;
+ }
+ char state[3] = {'p', incmd->is_mouse_button ? 'm' : '-'};
+ if (incmd->is_up_down)
+ state[0] = incmd->repeated ? 'r' : (incmd->is_up ? 'u' : 'd');
+ event.num_args = 5;
+ event.args = (const char*[5]){"key-binding", name, state,
+ incmd->key_name ? incmd->key_name : "",
+ incmd->key_text ? incmd->key_text : ""};
+ if (mp_client_send_event_dup(mpctx, target,
+ MPV_EVENT_CLIENT_MESSAGE, &event) < 0)
+ {
+ MP_VERBOSE(mpctx, "Can't find script '%s' when handling input.\n",
+ target ? target : "-");
+ cmd->success = false;
+ }
+}
+
+static void cmd_script_message_to(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mpv_event_client_message *event = talloc_ptrtype(NULL, event);
+ *event = (mpv_event_client_message){0};
+ for (int n = 1; n < cmd->num_args; n++) {
+ MP_TARRAY_APPEND(event, event->args, event->num_args,
+ talloc_strdup(event, cmd->args[n].v.s));
+ }
+ if (mp_client_send_event(mpctx, cmd->args[0].v.s, 0,
+ MPV_EVENT_CLIENT_MESSAGE, event) < 0)
+ {
+ MP_VERBOSE(mpctx, "Can't find script '%s' to send message to.\n",
+ cmd->args[0].v.s);
+ cmd->success = false;
+ }
+}
+
+static void cmd_script_message(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ const char **args = talloc_array(NULL, const char *, cmd->num_args);
+ mpv_event_client_message event = {.args = args};
+ for (int n = 0; n < cmd->num_args; n++)
+ event.args[event.num_args++] = cmd->args[n].v.s;
+ mp_client_broadcast_event(mpctx, MPV_EVENT_CLIENT_MESSAGE, &event);
+ talloc_free(args);
+}
+
+static void cmd_ignore(void *p)
+{
+}
+
+static void cmd_write_watch_later_config(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mp_write_watch_later_conf(mpctx);
+}
+
+static void cmd_delete_watch_later_config(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *filename = cmd->args[0].v.s;
+ if (filename && !*filename)
+ filename = NULL;
+ mp_delete_watch_later_conf(mpctx, filename);
+}
+
+static void cmd_mouse(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int pre_key = 0;
+
+ const int x = cmd->args[0].v.i, y = cmd->args[1].v.i;
+ int button = cmd->args[2].v.i;
+
+ if (mpctx->video_out && mpctx->video_out->config_ok) {
+ int oldx, oldy, oldhover;
+ mp_input_get_mouse_pos(mpctx->input, &oldx, &oldy, &oldhover);
+ struct mp_osd_res vo_res = osd_get_vo_res(mpctx->osd);
+
+ // TODO: VOs don't send outside positions. should we abort if outside?
+ int hover = x >= 0 && y >= 0 && x < vo_res.w && y < vo_res.h;
+
+ if (vo_res.w && vo_res.h && hover != oldhover)
+ pre_key = hover ? MP_KEY_MOUSE_ENTER : MP_KEY_MOUSE_LEAVE;
+ }
+
+ if (button == -1) {// no button
+ if (pre_key)
+ mp_input_put_key_artificial(mpctx->input, pre_key);
+ mp_input_set_mouse_pos_artificial(mpctx->input, x, y);
+ return;
+ }
+ if (button < 0 || button >= MP_KEY_MOUSE_BTN_COUNT) {// invalid button
+ MP_ERR(mpctx, "%d is not a valid mouse button number.\n", button);
+ cmd->success = false;
+ return;
+ }
+ const bool dbc = cmd->args[3].v.i;
+ if (dbc && button > (MP_MBTN_RIGHT - MP_MBTN_BASE)) {
+ MP_ERR(mpctx, "%d is not a valid mouse button for double-clicks.\n",
+ button);
+ cmd->success = false;
+ return;
+ }
+ button += dbc ? MP_MBTN_DBL_BASE : MP_MBTN_BASE;
+ if (pre_key)
+ mp_input_put_key_artificial(mpctx->input, pre_key);
+ mp_input_set_mouse_pos_artificial(mpctx->input, x, y);
+ mp_input_put_key_artificial(mpctx->input, button);
+}
+
+static void cmd_key(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int action = *(int *)cmd->priv;
+
+ const char *key_name = cmd->args[0].v.s;
+ if (key_name[0] == '\0' && action == MP_KEY_STATE_UP) {
+ mp_input_put_key_artificial(mpctx->input, MP_INPUT_RELEASE_ALL);
+ } else {
+ int code = mp_input_get_key_from_name(key_name);
+ if (code < 0) {
+ MP_ERR(mpctx, "%s is not a valid input name.\n", key_name);
+ cmd->success = false;
+ return;
+ }
+ mp_input_put_key_artificial(mpctx->input, code | action);
+ }
+}
+
+static void cmd_key_bind(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ int code = mp_input_get_key_from_name(cmd->args[0].v.s);
+ if (code < 0) {
+ MP_ERR(mpctx, "%s is not a valid input name.\n", cmd->args[0].v.s);
+ cmd->success = false;
+ return;
+ }
+ const char *target_cmd = cmd->args[1].v.s;
+ mp_input_bind_key(mpctx->input, code, bstr0(target_cmd));
+}
+
+static void cmd_apply_profile(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *profile = cmd->args[0].v.s;
+ int mode = cmd->args[1].v.i;
+ if (mode == 0) {
+ cmd->success = m_config_set_profile(mpctx->mconfig, profile, 0) >= 0;
+ } else {
+ cmd->success = m_config_restore_profile(mpctx->mconfig, profile) >= 0;
+ }
+}
+
+static void cmd_load_script(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *script = cmd->args[0].v.s;
+ int64_t id = mp_load_user_script(mpctx, script);
+ if (id > 0) {
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "client_id", id);
+ } else {
+ cmd->success = false;
+ }
+}
+
+static void cache_dump_poll(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ struct mp_cmd_ctx *cmd = ctx->cache_dump_cmd;
+
+ if (!cmd)
+ return;
+
+ // Can't close demuxer without stopping dumping.
+ assert(mpctx->demuxer);
+
+ if (mp_cancel_test(cmd->abort->cancel)) {
+ // Synchronous abort. In particular, the dump command shall not report
+ // completion to the user before the dump target file was closed.
+ demux_cache_dump_set(mpctx->demuxer, 0, 0, NULL);
+ assert(demux_cache_dump_get_status(mpctx->demuxer) <= 0);
+ }
+
+ int status = demux_cache_dump_get_status(mpctx->demuxer);
+ if (status <= 0) {
+ if (status < 0) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Cache dumping stopped due to error.");
+ cmd->success = false;
+ } else {
+ mp_cmd_msg(cmd, MSGL_INFO, "Cache dumping successfully ended.");
+ cmd->success = true;
+ }
+ ctx->cache_dump_cmd = NULL;
+ mp_cmd_ctx_complete(cmd);
+ }
+}
+
+void mp_abort_cache_dumping(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ if (ctx->cache_dump_cmd)
+ mp_cancel_trigger(ctx->cache_dump_cmd->abort->cancel);
+ cache_dump_poll(mpctx);
+ assert(!ctx->cache_dump_cmd); // synchronous abort, must have worked
+}
+
+static void run_dump_cmd(struct mp_cmd_ctx *cmd, double start, double end,
+ char *filename)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ mp_abort_cache_dumping(mpctx);
+
+ if (!mpctx->demuxer) {
+ mp_cmd_msg(cmd, MSGL_ERR, "No demuxer open.");
+ cmd->success = false;
+ mp_cmd_ctx_complete(cmd);
+ return;
+ }
+
+ mp_cmd_msg(cmd, MSGL_INFO, "Cache dumping started.");
+
+ if (!demux_cache_dump_set(mpctx->demuxer, start, end, filename)) {
+ mp_cmd_msg(cmd, MSGL_INFO, "Cache dumping stopped.");
+ mp_cmd_ctx_complete(cmd);
+ return;
+ }
+
+ ctx->cache_dump_cmd = cmd;
+ cache_dump_poll(mpctx);
+}
+
+static void cmd_dump_cache(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ run_dump_cmd(cmd, cmd->args[0].v.d, cmd->args[1].v.d, cmd->args[2].v.s);
+}
+
+static void cmd_dump_cache_ab(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ run_dump_cmd(cmd, mpctx->opts->ab_loop[0], mpctx->opts->ab_loop[1],
+ cmd->args[0].v.s);
+}
+
+/* This array defines all known commands.
+ * The first field the command name used in libmpv and input.conf.
+ * The second field is the handler function (see mp_cmd_def.handler and
+ * run_command()).
+ * Then comes the definition of each argument. They are defined like options,
+ * except that the result is parsed into mp_cmd.args[] (thus the option variable
+ * is a field in the mp_cmd_arg union field). Arguments are optional if either
+ * defval is set (usually via OPTDEF_ macros), or the MP_CMD_OPT_ARG flag is
+ * set, or if it's the last argument and .vararg is set. If .vararg is set, the
+ * command has an arbitrary number of arguments, all using the type indicated by
+ * the last argument (they are appended to mp_cmd.args[] starting at the last
+ * argument's index).
+ * Arguments have names, which can be used by named argument functions, e.g. in
+ * Lua with mp.command_native().
+ */
+
+// This does not specify the real destination of the command parameter values,
+// it just provides a dummy for the OPT_ macros. The real destination is an
+// array item in mp_cmd.args[], using the index of the option definition.
+#define OPT_BASE_STRUCT struct mp_cmd_arg
+
+const struct mp_cmd_def mp_cmds[] = {
+ { "ignore", cmd_ignore, .is_ignore = true, .is_noisy = true, },
+
+ { "seek", cmd_seek,
+ {
+ {"target", OPT_TIME(v.d)},
+ {"flags", OPT_FLAGS(v.i,
+ {"relative", 4|0}, {"-", 4|0},
+ {"absolute-percent", 4|1},
+ {"absolute", 4|2},
+ {"relative-percent", 4|3},
+ {"keyframes", 32|8},
+ {"exact", 32|16}),
+ OPTDEF_INT(4|0)},
+ // backwards compatibility only
+ {"legacy", OPT_CHOICE(v.i,
+ {"unused", 0}, {"default-precise", 0},
+ {"keyframes", 32|8},
+ {"exact", 32|16}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .allow_auto_repeat = true,
+ .scalable = true,
+ },
+ { "revert-seek", cmd_revert_seek,
+ { {"flags", OPT_FLAGS(v.i, {"mark", 2|0}, {"mark-permanent", 2|1}),
+ .flags = MP_CMD_OPT_ARG} },
+ },
+ { "quit", cmd_quit, { {"code", OPT_INT(v.i), .flags = MP_CMD_OPT_ARG} },
+ .priv = &(const bool){0} },
+ { "quit-watch-later", cmd_quit, { {"code", OPT_INT(v.i),
+ .flags = MP_CMD_OPT_ARG} },
+ .priv = &(const bool){1} },
+ { "stop", cmd_stop,
+ { {"flags", OPT_FLAGS(v.i, {"keep-playlist", 1}), .flags = MP_CMD_OPT_ARG} }
+ },
+ { "frame-step", cmd_frame_step, .allow_auto_repeat = true,
+ .on_updown = true },
+ { "frame-back-step", cmd_frame_back_step, .allow_auto_repeat = true },
+ { "playlist-next", cmd_playlist_next_prev,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"weak", 0},
+ {"force", 1}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){1},
+ },
+ { "playlist-prev", cmd_playlist_next_prev,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"weak", 0},
+ {"force", 1}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){-1},
+ },
+ { "playlist-next-playlist", cmd_playlist_next_prev_playlist,
+ .priv = &(const int){1} },
+ { "playlist-prev-playlist", cmd_playlist_next_prev_playlist,
+ .priv = &(const int){-1} },
+ { "playlist-play-index", cmd_playlist_play_index,
+ {
+ {"index", OPT_CHOICE(v.i, {"current", -2}, {"none", -1}),
+ M_RANGE(-1, INT_MAX)},
+ }
+ },
+ { "playlist-shuffle", cmd_playlist_shuffle, },
+ { "playlist-unshuffle", cmd_playlist_unshuffle, },
+ { "sub-step", cmd_sub_step_seek,
+ {
+ {"skip", OPT_INT(v.i)},
+ {"flags", OPT_CHOICE(v.i,
+ {"primary", 0},
+ {"secondary", 1}),
+ OPTDEF_INT(0)},
+ },
+ .allow_auto_repeat = true,
+ .priv = &(const bool){true}
+ },
+ { "sub-seek", cmd_sub_step_seek,
+ {
+ {"skip", OPT_INT(v.i)},
+ {"flags", OPT_CHOICE(v.i,
+ {"primary", 0},
+ {"secondary", 1}),
+ OPTDEF_INT(0)},
+ },
+ .allow_auto_repeat = true,
+ .priv = &(const bool){false}
+ },
+ { "print-text", cmd_print_text, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true, .allow_auto_repeat = true },
+ { "show-text", cmd_show_text,
+ {
+ {"text", OPT_STRING(v.s)},
+ {"duration", OPT_INT(v.i), OPTDEF_INT(-1)},
+ {"level", OPT_INT(v.i), .flags = MP_CMD_OPT_ARG},
+ },
+ .is_noisy = true, .allow_auto_repeat = true},
+ { "expand-text", cmd_expand_text, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true },
+ { "expand-path", cmd_expand_path, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true },
+ { "show-progress", cmd_show_progress, .allow_auto_repeat = true,
+ .is_noisy = true },
+
+ { "sub-add", cmd_track_add,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"select", 0}, {"auto", 1}, {"cached", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"title", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"lang", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_SUB},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "audio-add", cmd_track_add,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"select", 0}, {"auto", 1}, {"cached", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"title", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"lang", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_AUDIO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "video-add", cmd_track_add,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i, {"select", 0}, {"auto", 1}, {"cached", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"title", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"lang", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"albumart", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_VIDEO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+
+ { "sub-remove", cmd_track_remove, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_SUB}, },
+ { "audio-remove", cmd_track_remove, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_AUDIO}, },
+ { "video-remove", cmd_track_remove, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_VIDEO}, },
+
+ { "sub-reload", cmd_track_reload, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_SUB},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "audio-reload", cmd_track_reload, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_AUDIO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "video-reload", cmd_track_reload, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_VIDEO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+
+ { "rescan-external-files", cmd_rescan_external_files,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"keep-selection", 1},
+ {"reselect", 0}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+
+ { "screenshot", cmd_screenshot,
+ {
+ {"flags", OPT_FLAGS(v.i,
+ {"video", 4|0}, {"-", 4|0},
+ {"window", 4|1},
+ {"subtitles", 4|2},
+ {"each-frame", 8}),
+ OPTDEF_INT(4|2)},
+ // backwards compatibility
+ {"legacy", OPT_CHOICE(v.i,
+ {"unused", 0}, {"single", 0},
+ {"each-frame", 8}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ },
+ { "screenshot-to-file", cmd_screenshot_to_file,
+ {
+ {"filename", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"video", 0},
+ {"window", 1},
+ {"subtitles", 2}),
+ OPTDEF_INT(2)},
+ },
+ .spawn_thread = true,
+ },
+ { "screenshot-raw", cmd_screenshot_raw,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"video", 0},
+ {"window", 1},
+ {"subtitles", 2}),
+ OPTDEF_INT(2)},
+ },
+ },
+ { "loadfile", cmd_loadfile,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"replace", 0},
+ {"append", 1},
+ {"append-play", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"options", OPT_KEYVALUELIST(v.str_list), .flags = MP_CMD_OPT_ARG},
+ },
+ },
+ { "loadlist", cmd_loadlist,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"replace", 0},
+ {"append", 1},
+ {"append-play", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ .can_abort = true,
+ },
+ { "playlist-clear", cmd_playlist_clear },
+ { "playlist-remove", cmd_playlist_remove, {
+ {"index", OPT_CHOICE(v.i, {"current", -1}),
+ .flags = MP_CMD_OPT_ARG, M_RANGE(0, INT_MAX)}, }},
+ { "playlist-move", cmd_playlist_move, { {"index1", OPT_INT(v.i)},
+ {"index2", OPT_INT(v.i)}, }},
+ { "run", cmd_run, { {"command", OPT_STRING(v.s)},
+ {"args", OPT_STRING(v.s)}, },
+ .vararg = true,
+ },
+ { "subprocess", cmd_subprocess,
+ {
+ {"args", OPT_STRINGLIST(v.str_list)},
+ {"playback_only", OPT_BOOL(v.b), OPTDEF_INT(1)},
+ {"capture_size", OPT_BYTE_SIZE(v.i64), M_RANGE(0, INT_MAX),
+ OPTDEF_INT64(64 * 1024 * 1024)},
+ {"capture_stdout", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ {"capture_stderr", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ {"detach", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ {"env", OPT_STRINGLIST(v.str_list), .flags = MP_CMD_OPT_ARG},
+ {"stdin_data", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"passthrough_stdin", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ .can_abort = true,
+ },
+
+ { "set", cmd_set, {{"name", OPT_STRING(v.s)}, {"value", OPT_STRING(v.s)}}},
+ { "del", cmd_del, {{"name", OPT_STRING(v.s)}}},
+ { "change-list", cmd_change_list, { {"name", OPT_STRING(v.s)},
+ {"operation", OPT_STRING(v.s)},
+ {"value", OPT_STRING(v.s)} }},
+ { "add", cmd_add_cycle, { {"name", OPT_STRING(v.s)},
+ {"value", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(1)}, },
+ .allow_auto_repeat = true,
+ .scalable = true,
+ },
+ { "cycle", cmd_add_cycle, { {"name", OPT_STRING(v.s)},
+ {"value", OPT_CYCLEDIR(v.d), OPTDEF_DOUBLE(1)}, },
+ .allow_auto_repeat = true,
+ .scalable = true,
+ .priv = "",
+ },
+ { "multiply", cmd_multiply, { {"name", OPT_STRING(v.s)},
+ {"value", OPT_DOUBLE(v.d)}},
+ .allow_auto_repeat = true},
+
+ { "cycle-values", cmd_cycle_values, { {"arg0", OPT_STRING(v.s)},
+ {"arg1", OPT_STRING(v.s)},
+ {"argN", OPT_STRING(v.s)}, },
+ .vararg = true},
+
+ { "enable-section", cmd_enable_input_section,
+ {
+ {"name", OPT_STRING(v.s)},
+ {"flags", OPT_FLAGS(v.i,
+ {"default", 0},
+ {"exclusive", MP_INPUT_EXCLUSIVE},
+ {"allow-hide-cursor", MP_INPUT_ALLOW_HIDE_CURSOR},
+ {"allow-vo-dragging", MP_INPUT_ALLOW_VO_DRAGGING}),
+ .flags = MP_CMD_OPT_ARG},
+ }
+ },
+ { "disable-section", cmd_disable_input_section,
+ {{"name", OPT_STRING(v.s)} }},
+ { "define-section", cmd_define_input_section,
+ {
+ {"name", OPT_STRING(v.s)},
+ {"contents", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i, {"default", 0}, {"force", 1}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ },
+
+ { "ab-loop", cmd_ab_loop },
+
+ { "drop-buffers", cmd_drop_buffers, },
+
+ { "af", cmd_filter, { {"operation", OPT_STRING(v.s)},
+ {"value", OPT_STRING(v.s)}, },
+ .priv = &(const int){STREAM_AUDIO} },
+ { "vf", cmd_filter, { {"operation", OPT_STRING(v.s)},
+ {"value", OPT_STRING(v.s)}, },
+ .priv = &(const int){STREAM_VIDEO} },
+
+ { "af-command", cmd_filter_command,
+ {
+ {"label", OPT_STRING(v.s)},
+ {"command", OPT_STRING(v.s)},
+ {"argument", OPT_STRING(v.s)},
+ {"target", OPT_STRING(v.s), OPTDEF_STR("all"),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_AUDIO} },
+ { "vf-command", cmd_filter_command,
+ {
+ {"label", OPT_STRING(v.s)},
+ {"command", OPT_STRING(v.s)},
+ {"argument", OPT_STRING(v.s)},
+ {"target", OPT_STRING(v.s), OPTDEF_STR("all"),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_VIDEO} },
+
+ { "ao-reload", cmd_ao_reload },
+
+ { "script-binding", cmd_script_binding, { {"name", OPT_STRING(v.s)} },
+ .allow_auto_repeat = true, .on_updown = true},
+
+ { "script-message", cmd_script_message, { {"args", OPT_STRING(v.s)} },
+ .vararg = true },
+ { "script-message-to", cmd_script_message_to, { {"target", OPT_STRING(v.s)},
+ {"args", OPT_STRING(v.s)} },
+ .vararg = true },
+
+ { "overlay-add", cmd_overlay_add, { {"id", OPT_INT(v.i)},
+ {"x", OPT_INT(v.i)},
+ {"y", OPT_INT(v.i)},
+ {"file", OPT_STRING(v.s)},
+ {"offset", OPT_INT(v.i)},
+ {"fmt", OPT_STRING(v.s)},
+ {"w", OPT_INT(v.i)},
+ {"h", OPT_INT(v.i)},
+ {"stride", OPT_INT(v.i)}, }},
+ { "overlay-remove", cmd_overlay_remove, { {"id", OPT_INT(v.i)} } },
+
+ { "osd-overlay", cmd_osd_overlay,
+ {
+ {"id", OPT_INT64(v.i64)},
+ {"format", OPT_CHOICE(v.i, {"none", 0}, {"ass-events", 1})},
+ {"data", OPT_STRING(v.s)},
+ {"res_x", OPT_INT(v.i), OPTDEF_INT(0)},
+ {"res_y", OPT_INT(v.i), OPTDEF_INT(720)},
+ {"z", OPT_INT(v.i), OPTDEF_INT(0)},
+ {"hidden", OPT_BOOL(v.b), OPTDEF_INT(0)},
+ {"compute_bounds", OPT_BOOL(v.b), OPTDEF_INT(0)},
+ },
+ .is_noisy = true,
+ },
+
+ { "write-watch-later-config", cmd_write_watch_later_config },
+ { "delete-watch-later-config", cmd_delete_watch_later_config,
+ {{"filename", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG} }},
+
+ { "mouse", cmd_mouse, { {"x", OPT_INT(v.i)},
+ {"y", OPT_INT(v.i)},
+ {"button", OPT_INT(v.i), OPTDEF_INT(-1)},
+ {"mode", OPT_CHOICE(v.i,
+ {"single", 0}, {"double", 1}),
+ .flags = MP_CMD_OPT_ARG}}},
+ { "keybind", cmd_key_bind, { {"name", OPT_STRING(v.s)},
+ {"cmd", OPT_STRING(v.s)} }},
+ { "keypress", cmd_key, { {"name", OPT_STRING(v.s)} },
+ .priv = &(const int){0}},
+ { "keydown", cmd_key, { {"name", OPT_STRING(v.s)} },
+ .priv = &(const int){MP_KEY_STATE_DOWN}},
+ { "keyup", cmd_key, { {"name", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG} },
+ .priv = &(const int){MP_KEY_STATE_UP}},
+
+ { "apply-profile", cmd_apply_profile, {
+ {"name", OPT_STRING(v.s)},
+ {"mode", OPT_CHOICE(v.i, {"apply", 0}, {"restore", 1}),
+ .flags = MP_CMD_OPT_ARG}, }
+ },
+
+ { "load-script", cmd_load_script, {{"filename", OPT_STRING(v.s)}} },
+
+ { "dump-cache", cmd_dump_cache, { {"start", OPT_TIME(v.d),
+ .flags = M_OPT_ALLOW_NO},
+ {"end", OPT_TIME(v.d),
+ .flags = M_OPT_ALLOW_NO},
+ {"filename", OPT_STRING(v.s)} },
+ .exec_async = true,
+ .can_abort = true,
+ },
+
+ { "ab-loop-dump-cache", cmd_dump_cache_ab, { {"filename", OPT_STRING(v.s)} },
+ .exec_async = true,
+ .can_abort = true,
+ },
+
+ { "ab-loop-align-cache", cmd_align_cache_ab },
+
+ {0}
+};
+
+#undef OPT_BASE_STRUCT
+#undef ARG
+
+void command_uninit(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ assert(!ctx->cache_dump_cmd); // closing the demuxer must have aborted it
+
+ overlay_uninit(mpctx);
+ ao_hotplug_destroy(ctx->hotplug);
+
+ m_option_free(&script_props_type, &ctx->script_props);
+
+ talloc_free(mpctx->command_ctx);
+ mpctx->command_ctx = NULL;
+}
+
+void command_init(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = talloc(NULL, struct command_ctx);
+ *ctx = (struct command_ctx){
+ .last_seek_pts = MP_NOPTS_VALUE,
+ };
+ mpctx->command_ctx = ctx;
+
+ int num_base = MP_ARRAY_SIZE(mp_properties_base);
+ int num_opts = m_config_get_co_count(mpctx->mconfig);
+ ctx->properties =
+ talloc_zero_array(ctx, struct m_property, num_base + num_opts + 1);
+ memcpy(ctx->properties, mp_properties_base, sizeof(mp_properties_base));
+
+ int count = num_base;
+ for (int n = 0; n < num_opts; n++) {
+ struct m_config_option *co = m_config_get_co_index(mpctx->mconfig, n);
+ assert(co->name[0]);
+ if (co->opt->flags & M_OPT_NOPROP)
+ continue;
+
+ struct m_property prop = {
+ .name = co->name,
+ .call = mp_property_generic_option,
+ .is_option = true,
+ };
+
+ if (co->opt->type == &m_option_type_alias) {
+ prop.priv = co->opt->priv;
+
+ prop.call = co->opt->deprecation_message ?
+ mp_property_deprecated_alias : mp_property_alias;
+
+ // Check whether this eventually arrives at a real option. If not,
+ // it's some CLI special handling thing. For example, "nosound" is
+ // mapped to "no-audio", which has CLI special-handling, and cannot
+ // be set as property.
+ struct m_config_option *co2 = co;
+ while (co2 && co2->opt->type == &m_option_type_alias) {
+ const char *alias = (const char *)co2->opt->priv;
+ co2 = m_config_get_co_raw(mpctx->mconfig, bstr0(alias));
+ }
+ if (!co2)
+ continue;
+ }
+
+ // The option might be covered by a manual property already.
+ if (m_property_list_find(ctx->properties, prop.name))
+ continue;
+
+ ctx->properties[count++] = prop;
+ }
+
+ node_init(&ctx->udata, MPV_FORMAT_NODE_MAP, NULL);
+ talloc_steal(ctx, ctx->udata.u.list);
+}
+
+static void command_event(struct MPContext *mpctx, int event, void *arg)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ if (event == MPV_EVENT_START_FILE) {
+ ctx->last_seek_pts = MP_NOPTS_VALUE;
+ ctx->marked_pts = MP_NOPTS_VALUE;
+ ctx->marked_permanent = false;
+ }
+
+ if (event == MPV_EVENT_PLAYBACK_RESTART)
+ ctx->last_seek_time = mp_time_sec();
+
+ if (event == MPV_EVENT_END_FILE || event == MPV_EVENT_FILE_LOADED) {
+ // Update chapters - does nothing if something else is visible.
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ }
+ if (event == MP_EVENT_WIN_STATE2)
+ ctx->cached_window_scale = 0;
+
+ if (event == MP_EVENT_METADATA_UPDATE) {
+ struct playlist_entry *const pe = mpctx->playing;
+ if (pe && !pe->title) {
+ const char *const name = find_non_filename_media_title(mpctx);
+ if (name && name[0]) {
+ pe->title = talloc_strdup(pe, name);
+ mp_notify_property(mpctx, "playlist");
+ }
+ }
+ }
+}
+
+void handle_command_updates(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ // This is a bit messy: ao_hotplug wakes up the player, and then we have
+ // to recheck the state. Then the client(s) will read the property.
+ if (ctx->hotplug && ao_hotplug_check_update(ctx->hotplug))
+ mp_notify_property(mpctx, "audio-device-list");
+
+ // Depends on polling demuxer wakeup callback notifications.
+ cache_dump_poll(mpctx);
+}
+
+void mp_notify(struct MPContext *mpctx, int event, void *arg)
+{
+ // The OSD can implicitly reference some properties.
+ mpctx->osd_idle_update = true;
+
+ command_event(mpctx, event, arg);
+
+ mp_client_broadcast_event(mpctx, event, arg);
+}
+
+static void update_priority(struct MPContext *mpctx)
+{
+#if HAVE_WIN32_DESKTOP
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->w32_priority > 0)
+ SetPriorityClass(GetCurrentProcess(), opts->w32_priority);
+#endif
+}
+
+static void update_track_switch(struct MPContext *mpctx, int order, int type)
+{
+ if (!mpctx->playback_initialized)
+ return;
+
+ int tid = mpctx->opts->stream_id[order][type];
+ struct track *track;
+ if (tid == -1) {
+ // If "auto" reset to default track selection
+ track = select_default_track(mpctx, order, type);
+ mark_track_selection(mpctx, order, type, -1);
+ } else {
+ track = mp_track_by_tid(mpctx, type, tid);
+ }
+ mp_switch_track_n(mpctx, order, type, track, (tid == -1) ? 0 : FLAG_MARK_SELECTION);
+ print_track_list(mpctx, "Track switched:");
+ mp_wakeup_core(mpctx);
+}
+
+void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
+ bool self_update)
+{
+ struct MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ bool init = !co;
+ void *opt_ptr = init ? NULL : co->data; // NULL on start
+
+ if (co)
+ mp_notify_property(mpctx, co->name);
+ if (opt_ptr == &opts->media_title)
+ mp_notify(mpctx, MP_EVENT_METADATA_UPDATE, NULL);
+
+ if (self_update)
+ return;
+
+ if (flags & UPDATE_TERM)
+ mp_update_logging(mpctx, false);
+
+ if (flags & (UPDATE_OSD | UPDATE_SUB_FILT | UPDATE_SUB_HARD)) {
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++) {
+ struct track *track = mpctx->current_track[n][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ if (sub) {
+ int ret = sub_control(sub, SD_CTRL_UPDATE_OPTS,
+ (void *)(uintptr_t)flags);
+ if (ret == CONTROL_OK && flags & (UPDATE_SUB_FILT | UPDATE_SUB_HARD))
+ sub_redecode_cached_packets(sub);
+ }
+ }
+ osd_changed(mpctx->osd);
+ }
+
+ if (flags & UPDATE_BUILTIN_SCRIPTS)
+ mp_load_builtin_scripts(mpctx);
+
+ if (flags & UPDATE_IMGPAR) {
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ if (track && track->dec) {
+ mp_decoder_wrapper_reset_params(track->dec);
+ mp_force_video_refresh(mpctx);
+ }
+ }
+
+ if (flags & UPDATE_INPUT)
+ mp_input_update_opts(mpctx->input);
+
+ if (flags & UPDATE_SUB_EXTS)
+ mp_update_subtitle_exts(mpctx->opts);
+
+ if (init || opt_ptr == &opts->ipc_path || opt_ptr == &opts->ipc_client) {
+ mp_uninit_ipc(mpctx->ipc_ctx);
+ mpctx->ipc_ctx = mp_init_ipc(mpctx->clients, mpctx->global);
+ }
+
+ if (opt_ptr == &opts->vo->video_driver_list) {
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ uninit_video_out(mpctx);
+ handle_force_window(mpctx, true);
+ reinit_video_chain(mpctx);
+ if (track)
+ reselect_demux_stream(mpctx, track, true);
+
+ mp_wakeup_core(mpctx);
+ }
+
+ if (flags & UPDATE_AUDIO)
+ reload_audio_output(mpctx);
+
+ if (flags & UPDATE_PRIORITY)
+ update_priority(mpctx);
+
+ if (flags & UPDATE_SCREENSAVER)
+ update_screensaver_state(mpctx);
+
+ if (flags & UPDATE_VOL)
+ audio_update_volume(mpctx);
+
+ if (flags & UPDATE_LAVFI_COMPLEX)
+ update_lavfi_complex(mpctx);
+
+ if (opt_ptr == &opts->vo->android_surface_size) {
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_EXTERNAL_RESIZE, NULL);
+ }
+
+ if (opt_ptr == &opts->playback_speed) {
+ update_playback_speed(mpctx);
+ mp_wakeup_core(mpctx);
+ }
+
+ if (opt_ptr == &opts->play_dir) {
+ if (mpctx->play_dir != opts->play_dir) {
+ // Some weird things for play_dir if we're at EOF.
+ // 1. The option must be set before we seek.
+ // 2. queue_seek can change the stop_play value; always keep the old one.
+ int old_stop_play = mpctx->stop_play;
+ if (old_stop_play == AT_END_OF_FILE)
+ mpctx->play_dir = opts->play_dir;
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, get_current_time(mpctx),
+ MPSEEK_EXACT, 0);
+ if (old_stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = old_stop_play;
+ }
+ }
+
+ if (opt_ptr == &opts->edition_id) {
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (mpctx->playback_initialized && demuxer && demuxer->num_editions > 0) {
+ if (opts->edition_id != demuxer->edition) {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+ mp_wakeup_core(mpctx);
+ }
+ }
+ }
+
+ if (opt_ptr == &opts->pause)
+ set_pause_state(mpctx, opts->pause);
+
+ if (opt_ptr == &opts->audio_delay) {
+ if (mpctx->ao_chain) {
+ mpctx->delay += mpctx->opts->audio_delay - mpctx->ao_chain->delay;
+ mpctx->ao_chain->delay = mpctx->opts->audio_delay;
+ }
+ mp_wakeup_core(mpctx);
+ }
+
+ if (flags & UPDATE_HWDEC) {
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_decoder_wrapper *dec = track ? track->dec : NULL;
+ if (dec) {
+ mp_decoder_wrapper_control(dec, VDCTRL_REINIT, NULL);
+ double last_pts = mpctx->video_pts;
+ if (last_pts != MP_NOPTS_VALUE)
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, last_pts, MPSEEK_EXACT, 0);
+ }
+ }
+
+ if (opt_ptr == &opts->vo->window_scale)
+ update_window_scale(mpctx);
+
+ if (opt_ptr == &opts->cursor_autohide_delay)
+ mpctx->mouse_timer = 0;
+
+ if (flags & UPDATE_DVB_PROG) {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+ }
+
+ if (opt_ptr == &opts->ab_loop[0] || opt_ptr == &opts->ab_loop[1]) {
+ update_ab_loop_clip(mpctx);
+ // Update if visible
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ mp_wakeup_core(mpctx);
+ }
+
+ if (opt_ptr == &opts->vf_settings)
+ set_filters(mpctx, STREAM_VIDEO, opts->vf_settings);
+
+ if (opt_ptr == &opts->af_settings)
+ set_filters(mpctx, STREAM_AUDIO, opts->af_settings);
+
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ for (int order = 0; order < num_ptracks[type]; order++) {
+ if (opt_ptr == &opts->stream_id[order][type])
+ update_track_switch(mpctx, order, type);
+ }
+ }
+
+ if (opt_ptr == &opts->vo->fullscreen && !opts->vo->fullscreen)
+ mpctx->mouse_event_ts--; // Show mouse cursor
+
+ if (opt_ptr == &opts->vo->taskbar_progress)
+ update_vo_playback_state(mpctx);
+
+ if (opt_ptr == &opts->image_display_duration && mpctx->vo_chain
+ && mpctx->vo_chain->is_sparse && !mpctx->ao_chain
+ && mpctx->video_status == STATUS_DRAINING)
+ mpctx->time_frame = opts->image_display_duration;
+}
+
+void mp_notify_property(struct MPContext *mpctx, const char *property)
+{
+ mp_client_property_change(mpctx, property);
+}
diff --git a/player/command.h b/player/command.h
new file mode 100644
index 0000000..185b78f
--- /dev/null
+++ b/player/command.h
@@ -0,0 +1,123 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_COMMAND_H
+#define MPLAYER_COMMAND_H
+
+#include <stdbool.h>
+
+#include "libmpv/client.h"
+#include "osdep/compiler.h"
+
+struct MPContext;
+struct mp_cmd;
+struct mp_log;
+struct mpv_node;
+struct m_config_option;
+
+void command_init(struct MPContext *mpctx);
+void command_uninit(struct MPContext *mpctx);
+
+// Runtime context for a single command.
+struct mp_cmd_ctx {
+ struct MPContext *mpctx;
+ struct mp_cmd *cmd; // original command
+ // Fields from cmd (for convenience)
+ struct mp_cmd_arg *args;
+ int num_args;
+ const void *priv; // cmd->def->priv
+ // OSD control
+ int on_osd; // MP_ON_OSD_FLAGS;
+ bool msg_osd; // OSD message requested
+ bool bar_osd; // OSD bar requested
+ bool seek_msg_osd; // same as above, but for seek commands
+ bool seek_bar_osd;
+ // If mp_cmd_def.can_abort is set, this will be set.
+ struct mp_abort_entry *abort;
+ // Return values (to be set by command implementation, read by the
+ // completion callback).
+ bool success; // true by default
+ struct mpv_node result;
+ // Command handlers can set this to false if returning from the command
+ // handler does not complete the command. It stops the common command code
+ // from signaling the completion automatically, and you can call
+ // mp_cmd_ctx_complete() to invoke on_completion() properly (including all
+ // the bookkeeping).
+ /// (Note that in no case you can call mp_cmd_ctx_complete() from within
+ // the command handler, because it frees the mp_cmd_ctx.)
+ bool completed; // true by default
+ // This is managed by the common command code. For rules about how and where
+ // this is called see run_command() comments.
+ void (*on_completion)(struct mp_cmd_ctx *cmd);
+ void *on_completion_priv; // for free use by on_completion callback
+};
+
+void run_command(struct MPContext *mpctx, struct mp_cmd *cmd,
+ struct mp_abort_entry *abort,
+ void (*on_completion)(struct mp_cmd_ctx *cmd),
+ void *on_completion_priv);
+void mp_cmd_ctx_complete(struct mp_cmd_ctx *cmd);
+PRINTF_ATTRIBUTE(3, 4)
+void mp_cmd_msg(struct mp_cmd_ctx *cmd, int status, const char *msg, ...);
+char *mp_property_expand_string(struct MPContext *mpctx, const char *str);
+char *mp_property_expand_escaped_string(struct MPContext *mpctx, const char *str);
+void property_print_help(struct MPContext *mpctx);
+int mp_property_do(const char* name, int action, void* val,
+ struct MPContext *mpctx);
+
+void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
+ bool self_update);
+
+void mp_notify(struct MPContext *mpctx, int event, void *arg);
+void mp_notify_property(struct MPContext *mpctx, const char *property);
+
+void handle_command_updates(struct MPContext *mpctx);
+
+int mp_get_property_id(struct MPContext *mpctx, const char *name);
+uint64_t mp_get_property_event_mask(const char *name);
+
+enum {
+ // Must start with the first unused positive value in enum mpv_event_id
+ // MPV_EVENT_* and MP_EVENT_* must not overlap.
+ INTERNAL_EVENT_BASE = 26,
+ MP_EVENT_CHANGE_ALL,
+ MP_EVENT_CACHE_UPDATE,
+ MP_EVENT_WIN_RESIZE,
+ MP_EVENT_WIN_STATE,
+ MP_EVENT_WIN_STATE2,
+ MP_EVENT_FOCUS,
+ MP_EVENT_CHANGE_PLAYLIST,
+ MP_EVENT_CORE_IDLE,
+ MP_EVENT_DURATION_UPDATE,
+ MP_EVENT_INPUT_PROCESSED,
+ MP_EVENT_TRACKS_CHANGED,
+ MP_EVENT_TRACK_SWITCHED,
+ MP_EVENT_METADATA_UPDATE,
+ MP_EVENT_CHAPTER_CHANGE,
+};
+
+bool mp_hook_test_completion(struct MPContext *mpctx, char *type);
+void mp_hook_start(struct MPContext *mpctx, char *type);
+int mp_hook_continue(struct MPContext *mpctx, int64_t client_id, uint64_t id);
+void mp_hook_add(struct MPContext *mpctx, char *client, int64_t client_id,
+ const char *name, uint64_t user_id, int pri);
+
+void mark_seek(struct MPContext *mpctx);
+
+void mp_abort_cache_dumping(struct MPContext *mpctx);
+
+#endif /* MPLAYER_COMMAND_H */
diff --git a/player/configfiles.c b/player/configfiles.c
new file mode 100644
index 0000000..9441638
--- /dev/null
+++ b/player/configfiles.c
@@ -0,0 +1,472 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include <libavutil/md5.h>
+
+#include "mpv_talloc.h"
+
+#include "osdep/io.h"
+
+#include "common/global.h"
+#include "common/encode.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "options/path.h"
+#include "options/m_config.h"
+#include "options/m_config_frontend.h"
+#include "options/parse_configfile.h"
+#include "common/playlist.h"
+#include "options/options.h"
+#include "options/m_property.h"
+
+#include "stream/stream.h"
+
+#include "core.h"
+#include "command.h"
+
+static void load_all_cfgfiles(struct MPContext *mpctx, char *section,
+ char *filename)
+{
+ char **cf = mp_find_all_config_files(NULL, mpctx->global, filename);
+ for (int i = 0; cf && cf[i]; i++)
+ m_config_parse_config_file(mpctx->mconfig, mpctx->global, cf[i], section, 0);
+ talloc_free(cf);
+}
+
+// This name is used in builtin.conf to force encoding defaults (like ao/vo).
+#define SECT_ENCODE "encoding"
+
+void mp_parse_cfgfiles(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ mp_mk_user_dir(mpctx->global, "home", "");
+
+ char *p1 = mp_get_user_path(NULL, mpctx->global, "~~home/");
+ char *p2 = mp_get_user_path(NULL, mpctx->global, "~~old_home/");
+ if (strcmp(p1, p2) != 0 && mp_path_exists(p2)) {
+ MP_WARN(mpctx, "Warning, two config dirs found:\n %s (main)\n"
+ " %s (bogus)\nYou should merge or delete the second one.\n",
+ p1, p2);
+ }
+ talloc_free(p1);
+ talloc_free(p2);
+
+ char *section = NULL;
+ bool encoding = opts->encode_opts &&
+ opts->encode_opts->file && opts->encode_opts->file[0];
+ // In encoding mode, we don't want to apply normal config options.
+ // So we "divert" normal options into a separate section, and the diverted
+ // section is never used - unless maybe it's explicitly referenced from an
+ // encoding profile.
+ if (encoding)
+ section = "playback-default";
+
+ load_all_cfgfiles(mpctx, NULL, "encoding-profiles.conf");
+
+ load_all_cfgfiles(mpctx, section, "mpv.conf|config");
+
+ if (encoding)
+ m_config_set_profile(mpctx->mconfig, SECT_ENCODE, 0);
+}
+
+static int try_load_config(struct MPContext *mpctx, const char *file, int flags,
+ int msgl)
+{
+ if (!mp_path_exists(file))
+ return 0;
+ MP_MSG(mpctx, msgl, "Loading config '%s'\n", file);
+ m_config_parse_config_file(mpctx->mconfig, mpctx->global, file, NULL, flags);
+ return 1;
+}
+
+// Set options file-local, and don't set them if the user set them via the
+// command line.
+#define FILE_LOCAL_FLAGS (M_SETOPT_BACKUP | M_SETOPT_PRESERVE_CMDLINE)
+
+static void mp_load_per_file_config(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ char *confpath;
+ char cfg[512];
+ const char *file = mpctx->filename;
+
+ if (opts->use_filedir_conf) {
+ if (snprintf(cfg, sizeof(cfg), "%s.conf", file) >= sizeof(cfg)) {
+ MP_VERBOSE(mpctx, "Filename is too long, can not load file or "
+ "directory specific config files\n");
+ return;
+ }
+
+ char *name = mp_basename(cfg);
+
+ bstr dir = mp_dirname(cfg);
+ char *dircfg = mp_path_join_bstr(NULL, dir, bstr0("mpv.conf"));
+ try_load_config(mpctx, dircfg, FILE_LOCAL_FLAGS, MSGL_INFO);
+ talloc_free(dircfg);
+
+ if (try_load_config(mpctx, cfg, FILE_LOCAL_FLAGS, MSGL_INFO))
+ return;
+
+ if ((confpath = mp_find_config_file(NULL, mpctx->global, name))) {
+ try_load_config(mpctx, confpath, FILE_LOCAL_FLAGS, MSGL_INFO);
+
+ talloc_free(confpath);
+ }
+ }
+}
+
+static void mp_auto_load_profile(struct MPContext *mpctx, char *category,
+ bstr item)
+{
+ if (!item.len)
+ return;
+
+ char t[512];
+ snprintf(t, sizeof(t), "%s.%.*s", category, BSTR_P(item));
+ m_profile_t *p = m_config_get_profile0(mpctx->mconfig, t);
+ if (p) {
+ MP_INFO(mpctx, "Auto-loading profile '%s'\n", t);
+ m_config_set_profile(mpctx->mconfig, t, FILE_LOCAL_FLAGS);
+ }
+}
+
+void mp_load_auto_profiles(struct MPContext *mpctx)
+{
+ mp_auto_load_profile(mpctx, "protocol",
+ mp_split_proto(bstr0(mpctx->filename), NULL));
+ mp_auto_load_profile(mpctx, "extension",
+ bstr0(mp_splitext(mpctx->filename, NULL)));
+
+ mp_load_per_file_config(mpctx);
+}
+
+#define MP_WATCH_LATER_CONF "watch_later"
+
+static bool check_mtime(const char *f1, const char *f2)
+{
+ struct stat st1, st2;
+ if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0)
+ return false;
+ return st1.st_mtime == st2.st_mtime;
+}
+
+static bool copy_mtime(const char *f1, const char *f2)
+{
+ struct stat st1, st2;
+
+ if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0)
+ return false;
+
+ struct utimbuf ut = {
+ .actime = st2.st_atime, // we want to pass this through intact
+ .modtime = st1.st_mtime,
+ };
+
+ if (utime(f2, &ut) != 0)
+ return false;
+
+ return true;
+}
+
+static char *mp_get_playback_resume_dir(struct MPContext *mpctx)
+{
+ char *wl_dir = mpctx->opts->watch_later_dir;
+ if (wl_dir && wl_dir[0]) {
+ wl_dir = mp_get_user_path(mpctx, mpctx->global, wl_dir);
+ } else {
+ wl_dir = mp_find_user_file(mpctx, mpctx->global, "state", MP_WATCH_LATER_CONF);
+ }
+ return wl_dir;
+}
+
+static char *mp_get_playback_resume_config_filename(struct MPContext *mpctx,
+ const char *fname)
+{
+ struct MPOpts *opts = mpctx->opts;
+ char *res = NULL;
+ void *tmp = talloc_new(NULL);
+ const char *realpath = fname;
+ bstr bfname = bstr0(fname);
+ if (!mp_is_url(bfname)) {
+ if (opts->ignore_path_in_watch_later_config) {
+ realpath = mp_basename(fname);
+ } else {
+ char *cwd = mp_getcwd(tmp);
+ if (!cwd)
+ goto exit;
+ realpath = mp_path_join(tmp, cwd, fname);
+ }
+ }
+ uint8_t md5[16];
+ av_md5_sum(md5, realpath, strlen(realpath));
+ char *conf = talloc_strdup(tmp, "");
+ for (int i = 0; i < 16; i++)
+ conf = talloc_asprintf_append(conf, "%02X", md5[i]);
+
+ char *wl_dir = mp_get_playback_resume_dir(mpctx);
+ if (wl_dir && wl_dir[0])
+ res = mp_path_join(NULL, wl_dir, conf);
+
+exit:
+ talloc_free(tmp);
+ return res;
+}
+
+// Should follow what parser-cfg.c does/needs
+static bool needs_config_quoting(const char *s)
+{
+ if (s[0] == '%')
+ return true;
+ for (int i = 0; s[i]; i++) {
+ unsigned char c = s[i];
+ if (!mp_isprint(c) || mp_isspace(c) || c == '#' || c == '\'' || c == '"')
+ return true;
+ }
+ return false;
+}
+
+static void write_filename(struct MPContext *mpctx, FILE *file, char *filename)
+{
+ if (mpctx->opts->write_filename_in_watch_later_config) {
+ char write_name[1024] = {0};
+ for (int n = 0; filename[n] && n < sizeof(write_name) - 1; n++)
+ write_name[n] = (unsigned char)filename[n] < 32 ? '_' : filename[n];
+ fprintf(file, "# %s\n", write_name);
+ }
+}
+
+static void write_redirect(struct MPContext *mpctx, char *path)
+{
+ char *conffile = mp_get_playback_resume_config_filename(mpctx, path);
+ if (conffile) {
+ FILE *file = fopen(conffile, "wb");
+ if (file) {
+ fprintf(file, "# redirect entry\n");
+ write_filename(mpctx, file, path);
+ fclose(file);
+ }
+
+ if (mpctx->opts->position_check_mtime &&
+ !mp_is_url(bstr0(path)) && !copy_mtime(path, conffile))
+ MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", path, conffile);
+
+ talloc_free(conffile);
+ }
+}
+
+static void write_redirects_for_parent_dirs(struct MPContext *mpctx, char *path)
+{
+ if (mp_is_url(bstr0(path)))
+ return;
+
+ // Write redirect entries for the file's parent directories to allow
+ // resuming playback when playing parent directories whose entries are
+ // expanded only the first time they are "played". For example, if
+ // "/a/b/c.mkv" is the current entry, also create resume files for /a/b and
+ // /a, so that "mpv --directory-mode=lazy /a" resumes playback from
+ // /a/b/c.mkv even when b isn't the first directory in /a.
+ bstr dir = mp_dirname(path);
+ // There is no need to write a redirect entry for "/".
+ while (dir.len > 1 && dir.len < strlen(path)) {
+ path[dir.len] = '\0';
+ mp_path_strip_trailing_separator(path);
+ write_redirect(mpctx, path);
+ dir = mp_dirname(path);
+ }
+}
+
+void mp_write_watch_later_conf(struct MPContext *mpctx)
+{
+ struct playlist_entry *cur = mpctx->playing;
+ char *conffile = NULL;
+ void *ctx = talloc_new(NULL);
+
+ if (!cur)
+ goto exit;
+
+ char *path = mp_normalize_path(ctx, cur->filename);
+
+ struct demuxer *demux = mpctx->demuxer;
+
+ conffile = mp_get_playback_resume_config_filename(mpctx, path);
+ if (!conffile)
+ goto exit;
+
+ char *wl_dir = mp_get_playback_resume_dir(mpctx);
+ mp_mkdirp(wl_dir);
+
+ MP_INFO(mpctx, "Saving state.\n");
+
+ FILE *file = fopen(conffile, "wb");
+ if (!file) {
+ MP_WARN(mpctx, "Can't open %s for writing\n", conffile);
+ goto exit;
+ }
+
+ write_filename(mpctx, file, path);
+
+ bool write_start = true;
+ double pos = get_current_time(mpctx);
+
+ if ((demux && (!demux->seekable || demux->partially_seekable)) ||
+ pos == MP_NOPTS_VALUE)
+ {
+ write_start = false;
+ MP_INFO(mpctx, "Not seekable, or time unknown - not saving position.\n");
+ }
+ char **watch_later_options = mpctx->opts->watch_later_options;
+ for (int i = 0; watch_later_options && watch_later_options[i]; i++) {
+ char *pname = watch_later_options[i];
+ // Always save start if we have it in the array.
+ if (write_start && strcmp(pname, "start") == 0) {
+ fprintf(file, "%s=%f\n", pname, pos);
+ continue;
+ }
+ // Only store it if it's different from the initial value.
+ if (m_config_watch_later_backup_opt_changed(mpctx->mconfig, pname)) {
+ char *val = NULL;
+ mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx);
+ if (needs_config_quoting(val)) {
+ // e.g. '%6%STRING'
+ fprintf(file, "%s=%%%d%%%s\n", pname, (int)strlen(val), val);
+ } else {
+ fprintf(file, "%s=%s\n", pname, val);
+ }
+ talloc_free(val);
+ }
+ }
+ fclose(file);
+
+ if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(path)) &&
+ !copy_mtime(path, conffile))
+ {
+ MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", cur->filename,
+ conffile);
+ }
+
+ write_redirects_for_parent_dirs(mpctx, path);
+
+ // Also write redirect entries for a playlist that mpv expanded if the
+ // current entry is a URL, this is mostly useful for playing multiple
+ // archives of images, e.g. with mpv 1.zip 2.zip and quit-watch-later
+ // on 2.zip, write redirect entries for 2.zip, not just for the archive://
+ // URL.
+ if (cur->playlist_path && mp_is_url(bstr0(path))) {
+ char *playlist_path = mp_normalize_path(ctx, cur->playlist_path);
+ write_redirect(mpctx, playlist_path);
+ write_redirects_for_parent_dirs(mpctx, playlist_path);
+ }
+
+exit:
+ talloc_free(conffile);
+ talloc_free(ctx);
+}
+
+void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file)
+{
+ if (!file) {
+ struct playlist_entry *cur = mpctx->playing;
+ if (!cur)
+ return;
+ file = cur->filename;
+ if (!file)
+ return;
+ }
+
+ char *fname = mp_get_playback_resume_config_filename(mpctx, file);
+ if (fname) {
+ unlink(fname);
+ talloc_free(fname);
+ }
+
+ if (mp_is_url(bstr0(file)))
+ return;
+
+ void *ctx = talloc_new(NULL);
+ char *path = mp_normalize_path(ctx, file);
+
+ bstr dir = mp_dirname(path);
+ while (dir.len > 1 && dir.len < strlen(path)) {
+ path[dir.len] = '\0';
+ mp_path_strip_trailing_separator(path);
+ fname = mp_get_playback_resume_config_filename(mpctx, path);
+ if (fname) {
+ unlink(fname);
+ talloc_free(fname);
+ }
+ dir = mp_dirname(path);
+ }
+
+ talloc_free(ctx);
+}
+
+bool mp_load_playback_resume(struct MPContext *mpctx, const char *file)
+{
+ bool resume = false;
+ if (!mpctx->opts->position_resume)
+ return resume;
+ char *fname = mp_get_playback_resume_config_filename(mpctx, file);
+ if (fname && mp_path_exists(fname)) {
+ if (mpctx->opts->position_check_mtime &&
+ !mp_is_url(bstr0(file)) && !check_mtime(file, fname))
+ {
+ talloc_free(fname);
+ return resume;
+ }
+
+ // Never apply the saved start position to following files
+ m_config_backup_opt(mpctx->mconfig, "start");
+ MP_INFO(mpctx, "Resuming playback. This behavior can "
+ "be disabled with --no-resume-playback.\n");
+ try_load_config(mpctx, fname, M_SETOPT_PRESERVE_CMDLINE, MSGL_V);
+ resume = true;
+ }
+ talloc_free(fname);
+ return resume;
+}
+
+// Returns the first file that has a resume config.
+// Compared to hashing the playlist file or contents and managing separate
+// resume file for them, this is simpler, and also has the nice property
+// that appending to a playlist doesn't interfere with resuming (especially
+// if the playlist comes from the command line).
+struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx,
+ struct playlist *playlist)
+{
+ if (!mpctx->opts->position_resume)
+ return NULL;
+ for (int n = 0; n < playlist->num_entries; n++) {
+ struct playlist_entry *e = playlist->entries[n];
+ char *conf = mp_get_playback_resume_config_filename(mpctx, e->filename);
+ bool exists = conf && mp_path_exists(conf);
+ talloc_free(conf);
+ if (exists)
+ return e;
+ }
+ return NULL;
+}
+
diff --git a/player/core.h b/player/core.h
new file mode 100644
index 0000000..8a49585
--- /dev/null
+++ b/player/core.h
@@ -0,0 +1,644 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_MP_CORE_H
+#define MPLAYER_MP_CORE_H
+
+#include <stdatomic.h>
+#include <stdbool.h>
+
+#include "libmpv/client.h"
+
+#include "audio/aframe.h"
+#include "common/common.h"
+#include "filters/f_output_chain.h"
+#include "filters/filter.h"
+#include "options/options.h"
+#include "osdep/threads.h"
+#include "sub/osd.h"
+#include "video/mp_image.h"
+#include "video/out/vo.h"
+
+// definitions used internally by the core player code
+
+enum stop_play_reason {
+ KEEP_PLAYING = 0, // playback of a file is actually going on
+ // must be 0, numeric values of others do not matter
+ AT_END_OF_FILE, // file has ended, prepare to play next
+ // also returned on unrecoverable playback errors
+ PT_NEXT_ENTRY, // prepare to play next entry in playlist
+ PT_CURRENT_ENTRY, // prepare to play mpctx->playlist->current
+ PT_STOP, // stop playback / idle mode
+ PT_QUIT, // stop playback, quit player
+ PT_ERROR, // play next playlist entry (due to an error)
+};
+
+enum mp_osd_seek_info {
+ OSD_SEEK_INFO_BAR = 1,
+ OSD_SEEK_INFO_TEXT = 2,
+ OSD_SEEK_INFO_CHAPTER_TEXT = 4,
+ OSD_SEEK_INFO_CURRENT_FILE = 8,
+};
+
+
+enum {
+ // other constants
+ OSD_LEVEL_INVISIBLE = 4,
+ OSD_BAR_SEEK = 256,
+
+ MAX_NUM_VO_PTS = 100,
+};
+
+enum seek_type {
+ MPSEEK_NONE = 0,
+ MPSEEK_RELATIVE,
+ MPSEEK_ABSOLUTE,
+ MPSEEK_FACTOR,
+ MPSEEK_BACKSTEP,
+ MPSEEK_CHAPTER,
+};
+
+enum seek_precision {
+ // The following values are numerically sorted by increasing precision
+ MPSEEK_DEFAULT = 0,
+ MPSEEK_KEYFRAME,
+ MPSEEK_EXACT,
+ MPSEEK_VERY_EXACT,
+};
+
+enum seek_flags {
+ MPSEEK_FLAG_DELAY = 1 << 0, // give player chance to coalesce multiple seeks
+ MPSEEK_FLAG_NOFLUSH = 1 << 1, // keeping remaining data for seamless loops
+};
+
+struct seek_params {
+ enum seek_type type;
+ enum seek_precision exact;
+ double amount;
+ unsigned flags; // MPSEEK_FLAG_*
+};
+
+// Information about past video frames that have been sent to the VO.
+struct frame_info {
+ double pts;
+ double duration; // PTS difference to next frame
+ double approx_duration; // possibly fixed/smoothed out duration
+ double av_diff; // A/V diff at time of scheduling
+ int num_vsyncs; // scheduled vsyncs, if using display-sync
+};
+
+struct track {
+ enum stream_type type;
+
+ // Currently used for decoding.
+ bool selected;
+
+ // The type specific ID, also called aid (audio), sid (subs), vid (video).
+ // For UI purposes only; this ID doesn't have anything to do with any
+ // IDs coming from demuxers or container files.
+ int user_tid;
+
+ int demuxer_id; // same as stream->demuxer_id. -1 if not set.
+ int ff_index; // same as stream->ff_index, or 0.
+ int hls_bitrate; // same as stream->hls_bitrate. 0 if not set.
+ int program_id; // same as stream->program_id. -1 if not set.
+
+ char *title;
+ bool default_track, forced_track, dependent_track;
+ bool visual_impaired_track, hearing_impaired_track;
+ bool image;
+ bool attached_picture;
+ char *lang;
+
+ // If this track is from an external file (e.g. subtitle file).
+ bool is_external;
+ bool no_default; // pretend it's not external for auto-selection
+ bool no_auto_select;
+ char *external_filename;
+ bool auto_loaded;
+
+ struct demuxer *demuxer;
+ // Invariant: !stream || stream->demuxer == demuxer
+ struct sh_stream *stream;
+
+ // Current subtitle state (or cached state if selected==false).
+ struct dec_sub *d_sub;
+
+ // Current decoding state (NULL if selected==false)
+ struct mp_decoder_wrapper *dec;
+
+ // Where the decoded result goes to (one of them is not NULL if active)
+ struct vo_chain *vo_c;
+ struct ao_chain *ao_c;
+ struct mp_pin *sink;
+};
+
+// Summarizes video filtering and output.
+struct vo_chain {
+ struct mp_log *log;
+
+ struct mp_output_chain *filter;
+
+ struct vo *vo;
+
+ struct track *track;
+ struct mp_pin *filter_src;
+ struct mp_pin *dec_src;
+
+ // - video consists of a single picture, which should be shown only once
+ // - do not sync audio to video in any way
+ bool is_coverart;
+ // - video consists of sparse still images
+ bool is_sparse;
+ bool sparse_eof_signalled;
+
+ bool underrun;
+ bool underrun_signaled;
+};
+
+// Like vo_chain, for audio.
+struct ao_chain {
+ struct mp_log *log;
+ struct MPContext *mpctx;
+
+ bool spdif_passthrough, spdif_failed;
+
+ struct mp_output_chain *filter;
+
+ struct ao *ao;
+ struct mp_async_queue *ao_queue;
+ struct mp_filter *queue_filter;
+ struct mp_filter *ao_filter;
+ double ao_resume_time;
+
+ bool out_eof;
+ double last_out_pts;
+
+ double start_pts;
+ bool start_pts_known;
+
+ struct track *track;
+ struct mp_pin *filter_src;
+ struct mp_pin *dec_src;
+
+ double delay;
+ bool untimed_throttle;
+
+ bool ao_underrun; // last known AO state
+ bool underrun; // for cache pause logic
+};
+
+/* Note that playback can be paused, stopped, etc. at any time. While paused,
+ * playback restart is still active, because you want seeking to work even
+ * if paused.
+ * The main purpose of distinguishing these states is proper reinitialization
+ * of A/V sync.
+ */
+enum playback_status {
+ // code may compare status values numerically
+ STATUS_SYNCING, // seeking for a position to resume
+ STATUS_READY, // buffers full, playback can be started any time
+ STATUS_PLAYING, // normal playback
+ STATUS_DRAINING, // decoding has ended; still playing out queued buffers
+ STATUS_EOF, // playback has ended, or is disabled
+};
+
+const char *mp_status_str(enum playback_status st);
+
+extern const int num_ptracks[STREAM_TYPE_COUNT];
+
+// Maximum of all num_ptracks[] values.
+#define MAX_PTRACKS 2
+
+typedef struct MPContext {
+ bool initialized;
+ bool is_cli;
+ struct mpv_global *global;
+ struct MPOpts *opts;
+ struct mp_log *log;
+ struct stats_ctx *stats;
+ struct m_config *mconfig;
+ struct input_ctx *input;
+ struct mp_client_api *clients;
+ struct mp_dispatch_queue *dispatch;
+ struct mp_cancel *playback_abort;
+ // Number of asynchronous tasks that still need to finish until MPContext
+ // destruction is ok. It's implied that the async tasks call
+ // mp_wakeup_core() each time this is decremented.
+ // As using an atomic+wakeup would be racy, this is a normal integer, and
+ // mp_dispatch_lock must be called to change it.
+ int64_t outstanding_async;
+
+ struct mp_thread_pool *thread_pool; // for coarse I/O, often during loading
+
+ struct mp_log *statusline;
+ struct osd_state *osd;
+ char *term_osd_text;
+ char *term_osd_status;
+ char *term_osd_subs;
+ char *term_osd_contents;
+ char *term_osd_title;
+ char *last_window_title;
+ struct voctrl_playback_state vo_playback_state;
+
+ int add_osd_seek_info; // bitfield of enum mp_osd_seek_info
+ double osd_visible; // for the osd bar only
+ int osd_function;
+ double osd_function_visible;
+ double osd_msg_visible;
+ double osd_msg_next_duration;
+ double osd_last_update;
+ bool osd_force_update, osd_idle_update;
+ char *osd_msg_text;
+ bool osd_show_pos;
+ struct osd_progbar_state osd_progbar;
+
+ struct playlist *playlist;
+ struct playlist_entry *playing; // currently playing file
+ char *filename; // immutable copy of playing->filename (or NULL)
+ char *stream_open_filename;
+ char **playlist_paths; // used strictly for playlist validation
+ int playlist_paths_len;
+ enum stop_play_reason stop_play;
+ bool playback_initialized; // playloop can be run/is running
+ int error_playing;
+
+ // Return code to use with PT_QUIT
+ int quit_custom_rc;
+ bool has_quit_custom_rc;
+
+ // Global file statistics
+ int files_played; // played without issues (even if stopped by user)
+ int files_errored; // played, but errors happened at one point
+ int files_broken; // couldn't be played at all
+
+ // Current file statistics
+ int64_t shown_vframes, shown_aframes;
+
+ struct demux_chapter *chapters;
+ int num_chapters;
+
+ struct demuxer *demuxer;
+ struct mp_tags *filtered_tags;
+
+ struct track **tracks;
+ int num_tracks;
+
+ char *track_layout_hash;
+
+ // Selected tracks. NULL if no track selected.
+ // There can be num_ptracks[type] of the same STREAM_TYPE selected at once.
+ // Currently, this is used for the secondary subtitle track only.
+ struct track *current_track[MAX_PTRACKS][STREAM_TYPE_COUNT];
+
+ struct mp_filter *filter_root;
+
+ struct mp_filter *lavfi;
+ char *lavfi_graph;
+
+ struct ao *ao;
+ struct mp_aframe *ao_filter_fmt; // for weak gapless audio check
+ struct ao_chain *ao_chain;
+
+ struct vo_chain *vo_chain;
+
+ struct vo *video_out;
+ // next_frame[0] is the next frame, next_frame[1] the one after that.
+ // The +1 is for adding 1 additional frame in backstep mode.
+ struct mp_image *next_frames[VO_MAX_REQ_FRAMES + 1];
+ int num_next_frames;
+ struct mp_image *saved_frame; // for hrseek_lastframe and hrseek_backstep
+
+ enum playback_status video_status, audio_status;
+ bool restart_complete;
+ int play_dir;
+ // Factors to multiply with opts->playback_speed to get the total audio or
+ // video speed (usually 1.0, but can be set to by the sync code).
+ double speed_factor_v, speed_factor_a;
+ // Redundant values set from opts->playback_speed and speed_factor_*.
+ // update_playback_speed() updates them from the other fields.
+ double audio_speed, video_speed;
+ bool display_sync_active;
+ int display_sync_drift_dir;
+ // Timing error (in seconds) due to rounding on vsync boundaries
+ double display_sync_error;
+ // Number of mistimed frames.
+ int mistimed_frames_total;
+ bool hrseek_active; // skip all data until hrseek_pts
+ bool hrseek_lastframe; // drop everything until last frame reached
+ bool hrseek_backstep; // go to frame before seek target
+ double hrseek_pts;
+ struct seek_params current_seek;
+ bool ab_loop_clip; // clip to the "b" part of an A-B loop if available
+ // AV sync: the next frame should be shown when the audio out has this
+ // much (in seconds) buffered data left. Increased when more data is
+ // written to the ao, decreased when moving to the next video frame.
+ double delay;
+ // AV sync: time in seconds until next frame should be shown
+ double time_frame;
+ // How much video timing has been changed to make it match the audio
+ // timeline. Used for status line information only.
+ double total_avsync_change;
+ // A-V sync difference when last frame was displayed. Kept to display
+ // the same value if the status line is updated at a time where no new
+ // video frame is shown.
+ double last_av_difference;
+ /* timestamp of video frame currently visible on screen
+ * (or at least queued to be flipped by VO) */
+ double video_pts;
+ // Last seek target.
+ double last_seek_pts;
+ // Frame duration field from demuxer. Only used for duration of the last
+ // video frame.
+ double last_frame_duration;
+ // Video PTS, or audio PTS if video has ended.
+ double playback_pts;
+ // For logging only.
+ double logged_async_diff;
+
+ int last_chapter;
+
+ // Past timestamps etc.
+ // The newest frame is at index 0.
+ struct frame_info *past_frames;
+ int num_past_frames;
+
+ double last_idle_tick;
+ double next_cache_update;
+
+ double sleeptime; // number of seconds to sleep before next iteration
+
+ double mouse_timer;
+ unsigned int mouse_event_ts;
+ bool mouse_cursor_visible;
+
+ // used to prevent hanging in some error cases
+ double start_timestamp;
+
+ // Timestamp from the last time some timing functions read the
+ // current time, in nanoseconds.
+ // Used to turn a new time value to a delta from last time.
+ int64_t last_time;
+
+ struct seek_params seek;
+
+ /* Heuristic for relative chapter seeks: keep track which chapter
+ * the user wanted to go to, even if we aren't exactly within the
+ * boundaries of that chapter due to an inaccurate seek. */
+ int last_chapter_seek;
+ bool last_chapter_flag;
+
+ bool paused; // internal pause state
+ bool playback_active; // not paused, restarting, loading, unloading
+ bool in_playloop;
+
+ // step this many frames, then pause
+ int step_frames;
+ // Counted down each frame, stop playback if 0 is reached. (-1 = disable)
+ int max_frames;
+ bool playing_msg_shown;
+
+ bool paused_for_cache;
+ bool demux_underrun;
+ double cache_stop_time;
+ int cache_buffer;
+ double cache_update_pts;
+
+ // Set after showing warning about decoding being too slow for realtime
+ // playback rate. Used to avoid showing it multiple times.
+ bool drop_message_shown;
+
+ struct screenshot_ctx *screenshot_ctx;
+ struct command_ctx *command_ctx;
+ struct encode_lavc_context *encode_lavc_ctx;
+
+ struct mp_ipc_ctx *ipc_ctx;
+
+ int64_t builtin_script_ids[5];
+
+ mp_mutex abort_lock;
+
+ // --- The following fields are protected by abort_lock
+ struct mp_abort_entry **abort_list;
+ int num_abort_list;
+ bool abort_all; // during final termination
+
+ // --- Owned by MPContext
+ mp_thread open_thread;
+ bool open_active; // open_thread is a valid thread handle, all setup
+ atomic_bool open_done;
+ // --- All fields below are immutable while open_active is true.
+ // Otherwise, they're owned by MPContext.
+ struct mp_cancel *open_cancel;
+ char *open_url;
+ char *open_format;
+ int open_url_flags;
+ bool open_for_prefetch;
+ // --- All fields below are owned by open_thread, unless open_done was set
+ // to true.
+ struct demuxer *open_res_demuxer;
+ int open_res_error;
+} MPContext;
+
+// Contains information about an asynchronous work item, how it can be aborted,
+// and when. All fields are protected by MPContext.abort_lock.
+struct mp_abort_entry {
+ // General conditions.
+ bool coupled_to_playback; // trigger when playback is terminated
+ // Actual trigger to abort the work. Pointer immutable, owner may access
+ // without holding the abort_lock.
+ struct mp_cancel *cancel;
+ // For client API.
+ struct mpv_handle *client; // non-NULL if done by a client API user
+ int client_work_type; // client API type, e.h. MPV_EVENT_COMMAND_REPLY
+ uint64_t client_work_id; // client API user reply_userdata value
+ // (only valid if client_work_type set)
+};
+
+// audio.c
+void reset_audio_state(struct MPContext *mpctx);
+void reinit_audio_chain(struct MPContext *mpctx);
+int init_audio_decoder(struct MPContext *mpctx, struct track *track);
+int reinit_audio_filters(struct MPContext *mpctx);
+double playing_audio_pts(struct MPContext *mpctx);
+void fill_audio_out_buffers(struct MPContext *mpctx);
+double written_audio_pts(struct MPContext *mpctx);
+void clear_audio_output_buffers(struct MPContext *mpctx);
+void update_playback_speed(struct MPContext *mpctx);
+void uninit_audio_out(struct MPContext *mpctx);
+void uninit_audio_chain(struct MPContext *mpctx);
+void reinit_audio_chain_src(struct MPContext *mpctx, struct track *track);
+void audio_update_volume(struct MPContext *mpctx);
+void reload_audio_output(struct MPContext *mpctx);
+void audio_start_ao(struct MPContext *mpctx);
+
+// configfiles.c
+void mp_parse_cfgfiles(struct MPContext *mpctx);
+void mp_load_auto_profiles(struct MPContext *mpctx);
+bool mp_load_playback_resume(struct MPContext *mpctx, const char *file);
+void mp_write_watch_later_conf(struct MPContext *mpctx);
+void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file);
+struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx,
+ struct playlist *playlist);
+
+// loadfile.c
+void mp_abort_playback_async(struct MPContext *mpctx);
+void mp_abort_add(struct MPContext *mpctx, struct mp_abort_entry *abort);
+void mp_abort_remove(struct MPContext *mpctx, struct mp_abort_entry *abort);
+void mp_abort_recheck_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort);
+void mp_abort_trigger_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort);
+int mp_add_external_file(struct MPContext *mpctx, char *filename,
+ enum stream_type filter, struct mp_cancel *cancel,
+ bool cover_art);
+void mark_track_selection(struct MPContext *mpctx, int order,
+ enum stream_type type, int value);
+#define FLAG_MARK_SELECTION 1
+void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
+ struct track *track, int flags);
+void mp_switch_track_n(struct MPContext *mpctx, int order,
+ enum stream_type type, struct track *track, int flags);
+void mp_deselect_track(struct MPContext *mpctx, struct track *track);
+struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
+ int tid);
+void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer);
+bool mp_remove_track(struct MPContext *mpctx, struct track *track);
+struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
+ bool force);
+void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e);
+void mp_play_files(struct MPContext *mpctx);
+void update_demuxer_properties(struct MPContext *mpctx);
+void print_track_list(struct MPContext *mpctx, const char *msg);
+void reselect_demux_stream(struct MPContext *mpctx, struct track *track,
+ bool refresh_only);
+void prepare_playlist(struct MPContext *mpctx, struct playlist *pl);
+void autoload_external_files(struct MPContext *mpctx, struct mp_cancel *cancel);
+struct track *select_default_track(struct MPContext *mpctx, int order,
+ enum stream_type type);
+void prefetch_next(struct MPContext *mpctx);
+void update_lavfi_complex(struct MPContext *mpctx);
+
+// main.c
+int mp_initialize(struct MPContext *mpctx, char **argv);
+struct MPContext *mp_create(void);
+void mp_destroy(struct MPContext *mpctx);
+void mp_print_version(struct mp_log *log, int always);
+void mp_update_logging(struct MPContext *mpctx, bool preinit);
+void issue_refresh_seek(struct MPContext *mpctx, enum seek_precision min_prec);
+
+// misc.c
+double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t);
+double get_play_end_pts(struct MPContext *mpctx);
+double get_play_start_pts(struct MPContext *mpctx);
+bool get_ab_loop_times(struct MPContext *mpctx, double t[2]);
+void merge_playlist_files(struct playlist *pl);
+void update_content_type(struct MPContext *mpctx, struct track *track);
+void update_vo_playback_state(struct MPContext *mpctx);
+void update_window_title(struct MPContext *mpctx, bool force);
+void error_on_track(struct MPContext *mpctx, struct track *track);
+int stream_dump(struct MPContext *mpctx, const char *source_filename);
+double get_track_seek_offset(struct MPContext *mpctx, struct track *track);
+
+// osd.c
+void set_osd_bar(struct MPContext *mpctx, int type,
+ double min, double max, double neutral, double val);
+bool set_osd_msg(struct MPContext *mpctx, int level, int time,
+ const char* fmt, ...) PRINTF_ATTRIBUTE(4,5);
+void set_osd_function(struct MPContext *mpctx, int osd_function);
+void term_osd_set_subs(struct MPContext *mpctx, const char *text);
+void get_current_osd_sym(struct MPContext *mpctx, char *buf, size_t buf_size);
+void set_osd_bar_chapters(struct MPContext *mpctx, int type);
+
+// playloop.c
+void mp_wait_events(struct MPContext *mpctx);
+void mp_set_timeout(struct MPContext *mpctx, double sleeptime);
+void mp_wakeup_core(struct MPContext *mpctx);
+void mp_wakeup_core_cb(void *ctx);
+void mp_core_lock(struct MPContext *mpctx);
+void mp_core_unlock(struct MPContext *mpctx);
+double get_relative_time(struct MPContext *mpctx);
+void reset_playback_state(struct MPContext *mpctx);
+void set_pause_state(struct MPContext *mpctx, bool user_pause);
+void update_internal_pause_state(struct MPContext *mpctx);
+void update_core_idle_state(struct MPContext *mpctx);
+void add_step_frame(struct MPContext *mpctx, int dir);
+void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
+ enum seek_precision exact, int flags);
+double get_time_length(struct MPContext *mpctx);
+double get_start_time(struct MPContext *mpctx, int dir);
+double get_current_time(struct MPContext *mpctx);
+double get_playback_time(struct MPContext *mpctx);
+int get_percent_pos(struct MPContext *mpctx);
+double get_current_pos_ratio(struct MPContext *mpctx, bool use_range);
+int get_current_chapter(struct MPContext *mpctx);
+char *chapter_display_name(struct MPContext *mpctx, int chapter);
+char *chapter_name(struct MPContext *mpctx, int chapter);
+double chapter_start_time(struct MPContext *mpctx, int chapter);
+int get_chapter_count(struct MPContext *mpctx);
+int get_cache_buffering_percentage(struct MPContext *mpctx);
+void execute_queued_seek(struct MPContext *mpctx);
+void run_playloop(struct MPContext *mpctx);
+void mp_idle(struct MPContext *mpctx);
+void idle_loop(struct MPContext *mpctx);
+int handle_force_window(struct MPContext *mpctx, bool force);
+void seek_to_last_frame(struct MPContext *mpctx);
+void update_screensaver_state(struct MPContext *mpctx);
+void update_ab_loop_clip(struct MPContext *mpctx);
+bool get_internal_paused(struct MPContext *mpctx);
+
+// scripting.c
+struct mp_script_args {
+ const struct mp_scripting *backend;
+ struct MPContext *mpctx;
+ struct mp_log *log;
+ struct mpv_handle *client;
+ const char *filename;
+ const char *path;
+};
+struct mp_scripting {
+ const char *name; // e.g. "lua script"
+ const char *file_ext; // e.g. "lua"
+ bool no_thread; // don't run load() on dedicated thread
+ int (*load)(struct mp_script_args *args);
+};
+bool mp_load_scripts(struct MPContext *mpctx);
+void mp_load_builtin_scripts(struct MPContext *mpctx);
+int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname);
+
+// sub.c
+void reset_subtitle_state(struct MPContext *mpctx);
+void reinit_sub(struct MPContext *mpctx, struct track *track);
+void reinit_sub_all(struct MPContext *mpctx);
+void uninit_sub(struct MPContext *mpctx, struct track *track);
+void uninit_sub_all(struct MPContext *mpctx);
+void update_osd_msg(struct MPContext *mpctx);
+bool update_subtitles(struct MPContext *mpctx, double video_pts);
+
+// video.c
+void reset_video_state(struct MPContext *mpctx);
+int init_video_decoder(struct MPContext *mpctx, struct track *track);
+void reinit_video_chain(struct MPContext *mpctx);
+void reinit_video_chain_src(struct MPContext *mpctx, struct track *track);
+int reinit_video_filters(struct MPContext *mpctx);
+void write_video(struct MPContext *mpctx);
+void mp_force_video_refresh(struct MPContext *mpctx);
+void uninit_video_out(struct MPContext *mpctx);
+void uninit_video_chain(struct MPContext *mpctx);
+double calc_average_frame_duration(struct MPContext *mpctx);
+
+#endif /* MPLAYER_MP_CORE_H */
diff --git a/player/external_files.c b/player/external_files.c
new file mode 100644
index 0000000..e9a6081
--- /dev/null
+++ b/player/external_files.c
@@ -0,0 +1,359 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <dirent.h>
+#include <string.h>
+#include <strings.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "misc/charset_conv.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "external_files.h"
+
+// Stolen from: vlc/-/blob/master/modules/meta_engine/folder.c#L40
+// sorted by priority (descending)
+static const char *const cover_files[] = {
+ "AlbumArt",
+ "Album",
+ "cover",
+ "front",
+ "AlbumArtSmall",
+ "Folder",
+ ".folder",
+ "thumb",
+ NULL
+};
+
+// Needed for mp_might_be_subtitle_file
+char **sub_exts;
+
+static bool test_ext_list(bstr ext, char **list)
+{
+ if (!list)
+ goto done;
+ for (int n = 0; list[n]; n++) {
+ if (bstrcasecmp(bstr0(list[n]), ext) == 0)
+ return true;
+ }
+done:
+ return false;
+}
+
+static int test_ext(MPOpts *opts, bstr ext)
+{
+ if (test_ext_list(ext, opts->sub_auto_exts))
+ return STREAM_SUB;
+ if (test_ext_list(ext, opts->audiofile_auto_exts))
+ return STREAM_AUDIO;
+ if (test_ext_list(ext, opts->coverart_auto_exts))
+ return STREAM_VIDEO;
+ return -1;
+}
+
+static int test_cover_filename(bstr fname)
+{
+ for (int n = 0; cover_files[n]; n++) {
+ if (bstrcasecmp(bstr0(cover_files[n]), fname) == 0) {
+ return MP_ARRAY_SIZE(cover_files) - n;
+ }
+ }
+ return 0;
+}
+
+bool mp_might_be_subtitle_file(const char *filename)
+{
+ return test_ext_list(bstr_get_ext(bstr0(filename)), sub_exts);
+}
+
+void mp_update_subtitle_exts(struct MPOpts *opts)
+{
+ sub_exts = opts->sub_auto_exts;
+}
+
+static int compare_sub_filename(const void *a, const void *b)
+{
+ const struct subfn *s1 = a;
+ const struct subfn *s2 = b;
+ return strcoll(s1->fname, s2->fname);
+}
+
+static int compare_sub_priority(const void *a, const void *b)
+{
+ const struct subfn *s1 = a;
+ const struct subfn *s2 = b;
+ if (s1->priority > s2->priority)
+ return -1;
+ if (s1->priority < s2->priority)
+ return 1;
+ return strcoll(s1->fname, s2->fname);
+}
+
+static struct bstr guess_lang_from_filename(struct bstr name, int *fn_start)
+{
+ if (name.len < 2)
+ return (struct bstr){NULL, 0};
+
+ int n = 0;
+ int i = name.len - 1;
+
+ char thing = '.';
+ if (name.start[i] == ')') {
+ thing = '(';
+ i--;
+ }
+ if (name.start[i] == ']') {
+ thing = '[';
+ i--;
+ }
+
+ while (i >= 0 && mp_isalpha(name.start[i])) {
+ n++;
+ if (n > 3)
+ return (struct bstr){NULL, 0};
+ i--;
+ }
+
+ if (n < 2 || i == 0 || name.start[i] != thing)
+ return (struct bstr){NULL, 0};
+
+ *fn_start = i;
+ return (struct bstr){name.start + i + 1, n};
+}
+
+static void append_dir_subtitles(struct mpv_global *global, struct MPOpts *opts,
+ struct subfn **slist, int *nsub,
+ struct bstr path, const char *fname,
+ int limit_fuzziness, int limit_type)
+{
+ void *tmpmem = talloc_new(NULL);
+ struct mp_log *log = mp_log_new(tmpmem, global->log, "find_files");
+
+ struct bstr f_fbname = bstr0(mp_basename(fname));
+ struct bstr f_fname = mp_iconv_to_utf8(log, f_fbname,
+ "UTF-8-MAC", MP_NO_LATIN1_FALLBACK);
+ struct bstr f_fname_noext = bstrdup(tmpmem, bstr_strip_ext(f_fname));
+ bstr_lower(f_fname_noext);
+ struct bstr f_fname_trim = bstr_strip(f_fname_noext);
+
+ if (f_fbname.start != f_fname.start)
+ talloc_steal(tmpmem, f_fname.start);
+
+ char *path0 = bstrdup0(tmpmem, path);
+
+ if (mp_is_url(bstr0(path0)))
+ goto out;
+
+ DIR *d = opendir(path0);
+ if (!d)
+ goto out;
+ mp_verbose(log, "Loading external files in %.*s\n", BSTR_P(path));
+ struct dirent *de;
+ while ((de = readdir(d))) {
+ void *tmpmem2 = talloc_new(tmpmem);
+ struct bstr den = bstr0(de->d_name);
+ struct bstr dename = mp_iconv_to_utf8(log, den,
+ "UTF-8-MAC", MP_NO_LATIN1_FALLBACK);
+ // retrieve various parts of the filename
+ struct bstr tmp_fname_noext = bstrdup(tmpmem2, bstr_strip_ext(dename));
+ bstr_lower(tmp_fname_noext);
+ struct bstr tmp_fname_ext = bstr_get_ext(dename);
+ struct bstr tmp_fname_trim = bstr_strip(tmp_fname_noext);
+
+ if (den.start != dename.start)
+ talloc_steal(tmpmem2, dename.start);
+
+ // check what it is (most likely)
+ int type = test_ext(opts, tmp_fname_ext);
+ char **langs = NULL;
+ int fuzz = -1;
+ switch (type) {
+ case STREAM_SUB:
+ langs = opts->stream_lang[type];
+ fuzz = opts->sub_auto;
+ break;
+ case STREAM_AUDIO:
+ langs = opts->stream_lang[type];
+ fuzz = opts->audiofile_auto;
+ break;
+ case STREAM_VIDEO:
+ fuzz = opts->coverart_auto;
+ break;
+ }
+
+ if (fuzz < 0 || (limit_type >= 0 && limit_type != type))
+ goto next_sub;
+
+ // we have a (likely) subtitle file
+ // higher prio -> auto-selection may prefer it (0 = not loaded)
+ int prio = 0;
+
+ if (bstrcmp(tmp_fname_trim, f_fname_trim) == 0)
+ prio |= 32; // exact movie name match
+
+ bstr lang = {0};
+ int start = 0;
+ lang = guess_lang_from_filename(tmp_fname_trim, &start);
+ if (bstr_startswith(tmp_fname_trim, f_fname_trim)) {
+ if (lang.len && start == f_fname_trim.len)
+ prio |= 16; // exact movie name + followed by lang
+
+ if (lang.len && fuzz >= 1)
+ prio |= 4; // matches the movie name + a language was matched
+
+ for (int n = 0; langs && langs[n]; n++) {
+ if (lang.len && bstr_case_startswith(lang, bstr0(langs[n]))) {
+ if (fuzz >= 1)
+ prio |= 8; // known language -> boost priority
+ break;
+ }
+ }
+ }
+
+ if (bstr_find(tmp_fname_trim, f_fname_trim) >= 0 && fuzz >= 1)
+ prio |= 2; // contains the movie name
+
+ if (type == STREAM_VIDEO && opts->coverart_whitelist && prio == 0)
+ prio = test_cover_filename(tmp_fname_trim);
+
+ // doesn't contain the movie name
+ // don't try in the mplayer subtitle directory
+ if (!limit_fuzziness && fuzz >= 2)
+ prio |= 1;
+
+ mp_trace(log, "Potential external file: \"%s\" Priority: %d\n",
+ de->d_name, prio);
+
+ if (prio) {
+ char *subpath = mp_path_join_bstr(*slist, path, dename);
+ if (mp_path_exists(subpath)) {
+ MP_TARRAY_GROW(NULL, *slist, *nsub);
+ struct subfn *sub = *slist + (*nsub)++;
+
+ // annoying and redundant
+ if (strncmp(subpath, "./", 2) == 0)
+ subpath += 2;
+
+ sub->type = type;
+ sub->priority = prio;
+ sub->fname = subpath;
+ sub->lang = lang.len ? bstrdup0(*slist, lang) : NULL;
+ } else
+ talloc_free(subpath);
+ }
+
+ next_sub:
+ talloc_free(tmpmem2);
+ }
+ closedir(d);
+
+ out:
+ talloc_free(tmpmem);
+}
+
+static bool case_endswith(const char *s, const char *end)
+{
+ size_t len = strlen(s);
+ size_t elen = strlen(end);
+ return len >= elen && strcasecmp(s + len - elen, end) == 0;
+}
+
+// Drop .sub file if .idx file exists.
+// Assumes slist is sorted by compare_sub_filename.
+static void filter_subidx(struct subfn **slist, int *nsub)
+{
+ const char *prev = NULL;
+ for (int n = 0; n < *nsub; n++) {
+ const char *fname = (*slist)[n].fname;
+ if (case_endswith(fname, ".idx")) {
+ prev = fname;
+ } else if (case_endswith(fname, ".sub")) {
+ if (prev && strncmp(prev, fname, strlen(fname) - 4) == 0)
+ (*slist)[n].priority = -1;
+ }
+ }
+ for (int n = *nsub - 1; n >= 0; n--) {
+ if ((*slist)[n].priority < 0)
+ MP_TARRAY_REMOVE_AT(*slist, *nsub, n);
+ }
+}
+
+static void load_paths(struct mpv_global *global, struct MPOpts *opts,
+ struct subfn **slist, int *nsubs, const char *fname,
+ char **paths, char *cfg_path, int type)
+{
+ for (int i = 0; paths && paths[i]; i++) {
+ char *expanded_path = mp_get_user_path(NULL, global, paths[i]);
+ char *path = mp_path_join_bstr(
+ *slist, mp_dirname(fname),
+ bstr0(expanded_path ? expanded_path : paths[i]));
+ append_dir_subtitles(global, opts, slist, nsubs, bstr0(path),
+ fname, 0, type);
+ talloc_free(expanded_path);
+ }
+
+ // Load subtitles in ~/.mpv/sub (or similar) limiting sub fuzziness
+ char *mp_subdir = mp_find_config_file(NULL, global, cfg_path);
+ if (mp_subdir) {
+ append_dir_subtitles(global, opts, slist, nsubs, bstr0(mp_subdir),
+ fname, 1, type);
+ }
+ talloc_free(mp_subdir);
+}
+
+// Return a list of subtitles and audio files found, sorted by priority.
+// Last element is terminated with a fname==NULL entry.
+struct subfn *find_external_files(struct mpv_global *global, const char *fname,
+ struct MPOpts *opts)
+{
+ struct subfn *slist = talloc_array_ptrtype(NULL, slist, 1);
+ int n = 0;
+
+ // Load subtitles from current media directory
+ append_dir_subtitles(global, opts, &slist, &n, mp_dirname(fname), fname, 0, -1);
+
+ // Load subtitles in dirs specified by sub-paths option
+ if (opts->sub_auto >= 0) {
+ load_paths(global, opts, &slist, &n, fname, opts->sub_paths, "sub",
+ STREAM_SUB);
+ }
+
+ if (opts->audiofile_auto >= 0) {
+ load_paths(global, opts, &slist, &n, fname, opts->audiofile_paths,
+ "audio", STREAM_AUDIO);
+ }
+
+ // Sort by name for filter_subidx()
+ qsort(slist, n, sizeof(*slist), compare_sub_filename);
+
+ filter_subidx(&slist, &n);
+
+ // Sort subs by priority and append them
+ qsort(slist, n, sizeof(*slist), compare_sub_priority);
+
+ struct subfn z = {0};
+ MP_TARRAY_APPEND(NULL, slist, n, z);
+
+ return slist;
+}
diff --git a/player/external_files.h b/player/external_files.h
new file mode 100644
index 0000000..20b37c3
--- /dev/null
+++ b/player/external_files.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_FIND_SUBFILES_H
+#define MPLAYER_FIND_SUBFILES_H
+
+#include <stdbool.h>
+
+struct subfn {
+ int type; // STREAM_SUB/STREAM_AUDIO/STREAM_VIDEO(coverart)
+ int priority;
+ char *fname;
+ char *lang;
+};
+
+struct mpv_global;
+struct MPOpts;
+struct subfn *find_external_files(struct mpv_global *global, const char *fname,
+ struct MPOpts *opts);
+
+bool mp_might_be_subtitle_file(const char *filename);
+void mp_update_subtitle_exts(struct MPOpts *opts);
+
+#endif /* MPLAYER_FINDFILES_H */
diff --git a/player/javascript.c b/player/javascript.c
new file mode 100644
index 0000000..5be7277
--- /dev/null
+++ b/player/javascript.c
@@ -0,0 +1,1262 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <math.h>
+#include <stdint.h>
+
+#include <mujs.h>
+
+#include "osdep/io.h"
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "options/m_property.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "options/m_option.h"
+#include "input/input.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+#include "core.h"
+#include "command.h"
+#include "client.h"
+#include "libmpv/client.h"
+
+// List of builtin modules and their contents as strings.
+// All these are generated from player/javascript/*.js
+static const char *const builtin_files[][3] = {
+ {"@/defaults.js",
+# include "player/javascript/defaults.js.inc"
+ },
+ {0}
+};
+
+// Represents a loaded script. Each has its own js state.
+struct script_ctx {
+ const char *filename;
+ const char *path; // NULL if single file
+ struct mpv_handle *client;
+ struct MPContext *mpctx;
+ struct mp_log *log;
+ char *last_error_str;
+ size_t js_malloc_size;
+ struct stats_ctx *stats;
+};
+
+static struct script_ctx *jctx(js_State *J)
+{
+ return (struct script_ctx *)js_getcontext(J);
+}
+
+static mpv_handle *jclient(js_State *J)
+{
+ return jctx(J)->client;
+}
+
+static void pushnode(js_State *J, mpv_node *node);
+static void makenode(void *ta_ctx, mpv_node *dst, js_State *J, int idx);
+static int jsL_checkint(js_State *J, int idx);
+static uint64_t jsL_checkuint64(js_State *J, int idx);
+
+/**********************************************************************
+ * conventions, MuJS notes and vm errors
+ *********************************************************************/
+// - push_foo functions are called from C and push a value to the vm stack.
+//
+// - JavaScript C functions are code which the vm can call as a js function.
+// By convention, script_bar and script__baz are js C functions. The former
+// is exposed to end users as bar, and _baz is for internal use.
+//
+// - js C functions get a fresh vm stack with their arguments, and may
+// manipulate their stack as they see fit. On exit, the vm considers the
+// top value of their stack as their return value, and GC the rest.
+//
+// - js C function's stack[0] is "this", and the rest (1, 2, ...) are the args.
+// On entry the stack has at least the number of args defined for the func,
+// padded with undefined if called with less, or bigger if called with more.
+//
+// - Almost all vm APIs (js_*) may throw an error - a longjmp to the last
+// recovery/catch point, which could skip releasing resources. This includes
+// js_try itself(!), except at the outer-most [1] js_try which is always
+// entering the try part (and the catch part if the try part throws).
+// The assumption should be that anything can throw and needs careful setup.
+// One such automated setup is the autofree mechanism. Details later.
+//
+// - Unless named s_foo, all the functions at this file (inc. init) which
+// touch the vm may throw, but either cleanup resources regardless (mostly
+// autofree) or leave allocated resources on caller-provided talloc context
+// which the caller should release, typically with autofree (e.g. makenode).
+//
+// - Functions named s_foo (safe foo) never throw if called at the outer-most
+// try-levels, or, inside JS C functions - never throw after allocating.
+// If they didn't throw then they return 0 on success, 1 on js-errors.
+//
+// [1] In practice the N outer-most (nested) tries are guaranteed to enter the
+// try/carch code, where N is the mujs try-stack size (64 with mujs 1.1.3).
+// But because we can't track try-level at (called-back) JS C functions,
+// it's only guaranteed when we know we're near the outer-most try level.
+
+/**********************************************************************
+ * mpv scripting API error handling
+ *********************************************************************/
+// - Errors may be thrown on some cases - the reason is at the exception.
+//
+// - Some APIs also set last error which can be fetched with mp.last_error(),
+// where empty string (false-y) is success, or an error string otherwise.
+//
+// - The rest of the APIs are guaranteed to return undefined on error or a
+// true-thy value on success and may or may not set last error.
+//
+// - push_success, push_failure, push_status and pushed_error set last error.
+
+// iserr as true indicates an error, and if so, str may indicate a reason.
+// Internally ctx->last_error_str is never NULL, and empty indicates success.
+static void set_last_error(struct script_ctx *ctx, bool iserr, const char *str)
+{
+ ctx->last_error_str[0] = 0;
+ if (!iserr)
+ return;
+ if (!str || !str[0])
+ str = "Error";
+ ctx->last_error_str = talloc_strdup_append(ctx->last_error_str, str);
+}
+
+// For use only by wrappers at defaults.js.
+// arg: error string. Use empty string to indicate success.
+static void script__set_last_error(js_State *J)
+{
+ const char *e = js_tostring(J, 1);
+ set_last_error(jctx(J), e[0], e);
+}
+
+// mp.last_error() . args: none. return the last error without modifying it.
+static void script_last_error(js_State *J)
+{
+ js_pushstring(J, jctx(J)->last_error_str);
+}
+
+// Generic success for APIs which don't return an actual value.
+static void push_success(js_State *J)
+{
+ set_last_error(jctx(J), 0, NULL);
+ js_pushboolean(J, true);
+}
+
+// Doesn't (intentionally) throw. Just sets last_error and pushes undefined
+static void push_failure(js_State *J, const char *str)
+{
+ set_last_error(jctx(J), 1, str);
+ js_pushundefined(J);
+}
+
+// Most of the scripting APIs are either sending some values and getting status
+// code in return, or requesting some value while providing a default in case an
+// error happened. These simplify the C code for that and always set last_error.
+
+static void push_status(js_State *J, int err)
+{
+ if (err >= 0) {
+ push_success(J);
+ } else {
+ push_failure(J, mpv_error_string(err));
+ }
+}
+
+ // If err is success then return 0, else push the item at def and return 1
+static bool pushed_error(js_State *J, int err, int def)
+{
+ bool iserr = err < 0;
+ set_last_error(jctx(J), iserr, iserr ? mpv_error_string(err) : NULL);
+ if (!iserr)
+ return false;
+
+ js_copy(J, def);
+ return true;
+}
+
+/**********************************************************************
+ * Autofree - care-free resource deallocation on vm errors, and otherwise
+ *********************************************************************/
+// - Autofree (af) functions are called with a talloc context argument which is
+// freed after the function exits - either normally or because it threw an
+// error, on the latter case it then re-throws the error after the cleanup.
+//
+// Autofree js C functions should have an additional void* talloc arg and
+// inserted into the vm using af_newcfunction, but otherwise used normally.
+//
+// To wrap an autofree function af_TARGET in C:
+// 1. Create a wrapper s_TARGET which does this:
+// if (js_try(J))
+// return 1;
+// *af = talloc_new(NULL);
+// af_TARGET(J, ..., *af);
+// js_endtry(J);
+// return 0;
+// 2. Use s_TARGET like so (frees if allocated, throws if {s,af}_TARGET threw):
+// void *af = NULL;
+// int r = s_TARGET(J, ..., &af); // use J, af where the callee expects.
+// talloc_free(af);
+// if (r)
+// js_throw(J);
+//
+// The reason that the allocation happens inside try/catch is that js_try
+// itself can throw (if it runs out of try-stack) and therefore the code
+// inside the try part is not reached - but neither is the catch part(!),
+// and instead it throws to the next outer catch - but before we've allocated
+// anything, hence no leaks on such case. If js_try did get entered, then the
+// allocation happened, and then if af_TARGET threw then s_TARGET will catch
+// it (and return 1) and we'll free if afterwards.
+
+// add_af_file, add_af_dir, add_af_mpv_alloc take a valid FILE*/DIR*/char* value
+// respectively, and fclose/closedir/mpv_free it when the parent is freed.
+
+static void destruct_af_file(void *p)
+{
+ fclose(*(FILE**)p);
+}
+
+static void add_af_file(void *parent, FILE *f)
+{
+ FILE **pf = talloc(parent, FILE*);
+ *pf = f;
+ talloc_set_destructor(pf, destruct_af_file);
+}
+
+static void destruct_af_dir(void *p)
+{
+ closedir(*(DIR**)p);
+}
+
+static void add_af_dir(void *parent, DIR *d)
+{
+ DIR **pd = talloc(parent, DIR*);
+ *pd = d;
+ talloc_set_destructor(pd, destruct_af_dir);
+}
+
+static void destruct_af_mpv_alloc(void *p)
+{
+ mpv_free(*(char**)p);
+}
+
+static void add_af_mpv_alloc(void *parent, char *ma)
+{
+ char **p = talloc(parent, char*);
+ *p = ma;
+ talloc_set_destructor(p, destruct_af_mpv_alloc);
+}
+
+static void destruct_af_mpv_node(void *p)
+{
+ mpv_free_node_contents((mpv_node*)p); // does nothing for MPV_FORMAT_NONE
+}
+
+// returns a new zeroed allocated struct mpv_node, and free it and its content
+// when the parent is freed.
+static mpv_node *new_af_mpv_node(void *parent)
+{
+ mpv_node *p = talloc_zero(parent, mpv_node); // .format == MPV_FORMAT_NONE
+ talloc_set_destructor(p, destruct_af_mpv_node);
+ return p;
+}
+
+// Prototype for autofree functions which can be called from inside the vm.
+typedef void (*af_CFunction)(js_State*, void*);
+
+// safely run autofree js c function directly
+static int s_run_af_jsc(js_State *J, af_CFunction fn, void **af)
+{
+ if (js_try(J))
+ return 1;
+ *af = talloc_new(NULL);
+ fn(J, *af);
+ js_endtry(J);
+ return 0;
+}
+
+// The trampoline function through which all autofree functions are called from
+// inside the vm. Obtains the target function address and autofree-call it.
+static void script__autofree(js_State *J)
+{
+ // The target function is at the "af_" property of this function instance.
+ js_currentfunction(J);
+ js_getproperty(J, -1, "af_");
+ af_CFunction fn = (af_CFunction)js_touserdata(J, -1, "af_fn");
+ js_pop(J, 2);
+
+ void *af = NULL;
+ int r = s_run_af_jsc(J, fn, &af);
+ talloc_free(af);
+ if (r)
+ js_throw(J);
+}
+
+// Identical to js_newcfunction, but the function is inserted with an autofree
+// wrapper, and its prototype should have the additional af argument.
+static void af_newcfunction(js_State *J, af_CFunction fn, const char *name,
+ int length)
+{
+ js_newcfunction(J, script__autofree, name, length);
+ js_pushnull(J); // a prototype for the userdata object
+ js_newuserdata(J, "af_fn", fn, NULL); // uses a "af_fn" verification tag
+ js_defproperty(J, -2, "af_", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
+}
+
+/**********************************************************************
+ * Initialization and file loading
+ *********************************************************************/
+
+static const char *get_builtin_file(const char *name)
+{
+ for (int n = 0; builtin_files[n][0]; n++) {
+ if (strcmp(builtin_files[n][0], name) == 0)
+ return builtin_files[n][1];
+ }
+ return NULL;
+}
+
+// Push up to limit bytes of file fname: from builtin_files, else from the OS.
+static void af_push_file(js_State *J, const char *fname, int limit, void *af)
+{
+ char *filename = mp_get_user_path(af, jctx(J)->mpctx->global, fname);
+ MP_VERBOSE(jctx(J), "Reading file '%s'\n", filename);
+ if (limit < 0)
+ limit = INT_MAX - 1;
+
+ const char *builtin = get_builtin_file(filename);
+ if (builtin) {
+ js_pushlstring(J, builtin, MPMIN(limit, strlen(builtin)));
+ return;
+ }
+
+ FILE *f = fopen(filename, "rb");
+ if (!f)
+ js_error(J, "cannot open file: '%s'", filename);
+ add_af_file(af, f);
+
+ int len = MPMIN(limit, 32 * 1024); // initial allocation, size*2 strategy
+ int got = 0;
+ char *s = NULL;
+ while ((s = talloc_realloc(af, s, char, len))) {
+ int want = len - got;
+ int r = fread(s + got, 1, want, f);
+
+ if (feof(f) || (len == limit && r == want)) {
+ js_pushlstring(J, s, got + r);
+ return;
+ }
+ if (r != want)
+ js_error(J, "cannot read data from file: '%s'", filename);
+
+ got = got + r;
+ len = MPMIN(limit, len * 2);
+ }
+
+ js_error(J, "cannot allocate %d bytes for file: '%s'", len, filename);
+}
+
+// Safely run af_push_file.
+static int s_push_file(js_State *J, const char *fname, int limit, void **af)
+{
+ if (js_try(J))
+ return 1;
+ *af = talloc_new(NULL);
+ af_push_file(J, fname, limit, *af);
+ js_endtry(J);
+ return 0;
+}
+
+// Called directly, push up to limit bytes of file fname (from builtin/os).
+static void push_file_content(js_State *J, const char *fname, int limit)
+{
+ void *af = NULL;
+ int r = s_push_file(J, fname, limit, &af);
+ talloc_free(af);
+ if (r)
+ js_throw(J);
+}
+
+// utils.read_file(..). args: fname [,max]. returns [up to max] bytes as string.
+static void script_read_file(js_State *J)
+{
+ int limit = js_isundefined(J, 2) ? -1 : jsL_checkint(J, 2);
+ push_file_content(J, js_tostring(J, 1), limit);
+}
+
+// Runs a file with the caller's this, leaves the stack as is.
+static void run_file(js_State *J, const char *fname)
+{
+ MP_VERBOSE(jctx(J), "Loading file %s\n", fname);
+ push_file_content(J, fname, -1);
+ js_loadstring(J, fname, js_tostring(J, -1));
+ js_copy(J, 0); // use the caller's this
+ js_call(J, 0);
+ js_pop(J, 2); // result, file content
+}
+
+// The spec defines .name and .message for Error objects. Most engines also set
+// a very convenient .stack = name + message + trace, but MuJS instead sets
+// .stackTrace = trace only. Normalize by adding such .stack if required.
+// Run this before anything such that we can get traces on any following errors.
+static const char *norm_err_proto_js = "\
+ if (Error().stackTrace && !Error().stack) {\
+ Object.defineProperty(Error.prototype, 'stack', {\
+ get: function() {\
+ return this.name + ': ' + this.message + this.stackTrace;\
+ }\
+ });\
+ }\
+";
+
+static void add_functions(js_State*, struct script_ctx*);
+
+// args: none. called as script, setup and run the main script
+static void script__run_script(js_State *J)
+{
+ js_loadstring(J, "@/norm_err.js", norm_err_proto_js);
+ js_copy(J, 0);
+ js_pcall(J, 0);
+
+ struct script_ctx *ctx = jctx(J);
+ add_functions(J, ctx);
+ run_file(J, "@/defaults.js");
+ run_file(J, ctx->filename); // the main file to run
+
+ if (!js_hasproperty(J, 0, "mp_event_loop") || !js_iscallable(J, -1))
+ js_error(J, "no event loop function");
+ js_copy(J, 0);
+ js_call(J, 0); // mp_event_loop
+}
+
+// Safely set last error from stack top: stack trace or toString or generic.
+// May leave items on stack - the caller should detect and pop if it cares.
+static void s_top_to_last_error(struct script_ctx *ctx, js_State *J)
+{
+ set_last_error(ctx, 1, "unknown error");
+ if (js_try(J))
+ return;
+ if (js_isobject(J, -1))
+ js_hasproperty(J, -1, "stack"); // fetches it if exists
+ set_last_error(ctx, 1, js_tostring(J, -1));
+ js_endtry(J);
+}
+
+// MuJS can report warnings through this.
+static void report_handler(js_State *J, const char *msg)
+{
+ MP_WARN(jctx(J), "[JS] %s\n", msg);
+}
+
+// Safely setup the js vm for calling run_script.
+static int s_init_js(js_State *J, struct script_ctx *ctx)
+{
+ if (js_try(J))
+ return 1;
+ js_setcontext(J, ctx);
+ js_setreport(J, report_handler);
+ js_newcfunction(J, script__run_script, "run_script", 0);
+ js_pushglobal(J); // 'this' for script__run_script
+ js_endtry(J);
+ return 0;
+}
+
+static void *mp_js_alloc(void *actx, void *ptr, int size_)
+{
+ if (size_ < 0)
+ return NULL;
+
+ struct script_ctx* ctx = actx;
+ size_t size = size_, osize = 0;
+ if (ptr) // free/realloc
+ osize = ta_get_size(ptr);
+
+ void *ret = talloc_realloc_size(actx, ptr, size);
+
+ if (!size || ret) { // free / successful realloc/malloc
+ ctx->js_malloc_size = ctx->js_malloc_size - osize + size;
+ stats_size_value(ctx->stats, "mem", ctx->js_malloc_size);
+ }
+ return ret;
+}
+
+/**********************************************************************
+ * Initialization - booting the script
+ *********************************************************************/
+// s_load_javascript: (entry point) creates the js vm, runs the script, returns
+// on script exit or uncaught js errors. Never throws.
+// script__run_script: - loads the built in functions and vars into the vm
+// - runs the default file[s] and the main script file
+// - calls mp_event_loop, returns on script-exit or throws.
+//
+// Note: init functions don't need autofree. They can use ctx as a talloc
+// context and free normally. If they throw - ctx is freed right afterwards.
+static int s_load_javascript(struct mp_script_args *args)
+{
+ struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct script_ctx) {
+ .client = args->client,
+ .mpctx = args->mpctx,
+ .log = args->log,
+ .last_error_str = talloc_strdup(ctx, "Cannot initialize JavaScript"),
+ .filename = args->filename,
+ .path = args->path,
+ .js_malloc_size = 0,
+ .stats = stats_ctx_create(ctx, args->mpctx->global,
+ mp_tprintf(80, "script/%s", mpv_client_name(args->client))),
+ };
+
+ stats_register_thread_cputime(ctx->stats, "cpu");
+
+ js_Alloc alloc_fn = NULL;
+ void *actx = NULL;
+
+ if (args->mpctx->opts->js_memory_report) {
+ alloc_fn = mp_js_alloc;
+ actx = ctx;
+ }
+
+ int r = -1;
+ js_State *J = js_newstate(alloc_fn, actx, 0);
+ if (!J || s_init_js(J, ctx))
+ goto error_out;
+
+ set_last_error(ctx, 0, NULL);
+ if (js_pcall(J, 0)) { // script__run_script
+ s_top_to_last_error(ctx, J);
+ goto error_out;
+ }
+
+ r = 0;
+
+error_out:
+ if (r)
+ MP_FATAL(ctx, "%s\n", ctx->last_error_str);
+ if (J)
+ js_freestate(J);
+
+ talloc_free(ctx);
+ return r;
+}
+
+/**********************************************************************
+ * Main mp.* scripting APIs and helpers
+ *********************************************************************/
+// Return the index in opts of stack[idx] (or of def if undefined), else throws.
+static int checkopt(js_State *J, int idx, const char *def, const char *opts[],
+ const char *desc)
+{
+ const char *opt = js_isundefined(J, idx) ? def : js_tostring(J, idx);
+ for (int i = 0; opts[i]; i++) {
+ if (strcmp(opt, opts[i]) == 0)
+ return i;
+ }
+ js_error(J, "Invalid %s '%s'", desc, opt);
+}
+
+// args: level as string and a variable numbers of args to print. adds final \n
+static void script_log(js_State *J)
+{
+ const char *level = js_tostring(J, 1);
+ int msgl = mp_msg_find_level(level);
+ if (msgl < 0)
+ js_error(J, "Invalid log level '%s'", level);
+
+ struct mp_log *log = jctx(J)->log;
+ for (int top = js_gettop(J), i = 2; i < top; i++)
+ mp_msg(log, msgl, (i == 2 ? "%s" : " %s"), js_tostring(J, i));
+ mp_msg(log, msgl, "\n");
+ push_success(J);
+}
+
+static void script_find_config_file(js_State *J, void *af)
+{
+ const char *fname = js_tostring(J, 1);
+ char *path = mp_find_config_file(af, jctx(J)->mpctx->global, fname);
+ if (path) {
+ js_pushstring(J, path);
+ } else {
+ push_failure(J, "not found");
+ }
+}
+
+static void script__request_event(js_State *J)
+{
+ const char *event = js_tostring(J, 1);
+ bool enable = js_toboolean(J, 2);
+
+ for (int n = 0; n < 256; n++) {
+ // some n's may be missing ("holes"), returning NULL
+ const char *name = mpv_event_name(n);
+ if (name && strcmp(name, event) == 0) {
+ push_status(J, mpv_request_event(jclient(J), n, enable));
+ return;
+ }
+ }
+ push_failure(J, "Unknown event name");
+}
+
+static void script_enable_messages(js_State *J)
+{
+ const char *level = js_tostring(J, 1);
+ int e = mpv_request_log_messages(jclient(J), level);
+ if (e == MPV_ERROR_INVALID_PARAMETER)
+ js_error(J, "Invalid log level '%s'", level);
+ push_status(J, e);
+}
+
+// args - command [with arguments] as string
+static void script_command(js_State *J)
+{
+ push_status(J, mpv_command_string(jclient(J), js_tostring(J, 1)));
+}
+
+// args: strings of command and then variable number of arguments
+static void script_commandv(js_State *J)
+{
+ const char *argv[MP_CMD_MAX_ARGS + 1];
+ int length = js_gettop(J) - 1;
+ if (length >= MP_ARRAY_SIZE(argv))
+ js_error(J, "Too many arguments");
+
+ for (int i = 0; i < length; i++)
+ argv[i] = js_tostring(J, 1 + i);
+ argv[length] = NULL;
+ push_status(J, mpv_command(jclient(J), argv));
+}
+
+// args: name, string value
+static void script_set_property(js_State *J)
+{
+ int e = mpv_set_property_string(jclient(J), js_tostring(J, 1),
+ js_tostring(J, 2));
+ push_status(J, e);
+}
+
+// args: name, boolean
+static void script_set_property_bool(js_State *J)
+{
+ int v = js_toboolean(J, 2);
+ int e = mpv_set_property(jclient(J), js_tostring(J, 1), MPV_FORMAT_FLAG, &v);
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property_number(js_State *J)
+{
+ double result;
+ const char *name = js_tostring(J, 1);
+ int e = mpv_get_property(jclient(J), name, MPV_FORMAT_DOUBLE, &result);
+ if (!pushed_error(J, e, 2))
+ js_pushnumber(J, result);
+}
+
+// args: name, native value
+static void script_set_property_native(js_State *J, void *af)
+{
+ mpv_node node;
+ makenode(af, &node, J, 2);
+ mpv_handle *h = jclient(J);
+ int e = mpv_set_property(h, js_tostring(J, 1), MPV_FORMAT_NODE, &node);
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property(js_State *J, void *af)
+{
+ mpv_handle *h = jclient(J);
+ char *res = NULL;
+ int e = mpv_get_property(h, js_tostring(J, 1), MPV_FORMAT_STRING, &res);
+ if (e >= 0)
+ add_af_mpv_alloc(af, res);
+ if (!pushed_error(J, e, 2))
+ js_pushstring(J, res);
+}
+
+// args: name
+static void script_del_property(js_State *J)
+{
+ int e = mpv_del_property(jclient(J), js_tostring(J, 1));
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property_bool(js_State *J)
+{
+ int result;
+ mpv_handle *h = jclient(J);
+ int e = mpv_get_property(h, js_tostring(J, 1), MPV_FORMAT_FLAG, &result);
+ if (!pushed_error(J, e, 2))
+ js_pushboolean(J, result);
+}
+
+// args: name, number
+static void script_set_property_number(js_State *J)
+{
+ double v = js_tonumber(J, 2);
+ mpv_handle *h = jclient(J);
+ int e = mpv_set_property(h, js_tostring(J, 1), MPV_FORMAT_DOUBLE, &v);
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property_native(js_State *J, void *af)
+{
+ const char *name = js_tostring(J, 1);
+ mpv_handle *h = jclient(J);
+ mpv_node *presult_node = new_af_mpv_node(af);
+ int e = mpv_get_property(h, name, MPV_FORMAT_NODE, presult_node);
+ if (!pushed_error(J, e, 2))
+ pushnode(J, presult_node);
+}
+
+// args: name [,def]
+static void script_get_property_osd(js_State *J, void *af)
+{
+ const char *name = js_tostring(J, 1);
+ mpv_handle *h = jclient(J);
+ char *res = NULL;
+ int e = mpv_get_property(h, name, MPV_FORMAT_OSD_STRING, &res);
+ if (e >= 0)
+ add_af_mpv_alloc(af, res);
+ if (!pushed_error(J, e, 2))
+ js_pushstring(J, res);
+}
+
+// args: id, name, type
+static void script__observe_property(js_State *J)
+{
+ const char *fmts[] = {"none", "native", "bool", "string", "number", NULL};
+ const mpv_format mf[] = {MPV_FORMAT_NONE, MPV_FORMAT_NODE, MPV_FORMAT_FLAG,
+ MPV_FORMAT_STRING, MPV_FORMAT_DOUBLE};
+
+ mpv_format f = mf[checkopt(J, 3, "none", fmts, "observe type")];
+ int e = mpv_observe_property(jclient(J), jsL_checkuint64(J, 1),
+ js_tostring(J, 2),
+ f);
+ push_status(J, e);
+}
+
+// args: id
+static void script__unobserve_property(js_State *J)
+{
+ int e = mpv_unobserve_property(jclient(J), jsL_checkuint64(J, 1));
+ push_status(J, e);
+}
+
+// args: native (array of command and args, similar to commandv) [,def]
+static void script_command_native(js_State *J, void *af)
+{
+ mpv_node cmd;
+ makenode(af, &cmd, J, 1);
+ mpv_node *presult_node = new_af_mpv_node(af);
+ int e = mpv_command_node(jclient(J), &cmd, presult_node);
+ if (!pushed_error(J, e, 2))
+ pushnode(J, presult_node);
+}
+
+// args: async-command-id, native-command
+static void script__command_native_async(js_State *J, void *af)
+{
+ uint64_t id = jsL_checkuint64(J, 1);
+ struct mpv_node node;
+ makenode(af, &node, J, 2);
+ push_status(J, mpv_command_node_async(jclient(J), id, &node));
+}
+
+// args: async-command-id
+static void script__abort_async_command(js_State *J)
+{
+ mpv_abort_async_command(jclient(J), jsL_checkuint64(J, 1));
+ push_success(J);
+}
+
+// args: none, result in millisec
+static void script_get_time_ms(js_State *J)
+{
+ js_pushnumber(J, mpv_get_time_us(jclient(J)) / (double)(1000));
+}
+
+// push object with properties names (NULL terminated) with respective vals
+static void push_nums_obj(js_State *J, const char * const names[],
+ const double vals[])
+{
+ js_newobject(J);
+ for (int i = 0; names[i]; i++) {
+ js_pushnumber(J, vals[i]);
+ js_setproperty(J, -2, names[i]);
+ }
+}
+
+// args: input-section-name, x0, y0, x1, y1
+static void script_input_set_section_mouse_area(js_State *J)
+{
+ char *section = (char *)js_tostring(J, 1);
+ mp_input_set_section_mouse_area(jctx(J)->mpctx->input, section,
+ jsL_checkint(J, 2), jsL_checkint(J, 3), // x0, y0
+ jsL_checkint(J, 4), jsL_checkint(J, 5)); // x1, y1
+ push_success(J);
+}
+
+// args: time-in-ms [,format-string]
+static void script_format_time(js_State *J, void *af)
+{
+ double t = js_tonumber(J, 1);
+ const char *fmt = js_isundefined(J, 2) ? "%H:%M:%S" : js_tostring(J, 2);
+ char *r = talloc_steal(af, mp_format_time_fmt(fmt, t));
+ if (!r)
+ js_error(J, "Invalid time format string '%s'", fmt);
+ js_pushstring(J, r);
+}
+
+// TODO: untested
+static void script_get_wakeup_pipe(js_State *J)
+{
+ js_pushnumber(J, mpv_get_wakeup_pipe(jclient(J)));
+}
+
+// args: name (str), priority (int), id (uint)
+static void script__hook_add(js_State *J)
+{
+ const char *name = js_tostring(J, 1);
+ int pri = jsL_checkint(J, 2);
+ uint64_t id = jsL_checkuint64(J, 3);
+ push_status(J, mpv_hook_add(jclient(J), id, name, pri));
+}
+
+// args: id (uint)
+static void script__hook_continue(js_State *J)
+{
+ push_status(J, mpv_hook_continue(jclient(J), jsL_checkuint64(J, 1)));
+}
+
+/**********************************************************************
+ * mp.utils
+ *********************************************************************/
+
+// args: [path [,filter]]
+static void script_readdir(js_State *J, void *af)
+{
+ // 0 1 2 3
+ const char *filters[] = {"all", "files", "dirs", "normal", NULL};
+ const char *path = js_isundefined(J, 1) ? "." : js_tostring(J, 1);
+ int t = checkopt(J, 2, "normal", filters, "listing filter");
+
+ DIR *dir = opendir(path);
+ if (!dir) {
+ push_failure(J, "Cannot open dir");
+ return;
+ }
+ add_af_dir(af, dir);
+ set_last_error(jctx(J), 0, NULL);
+ js_newarray(J); // the return value
+ char *fullpath = talloc_strdup(af, "");
+ struct dirent *e;
+ int n = 0;
+ while ((e = readdir(dir))) {
+ char *name = e->d_name;
+ if (t) {
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
+ continue;
+ if (fullpath)
+ fullpath[0] = '\0';
+ fullpath = talloc_asprintf_append(fullpath, "%s/%s", path, name);
+ struct stat st;
+ if (stat(fullpath, &st))
+ continue;
+ if (!(((t & 1) && S_ISREG(st.st_mode)) ||
+ ((t & 2) && S_ISDIR(st.st_mode))))
+ {
+ continue;
+ }
+ }
+ js_pushstring(J, name);
+ js_setindex(J, -2, n++);
+ }
+}
+
+static void script_file_info(js_State *J)
+{
+ const char *path = js_tostring(J, 1);
+
+ struct stat statbuf;
+ if (stat(path, &statbuf) != 0) {
+ push_failure(J, "Cannot stat path");
+ return;
+ }
+ // Clear last error
+ set_last_error(jctx(J), 0, NULL);
+
+ const char * stat_names[] = {
+ "mode", "size",
+ "atime", "mtime", "ctime", NULL
+ };
+ const double stat_values[] = {
+ statbuf.st_mode,
+ statbuf.st_size,
+ statbuf.st_atime,
+ statbuf.st_mtime,
+ statbuf.st_ctime
+ };
+ // Create an object and add all fields
+ push_nums_obj(J, stat_names, stat_values);
+
+ // Convenience booleans
+ js_pushboolean(J, S_ISREG(statbuf.st_mode));
+ js_setproperty(J, -2, "is_file");
+
+ js_pushboolean(J, S_ISDIR(statbuf.st_mode));
+ js_setproperty(J, -2, "is_dir");
+}
+
+
+static void script_split_path(js_State *J)
+{
+ const char *p = js_tostring(J, 1);
+ bstr fname = mp_dirname(p);
+ js_newarray(J);
+ js_pushlstring(J, fname.start, fname.len);
+ js_setindex(J, -2, 0);
+ js_pushstring(J, mp_basename(p));
+ js_setindex(J, -2, 1);
+}
+
+static void script_join_path(js_State *J, void *af)
+{
+ js_pushstring(J, mp_path_join(af, js_tostring(J, 1), js_tostring(J, 2)));
+}
+
+// args: is_append, prefixed file name, data (c-str)
+static void script__write_file(js_State *J, void *af)
+{
+ static const char *prefix = "file://";
+ bool append = js_toboolean(J, 1);
+ const char *fname = js_tostring(J, 2);
+ const char *data = js_tostring(J, 3);
+ const char *opstr = append ? "append" : "write";
+
+ if (strstr(fname, prefix) != fname) // simple protection for incorrect use
+ js_error(J, "File name must be prefixed with '%s'", prefix);
+ fname += strlen(prefix);
+ fname = mp_get_user_path(af, jctx(J)->mpctx->global, fname);
+ MP_VERBOSE(jctx(J), "%s file '%s'\n", opstr, fname);
+
+ FILE *f = fopen(fname, append ? "ab" : "wb");
+ if (!f)
+ js_error(J, "Cannot open (%s) file: '%s'", opstr, fname);
+ add_af_file(af, f);
+
+ int len = strlen(data); // limited by terminating null
+ int wrote = fwrite(data, 1, len, f);
+ if (len != wrote)
+ js_error(J, "Cannot %s to file: '%s'", opstr, fname);
+ js_pushboolean(J, 1); // success. doesn't touch last_error
+}
+
+// args: env var name
+static void script_getenv(js_State *J)
+{
+ const char *v = getenv(js_tostring(J, 1));
+ if (v) {
+ js_pushstring(J, v);
+ } else {
+ js_pushundefined(J);
+ }
+}
+
+// args: none
+static void script_get_env_list(js_State *J)
+{
+ js_newarray(J);
+ for (int n = 0; environ && environ[n]; n++) {
+ js_pushstring(J, environ[n]);
+ js_setindex(J, -2, n);
+ }
+}
+
+// args: as-filename, content-string, returns the compiled result as a function
+static void script_compile_js(js_State *J)
+{
+ js_loadstring(J, js_tostring(J, 1), js_tostring(J, 2));
+}
+
+// args: true = print info (with the warning report function - no info report)
+static void script__gc(js_State *J)
+{
+ js_gc(J, js_toboolean(J, 1) ? 1 : 0);
+ push_success(J);
+}
+
+/**********************************************************************
+ * Core functions: pushnode, makenode and the event loop backend
+ *********************************************************************/
+
+// pushes a js value/array/object from an mpv_node
+static void pushnode(js_State *J, mpv_node *node)
+{
+ int len;
+ switch (node->format) {
+ case MPV_FORMAT_NONE: js_pushnull(J); break;
+ case MPV_FORMAT_STRING: js_pushstring(J, node->u.string); break;
+ case MPV_FORMAT_INT64: js_pushnumber(J, node->u.int64); break;
+ case MPV_FORMAT_DOUBLE: js_pushnumber(J, node->u.double_); break;
+ case MPV_FORMAT_FLAG: js_pushboolean(J, node->u.flag); break;
+ case MPV_FORMAT_BYTE_ARRAY:
+ js_pushlstring(J, node->u.ba->data, node->u.ba->size);
+ break;
+ case MPV_FORMAT_NODE_ARRAY:
+ js_newarray(J);
+ len = node->u.list->num;
+ for (int n = 0; n < len; n++) {
+ pushnode(J, &node->u.list->values[n]);
+ js_setindex(J, -2, n);
+ }
+ break;
+ case MPV_FORMAT_NODE_MAP:
+ js_newobject(J);
+ len = node->u.list->num;
+ for (int n = 0; n < len; n++) {
+ pushnode(J, &node->u.list->values[n]);
+ js_setproperty(J, -2, node->u.list->keys[n]);
+ }
+ break;
+ default:
+ js_pushstring(J, "[UNSUPPORTED_MPV_FORMAT]");
+ break;
+ }
+}
+
+// For the object at stack index idx, extract the (own) property names into
+// keys array (and allocate it to accommodate) and return the number of keys.
+static int get_obj_properties(void *ta_ctx, char ***keys, js_State *J, int idx)
+{
+ int length = 0;
+ js_pushiterator(J, idx, 1);
+
+ *keys = talloc_new(ta_ctx);
+ const char *name;
+ while ((name = js_nextiterator(J, -1)))
+ MP_TARRAY_APPEND(ta_ctx, *keys, length, talloc_strdup(ta_ctx, name));
+
+ js_pop(J, 1); // the iterator
+ return length;
+}
+
+// true if we don't lose (too much) precision when casting to int64
+static bool same_as_int64(double d)
+{
+ // The range checks also validly filter inf and nan, so behavior is defined
+ return d >= INT64_MIN && d <= (double) INT64_MAX && d == (int64_t)d;
+}
+
+static int jsL_checkint(js_State *J, int idx)
+{
+ double d = js_tonumber(J, idx);
+ if (!(d >= INT_MIN && d <= INT_MAX))
+ js_error(J, "int out of range at index %d", idx);
+ return d;
+}
+
+static uint64_t jsL_checkuint64(js_State *J, int idx)
+{
+ double d = js_tonumber(J, idx);
+ if (!(d >= 0 && d <= (double) UINT64_MAX))
+ js_error(J, "uint64 out of range at index %d", idx);
+ return d;
+}
+
+// From the js stack value/array/object at index idx
+static void makenode(void *ta_ctx, mpv_node *dst, js_State *J, int idx)
+{
+ if (js_isundefined(J, idx) || js_isnull(J, idx)) {
+ dst->format = MPV_FORMAT_NONE;
+
+ } else if (js_isboolean(J, idx)) {
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = js_toboolean(J, idx);
+
+ } else if (js_isnumber(J, idx)) {
+ double val = js_tonumber(J, idx);
+ if (same_as_int64(val)) { // use int, because we can
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = val;
+ } else {
+ dst->format = MPV_FORMAT_DOUBLE;
+ dst->u.double_ = val;
+ }
+
+ } else if (js_isarray(J, idx)) {
+ dst->format = MPV_FORMAT_NODE_ARRAY;
+ dst->u.list = talloc(ta_ctx, struct mpv_node_list);
+ dst->u.list->keys = NULL;
+
+ int length = js_getlength(J, idx);
+ dst->u.list->num = length;
+ dst->u.list->values = talloc_array(ta_ctx, mpv_node, length);
+ for (int n = 0; n < length; n++) {
+ js_getindex(J, idx, n);
+ makenode(ta_ctx, &dst->u.list->values[n], J, -1);
+ js_pop(J, 1);
+ }
+
+ } else if (js_isobject(J, idx)) {
+ dst->format = MPV_FORMAT_NODE_MAP;
+ dst->u.list = talloc(ta_ctx, struct mpv_node_list);
+
+ int length = get_obj_properties(ta_ctx, &dst->u.list->keys, J, idx);
+ dst->u.list->num = length;
+ dst->u.list->values = talloc_array(ta_ctx, mpv_node, length);
+ for (int n = 0; n < length; n++) {
+ js_getproperty(J, idx, dst->u.list->keys[n]);
+ makenode(ta_ctx, &dst->u.list->values[n], J, -1);
+ js_pop(J, 1);
+ }
+
+ } else { // string, or anything else as string
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(ta_ctx, js_tostring(J, idx));
+ }
+}
+
+// args: wait in secs (infinite if negative) if mpv doesn't send events earlier.
+static void script_wait_event(js_State *J, void *af)
+{
+ double timeout = js_isnumber(J, 1) ? js_tonumber(J, 1) : -1;
+ mpv_event *event = mpv_wait_event(jclient(J), timeout);
+
+ mpv_node *rn = new_af_mpv_node(af);
+ mpv_event_to_node(rn, event);
+ pushnode(J, rn);
+}
+
+/**********************************************************************
+ * Script functions setup
+ *********************************************************************/
+#define FN_ENTRY(name, length) {#name, length, script_ ## name, NULL}
+#define AF_ENTRY(name, length) {#name, length, NULL, script_ ## name}
+struct fn_entry {
+ const char *name;
+ int length;
+ js_CFunction jsc_fn;
+ af_CFunction afc_fn;
+};
+
+// Names starting with underscore are wrapped at @defaults.js
+// FN_ENTRY is a normal js C function, AF_ENTRY is an autofree js C function.
+static const struct fn_entry main_fns[] = {
+ FN_ENTRY(log, 1),
+ AF_ENTRY(wait_event, 1),
+ FN_ENTRY(_request_event, 2),
+ AF_ENTRY(find_config_file, 1),
+ FN_ENTRY(command, 1),
+ FN_ENTRY(commandv, 0),
+ AF_ENTRY(command_native, 2),
+ AF_ENTRY(_command_native_async, 2),
+ FN_ENTRY(_abort_async_command, 1),
+ FN_ENTRY(del_property, 1),
+ FN_ENTRY(get_property_bool, 2),
+ FN_ENTRY(get_property_number, 2),
+ AF_ENTRY(get_property_native, 2),
+ AF_ENTRY(get_property, 2),
+ AF_ENTRY(get_property_osd, 2),
+ FN_ENTRY(set_property, 2),
+ FN_ENTRY(set_property_bool, 2),
+ FN_ENTRY(set_property_number, 2),
+ AF_ENTRY(set_property_native, 2),
+ FN_ENTRY(_observe_property, 3),
+ FN_ENTRY(_unobserve_property, 1),
+ FN_ENTRY(get_time_ms, 0),
+ AF_ENTRY(format_time, 2),
+ FN_ENTRY(enable_messages, 1),
+ FN_ENTRY(get_wakeup_pipe, 0),
+ FN_ENTRY(_hook_add, 3),
+ FN_ENTRY(_hook_continue, 1),
+ FN_ENTRY(input_set_section_mouse_area, 5),
+ FN_ENTRY(last_error, 0),
+ FN_ENTRY(_set_last_error, 1),
+ {0}
+};
+
+static const struct fn_entry utils_fns[] = {
+ AF_ENTRY(readdir, 2),
+ FN_ENTRY(file_info, 1),
+ FN_ENTRY(split_path, 1),
+ AF_ENTRY(join_path, 2),
+ FN_ENTRY(get_env_list, 0),
+
+ FN_ENTRY(read_file, 2),
+ AF_ENTRY(_write_file, 3),
+ FN_ENTRY(getenv, 1),
+ FN_ENTRY(compile_js, 2),
+ FN_ENTRY(_gc, 1),
+ {0}
+};
+
+// Adds an object <module> with the functions at e to the top object
+static void add_package_fns(js_State *J, const char *module,
+ const struct fn_entry *e)
+{
+ js_newobject(J);
+ for (int n = 0; e[n].name; n++) {
+ if (e[n].jsc_fn) {
+ js_newcfunction(J, e[n].jsc_fn, e[n].name, e[n].length);
+ } else {
+ af_newcfunction(J, e[n].afc_fn, e[n].name, e[n].length);
+ }
+ js_setproperty(J, -2, e[n].name);
+ }
+ js_setproperty(J, -2, module);
+}
+
+// Called directly, adds functions/vars to the caller's this.
+static void add_functions(js_State *J, struct script_ctx *ctx)
+{
+ js_copy(J, 0);
+ add_package_fns(J, "mp", main_fns);
+ js_getproperty(J, 0, "mp"); // + this mp
+ add_package_fns(J, "utils", utils_fns);
+
+ js_pushstring(J, mpv_client_name(ctx->client));
+ js_setproperty(J, -2, "script_name");
+
+ js_pushstring(J, ctx->filename);
+ js_setproperty(J, -2, "script_file");
+
+ if (ctx->path) {
+ js_pushstring(J, ctx->path);
+ js_setproperty(J, -2, "script_path");
+ }
+
+ js_pop(J, 2); // leave the stack as we got it
+}
+
+// main export of this file, used by cplayer to load js scripts
+const struct mp_scripting mp_scripting_js = {
+ .name = "js",
+ .file_ext = "js",
+ .load = s_load_javascript,
+};
diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js
new file mode 100644
index 0000000..d906ec2
--- /dev/null
+++ b/player/javascript/defaults.js
@@ -0,0 +1,782 @@
+"use strict";
+(function main_default_js(g) {
+// - g is the global object.
+// - User callbacks called without 'this', global only if callee is non-strict.
+// - The names of function expressions are not required, but are used in stack
+// traces. We name them where useful to show up (fname:#line always shows).
+
+mp.msg = { log: mp.log };
+mp.msg.verbose = mp.log.bind(null, "v");
+var levels = ["fatal", "error", "warn", "info", "debug", "trace"];
+levels.forEach(function(l) { mp.msg[l] = mp.log.bind(null, l) });
+
+// same as {} but without inherited stuff, e.g. o["toString"] doesn't exist.
+// used where we try to fetch items by keys which we don't absolutely trust.
+function new_cache() {
+ return Object.create(null, {});
+}
+
+/**********************************************************************
+ * event handlers, property observers, idle, client messages, hooks, async
+ *********************************************************************/
+var ehandlers = new_cache() // items of event-name: array of {maybe cb: fn}
+
+mp.register_event = function(name, fn) {
+ if (!ehandlers[name])
+ ehandlers[name] = [];
+ ehandlers[name] = ehandlers[name].concat([{cb: fn}]); // replaces the arr
+ return mp._request_event(name, true);
+}
+
+mp.unregister_event = function(fn) {
+ for (var name in ehandlers) {
+ ehandlers[name] = ehandlers[name].filter(function(h) {
+ if (h.cb != fn)
+ return true;
+ delete h.cb; // dispatch could have a ref to h
+ }); // replacing, not mutating the array
+ if (!ehandlers[name].length) {
+ delete ehandlers[name];
+ mp._request_event(name, false);
+ }
+ }
+}
+
+// call only pre-registered handlers, but not ones which got unregistered
+function dispatch_event(e) {
+ var handlers = ehandlers[e.event];
+ if (handlers) {
+ for (var len = handlers.length, i = 0; i < len; i++) {
+ var cb = handlers[i].cb; // 'handlers' won't mutate, but unregister
+ if (cb) // could remove cb from some items
+ cb(e);
+ }
+ }
+}
+
+// ----- idle observers -----
+var iobservers = [], // array of callbacks
+ ideleted = false;
+
+mp.register_idle = function(fn) {
+ iobservers.push(fn);
+}
+
+mp.unregister_idle = function(fn) {
+ iobservers.forEach(function(f, i) {
+ if (f == fn)
+ delete iobservers[i]; // -> same length but [more] sparse
+ });
+ ideleted = true;
+}
+
+function notify_idle_observers() {
+ // forEach and filter skip deleted items and newly added items
+ iobservers.forEach(function(f) { f() });
+ if (ideleted) {
+ iobservers = iobservers.filter(function() { return true });
+ ideleted = false;
+ }
+}
+
+// ----- property observers -----
+var next_oid = 1,
+ observers = new_cache(); // items of id: fn
+
+mp.observe_property = function(name, format, fn) {
+ var id = next_oid++;
+ observers[id] = fn;
+ return mp._observe_property(id, name, format || undefined); // allow null
+}
+
+mp.unobserve_property = function(fn) {
+ for (var id in observers) {
+ if (observers[id] == fn) {
+ delete observers[id];
+ mp._unobserve_property(id);
+ }
+ }
+}
+
+function notify_observer(e) {
+ var cb = observers[e.id];
+ if (cb)
+ cb(e.name, e.data);
+}
+
+// ----- Client messages -----
+var messages = new_cache(); // items of name: fn
+
+// overrides name. no libmpv API to reg/unreg specific messages.
+mp.register_script_message = function(name, fn) {
+ messages[name] = fn;
+}
+
+mp.unregister_script_message = function(name) {
+ delete messages[name];
+}
+
+function dispatch_message(ev) {
+ var cb = ev.args.length ? messages[ev.args[0]] : false;
+ if (cb)
+ cb.apply(null, ev.args.slice(1));
+}
+
+// ----- hooks -----
+var hooks = []; // array of callbacks, id is index+1
+
+function run_hook(ev) {
+ var state = 0; // 0:initial, 1:deferred, 2:continued
+ function do_cont() { return state = 2, mp._hook_continue(ev.hook_id) }
+
+ function err() { return mp.msg.error("hook already continued"), undefined }
+ function usr_defer() { return state == 2 ? err() : (state = 1, true) }
+ function usr_cont() { return state == 2 ? err() : do_cont() }
+
+ var cb = ev.id > 0 && hooks[ev.id - 1];
+ if (cb)
+ cb({ defer: usr_defer, cont: usr_cont });
+ return state == 0 ? do_cont() : true;
+}
+
+mp.add_hook = function add_hook(name, pri, fn) {
+ hooks.push(fn);
+ // 50 (scripting docs default priority) maps to 0 (default in C API docs)
+ return mp._hook_add(name, pri - 50, hooks.length);
+}
+
+// ----- async commands -----
+var async_callbacks = new_cache(); // items of id: fn
+var async_next_id = 1;
+
+mp.command_native_async = function command_native_async(node, cb) {
+ var id = async_next_id++;
+ cb = cb || function dummy() {};
+ if (!mp._command_native_async(id, node)) {
+ var le = mp.last_error();
+ setTimeout(cb, 0, false, undefined, le); /* callback async */
+ mp._set_last_error(le);
+ return undefined;
+ }
+ async_callbacks[id] = cb;
+ return id;
+}
+
+function async_command_handler(ev) {
+ var cb = async_callbacks[ev.id];
+ delete async_callbacks[ev.id];
+ if (ev.error)
+ cb(false, undefined, ev.error);
+ else
+ cb(true, ev.result, "");
+}
+
+mp.abort_async_command = function abort_async_command(id) {
+ // cb will be invoked regardless, possibly with the abort result
+ if (async_callbacks[id])
+ mp._abort_async_command(id);
+}
+
+// shared-script-properties - always an object, even if without properties
+function shared_script_property_set(name, val) {
+ if (arguments.length > 1)
+ return mp.commandv("change-list", "shared-script-properties", "append", "" + name + "=" + val);
+ else
+ return mp.commandv("change-list", "shared-script-properties", "remove", name);
+}
+
+function shared_script_property_get(name) {
+ return mp.get_property_native("shared-script-properties")[name];
+}
+
+function shared_script_property_observe(name, cb) {
+ return mp.observe_property("shared-script-properties", "native",
+ function shared_props_cb(_name, val) { cb(name, val[name]) }
+ );
+}
+
+mp.utils.shared_script_property_set = shared_script_property_set;
+mp.utils.shared_script_property_get = shared_script_property_get;
+mp.utils.shared_script_property_observe = shared_script_property_observe;
+
+// osd-ass
+var next_assid = 1;
+mp.create_osd_overlay = function create_osd_overlay(format) {
+ return {
+ format: format || "ass-events",
+ id: next_assid++,
+ data: "",
+ res_x: 0,
+ res_y: 720,
+ z: 0,
+
+ update: function ass_update() {
+ var cmd = {}; // shallow clone of `this', excluding methods
+ for (var k in this) {
+ if (typeof this[k] != "function")
+ cmd[k] = this[k];
+ }
+
+ cmd.name = "osd-overlay";
+ cmd.res_x = Math.round(this.res_x);
+ cmd.res_y = Math.round(this.res_y);
+
+ return mp.command_native(cmd);
+ },
+
+ remove: function ass_remove() {
+ mp.command_native({
+ name: "osd-overlay",
+ id: this.id,
+ format: "none",
+ data: "",
+ });
+ return mp.last_error() ? undefined : true;
+ },
+ };
+}
+
+// osd-ass legacy API
+mp.set_osd_ass = function set_osd_ass(res_x, res_y, data) {
+ if (!mp._legacy_overlay)
+ mp._legacy_overlay = mp.create_osd_overlay("ass-events");
+
+ var lo = mp._legacy_overlay;
+ if (lo.res_x == res_x && lo.res_y == res_y && lo.data == data)
+ return true;
+
+ mp._legacy_overlay.res_x = res_x;
+ mp._legacy_overlay.res_y = res_y;
+ mp._legacy_overlay.data = data;
+ return mp._legacy_overlay.update();
+}
+
+// the following return undefined on error, null passthrough, or legacy object
+mp.get_osd_size = function get_osd_size() {
+ var d = mp.get_property_native("osd-dimensions");
+ return d && {width: d.w, height: d.h, aspect: d.aspect};
+}
+mp.get_osd_margins = function get_osd_margins() {
+ var d = mp.get_property_native("osd-dimensions");
+ return d && {left: d.ml, right: d.mr, top: d.mt, bottom: d.mb};
+}
+
+/**********************************************************************
+ * key bindings
+ *********************************************************************/
+// binds: items of (binding) name which are objects of:
+// {cb: fn, forced: bool, maybe input: str, repeatable: bool, complex: bool}
+var binds = new_cache();
+
+function dispatch_key_binding(name, state, key_name) {
+ var cb = binds[name] ? binds[name].cb : false;
+ if (cb) // "script-binding [<script_name>/]<name>" command was invoked
+ cb(state, key_name);
+}
+
+var binds_tid = 0; // flush timer id. actual id's are always true-thy
+mp.flush_key_bindings = function flush_key_bindings() {
+ function prioritized_inputs(arr) {
+ return arr.sort(function(a, b) { return a.id - b.id })
+ .map(function(bind) { return bind.input });
+ }
+
+ var def = [], forced = [];
+ for (var n in binds)
+ if (binds[n].input)
+ (binds[n].forced ? forced : def).push(binds[n]);
+ // newer bindings for the same key override/hide older ones
+ def = prioritized_inputs(def);
+ forced = prioritized_inputs(forced);
+
+ var sect = "input_" + mp.script_name;
+ mp.commandv("define-section", sect, def.join("\n"), "default");
+ mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
+
+ sect = "input_forced_" + mp.script_name;
+ mp.commandv("define-section", sect, forced.join("\n"), "force");
+ mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
+
+ clearTimeout(binds_tid); // cancel future flush if called directly
+ binds_tid = 0;
+}
+
+function sched_bindings_flush() {
+ if (!binds_tid)
+ binds_tid = setTimeout(mp.flush_key_bindings, 0); // fires on idle
+}
+
+// name/opts maybe omitted. opts: object with optional bool members: repeatable,
+// complex, forced, or a string str which is evaluated as object {str: true}.
+var next_bid = 1;
+function add_binding(forced, key, name, fn, opts) {
+ if (typeof name == "function") { // as if "name" is not part of the args
+ opts = fn;
+ fn = name;
+ name = false;
+ }
+ var key_data = {forced: forced};
+ switch (typeof opts) { // merge opts into key_data
+ case "string": key_data[opts] = true; break;
+ case "object": for (var o in opts) key_data[o] = opts[o];
+ }
+ key_data.id = next_bid++;
+ if (!name)
+ name = "__keybinding" + key_data.id; // new unique binding name
+
+ if (key_data.complex) {
+ mp.register_script_message(name, function msg_cb() {
+ fn({event: "press", is_mouse: false});
+ });
+ var KEY_STATES = { u: "up", d: "down", r: "repeat", p: "press" };
+ key_data.cb = function key_cb(state, key_name) {
+ fn({
+ event: KEY_STATES[state[0]] || "unknown",
+ is_mouse: state[1] == "m",
+ key_name: key_name || undefined
+ });
+ }
+ } else {
+ mp.register_script_message(name, fn);
+ key_data.cb = function key_cb(state) {
+ // Emulate the semantics at input.c: mouse emits on up, kb on down.
+ // Also, key repeat triggers the binding again.
+ var e = state[0],
+ emit = (state[1] == "m") ? (e == "u") : (e == "d");
+ if (emit || e == "p" || e == "r" && key_data.repeatable)
+ fn();
+ }
+ }
+
+ if (key)
+ key_data.input = key + " script-binding " + mp.script_name + "/" + name;
+ binds[name] = key_data; // used by user and/or our (key) script-binding
+ sched_bindings_flush();
+}
+
+mp.add_key_binding = add_binding.bind(null, false);
+mp.add_forced_key_binding = add_binding.bind(null, true);
+
+mp.remove_key_binding = function(name) {
+ mp.unregister_script_message(name);
+ delete binds[name];
+ sched_bindings_flush();
+}
+
+/**********************************************************************
+ Timers: compatible HTML5 WindowTimers - set/clear Timeout/Interval
+ - Spec: https://www.w3.org/TR/html5/webappapis.html#timers
+ - Guaranteed to callback a-sync to [re-]insertion (event-loop wise).
+ - Guaranteed to callback by expiration order, or, if equal, by insertion order.
+ - Not guaranteed schedule accuracy, though intervals should have good average.
+ *********************************************************************/
+
+// pending 'timers' ordered by expiration: latest at index 0 (top fires first).
+// Earlier timers are quicker to handle - just push/pop or fewer items to shift.
+var next_tid = 1,
+ timers = [], // while in process_timers, just insertion-ordered (push)
+ tset_is_push = false, // signal set_timer that we're in process_timers
+ tcanceled = false, // or object of items timer-id: true
+ now = mp.get_time_ms; // just an alias
+
+function insert_sorted(arr, t) {
+ for (var i = arr.length - 1; i >= 0 && t.when >= arr[i].when; i--)
+ arr[i + 1] = arr[i]; // move up timers which fire earlier than t
+ arr[i + 1] = t; // i is -1 or fires later than t
+}
+
+// args (is "arguments"): fn_or_str [,duration [,user_arg1 [, user_arg2 ...]]]
+function set_timer(repeat, args) {
+ var fos = args[0],
+ duration = Math.max(0, (args[1] || 0)), // minimum and default are 0
+ t = {
+ id: next_tid++,
+ when: now() + duration,
+ interval: repeat ? duration : -1,
+ callback: (typeof fos == "function") ? fos : Function(fos),
+ args: (args.length < 3) ? false : [].slice.call(args, 2),
+ };
+
+ if (tset_is_push) {
+ timers.push(t);
+ } else {
+ insert_sorted(timers, t);
+ }
+ return t.id;
+}
+
+g.setTimeout = function setTimeout() { return set_timer(false, arguments) };
+g.setInterval = function setInterval() { return set_timer(true, arguments) };
+
+g.clearTimeout = g.clearInterval = function(id) {
+ if (id < next_tid) { // must ignore if not active timer id.
+ if (!tcanceled)
+ tcanceled = {};
+ tcanceled[id] = true;
+ }
+}
+
+// arr: ordered timers array. ret: -1: no timers, 0: due, positive: ms to wait
+function peek_wait(arr) {
+ return arr.length ? Math.max(0, arr[arr.length - 1].when - now()) : -1;
+}
+
+function peek_timers_wait() {
+ return peek_wait(timers); // must not be called while in process_timers
+}
+
+// Callback all due non-canceled timers which were inserted before calling us.
+// Returns wait in ms till the next timer (possibly 0), or -1 if nothing pends.
+function process_timers() {
+ var wait = peek_wait(timers);
+ if (wait != 0)
+ return wait;
+
+ var actives = timers; // only process those already inserted by now
+ timers = []; // we'll handle added new timers at the end of processing.
+ tset_is_push = true; // signal set_timer to just push-insert
+
+ do {
+ var t = actives.pop();
+ if (tcanceled && tcanceled[t.id])
+ continue;
+
+ if (t.args) {
+ t.callback.apply(null, t.args);
+ } else {
+ (0, t.callback)(); // faster, nicer stack trace than t.cb.call()
+ }
+
+ if (t.interval >= 0) {
+ // allow 20 ms delay/clock-resolution/gc before we skip and reset
+ t.when = Math.max(now() - 20, t.when + t.interval);
+ timers.push(t); // insertion order only
+ }
+ } while (peek_wait(actives) == 0);
+
+ // new 'timers' are insertion-ordered. remains of actives are fully ordered
+ timers.forEach(function(t) { insert_sorted(actives, t) });
+ timers = actives; // now we're fully ordered again, and with all timers
+ tset_is_push = false;
+ if (tcanceled) {
+ timers = timers.filter(function(t) { return !tcanceled[t.id] });
+ tcanceled = false;
+ }
+ return peek_wait(timers);
+}
+
+/**********************************************************************
+ CommonJS module/require
+
+ Spec: http://wiki.commonjs.org/wiki/Modules/1.1.1
+ - All the mandatory requirements are implemented, all the unit tests pass.
+ - The implementation makes the following exception:
+ - Allows the chars [~@:\\] in module id for meta-dir/builtin/dos-drive/UNC.
+
+ Implementation choices beyond the specification:
+ - A module may assign to module.exports (rather than only to exports).
+ - A module's 'this' is the global object, also if it sets strict mode.
+ - No 'global'/'self'. Users can do "this.global = this;" before require(..)
+ - A module has "privacy of its top scope", runs in its own function context.
+ - No id identity with symlinks - a valid choice which others make too.
+ - require("X") always maps to "X.js" -> require("foo.js") is file "foo.js.js".
+ - Global modules search paths are 'scripts/modules.js/' in mpv config dirs.
+ - A main script could e.g. require("./abc") to load a non-global module.
+ - Module id supports mpv path enhancements, e.g. ~/foo, ~~/bar, ~~desktop/baz
+ *********************************************************************/
+
+mp.module_paths = []; // global modules search paths
+if (mp.script_path !== undefined) // loaded as a directory
+ mp.module_paths.push(mp.utils.join_path(mp.script_path, "modules"));
+
+// Internal meta top-dirs. Users should not rely on these names.
+var MODULES_META = "~~modules",
+ SCRIPTDIR_META = "~~scriptdir", // relative script path -> meta absolute id
+ main_script = mp.utils.split_path(mp.script_file); // -> [ path, file ]
+
+function resolve_module_file(id) {
+ var sep = id.indexOf("/"),
+ base = id.substring(0, sep),
+ rest = id.substring(sep + 1) + ".js";
+
+ if (base == SCRIPTDIR_META)
+ return mp.utils.join_path(main_script[0], rest);
+
+ if (base == MODULES_META) {
+ for (var i = 0; i < mp.module_paths.length; i++) {
+ try {
+ var f = mp.utils.join_path(mp.module_paths[i], rest);
+ mp.utils.read_file(f, 1); // throws on any error
+ return f;
+ } catch (e) {}
+ }
+ throw(Error("Cannot find module file '" + rest + "'"));
+ }
+
+ return id + ".js";
+}
+
+// Delimiter '/', remove redundancies, prefix with modules meta-root if needed.
+// E.g. c:\x -> c:/x, or ./x//y/../z -> ./x/z, or utils/x -> ~~modules/utils/x .
+function canonicalize(id) {
+ var path = id.replace(/\\/g,"/").split("/"),
+ t = path[0],
+ base = [];
+
+ // if not strictly relative then must be top-level. figure out base/rest
+ if (t != "." && t != "..") {
+ // global module if it's not fs-root/home/dos-drive/builtin/meta-dir
+ if (!(t == "" || t == "~" || t[1] == ":" || t == "@" || t.match(/^~~/)))
+ path.unshift(MODULES_META); // add an explicit modules meta-root
+
+ if (id.match(/^\\\\/)) // simple UNC handling, preserve leading \\srv
+ path = ["\\\\" + path[2]].concat(path.slice(3)); // [ \\srv, shr..]
+
+ if (t[1] == ":" && t.length > 2) { // path: [ "c:relative", "path" ]
+ path[0] = t.substring(2);
+ path.unshift(t[0] + ":."); // -> [ "c:.", "relative", "path" ]
+ }
+ base = [path.shift()];
+ }
+
+ // path is now logically relative. base, if not empty, is its [meta] root.
+ // normalize the relative part - always id-based (spec Module Id, 1.3.6).
+ var cr = []; // canonicalized relative
+ for (var i = 0; i < path.length; i++) {
+ if (path[i] == "." || path[i] == "")
+ continue;
+ if (path[i] == ".." && cr.length && cr[cr.length - 1] != "..") {
+ cr.pop();
+ continue;
+ }
+ cr.push(path[i]);
+ }
+
+ if (!base.length && cr[0] != "..")
+ base = ["."]; // relative and not ../<stuff> so must start with ./
+ return base.concat(cr).join("/");
+}
+
+function resolve_module_id(base_id, new_id) {
+ new_id = canonicalize(new_id);
+ if (!new_id.match(/^\.\/|^\.\.\//)) // doesn't start with ./ or ../
+ return new_id; // not relative, we don't care about base_id
+
+ var combined = mp.utils.join_path(mp.utils.split_path(base_id)[0], new_id);
+ return canonicalize(combined);
+}
+
+var req_cache = new_cache(); // global for all instances of require
+
+// ret: a require function instance which uses base_id to resolve relative id's
+function new_require(base_id) {
+ return function require(id) {
+ id = resolve_module_id(base_id, id); // id is now top-level
+ if (req_cache[id])
+ return req_cache[id].exports;
+
+ var new_module = {id: id, exports: {}};
+ req_cache[id] = new_module;
+ try {
+ var filename = resolve_module_file(id);
+ // we need dedicated free vars + filename in traces + allow strict
+ var str = "mp._req = function(require, exports, module) {" +
+ mp.utils.read_file(filename) +
+ "\n;}";
+ mp.utils.compile_js(filename, str)(); // only runs the assignment
+ var tmp = mp._req; // we have mp._req, or else we'd have thrown
+ delete mp._req;
+ tmp.call(g, new_require(id), new_module.exports, new_module);
+ } catch (e) {
+ delete req_cache[id];
+ throw(e);
+ }
+
+ return new_module.exports;
+ };
+}
+
+g.require = new_require(SCRIPTDIR_META + "/" + main_script[1]);
+
+/**********************************************************************
+ * mp.options
+ *********************************************************************/
+function read_options(opts, id, on_update, conf_override) {
+ id = String(id ? id : mp.get_script_name());
+ mp.msg.debug("reading options for " + id);
+
+ var conf, fname = "~~/script-opts/" + id + ".conf";
+ try {
+ conf = arguments.length > 3 ? conf_override : mp.utils.read_file(fname);
+ } catch (e) {
+ mp.msg.verbose(fname + " not found.");
+ }
+
+ // data as config file lines array, or empty array
+ var data = conf ? conf.replace(/\r\n/g, "\n").split("\n") : [],
+ conf_len = data.length; // before we append script-opts below
+
+ // Append relevant script-opts as <key-sans-id>=<value> to data
+ var sopts = mp.get_property_native("options/script-opts"),
+ prefix = id + "-";
+ for (var key in sopts) {
+ if (key.indexOf(prefix) == 0)
+ data.push(key.substring(prefix.length) + "=" + sopts[key]);
+ }
+
+ // Update opts from data
+ data.forEach(function(line, i) {
+ if (line[0] == "#" || line.trim() == "")
+ return;
+
+ var key = line.substring(0, line.indexOf("=")),
+ val = line.substring(line.indexOf("=") + 1),
+ type = typeof opts[key],
+ info = i < conf_len ? fname + ":" + (i + 1) // 1-based line number
+ : "script-opts:" + prefix + key;
+
+ if (!opts.hasOwnProperty(key))
+ mp.msg.warn(info, "Ignoring unknown key '" + key + "'");
+ else if (type == "string")
+ opts[key] = val;
+ else if (type == "boolean" && (val == "yes" || val == "no"))
+ opts[key] = (val == "yes");
+ else if (type == "number" && val.trim() != "" && !isNaN(val))
+ opts[key] = Number(val);
+ else
+ mp.msg.error(info, "Error: can't convert '" + val + "' to " + type);
+ });
+
+ if (on_update) {
+ mp.observe_property("options/script-opts", "native", function(_n, _v) {
+ var saved = JSON.parse(JSON.stringify(opts)); // clone
+ var changelist = {}, changed = false;
+ read_options(opts, id, 0, conf); // re-apply orig-file + script-opts
+ for (var key in opts) {
+ if (opts[key] != saved[key]) // type always stays the same
+ changelist[key] = changed = true;
+ }
+ if (changed)
+ on_update(changelist);
+ });
+ }
+}
+
+mp.options = { read_options: read_options };
+
+/**********************************************************************
+ * various
+ *********************************************************************/
+g.print = mp.msg.info; // convenient alias
+mp.get_script_name = function() { return mp.script_name };
+mp.get_script_file = function() { return mp.script_file };
+mp.get_script_directory = function() { return mp.script_path };
+mp.get_time = function() { return mp.get_time_ms() / 1000 };
+mp.utils.getcwd = function() { return mp.get_property("working-directory") };
+mp.utils.getpid = function() { return mp.get_property_number("pid") }
+mp.utils.get_user_path =
+ function(p) { return mp.command_native(["expand-path", String(p)]) };
+mp.get_mouse_pos = function() { return mp.get_property_native("mouse-pos") };
+mp.utils.write_file = mp.utils._write_file.bind(null, false);
+mp.utils.append_file = mp.utils._write_file.bind(null, true);
+mp.dispatch_event = dispatch_event;
+mp.process_timers = process_timers;
+mp.notify_idle_observers = notify_idle_observers;
+mp.peek_timers_wait = peek_timers_wait;
+
+mp.get_opt = function(key, def) {
+ var v = mp.get_property_native("options/script-opts")[key];
+ return (typeof v != "undefined") ? v : def;
+}
+
+mp.osd_message = function osd_message(text, duration) {
+ mp.commandv("show_text", text, Math.round(1000 * (duration || -1)));
+}
+
+mp.utils.subprocess = function subprocess(t) {
+ var cmd = { name: "subprocess", capture_stdout: true };
+ var new_names = { cancellable: "playback_only", max_size: "capture_size" };
+ for (var k in t)
+ cmd[new_names[k] || k] = t[k];
+
+ var rv = mp.command_native(cmd);
+ if (mp.last_error()) /* typically on missing/incorrect args */
+ rv = { error_string: mp.last_error(), status: -1 };
+ if (rv.error_string)
+ rv.error = rv.error_string;
+ return rv;
+}
+
+mp.utils.subprocess_detached = function subprocess_detached(t) {
+ return mp.commandv.apply(null, ["run"].concat(t.args));
+}
+
+
+// ----- dump: like print, but expands objects/arrays recursively -----
+function replacer(k, v) {
+ var t = typeof v;
+ if (t == "function" || t == "undefined")
+ return "<" + t + ">";
+ if (Array.isArray(this) && t == "object" && v !== null) { // "safe" mode
+ if (this.indexOf(v) >= 0)
+ return "<VISITED>";
+ this.push(v);
+ }
+ return v;
+}
+
+function obj2str(v) {
+ try { // can process objects more than once, but throws on cycles
+ return JSON.stringify(v, replacer.bind(null), 2);
+ } catch (e) { // simple safe: exclude visited objects, even if not cyclic
+ return JSON.stringify(v, replacer.bind([]), 2);
+ }
+}
+
+g.dump = function dump() {
+ var toprint = [];
+ for (var i = 0; i < arguments.length; i++) {
+ var v = arguments[i];
+ toprint.push((typeof v == "object") ? obj2str(v) : replacer(0, v));
+ }
+ print.apply(null, toprint);
+}
+
+/**********************************************************************
+ * main listeners and event loop
+ *********************************************************************/
+mp.keep_running = true;
+g.exit = function() { mp.keep_running = false }; // user-facing too
+mp.register_event("shutdown", g.exit);
+mp.register_event("property-change", notify_observer);
+mp.register_event("hook", run_hook);
+mp.register_event("command-reply", async_command_handler);
+mp.register_event("client-message", dispatch_message);
+mp.register_script_message("key-binding", dispatch_key_binding);
+
+g.mp_event_loop = function mp_event_loop() {
+ var wait = 0; // seconds
+ do { // distapch events as long as they arrive, then do the timers/idle
+ var e = mp.wait_event(wait);
+ if (e.event != "none") {
+ dispatch_event(e);
+ wait = 0; // poll the next one
+ } else {
+ wait = process_timers() / 1000;
+ if (wait != 0 && iobservers.length) {
+ notify_idle_observers(); // can add timers -> recalculate wait
+ wait = peek_timers_wait() / 1000;
+ }
+ }
+ } while (mp.keep_running);
+};
+
+
+// let the user extend us, e.g. by adding items to mp.module_paths
+var initjs = mp.find_config_file("init.js"); // ~~/init.js
+if (initjs)
+ require(initjs.slice(0, -3)); // remove ".js"
+else if ((initjs = mp.find_config_file(".init.js")))
+ mp.msg.warn("Use init.js instead of .init.js (ignoring " + initjs + ")");
+
+})(this)
diff --git a/player/javascript/meson.build b/player/javascript/meson.build
new file mode 100644
index 0000000..bfff4b4
--- /dev/null
+++ b/player/javascript/meson.build
@@ -0,0 +1,6 @@
+defaults_js = custom_target('defaults.js',
+ input: join_paths(source_root, 'player', 'javascript', 'defaults.js'),
+ output: 'defaults.js.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+)
+sources += defaults_js
diff --git a/player/loadfile.c b/player/loadfile.c
new file mode 100644
index 0000000..1d25dc3
--- /dev/null
+++ b/player/loadfile.c
@@ -0,0 +1,2066 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <strings.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include <libavutil/avutil.h>
+
+#include "mpv_talloc.h"
+
+#include "misc/thread_pool.h"
+#include "misc/thread_tools.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "client.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/global.h"
+#include "options/path.h"
+#include "options/m_config.h"
+#include "options/parse_configfile.h"
+#include "common/playlist.h"
+#include "options/options.h"
+#include "options/m_property.h"
+#include "common/common.h"
+#include "common/encode.h"
+#include "common/stats.h"
+#include "input/input.h"
+#include "misc/language.h"
+
+#include "audio/out/ao.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/f_lavfi.h"
+#include "filters/filter_internal.h"
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "sub/dec_sub.h"
+#include "external_files.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+#include "libmpv/client.h"
+
+// Called from the demuxer thread if a new packet is available, or other changes.
+static void wakeup_demux(void *pctx)
+{
+ struct MPContext *mpctx = pctx;
+ mp_wakeup_core(mpctx);
+}
+
+// Called by foreign threads when playback should be stopped and such.
+void mp_abort_playback_async(struct MPContext *mpctx)
+{
+ mp_cancel_trigger(mpctx->playback_abort);
+
+ mp_mutex_lock(&mpctx->abort_lock);
+
+ for (int n = 0; n < mpctx->num_abort_list; n++) {
+ struct mp_abort_entry *abort = mpctx->abort_list[n];
+ if (abort->coupled_to_playback)
+ mp_abort_trigger_locked(mpctx, abort);
+ }
+
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+// Add it to the global list, and allocate required data structures.
+void mp_abort_add(struct MPContext *mpctx, struct mp_abort_entry *abort)
+{
+ mp_mutex_lock(&mpctx->abort_lock);
+ assert(!abort->cancel);
+ abort->cancel = mp_cancel_new(NULL);
+ MP_TARRAY_APPEND(NULL, mpctx->abort_list, mpctx->num_abort_list, abort);
+ mp_abort_recheck_locked(mpctx, abort);
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+// Remove Add it to the global list, and free/clear required data structures.
+// Does not deallocate the abort value itself.
+void mp_abort_remove(struct MPContext *mpctx, struct mp_abort_entry *abort)
+{
+ mp_mutex_lock(&mpctx->abort_lock);
+ for (int n = 0; n < mpctx->num_abort_list; n++) {
+ if (mpctx->abort_list[n] == abort) {
+ MP_TARRAY_REMOVE_AT(mpctx->abort_list, mpctx->num_abort_list, n);
+ TA_FREEP(&abort->cancel);
+ abort = NULL; // it's not free'd, just clear for the assert below
+ break;
+ }
+ }
+ assert(!abort); // should have been in the list
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+// Verify whether the abort needs to be signaled after changing certain fields
+// in abort.
+void mp_abort_recheck_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort)
+{
+ if ((abort->coupled_to_playback && mp_cancel_test(mpctx->playback_abort)) ||
+ mpctx->abort_all)
+ {
+ mp_abort_trigger_locked(mpctx, abort);
+ }
+}
+
+void mp_abort_trigger_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort)
+{
+ mp_cancel_trigger(abort->cancel);
+}
+
+static void kill_demuxers_reentrant(struct MPContext *mpctx,
+ struct demuxer **demuxers, int num_demuxers)
+{
+ struct demux_free_async_state **items = NULL;
+ int num_items = 0;
+
+ for (int n = 0; n < num_demuxers; n++) {
+ struct demuxer *d = demuxers[n];
+
+ if (!demux_cancel_test(d)) {
+ // Make sure it is set if it wasn't yet.
+ demux_set_wakeup_cb(d, wakeup_demux, mpctx);
+
+ struct demux_free_async_state *item = demux_free_async(d);
+ if (item) {
+ MP_TARRAY_APPEND(NULL, items, num_items, item);
+ d = NULL;
+ }
+ }
+
+ demux_cancel_and_free(d);
+ }
+
+ if (!num_items)
+ return;
+
+ MP_DBG(mpctx, "Terminating demuxers...\n");
+
+ double end = mp_time_sec() + mpctx->opts->demux_termination_timeout;
+ bool force = false;
+ while (num_items) {
+ double wait = end - mp_time_sec();
+
+ for (int n = 0; n < num_items; n++) {
+ struct demux_free_async_state *item = items[n];
+ if (demux_free_async_finish(item)) {
+ items[n] = items[num_items - 1];
+ num_items -= 1;
+ n--;
+ goto repeat;
+ } else if (wait < 0) {
+ demux_free_async_force(item);
+ if (!force)
+ MP_VERBOSE(mpctx, "Forcefully terminating demuxers...\n");
+ force = true;
+ }
+ }
+
+ if (wait >= 0)
+ mp_set_timeout(mpctx, wait);
+ mp_idle(mpctx);
+ repeat:;
+ }
+
+ talloc_free(items);
+
+ MP_DBG(mpctx, "Done terminating demuxers.\n");
+}
+
+static void uninit_demuxer(struct MPContext *mpctx)
+{
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int r = 0; r < num_ptracks[t]; r++)
+ mpctx->current_track[r][t] = NULL;
+ }
+
+ talloc_free(mpctx->chapters);
+ mpctx->chapters = NULL;
+ mpctx->num_chapters = 0;
+
+ mp_abort_cache_dumping(mpctx);
+
+ struct demuxer **demuxers = NULL;
+ int num_demuxers = 0;
+
+ if (mpctx->demuxer)
+ MP_TARRAY_APPEND(NULL, demuxers, num_demuxers, mpctx->demuxer);
+ mpctx->demuxer = NULL;
+
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+
+ assert(!track->dec && !track->d_sub);
+ assert(!track->vo_c && !track->ao_c);
+ assert(!track->sink);
+
+ // Demuxers can be added in any order (if they appear mid-stream), and
+ // we can't know which tracks uses which, so here's some O(n^2) trash.
+ for (int n = 0; n < num_demuxers; n++) {
+ if (demuxers[n] == track->demuxer) {
+ track->demuxer = NULL;
+ break;
+ }
+ }
+ if (track->demuxer)
+ MP_TARRAY_APPEND(NULL, demuxers, num_demuxers, track->demuxer);
+
+ talloc_free(track);
+ }
+ mpctx->num_tracks = 0;
+
+ kill_demuxers_reentrant(mpctx, demuxers, num_demuxers);
+ talloc_free(demuxers);
+}
+
+#define APPEND(s, ...) mp_snprintf_cat(s, sizeof(s), __VA_ARGS__)
+
+static void print_stream(struct MPContext *mpctx, struct track *t)
+{
+ struct sh_stream *s = t->stream;
+ const char *tname = "?";
+ const char *selopt = "?";
+ const char *langopt = "?";
+ switch (t->type) {
+ case STREAM_VIDEO:
+ tname = "Video"; selopt = "vid"; langopt = NULL;
+ break;
+ case STREAM_AUDIO:
+ tname = "Audio"; selopt = "aid"; langopt = "alang";
+ break;
+ case STREAM_SUB:
+ tname = "Subs"; selopt = "sid"; langopt = "slang";
+ break;
+ }
+ char b[2048] = {0};
+ bool forced_only = false;
+ if (t->type == STREAM_SUB) {
+ bool forced_opt = mpctx->opts->subs_rend->sub_forced_events_only;
+ if (forced_opt)
+ forced_only = t->selected;
+ }
+ APPEND(b, " %3s %-5s", t->selected ? (forced_only ? "(*)" : "(+)") : "", tname);
+ APPEND(b, " --%s=%d", selopt, t->user_tid);
+ if (t->lang && langopt)
+ APPEND(b, " --%s=%s", langopt, t->lang);
+ if (t->default_track)
+ APPEND(b, " (*)");
+ if (t->forced_track)
+ APPEND(b, " (f)");
+ if (t->attached_picture)
+ APPEND(b, " [P]");
+ if (forced_only)
+ APPEND(b, " [F]");
+ if (t->title)
+ APPEND(b, " '%s'", t->title);
+ const char *codec = s ? s->codec->codec : NULL;
+ APPEND(b, " (%s", codec ? codec : "<unknown>");
+ if (t->type == STREAM_VIDEO) {
+ if (s && s->codec->disp_w)
+ APPEND(b, " %dx%d", s->codec->disp_w, s->codec->disp_h);
+ if (s && s->codec->fps)
+ APPEND(b, " %.3ffps", s->codec->fps);
+ } else if (t->type == STREAM_AUDIO) {
+ if (s && s->codec->channels.num)
+ APPEND(b, " %dch", s->codec->channels.num);
+ if (s && s->codec->samplerate)
+ APPEND(b, " %dHz", s->codec->samplerate);
+ }
+ APPEND(b, ")");
+ if (s && s->hls_bitrate > 0)
+ APPEND(b, " (%d kbps)", (s->hls_bitrate + 500) / 1000);
+ if (t->is_external)
+ APPEND(b, " (external)");
+ MP_INFO(mpctx, "%s\n", b);
+}
+
+void print_track_list(struct MPContext *mpctx, const char *msg)
+{
+ if (msg)
+ MP_INFO(mpctx, "%s\n", msg);
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ if (mpctx->tracks[n]->type == t)
+ print_stream(mpctx, mpctx->tracks[n]);
+ }
+}
+
+void update_demuxer_properties(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return;
+ demux_update(demuxer, get_current_time(mpctx));
+ int events = demuxer->events;
+ if ((events & DEMUX_EVENT_INIT) && demuxer->num_editions > 1) {
+ for (int n = 0; n < demuxer->num_editions; n++) {
+ struct demux_edition *edition = &demuxer->editions[n];
+ char b[128] = {0};
+ APPEND(b, " %3s --edition=%d",
+ n == demuxer->edition ? "(+)" : "", n);
+ char *name = mp_tags_get_str(edition->metadata, "title");
+ if (name)
+ APPEND(b, " '%s'", name);
+ if (edition->default_edition)
+ APPEND(b, " (*)");
+ MP_INFO(mpctx, "%s\n", b);
+ }
+ }
+ struct demuxer *tracks = mpctx->demuxer;
+ if (tracks->events & DEMUX_EVENT_STREAMS) {
+ add_demuxer_tracks(mpctx, tracks);
+ print_track_list(mpctx, NULL);
+ tracks->events &= ~DEMUX_EVENT_STREAMS;
+ }
+ if (events & DEMUX_EVENT_METADATA) {
+ struct mp_tags *info =
+ mp_tags_filtered(mpctx, demuxer->metadata, mpctx->opts->display_tags);
+ // prev is used to attempt to print changed tags only (to some degree)
+ struct mp_tags *prev = mpctx->filtered_tags;
+ int n_prev = 0;
+ bool had_output = false;
+ for (int n = 0; n < info->num_keys; n++) {
+ if (prev && n_prev < prev->num_keys) {
+ if (strcmp(prev->keys[n_prev], info->keys[n]) == 0) {
+ n_prev++;
+ if (strcmp(prev->values[n_prev - 1], info->values[n]) == 0)
+ continue;
+ }
+ }
+ struct mp_log *log = mp_log_new(NULL, mpctx->log, "!display-tags");
+ if (!had_output)
+ mp_info(log, "File tags:\n");
+ mp_info(log, " %s: %s\n", info->keys[n], info->values[n]);
+ had_output = true;
+ talloc_free(log);
+ }
+ talloc_free(mpctx->filtered_tags);
+ mpctx->filtered_tags = info;
+ mp_notify(mpctx, MP_EVENT_METADATA_UPDATE, NULL);
+ }
+ if (events & DEMUX_EVENT_DURATION)
+ mp_notify(mpctx, MP_EVENT_DURATION_UPDATE, NULL);
+ demuxer->events = 0;
+}
+
+// Enables or disables the stream for the given track, according to
+// track->selected.
+// With refresh_only=true, refreshes the stream if it's enabled.
+void reselect_demux_stream(struct MPContext *mpctx, struct track *track,
+ bool refresh_only)
+{
+ if (!track->stream)
+ return;
+ double pts = get_current_time(mpctx);
+ if (pts != MP_NOPTS_VALUE) {
+ pts += get_track_seek_offset(mpctx, track);
+ if (track->type == STREAM_SUB)
+ pts -= 10.0;
+ }
+ if (refresh_only)
+ demuxer_refresh_track(track->demuxer, track->stream, pts);
+ else
+ demuxer_select_track(track->demuxer, track->stream, pts, track->selected);
+}
+
+static void enable_demux_thread(struct MPContext *mpctx, struct demuxer *demux)
+{
+ if (mpctx->opts->demuxer_thread && !demux->fully_read) {
+ demux_set_wakeup_cb(demux, wakeup_demux, mpctx);
+ demux_start_thread(demux);
+ }
+}
+
+static int find_new_tid(struct MPContext *mpctx, enum stream_type t)
+{
+ int new_id = 0;
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+ if (track->type == t)
+ new_id = MPMAX(new_id, track->user_tid);
+ }
+ return new_id + 1;
+}
+
+static struct track *add_stream_track(struct MPContext *mpctx,
+ struct demuxer *demuxer,
+ struct sh_stream *stream)
+{
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+ if (track->stream == stream)
+ return track;
+ }
+
+ struct track *track = talloc_ptrtype(NULL, track);
+ *track = (struct track) {
+ .type = stream->type,
+ .user_tid = find_new_tid(mpctx, stream->type),
+ .demuxer_id = stream->demuxer_id,
+ .ff_index = stream->ff_index,
+ .hls_bitrate = stream->hls_bitrate,
+ .program_id = stream->program_id,
+ .title = stream->title,
+ .default_track = stream->default_track,
+ .forced_track = stream->forced_track,
+ .dependent_track = stream->dependent_track,
+ .visual_impaired_track = stream->visual_impaired_track,
+ .hearing_impaired_track = stream->hearing_impaired_track,
+ .image = stream->image,
+ .attached_picture = stream->attached_picture != NULL,
+ .lang = stream->lang,
+ .demuxer = demuxer,
+ .stream = stream,
+ };
+ MP_TARRAY_APPEND(mpctx, mpctx->tracks, mpctx->num_tracks, track);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return track;
+}
+
+void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer)
+{
+ for (int n = 0; n < demux_get_num_stream(demuxer); n++)
+ add_stream_track(mpctx, demuxer, demux_get_stream(demuxer, n));
+}
+
+// Result numerically higher => better match. 0 == no match.
+static int match_lang(char **langs, const char *lang)
+{
+ if (!lang)
+ return 0;
+ for (int idx = 0; langs && langs[idx]; idx++) {
+ int score = mp_match_lang_single(langs[idx], lang);
+ if (score > 0)
+ return INT_MAX - (idx + 1) * LANGUAGE_SCORE_MAX + score - 1;
+ }
+ return 0;
+}
+
+/* Get the track wanted by the user.
+ * tid is the track ID requested by the user (-2: deselect, -1: default)
+ * lang is a string list, NULL is same as empty list
+ * Sort tracks based on the following criteria, and pick the first:
+ *0a) track matches tid (always wins)
+ * 0b) track is not from --external-file
+ * 1) track is external (no_default cancels this)
+ * 1b) track was passed explicitly (is not an auto-loaded subtitle)
+ * 1c) track matches the program ID of the video
+ * 2) earlier match in lang list but not if we're using os_langs
+ * 3a) track is marked forced and we're preferring forced tracks
+ * 3b) track is marked non-forced and we're preferring non-forced tracks
+ * 3c) track is marked default
+ * 3d) match in lang list with os_langs
+ * 4) attached picture, HLS bitrate
+ * 5) lower track number
+ * If select_fallback is not set, 5) is only used to determine whether a
+ * matching track is preferred over another track. Otherwise, always pick a
+ * track (if nothing else matches, return the track with lowest ID).
+ * Forced tracks are preferred when the user prefers not to display subtitles
+ */
+// Return whether t1 is preferred over t2
+static bool compare_track(struct track *t1, struct track *t2, char **langs,
+ bool os_langs, struct MPOpts *opts, int preferred_program)
+{
+ if (!opts->autoload_files && t1->is_external != t2->is_external)
+ return !t1->is_external;
+ bool ext1 = t1->is_external && !t1->no_default;
+ bool ext2 = t2->is_external && !t2->no_default;
+ if (ext1 != ext2) {
+ if (t1->attached_picture && t2->attached_picture
+ && opts->audio_display == 1)
+ return !ext1;
+ return ext1;
+ }
+ if (t1->auto_loaded != t2->auto_loaded)
+ return !t1->auto_loaded;
+ if (preferred_program != -1 && t1->program_id != -1 && t2->program_id != -1) {
+ if ((t1->program_id == preferred_program) !=
+ (t2->program_id == preferred_program))
+ return t1->program_id == preferred_program;
+ }
+ int forced = t1->type == STREAM_SUB ? opts->subs_fallback_forced : 1;
+ bool force_match = forced == 1 || (t1->forced_track && forced == 2) ||
+ (!t1->forced_track && !forced);
+ int l1 = match_lang(langs, t1->lang), l2 = match_lang(langs, t2->lang);
+ if (!os_langs && l1 != l2)
+ return l1 > l2 && force_match;
+ if (t1->default_track != t2->default_track)
+ return t1->default_track && force_match;
+ if (os_langs && l1 != l2)
+ return l1 > l2 && force_match;
+ if (t1->attached_picture != t2->attached_picture)
+ return !t1->attached_picture;
+ if (t1->stream && t2->stream && opts->hls_bitrate >= 0 &&
+ t1->stream->hls_bitrate != t2->stream->hls_bitrate)
+ {
+ bool t1_ok = t1->stream->hls_bitrate <= opts->hls_bitrate;
+ bool t2_ok = t2->stream->hls_bitrate <= opts->hls_bitrate;
+ if (t1_ok != t2_ok)
+ return t1_ok;
+ if (t1_ok && t2_ok)
+ return t1->stream->hls_bitrate > t2->stream->hls_bitrate;
+ return t1->stream->hls_bitrate < t2->stream->hls_bitrate;
+ }
+ return t1->user_tid <= t2->user_tid;
+}
+
+static bool duplicate_track(struct MPContext *mpctx, int order,
+ enum stream_type type, struct track *track)
+{
+ for (int i = 0; i < order; i++) {
+ if (mpctx->current_track[i][type] == track)
+ return true;
+ }
+ return false;
+}
+
+static bool append_lang(size_t *nb, char ***out, char *in)
+{
+ if (!in)
+ return false;
+ MP_TARRAY_GROW(NULL, *out, *nb + 1);
+ (*out)[(*nb)++] = in;
+ (*out)[*nb] = NULL;
+ ta_set_parent(in, *out);
+ return true;
+}
+
+static char **add_os_langs(void)
+{
+ size_t nb = 0;
+ char **out = NULL;
+ char **autos = mp_get_user_langs();
+ for (int i = 0; autos && autos[i]; i++) {
+ if (!append_lang(&nb, &out, autos[i]))
+ goto cleanup;
+ }
+
+cleanup:
+ talloc_free(autos);
+ return out;
+}
+
+static char **process_langs(char **in)
+{
+ size_t nb = 0;
+ char **out = NULL;
+ for (int i = 0; in && in[i]; i++) {
+ if (!append_lang(&nb, &out, talloc_strdup(NULL, in[i])))
+ break;
+ }
+ return out;
+}
+
+static const char *get_audio_lang(struct MPContext *mpctx)
+{
+ // If we have a single current audio track, this is simple.
+ if (mpctx->current_track[0][STREAM_AUDIO])
+ return mpctx->current_track[0][STREAM_AUDIO]->lang;
+
+ const char *ret = NULL;
+
+ // Otherwise, we may be using a filter with multiple inputs.
+ // Iterate over the tracks and find the ones in use.
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ const struct track *t = mpctx->tracks[i];
+ if (t->type != STREAM_AUDIO || !t->selected)
+ continue;
+
+ // If we have input in multiple audio languages, bail out;
+ // we don't have a meaningful single language.
+ // Partial matches (e.g. en-US vs en-GB) are acceptable here.
+ if (ret && t->lang && !mp_match_lang_single(t->lang, ret))
+ return NULL;
+
+ // We'll return the first non-null tag we see
+ if (!ret)
+ ret = t->lang;
+ }
+
+ return ret;
+}
+
+struct track *select_default_track(struct MPContext *mpctx, int order,
+ enum stream_type type)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int tid = opts->stream_id[order][type];
+ int preferred_program = (type != STREAM_VIDEO && mpctx->current_track[0][STREAM_VIDEO]) ?
+ mpctx->current_track[0][STREAM_VIDEO]->program_id : -1;
+ if (tid == -2)
+ return NULL;
+ char **langs = process_langs(opts->stream_lang[type]);
+ bool os_langs = false;
+ // Try to add OS languages if enabled by the user and we don't already have a lang from slang.
+ if (type == STREAM_SUB && (!langs || !strcmp(langs[0], "")) && opts->subs_match_os_language) {
+ talloc_free(langs);
+ langs = add_os_langs();
+ os_langs = true;
+ }
+ const char *audio_lang = get_audio_lang(mpctx);
+ bool sub = type == STREAM_SUB;
+ bool fallback_forced = sub && opts->subs_fallback_forced;
+ bool audio_matches = false;
+ bool sub_fallback = false;
+ struct track *pick = NULL;
+ struct track *forced_pick = NULL;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type != type)
+ continue;
+ if (track->user_tid == tid) {
+ pick = track;
+ goto cleanup;
+ }
+ if (tid >= 0)
+ continue;
+ if (track->no_auto_select)
+ continue;
+ if (duplicate_track(mpctx, order, type, track))
+ continue;
+ if (!pick || compare_track(track, pick, langs, os_langs, mpctx->opts, preferred_program))
+ pick = track;
+
+ // Autoselecting forced sub tracks requires the following:
+ // 1. Matches the audio language or --subs-fallback-forced=always.
+ // 2. Matches the users list of preferred languages or none were specified (i.e. slang was not set).
+ // 3. A track *wasn't* already selected by slang previously or the track->lang matches pick->lang and isn't forced.
+ bool valid_forced_slang = (os_langs || (mp_match_lang_single(pick->lang, track->lang) && !pick->forced_track) ||
+ (match_lang(langs, track->lang) && !match_lang(langs, pick->lang)));
+ bool audio_lang_match = mp_match_lang_single(audio_lang, track->lang);
+ if (fallback_forced && track->forced_track && valid_forced_slang && audio_lang_match &&
+ (!forced_pick || compare_track(track, forced_pick, langs, os_langs, mpctx->opts, preferred_program)))
+ {
+ forced_pick = track;
+ }
+ }
+
+ // If we found a forced track, use that.
+ if (forced_pick)
+ pick = forced_pick;
+
+ // Clear out any picks for these special cases for subtitles
+ if (pick) {
+ audio_matches = mp_match_lang_single(pick->lang, audio_lang);
+ sub_fallback = (pick->is_external && !pick->no_default) || opts->subs_fallback == 2 ||
+ (opts->subs_fallback == 1 && pick->default_track);
+ }
+ if (pick && !forced_pick && sub && (!match_lang(langs, pick->lang) || os_langs) && !sub_fallback)
+ pick = NULL;
+ // Handle this after matching langs and selecting a fallback.
+ if (pick && sub && (!opts->subs_with_matching_audio && audio_matches))
+ pick = NULL;
+ // Handle edge cases if we picked a track that doesn't match the --subs-fallback-force value
+ if (pick && sub && ((!pick->forced_track && opts->subs_fallback_forced == 2) ||
+ (pick->forced_track && !opts->subs_fallback_forced)))
+ {
+ pick = NULL;
+ }
+
+ if (pick && pick->attached_picture && !mpctx->opts->audio_display)
+ pick = NULL;
+ if (pick && !opts->autoload_files && pick->is_external)
+ pick = NULL;
+cleanup:
+ talloc_free(langs);
+ return pick;
+}
+
+static char *track_layout_hash(struct MPContext *mpctx)
+{
+ char *h = talloc_strdup(NULL, "");
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type != type)
+ continue;
+ h = talloc_asprintf_append_buffer(h, "%d-%d-%d-%d-%s\n", type,
+ track->user_tid, track->default_track, track->is_external,
+ track->lang ? track->lang : "");
+ }
+ }
+ return h;
+}
+
+// Normally, video/audio/sub track selection is persistent across files. This
+// code resets track selection if the new file has a different track layout.
+static void check_previous_track_selection(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->track_layout_hash)
+ return;
+
+ char *h = track_layout_hash(mpctx);
+ if (strcmp(h, mpctx->track_layout_hash) != 0) {
+ // Reset selection, but only if they're not "auto" or "off". The
+ // defaults are -1 (default selection), or -2 (off) for secondary tracks.
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int i = 0; i < num_ptracks[t]; i++) {
+ if (opts->stream_id[i][t] >= 0)
+ mark_track_selection(mpctx, i, t, i == 0 ? -1 : -2);
+ }
+ }
+ talloc_free(mpctx->track_layout_hash);
+ mpctx->track_layout_hash = NULL;
+ }
+ talloc_free(h);
+}
+
+// Update the matching track selection user option to the given value.
+void mark_track_selection(struct MPContext *mpctx, int order,
+ enum stream_type type, int value)
+{
+ assert(order >= 0 && order < num_ptracks[type]);
+ mpctx->opts->stream_id[order][type] = value;
+ m_config_notify_change_opt_ptr(mpctx->mconfig,
+ &mpctx->opts->stream_id[order][type]);
+}
+
+void mp_switch_track_n(struct MPContext *mpctx, int order, enum stream_type type,
+ struct track *track, int flags)
+{
+ assert(!track || track->type == type);
+ assert(type >= 0 && type < STREAM_TYPE_COUNT);
+ assert(order >= 0 && order < num_ptracks[type]);
+
+ // Mark the current track selection as explicitly user-requested. (This is
+ // different from auto-selection or disabling a track due to errors.)
+ if (flags & FLAG_MARK_SELECTION)
+ mark_track_selection(mpctx, order, type, track ? track->user_tid : -2);
+
+ // No decoder should be initialized yet.
+ if (!mpctx->demuxer)
+ return;
+
+ struct track *current = mpctx->current_track[order][type];
+ if (track == current)
+ return;
+
+ if (current && current->sink) {
+ MP_ERR(mpctx, "Can't disable input to complex filter.\n");
+ goto error;
+ }
+ if ((type == STREAM_VIDEO && mpctx->vo_chain && !mpctx->vo_chain->track) ||
+ (type == STREAM_AUDIO && mpctx->ao_chain && !mpctx->ao_chain->track))
+ {
+ MP_ERR(mpctx, "Can't switch away from complex filter output.\n");
+ goto error;
+ }
+
+ if (track && track->selected) {
+ // Track has been selected in a different order parameter.
+ MP_ERR(mpctx, "Track %d is already selected.\n", track->user_tid);
+ goto error;
+ }
+
+ if (order == 0) {
+ if (type == STREAM_VIDEO) {
+ uninit_video_chain(mpctx);
+ if (!track)
+ handle_force_window(mpctx, true);
+ } else if (type == STREAM_AUDIO) {
+ clear_audio_output_buffers(mpctx);
+ uninit_audio_chain(mpctx);
+ if (!track)
+ uninit_audio_out(mpctx);
+ }
+ }
+ if (type == STREAM_SUB)
+ uninit_sub(mpctx, current);
+
+ if (current) {
+ current->selected = false;
+ reselect_demux_stream(mpctx, current, false);
+ }
+
+ mpctx->current_track[order][type] = track;
+
+ if (track) {
+ track->selected = true;
+ reselect_demux_stream(mpctx, track, false);
+ }
+
+ if (type == STREAM_VIDEO && order == 0) {
+ reinit_video_chain(mpctx);
+ } else if (type == STREAM_AUDIO && order == 0) {
+ reinit_audio_chain(mpctx);
+ } else if (type == STREAM_SUB && order >= 0 && order <= 2) {
+ reinit_sub(mpctx, track);
+ }
+
+ mp_notify(mpctx, MP_EVENT_TRACK_SWITCHED, NULL);
+ mp_wakeup_core(mpctx);
+
+ talloc_free(mpctx->track_layout_hash);
+ mpctx->track_layout_hash = talloc_steal(mpctx, track_layout_hash(mpctx));
+
+ return;
+error:
+ mark_track_selection(mpctx, order, type, -1);
+}
+
+void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
+ struct track *track, int flags)
+{
+ mp_switch_track_n(mpctx, 0, type, track, flags);
+}
+
+void mp_deselect_track(struct MPContext *mpctx, struct track *track)
+{
+ if (track && track->selected) {
+ for (int t = 0; t < num_ptracks[track->type]; t++) {
+ if (mpctx->current_track[t][track->type] != track)
+ continue;
+ mp_switch_track_n(mpctx, t, track->type, NULL, 0);
+ mark_track_selection(mpctx, t, track->type, -1); // default
+ }
+ }
+}
+
+struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
+ int tid)
+{
+ if (tid == -1)
+ return mpctx->current_track[0][type];
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type == type && track->user_tid == tid)
+ return track;
+ }
+ return NULL;
+}
+
+bool mp_remove_track(struct MPContext *mpctx, struct track *track)
+{
+ if (!track->is_external)
+ return false;
+
+ mp_deselect_track(mpctx, track);
+ if (track->selected)
+ return false;
+
+ struct demuxer *d = track->demuxer;
+
+ int index = 0;
+ while (index < mpctx->num_tracks && mpctx->tracks[index] != track)
+ index++;
+ MP_TARRAY_REMOVE_AT(mpctx->tracks, mpctx->num_tracks, index);
+ talloc_free(track);
+
+ // Close the demuxer, unless there is still a track using it. These are
+ // all external tracks.
+ bool in_use = false;
+ for (int n = mpctx->num_tracks - 1; n >= 0 && !in_use; n--)
+ in_use |= mpctx->tracks[n]->demuxer == d;
+
+ if (!in_use)
+ demux_cancel_and_free(d);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return true;
+}
+
+// Add the given file as additional track. The filter argument controls how or
+// if tracks are auto-selected at any point.
+// To be run on a worker thread, locked (temporarily unlocks core).
+// cancel will generally be used to abort the loading process, but on success
+// the demuxer is changed to be slaved to mpctx->playback_abort instead.
+int mp_add_external_file(struct MPContext *mpctx, char *filename,
+ enum stream_type filter, struct mp_cancel *cancel,
+ bool cover_art)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (!filename || mp_cancel_test(cancel))
+ return -1;
+
+ char *disp_filename = filename;
+ if (strncmp(disp_filename, "memory://", 9) == 0)
+ disp_filename = "memory://"; // avoid noise
+
+ struct demuxer_params params = {
+ .is_top_level = true,
+ .stream_flags = STREAM_ORIGIN_DIRECT,
+ };
+
+ switch (filter) {
+ case STREAM_SUB:
+ params.force_format = opts->sub_demuxer_name;
+ break;
+ case STREAM_AUDIO:
+ params.force_format = opts->audio_demuxer_name;
+ break;
+ }
+
+ mp_core_unlock(mpctx);
+
+ struct demuxer *demuxer =
+ demux_open_url(filename, &params, cancel, mpctx->global);
+ if (demuxer)
+ enable_demux_thread(mpctx, demuxer);
+
+ mp_core_lock(mpctx);
+
+ // The command could have overlapped with playback exiting. (We don't care
+ // if playback has started again meanwhile - weird, but not a problem.)
+ if (mpctx->stop_play)
+ goto err_out;
+
+ if (!demuxer)
+ goto err_out;
+
+ if (filter != STREAM_SUB && opts->rebase_start_time)
+ demux_set_ts_offset(demuxer, -demuxer->start_time);
+
+ bool has_any = false;
+ for (int n = 0; n < demux_get_num_stream(demuxer); n++) {
+ struct sh_stream *sh = demux_get_stream(demuxer, n);
+ if (sh->type == filter || filter == STREAM_TYPE_COUNT) {
+ has_any = true;
+ break;
+ }
+ }
+
+ if (!has_any) {
+ char *tname = mp_tprintf(20, "%s ", stream_type_name(filter));
+ if (filter == STREAM_TYPE_COUNT)
+ tname = "";
+ MP_ERR(mpctx, "No %sstreams in file %s.\n", tname, disp_filename);
+ goto err_out;
+ }
+
+ int first_num = -1;
+ for (int n = 0; n < demux_get_num_stream(demuxer); n++) {
+ struct sh_stream *sh = demux_get_stream(demuxer, n);
+ struct track *t = add_stream_track(mpctx, demuxer, sh);
+ t->is_external = true;
+ if (sh->title && sh->title[0]) {
+ t->title = talloc_strdup(t, sh->title);
+ } else {
+ t->title = talloc_strdup(t, mp_basename(disp_filename));
+ }
+ t->external_filename = talloc_strdup(t, filename);
+ t->no_default = sh->type != filter;
+ t->no_auto_select = t->no_default;
+ // if we found video, and we are loading cover art, flag as such.
+ t->attached_picture = t->type == STREAM_VIDEO && cover_art;
+ if (first_num < 0 && (filter == STREAM_TYPE_COUNT || sh->type == filter))
+ first_num = mpctx->num_tracks - 1;
+ }
+
+ mp_cancel_set_parent(demuxer->cancel, mpctx->playback_abort);
+
+ return first_num;
+
+err_out:
+ demux_cancel_and_free(demuxer);
+ if (!mp_cancel_test(cancel))
+ MP_ERR(mpctx, "Can not open external file %s.\n", disp_filename);
+ return -1;
+}
+
+// to be run on a worker thread, locked (temporarily unlocks core)
+static void open_external_files(struct MPContext *mpctx, char **files,
+ enum stream_type filter)
+{
+ // Need a copy, because the option value could be mutated during iteration.
+ void *tmp = talloc_new(NULL);
+ files = mp_dup_str_array(tmp, files);
+
+ for (int n = 0; files && files[n]; n++)
+ // when given filter is set to video, we are loading up cover art
+ mp_add_external_file(mpctx, files[n], filter, mpctx->playback_abort,
+ filter == STREAM_VIDEO);
+
+ talloc_free(tmp);
+}
+
+// See mp_add_external_file() for meaning of cancel parameter.
+void autoload_external_files(struct MPContext *mpctx, struct mp_cancel *cancel)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (opts->sub_auto < 0 && opts->audiofile_auto < 0 && opts->coverart_auto < 0)
+ return;
+ if (!opts->autoload_files || strcmp(mpctx->filename, "-") == 0)
+ return;
+
+ void *tmp = talloc_new(NULL);
+ struct subfn *list = find_external_files(mpctx->global, mpctx->filename, opts);
+ talloc_steal(tmp, list);
+
+ int sc[STREAM_TYPE_COUNT] = {0};
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ if (!mpctx->tracks[n]->attached_picture)
+ sc[mpctx->tracks[n]->type]++;
+ }
+
+ for (int i = 0; list && list[i].fname; i++) {
+ struct subfn *e = &list[i];
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ if (t->demuxer && strcmp(t->demuxer->filename, e->fname) == 0)
+ goto skip;
+ }
+ if (e->type == STREAM_SUB && !sc[STREAM_VIDEO] && !sc[STREAM_AUDIO])
+ goto skip;
+ if (e->type == STREAM_AUDIO && !sc[STREAM_VIDEO])
+ goto skip;
+ if (e->type == STREAM_VIDEO && (sc[STREAM_VIDEO] || !sc[STREAM_AUDIO]))
+ goto skip;
+
+ // when given filter is set to video, we are loading up cover art
+ int first = mp_add_external_file(mpctx, e->fname, e->type, cancel,
+ e->type == STREAM_VIDEO);
+ if (first < 0)
+ goto skip;
+
+ for (int n = first; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ t->auto_loaded = true;
+ if (!t->lang)
+ t->lang = talloc_strdup(t, e->lang);
+ }
+ skip:;
+ }
+
+ talloc_free(tmp);
+}
+
+// Do stuff to a newly loaded playlist. This includes any processing that may
+// be required after loading a playlist.
+void prepare_playlist(struct MPContext *mpctx, struct playlist *pl)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ pl->current = NULL;
+
+ if (opts->playlist_pos >= 0)
+ pl->current = playlist_entry_from_index(pl, opts->playlist_pos);
+
+ if (opts->shuffle)
+ playlist_shuffle(pl);
+
+ if (opts->merge_files)
+ merge_playlist_files(pl);
+
+ if (!pl->current)
+ pl->current = mp_check_playlist_resume(mpctx, pl);
+
+ if (!pl->current)
+ pl->current = playlist_get_first(pl);
+}
+
+// Replace the current playlist entry with playlist contents. Moves the entries
+// from the given playlist pl, so the entries don't actually need to be copied.
+static void transfer_playlist(struct MPContext *mpctx, struct playlist *pl,
+ int64_t *start_id, int *num_new_entries)
+{
+ if (pl->num_entries) {
+ prepare_playlist(mpctx, pl);
+ struct playlist_entry *new = pl->current;
+ *num_new_entries = pl->num_entries;
+ *start_id = playlist_transfer_entries(mpctx->playlist, pl);
+ // current entry is replaced
+ if (mpctx->playlist->current)
+ playlist_remove(mpctx->playlist, mpctx->playlist->current);
+ if (new)
+ mpctx->playlist->current = new;
+ } else {
+ MP_WARN(mpctx, "Empty playlist!\n");
+ }
+}
+
+static void process_hooks(struct MPContext *mpctx, char *name)
+{
+ mp_hook_start(mpctx, name);
+
+ while (!mp_hook_test_completion(mpctx, name)) {
+ mp_idle(mpctx);
+
+ // We have no idea what blocks a hook, so just do a full abort. This
+ // does nothing for hooks that happen outside of playback.
+ if (mpctx->stop_play)
+ mp_abort_playback_async(mpctx);
+ }
+}
+
+// to be run on a worker thread, locked (temporarily unlocks core)
+static void load_chapters(struct MPContext *mpctx)
+{
+ struct demuxer *src = mpctx->demuxer;
+ bool free_src = false;
+ char *chapter_file = mpctx->opts->chapter_file;
+ if (chapter_file && chapter_file[0]) {
+ chapter_file = talloc_strdup(NULL, chapter_file);
+ mp_core_unlock(mpctx);
+ struct demuxer_params p = {.stream_flags = STREAM_ORIGIN_DIRECT};
+ struct demuxer *demux = demux_open_url(chapter_file, &p,
+ mpctx->playback_abort,
+ mpctx->global);
+ mp_core_lock(mpctx);
+ if (demux) {
+ src = demux;
+ free_src = true;
+ }
+ talloc_free(mpctx->chapters);
+ mpctx->chapters = NULL;
+ talloc_free(chapter_file);
+ }
+ if (src && !mpctx->chapters) {
+ talloc_free(mpctx->chapters);
+ mpctx->num_chapters = src->num_chapters;
+ mpctx->chapters = demux_copy_chapter_data(src->chapters, src->num_chapters);
+ if (mpctx->opts->rebase_start_time) {
+ for (int n = 0; n < mpctx->num_chapters; n++)
+ mpctx->chapters[n].pts -= src->start_time;
+ }
+ }
+ if (free_src)
+ demux_cancel_and_free(src);
+}
+
+static void load_per_file_options(m_config_t *conf,
+ struct playlist_param *params,
+ int params_count)
+{
+ for (int n = 0; n < params_count; n++) {
+ m_config_set_option_cli(conf, params[n].name, params[n].value,
+ M_SETOPT_BACKUP);
+ }
+}
+
+static MP_THREAD_VOID open_demux_thread(void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+
+ mp_thread_set_name("opener");
+
+ struct demuxer_params p = {
+ .force_format = mpctx->open_format,
+ .stream_flags = mpctx->open_url_flags,
+ .stream_record = true,
+ .is_top_level = true,
+ };
+ struct demuxer *demux =
+ demux_open_url(mpctx->open_url, &p, mpctx->open_cancel, mpctx->global);
+ mpctx->open_res_demuxer = demux;
+
+ if (demux) {
+ MP_VERBOSE(mpctx, "Opening done: %s\n", mpctx->open_url);
+
+ if (mpctx->open_for_prefetch && !demux->fully_read) {
+ int num_streams = demux_get_num_stream(demux);
+ for (int n = 0; n < num_streams; n++) {
+ struct sh_stream *sh = demux_get_stream(demux, n);
+ demuxer_select_track(demux, sh, MP_NOPTS_VALUE, true);
+ }
+
+ demux_set_wakeup_cb(demux, wakeup_demux, mpctx);
+ demux_start_thread(demux);
+ demux_start_prefetch(demux);
+ }
+ } else {
+ MP_VERBOSE(mpctx, "Opening failed or was aborted: %s\n", mpctx->open_url);
+
+ if (p.demuxer_failed) {
+ mpctx->open_res_error = MPV_ERROR_UNKNOWN_FORMAT;
+ } else {
+ mpctx->open_res_error = MPV_ERROR_LOADING_FAILED;
+ }
+ }
+
+ atomic_store(&mpctx->open_done, true);
+ mp_wakeup_core(mpctx);
+ MP_THREAD_RETURN();
+}
+
+static void cancel_open(struct MPContext *mpctx)
+{
+ if (mpctx->open_cancel)
+ mp_cancel_trigger(mpctx->open_cancel);
+
+ if (mpctx->open_active)
+ mp_thread_join(mpctx->open_thread);
+ mpctx->open_active = false;
+
+ if (mpctx->open_res_demuxer)
+ demux_cancel_and_free(mpctx->open_res_demuxer);
+ mpctx->open_res_demuxer = NULL;
+
+ TA_FREEP(&mpctx->open_cancel);
+ TA_FREEP(&mpctx->open_url);
+ TA_FREEP(&mpctx->open_format);
+
+ atomic_store(&mpctx->open_done, false);
+}
+
+// Setup all the field to open this url, and make sure a thread is running.
+static void start_open(struct MPContext *mpctx, char *url, int url_flags,
+ bool for_prefetch)
+{
+ cancel_open(mpctx);
+
+ assert(!mpctx->open_active);
+ assert(!mpctx->open_cancel);
+ assert(!mpctx->open_res_demuxer);
+ assert(!atomic_load(&mpctx->open_done));
+
+ mpctx->open_cancel = mp_cancel_new(NULL);
+ mpctx->open_url = talloc_strdup(NULL, url);
+ mpctx->open_format = talloc_strdup(NULL, mpctx->opts->demuxer_name);
+ mpctx->open_url_flags = url_flags;
+ mpctx->open_for_prefetch = for_prefetch && mpctx->opts->demuxer_thread;
+
+ if (mp_thread_create(&mpctx->open_thread, open_demux_thread, mpctx)) {
+ cancel_open(mpctx);
+ return;
+ }
+
+ mpctx->open_active = true;
+}
+
+static void open_demux_reentrant(struct MPContext *mpctx)
+{
+ char *url = mpctx->stream_open_filename;
+
+ if (mpctx->open_active) {
+ bool done = atomic_load(&mpctx->open_done);
+ bool failed = done && !mpctx->open_res_demuxer;
+ bool correct_url = strcmp(mpctx->open_url, url) == 0;
+
+ if (correct_url && !failed) {
+ MP_VERBOSE(mpctx, "Using prefetched/prefetching URL.\n");
+ } else if (correct_url && failed) {
+ MP_VERBOSE(mpctx, "Prefetched URL failed, retrying.\n");
+ cancel_open(mpctx);
+ } else {
+ if (done) {
+ MP_VERBOSE(mpctx, "Dropping finished prefetch of wrong URL.\n");
+ } else {
+ MP_VERBOSE(mpctx, "Aborting ongoing prefetch of wrong URL.\n");
+ }
+ cancel_open(mpctx);
+ }
+ }
+
+ if (!mpctx->open_active)
+ start_open(mpctx, url, mpctx->playing->stream_flags, false);
+
+ // User abort should cancel the opener now.
+ mp_cancel_set_parent(mpctx->open_cancel, mpctx->playback_abort);
+
+ while (!atomic_load(&mpctx->open_done)) {
+ mp_idle(mpctx);
+
+ if (mpctx->stop_play)
+ mp_abort_playback_async(mpctx);
+ }
+
+ if (mpctx->open_res_demuxer) {
+ mpctx->demuxer = mpctx->open_res_demuxer;
+ mpctx->open_res_demuxer = NULL;
+ mp_cancel_set_parent(mpctx->demuxer->cancel, mpctx->playback_abort);
+ } else {
+ mpctx->error_playing = mpctx->open_res_error;
+ }
+
+ cancel_open(mpctx); // cleanup
+}
+
+void prefetch_next(struct MPContext *mpctx)
+{
+ if (!mpctx->opts->prefetch_open)
+ return;
+
+ struct playlist_entry *new_entry = mp_next_file(mpctx, +1, false);
+ if (new_entry && !mpctx->open_active && new_entry->filename) {
+ MP_VERBOSE(mpctx, "Prefetching: %s\n", new_entry->filename);
+ start_open(mpctx, new_entry->filename, new_entry->stream_flags, true);
+ }
+}
+
+static void clear_playlist_paths(struct MPContext *mpctx)
+{
+ TA_FREEP(&mpctx->playlist_paths);
+ mpctx->playlist_paths_len = 0;
+}
+
+static bool infinite_playlist_loading_loop(struct MPContext *mpctx, struct playlist *pl)
+{
+ if (pl->num_entries) {
+ struct playlist_entry *e = pl->entries[0];
+ for (int n = 0; n < mpctx->playlist_paths_len; n++) {
+ if (strcmp(mpctx->playlist_paths[n], e->filename) == 0) {
+ clear_playlist_paths(mpctx);
+ return true;
+ }
+ }
+ }
+ MP_TARRAY_APPEND(mpctx, mpctx->playlist_paths, mpctx->playlist_paths_len,
+ talloc_strdup(mpctx->playlist_paths, mpctx->filename));
+ return false;
+}
+
+// Destroy the complex filter, and remove the references to the filter pads.
+// (Call cleanup_deassociated_complex_filters() to close decoders/VO/AO
+// that are not connected anymore due to this.)
+static void deassociate_complex_filters(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ mpctx->tracks[n]->sink = NULL;
+ if (mpctx->vo_chain)
+ mpctx->vo_chain->filter_src = NULL;
+ if (mpctx->ao_chain)
+ mpctx->ao_chain->filter_src = NULL;
+ TA_FREEP(&mpctx->lavfi);
+ TA_FREEP(&mpctx->lavfi_graph);
+}
+
+// Close all decoders and sinks (AO/VO) that are not connected to either
+// a track or a filter pad.
+static void cleanup_deassociated_complex_filters(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (!(track->sink || track->vo_c || track->ao_c)) {
+ if (track->dec && !track->vo_c && !track->ao_c) {
+ talloc_free(track->dec->f);
+ track->dec = NULL;
+ }
+ track->selected = false;
+ }
+ }
+
+ if (mpctx->vo_chain && !mpctx->vo_chain->dec_src &&
+ !mpctx->vo_chain->filter_src)
+ {
+ uninit_video_chain(mpctx);
+ }
+ if (mpctx->ao_chain && !mpctx->ao_chain->dec_src &&
+ !mpctx->ao_chain->filter_src)
+ {
+ uninit_audio_chain(mpctx);
+ }
+}
+
+static void kill_outputs(struct MPContext *mpctx, struct track *track)
+{
+ if (track->vo_c || track->ao_c) {
+ MP_VERBOSE(mpctx, "deselecting track %d for lavfi-complex option\n",
+ track->user_tid);
+ mp_switch_track(mpctx, track->type, NULL, 0);
+ }
+ assert(!(track->vo_c || track->ao_c));
+}
+
+// >0: changed, 0: no change, -1: error
+static int reinit_complex_filters(struct MPContext *mpctx, bool force_uninit)
+{
+ char *graph = mpctx->opts->lavfi_complex;
+ bool have_graph = graph && graph[0] && !force_uninit;
+ if (have_graph && mpctx->lavfi &&
+ strcmp(graph, mpctx->lavfi_graph) == 0 &&
+ !mp_filter_has_failed(mpctx->lavfi))
+ return 0;
+ if (!mpctx->lavfi && !have_graph)
+ return 0;
+
+ // Deassociate the old filter pads. We leave both sources (tracks) and
+ // sinks (AO/VO) "dangling", connected to neither track or filter pad.
+ // Later, we either reassociate them with new pads, or uninit them if
+ // they are still dangling. This avoids too interruptive actions like
+ // recreating the VO.
+ deassociate_complex_filters(mpctx);
+
+ bool success = false;
+ if (!have_graph) {
+ success = true; // normal full removal of graph
+ goto done;
+ }
+
+ struct mp_lavfi *l =
+ mp_lavfi_create_graph(mpctx->filter_root, 0, false, NULL, NULL, graph);
+ if (!l)
+ goto done;
+ mpctx->lavfi = l->f;
+ mpctx->lavfi_graph = talloc_strdup(NULL, graph);
+
+ mp_filter_set_error_handler(mpctx->lavfi, mpctx->filter_root);
+
+ for (int n = 0; n < mpctx->lavfi->num_pins; n++)
+ mp_pin_disconnect(mpctx->lavfi->pins[n]);
+
+ struct mp_pin *pad = mp_filter_get_named_pin(mpctx->lavfi, "vo");
+ if (pad && mp_pin_get_dir(pad) == MP_PIN_OUT) {
+ if (mpctx->vo_chain && mpctx->vo_chain->track)
+ kill_outputs(mpctx, mpctx->vo_chain->track);
+ if (!mpctx->vo_chain) {
+ reinit_video_chain_src(mpctx, NULL);
+ if (!mpctx->vo_chain)
+ goto done;
+ }
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ assert(!vo_c->track);
+ vo_c->filter_src = pad;
+ mp_pin_connect(vo_c->filter->f->pins[0], vo_c->filter_src);
+ }
+
+ pad = mp_filter_get_named_pin(mpctx->lavfi, "ao");
+ if (pad && mp_pin_get_dir(pad) == MP_PIN_OUT) {
+ if (mpctx->ao_chain && mpctx->ao_chain->track)
+ kill_outputs(mpctx, mpctx->ao_chain->track);
+ if (!mpctx->ao_chain) {
+ reinit_audio_chain_src(mpctx, NULL);
+ if (!mpctx->ao_chain)
+ goto done;
+ }
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ assert(!ao_c->track);
+ ao_c->filter_src = pad;
+ mp_pin_connect(ao_c->filter->f->pins[0], ao_c->filter_src);
+ }
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+
+ char label[32];
+ char prefix;
+ switch (track->type) {
+ case STREAM_VIDEO: prefix = 'v'; break;
+ case STREAM_AUDIO: prefix = 'a'; break;
+ default: continue;
+ }
+ snprintf(label, sizeof(label), "%cid%d", prefix, track->user_tid);
+
+ pad = mp_filter_get_named_pin(mpctx->lavfi, label);
+ if (!pad)
+ continue;
+ if (mp_pin_get_dir(pad) != MP_PIN_IN)
+ continue;
+ assert(!mp_pin_is_connected(pad));
+
+ assert(!track->sink);
+
+ kill_outputs(mpctx, track);
+
+ track->sink = pad;
+ track->selected = true;
+
+ if (!track->dec) {
+ if (track->type == STREAM_VIDEO && !init_video_decoder(mpctx, track))
+ goto done;
+ if (track->type == STREAM_AUDIO && !init_audio_decoder(mpctx, track))
+ goto done;
+ }
+
+ mp_pin_connect(track->sink, track->dec->f->pins[0]);
+ }
+
+ // Don't allow unconnected pins. Libavfilter would make the data flow a
+ // real pain anyway.
+ for (int n = 0; n < mpctx->lavfi->num_pins; n++) {
+ struct mp_pin *pin = mpctx->lavfi->pins[n];
+ if (!mp_pin_is_connected(pin)) {
+ MP_ERR(mpctx, "Pad %s is not connected to anything.\n",
+ mp_pin_get_name(pin));
+ goto done;
+ }
+ }
+
+ success = true;
+done:
+
+ if (!success)
+ deassociate_complex_filters(mpctx);
+
+ cleanup_deassociated_complex_filters(mpctx);
+
+ if (mpctx->playback_initialized) {
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ reselect_demux_stream(mpctx, mpctx->tracks[n], false);
+ }
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return success ? 1 : -1;
+}
+
+void update_lavfi_complex(struct MPContext *mpctx)
+{
+ if (mpctx->playback_initialized) {
+ if (reinit_complex_filters(mpctx, false) != 0)
+ issue_refresh_seek(mpctx, MPSEEK_EXACT);
+ }
+}
+
+
+// Worker thread for loading external files and such. This is needed to avoid
+// freezing the core when waiting for network while loading these.
+static void load_external_opts_thread(void *p)
+{
+ void **a = p;
+ struct MPContext *mpctx = a[0];
+ struct mp_waiter *waiter = a[1];
+
+ mp_core_lock(mpctx);
+
+ load_chapters(mpctx);
+ open_external_files(mpctx, mpctx->opts->audio_files, STREAM_AUDIO);
+ open_external_files(mpctx, mpctx->opts->sub_name, STREAM_SUB);
+ open_external_files(mpctx, mpctx->opts->coverart_files, STREAM_VIDEO);
+ open_external_files(mpctx, mpctx->opts->external_files, STREAM_TYPE_COUNT);
+ autoload_external_files(mpctx, mpctx->playback_abort);
+
+ mp_waiter_wakeup(waiter, 0);
+ mp_wakeup_core(mpctx);
+ mp_core_unlock(mpctx);
+}
+
+static void load_external_opts(struct MPContext *mpctx)
+{
+ struct mp_waiter wait = MP_WAITER_INITIALIZER;
+
+ void *a[] = {mpctx, &wait};
+ if (!mp_thread_pool_queue(mpctx->thread_pool, load_external_opts_thread, a)) {
+ mpctx->stop_play = PT_ERROR;
+ return;
+ }
+
+ while (!mp_waiter_poll(&wait)) {
+ mp_idle(mpctx);
+
+ if (mpctx->stop_play)
+ mp_abort_playback_async(mpctx);
+ }
+
+ mp_waiter_wait(&wait);
+}
+
+// Start playing the current playlist entry.
+// Handle initialization and deinitialization.
+static void play_current_file(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ assert(mpctx->stop_play);
+ mpctx->stop_play = 0;
+
+ process_hooks(mpctx, "on_before_start_file");
+ if (mpctx->stop_play || !mpctx->playlist->current)
+ return;
+
+ mpv_event_start_file start_event = {
+ .playlist_entry_id = mpctx->playlist->current->id,
+ };
+ mpv_event_end_file end_event = {
+ .playlist_entry_id = start_event.playlist_entry_id,
+ };
+
+ mp_notify(mpctx, MPV_EVENT_START_FILE, &start_event);
+
+ mp_cancel_reset(mpctx->playback_abort);
+
+ mpctx->error_playing = MPV_ERROR_LOADING_FAILED;
+ mpctx->filename = NULL;
+ mpctx->shown_aframes = 0;
+ mpctx->shown_vframes = 0;
+ mpctx->last_chapter_seek = -2;
+ mpctx->last_chapter_flag = false;
+ mpctx->last_chapter = -2;
+ mpctx->paused = false;
+ mpctx->playing_msg_shown = false;
+ mpctx->max_frames = -1;
+ mpctx->video_speed = mpctx->audio_speed = opts->playback_speed;
+ mpctx->speed_factor_a = mpctx->speed_factor_v = 1.0;
+ mpctx->display_sync_error = 0.0;
+ mpctx->display_sync_active = false;
+ // let get_current_time() show 0 as start time (before playback_pts is set)
+ mpctx->last_seek_pts = 0.0;
+ mpctx->seek = (struct seek_params){ 0 };
+ mpctx->filter_root = mp_filter_create_root(mpctx->global);
+ mp_filter_graph_set_wakeup_cb(mpctx->filter_root, mp_wakeup_core_cb, mpctx);
+ mp_filter_graph_set_max_run_time(mpctx->filter_root, 0.1);
+
+ reset_playback_state(mpctx);
+
+ mpctx->playing = mpctx->playlist->current;
+ assert(mpctx->playing);
+ assert(mpctx->playing->filename);
+ mpctx->playing->reserved += 1;
+
+ mpctx->filename = talloc_strdup(NULL, mpctx->playing->filename);
+ mpctx->stream_open_filename = mpctx->filename;
+
+ mpctx->add_osd_seek_info &= OSD_SEEK_INFO_CURRENT_FILE;
+
+ if (opts->reset_options) {
+ for (int n = 0; opts->reset_options[n]; n++) {
+ const char *opt = opts->reset_options[n];
+ if (opt[0]) {
+ if (strcmp(opt, "all") == 0) {
+ m_config_backup_all_opts(mpctx->mconfig);
+ } else {
+ m_config_backup_opt(mpctx->mconfig, opt);
+ }
+ }
+ }
+ }
+
+ mp_load_auto_profiles(mpctx);
+
+ bool watch_later = mp_load_playback_resume(mpctx, mpctx->filename);
+
+ load_per_file_options(mpctx->mconfig, mpctx->playing->params,
+ mpctx->playing->num_params);
+
+ mpctx->max_frames = opts->play_frames;
+
+ handle_force_window(mpctx, false);
+
+ if (mpctx->playlist->num_entries > 1 ||
+ mpctx->playing->playlist_path)
+ MP_INFO(mpctx, "Playing: %s\n", mpctx->filename);
+
+ assert(mpctx->demuxer == NULL);
+
+ process_hooks(mpctx, "on_load");
+ if (mpctx->stop_play)
+ goto terminate_playback;
+
+ if (opts->stream_dump && opts->stream_dump[0]) {
+ if (stream_dump(mpctx, mpctx->stream_open_filename) >= 0)
+ mpctx->error_playing = 1;
+ goto terminate_playback;
+ }
+
+ open_demux_reentrant(mpctx);
+ if (!mpctx->stop_play && !mpctx->demuxer) {
+ process_hooks(mpctx, "on_load_fail");
+ if (strcmp(mpctx->stream_open_filename, mpctx->filename) != 0 &&
+ !mpctx->stop_play)
+ {
+ mpctx->error_playing = MPV_ERROR_LOADING_FAILED;
+ open_demux_reentrant(mpctx);
+ }
+ }
+ if (!mpctx->demuxer || mpctx->stop_play)
+ goto terminate_playback;
+
+ if (mpctx->demuxer->playlist) {
+ if (watch_later)
+ mp_delete_watch_later_conf(mpctx, mpctx->filename);
+ struct playlist *pl = mpctx->demuxer->playlist;
+ playlist_populate_playlist_path(pl, mpctx->filename);
+ if (infinite_playlist_loading_loop(mpctx, pl)) {
+ mpctx->stop_play = PT_STOP;
+ MP_ERR(mpctx, "Infinite playlist loading loop detected.\n");
+ goto terminate_playback;
+ }
+ transfer_playlist(mpctx, pl, &end_event.playlist_insert_id,
+ &end_event.playlist_insert_num_entries);
+ mp_notify_property(mpctx, "playlist");
+ mpctx->error_playing = 2;
+ goto terminate_playback;
+ }
+
+ if (mpctx->opts->rebase_start_time)
+ demux_set_ts_offset(mpctx->demuxer, -mpctx->demuxer->start_time);
+ enable_demux_thread(mpctx, mpctx->demuxer);
+
+ add_demuxer_tracks(mpctx, mpctx->demuxer);
+
+ load_external_opts(mpctx);
+ if (mpctx->stop_play)
+ goto terminate_playback;
+
+ check_previous_track_selection(mpctx);
+
+ process_hooks(mpctx, "on_preloaded");
+ if (mpctx->stop_play)
+ goto terminate_playback;
+
+ if (reinit_complex_filters(mpctx, false) < 0)
+ goto terminate_playback;
+
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int i = 0; i < num_ptracks[t]; i++) {
+ struct track *sel = NULL;
+ bool taken = (t == STREAM_VIDEO && mpctx->vo_chain) ||
+ (t == STREAM_AUDIO && mpctx->ao_chain);
+ if (!taken && opts->stream_auto_sel)
+ sel = select_default_track(mpctx, i, t);
+ mpctx->current_track[i][t] = sel;
+ }
+ }
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int i = 0; i < num_ptracks[t]; i++) {
+ // One track can strictly feed at most 1 decoder
+ struct track *track = mpctx->current_track[i][t];
+ if (track) {
+ if (track->type != STREAM_SUB &&
+ mpctx->encode_lavc_ctx &&
+ !encode_lavc_stream_type_ok(mpctx->encode_lavc_ctx,
+ track->type))
+ {
+ MP_WARN(mpctx, "Disabling %s (not supported by target "
+ "format).\n", stream_type_name(track->type));
+ mpctx->current_track[i][t] = NULL;
+ mark_track_selection(mpctx, i, t, -2); // disable
+ } else if (track->selected) {
+ MP_ERR(mpctx, "Track %d can't be selected twice.\n",
+ track->user_tid);
+ mpctx->current_track[i][t] = NULL;
+ mark_track_selection(mpctx, i, t, -2); // disable
+ } else {
+ track->selected = true;
+ }
+ }
+
+ // Revert selection of unselected tracks to default. This is needed
+ // because track properties have inconsistent behavior.
+ if (!track && opts->stream_id[i][t] >= 0)
+ mark_track_selection(mpctx, i, t, -1); // default
+ }
+ }
+
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ reselect_demux_stream(mpctx, mpctx->tracks[n], false);
+
+ update_demuxer_properties(mpctx);
+
+ update_playback_speed(mpctx);
+
+ reinit_video_chain(mpctx);
+ reinit_audio_chain(mpctx);
+ reinit_sub_all(mpctx);
+
+ if (mpctx->encode_lavc_ctx) {
+ if (mpctx->vo_chain)
+ encode_lavc_expect_stream(mpctx->encode_lavc_ctx, STREAM_VIDEO);
+ if (mpctx->ao_chain)
+ encode_lavc_expect_stream(mpctx->encode_lavc_ctx, STREAM_AUDIO);
+ encode_lavc_set_metadata(mpctx->encode_lavc_ctx,
+ mpctx->demuxer->metadata);
+ }
+
+ if (!mpctx->vo_chain && !mpctx->ao_chain && opts->stream_auto_sel) {
+ MP_FATAL(mpctx, "No video or audio streams selected.\n");
+ mpctx->error_playing = MPV_ERROR_NOTHING_TO_PLAY;
+ goto terminate_playback;
+ }
+
+ if (mpctx->vo_chain && mpctx->vo_chain->is_coverart) {
+ MP_INFO(mpctx,
+ "Displaying cover art. Use --no-audio-display to prevent this.\n");
+ }
+
+ if (!mpctx->vo_chain)
+ handle_force_window(mpctx, true);
+
+ MP_VERBOSE(mpctx, "Starting playback...\n");
+
+ mpctx->playback_initialized = true;
+ mpctx->playing->playlist_prev_attempt = false;
+ mp_notify(mpctx, MPV_EVENT_FILE_LOADED, NULL);
+ update_screensaver_state(mpctx);
+ clear_playlist_paths(mpctx);
+
+ if (watch_later)
+ mp_delete_watch_later_conf(mpctx, mpctx->filename);
+
+ if (mpctx->max_frames == 0) {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mpctx->error_playing = 0;
+ goto terminate_playback;
+ }
+
+ if (opts->demuxer_cache_wait) {
+ demux_start_prefetch(mpctx->demuxer);
+
+ while (!mpctx->stop_play) {
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+ if (s.idle)
+ break;
+
+ mp_idle(mpctx);
+ }
+ }
+
+ // (Not get_play_start_pts(), which would always trigger a seek.)
+ double play_start_pts = rel_time_to_abs(mpctx, opts->play_start);
+
+ // Backward playback -> start from end by default.
+ if (play_start_pts == MP_NOPTS_VALUE && opts->play_dir < 0)
+ play_start_pts = get_start_time(mpctx, -1);
+
+ if (play_start_pts != MP_NOPTS_VALUE) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, play_start_pts, MPSEEK_DEFAULT, 0);
+ execute_queued_seek(mpctx);
+ }
+
+ update_internal_pause_state(mpctx);
+
+ mpctx->error_playing = 0;
+ mpctx->in_playloop = true;
+ while (!mpctx->stop_play)
+ run_playloop(mpctx);
+ mpctx->in_playloop = false;
+
+ MP_VERBOSE(mpctx, "EOF code: %d \n", mpctx->stop_play);
+
+terminate_playback:
+
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_ERROR;
+
+ if (mpctx->stop_play != AT_END_OF_FILE)
+ clear_audio_output_buffers(mpctx);
+
+ update_core_idle_state(mpctx);
+
+ if (mpctx->step_frames) {
+ opts->pause = true;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->pause);
+ }
+
+ process_hooks(mpctx, "on_unload");
+
+ // time to uninit all, except global stuff:
+ reinit_complex_filters(mpctx, true);
+ uninit_audio_chain(mpctx);
+ uninit_video_chain(mpctx);
+ uninit_sub_all(mpctx);
+ if (!opts->gapless_audio && !mpctx->encode_lavc_ctx)
+ uninit_audio_out(mpctx);
+
+ mpctx->playback_initialized = false;
+
+ uninit_demuxer(mpctx);
+
+ // Possibly stop ongoing async commands.
+ mp_abort_playback_async(mpctx);
+
+ m_config_restore_backups(mpctx->mconfig);
+
+ TA_FREEP(&mpctx->filter_root);
+ talloc_free(mpctx->filtered_tags);
+ mpctx->filtered_tags = NULL;
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ if (encode_lavc_didfail(mpctx->encode_lavc_ctx))
+ mpctx->stop_play = PT_ERROR;
+
+ if (mpctx->stop_play == PT_ERROR && !mpctx->error_playing)
+ mpctx->error_playing = MPV_ERROR_GENERIC;
+
+ bool nothing_played = !mpctx->shown_aframes && !mpctx->shown_vframes &&
+ mpctx->error_playing <= 0;
+ bool playlist_prev_continue = false;
+ switch (mpctx->stop_play) {
+ case PT_ERROR:
+ case AT_END_OF_FILE:
+ {
+ if (mpctx->error_playing == 0 && nothing_played)
+ mpctx->error_playing = MPV_ERROR_NOTHING_TO_PLAY;
+ if (mpctx->error_playing < 0) {
+ end_event.error = mpctx->error_playing;
+ end_event.reason = MPV_END_FILE_REASON_ERROR;
+ } else if (mpctx->error_playing == 2) {
+ end_event.reason = MPV_END_FILE_REASON_REDIRECT;
+ } else {
+ end_event.reason = MPV_END_FILE_REASON_EOF;
+ }
+ if (mpctx->playing) {
+ mpctx->playing->init_failed = nothing_played;
+ playlist_prev_continue = mpctx->playing->playlist_prev_attempt &&
+ nothing_played;
+ mpctx->playing->playlist_prev_attempt = false;
+ }
+ break;
+ }
+ // Note that error_playing is meaningless in these cases.
+ case PT_NEXT_ENTRY:
+ case PT_CURRENT_ENTRY:
+ case PT_STOP: end_event.reason = MPV_END_FILE_REASON_STOP; break;
+ case PT_QUIT: end_event.reason = MPV_END_FILE_REASON_QUIT; break;
+ };
+ mp_notify(mpctx, MPV_EVENT_END_FILE, &end_event);
+
+ MP_VERBOSE(mpctx, "finished playback, %s (reason %d)\n",
+ mpv_error_string(end_event.error), end_event.reason);
+ if (end_event.error == MPV_ERROR_UNKNOWN_FORMAT)
+ MP_ERR(mpctx, "Failed to recognize file format.\n");
+
+ if (mpctx->playing)
+ playlist_entry_unref(mpctx->playing);
+ mpctx->playing = NULL;
+ talloc_free(mpctx->filename);
+ mpctx->filename = NULL;
+ mpctx->stream_open_filename = NULL;
+
+ if (end_event.error < 0 && nothing_played) {
+ mpctx->files_broken++;
+ } else if (end_event.error < 0) {
+ mpctx->files_errored++;
+ } else {
+ mpctx->files_played++;
+ }
+
+ assert(mpctx->stop_play);
+
+ process_hooks(mpctx, "on_after_end_file");
+
+ if (playlist_prev_continue) {
+ struct playlist_entry *e = mp_next_file(mpctx, -1, false);
+ if (e) {
+ mp_set_playlist_entry(mpctx, e);
+ play_current_file(mpctx);
+ }
+ }
+}
+
+// Determine the next file to play. Note that if this function returns non-NULL,
+// it can have side-effects and mutate mpctx.
+// direction: -1 (previous) or +1 (next)
+// force: if true, don't skip playlist entries marked as failed
+struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
+ bool force)
+{
+ struct playlist_entry *next = playlist_get_next(mpctx->playlist, direction);
+ if (next && direction < 0 && !force)
+ next->playlist_prev_attempt = true;
+ if (!next && mpctx->opts->loop_times != 1) {
+ if (direction > 0) {
+ if (mpctx->opts->shuffle)
+ playlist_shuffle(mpctx->playlist);
+ next = playlist_get_first(mpctx->playlist);
+ if (next && mpctx->opts->loop_times > 1) {
+ mpctx->opts->loop_times--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig,
+ &mpctx->opts->loop_times);
+ }
+ } else {
+ next = playlist_get_last(mpctx->playlist);
+ }
+ bool ignore_failures = mpctx->opts->loop_times == -2;
+ if (!force && next && next->init_failed && !ignore_failures) {
+ // Don't endless loop if no file in playlist is playable
+ bool all_failed = true;
+ for (int n = 0; n < mpctx->playlist->num_entries; n++) {
+ all_failed &= mpctx->playlist->entries[n]->init_failed;
+ if (!all_failed)
+ break;
+ }
+ if (all_failed)
+ next = NULL;
+ }
+ }
+ return next;
+}
+
+// Play all entries on the playlist, starting from the current entry.
+// Return if all done.
+void mp_play_files(struct MPContext *mpctx)
+{
+ stats_register_thread_cputime(mpctx->stats, "thread");
+
+ // Wait for all scripts to load before possibly starting playback.
+ if (!mp_clients_all_initialized(mpctx)) {
+ MP_VERBOSE(mpctx, "Waiting for scripts...\n");
+ while (!mp_clients_all_initialized(mpctx))
+ mp_idle(mpctx);
+ mp_wakeup_core(mpctx); // avoid lost wakeups during waiting
+ MP_VERBOSE(mpctx, "Done loading scripts.\n");
+ }
+ // After above is finished; but even if it's skipped.
+ mp_msg_set_early_logging(mpctx->global, false);
+
+ prepare_playlist(mpctx, mpctx->playlist);
+
+ for (;;) {
+ idle_loop(mpctx);
+
+ if (mpctx->stop_play == PT_QUIT)
+ break;
+
+ if (mpctx->playlist->current)
+ play_current_file(mpctx);
+
+ if (mpctx->stop_play == PT_QUIT)
+ break;
+
+ struct playlist_entry *new_entry = NULL;
+ if (mpctx->stop_play == PT_NEXT_ENTRY || mpctx->stop_play == PT_ERROR ||
+ mpctx->stop_play == AT_END_OF_FILE)
+ {
+ new_entry = mp_next_file(mpctx, +1, false);
+ } else if (mpctx->stop_play == PT_CURRENT_ENTRY) {
+ new_entry = mpctx->playlist->current;
+ }
+
+ mpctx->playlist->current = new_entry;
+ mpctx->playlist->current_was_replaced = false;
+ mpctx->stop_play = new_entry ? PT_NEXT_ENTRY : PT_STOP;
+
+ if (!mpctx->playlist->current && mpctx->opts->player_idle_mode < 2)
+ break;
+ }
+
+ cancel_open(mpctx);
+
+ if (mpctx->encode_lavc_ctx) {
+ // Make sure all streams get finished.
+ uninit_audio_out(mpctx);
+ uninit_video_out(mpctx);
+
+ if (!encode_lavc_free(mpctx->encode_lavc_ctx))
+ mpctx->files_errored += 1;
+
+ mpctx->encode_lavc_ctx = NULL;
+ }
+}
+
+// Abort current playback and set the given entry to play next.
+// e must be on the mpctx->playlist.
+void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e)
+{
+ assert(!e || playlist_entry_to_index(mpctx->playlist, e) >= 0);
+ mpctx->playlist->current = e;
+ mpctx->playlist->current_was_replaced = false;
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ // Make it pick up the new entry.
+ if (mpctx->stop_play != PT_QUIT)
+ mpctx->stop_play = e ? PT_CURRENT_ENTRY : PT_STOP;
+ mp_wakeup_core(mpctx);
+}
diff --git a/player/lua.c b/player/lua.c
new file mode 100644
index 0000000..41fd520
--- /dev/null
+++ b/player/lua.c
@@ -0,0 +1,1341 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <math.h>
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+
+#include "common/common.h"
+#include "options/m_property.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "options/m_option.h"
+#include "input/input.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "misc/json.h"
+#include "osdep/subprocess.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+#include "core.h"
+#include "command.h"
+#include "client.h"
+#include "libmpv/client.h"
+
+// List of builtin modules and their contents as strings.
+// All these are generated from player/lua/*.lua
+static const char * const builtin_lua_scripts[][2] = {
+ {"mp.defaults",
+# include "player/lua/defaults.lua.inc"
+ },
+ {"mp.assdraw",
+# include "player/lua/assdraw.lua.inc"
+ },
+ {"mp.options",
+# include "player/lua/options.lua.inc"
+ },
+ {"@osc.lua",
+# include "player/lua/osc.lua.inc"
+ },
+ {"@ytdl_hook.lua",
+# include "player/lua/ytdl_hook.lua.inc"
+ },
+ {"@stats.lua",
+# include "player/lua/stats.lua.inc"
+ },
+ {"@console.lua",
+# include "player/lua/console.lua.inc"
+ },
+ {"@auto_profiles.lua",
+# include "player/lua/auto_profiles.lua.inc"
+ },
+ {0}
+};
+
+// Represents a loaded script. Each has its own Lua state.
+struct script_ctx {
+ const char *name;
+ const char *filename;
+ const char *path; // NULL if single file
+ lua_State *state;
+ struct mp_log *log;
+ struct mpv_handle *client;
+ struct MPContext *mpctx;
+ size_t lua_malloc_size;
+ lua_Alloc lua_allocf;
+ void *lua_alloc_ud;
+ struct stats_ctx *stats;
+};
+
+#if LUA_VERSION_NUM <= 501
+#define mp_cpcall lua_cpcall
+#define mp_lua_len lua_objlen
+#else
+// Curse whoever had this stupid idea. Curse whoever thought it would be a good
+// idea not to include an emulated lua_cpcall() even more.
+static int mp_cpcall (lua_State *L, lua_CFunction func, void *ud)
+{
+ lua_pushcfunction(L, func); // doesn't allocate in 5.2 (but does in 5.1)
+ lua_pushlightuserdata(L, ud);
+ return lua_pcall(L, 1, 0, 0);
+}
+#define mp_lua_len lua_rawlen
+#endif
+
+// Ensure that the given argument exists, even if it's nil. Can be used to
+// avoid confusing the last missing optional arg with the first temporary value
+// pushed to the stack.
+static void mp_lua_optarg(lua_State *L, int arg)
+{
+ while (arg > lua_gettop(L))
+ lua_pushnil(L);
+}
+
+// autofree: avoid leaks if a lua-error occurs between talloc new/free.
+// If a lua c-function does a new allocation (not tied to an existing context),
+// and an uncaught lua-error occurs before "free" - the allocation is leaked.
+
+// autofree lua C function: same as lua_CFunction but with these differences:
+// - It accepts an additional void* argument - a pre-initialized talloc context
+// which it can use, and which is freed with its children once the function
+// completes - regardless if a lua error occurred or not. If a lua error did
+// occur then it's re-thrown after the ctx is freed.
+// The stack/arguments/upvalues/return are the same as with lua_CFunction.
+// - It's inserted into the lua VM using af_pushc{function,closure} instead of
+// lua_pushc{function,closure}, which takes care of wrapping it with the
+// automatic talloc allocation + lua-error-handling + talloc release.
+// This requires using AF_ENTRY instead of FN_ENTRY at struct fn_entry.
+// - The autofree overhead per call is roughly two additional plain lua calls.
+// Typically that's up to 20% slower than plain new+free without "auto",
+// and at most about twice slower - compared to bare new+free lua_CFunction.
+// - The overhead of af_push* is one additional lua-c-closure with two upvalues.
+typedef int (*af_CFunction)(lua_State *L, void *ctx);
+
+static void af_pushcclosure(lua_State *L, af_CFunction fn, int n);
+#define af_pushcfunction(L, fn) af_pushcclosure((L), (fn), 0)
+
+
+// add_af_dir, add_af_mpv_alloc take a valid DIR*/char* value respectively,
+// and closedir/mpv_free it when the parent is freed.
+
+static void destruct_af_dir(void *p)
+{
+ closedir(*(DIR**)p);
+}
+
+static void add_af_dir(void *parent, DIR *d)
+{
+ DIR **pd = talloc(parent, DIR*);
+ *pd = d;
+ talloc_set_destructor(pd, destruct_af_dir);
+}
+
+static void destruct_af_mpv_alloc(void *p)
+{
+ mpv_free(*(char**)p);
+}
+
+static void add_af_mpv_alloc(void *parent, char *ma)
+{
+ char **p = talloc(parent, char*);
+ *p = ma;
+ talloc_set_destructor(p, destruct_af_mpv_alloc);
+}
+
+
+// Perform the equivalent of mpv_free_node_contents(node) when tmp is freed.
+static void steal_node_allocations(void *tmp, mpv_node *node)
+{
+ talloc_steal(tmp, node_get_alloc(node));
+}
+
+// lua_Alloc compatible. Serves only to track memory usage. This wraps the
+// existing allocator, partly because luajit requires the use of its internal
+// allocator on 64-bit platforms.
+static void *mp_lua_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
+{
+ struct script_ctx *ctx = ud;
+
+ // Ah, what the fuck, screw whoever introduced this to Lua 5.2.
+ if (!ptr)
+ osize = 0;
+
+ ptr = ctx->lua_allocf(ctx->lua_alloc_ud, ptr, osize, nsize);
+ if (nsize && !ptr)
+ return NULL; // allocation failed, so original memory left untouched
+
+ ctx->lua_malloc_size = ctx->lua_malloc_size - osize + nsize;
+ stats_size_value(ctx->stats, "mem", ctx->lua_malloc_size);
+
+ return ptr;
+}
+
+static struct script_ctx *get_ctx(lua_State *L)
+{
+ lua_getfield(L, LUA_REGISTRYINDEX, "ctx");
+ struct script_ctx *ctx = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ assert(ctx);
+ return ctx;
+}
+
+static struct MPContext *get_mpctx(lua_State *L)
+{
+ return get_ctx(L)->mpctx;
+}
+
+static int error_handler(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ if (luaL_loadstring(L, "return debug.traceback('', 3)") == 0) { // e fn|err
+ lua_call(L, 0, 1); // e backtrace
+ const char *tr = lua_tostring(L, -1);
+ MP_WARN(ctx, "%s\n", tr ? tr : "(unknown)");
+ }
+ lua_pop(L, 1); // e
+
+ return 1;
+}
+
+// Check client API error code:
+// if err >= 0, push "true" to the stack, and return 1
+// if err < 0, push nil and then the error string to the stack, and return 2
+static int check_error(lua_State *L, int err)
+{
+ if (err >= 0) {
+ lua_pushboolean(L, 1);
+ return 1;
+ }
+ lua_pushnil(L);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+}
+
+static void add_functions(struct script_ctx *ctx);
+
+static void load_file(lua_State *L, const char *fname)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ MP_DBG(ctx, "loading file %s\n", fname);
+ void *tmp = talloc_new(ctx);
+ // according to Lua manual chunkname should be '@' plus the filename
+ char *dispname = talloc_asprintf(tmp, "@%s", fname);
+ struct bstr s = stream_read_file(fname, tmp, ctx->mpctx->global, 100000000);
+ if (!s.start)
+ luaL_error(L, "Could not read file.\n");
+ if (luaL_loadbuffer(L, s.start, s.len, dispname))
+ lua_error(L);
+ lua_call(L, 0, 1);
+ talloc_free(tmp);
+}
+
+static int load_builtin(lua_State *L)
+{
+ const char *name = luaL_checkstring(L, 1);
+ char dispname[80];
+ snprintf(dispname, sizeof(dispname), "@%s", name);
+ for (int n = 0; builtin_lua_scripts[n][0]; n++) {
+ if (strcmp(name, builtin_lua_scripts[n][0]) == 0) {
+ const char *script = builtin_lua_scripts[n][1];
+ if (luaL_loadbuffer(L, script, strlen(script), dispname))
+ lua_error(L);
+ lua_call(L, 0, 1);
+ return 1;
+ }
+ }
+ luaL_error(L, "builtin module '%s' not found\n", name);
+ return 0;
+}
+
+// Execute "require " .. name
+static void require(lua_State *L, const char *name)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ MP_DBG(ctx, "loading %s\n", name);
+ // Lazy, but better than calling the "require" function manually
+ char buf[80];
+ snprintf(buf, sizeof(buf), "require '%s'", name);
+ if (luaL_loadstring(L, buf))
+ lua_error(L);
+ lua_call(L, 0, 0);
+}
+
+// Push the table of a module. If it doesn't exist, it's created.
+// The Lua script can call "require(module)" to "load" it.
+static void push_module_table(lua_State *L, const char *module)
+{
+ lua_getglobal(L, "package"); // package
+ lua_getfield(L, -1, "loaded"); // package loaded
+ lua_remove(L, -2); // loaded
+ lua_getfield(L, -1, module); // loaded module
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1); // loaded
+ lua_newtable(L); // loaded module
+ lua_pushvalue(L, -1); // loaded module module
+ lua_setfield(L, -3, module); // loaded module
+ }
+ lua_remove(L, -2); // module
+}
+
+static int load_scripts(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *fname = ctx->filename;
+
+ require(L, "mp.defaults");
+
+ if (fname[0] == '@') {
+ require(L, fname);
+ } else {
+ load_file(L, fname);
+ }
+
+ lua_getglobal(L, "mp_event_loop"); // fn
+ if (lua_isnil(L, -1))
+ luaL_error(L, "no event loop function\n");
+ lua_call(L, 0, 0); // -
+
+ return 0;
+}
+
+static void fuck_lua(lua_State *L, const char *search_path, const char *extra)
+{
+ void *tmp = talloc_new(NULL);
+
+ lua_getglobal(L, "package"); // package
+ lua_getfield(L, -1, search_path); // package search_path
+ bstr path = bstr0(lua_tostring(L, -1));
+ char *newpath = talloc_strdup(tmp, "");
+
+ // Script-directory paths take priority.
+ if (extra) {
+ newpath = talloc_asprintf_append(newpath, "%s%s",
+ newpath[0] ? ";" : "",
+ mp_path_join(tmp, extra, "?.lua"));
+ }
+
+ // Unbelievable but true: Lua loads .lua files AND dynamic libraries from
+ // the working directory. This is highly security relevant.
+ // Lua scripts are still supposed to load globally installed libraries, so
+ // try to get by by filtering out any relative paths.
+ while (path.len) {
+ bstr item;
+ bstr_split_tok(path, ";", &item, &path);
+ if (mp_path_is_absolute(item)) {
+ newpath = talloc_asprintf_append(newpath, "%s%.*s",
+ newpath[0] ? ";" : "",
+ BSTR_P(item));
+ }
+ }
+
+ lua_pushstring(L, newpath); // package search_path newpath
+ lua_setfield(L, -3, search_path); // package search_path
+ lua_pop(L, 2); // -
+
+ talloc_free(tmp);
+}
+
+static int run_lua(lua_State *L)
+{
+ struct script_ctx *ctx = lua_touserdata(L, -1);
+ lua_pop(L, 1); // -
+
+ luaL_openlibs(L);
+
+ // used by get_ctx()
+ lua_pushlightuserdata(L, ctx); // ctx
+ lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // -
+
+ add_functions(ctx); // mp
+
+ push_module_table(L, "mp"); // mp
+
+ // "mp" is available by default, and no "require 'mp'" is needed
+ lua_pushvalue(L, -1); // mp mp
+ lua_setglobal(L, "mp"); // mp
+
+ lua_pushstring(L, ctx->name); // mp name
+ lua_setfield(L, -2, "script_name"); // mp
+
+ // used by pushnode()
+ lua_newtable(L); // mp table
+ lua_pushvalue(L, -1); // mp table table
+ lua_setfield(L, LUA_REGISTRYINDEX, "UNKNOWN_TYPE"); // mp table
+ lua_setfield(L, -2, "UNKNOWN_TYPE"); // mp
+ lua_newtable(L); // mp table
+ lua_pushvalue(L, -1); // mp table table
+ lua_setfield(L, LUA_REGISTRYINDEX, "MAP"); // mp table
+ lua_setfield(L, -2, "MAP"); // mp
+ lua_newtable(L); // mp table
+ lua_pushvalue(L, -1); // mp table table
+ lua_setfield(L, LUA_REGISTRYINDEX, "ARRAY"); // mp table
+ lua_setfield(L, -2, "ARRAY"); // mp
+
+ lua_pop(L, 1); // -
+
+ assert(lua_gettop(L) == 0);
+
+ // Add a preloader for each builtin Lua module
+ lua_getglobal(L, "package"); // package
+ assert(lua_type(L, -1) == LUA_TTABLE);
+ lua_getfield(L, -1, "preload"); // package preload
+ assert(lua_type(L, -1) == LUA_TTABLE);
+ for (int n = 0; builtin_lua_scripts[n][0]; n++) {
+ lua_pushcfunction(L, load_builtin); // package preload load_builtin
+ lua_setfield(L, -2, builtin_lua_scripts[n][0]);
+ }
+ lua_pop(L, 2); // -
+
+ assert(lua_gettop(L) == 0);
+
+ fuck_lua(L, "path", ctx->path);
+ fuck_lua(L, "cpath", NULL);
+ assert(lua_gettop(L) == 0);
+
+ // run this under an error handler that can do backtraces
+ lua_pushcfunction(L, error_handler); // errf
+ lua_pushcfunction(L, load_scripts); // errf fn
+ if (lua_pcall(L, 0, 0, -2)) { // errf [error]
+ const char *e = lua_tostring(L, -1);
+ MP_FATAL(ctx, "Lua error: %s\n", e ? e : "(unknown)");
+ }
+
+ return 0;
+}
+
+static int load_lua(struct mp_script_args *args)
+{
+ int r = -1;
+
+ struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct script_ctx) {
+ .mpctx = args->mpctx,
+ .client = args->client,
+ .name = mpv_client_name(args->client),
+ .log = args->log,
+ .filename = args->filename,
+ .path = args->path,
+ .stats = stats_ctx_create(ctx, args->mpctx->global,
+ mp_tprintf(80, "script/%s", mpv_client_name(args->client))),
+ };
+
+ stats_register_thread_cputime(ctx->stats, "cpu");
+
+ if (LUA_VERSION_NUM != 501 && LUA_VERSION_NUM != 502) {
+ MP_FATAL(ctx, "Only Lua 5.1 and 5.2 are supported.\n");
+ goto error_out;
+ }
+
+ lua_State *L = ctx->state = luaL_newstate();
+ if (!L) {
+ MP_FATAL(ctx, "Could not initialize Lua.\n");
+ goto error_out;
+ }
+
+ // Wrap the internal allocator with our version that does accounting
+ ctx->lua_allocf = lua_getallocf(L, &ctx->lua_alloc_ud);
+ lua_setallocf(L, mp_lua_alloc, ctx);
+
+ if (mp_cpcall(L, run_lua, ctx)) {
+ const char *err = "unknown error";
+ if (lua_type(L, -1) == LUA_TSTRING) // avoid allocation
+ err = lua_tostring(L, -1);
+ MP_FATAL(ctx, "Lua error: %s\n", err);
+ goto error_out;
+ }
+
+ r = 0;
+
+error_out:
+ if (ctx->state)
+ lua_close(ctx->state);
+ talloc_free(ctx);
+ return r;
+}
+
+static int check_loglevel(lua_State *L, int arg)
+{
+ const char *level = luaL_checkstring(L, arg);
+ int n = mp_msg_find_level(level);
+ if (n >= 0)
+ return n;
+ luaL_error(L, "Invalid log level '%s'", level);
+ abort();
+}
+
+static int script_log(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ int msgl = check_loglevel(L, 1);
+
+ int last = lua_gettop(L);
+ lua_getglobal(L, "tostring"); // args... tostring
+ for (int i = 2; i <= last; i++) {
+ lua_pushvalue(L, -1); // args... tostring tostring
+ lua_pushvalue(L, i); // args... tostring tostring args[i]
+ lua_call(L, 1, 1); // args... tostring str
+ const char *s = lua_tostring(L, -1);
+ if (s == NULL)
+ return luaL_error(L, "Invalid argument");
+ mp_msg(ctx->log, msgl, "%s%s", s, i > 0 ? " " : "");
+ lua_pop(L, 1); // args... tostring
+ }
+ mp_msg(ctx->log, msgl, "\n");
+
+ return 0;
+}
+
+static int script_find_config_file(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ const char *s = luaL_checkstring(L, 1);
+ char *path = mp_find_config_file(NULL, mpctx->global, s);
+ if (path) {
+ lua_pushstring(L, path);
+ } else {
+ lua_pushnil(L);
+ }
+ talloc_free(path);
+ return 1;
+}
+
+static int script_get_script_directory(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ if (ctx->path) {
+ lua_pushstring(L, ctx->path);
+ return 1;
+ }
+ return 0;
+}
+
+static void pushnode(lua_State *L, mpv_node *node);
+
+static int script_raw_wait_event(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ mpv_event *event = mpv_wait_event(ctx->client, luaL_optnumber(L, 1, 1e20));
+
+ struct mpv_node rn;
+ mpv_event_to_node(&rn, event);
+ steal_node_allocations(tmp, &rn);
+
+ pushnode(L, &rn); // event
+
+ // return event
+ return 1;
+}
+
+static int script_request_event(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *event = luaL_checkstring(L, 1);
+ bool enable = lua_toboolean(L, 2);
+ // brute force event name -> id; stops working for events > assumed max
+ int event_id = -1;
+ for (int n = 0; n < 256; n++) {
+ const char *name = mpv_event_name(n);
+ if (name && strcmp(name, event) == 0) {
+ event_id = n;
+ break;
+ }
+ }
+ lua_pushboolean(L, mpv_request_event(ctx->client, event_id, enable) >= 0);
+ return 1;
+}
+
+static int script_enable_messages(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *level = luaL_checkstring(L, 1);
+ int r = mpv_request_log_messages(ctx->client, level);
+ if (r == MPV_ERROR_INVALID_PARAMETER)
+ luaL_error(L, "Invalid log level '%s'", level);
+ return check_error(L, r);
+}
+
+static int script_command(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *s = luaL_checkstring(L, 1);
+
+ return check_error(L, mpv_command_string(ctx->client, s));
+}
+
+static int script_commandv(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ int num = lua_gettop(L);
+ const char *args[50];
+ if (num + 1 > MP_ARRAY_SIZE(args))
+ luaL_error(L, "too many arguments");
+ for (int n = 1; n <= num; n++) {
+ const char *s = lua_tostring(L, n);
+ if (!s)
+ luaL_error(L, "argument %d is not a string", n);
+ args[n - 1] = s;
+ }
+ args[num] = NULL;
+ return check_error(L, mpv_command(ctx->client, args));
+}
+
+static int script_del_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+
+ return check_error(L, mpv_del_property(ctx->client, p));
+}
+
+static int script_set_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ const char *v = luaL_checkstring(L, 2);
+
+ return check_error(L, mpv_set_property_string(ctx->client, p, v));
+}
+
+static int script_set_property_bool(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ int v = lua_toboolean(L, 2);
+
+ return check_error(L, mpv_set_property(ctx->client, p, MPV_FORMAT_FLAG, &v));
+}
+
+static bool is_int(double d)
+{
+ int64_t v = d;
+ return d == (double)v;
+}
+
+static int script_set_property_number(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ double d = luaL_checknumber(L, 2);
+ // If the number might be an integer, then set it as integer. The mpv core
+ // will (probably) convert INT64 to DOUBLE when setting, but not the other
+ // way around.
+ int res;
+ if (is_int(d)) {
+ res = mpv_set_property(ctx->client, p, MPV_FORMAT_INT64, &(int64_t){d});
+ } else {
+ res = mpv_set_property(ctx->client, p, MPV_FORMAT_DOUBLE, &d);
+ }
+ return check_error(L, res);
+}
+
+static void makenode(void *tmp, mpv_node *dst, lua_State *L, int t)
+{
+ luaL_checkstack(L, 6, "makenode");
+
+ if (t < 0)
+ t = lua_gettop(L) + (t + 1);
+ switch (lua_type(L, t)) {
+ case LUA_TNIL:
+ dst->format = MPV_FORMAT_NONE;
+ break;
+ case LUA_TNUMBER: {
+ double d = lua_tonumber(L, t);
+ if (is_int(d)) {
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = d;
+ } else {
+ dst->format = MPV_FORMAT_DOUBLE;
+ dst->u.double_ = d;
+ }
+ break;
+ }
+ case LUA_TBOOLEAN:
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = !!lua_toboolean(L, t);
+ break;
+ case LUA_TSTRING: {
+ size_t len = 0;
+ char *s = (char *)lua_tolstring(L, t, &len);
+ bool has_zeros = !!memchr(s, 0, len);
+ if (has_zeros) {
+ mpv_byte_array *ba = talloc_zero(tmp, mpv_byte_array);
+ *ba = (mpv_byte_array){talloc_memdup(tmp, s, len), len};
+ dst->format = MPV_FORMAT_BYTE_ARRAY;
+ dst->u.ba = ba;
+ } else {
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(tmp, s);
+ }
+ break;
+ }
+ case LUA_TTABLE: {
+ // Lua uses the same type for arrays and maps, so guess the correct one.
+ int format = MPV_FORMAT_NONE;
+ if (lua_getmetatable(L, t)) { // mt
+ lua_getfield(L, -1, "type"); // mt val
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ const char *type = lua_tostring(L, -1);
+ if (strcmp(type, "MAP") == 0) {
+ format = MPV_FORMAT_NODE_MAP;
+ } else if (strcmp(type, "ARRAY") == 0) {
+ format = MPV_FORMAT_NODE_ARRAY;
+ }
+ }
+ lua_pop(L, 2);
+ }
+ if (format == MPV_FORMAT_NONE) {
+ // If all keys are integers, and they're in sequence, take it
+ // as an array.
+ int count = 0;
+ for (int n = 1; ; n++) {
+ lua_pushinteger(L, n); // n
+ lua_gettable(L, t); // t[n]
+ bool empty = lua_isnil(L, -1); // t[n]
+ lua_pop(L, 1); // -
+ if (empty) {
+ count = n - 1;
+ break;
+ }
+ }
+ if (count > 0)
+ format = MPV_FORMAT_NODE_ARRAY;
+ lua_pushnil(L); // nil
+ while (lua_next(L, t) != 0) { // key value
+ count--;
+ lua_pop(L, 1); // key
+ if (count < 0) {
+ lua_pop(L, 1); // -
+ format = MPV_FORMAT_NODE_MAP;
+ break;
+ }
+ }
+ }
+ if (format == MPV_FORMAT_NONE)
+ format = MPV_FORMAT_NODE_ARRAY; // probably empty table; assume array
+ mpv_node_list *list = talloc_zero(tmp, mpv_node_list);
+ dst->format = format;
+ dst->u.list = list;
+ if (format == MPV_FORMAT_NODE_ARRAY) {
+ for (int n = 0; ; n++) {
+ lua_pushinteger(L, n + 1); // n1
+ lua_gettable(L, t); // t[n1]
+ if (lua_isnil(L, -1))
+ break;
+ MP_TARRAY_GROW(tmp, list->values, list->num);
+ makenode(tmp, &list->values[n], L, -1);
+ list->num++;
+ lua_pop(L, 1); // -
+ }
+ lua_pop(L, 1); // -
+ } else {
+ lua_pushnil(L); // nil
+ while (lua_next(L, t) != 0) { // key value
+ MP_TARRAY_GROW(tmp, list->values, list->num);
+ MP_TARRAY_GROW(tmp, list->keys, list->num);
+ makenode(tmp, &list->values[list->num], L, -1);
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ luaL_error(L, "key must be a string, but got %s",
+ lua_typename(L, lua_type(L, -2)));
+ }
+ list->keys[list->num] = talloc_strdup(tmp, lua_tostring(L, -2));
+ list->num++;
+ lua_pop(L, 1); // key
+ }
+ }
+ break;
+ }
+ default:
+ // unknown type
+ luaL_error(L, "disallowed Lua type found: %s\n", lua_typename(L, t));
+ }
+}
+
+static int script_set_property_native(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ struct mpv_node node;
+ makenode(tmp, &node, L, 2);
+ int res = mpv_set_property(ctx->client, p, MPV_FORMAT_NODE, &node);
+ return check_error(L, res);
+
+}
+
+static int script_get_property_base(lua_State *L, void *tmp, int is_osd)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+ int type = is_osd ? MPV_FORMAT_OSD_STRING : MPV_FORMAT_STRING;
+
+ char *result = NULL;
+ int err = mpv_get_property(ctx->client, name, type, &result);
+ if (err >= 0) {
+ add_af_mpv_alloc(tmp, result);
+ lua_pushstring(L, result);
+ return 1;
+ } else {
+ if (lua_isnoneornil(L, 2) && type == MPV_FORMAT_OSD_STRING) {
+ lua_pushstring(L, "");
+ } else {
+ lua_pushvalue(L, 2);
+ }
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+ }
+}
+
+static int script_get_property(lua_State *L, void *tmp)
+{
+ return script_get_property_base(L, tmp, 0);
+}
+
+static int script_get_property_osd(lua_State *L, void *tmp)
+{
+ return script_get_property_base(L, tmp, 1);
+}
+
+static int script_get_property_bool(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+
+ int result = 0;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_FLAG, &result);
+ if (err >= 0) {
+ lua_pushboolean(L, !!result);
+ return 1;
+ } else {
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+ }
+}
+
+static int script_get_property_number(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+
+ // Note: the mpv core will (hopefully) convert INT64 to DOUBLE
+ double result = 0;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_DOUBLE, &result);
+ if (err >= 0) {
+ lua_pushnumber(L, result);
+ return 1;
+ } else {
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+ }
+}
+
+static void pushnode(lua_State *L, mpv_node *node)
+{
+ luaL_checkstack(L, 6, "pushnode");
+
+ switch (node->format) {
+ case MPV_FORMAT_STRING:
+ lua_pushstring(L, node->u.string);
+ break;
+ case MPV_FORMAT_INT64:
+ lua_pushnumber(L, node->u.int64);
+ break;
+ case MPV_FORMAT_DOUBLE:
+ lua_pushnumber(L, node->u.double_);
+ break;
+ case MPV_FORMAT_NONE:
+ lua_pushnil(L);
+ break;
+ case MPV_FORMAT_FLAG:
+ lua_pushboolean(L, node->u.flag);
+ break;
+ case MPV_FORMAT_NODE_ARRAY:
+ lua_newtable(L); // table
+ lua_getfield(L, LUA_REGISTRYINDEX, "ARRAY"); // table mt
+ lua_setmetatable(L, -2); // table
+ for (int n = 0; n < node->u.list->num; n++) {
+ pushnode(L, &node->u.list->values[n]); // table value
+ lua_rawseti(L, -2, n + 1); // table
+ }
+ break;
+ case MPV_FORMAT_NODE_MAP:
+ lua_newtable(L); // table
+ lua_getfield(L, LUA_REGISTRYINDEX, "MAP"); // table mt
+ lua_setmetatable(L, -2); // table
+ for (int n = 0; n < node->u.list->num; n++) {
+ lua_pushstring(L, node->u.list->keys[n]); // table key
+ pushnode(L, &node->u.list->values[n]); // table key value
+ lua_rawset(L, -3);
+ }
+ break;
+ case MPV_FORMAT_BYTE_ARRAY:
+ lua_pushlstring(L, node->u.ba->data, node->u.ba->size);
+ break;
+ default:
+ // unknown value - what do we do?
+ // for now, set a unique dummy value
+ lua_newtable(L); // table
+ lua_getfield(L, LUA_REGISTRYINDEX, "UNKNOWN_TYPE");
+ lua_setmetatable(L, -2); // table
+ break;
+ }
+}
+
+static int script_get_property_native(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+ mp_lua_optarg(L, 2);
+
+ mpv_node node;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_NODE, &node);
+ if (err >= 0) {
+ steal_node_allocations(tmp, &node);
+ pushnode(L, &node);
+ return 1;
+ }
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+}
+
+static mpv_format check_property_format(lua_State *L, int arg)
+{
+ if (lua_isnil(L, arg))
+ return MPV_FORMAT_NONE;
+ const char *fmts[] = {"none", "native", "bool", "string", "number", NULL};
+ switch (luaL_checkoption(L, arg, "none", fmts)) {
+ case 0: return MPV_FORMAT_NONE;
+ case 1: return MPV_FORMAT_NODE;
+ case 2: return MPV_FORMAT_FLAG;
+ case 3: return MPV_FORMAT_STRING;
+ case 4: return MPV_FORMAT_DOUBLE;
+ }
+ abort();
+}
+
+// It has a raw_ prefix, because there is a more high level API in defaults.lua.
+static int script_raw_observe_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ mpv_format format = check_property_format(L, 3);
+ return check_error(L, mpv_observe_property(ctx->client, id, name, format));
+}
+
+static int script_raw_unobserve_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ lua_pushnumber(L, mpv_unobserve_property(ctx->client, id));
+ return 1;
+}
+
+static int script_command_native(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ mp_lua_optarg(L, 2);
+ struct mpv_node node;
+ struct mpv_node result;
+ makenode(tmp, &node, L, 1);
+ int err = mpv_command_node(ctx->client, &node, &result);
+ if (err >= 0) {
+ steal_node_allocations(tmp, &result);
+ pushnode(L, &result);
+ return 1;
+ }
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+}
+
+static int script_raw_command_native_async(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ struct mpv_node node;
+ makenode(tmp, &node, L, 2);
+ int res = mpv_command_node_async(ctx->client, id, &node);
+ return check_error(L, res);
+}
+
+static int script_raw_abort_async_command(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ mpv_abort_async_command(ctx->client, id);
+ return 0;
+}
+
+static int script_get_time(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ lua_pushnumber(L, mpv_get_time_us(ctx->client) / (double)(1000 * 1000));
+ return 1;
+}
+
+static int script_input_set_section_mouse_area(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+
+ char *section = (char *)luaL_checkstring(L, 1);
+ int x0 = luaL_checkinteger(L, 2);
+ int y0 = luaL_checkinteger(L, 3);
+ int x1 = luaL_checkinteger(L, 4);
+ int y1 = luaL_checkinteger(L, 5);
+ mp_input_set_section_mouse_area(mpctx->input, section, x0, y0, x1, y1);
+ return 0;
+}
+
+static int script_format_time(lua_State *L)
+{
+ double t = luaL_checknumber(L, 1);
+ const char *fmt = luaL_optstring(L, 2, "%H:%M:%S");
+ char *r = mp_format_time_fmt(fmt, t);
+ if (!r)
+ luaL_error(L, "Invalid time format string '%s'", fmt);
+ lua_pushstring(L, r);
+ talloc_free(r);
+ return 1;
+}
+
+static int script_get_wakeup_pipe(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ lua_pushinteger(L, mpv_get_wakeup_pipe(ctx->client));
+ return 1;
+}
+
+static int script_raw_hook_add(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t ud = luaL_checkinteger(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ int pri = luaL_checkinteger(L, 3);
+ return check_error(L, mpv_hook_add(ctx->client, ud, name, pri));
+}
+
+static int script_raw_hook_continue(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ lua_Integer id = luaL_checkinteger(L, 1);
+ return check_error(L, mpv_hook_continue(ctx->client, id));
+}
+
+static int script_readdir(lua_State *L, void *tmp)
+{
+ // 0 1 2 3
+ const char *fmts[] = {"all", "files", "dirs", "normal", NULL};
+ const char *path = luaL_checkstring(L, 1);
+ int t = luaL_checkoption(L, 2, "normal", fmts);
+ DIR *dir = opendir(path);
+ if (!dir) {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ return 2;
+ }
+ add_af_dir(tmp, dir);
+ lua_newtable(L); // list
+ char *fullpath = talloc_strdup(tmp, "");
+ struct dirent *e;
+ int n = 0;
+ while ((e = readdir(dir))) {
+ char *name = e->d_name;
+ if (t) {
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
+ continue;
+ if (fullpath)
+ fullpath[0] = '\0';
+ fullpath = talloc_asprintf_append(fullpath, "%s/%s", path, name);
+ struct stat st;
+ if (stat(fullpath, &st))
+ continue;
+ if (!(((t & 1) && S_ISREG(st.st_mode)) ||
+ ((t & 2) && S_ISDIR(st.st_mode))))
+ continue;
+ }
+ lua_pushinteger(L, ++n); // list index
+ lua_pushstring(L, name); // list index name
+ lua_settable(L, -3); // list
+ }
+ return 1;
+}
+
+static int script_file_info(lua_State *L)
+{
+ const char *path = luaL_checkstring(L, 1);
+
+ struct stat statbuf;
+ if (stat(path, &statbuf) != 0) {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ return 2;
+ }
+
+ lua_newtable(L); // Result stat table
+
+ const char * stat_names[] = {
+ "mode", "size",
+ "atime", "mtime", "ctime", NULL
+ };
+ const lua_Number stat_values[] = {
+ statbuf.st_mode,
+ statbuf.st_size,
+ statbuf.st_atime,
+ statbuf.st_mtime,
+ statbuf.st_ctime
+ };
+
+ // Add all fields
+ for (int i = 0; stat_names[i]; i++) {
+ lua_pushnumber(L, stat_values[i]);
+ lua_setfield(L, -2, stat_names[i]);
+ }
+
+ // Convenience booleans
+ lua_pushboolean(L, S_ISREG(statbuf.st_mode));
+ lua_setfield(L, -2, "is_file");
+
+ lua_pushboolean(L, S_ISDIR(statbuf.st_mode));
+ lua_setfield(L, -2, "is_dir");
+
+ // Return table
+ return 1;
+}
+
+static int script_split_path(lua_State *L)
+{
+ const char *p = luaL_checkstring(L, 1);
+ bstr fname = mp_dirname(p);
+ lua_pushlstring(L, fname.start, fname.len);
+ lua_pushstring(L, mp_basename(p));
+ return 2;
+}
+
+static int script_join_path(lua_State *L, void *tmp)
+{
+ const char *p1 = luaL_checkstring(L, 1);
+ const char *p2 = luaL_checkstring(L, 2);
+ char *r = mp_path_join(tmp, p1, p2);
+ lua_pushstring(L, r);
+ return 1;
+}
+
+static int script_parse_json(lua_State *L, void *tmp)
+{
+ mp_lua_optarg(L, 2);
+ char *text = talloc_strdup(tmp, luaL_checkstring(L, 1));
+ bool trail = lua_toboolean(L, 2);
+ bool ok = false;
+ struct mpv_node node;
+ if (json_parse(tmp, &node, &text, MAX_JSON_DEPTH) >= 0) {
+ json_skip_whitespace(&text);
+ ok = !text[0] || trail;
+ }
+ if (ok) {
+ pushnode(L, &node);
+ lua_pushnil(L);
+ } else {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ }
+ lua_pushstring(L, text);
+ return 3;
+}
+
+static int script_format_json(lua_State *L, void *tmp)
+{
+ struct mpv_node node;
+ makenode(tmp, &node, L, 1);
+ char *dst = talloc_strdup(tmp, "");
+ if (json_write(&dst, &node) >= 0) {
+ lua_pushstring(L, dst);
+ lua_pushnil(L);
+ } else {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ }
+ return 2;
+}
+
+static int script_get_env_list(lua_State *L)
+{
+ lua_newtable(L); // table
+ for (int n = 0; environ && environ[n]; n++) {
+ lua_pushstring(L, environ[n]); // table str
+ lua_rawseti(L, -2, n + 1); // table
+ }
+ return 1;
+}
+
+#define FN_ENTRY(name) {#name, script_ ## name, 0}
+#define AF_ENTRY(name) {#name, 0, script_ ## name}
+struct fn_entry {
+ const char *name;
+ int (*fn)(lua_State *L); // lua_CFunction
+ int (*af)(lua_State *L, void *); // af_CFunction
+};
+
+static const struct fn_entry main_fns[] = {
+ FN_ENTRY(log),
+ AF_ENTRY(raw_wait_event),
+ FN_ENTRY(request_event),
+ FN_ENTRY(find_config_file),
+ FN_ENTRY(get_script_directory),
+ FN_ENTRY(command),
+ FN_ENTRY(commandv),
+ AF_ENTRY(command_native),
+ AF_ENTRY(raw_command_native_async),
+ FN_ENTRY(raw_abort_async_command),
+ AF_ENTRY(get_property),
+ AF_ENTRY(get_property_osd),
+ FN_ENTRY(get_property_bool),
+ FN_ENTRY(get_property_number),
+ AF_ENTRY(get_property_native),
+ FN_ENTRY(del_property),
+ FN_ENTRY(set_property),
+ FN_ENTRY(set_property_bool),
+ FN_ENTRY(set_property_number),
+ AF_ENTRY(set_property_native),
+ FN_ENTRY(raw_observe_property),
+ FN_ENTRY(raw_unobserve_property),
+ FN_ENTRY(get_time),
+ FN_ENTRY(input_set_section_mouse_area),
+ FN_ENTRY(format_time),
+ FN_ENTRY(enable_messages),
+ FN_ENTRY(get_wakeup_pipe),
+ FN_ENTRY(raw_hook_add),
+ FN_ENTRY(raw_hook_continue),
+ {0}
+};
+
+static const struct fn_entry utils_fns[] = {
+ AF_ENTRY(readdir),
+ FN_ENTRY(file_info),
+ FN_ENTRY(split_path),
+ AF_ENTRY(join_path),
+ AF_ENTRY(parse_json),
+ AF_ENTRY(format_json),
+ FN_ENTRY(get_env_list),
+ {0}
+};
+
+typedef struct autofree_data {
+ af_CFunction target;
+ void *ctx;
+} autofree_data;
+
+/* runs the target autofree script_* function with the ctx argument */
+static int script_autofree_call(lua_State *L)
+{
+ // n*args &data
+ autofree_data *data = lua_touserdata(L, -1);
+ lua_pop(L, 1); // n*args
+ assert(data && data->target && data->ctx);
+ return data->target(L, data->ctx);
+}
+
+static int script_autofree_trampoline(lua_State *L)
+{
+ // n*args
+ autofree_data data = {
+ .target = lua_touserdata(L, lua_upvalueindex(2)), // fn
+ .ctx = NULL,
+ };
+ assert(data.target);
+
+ lua_pushvalue(L, lua_upvalueindex(1)); // n*args autofree_call (closure)
+ lua_insert(L, 1); // autofree_call n*args
+ lua_pushlightuserdata(L, &data); // autofree_call n*args &data
+
+ data.ctx = talloc_new(NULL);
+ int r = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0); // m*retvals
+ talloc_free(data.ctx);
+
+ if (r)
+ lua_error(L);
+
+ return lua_gettop(L); // m (retvals)
+}
+
+static void af_pushcclosure(lua_State *L, af_CFunction fn, int n)
+{
+ // Instead of pushing a direct closure of fn with n upvalues, we push an
+ // autofree_trampoline closure with two upvalues:
+ // 1: autofree_call closure with the n upvalues given here.
+ // 2: fn
+ //
+ // when called the autofree_trampoline closure will pcall the autofree_call
+ // closure with the current lua call arguments and an additional argument
+ // which holds ctx and fn. the autofree_call closure (with the n upvalues
+ // given here) calls fn directly and provides it with the ctx C argument,
+ // so that fn sees the exact n upvalues and lua call arguments as intended,
+ // wrapped with ctx init/cleanup.
+
+ lua_pushcclosure(L, script_autofree_call, n);
+ lua_pushlightuserdata(L, fn);
+ lua_pushcclosure(L, script_autofree_trampoline, 2);
+}
+
+static void register_package_fns(lua_State *L, char *module,
+ const struct fn_entry *e)
+{
+ push_module_table(L, module); // modtable
+ for (int n = 0; e[n].name; n++) {
+ if (e[n].af) {
+ af_pushcclosure(L, e[n].af, 0); // modtable fn
+ } else {
+ lua_pushcclosure(L, e[n].fn, 0); // modtable fn
+ }
+ lua_setfield(L, -2, e[n].name); // modtable
+ }
+ lua_pop(L, 1); // -
+}
+
+static void add_functions(struct script_ctx *ctx)
+{
+ lua_State *L = ctx->state;
+
+ register_package_fns(L, "mp", main_fns);
+ register_package_fns(L, "mp.utils", utils_fns);
+}
+
+const struct mp_scripting mp_scripting_lua = {
+ .name = "lua",
+ .file_ext = "lua",
+ .load = load_lua,
+};
diff --git a/player/lua/assdraw.lua b/player/lua/assdraw.lua
new file mode 100644
index 0000000..06079d5
--- /dev/null
+++ b/player/lua/assdraw.lua
@@ -0,0 +1,160 @@
+local ass_mt = {}
+ass_mt.__index = ass_mt
+local c = 0.551915024494 -- circle approximation
+
+local function ass_new()
+ return setmetatable({ scale = 4, text = "" }, ass_mt)
+end
+
+function ass_mt.new_event(ass)
+ -- osd_libass.c adds an event per line
+ if #ass.text > 0 then
+ ass.text = ass.text .. "\n"
+ end
+end
+
+function ass_mt.draw_start(ass)
+ ass.text = string.format("%s{\\p%d}", ass.text, ass.scale)
+end
+
+function ass_mt.draw_stop(ass)
+ ass.text = ass.text .. "{\\p0}"
+end
+
+function ass_mt.coord(ass, x, y)
+ local scale = 2 ^ (ass.scale - 1)
+ local ix = math.ceil(x * scale)
+ local iy = math.ceil(y * scale)
+ ass.text = string.format("%s %d %d", ass.text, ix, iy)
+end
+
+function ass_mt.append(ass, s)
+ ass.text = ass.text .. s
+end
+
+function ass_mt.merge(ass1, ass2)
+ ass1.text = ass1.text .. ass2.text
+end
+
+function ass_mt.pos(ass, x, y)
+ ass:append(string.format("{\\pos(%f,%f)}", x, y))
+end
+
+function ass_mt.an(ass, an)
+ ass:append(string.format("{\\an%d}", an))
+end
+
+function ass_mt.move_to(ass, x, y)
+ ass:append(" m")
+ ass:coord(x, y)
+end
+
+function ass_mt.line_to(ass, x, y)
+ ass:append(" l")
+ ass:coord(x, y)
+end
+
+function ass_mt.bezier_curve(ass, x1, y1, x2, y2, x3, y3)
+ ass:append(" b")
+ ass:coord(x1, y1)
+ ass:coord(x2, y2)
+ ass:coord(x3, y3)
+end
+
+
+function ass_mt.rect_ccw(ass, x0, y0, x1, y1)
+ ass:move_to(x0, y0)
+ ass:line_to(x0, y1)
+ ass:line_to(x1, y1)
+ ass:line_to(x1, y0)
+end
+
+function ass_mt.rect_cw(ass, x0, y0, x1, y1)
+ ass:move_to(x0, y0)
+ ass:line_to(x1, y0)
+ ass:line_to(x1, y1)
+ ass:line_to(x0, y1)
+end
+
+function ass_mt.hexagon_cw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ ass:move_to(x0 + r1, y0)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y0)
+ end
+ ass:line_to(x1, y0 + r2)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y1)
+ end
+ ass:line_to(x0 + r1, y1)
+ ass:line_to(x0, y0 + r1)
+end
+
+function ass_mt.hexagon_ccw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ ass:move_to(x0 + r1, y0)
+ ass:line_to(x0, y0 + r1)
+ ass:line_to(x0 + r1, y1)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y1)
+ end
+ ass:line_to(x1, y0 + r2)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y0)
+ end
+end
+
+function ass_mt.round_rect_cw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ local c1 = c * r1 -- circle approximation
+ local c2 = c * r2 -- circle approximation
+ ass:move_to(x0 + r1, y0)
+ ass:line_to(x1 - r2, y0) -- top line
+ if r2 > 0 then
+ ass:bezier_curve(x1 - r2 + c2, y0, x1, y0 + r2 - c2, x1, y0 + r2) -- top right corner
+ end
+ ass:line_to(x1, y1 - r2) -- right line
+ if r2 > 0 then
+ ass:bezier_curve(x1, y1 - r2 + c2, x1 - r2 + c2, y1, x1 - r2, y1) -- bottom right corner
+ end
+ ass:line_to(x0 + r1, y1) -- bottom line
+ if r1 > 0 then
+ ass:bezier_curve(x0 + r1 - c1, y1, x0, y1 - r1 + c1, x0, y1 - r1) -- bottom left corner
+ end
+ ass:line_to(x0, y0 + r1) -- left line
+ if r1 > 0 then
+ ass:bezier_curve(x0, y0 + r1 - c1, x0 + r1 - c1, y0, x0 + r1, y0) -- top left corner
+ end
+end
+
+function ass_mt.round_rect_ccw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ local c1 = c * r1 -- circle approximation
+ local c2 = c * r2 -- circle approximation
+ ass:move_to(x0 + r1, y0)
+ if r1 > 0 then
+ ass:bezier_curve(x0 + r1 - c1, y0, x0, y0 + r1 - c1, x0, y0 + r1) -- top left corner
+ end
+ ass:line_to(x0, y1 - r1) -- left line
+ if r1 > 0 then
+ ass:bezier_curve(x0, y1 - r1 + c1, x0 + r1 - c1, y1, x0 + r1, y1) -- bottom left corner
+ end
+ ass:line_to(x1 - r2, y1) -- bottom line
+ if r2 > 0 then
+ ass:bezier_curve(x1 - r2 + c2, y1, x1, y1 - r2 + c2, x1, y1 - r2) -- bottom right corner
+ end
+ ass:line_to(x1, y0 + r2) -- right line
+ if r2 > 0 then
+ ass:bezier_curve(x1, y0 + r2 - c2, x1 - r2 + c2, y0, x1 - r2, y0) -- top right corner
+ end
+end
+
+return {ass_new = ass_new}
diff --git a/player/lua/auto_profiles.lua b/player/lua/auto_profiles.lua
new file mode 100644
index 0000000..9dca878
--- /dev/null
+++ b/player/lua/auto_profiles.lua
@@ -0,0 +1,198 @@
+-- Note: anything global is accessible by profile condition expressions.
+
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+
+local profiles = {}
+local watched_properties = {} -- indexed by property name (used as a set)
+local cached_properties = {} -- property name -> last known raw value
+local properties_to_profiles = {} -- property name -> set of profiles using it
+local have_dirty_profiles = false -- at least one profile is marked dirty
+local pending_hooks = {} -- as set (keys only, meaningless values)
+
+-- Used during evaluation of the profile condition, and should contain the
+-- profile the condition is evaluated for.
+local current_profile = nil
+
+-- Cached set of all top-level mpv properities. Only used for extra validation.
+local property_set = {}
+for _, property in pairs(mp.get_property_native("property-list")) do
+ property_set[property] = true
+end
+
+local function evaluate(profile)
+ msg.verbose("Re-evaluating auto profile " .. profile.name)
+
+ current_profile = profile
+ local status, res = pcall(profile.cond)
+ current_profile = nil
+
+ if not status then
+ -- errors can be "normal", e.g. in case properties are unavailable
+ msg.verbose("Profile condition error on evaluating: " .. res)
+ res = false
+ end
+ res = not not res
+ if res ~= profile.status then
+ if res == true then
+ msg.info("Applying auto profile: " .. profile.name)
+ mp.commandv("apply-profile", profile.name)
+ elseif profile.status == true and profile.has_restore_opt then
+ msg.info("Restoring profile: " .. profile.name)
+ mp.commandv("apply-profile", profile.name, "restore")
+ end
+ end
+ profile.status = res
+ profile.dirty = false
+end
+
+local function on_property_change(name, val)
+ cached_properties[name] = val
+ -- Mark all profiles reading this property as dirty, so they get re-evaluated
+ -- the next time the script goes back to sleep.
+ local dependent_profiles = properties_to_profiles[name]
+ if dependent_profiles then
+ for profile, _ in pairs(dependent_profiles) do
+ assert(profile.cond) -- must be a profile table
+ profile.dirty = true
+ have_dirty_profiles = true
+ end
+ end
+end
+
+local function on_idle()
+ -- When events and property notifications stop, re-evaluate all dirty profiles.
+ if have_dirty_profiles then
+ for _, profile in ipairs(profiles) do
+ if profile.dirty then
+ evaluate(profile)
+ end
+ end
+ end
+ have_dirty_profiles = false
+ -- Release all hooks (the point was to wait until an idle event)
+ while true do
+ local h = next(pending_hooks)
+ if not h then
+ break
+ end
+ pending_hooks[h] = nil
+ h:cont()
+ end
+end
+
+local function on_hook(h)
+ h:defer()
+ pending_hooks[h] = true
+end
+
+function get(name, default)
+ -- Normally, we use the cached value only
+ if not watched_properties[name] then
+ watched_properties[name] = true
+ local res, err = mp.get_property_native(name)
+ -- Property has to not exist and the toplevel of property in the name must also
+ -- not have an existing match in the property set for this to be considered an error.
+ -- This allows things like user-data/test to still work.
+ if err == "property not found" and property_set[name:match("^([^/]+)")] == nil then
+ msg.error("Property '" .. name .. "' was not found.")
+ return default
+ end
+ cached_properties[name] = res
+ mp.observe_property(name, "native", on_property_change)
+ end
+ -- The first time the property is read we need add it to the
+ -- properties_to_profiles table, which will be used to mark the profile
+ -- dirty if a property referenced by it changes.
+ if current_profile then
+ local map = properties_to_profiles[name]
+ if not map then
+ map = {}
+ properties_to_profiles[name] = map
+ end
+ map[current_profile] = true
+ end
+ local val = cached_properties[name]
+ if val == nil then
+ val = default
+ end
+ return val
+end
+
+local function magic_get(name)
+ -- Lua identifiers can't contain "-", so in order to match with mpv
+ -- property conventions, replace "_" to "-"
+ name = string.gsub(name, "_", "-")
+ return get(name, nil)
+end
+
+local evil_magic = {}
+setmetatable(evil_magic, {
+ __index = function(table, key)
+ -- interpret everything as property, unless it already exists as
+ -- a non-nil global value
+ local v = _G[key]
+ if type(v) ~= "nil" then
+ return v
+ end
+ return magic_get(key)
+ end,
+})
+
+p = {}
+setmetatable(p, {
+ __index = function(table, key)
+ return magic_get(key)
+ end,
+})
+
+local function compile_cond(name, s)
+ local code, chunkname = "return " .. s, "profile " .. name .. " condition"
+ local chunk, err
+ if setfenv then -- lua 5.1
+ chunk, err = loadstring(code, chunkname)
+ if chunk then
+ setfenv(chunk, evil_magic)
+ end
+ else -- lua 5.2
+ chunk, err = load(code, chunkname, "t", evil_magic)
+ end
+ if not chunk then
+ msg.error("Profile '" .. name .. "' condition: " .. err)
+ chunk = function() return false end
+ end
+ return chunk
+end
+
+local function load_profiles()
+ for i, v in ipairs(mp.get_property_native("profile-list")) do
+ local cond = v["profile-cond"]
+ if cond and #cond > 0 then
+ local profile = {
+ name = v.name,
+ cond = compile_cond(v.name, cond),
+ properties = {},
+ status = nil,
+ dirty = true, -- need re-evaluate
+ has_restore_opt = v["profile-restore"] and v["profile-restore"] ~= "default"
+ }
+ profiles[#profiles + 1] = profile
+ have_dirty_profiles = true
+ end
+ end
+end
+
+load_profiles()
+
+if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then
+ -- make it exit immediately
+ _G.mp_event_loop = function() end
+ return
+end
+
+mp.register_idle(on_idle)
+for _, name in ipairs({"on_load", "on_preloaded", "on_before_start_file"}) do
+ mp.add_hook(name, 50, on_hook)
+end
+
+on_idle() -- re-evaluate all profiles immediately
diff --git a/player/lua/console.lua b/player/lua/console.lua
new file mode 100644
index 0000000..44e9436
--- /dev/null
+++ b/player/lua/console.lua
@@ -0,0 +1,1204 @@
+-- Copyright (C) 2019 the mpv developers
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+local utils = require 'mp.utils'
+local assdraw = require 'mp.assdraw'
+
+-- Default options
+local opts = {
+ -- All drawing is scaled by this value, including the text borders and the
+ -- cursor. Change it if you have a high-DPI display.
+ scale = 1,
+ -- Set the font used for the REPL and the console.
+ -- This has to be a monospaced font.
+ font = "",
+ -- Set the font size used for the REPL and the console. This will be
+ -- multiplied by "scale".
+ font_size = 16,
+ border_size = 1,
+ -- Remove duplicate entries in history as to only keep the latest one.
+ history_dedup = true,
+ -- The ratio of font height to font width.
+ -- Adjusts table width of completion suggestions.
+ font_hw_ratio = 2.0,
+}
+
+function detect_platform()
+ local platform = mp.get_property_native('platform')
+ if platform == 'darwin' or platform == 'windows' then
+ return platform
+ elseif os.getenv('WAYLAND_DISPLAY') then
+ return 'wayland'
+ end
+ return 'x11'
+end
+
+-- Pick a better default font for Windows and macOS
+local platform = detect_platform()
+if platform == 'windows' then
+ opts.font = 'Consolas'
+elseif platform == 'darwin' then
+ opts.font = 'Menlo'
+else
+ opts.font = 'monospace'
+end
+
+-- Apply user-set options
+require 'mp.options'.read_options(opts)
+
+local styles = {
+ -- Colors are stolen from base16 Eighties by Chris Kempson
+ -- and converted to BGR as is required by ASS.
+ -- 2d2d2d 393939 515151 697374
+ -- 939fa0 c8d0d3 dfe6e8 ecf0f2
+ -- 7a77f2 5791f9 66ccff 99cc99
+ -- cccc66 cc9966 cc99cc 537bd2
+
+ debug = '{\\1c&Ha09f93&}',
+ verbose = '{\\1c&H99cc99&}',
+ warn = '{\\1c&H66ccff&}',
+ error = '{\\1c&H7a77f2&}',
+ fatal = '{\\1c&H5791f9&\\b1}',
+ suggestion = '{\\1c&Hcc99cc&}',
+}
+
+local repl_active = false
+local insert_mode = false
+local pending_update = false
+local line = ''
+local cursor = 1
+local history = {}
+local history_pos = 1
+local log_buffer = {}
+local suggestion_buffer = {}
+local key_bindings = {}
+local global_margins = { t = 0, b = 0 }
+
+local file_commands = {}
+local path_separator = platform == 'windows' and '\\' or '/'
+
+local update_timer = nil
+update_timer = mp.add_periodic_timer(0.05, function()
+ if pending_update then
+ update()
+ else
+ update_timer:kill()
+ end
+end)
+update_timer:kill()
+
+mp.observe_property("user-data/osc/margins", "native", function(_, val)
+ if val then
+ global_margins = val
+ else
+ global_margins = { t = 0, b = 0 }
+ end
+ update()
+end)
+
+-- Add a line to the log buffer (which is limited to 100 lines)
+function log_add(style, text)
+ log_buffer[#log_buffer + 1] = { style = style, text = text }
+ if #log_buffer > 100 then
+ table.remove(log_buffer, 1)
+ end
+
+ if repl_active then
+ if not update_timer:is_enabled() then
+ update()
+ update_timer:resume()
+ else
+ pending_update = true
+ end
+ end
+end
+
+-- Escape a string for verbatim display on the OSD
+function ass_escape(str)
+ -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
+ -- it isn't followed by a recognised character, so add a zero-width
+ -- non-breaking space
+ str = str:gsub('\\', '\\\239\187\191')
+ str = str:gsub('{', '\\{')
+ str = str:gsub('}', '\\}')
+ -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
+ -- consecutive newlines
+ str = str:gsub('\n', '\239\187\191\\N')
+ -- Turn leading spaces into hard spaces to prevent ASS from stripping them
+ str = str:gsub('\\N ', '\\N\\h')
+ str = str:gsub('^ ', '\\h')
+ return str
+end
+
+-- Takes a list of strings, a max width in characters and
+-- optionally a max row count.
+-- The result contains at least one column.
+-- Rows are cut off from the top if rows_max is specified.
+-- returns a string containing the formatted table and the row count
+function format_table(list, width_max, rows_max)
+ if #list == 0 then
+ return '', 0
+ end
+
+ local spaces_min = 2
+ local spaces_max = 8
+ local list_size = #list
+ local column_count = 1
+ local row_count = list_size
+ local column_widths
+ -- total width without spacing
+ local width_total = 0
+
+ local list_widths = {}
+ for i, item in ipairs(list) do
+ list_widths[i] = len_utf8(item)
+ end
+
+ -- use as many columns as possible
+ for columns = 2, list_size do
+ local rows_lower_bound = math.min(rows_max, math.ceil(list_size / columns))
+ local rows_upper_bound = math.min(rows_max, list_size, math.ceil(list_size / (columns - 1) - 1))
+ for rows = rows_upper_bound, rows_lower_bound, -1 do
+ cw = {}
+ width_total = 0
+
+ -- find out width of each column
+ for column = 1, columns do
+ local width = 0
+ for row = 1, rows do
+ local i = row + (column - 1) * rows
+ local item_width = list_widths[i]
+ if not item_width then break end
+ if width < item_width then
+ width = item_width
+ end
+ end
+ cw[column] = width
+ width_total = width_total + width
+ if width_total + (columns - 1) * spaces_min > width_max then
+ break
+ end
+ end
+
+ if width_total + (columns - 1) * spaces_min <= width_max then
+ row_count = rows
+ column_count = columns
+ column_widths = cw
+ else
+ break
+ end
+ end
+ if width_total + (columns - 1) * spaces_min > width_max then
+ break
+ end
+ end
+
+ local spaces = math.floor((width_max - width_total) / (column_count - 1))
+ spaces = math.max(spaces_min, math.min(spaces_max, spaces))
+ local spacing = column_count > 1 and string.format('%' .. spaces .. 's', ' ') or ''
+
+ local rows = {}
+ for row = 1, row_count do
+ local columns = {}
+ for column = 1, column_count do
+ local i = row + (column - 1) * row_count
+ if i > #list then break end
+ -- more then 99 leads to 'invalid format (width or precision too long)'
+ local format_string = column == column_count and '%s'
+ or '%-' .. math.min(column_widths[column], 99) .. 's'
+ columns[column] = string.format(format_string, list[i])
+ end
+ -- first row is at the bottom
+ rows[row_count - row + 1] = table.concat(columns, spacing)
+ end
+ return table.concat(rows, '\n'), row_count
+end
+
+local function print_to_terminal()
+ -- Clear the log after closing the console.
+ if not repl_active then
+ mp.osd_message('')
+ return
+ end
+
+ local log = ''
+ for _, log_line in ipairs(log_buffer) do
+ log = log .. log_line.text
+ end
+
+ local suggestions = table.concat(suggestion_buffer, '\t')
+ if suggestions ~= '' then
+ suggestions = suggestions .. '\n'
+ end
+
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+ -- Ensure there is a character with inverted colors to print.
+ if after_cur == '' then
+ after_cur = ' '
+ end
+
+ mp.osd_message(log .. suggestions .. '> ' .. before_cur .. '\027[7m' ..
+ after_cur:sub(1, 1) .. '\027[0m' .. after_cur:sub(2), 999)
+end
+
+-- Render the REPL and console as an ASS OSD
+function update()
+ pending_update = false
+
+ -- Print to the terminal when there is no VO. Check both vo-configured so
+ -- it works with --force-window --idle and no video tracks, and whether
+ -- there is a video track so that the condition doesn't become true while
+ -- switching VO at runtime, making mp.osd_message() print to the VO's OSD.
+ -- This issue does not happen when switching VO without any video track
+ -- regardless of the condition used.
+ if not mp.get_property_native('vo-configured')
+ and not mp.get_property('current-tracks/video') then
+ print_to_terminal()
+ return
+ end
+
+ local dpi_scale = mp.get_property_native("display-hidpi-scale", 1.0)
+
+ dpi_scale = dpi_scale * opts.scale
+
+ local screenx, screeny, aspect = mp.get_osd_size()
+ screenx = screenx / dpi_scale
+ screeny = screeny / dpi_scale
+
+ -- Clear the OSD if the REPL is not active
+ if not repl_active then
+ mp.set_osd_ass(screenx, screeny, '')
+ return
+ end
+
+ local coordinate_top = math.floor(global_margins.t * screeny + 0.5)
+ local clipping_coordinates = '0,' .. coordinate_top .. ',' ..
+ screenx .. ',' .. screeny
+ local ass = assdraw.ass_new()
+ local has_shadow = mp.get_property('osd-back-color'):sub(2, 3) == '00'
+ local style = '{\\r' ..
+ '\\1a&H00&\\3a&H00&\\1c&Heeeeee&\\3c&H111111&' ..
+ (has_shadow and '\\4a&H99&\\4c&H000000&' or '') ..
+ '\\fn' .. opts.font .. '\\fs' .. opts.font_size ..
+ '\\bord' .. opts.border_size .. '\\xshad0\\yshad1\\fsp0\\q1' ..
+ '\\clip(' .. clipping_coordinates .. ')}'
+ -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
+ -- inline with the surrounding text, but it sets the advance to the width
+ -- of the drawing. So the cursor doesn't affect layout too much, make it as
+ -- thin as possible and make it appear to be 1px wide by giving it 0.5px
+ -- horizontal borders.
+ local cheight = opts.font_size * 8
+ local cglyph = '{\\r' ..
+ '\\1a&H44&\\3a&H44&\\4a&H99&' ..
+ '\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' ..
+ '\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' ..
+ 'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight ..
+ '{\\p0}'
+ local before_cur = ass_escape(line:sub(1, cursor - 1))
+ local after_cur = ass_escape(line:sub(cursor))
+
+ -- Render log messages as ASS.
+ -- This will render at most screeny / font_size - 1 messages.
+
+ -- lines above the prompt
+ -- subtract 1.5 to account for the input line
+ local screeny_factor = (1 - global_margins.t - global_margins.b)
+ local lines_max = math.ceil(screeny * screeny_factor / opts.font_size - 1.5)
+ -- Estimate how many characters fit in one line
+ local width_max = math.ceil(screenx / opts.font_size * opts.font_hw_ratio)
+
+ local suggestions, rows = format_table(suggestion_buffer, width_max, lines_max)
+ local suggestion_ass = style .. styles.suggestion .. ass_escape(suggestions)
+
+ local log_ass = ''
+ local log_messages = #log_buffer
+ local log_max_lines = math.max(0, lines_max - rows)
+ if log_max_lines < log_messages then
+ log_messages = log_max_lines
+ end
+ for i = #log_buffer - log_messages + 1, #log_buffer do
+ log_ass = log_ass .. style .. log_buffer[i].style .. ass_escape(log_buffer[i].text)
+ end
+
+ ass:new_event()
+ ass:an(1)
+ ass:pos(2, screeny - 2 - global_margins.b * screeny)
+ ass:append(log_ass .. '\\N')
+ if #suggestions > 0 then
+ ass:append(suggestion_ass .. '\\N')
+ end
+ ass:append(style .. '> ' .. before_cur)
+ ass:append(cglyph)
+ ass:append(style .. after_cur)
+
+ -- Redraw the cursor with the REPL text invisible. This will make the
+ -- cursor appear in front of the text.
+ ass:new_event()
+ ass:an(1)
+ ass:pos(2, screeny - 2 - global_margins.b * screeny)
+ ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
+ ass:append(cglyph)
+ ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
+
+ mp.set_osd_ass(screenx, screeny, ass.text)
+end
+
+-- Set the REPL visibility ("enable", Esc)
+function set_active(active)
+ if active == repl_active then return end
+ if active then
+ repl_active = true
+ insert_mode = false
+ mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging')
+ mp.enable_messages('terminal-default')
+ define_key_bindings()
+ else
+ repl_active = false
+ undefine_key_bindings()
+ mp.enable_messages('silent:terminal-default')
+ collectgarbage()
+ end
+ update()
+end
+
+-- Show the repl if hidden and replace its contents with 'text'
+-- (script-message-to repl type)
+function show_and_type(text, cursor_pos)
+ text = text or ''
+ cursor_pos = tonumber(cursor_pos)
+
+ -- Save the line currently being edited, just in case
+ if line ~= text and line ~= '' and history[#history] ~= line then
+ history_add(line)
+ end
+
+ line = text
+ if cursor_pos ~= nil and cursor_pos >= 1
+ and cursor_pos <= line:len() + 1 then
+ cursor = math.floor(cursor_pos)
+ else
+ cursor = line:len() + 1
+ end
+ history_pos = #history + 1
+ insert_mode = false
+ if repl_active then
+ update()
+ else
+ set_active(true)
+ end
+end
+
+-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
+-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
+function next_utf8(str, pos)
+ if pos > str:len() then return pos end
+ repeat
+ pos = pos + 1
+ until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
+ return pos
+end
+
+-- As above, but finds the previous UTF-8 character in 'str' before 'pos'
+function prev_utf8(str, pos)
+ if pos <= 1 then return pos end
+ repeat
+ pos = pos - 1
+ until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
+ return pos
+end
+
+function len_utf8(str)
+ local len = 0
+ local pos = 1
+ while pos <= str:len() do
+ pos = next_utf8(str, pos)
+ len = len + 1
+ end
+ return len
+end
+
+-- Insert a character at the current cursor position (any_unicode)
+function handle_char_input(c)
+ if insert_mode then
+ line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
+ else
+ line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
+ end
+ cursor = cursor + #c
+ suggestion_buffer = {}
+ update()
+end
+
+-- Remove the character behind the cursor (Backspace)
+function handle_backspace()
+ if cursor <= 1 then return end
+ local prev = prev_utf8(line, cursor)
+ line = line:sub(1, prev - 1) .. line:sub(cursor)
+ cursor = prev
+ suggestion_buffer = {}
+ update()
+end
+
+-- Remove the character in front of the cursor (Del)
+function handle_del()
+ if cursor > line:len() then return end
+ line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
+ suggestion_buffer = {}
+ update()
+end
+
+-- Toggle insert mode (Ins)
+function handle_ins()
+ insert_mode = not insert_mode
+end
+
+-- Move the cursor to the next character (Right)
+function next_char(amount)
+ cursor = next_utf8(line, cursor)
+ update()
+end
+
+-- Move the cursor to the previous character (Left)
+function prev_char(amount)
+ cursor = prev_utf8(line, cursor)
+ update()
+end
+
+-- Clear the current line (Ctrl+C)
+function clear()
+ line = ''
+ cursor = 1
+ insert_mode = false
+ history_pos = #history + 1
+ suggestion_buffer = {}
+ update()
+end
+
+-- Close the REPL if the current line is empty, otherwise delete the next
+-- character (Ctrl+D)
+function maybe_exit()
+ if line == '' then
+ set_active(false)
+ else
+ handle_del()
+ end
+end
+
+function help_command(param)
+ local cmdlist = mp.get_property_native('command-list')
+ table.sort(cmdlist, function(c1, c2)
+ return c1.name < c2.name
+ end)
+ local output = ''
+ if param == '' then
+ output = 'Available commands:\n'
+ for _, cmd in ipairs(cmdlist) do
+ output = output .. ' ' .. cmd.name
+ end
+ output = output .. '\n'
+ output = output .. 'Use "help command" to show information about a command.\n'
+ output = output .. "ESC or Ctrl+d exits the console.\n"
+ else
+ local cmd = nil
+ for _, curcmd in ipairs(cmdlist) do
+ if curcmd.name:find(param, 1, true) then
+ cmd = curcmd
+ if curcmd.name == param then
+ break -- exact match
+ end
+ end
+ end
+ if not cmd then
+ log_add(styles.error, 'No command matches "' .. param .. '"!')
+ return
+ end
+ output = output .. 'Command "' .. cmd.name .. '"\n'
+ for _, arg in ipairs(cmd.args) do
+ output = output .. ' ' .. arg.name .. ' (' .. arg.type .. ')'
+ if arg.optional then
+ output = output .. ' (optional)'
+ end
+ output = output .. '\n'
+ end
+ if cmd.vararg then
+ output = output .. 'This command supports variable arguments.\n'
+ end
+ end
+ log_add('', output)
+end
+
+-- Add a line to the history and deduplicate
+function history_add(text)
+ if opts.history_dedup then
+ -- More recent entries are more likely to be repeated
+ for i = #history, 1, -1 do
+ if history[i] == text then
+ table.remove(history, i)
+ break
+ end
+ end
+ end
+
+ history[#history + 1] = text
+end
+
+-- Run the current command and clear the line (Enter)
+function handle_enter()
+ if line == '' then
+ return
+ end
+ if history[#history] ~= line then
+ history_add(line)
+ end
+
+ -- match "help [<text>]", return <text> or "", strip all whitespace
+ local help = line:match('^%s*help%s+(.-)%s*$') or
+ (line:match('^%s*help$') and '')
+ if help then
+ help_command(help)
+ else
+ mp.command(line)
+ end
+
+ clear()
+end
+
+-- Go to the specified position in the command history
+function go_history(new_pos)
+ local old_pos = history_pos
+ history_pos = new_pos
+
+ -- Restrict the position to a legal value
+ if history_pos > #history + 1 then
+ history_pos = #history + 1
+ elseif history_pos < 1 then
+ history_pos = 1
+ end
+
+ -- Do nothing if the history position didn't actually change
+ if history_pos == old_pos then
+ return
+ end
+
+ -- If the user was editing a non-history line, save it as the last history
+ -- entry. This makes it much less frustrating to accidentally hit Up/Down
+ -- while editing a line.
+ if old_pos == #history + 1 and line ~= '' and history[#history] ~= line then
+ history_add(line)
+ end
+
+ -- Now show the history line (or a blank line for #history + 1)
+ if history_pos <= #history then
+ line = history[history_pos]
+ else
+ line = ''
+ end
+ cursor = line:len() + 1
+ insert_mode = false
+ update()
+end
+
+-- Go to the specified relative position in the command history (Up, Down)
+function move_history(amount)
+ go_history(history_pos + amount)
+end
+
+-- Go to the first command in the command history (PgUp)
+function handle_pgup()
+ go_history(1)
+end
+
+-- Stop browsing history and start editing a blank line (PgDown)
+function handle_pgdown()
+ go_history(#history + 1)
+end
+
+-- Move to the start of the current word, or if already at the start, the start
+-- of the previous word. (Ctrl+Left)
+function prev_word()
+ -- This is basically the same as next_word() but backwards, so reverse the
+ -- string in order to do a "backwards" find. This wouldn't be as annoying
+ -- to do if Lua didn't insist on 1-based indexing.
+ cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1
+ update()
+end
+
+-- Move to the end of the current word, or if already at the end, the end of
+-- the next word. (Ctrl+Right)
+function next_word()
+ cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1
+ update()
+end
+
+local function command_list()
+ local commands = {}
+ for i, command in ipairs(mp.get_property_native('command-list')) do
+ commands[i] = command.name
+ end
+
+ return commands
+end
+
+local function command_list_and_help()
+ local commands = command_list()
+ commands[#commands + 1] = 'help'
+
+ return commands
+end
+
+local function property_list()
+ local option_info = {
+ 'name', 'type', 'set-from-commandline', 'set-locally', 'default-value',
+ 'min', 'max', 'choices',
+ }
+
+ local properties = mp.get_property_native('property-list')
+
+ for _, option in ipairs(mp.get_property_native('options')) do
+ properties[#properties + 1] = 'options/' .. option
+ properties[#properties + 1] = 'file-local-options/' .. option
+ properties[#properties + 1] = 'option-info/' .. option
+
+ for _, sub_property in ipairs(option_info) do
+ properties[#properties + 1] = 'option-info/' .. option .. '/' ..
+ sub_property
+ end
+ end
+
+ return properties
+end
+
+local function profile_list()
+ local profiles = {}
+
+ for i, profile in ipairs(mp.get_property_native('profile-list')) do
+ profiles[i] = profile.name
+ end
+
+ return profiles
+end
+
+local function list_option_list()
+ local options = {}
+
+ -- Don't log errors for renamed and removed properties.
+ -- (Just mp.enable_messages('fatal') still logs them to the terminal.)
+ local msg_level_backup = mp.get_property('msg-level')
+ mp.set_property('msg-level', msg_level_backup == '' and 'cplayer=no'
+ or msg_level_backup .. ',cplayer=no')
+
+ for _, option in pairs(mp.get_property_native('options')) do
+ if mp.get_property('option-info/' .. option .. '/type', ''):find(' list$') then
+ options[#options + 1] = option
+ end
+ end
+
+ mp.set_property('msg-level', msg_level_backup)
+
+ return options
+end
+
+local function list_option_verb_list(option)
+ local type = mp.get_property('option-info/' .. option .. '/type')
+
+ if type == 'Key/value list' then
+ return {'add', 'append', 'set', 'remove'}
+ end
+
+ if type == 'String list' or type == 'Object settings list' then
+ return {'add', 'append', 'clr', 'pre', 'set', 'remove', 'toggle'}
+ end
+
+ return {}
+end
+
+local function choice_list(option)
+ local info = mp.get_property_native('option-info/' .. option, {})
+
+ if info.type == 'Flag' then
+ return { 'no', 'yes' }
+ end
+
+ return info.choices or {}
+end
+
+local function find_commands_with_file_argument()
+ if #file_commands > 0 then
+ return file_commands
+ end
+
+ for _, command in pairs(mp.get_property_native('command-list')) do
+ if command.args[1] and
+ (command.args[1].name == 'filename' or command.args[1].name == 'url') then
+ file_commands[#file_commands + 1] = command.name
+ end
+ end
+
+ return file_commands
+end
+
+local function file_list(directory)
+ if directory == '' then
+ directory = '.'
+ end
+
+ local files = utils.readdir(directory, 'files') or {}
+
+ for _, dir in pairs(utils.readdir(directory, 'dirs') or {}) do
+ files[#files + 1] = dir .. path_separator
+ end
+
+ return files
+end
+
+-- List of tab-completions:
+-- pattern: A Lua pattern used in string:match. It should return the start
+-- position of the word to be completed in the first capture (using
+-- the empty parenthesis notation "()"). In patterns with 2
+-- captures, the first determines the completions, and the second is
+-- the start of the word to be completed.
+-- list: A function that returns a list of candidate completion values.
+-- append: An extra string to be appended to the end of a successful
+-- completion. It is only appended if 'list' contains exactly one
+-- match.
+function build_completers()
+ local completers = {
+ { pattern = '^%s*()[%w_-]*$', list = command_list_and_help, append = ' ' },
+ { pattern = '^%s*help%s+()[%w_-]*$', list = command_list },
+ { pattern = '^%s*set%s+"?([%w_-]+)"?%s+()%S*$', list = choice_list },
+ { pattern = '^%s*set%s+"?([%w_-]+)"?%s+"()%S*$', list = choice_list, append = '"' },
+ { pattern = '^%s*cycle[-_]values%s+"?([%w_-]+)"?.-%s+()%S*$', list = choice_list, append = " " },
+ { pattern = '^%s*cycle[-_]values%s+"?([%w_-]+)"?.-%s+"()%S*$', list = choice_list, append = '" ' },
+ { pattern = '^%s*apply[-_]profile%s+"()%S*$', list = profile_list, append = '"' },
+ { pattern = '^%s*apply[-_]profile%s+()%S*$', list = profile_list },
+ { pattern = '^%s*change[-_]list%s+()[%w_-]*$', list = list_option_list, append = ' ' },
+ { pattern = '^%s*change[-_]list%s+()"[%w_-]*$', list = list_option_list, append = '" ' },
+ { pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+()%a*$', list = list_option_verb_list, append = ' ' },
+ { pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+"()%a*$', list = list_option_verb_list, append = '" ' },
+ { pattern = '^%s*([av]f)%s+()%a*$', list = list_option_verb_list, append = ' ' },
+ { pattern = '^%s*([av]f)%s+"()%a*$', list = list_option_verb_list, append = '" ' },
+ { pattern = '${=?()[%w_/-]*$', list = property_list, append = '}' },
+ }
+
+ for _, command in pairs({'set', 'add', 'cycle', 'cycle[-_]values', 'multiply'}) do
+ completers[#completers + 1] = {
+ pattern = '^%s*' .. command .. '%s+()[%w_/-]*$',
+ list = property_list,
+ append = ' ',
+ }
+ completers[#completers + 1] = {
+ pattern = '^%s*' .. command .. '%s+"()[%w_/-]*$',
+ list = property_list,
+ append = '" ',
+ }
+ end
+
+
+ for _, command in pairs(find_commands_with_file_argument()) do
+ completers[#completers + 1] = {
+ pattern = '^%s*' .. command:gsub('-', '[-_]') ..
+ '%s+["\']?(.-)()[^' .. path_separator ..']*$',
+ list = file_list,
+ -- Unfortunately appending " here would append it everytime a
+ -- directory is fully completed, even if you intend to browse it
+ -- afterwards.
+ }
+ end
+
+ return completers
+end
+
+-- Use 'list' to find possible tab-completions for 'part.'
+-- Returns a list of all potential completions and the longest
+-- common prefix of all the matching list items.
+function complete_match(part, list)
+ local completions = {}
+ local prefix = nil
+
+ for _, candidate in ipairs(list) do
+ if candidate:sub(1, part:len()) == part then
+ if prefix and prefix ~= candidate then
+ local prefix_len = part:len()
+ while prefix:sub(1, prefix_len + 1)
+ == candidate:sub(1, prefix_len + 1) do
+ prefix_len = prefix_len + 1
+ end
+ prefix = candidate:sub(1, prefix_len)
+ else
+ prefix = candidate
+ end
+ completions[#completions + 1] = candidate
+ end
+ end
+
+ return completions, prefix
+end
+
+function common_prefix_length(s1, s2)
+ local common_count = 0
+ for i = 1, #s1 do
+ if s1:byte(i) ~= s2:byte(i) then
+ break
+ end
+ common_count = common_count + 1
+ end
+ return common_count
+end
+
+function max_overlap_length(s1, s2)
+ for s1_offset = 0, #s1 - 1 do
+ local match = true
+ for i = 1, #s1 - s1_offset do
+ if s1:byte(s1_offset + i) ~= s2:byte(i) then
+ match = false
+ break
+ end
+ end
+ if match then
+ return #s1 - s1_offset
+ end
+ end
+ return 0
+end
+
+-- Complete the option or property at the cursor (TAB)
+function complete()
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ -- Try the first completer that works
+ for _, completer in ipairs(build_completers()) do
+ -- Completer patterns should return the start of the word to be
+ -- completed as the first capture.
+ local s, s2 = before_cur:match(completer.pattern)
+ if not s then
+ -- Multiple input commands can be separated by semicolons, so all
+ -- completions that are anchored at the start of the string with
+ -- '^' can start from a semicolon as well. Replace ^ with ; and try
+ -- to match again.
+ s, s2 = before_cur:match(completer.pattern:gsub('^^', ';'))
+ end
+ if s then
+ local hint
+ if s2 then
+ hint = s
+ s = s2
+ end
+
+ -- If the completer's pattern found a word, check the completer's
+ -- list for possible completions
+ local part = before_cur:sub(s)
+ local completions, prefix = complete_match(part, completer.list(hint))
+ if #completions > 0 then
+ -- If there was only one full match from the list, add
+ -- completer.append to the final string. This is normally a
+ -- space or a quotation mark followed by a space.
+ local after_cur_index = 1
+ if #completions == 1 then
+ local append = completer.append or ''
+ prefix = prefix .. append
+
+ -- calculate offset into after_cur
+ local prefix_len = common_prefix_length(append, after_cur)
+ local overlap_size = max_overlap_length(append, after_cur)
+ after_cur_index = math.max(prefix_len, overlap_size) + 1
+ else
+ table.sort(completions)
+ suggestion_buffer = completions
+ end
+
+ -- Insert the completion and update
+ before_cur = before_cur:sub(1, s - 1) .. prefix
+ cursor = before_cur:len() + 1
+ line = before_cur .. after_cur:sub(after_cur_index)
+ update()
+ return
+ end
+ end
+ end
+end
+
+-- Move the cursor to the beginning of the line (HOME)
+function go_home()
+ cursor = 1
+ update()
+end
+
+-- Move the cursor to the end of the line (END)
+function go_end()
+ cursor = line:len() + 1
+ update()
+end
+
+-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
+function del_word()
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
+ line = before_cur .. after_cur
+ cursor = before_cur:len() + 1
+ update()
+end
+
+-- Delete from the cursor to the end of the word (Ctrl+Del)
+function del_next_word()
+ if cursor > line:len() then return end
+
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
+ line = before_cur .. after_cur
+ update()
+end
+
+-- Delete from the cursor to the end of the line (Ctrl+K)
+function del_to_eol()
+ line = line:sub(1, cursor - 1)
+ update()
+end
+
+-- Delete from the cursor back to the start of the line (Ctrl+U)
+function del_to_start()
+ line = line:sub(cursor)
+ cursor = 1
+ update()
+end
+
+-- Empty the log buffer of all messages (Ctrl+L)
+function clear_log_buffer()
+ log_buffer = {}
+ update()
+end
+
+-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
+function get_clipboard(clip)
+ if platform == 'x11' then
+ local res = utils.subprocess({
+ args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'wayland' then
+ local res = utils.subprocess({
+ args = { 'wl-paste', clip and '-n' or '-np' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'windows' then
+ local res = utils.subprocess({
+ args = { 'powershell', '-NoProfile', '-Command', [[& {
+ Trap {
+ Write-Error -ErrorRecord $_
+ Exit 1
+ }
+
+ $clip = ""
+ if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
+ $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
+ } else {
+ Add-Type -AssemblyName PresentationCore
+ $clip = [Windows.Clipboard]::GetText()
+ }
+
+ $clip = $clip -Replace "`r",""
+ $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
+ [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
+ }]] },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'darwin' then
+ local res = utils.subprocess({
+ args = { 'pbpaste' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ end
+ return ''
+end
+
+-- Paste text from the window-system's clipboard. 'clip' determines whether the
+-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
+function paste(clip)
+ local text = get_clipboard(clip)
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+ line = before_cur .. text .. after_cur
+ cursor = cursor + text:len()
+ update()
+end
+
+-- List of input bindings. This is a weird mashup between common GUI text-input
+-- bindings and readline bindings.
+function get_bindings()
+ local bindings = {
+ { 'esc', function() set_active(false) end },
+ { 'ctrl+[', function() set_active(false) end },
+ { 'enter', handle_enter },
+ { 'kp_enter', handle_enter },
+ { 'shift+enter', function() handle_char_input('\n') end },
+ { 'ctrl+j', handle_enter },
+ { 'ctrl+m', handle_enter },
+ { 'bs', handle_backspace },
+ { 'shift+bs', handle_backspace },
+ { 'ctrl+h', handle_backspace },
+ { 'del', handle_del },
+ { 'shift+del', handle_del },
+ { 'ins', handle_ins },
+ { 'shift+ins', function() paste(false) end },
+ { 'mbtn_mid', function() paste(false) end },
+ { 'left', function() prev_char() end },
+ { 'ctrl+b', function() prev_char() end },
+ { 'right', function() next_char() end },
+ { 'ctrl+f', function() next_char() end },
+ { 'up', function() move_history(-1) end },
+ { 'ctrl+p', function() move_history(-1) end },
+ { 'wheel_up', function() move_history(-1) end },
+ { 'down', function() move_history(1) end },
+ { 'ctrl+n', function() move_history(1) end },
+ { 'wheel_down', function() move_history(1) end },
+ { 'wheel_left', function() end },
+ { 'wheel_right', function() end },
+ { 'ctrl+left', prev_word },
+ { 'alt+b', prev_word },
+ { 'ctrl+right', next_word },
+ { 'alt+f', next_word },
+ { 'tab', complete },
+ { 'ctrl+i', complete },
+ { 'ctrl+a', go_home },
+ { 'home', go_home },
+ { 'ctrl+e', go_end },
+ { 'end', go_end },
+ { 'pgup', handle_pgup },
+ { 'pgdwn', handle_pgdown },
+ { 'ctrl+c', clear },
+ { 'ctrl+d', maybe_exit },
+ { 'ctrl+k', del_to_eol },
+ { 'ctrl+l', clear_log_buffer },
+ { 'ctrl+u', del_to_start },
+ { 'ctrl+v', function() paste(true) end },
+ { 'meta+v', function() paste(true) end },
+ { 'ctrl+bs', del_word },
+ { 'ctrl+w', del_word },
+ { 'ctrl+del', del_next_word },
+ { 'alt+d', del_next_word },
+ { 'kp_dec', function() handle_char_input('.') end },
+ }
+
+ for i = 0, 9 do
+ bindings[#bindings + 1] =
+ {'kp' .. i, function() handle_char_input('' .. i) end}
+ end
+
+ return bindings
+end
+
+local function text_input(info)
+ if info.key_text and (info.event == "press" or info.event == "down"
+ or info.event == "repeat")
+ then
+ handle_char_input(info.key_text)
+ end
+end
+
+function define_key_bindings()
+ if #key_bindings > 0 then
+ return
+ end
+ for _, bind in ipairs(get_bindings()) do
+ -- Generate arbitrary name for removing the bindings later.
+ local name = "_console_" .. (#key_bindings + 1)
+ key_bindings[#key_bindings + 1] = name
+ mp.add_forced_key_binding(bind[1], name, bind[2], {repeatable = true})
+ end
+ mp.add_forced_key_binding("any_unicode", "_console_text", text_input,
+ {repeatable = true, complex = true})
+ key_bindings[#key_bindings + 1] = "_console_text"
+end
+
+function undefine_key_bindings()
+ for _, name in ipairs(key_bindings) do
+ mp.remove_key_binding(name)
+ end
+ key_bindings = {}
+end
+
+-- Add a global binding for enabling the REPL. While it's enabled, its bindings
+-- will take over and it can be closed with ESC.
+mp.add_key_binding(nil, 'enable', function()
+ set_active(true)
+end)
+
+-- Add a script-message to show the REPL and fill it with the provided text
+mp.register_script_message('type', function(text, cursor_pos)
+ show_and_type(text, cursor_pos)
+end)
+
+-- Redraw the REPL when the OSD size changes. This is needed because the
+-- PlayRes of the OSD will need to be adjusted.
+mp.observe_property('osd-width', 'native', update)
+mp.observe_property('osd-height', 'native', update)
+mp.observe_property('display-hidpi-scale', 'native', update)
+
+-- Enable log messages. In silent mode, mpv will queue log messages in a buffer
+-- until enable_messages is called again without the silent: prefix.
+mp.enable_messages('silent:terminal-default')
+
+mp.register_event('log-message', function(e)
+ -- Ignore log messages from the OSD because of paranoia, since writing them
+ -- to the OSD could generate more messages in an infinite loop.
+ if e.prefix:sub(1, 3) == 'osd' then return end
+
+ -- Ignore messages output by this script.
+ if e.prefix == mp.get_script_name() then return end
+
+ -- Ignore buffer overflow warning messages. Overflowed log messages would
+ -- have been offscreen anyway.
+ if e.prefix == 'overflow' then return end
+
+ -- Filter out trace-level log messages, even if the terminal-default log
+ -- level includes them. These aren't too useful for an on-screen display
+ -- without scrollback and they include messages that are generated from the
+ -- OSD display itself.
+ if e.level == 'trace' then return end
+
+ -- Use color for debug/v/warn/error/fatal messages.
+ local style = ''
+ if e.level == 'debug' then
+ style = styles.debug
+ elseif e.level == 'v' then
+ style = styles.verbose
+ elseif e.level == 'warn' then
+ style = styles.warn
+ elseif e.level == 'error' then
+ style = styles.error
+ elseif e.level == 'fatal' then
+ style = styles.fatal
+ end
+
+ log_add(style, '[' .. e.prefix .. '] ' .. e.text)
+end)
+
+collectgarbage()
diff --git a/player/lua/defaults.lua b/player/lua/defaults.lua
new file mode 100644
index 0000000..233d1d6
--- /dev/null
+++ b/player/lua/defaults.lua
@@ -0,0 +1,836 @@
+-- Compatibility shim for lua 5.2/5.3
+unpack = unpack or table.unpack
+
+-- these are used internally by lua.c
+mp.UNKNOWN_TYPE.info = "this value is inserted if the C type is not supported"
+mp.UNKNOWN_TYPE.type = "UNKNOWN_TYPE"
+
+mp.ARRAY.info = "native array"
+mp.ARRAY.type = "ARRAY"
+
+mp.MAP.info = "native map"
+mp.MAP.type = "MAP"
+
+function mp.get_script_name()
+ return mp.script_name
+end
+
+function mp.get_opt(key, def)
+ local opts = mp.get_property_native("options/script-opts")
+ local val = opts[key]
+ if val == nil then
+ val = def
+ end
+ return val
+end
+
+function mp.input_define_section(section, contents, flags)
+ if flags == nil or flags == "" then
+ flags = "default"
+ end
+ mp.commandv("define-section", section, contents, flags)
+end
+
+function mp.input_enable_section(section, flags)
+ if flags == nil then
+ flags = ""
+ end
+ mp.commandv("enable-section", section, flags)
+end
+
+function mp.input_disable_section(section)
+ mp.commandv("disable-section", section)
+end
+
+function mp.get_mouse_pos()
+ local m = mp.get_property_native("mouse-pos")
+ return m.x, m.y
+end
+
+-- For dispatching script-binding. This is sent as:
+-- script-message-to $script_name $binding_name $keystate
+-- The array is indexed by $binding_name, and has functions like this as value:
+-- fn($binding_name, $keystate)
+local dispatch_key_bindings = {}
+
+local message_id = 0
+local function reserve_binding()
+ message_id = message_id + 1
+ return "__keybinding" .. tostring(message_id)
+end
+
+local function dispatch_key_binding(name, state, key_name, key_text)
+ local fn = dispatch_key_bindings[name]
+ if fn then
+ fn(name, state, key_name, key_text)
+ end
+end
+
+-- "Old", deprecated API
+
+-- each script has its own section, so that they don't conflict
+local default_section = "input_dispatch_" .. mp.script_name
+
+-- Set the list of key bindings. These will override the user's bindings, so
+-- you should use this sparingly.
+-- A call to this function will remove all bindings previously set with this
+-- function. For example, set_key_bindings({}) would remove all script defined
+-- key bindings.
+-- Note: the bindings are not active by default. Use enable_key_bindings().
+--
+-- list is an array of key bindings, where each entry is an array as follow:
+-- {key, callback_press, callback_down, callback_up}
+-- key is the key string as used in input.conf, like "ctrl+a"
+--
+-- callback can be a string too, in which case the following will be added like
+-- an input.conf line: key .. " " .. callback
+-- (And callback_down is ignored.)
+function mp.set_key_bindings(list, section, flags)
+ local cfg = ""
+ for i = 1, #list do
+ local entry = list[i]
+ local key = entry[1]
+ local cb = entry[2]
+ local cb_down = entry[3]
+ local cb_up = entry[4]
+ if type(cb) ~= "string" then
+ local mangle = reserve_binding()
+ dispatch_key_bindings[mangle] = function(name, state)
+ local event = state:sub(1, 1)
+ local is_mouse = state:sub(2, 2) == "m"
+ local def = (is_mouse and "u") or "d"
+ if event == "r" then
+ return
+ end
+ if event == "p" and cb then
+ cb()
+ elseif event == "d" and cb_down then
+ cb_down()
+ elseif event == "u" and cb_up then
+ cb_up()
+ elseif event == def and cb then
+ cb()
+ end
+ end
+ cfg = cfg .. key .. " script-binding " ..
+ mp.script_name .. "/" .. mangle .. "\n"
+ else
+ cfg = cfg .. key .. " " .. cb .. "\n"
+ end
+ end
+ mp.input_define_section(section or default_section, cfg, flags)
+end
+
+function mp.enable_key_bindings(section, flags)
+ mp.input_enable_section(section or default_section, flags)
+end
+
+function mp.disable_key_bindings(section)
+ mp.input_disable_section(section or default_section)
+end
+
+function mp.set_mouse_area(x0, y0, x1, y1, section)
+ mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1)
+end
+
+-- "Newer" and more convenient API
+
+local key_bindings = {}
+local key_binding_counter = 0
+local key_bindings_dirty = false
+
+function mp.flush_keybindings()
+ if not key_bindings_dirty then
+ return
+ end
+ key_bindings_dirty = false
+
+ for i = 1, 2 do
+ local section, flags
+ local def = i == 1
+ if def then
+ section = "input_" .. mp.script_name
+ flags = "default"
+ else
+ section = "input_forced_" .. mp.script_name
+ flags = "force"
+ end
+ local bindings = {}
+ for k, v in pairs(key_bindings) do
+ if v.bind and v.forced ~= def then
+ bindings[#bindings + 1] = v
+ end
+ end
+ table.sort(bindings, function(a, b)
+ return a.priority < b.priority
+ end)
+ local cfg = ""
+ for _, v in ipairs(bindings) do
+ cfg = cfg .. v.bind .. "\n"
+ end
+ mp.input_define_section(section, cfg, flags)
+ -- TODO: remove the section if the script is stopped
+ mp.input_enable_section(section, "allow-hide-cursor+allow-vo-dragging")
+ end
+end
+
+local function add_binding(attrs, key, name, fn, rp)
+ if type(name) ~= "string" and name ~= nil then
+ rp = fn
+ fn = name
+ name = nil
+ end
+ rp = rp or ""
+ if name == nil then
+ name = reserve_binding()
+ end
+ local repeatable = rp == "repeatable" or rp["repeatable"]
+ if rp["forced"] then
+ attrs.forced = true
+ end
+ local key_cb, msg_cb
+ if not fn then
+ fn = function() end
+ end
+ if rp["complex"] then
+ local key_states = {
+ ["u"] = "up",
+ ["d"] = "down",
+ ["r"] = "repeat",
+ ["p"] = "press",
+ }
+ key_cb = function(name, state, key_name, key_text)
+ if key_text == "" then
+ key_text = nil
+ end
+ fn({
+ event = key_states[state:sub(1, 1)] or "unknown",
+ is_mouse = state:sub(2, 2) == "m",
+ key_name = key_name,
+ key_text = key_text,
+ })
+ end
+ msg_cb = function()
+ fn({event = "press", is_mouse = false})
+ end
+ else
+ key_cb = function(name, state)
+ -- Emulate the same semantics as input.c uses for most bindings:
+ -- For keyboard, "down" runs the command, "up" does nothing;
+ -- for mouse, "down" does nothing, "up" runs the command.
+ -- Also, key repeat triggers the binding again.
+ local event = state:sub(1, 1)
+ local is_mouse = state:sub(2, 2) == "m"
+ if event == "r" and not repeatable then
+ return
+ end
+ if is_mouse and (event == "u" or event == "p") then
+ fn()
+ elseif not is_mouse and (event == "d" or event == "r" or event == "p") then
+ fn()
+ end
+ end
+ msg_cb = fn
+ end
+ if key and #key > 0 then
+ attrs.bind = key .. " script-binding " .. mp.script_name .. "/" .. name
+ end
+ attrs.name = name
+ -- new bindings override old ones (but do not overwrite them)
+ key_binding_counter = key_binding_counter + 1
+ attrs.priority = key_binding_counter
+ key_bindings[name] = attrs
+ key_bindings_dirty = true
+ dispatch_key_bindings[name] = key_cb
+ mp.register_script_message(name, msg_cb)
+end
+
+function mp.add_key_binding(...)
+ add_binding({forced=false}, ...)
+end
+
+function mp.add_forced_key_binding(...)
+ add_binding({forced=true}, ...)
+end
+
+function mp.remove_key_binding(name)
+ key_bindings[name] = nil
+ dispatch_key_bindings[name] = nil
+ key_bindings_dirty = true
+ mp.unregister_script_message(name)
+end
+
+local timers = {}
+
+local timer_mt = {}
+timer_mt.__index = timer_mt
+
+function mp.add_timeout(seconds, cb, disabled)
+ local t = mp.add_periodic_timer(seconds, cb, disabled)
+ t.oneshot = true
+ return t
+end
+
+function mp.add_periodic_timer(seconds, cb, disabled)
+ local t = {
+ timeout = seconds,
+ cb = cb,
+ oneshot = false,
+ }
+ setmetatable(t, timer_mt)
+ if not disabled then
+ t:resume()
+ end
+ return t
+end
+
+function timer_mt.stop(t)
+ if timers[t] then
+ timers[t] = nil
+ t.next_deadline = t.next_deadline - mp.get_time()
+ end
+end
+
+function timer_mt.kill(t)
+ timers[t] = nil
+ t.next_deadline = nil
+end
+mp.cancel_timer = timer_mt.kill
+
+function timer_mt.resume(t)
+ if not timers[t] then
+ local timeout = t.next_deadline
+ if timeout == nil then
+ timeout = t.timeout
+ end
+ t.next_deadline = mp.get_time() + timeout
+ timers[t] = t
+ end
+end
+
+function timer_mt.is_enabled(t)
+ return timers[t] ~= nil
+end
+
+-- Return the timer that expires next.
+local function get_next_timer()
+ local best = nil
+ for t, _ in pairs(timers) do
+ if best == nil or t.next_deadline < best.next_deadline then
+ best = t
+ end
+ end
+ return best
+end
+
+function mp.get_next_timeout()
+ local timer = get_next_timer()
+ if not timer then
+ return
+ end
+ local now = mp.get_time()
+ return timer.next_deadline - now
+end
+
+-- Run timers that have met their deadline at the time of invocation.
+-- Return: time>0 in seconds till the next due timer, 0 if there are due timers
+-- (aborted to avoid infinite loop), or nil if no timers
+local function process_timers()
+ local t0 = nil
+ while true do
+ local timer = get_next_timer()
+ if not timer then
+ return
+ end
+ local now = mp.get_time()
+ local wait = timer.next_deadline - now
+ if wait > 0 then
+ return wait
+ else
+ if not t0 then
+ t0 = now -- first due callback: always executes, remember t0
+ elseif timer.next_deadline > t0 then
+ -- don't block forever with slow callbacks and endless timers.
+ -- we'll continue right after checking mpv events.
+ return 0
+ end
+
+ if timer.oneshot then
+ timer:kill()
+ else
+ timer.next_deadline = now + timer.timeout
+ end
+ timer.cb()
+ end
+ end
+end
+
+local messages = {}
+
+function mp.register_script_message(name, fn)
+ messages[name] = fn
+end
+
+function mp.unregister_script_message(name)
+ messages[name] = nil
+end
+
+local function message_dispatch(ev)
+ if #ev.args > 0 then
+ local handler = messages[ev.args[1]]
+ if handler then
+ handler(unpack(ev.args, 2))
+ end
+ end
+end
+
+local property_id = 0
+local properties = {}
+
+function mp.observe_property(name, t, cb)
+ local id = property_id + 1
+ property_id = id
+ properties[id] = cb
+ mp.raw_observe_property(id, name, t)
+end
+
+function mp.unobserve_property(cb)
+ for prop_id, prop_cb in pairs(properties) do
+ if cb == prop_cb then
+ properties[prop_id] = nil
+ mp.raw_unobserve_property(prop_id)
+ end
+ end
+end
+
+local function property_change(ev)
+ local prop = properties[ev.id]
+ if prop then
+ prop(ev.name, ev.data)
+ end
+end
+
+-- used by default event loop (mp_event_loop()) to decide when to quit
+mp.keep_running = true
+
+local event_handlers = {}
+
+function mp.register_event(name, cb)
+ local list = event_handlers[name]
+ if not list then
+ list = {}
+ event_handlers[name] = list
+ end
+ list[#list + 1] = cb
+ return mp.request_event(name, true)
+end
+
+function mp.unregister_event(cb)
+ for name, sub in pairs(event_handlers) do
+ local found = false
+ for i, e in ipairs(sub) do
+ if e == cb then
+ found = true
+ break
+ end
+ end
+ if found then
+ -- create a new array, just in case this function was called
+ -- from an event handler
+ local new = {}
+ for i = 1, #sub do
+ if sub[i] ~= cb then
+ new[#new + 1] = sub[i]
+ end
+ end
+ event_handlers[name] = new
+ if #new == 0 then
+ mp.request_event(name, false)
+ end
+ end
+ end
+end
+
+-- default handlers
+mp.register_event("shutdown", function() mp.keep_running = false end)
+mp.register_event("client-message", message_dispatch)
+mp.register_event("property-change", property_change)
+
+-- called before the event loop goes back to sleep
+local idle_handlers = {}
+
+function mp.register_idle(cb)
+ idle_handlers[#idle_handlers + 1] = cb
+end
+
+function mp.unregister_idle(cb)
+ local new = {}
+ for _, handler in ipairs(idle_handlers) do
+ if handler ~= cb then
+ new[#new + 1] = handler
+ end
+ end
+ idle_handlers = new
+end
+
+-- sent by "script-binding"
+mp.register_script_message("key-binding", dispatch_key_binding)
+
+mp.msg = {
+ log = mp.log,
+ fatal = function(...) return mp.log("fatal", ...) end,
+ error = function(...) return mp.log("error", ...) end,
+ warn = function(...) return mp.log("warn", ...) end,
+ info = function(...) return mp.log("info", ...) end,
+ verbose = function(...) return mp.log("v", ...) end,
+ debug = function(...) return mp.log("debug", ...) end,
+ trace = function(...) return mp.log("trace", ...) end,
+}
+
+_G.print = mp.msg.info
+
+package.loaded["mp"] = mp
+package.loaded["mp.msg"] = mp.msg
+
+function mp.wait_event(t)
+ local r = mp.raw_wait_event(t)
+ if r and r.file_error and not r.error then
+ -- compat; deprecated
+ r.error = r.file_error
+ end
+ return r
+end
+
+_G.mp_event_loop = function()
+ mp.dispatch_events(true)
+end
+
+local function call_event_handlers(e)
+ local handlers = event_handlers[e.event]
+ if handlers then
+ for _, handler in ipairs(handlers) do
+ handler(e)
+ end
+ end
+end
+
+mp.use_suspend = false
+
+local suspend_warned = false
+
+function mp.dispatch_events(allow_wait)
+ local more_events = true
+ if mp.use_suspend then
+ if not suspend_warned then
+ mp.msg.error("mp.use_suspend is now ignored.")
+ suspend_warned = true
+ end
+ end
+ while mp.keep_running do
+ local wait = 0
+ if not more_events then
+ wait = process_timers() or 1e20 -- infinity for all practical purposes
+ if wait ~= 0 then
+ local idle_called = nil
+ for _, handler in ipairs(idle_handlers) do
+ idle_called = true
+ handler()
+ end
+ if idle_called then
+ -- handlers don't complete in 0 time, and may modify timers
+ wait = mp.get_next_timeout() or 1e20
+ if wait < 0 then
+ wait = 0
+ end
+ end
+ end
+ if allow_wait ~= true then
+ return
+ end
+ end
+ local e = mp.wait_event(wait)
+ more_events = false
+ if e.event ~= "none" then
+ call_event_handlers(e)
+ more_events = true
+ end
+ end
+end
+
+mp.register_idle(mp.flush_keybindings)
+
+-- additional helpers
+
+function mp.osd_message(text, duration)
+ if not duration then
+ duration = "-1"
+ else
+ duration = tostring(math.floor(duration * 1000))
+ end
+ mp.commandv("show-text", text, duration)
+end
+
+local hook_table = {}
+
+local hook_mt = {}
+hook_mt.__index = hook_mt
+
+function hook_mt.cont(t)
+ if t._id == nil then
+ mp.msg.error("hook already continued")
+ else
+ mp.raw_hook_continue(t._id)
+ t._id = nil
+ end
+end
+
+function hook_mt.defer(t)
+ t._defer = true
+end
+
+mp.register_event("hook", function(ev)
+ local fn = hook_table[tonumber(ev.id)]
+ local hookobj = {
+ _id = ev.hook_id,
+ _defer = false,
+ }
+ setmetatable(hookobj, hook_mt)
+ if fn then
+ fn(hookobj)
+ end
+ if not hookobj._defer and hookobj._id ~= nil then
+ hookobj:cont()
+ end
+end)
+
+function mp.add_hook(name, pri, cb)
+ local id = #hook_table + 1
+ hook_table[id] = cb
+ -- The C API suggests using 0 for a neutral priority, but lua.rst suggests
+ -- 50 (?), so whatever.
+ mp.raw_hook_add(id, name, pri - 50)
+end
+
+local async_call_table = {}
+local async_next_id = 1
+
+function mp.command_native_async(node, cb)
+ local id = async_next_id
+ async_next_id = async_next_id + 1
+ cb = cb or function() end
+ local res, err = mp.raw_command_native_async(id, node)
+ if not res then
+ mp.add_timeout(0, function() cb(false, nil, err) end)
+ return res, err
+ end
+ local t = {cb = cb, id = id}
+ async_call_table[id] = t
+ return t
+end
+
+mp.register_event("command-reply", function(ev)
+ local id = tonumber(ev.id)
+ local t = async_call_table[id]
+ local cb = t.cb
+ t.id = nil
+ async_call_table[id] = nil
+ if ev.error then
+ cb(false, nil, ev.error)
+ else
+ cb(true, ev.result, nil)
+ end
+end)
+
+function mp.abort_async_command(t)
+ if t.id ~= nil then
+ mp.raw_abort_async_command(t.id)
+ end
+end
+
+local overlay_mt = {}
+overlay_mt.__index = overlay_mt
+local overlay_new_id = 0
+
+function mp.create_osd_overlay(format)
+ overlay_new_id = overlay_new_id + 1
+ local overlay = {
+ format = format,
+ id = overlay_new_id,
+ data = "",
+ res_x = 0,
+ res_y = 720,
+ }
+ setmetatable(overlay, overlay_mt)
+ return overlay
+end
+
+function overlay_mt.update(ov)
+ local cmd = {}
+ for k, v in pairs(ov) do
+ cmd[k] = v
+ end
+ cmd.name = "osd-overlay"
+ cmd.res_x = math.floor(cmd.res_x)
+ cmd.res_y = math.floor(cmd.res_y)
+ return mp.command_native(cmd)
+end
+
+function overlay_mt.remove(ov)
+ mp.command_native {
+ name = "osd-overlay",
+ id = ov.id,
+ format = "none",
+ data = "",
+ }
+end
+
+-- legacy API
+function mp.set_osd_ass(res_x, res_y, data)
+ if not mp._legacy_overlay then
+ mp._legacy_overlay = mp.create_osd_overlay("ass-events")
+ end
+ if mp._legacy_overlay.res_x ~= res_x or
+ mp._legacy_overlay.res_y ~= res_y or
+ mp._legacy_overlay.data ~= data
+ then
+ mp._legacy_overlay.res_x = res_x
+ mp._legacy_overlay.res_y = res_y
+ mp._legacy_overlay.data = data
+ mp._legacy_overlay:update()
+ end
+end
+
+function mp.get_osd_size()
+ local prop = mp.get_property_native("osd-dimensions")
+ return prop.w, prop.h, prop.aspect
+end
+
+function mp.get_osd_margins()
+ local prop = mp.get_property_native("osd-dimensions")
+ return prop.ml, prop.mt, prop.mr, prop.mb
+end
+
+local mp_utils = package.loaded["mp.utils"]
+
+function mp_utils.format_table(t, set)
+ if not set then
+ set = { [t] = true }
+ end
+ local res = "{"
+ -- pretty expensive but simple way to distinguish array and map parts of t
+ local keys = {}
+ local vals = {}
+ local arr = 0
+ for i = 1, #t do
+ if t[i] == nil then
+ break
+ end
+ keys[i] = i
+ vals[i] = t[i]
+ arr = i
+ end
+ for k, v in pairs(t) do
+ if not (type(k) == "number" and k >= 1 and k <= arr and keys[k]) then
+ keys[#keys + 1] = k
+ vals[#keys] = v
+ end
+ end
+ for i = 1, #keys do
+ if #res > 1 then
+ res = res .. ", "
+ end
+ if i > arr then
+ res = res .. mp_utils.to_string(keys[i], set) .. " = "
+ end
+ res = res .. mp_utils.to_string(vals[i], set)
+ end
+ res = res .. "}"
+ return res
+end
+
+function mp_utils.to_string(v, set)
+ if type(v) == "string" then
+ return "\"" .. v .. "\""
+ elseif type(v) == "table" then
+ if set then
+ if set[v] then
+ return "[cycle]"
+ end
+ set[v] = true
+ end
+ return mp_utils.format_table(v, set)
+ else
+ return tostring(v)
+ end
+end
+
+function mp_utils.getcwd()
+ return mp.get_property("working-directory")
+end
+
+function mp_utils.getpid()
+ return mp.get_property_number("pid")
+end
+
+function mp_utils.format_bytes_humanized(b)
+ local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"}
+ local i = 1
+ while b >= 1024 do
+ b = b / 1024
+ i = i + 1
+ end
+ return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1))
+end
+
+function mp_utils.subprocess(t)
+ local cmd = {}
+ cmd.name = "subprocess"
+ cmd.capture_stdout = true
+ for k, v in pairs(t) do
+ if k == "cancellable" then
+ k = "playback_only"
+ elseif k == "max_size" then
+ k = "capture_size"
+ end
+ cmd[k] = v
+ end
+ local res, err = mp.command_native(cmd)
+ if res == nil then
+ -- an error usually happens only if parsing failed (or no args passed)
+ res = {error_string = err, status = -1}
+ end
+ if res.error_string ~= "" then
+ res.error = res.error_string
+ end
+ return res
+end
+
+function mp_utils.subprocess_detached(t)
+ mp.commandv("run", unpack(t.args))
+end
+
+function mp_utils.shared_script_property_set(name, value)
+ if value ~= nil then
+ -- no such thing as change-list with mpv_node, so build a string value
+ mp.commandv("change-list", "shared-script-properties", "append",
+ name .. "=" .. value)
+ else
+ mp.commandv("change-list", "shared-script-properties", "remove", name)
+ end
+end
+
+function mp_utils.shared_script_property_get(name)
+ local map = mp.get_property_native("shared-script-properties")
+ return map and map[name]
+end
+
+-- cb(name, value) on change and on init
+function mp_utils.shared_script_property_observe(name, cb)
+ -- it's _very_ wasteful to observe the mpv core "super" property for every
+ -- shared sub-property, but then again you shouldn't use this
+ mp.observe_property("shared-script-properties", "native", function(_, val)
+ cb(name, val and val[name])
+ end)
+end
+
+return {}
diff --git a/player/lua/meson.build b/player/lua/meson.build
new file mode 100644
index 0000000..362c87c
--- /dev/null
+++ b/player/lua/meson.build
@@ -0,0 +1,10 @@
+lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua',
+ 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua']
+foreach file: lua_files
+ lua_file = custom_target(file,
+ input: join_paths(source_root, 'player', 'lua', file),
+ output: file + '.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+ )
+ sources += lua_file
+endforeach
diff --git a/player/lua/options.lua b/player/lua/options.lua
new file mode 100644
index 0000000..b05b734
--- /dev/null
+++ b/player/lua/options.lua
@@ -0,0 +1,164 @@
+local msg = require 'mp.msg'
+
+-- converts val to type of desttypeval
+local function typeconv(desttypeval, val)
+ if type(desttypeval) == "boolean" then
+ if val == "yes" then
+ val = true
+ elseif val == "no" then
+ val = false
+ else
+ msg.error("Error: Can't convert '" .. val .. "' to boolean!")
+ val = nil
+ end
+ elseif type(desttypeval) == "number" then
+ if tonumber(val) ~= nil then
+ val = tonumber(val)
+ else
+ msg.error("Error: Can't convert '" .. val .. "' to number!")
+ val = nil
+ end
+ end
+ return val
+end
+
+-- performs a deep-copy of the given option value
+local function opt_copy(val)
+ return val -- no tables currently
+end
+
+-- compares the given option values for equality
+local function opt_equal(val1, val2)
+ return val1 == val2
+end
+
+-- performs a deep-copy of an entire option table
+local function opt_table_copy(opts)
+ local copy = {}
+ for key, value in pairs(opts) do
+ copy[key] = opt_copy(value)
+ end
+ return copy
+end
+
+
+local function read_options(options, identifier, on_update)
+ local option_types = opt_table_copy(options)
+ if identifier == nil then
+ identifier = mp.get_script_name()
+ end
+ msg.debug("reading options for " .. identifier)
+
+ -- read config file
+ local conffilename = "script-opts/" .. identifier .. ".conf"
+ local conffile = mp.find_config_file(conffilename)
+ if conffile == nil then
+ msg.debug(conffilename .. " not found.")
+ conffilename = "lua-settings/" .. identifier .. ".conf"
+ conffile = mp.find_config_file(conffilename)
+ if conffile then
+ msg.warn("lua-settings/ is deprecated, use directory script-opts/")
+ end
+ end
+ local f = conffile and io.open(conffile,"r")
+ if f == nil then
+ -- config not found
+ msg.debug(conffilename .. " not found.")
+ else
+ -- config exists, read values
+ msg.verbose("Opened config file " .. conffilename .. ".")
+ local linecounter = 1
+ for line in f:lines() do
+ if line:sub(#line) == "\r" then
+ line = line:sub(1, #line - 1)
+ end
+ if string.find(line, "#") == 1 then
+
+ else
+ local eqpos = string.find(line, "=")
+ if eqpos == nil then
+
+ else
+ local key = string.sub(line, 1, eqpos-1)
+ local val = string.sub(line, eqpos+1)
+
+ -- match found values with defaults
+ if option_types[key] == nil then
+ msg.warn(conffilename..":"..linecounter..
+ " unknown key '" .. key .. "', ignoring")
+ else
+ local convval = typeconv(option_types[key], val)
+ if convval == nil then
+ msg.error(conffilename..":"..linecounter..
+ " error converting value '" .. val ..
+ "' for key '" .. key .. "'")
+ else
+ options[key] = convval
+ end
+ end
+ end
+ end
+ linecounter = linecounter + 1
+ end
+ io.close(f)
+ end
+
+ --parse command-line options
+ local prefix = identifier.."-"
+ -- command line options are always applied on top of these
+ local conf_and_default_opts = opt_table_copy(options)
+
+ local function parse_opts(full, options)
+ for key, val in pairs(full) do
+ if string.find(key, prefix, 1, true) == 1 then
+ key = string.sub(key, string.len(prefix)+1)
+
+ -- match found values with defaults
+ if option_types[key] == nil then
+ msg.warn("script-opts: unknown key " .. key .. ", ignoring")
+ else
+ local convval = typeconv(option_types[key], val)
+ if convval == nil then
+ msg.error("script-opts: error converting value '" .. val ..
+ "' for key '" .. key .. "'")
+ else
+ options[key] = convval
+ end
+ end
+ end
+ end
+ end
+
+ --initial
+ parse_opts(mp.get_property_native("options/script-opts"), options)
+
+ --runtime updates
+ if on_update then
+ local last_opts = opt_table_copy(options)
+
+ mp.observe_property("options/script-opts", "native", function(name, val)
+ local new_opts = opt_table_copy(conf_and_default_opts)
+ parse_opts(val, new_opts)
+ local changelist = {}
+ for key, val in pairs(new_opts) do
+ if not opt_equal(last_opts[key], val) then
+ -- copy to user
+ options[key] = opt_copy(val)
+ changelist[key] = true
+ end
+ end
+ last_opts = new_opts
+ if next(changelist) ~= nil then
+ on_update(changelist)
+ end
+ end)
+ end
+
+end
+
+-- backwards compatibility with broken read_options export
+_G.read_options = read_options
+
+return {
+ read_options = read_options,
+}
diff --git a/player/lua/osc.lua b/player/lua/osc.lua
new file mode 100644
index 0000000..45a5d90
--- /dev/null
+++ b/player/lua/osc.lua
@@ -0,0 +1,2917 @@
+local assdraw = require 'mp.assdraw'
+local msg = require 'mp.msg'
+local opt = require 'mp.options'
+local utils = require 'mp.utils'
+
+--
+-- Parameters
+--
+-- default user option values
+-- do not touch, change them in osc.conf
+local user_opts = {
+ showwindowed = true, -- show OSC when windowed?
+ showfullscreen = true, -- show OSC when fullscreen?
+ idlescreen = true, -- show mpv logo on idle
+ scalewindowed = 1, -- scaling of the controller when windowed
+ scalefullscreen = 1, -- scaling of the controller when fullscreen
+ scaleforcedwindow = 2, -- scaling when rendered on a forced window
+ vidscale = true, -- scale the controller with the video?
+ valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom)
+ halign = 0, -- horizontal alignment, -1 (left) to 1 (right)
+ barmargin = 0, -- vertical margin of top/bottombar
+ boxalpha = 80, -- alpha of the background box,
+ -- 0 (opaque) to 255 (fully transparent)
+ hidetimeout = 500, -- duration in ms until the OSC hides if no
+ -- mouse movement. enforced non-negative for the
+ -- user, but internally negative is "always-on".
+ fadeduration = 200, -- duration of fade out in ms, 0 = no fade
+ deadzonesize = 0.5, -- size of deadzone
+ minmousemove = 0, -- minimum amount of pixels the mouse has to
+ -- move between ticks to make the OSC show up
+ iamaprogrammer = false, -- use native mpv values and disable OSC
+ -- internal track list management (and some
+ -- functions that depend on it)
+ layout = "bottombar",
+ seekbarstyle = "bar", -- bar, diamond or knob
+ seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle
+ seekrangestyle = "inverted",-- bar, line, slider, inverted or none
+ seekrangeseparate = true, -- whether the seekranges overlay on the bar-style seekbar
+ seekrangealpha = 200, -- transparency of seekranges
+ seekbarkeyframes = true, -- use keyframes when dragging the seekbar
+ title = "${media-title}", -- string compatible with property-expansion
+ -- to be shown as OSC title
+ tooltipborder = 1, -- border of tooltip in bottom/topbar
+ timetotal = false, -- display total time instead of remaining time?
+ remaining_playtime = true, -- display the remaining time in playtime or video-time mode
+ -- playtime takes speed into account, whereas video-time doesn't
+ timems = false, -- display timecodes with milliseconds?
+ tcspace = 100, -- timecode spacing (compensate font size estimation)
+ visibility = "auto", -- only used at init to set visibility_mode(...)
+ boxmaxchars = 80, -- title crop threshold for box layout
+ boxvideo = false, -- apply osc_param.video_margins to video
+ windowcontrols = "auto", -- whether to show window controls
+ windowcontrols_alignment = "right", -- which side to show window controls on
+ greenandgrumpy = false, -- disable santa hat
+ livemarkers = true, -- update seekbar chapter markers on duration change
+ chapters_osd = true, -- whether to show chapters OSD on next/prev
+ playlist_osd = true, -- whether to show playlist OSD on next/prev
+ chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable
+ unicodeminus = false, -- whether to use the Unicode minus sign character
+}
+
+-- read options from config and command-line
+opt.read_options(user_opts, "osc", function(list) update_options(list) end)
+
+local osc_param = { -- calculated by osc_init()
+ playresy = 0, -- canvas size Y
+ playresx = 0, -- canvas size X
+ display_aspect = 1,
+ unscaled_y = 0,
+ areas = {},
+ video_margins = {
+ l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom
+ },
+}
+
+local osc_styles = {
+ bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
+ smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
+ smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}",
+ smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
+ topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
+
+ elementDown = "{\\1c&H999999}",
+ timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
+ vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
+ box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
+
+ topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
+ smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
+ timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
+ timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
+ vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
+
+ wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}",
+ wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}",
+ wcBar = "{\\1c&H000000}",
+}
+
+-- internal states, do not touch
+local state = {
+ showtime, -- time of last invocation (last mouse move)
+ osc_visible = false,
+ anistart, -- time when the animation started
+ anitype, -- current type of animation
+ animation, -- current animation alpha
+ mouse_down_counter = 0, -- used for softrepeat
+ active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
+ active_event_source = nil, -- the "button" that issued the current event
+ rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
+ tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
+ mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
+ initREQ = false, -- is a re-init request pending?
+ marginsREQ = false, -- is a margins update pending?
+ last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
+ mouse_in_window = false,
+ message_text,
+ message_hide_timer,
+ fullscreen = false,
+ tick_timer = nil,
+ tick_last_time = 0, -- when the last tick() was run
+ hide_timer = nil,
+ cache_state = nil,
+ idle = false,
+ enabled = true,
+ input_enabled = true,
+ showhide_enabled = false,
+ windowcontrols_buttons = false,
+ dmx_cache = 0,
+ using_video_margins = false,
+ border = true,
+ maximized = false,
+ osd = mp.create_osd_overlay("ass-events"),
+ chapter_list = {}, -- sorted by time
+}
+
+local window_control_box_width = 80
+local tick_delay = 0.03
+
+local is_december = os.date("*t").month == 12
+
+--
+-- Helperfunctions
+--
+
+function kill_animation()
+ state.anistart = nil
+ state.animation = nil
+ state.anitype = nil
+end
+
+function set_osd(res_x, res_y, text, z)
+ if state.osd.res_x == res_x and
+ state.osd.res_y == res_y and
+ state.osd.data == text then
+ return
+ end
+ state.osd.res_x = res_x
+ state.osd.res_y = res_y
+ state.osd.data = text
+ state.osd.z = z
+ state.osd:update()
+end
+
+local margins_opts = {
+ {"l", "video-margin-ratio-left"},
+ {"r", "video-margin-ratio-right"},
+ {"t", "video-margin-ratio-top"},
+ {"b", "video-margin-ratio-bottom"},
+}
+
+-- scale factor for translating between real and virtual ASS coordinates
+function get_virt_scale_factor()
+ local w, h = mp.get_osd_size()
+ if w <= 0 or h <= 0 then
+ return 0, 0
+ end
+ return osc_param.playresx / w, osc_param.playresy / h
+end
+
+-- return mouse position in virtual ASS coordinates (playresx/y)
+function get_virt_mouse_pos()
+ if state.mouse_in_window then
+ local sx, sy = get_virt_scale_factor()
+ local x, y = mp.get_mouse_pos()
+ return x * sx, y * sy
+ else
+ return -1, -1
+ end
+end
+
+function set_virt_mouse_area(x0, y0, x1, y1, name)
+ local sx, sy = get_virt_scale_factor()
+ mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
+end
+
+function scale_value(x0, x1, y0, y1, val)
+ local m = (y1 - y0) / (x1 - x0)
+ local b = y0 - (m * x0)
+ return (m * val) + b
+end
+
+-- returns hitbox spanning coordinates (top left, bottom right corner)
+-- according to alignment
+function get_hitbox_coords(x, y, an, w, h)
+
+ local alignments = {
+ [1] = function () return x, y-h, x+w, y end,
+ [2] = function () return x-(w/2), y-h, x+(w/2), y end,
+ [3] = function () return x-w, y-h, x, y end,
+
+ [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
+ [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
+ [6] = function () return x-w, y-(h/2), x, y+(h/2) end,
+
+ [7] = function () return x, y, x+w, y+h end,
+ [8] = function () return x-(w/2), y, x+(w/2), y+h end,
+ [9] = function () return x-w, y, x, y+h end,
+ }
+
+ return alignments[an]()
+end
+
+function get_hitbox_coords_geo(geometry)
+ return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
+ geometry.w, geometry.h)
+end
+
+function get_element_hitbox(element)
+ return element.hitbox.x1, element.hitbox.y1,
+ element.hitbox.x2, element.hitbox.y2
+end
+
+function mouse_hit(element)
+ return mouse_hit_coords(get_element_hitbox(element))
+end
+
+function mouse_hit_coords(bX1, bY1, bX2, bY2)
+ local mX, mY = get_virt_mouse_pos()
+ return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
+end
+
+function limit_range(min, max, val)
+ if val > max then
+ val = max
+ elseif val < min then
+ val = min
+ end
+ return val
+end
+
+-- translate value into element coordinates
+function get_slider_ele_pos_for(element, val)
+
+ local ele_pos = scale_value(
+ element.slider.min.value, element.slider.max.value,
+ element.slider.min.ele_pos, element.slider.max.ele_pos,
+ val)
+
+ return limit_range(
+ element.slider.min.ele_pos, element.slider.max.ele_pos,
+ ele_pos)
+end
+
+-- translates global (mouse) coordinates to value
+function get_slider_value_at(element, glob_pos)
+
+ local val = scale_value(
+ element.slider.min.glob_pos, element.slider.max.glob_pos,
+ element.slider.min.value, element.slider.max.value,
+ glob_pos)
+
+ return limit_range(
+ element.slider.min.value, element.slider.max.value,
+ val)
+end
+
+-- get value at current mouse position
+function get_slider_value(element)
+ return get_slider_value_at(element, get_virt_mouse_pos())
+end
+
+function countone(val)
+ if not user_opts.iamaprogrammer then
+ val = val + 1
+ end
+ return val
+end
+
+-- align: -1 .. +1
+-- frame: size of the containing area
+-- obj: size of the object that should be positioned inside the area
+-- margin: min. distance from object to frame (as long as -1 <= align <= +1)
+function get_align(align, frame, obj, margin)
+ return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
+end
+
+-- multiplies two alpha values, formular can probably be improved
+function mult_alpha(alphaA, alphaB)
+ return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
+end
+
+function add_area(name, x1, y1, x2, y2)
+ -- create area if needed
+ if osc_param.areas[name] == nil then
+ osc_param.areas[name] = {}
+ end
+ table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
+end
+
+function ass_append_alpha(ass, alpha, modifier)
+ local ar = {}
+
+ for ai, av in pairs(alpha) do
+ av = mult_alpha(av, modifier)
+ if state.animation then
+ av = mult_alpha(av, state.animation)
+ end
+ ar[ai] = av
+ end
+
+ ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
+ ar[1], ar[2], ar[3], ar[4]))
+end
+
+function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
+ if hexagon then
+ ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
+ else
+ ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
+ end
+end
+
+function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
+ if hexagon then
+ ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
+ else
+ ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
+ end
+end
+
+
+--
+-- Tracklist Management
+--
+
+local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
+
+-- updates the OSC internal playlists, should be run each time the track-layout changes
+function update_tracklist()
+ local tracktable = mp.get_property_native("track-list", {})
+
+ -- by osc_id
+ tracks_osc = {}
+ tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
+ -- by mpv_id
+ tracks_mpv = {}
+ tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
+ for n = 1, #tracktable do
+ if tracktable[n].type ~= "unknown" then
+ local type = tracktable[n].type
+ local mpv_id = tonumber(tracktable[n].id)
+
+ -- by osc_id
+ table.insert(tracks_osc[type], tracktable[n])
+
+ -- by mpv_id
+ tracks_mpv[type][mpv_id] = tracktable[n]
+ tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
+ end
+ end
+end
+
+-- return a nice list of tracks of the given type (video, audio, sub)
+function get_tracklist(type)
+ local msg = "Available " .. nicetypes[type] .. " Tracks: "
+ if not tracks_osc or #tracks_osc[type] == 0 then
+ msg = msg .. "none"
+ else
+ for n = 1, #tracks_osc[type] do
+ local track = tracks_osc[type][n]
+ local lang, title, selected = "unknown", "", "○"
+ if track.lang ~= nil then lang = track.lang end
+ if track.title ~= nil then title = track.title end
+ if track.id == tonumber(mp.get_property(type)) then
+ selected = "●"
+ end
+ msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title
+ end
+ end
+ return msg
+end
+
+-- relatively change the track of given <type> by <next> tracks
+ --(+1 -> next, -1 -> previous)
+function set_track(type, next)
+ local current_track_mpv, current_track_osc
+ if mp.get_property(type) == "no" then
+ current_track_osc = 0
+ else
+ current_track_mpv = tonumber(mp.get_property(type))
+ current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
+ end
+ local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
+ local new_track_mpv
+ if new_track_osc == 0 then
+ new_track_mpv = "no"
+ else
+ new_track_mpv = tracks_osc[type][new_track_osc].id
+ end
+
+ mp.commandv("set", type, new_track_mpv)
+
+ if new_track_osc == 0 then
+ show_message(nicetypes[type] .. " Track: none")
+ else
+ show_message(nicetypes[type] .. " Track: "
+ .. new_track_osc .. "/" .. #tracks_osc[type]
+ .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
+ .. (tracks_osc[type][new_track_osc].title or ""))
+ end
+end
+
+-- get the currently selected track of <type>, OSC-style counted
+function get_track(type)
+ local track = mp.get_property(type)
+ if track ~= "no" and track ~= nil then
+ local tr = tracks_mpv[type][tonumber(track)]
+ if tr then
+ return tr.osc_id
+ end
+ end
+ return 0
+end
+
+-- WindowControl helpers
+function window_controls_enabled()
+ val = user_opts.windowcontrols
+ if val == "auto" then
+ return not state.border
+ else
+ return val ~= "no"
+ end
+end
+
+function window_controls_alignment()
+ return user_opts.windowcontrols_alignment
+end
+
+--
+-- Element Management
+--
+
+local elements = {}
+
+function prepare_elements()
+
+ -- remove elements without layout or invisible
+ local elements2 = {}
+ for n, element in pairs(elements) do
+ if element.layout ~= nil and element.visible then
+ table.insert(elements2, element)
+ end
+ end
+ elements = elements2
+
+ function elem_compare (a, b)
+ return a.layout.layer < b.layout.layer
+ end
+
+ table.sort(elements, elem_compare)
+
+
+ for _,element in pairs(elements) do
+
+ local elem_geo = element.layout.geometry
+
+ -- Calculate the hitbox
+ local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
+ element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
+
+ local style_ass = assdraw.ass_new()
+
+ -- prepare static elements
+ style_ass:append("{}") -- hack to troll new_event into inserting a \n
+ style_ass:new_event()
+ style_ass:pos(elem_geo.x, elem_geo.y)
+ style_ass:an(elem_geo.an)
+ style_ass:append(element.layout.style)
+
+ element.style_ass = style_ass
+
+ local static_ass = assdraw.ass_new()
+
+
+ if element.type == "box" then
+ --draw box
+ static_ass:draw_start()
+ ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
+ element.layout.box.radius, element.layout.box.hexagon)
+ static_ass:draw_stop()
+
+ elseif element.type == "slider" then
+ --draw static slider parts
+
+ local r1 = 0
+ local r2 = 0
+ local slider_lo = element.layout.slider
+ -- offset between element outline and drag-area
+ local foV = slider_lo.border + slider_lo.gap
+
+ -- calculate positions of min and max points
+ if slider_lo.stype ~= "bar" then
+ r1 = elem_geo.h / 2
+ element.slider.min.ele_pos = elem_geo.h / 2
+ element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
+ if slider_lo.stype == "diamond" then
+ r2 = (elem_geo.h - 2 * slider_lo.border) / 2
+ elseif slider_lo.stype == "knob" then
+ r2 = r1
+ end
+ else
+ element.slider.min.ele_pos =
+ slider_lo.border + slider_lo.gap
+ element.slider.max.ele_pos =
+ elem_geo.w - (slider_lo.border + slider_lo.gap)
+ end
+
+ element.slider.min.glob_pos =
+ element.hitbox.x1 + element.slider.min.ele_pos
+ element.slider.max.glob_pos =
+ element.hitbox.x1 + element.slider.max.ele_pos
+
+ -- -- --
+
+ static_ass:draw_start()
+
+ -- the box
+ ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1, slider_lo.stype == "diamond")
+
+ -- the "hole"
+ ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border,
+ elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border,
+ r2, slider_lo.stype == "diamond")
+
+ -- marker nibbles
+ if element.slider.markerF ~= nil and slider_lo.gap > 0 then
+ local markers = element.slider.markerF()
+ for _,marker in pairs(markers) do
+ if marker > element.slider.min.value and
+ marker < element.slider.max.value then
+
+ local s = get_slider_ele_pos_for(element, marker)
+
+ if slider_lo.gap > 1 then -- draw triangles
+
+ local a = slider_lo.gap / 0.5 --0.866
+
+ --top
+ if slider_lo.nibbles_top then
+ static_ass:move_to(s - (a / 2), slider_lo.border)
+ static_ass:line_to(s + (a / 2), slider_lo.border)
+ static_ass:line_to(s, foV)
+ end
+
+ --bottom
+ if slider_lo.nibbles_bottom then
+ static_ass:move_to(s - (a / 2),
+ elem_geo.h - slider_lo.border)
+ static_ass:line_to(s,
+ elem_geo.h - foV)
+ static_ass:line_to(s + (a / 2),
+ elem_geo.h - slider_lo.border)
+ end
+
+ else -- draw 2x1px nibbles
+
+ --top
+ if slider_lo.nibbles_top then
+ static_ass:rect_cw(s - 1, slider_lo.border,
+ s + 1, slider_lo.border + slider_lo.gap);
+ end
+
+ --bottom
+ if slider_lo.nibbles_bottom then
+ static_ass:rect_cw(s - 1,
+ elem_geo.h -slider_lo.border -slider_lo.gap,
+ s + 1, elem_geo.h - slider_lo.border);
+ end
+ end
+ end
+ end
+ end
+ end
+
+ element.static_ass = static_ass
+
+
+ -- if the element is supposed to be disabled,
+ -- style it accordingly and kill the eventresponders
+ if not element.enabled then
+ element.layout.alpha[1] = 136
+ element.eventresponder = nil
+ end
+ end
+end
+
+
+--
+-- Element Rendering
+--
+
+-- returns nil or a chapter element from the native property chapter-list
+function get_chapter(possec)
+ local cl = state.chapter_list -- sorted, get latest before possec, if any
+
+ for n=#cl,1,-1 do
+ if possec >= cl[n].time then
+ return cl[n]
+ end
+ end
+end
+
+function render_elements(master_ass)
+
+ -- when the slider is dragged or hovered and we have a target chapter name
+ -- then we use it instead of the normal title. we calculate it before the
+ -- render iterations because the title may be rendered before the slider.
+ state.forced_title = nil
+ local se, ae = state.slider_element, elements[state.active_element]
+ if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then
+ local dur = mp.get_property_number("duration", 0)
+ if dur > 0 then
+ local possec = get_slider_value(se) * dur / 100 -- of mouse pos
+ local ch = get_chapter(possec)
+ if ch and ch.title and ch.title ~= "" then
+ state.forced_title = string.format(user_opts.chapter_fmt, ch.title)
+ end
+ end
+ end
+
+ for n=1, #elements do
+ local element = elements[n]
+
+ local style_ass = assdraw.ass_new()
+ style_ass:merge(element.style_ass)
+ ass_append_alpha(style_ass, element.layout.alpha, 0)
+
+ if element.eventresponder and (state.active_element == n) then
+
+ -- run render event functions
+ if element.eventresponder.render ~= nil then
+ element.eventresponder.render(element)
+ end
+
+ if mouse_hit(element) then
+ -- mouse down styling
+ if element.styledown then
+ style_ass:append(osc_styles.elementDown)
+ end
+
+ if element.softrepeat and state.mouse_down_counter >= 15
+ and state.mouse_down_counter % 5 == 0 then
+
+ element.eventresponder[state.active_event_source.."_down"](element)
+ end
+ state.mouse_down_counter = state.mouse_down_counter + 1
+ end
+
+ end
+
+ local elem_ass = assdraw.ass_new()
+
+ elem_ass:merge(style_ass)
+
+ if element.type ~= "button" then
+ elem_ass:merge(element.static_ass)
+ end
+
+ if element.type == "slider" then
+
+ local slider_lo = element.layout.slider
+ local elem_geo = element.layout.geometry
+ local s_min = element.slider.min.value
+ local s_max = element.slider.max.value
+
+ -- draw pos marker
+ local foH, xp
+ local pos = element.slider.posF()
+ local foV = slider_lo.border + slider_lo.gap
+ local innerH = elem_geo.h - (2 * foV)
+ local seekRanges = element.slider.seekRangesF()
+ local seekRangeLineHeight = innerH / 5
+
+ if slider_lo.stype ~= "bar" then
+ foH = elem_geo.h / 2
+ else
+ foH = slider_lo.border + slider_lo.gap
+ end
+
+ if pos then
+ xp = get_slider_ele_pos_for(element, pos)
+
+ if slider_lo.stype ~= "bar" then
+ local r = (user_opts.seekbarhandlesize * innerH) / 2
+ ass_draw_rr_h_cw(elem_ass, xp - r, foH - r,
+ xp + r, foH + r,
+ r, slider_lo.stype == "diamond")
+ else
+ local h = 0
+ if seekRanges and user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
+ h = seekRangeLineHeight
+ end
+ elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h)
+
+ if seekRanges and not user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
+ -- Punch holes for the seekRanges to be drawn later
+ for _,range in pairs(seekRanges) do
+ if range["start"] < pos then
+ local pstart = get_slider_ele_pos_for(element, range["start"])
+ local pend = xp
+
+ if pos > range["end"] then
+ pend = get_slider_ele_pos_for(element, range["end"])
+ end
+ elem_ass:rect_ccw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
+ end
+ end
+ end
+ end
+
+ if slider_lo.rtype == "slider" then
+ ass_draw_rr_h_cw(elem_ass, foH - innerH / 6, foH - innerH / 6,
+ xp, foH + innerH / 6,
+ innerH / 6, slider_lo.stype == "diamond", 0)
+ ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15,
+ elem_geo.w - foH + innerH / 15, foH + innerH / 15,
+ 0, slider_lo.stype == "diamond", innerH / 15)
+ for _,range in pairs(seekRanges or {}) do
+ local pstart = get_slider_ele_pos_for(element, range["start"])
+ local pend = get_slider_ele_pos_for(element, range["end"])
+ ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21,
+ pend, foH + innerH / 21,
+ innerH / 21, slider_lo.stype == "diamond")
+ end
+ end
+ end
+
+ if seekRanges then
+ if slider_lo.rtype ~= "inverted" then
+ elem_ass:draw_stop()
+ elem_ass:merge(element.style_ass)
+ ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
+ elem_ass:merge(element.static_ass)
+ end
+
+ for _,range in pairs(seekRanges) do
+ local pstart = get_slider_ele_pos_for(element, range["start"])
+ local pend = get_slider_ele_pos_for(element, range["end"])
+
+ if slider_lo.rtype == "slider" then
+ ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21,
+ pend, foH + innerH / 21,
+ innerH / 21, slider_lo.stype == "diamond")
+ elseif slider_lo.rtype == "line" then
+ if slider_lo.stype == "bar" then
+ elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
+ else
+ ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8, foH - innerH / 8,
+ pend + innerH / 8, foH + innerH / 8,
+ innerH / 8, slider_lo.stype == "diamond")
+ end
+ elseif slider_lo.rtype == "bar" then
+ if slider_lo.stype ~= "bar" then
+ ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV,
+ pend + innerH / 2, foV + innerH,
+ innerH / 2, slider_lo.stype == "diamond")
+ elseif range["end"] >= (pos or 0) then
+ elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV)
+ else
+ elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
+ end
+ elseif slider_lo.rtype == "inverted" then
+ if slider_lo.stype ~= "bar" then
+ ass_draw_rr_h_ccw(elem_ass, pstart, (elem_geo.h / 2) - 1, pend,
+ (elem_geo.h / 2) + 1,
+ 1, slider_lo.stype == "diamond")
+ else
+ elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1, pend, (elem_geo.h / 2) + 1)
+ end
+ end
+ end
+ end
+
+ elem_ass:draw_stop()
+
+ -- add tooltip
+ if element.slider.tooltipF ~= nil then
+ if mouse_hit(element) then
+ local sliderpos = get_slider_value(element)
+ local tooltiplabel = element.slider.tooltipF(sliderpos)
+
+ local an = slider_lo.tooltip_an
+
+ local ty
+
+ if an == 2 then
+ ty = element.hitbox.y1 - slider_lo.border
+ else
+ ty = element.hitbox.y1 + elem_geo.h / 2
+ end
+
+ local tx = get_virt_mouse_pos()
+ if slider_lo.adjust_tooltip then
+ if an == 2 then
+ if sliderpos < (s_min + 3) then
+ an = an - 1
+ elseif sliderpos > (s_max - 3) then
+ an = an + 1
+ end
+ elseif sliderpos > (s_max+s_min) / 2 then
+ an = an + 1
+ tx = tx - 5
+ else
+ an = an - 1
+ tx = tx + 10
+ end
+ end
+
+ -- tooltip label
+ elem_ass:new_event()
+ elem_ass:pos(tx, ty)
+ elem_ass:an(an)
+ elem_ass:append(slider_lo.tooltip_style)
+ ass_append_alpha(elem_ass, slider_lo.alpha, 0)
+ elem_ass:append(tooltiplabel)
+
+ end
+ end
+
+ elseif element.type == "button" then
+
+ local buttontext
+ if type(element.content) == "function" then
+ buttontext = element.content() -- function objects
+ elseif element.content ~= nil then
+ buttontext = element.content -- text objects
+ end
+
+ local maxchars = element.layout.button.maxchars
+ if maxchars ~= nil and #buttontext > maxchars then
+ local max_ratio = 1.25 -- up to 25% more chars while shrinking
+ local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
+ if #buttontext > limit then
+ while (#buttontext > limit) do
+ buttontext = buttontext:gsub(".[\128-\191]*$", "")
+ end
+ buttontext = buttontext .. "..."
+ end
+ local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
+ local stretch = (maxchars/#buttontext)*100
+ buttontext = string.format("{\\fscx%f}",
+ (maxchars/#buttontext)*100) .. buttontext
+ end
+
+ elem_ass:append(buttontext)
+ end
+
+ master_ass:merge(elem_ass)
+ end
+end
+
+--
+-- Message display
+--
+
+-- pos is 1 based
+function limited_list(prop, pos)
+ local proplist = mp.get_property_native(prop, {})
+ local count = #proplist
+ if count == 0 then
+ return count, proplist
+ end
+
+ local fs = tonumber(mp.get_property('options/osd-font-size'))
+ local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
+ if max % 2 == 0 then
+ max = max - 1
+ end
+ local delta = math.ceil(max / 2) - 1
+ local begi = math.max(math.min(pos - delta, count - max + 1), 1)
+ local endi = math.min(begi + max - 1, count)
+
+ local reslist = {}
+ for i=begi, endi do
+ local item = proplist[i]
+ item.current = (i == pos) and true or nil
+ table.insert(reslist, item)
+ end
+ return count, reslist
+end
+
+function get_playlist()
+ local pos = mp.get_property_number('playlist-pos', 0) + 1
+ local count, limlist = limited_list('playlist', pos)
+ if count == 0 then
+ return 'Empty playlist.'
+ end
+
+ local message = string.format('Playlist [%d/%d]:\n', pos, count)
+ for i, v in ipairs(limlist) do
+ local title = v.title
+ local _, filename = utils.split_path(v.filename)
+ if title == nil then
+ title = filename
+ end
+ message = string.format('%s %s %s\n', message,
+ (v.current and '●' or '○'), title)
+ end
+ return message
+end
+
+function get_chapterlist()
+ local pos = mp.get_property_number('chapter', 0) + 1
+ local count, limlist = limited_list('chapter-list', pos)
+ if count == 0 then
+ return 'No chapters.'
+ end
+
+ local message = string.format('Chapters [%d/%d]:\n', pos, count)
+ for i, v in ipairs(limlist) do
+ local time = mp.format_time(v.time)
+ local title = v.title
+ if title == nil then
+ title = string.format('Chapter %02d', i)
+ end
+ message = string.format('%s[%s] %s %s\n', message, time,
+ (v.current and '●' or '○'), title)
+ end
+ return message
+end
+
+function show_message(text, duration)
+
+ --print("text: "..text.." duration: " .. duration)
+ if duration == nil then
+ duration = tonumber(mp.get_property("options/osd-duration")) / 1000
+ elseif not type(duration) == "number" then
+ print("duration: " .. duration)
+ end
+
+ -- cut the text short, otherwise the following functions
+ -- may slow down massively on huge input
+ text = string.sub(text, 0, 4000)
+
+ -- replace actual linebreaks with ASS linebreaks
+ text = string.gsub(text, "\n", "\\N")
+
+ state.message_text = text
+
+ if not state.message_hide_timer then
+ state.message_hide_timer = mp.add_timeout(0, request_tick)
+ end
+ state.message_hide_timer:kill()
+ state.message_hide_timer.timeout = duration
+ state.message_hide_timer:resume()
+ request_tick()
+end
+
+function render_message(ass)
+ if state.message_hide_timer and state.message_hide_timer:is_enabled() and
+ state.message_text
+ then
+ local _, lines = string.gsub(state.message_text, "\\N", "")
+
+ local fontsize = tonumber(mp.get_property("options/osd-font-size"))
+ local outline = tonumber(mp.get_property("options/osd-border-size"))
+ local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
+ local counterscale = osc_param.playresy / osc_param.unscaled_y
+
+ fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
+ outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
+
+ local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
+
+
+ ass:new_event()
+ ass:append(style .. state.message_text)
+ else
+ state.message_text = nil
+ end
+end
+
+--
+-- Initialisation and Layout
+--
+
+function new_element(name, type)
+ elements[name] = {}
+ elements[name].type = type
+
+ -- add default stuff
+ elements[name].eventresponder = {}
+ elements[name].visible = true
+ elements[name].enabled = true
+ elements[name].softrepeat = false
+ elements[name].styledown = (type == "button")
+ elements[name].state = {}
+
+ if type == "slider" then
+ elements[name].slider = {min = {value = 0}, max = {value = 100}}
+ end
+
+
+ return elements[name]
+end
+
+function add_layout(name)
+ if elements[name] ~= nil then
+ -- new layout
+ elements[name].layout = {}
+
+ -- set layout defaults
+ elements[name].layout.layer = 50
+ elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
+
+ if elements[name].type == "button" then
+ elements[name].layout.button = {
+ maxchars = nil,
+ }
+ elseif elements[name].type == "slider" then
+ -- slider defaults
+ elements[name].layout.slider = {
+ border = 1,
+ gap = 1,
+ nibbles_top = true,
+ nibbles_bottom = true,
+ stype = "slider",
+ adjust_tooltip = true,
+ tooltip_style = "",
+ tooltip_an = 2,
+ alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
+ }
+ elseif elements[name].type == "box" then
+ elements[name].layout.box = {radius = 0, hexagon = false}
+ end
+
+ return elements[name].layout
+ else
+ msg.error("Can't add_layout to element \""..name.."\", doesn't exist.")
+ end
+end
+
+-- Window Controls
+function window_controls(topbar)
+ local wc_geo = {
+ x = 0,
+ y = 30 + user_opts.barmargin,
+ an = 1,
+ w = osc_param.playresx,
+ h = 30,
+ }
+
+ local alignment = window_controls_alignment()
+ local controlbox_w = window_control_box_width
+ local titlebox_w = wc_geo.w - controlbox_w
+
+ -- Default alignment is "right"
+ local controlbox_left = wc_geo.w - controlbox_w
+ local titlebox_left = wc_geo.x
+ local titlebox_right = wc_geo.w - controlbox_w
+
+ if alignment == "left" then
+ controlbox_left = wc_geo.x
+ titlebox_left = wc_geo.x + controlbox_w
+ titlebox_right = wc_geo.w
+ end
+
+ add_area("window-controls",
+ get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
+ controlbox_w, wc_geo.h))
+
+ local lo
+
+ -- Background Bar
+ new_element("wcbar", "box")
+ lo = add_layout("wcbar")
+ lo.geometry = wc_geo
+ lo.layer = 10
+ lo.style = osc_styles.wcBar
+ lo.alpha[1] = user_opts.boxalpha
+
+ local button_y = wc_geo.y - (wc_geo.h / 2)
+ local first_geo =
+ {x = controlbox_left + 5, y = button_y, an = 4, w = 25, h = 25}
+ local second_geo =
+ {x = controlbox_left + 30, y = button_y, an = 4, w = 25, h = 25}
+ local third_geo =
+ {x = controlbox_left + 55, y = button_y, an = 4, w = 25, h = 25}
+
+ -- Window control buttons use symbols in the custom mpv osd font
+ -- because the official unicode codepoints are sufficiently
+ -- exotic that a system might lack an installed font with them,
+ -- and libass will complain that they are not present in the
+ -- default font, even if another font with them is available.
+
+ -- Close: 🗙
+ ne = new_element("close", "button")
+ ne.content = "\238\132\149"
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("quit") end
+ lo = add_layout("close")
+ lo.geometry = alignment == "left" and first_geo or third_geo
+ lo.style = osc_styles.wcButtons
+
+ -- Minimize: 🗕
+ ne = new_element("minimize", "button")
+ ne.content = "\238\132\146"
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "window-minimized") end
+ lo = add_layout("minimize")
+ lo.geometry = alignment == "left" and second_geo or first_geo
+ lo.style = osc_styles.wcButtons
+
+ -- Maximize: 🗖 /🗗
+ ne = new_element("maximize", "button")
+ if state.maximized or state.fullscreen then
+ ne.content = "\238\132\148"
+ else
+ ne.content = "\238\132\147"
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ if state.fullscreen then
+ mp.commandv("cycle", "fullscreen")
+ else
+ mp.commandv("cycle", "window-maximized")
+ end
+ end
+ lo = add_layout("maximize")
+ lo.geometry = alignment == "left" and third_geo or second_geo
+ lo.style = osc_styles.wcButtons
+
+ -- deadzone below window controls
+ local sh_area_y0, sh_area_y1
+ sh_area_y0 = user_opts.barmargin
+ sh_area_y1 = wc_geo.y + get_align(1 - (2 * user_opts.deadzonesize),
+ osc_param.playresy - wc_geo.y, 0, 0)
+ add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1)
+
+ if topbar then
+ -- The title is already there as part of the top bar
+ return
+ else
+ -- Apply boxvideo margins to the control bar
+ osc_param.video_margins.t = wc_geo.h / osc_param.playresy
+ end
+
+ -- Window Title
+ ne = new_element("wctitle", "button")
+ ne.content = function ()
+ local title = mp.command_native({"expand-text", user_opts.title})
+ -- escape ASS, and strip newlines and trailing slashes
+ title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
+ return not (title == "") and title or "mpv"
+ end
+ local left_pad = 5
+ local right_pad = 10
+ lo = add_layout("wctitle")
+ lo.geometry =
+ { x = titlebox_left + left_pad, y = wc_geo.y - 3, an = 1,
+ w = titlebox_w, h = wc_geo.h }
+ lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
+ osc_styles.wcTitle,
+ titlebox_left + left_pad, wc_geo.y - wc_geo.h,
+ titlebox_right - right_pad , wc_geo.y + wc_geo.h)
+
+ add_area("window-controls-title",
+ titlebox_left, 0, titlebox_right, wc_geo.h)
+end
+
+--
+-- Layouts
+--
+
+local layouts = {}
+
+-- Classic box layout
+layouts["box"] = function ()
+
+ local osc_geo = {
+ w = 550, -- width
+ h = 138, -- height
+ r = 10, -- corner-radius
+ p = 15, -- padding
+ }
+
+ -- make sure the OSC actually fits into the video
+ if osc_param.playresx < (osc_geo.w + (2 * osc_geo.p)) then
+ osc_param.playresy = (osc_geo.w + (2 * osc_geo.p)) / osc_param.display_aspect
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+ end
+
+ -- position of the controller according to video aspect and valignment
+ local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
+ osc_geo.w, 0))
+ local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
+ osc_geo.h, 0))
+
+ -- position offset for contents aligned at the borders of the box
+ local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2
+ local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2
+
+ osc_param.areas = {} -- delete areas
+
+ -- area for active mouse input
+ add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
+
+ -- area for show/hide
+ local sh_area_y0, sh_area_y1
+ if user_opts.valign > 0 then
+ -- deadzone above OSC
+ sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
+ posY - (osc_geo.h / 2), 0, 0)
+ sh_area_y1 = osc_param.playresy
+ else
+ -- deadzone below OSC
+ sh_area_y0 = 0
+ sh_area_y1 = (posY + (osc_geo.h / 2)) +
+ get_align(1 - (2*user_opts.deadzonesize),
+ osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
+ end
+ add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
+
+ -- fetch values
+ local osc_w, osc_h, osc_r, osc_p =
+ osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p
+
+ local lo
+
+ --
+ -- Background box
+ --
+
+ new_element("bgbox", "box")
+ lo = add_layout("bgbox")
+
+ lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
+ lo.layer = 10
+ lo.style = osc_styles.box
+ lo.alpha[1] = user_opts.boxalpha
+ lo.alpha[3] = user_opts.boxalpha
+ lo.box.radius = osc_r
+
+ --
+ -- Title row
+ --
+
+ local titlerowY = posY - pos_offsetY - 10
+
+ lo = add_layout("title")
+ lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
+ lo.style = osc_styles.vidtitle
+ lo.button.maxchars = user_opts.boxmaxchars
+
+ lo = add_layout("pl_prev")
+ lo.geometry =
+ {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12}
+ lo.style = osc_styles.topButtons
+
+ lo = add_layout("pl_next")
+ lo.geometry =
+ {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12}
+ lo.style = osc_styles.topButtons
+
+ --
+ -- Big buttons
+ --
+
+ local bigbtnrowY = posY - pos_offsetY + 35
+ local bigbtndist = 60
+
+ lo = add_layout("playpause")
+ lo.geometry =
+ {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("skipback")
+ lo.geometry =
+ {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("skipfrwd")
+ lo.geometry =
+ {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("ch_prev")
+ lo.geometry =
+ {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("ch_next")
+ lo.geometry =
+ {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("cy_audio")
+ lo.geometry =
+ {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18}
+ lo.style = osc_styles.smallButtonsL
+
+ lo = add_layout("cy_sub")
+ lo.geometry =
+ {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18}
+ lo.style = osc_styles.smallButtonsL
+
+ lo = add_layout("tog_fs")
+ lo.geometry =
+ {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25}
+ lo.style = osc_styles.smallButtonsR
+
+ lo = add_layout("volume")
+ lo.geometry =
+ {x = posX+pos_offsetX - (25 * 2) - osc_geo.p,
+ y = bigbtnrowY, an = 4, w = 25, h = 25}
+ lo.style = osc_styles.smallButtonsR
+
+ --
+ -- Seekbar
+ --
+
+ lo = add_layout("seekbar")
+ lo.geometry =
+ {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15}
+ lo.style = osc_styles.timecodes
+ lo.slider.tooltip_style = osc_styles.vidtitle
+ lo.slider.stype = user_opts["seekbarstyle"]
+ lo.slider.rtype = user_opts["seekrangestyle"]
+
+ --
+ -- Timecodes + Cache
+ --
+
+ local bottomrowY = posY + pos_offsetY - 5
+
+ lo = add_layout("tc_left")
+ lo.geometry =
+ {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18}
+ lo.style = osc_styles.timecodes
+
+ lo = add_layout("tc_right")
+ lo.geometry =
+ {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18}
+ lo.style = osc_styles.timecodes
+
+ lo = add_layout("cache")
+ lo.geometry =
+ {x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
+ lo.style = osc_styles.timecodes
+
+end
+
+-- slim box layout
+layouts["slimbox"] = function ()
+
+ local osc_geo = {
+ w = 660, -- width
+ h = 70, -- height
+ r = 10, -- corner-radius
+ }
+
+ -- make sure the OSC actually fits into the video
+ if osc_param.playresx < (osc_geo.w) then
+ osc_param.playresy = (osc_geo.w) / osc_param.display_aspect
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+ end
+
+ -- position of the controller according to video aspect and valignment
+ local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
+ osc_geo.w, 0))
+ local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
+ osc_geo.h, 0))
+
+ osc_param.areas = {} -- delete areas
+
+ -- area for active mouse input
+ add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
+
+ -- area for show/hide
+ local sh_area_y0, sh_area_y1
+ if user_opts.valign > 0 then
+ -- deadzone above OSC
+ sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
+ posY - (osc_geo.h / 2), 0, 0)
+ sh_area_y1 = osc_param.playresy
+ else
+ -- deadzone below OSC
+ sh_area_y0 = 0
+ sh_area_y1 = (posY + (osc_geo.h / 2)) +
+ get_align(1 - (2*user_opts.deadzonesize),
+ osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
+ end
+ add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
+
+ local lo
+
+ local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100
+
+ -- styles
+ local styles = {
+ box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
+ timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
+ tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}",
+ }
+
+
+ new_element("bgbox", "box")
+ lo = add_layout("bgbox")
+
+ lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
+ lo.layer = 10
+ lo.style = osc_styles.box
+ lo.alpha[1] = user_opts.boxalpha
+ lo.alpha[3] = 0
+ if user_opts["seekbarstyle"] ~= "bar" then
+ lo.box.radius = osc_geo.r
+ lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
+ end
+
+
+ lo = add_layout("seekbar")
+ lo.geometry =
+ {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
+ lo.style = osc_styles.timecodes
+ lo.slider.border = 0
+ lo.slider.gap = 1.5
+ lo.slider.tooltip_style = styles.tooltip
+ lo.slider.stype = user_opts["seekbarstyle"]
+ lo.slider.rtype = user_opts["seekrangestyle"]
+ lo.slider.adjust_tooltip = false
+
+ --
+ -- Timecodes
+ --
+
+ lo = add_layout("tc_left")
+ lo.geometry =
+ {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1,
+ an = 7, w = tc_w, h = ele_h}
+ lo.style = styles.timecodes
+ lo.alpha[3] = user_opts.boxalpha
+
+ lo = add_layout("tc_right")
+ lo.geometry =
+ {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1,
+ an = 9, w = tc_w, h = ele_h}
+ lo.style = styles.timecodes
+ lo.alpha[3] = user_opts.boxalpha
+
+ -- Cache
+
+ lo = add_layout("cache")
+ lo.geometry =
+ {x = posX, y = posY + 1,
+ an = 8, w = tc_w, h = ele_h}
+ lo.style = styles.timecodes
+ lo.alpha[3] = user_opts.boxalpha
+
+
+end
+
+function bar_layout(direction)
+ local osc_geo = {
+ x = -2,
+ y,
+ an = (direction < 0) and 7 or 1,
+ w,
+ h = 56,
+ }
+
+ local padX = 9
+ local padY = 3
+ local buttonW = 27
+ local tcW = (state.tc_ms) and 170 or 110
+ if user_opts.tcspace >= 50 and user_opts.tcspace <= 200 then
+ -- adjust our hardcoded font size estimation
+ tcW = tcW * user_opts.tcspace / 100
+ end
+
+ local tsW = 90
+ local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
+
+ -- Special topbar handling when window controls are present
+ local padwc_l
+ local padwc_r
+ if direction < 0 or not window_controls_enabled() then
+ padwc_l = 0
+ padwc_r = 0
+ elseif window_controls_alignment() == "left" then
+ padwc_l = window_control_box_width
+ padwc_r = 0
+ else
+ padwc_l = 0
+ padwc_r = window_control_box_width
+ end
+
+ if osc_param.display_aspect > 0 and osc_param.playresx < minW then
+ osc_param.playresy = minW / osc_param.display_aspect
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+ end
+
+ osc_geo.y = direction * (54 + user_opts.barmargin)
+ osc_geo.w = osc_param.playresx + 4
+ if direction < 0 then
+ osc_geo.y = osc_geo.y + osc_param.playresy
+ end
+
+ local line1 = osc_geo.y - direction * (9 + padY)
+ local line2 = osc_geo.y - direction * (36 + padY)
+
+ osc_param.areas = {}
+
+ add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
+ osc_geo.w, osc_geo.h))
+
+ local sh_area_y0, sh_area_y1
+ if direction > 0 then
+ -- deadzone below OSC
+ sh_area_y0 = user_opts.barmargin
+ sh_area_y1 = osc_geo.y + get_align(1 - (2 * user_opts.deadzonesize),
+ osc_param.playresy - osc_geo.y, 0, 0)
+ else
+ -- deadzone above OSC
+ sh_area_y0 = get_align(-1 + (2 * user_opts.deadzonesize), osc_geo.y, 0, 0)
+ sh_area_y1 = osc_param.playresy - user_opts.barmargin
+ end
+ add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
+
+ local lo, geo
+
+ -- Background bar
+ new_element("bgbox", "box")
+ lo = add_layout("bgbox")
+
+ lo.geometry = osc_geo
+ lo.layer = 10
+ lo.style = osc_styles.box
+ lo.alpha[1] = user_opts.boxalpha
+
+
+ -- Playlist prev/next
+ geo = { x = osc_geo.x + padX, y = line1,
+ an = 4, w = 18, h = 18 - padY }
+ lo = add_layout("pl_prev")
+ lo.geometry = geo
+ lo.style = osc_styles.topButtonsBar
+
+ geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("pl_next")
+ lo.geometry = geo
+ lo.style = osc_styles.topButtonsBar
+
+ local t_l = geo.x + geo.w + padX
+
+ -- Cache
+ geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
+ an = 6, w = 150, h = geo.h }
+ lo = add_layout("cache")
+ lo.geometry = geo
+ lo.style = osc_styles.vidtitleBar
+
+ local t_r = geo.x - geo.w - padX*2
+
+ -- Title
+ geo = { x = t_l, y = geo.y, an = 4,
+ w = t_r - t_l, h = geo.h }
+ lo = add_layout("title")
+ lo.geometry = geo
+ lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
+ osc_styles.vidtitleBar,
+ geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
+
+
+ -- Playback control buttons
+ geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4,
+ w = buttonW, h = 36 - padY*2}
+ lo = add_layout("playpause")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("ch_prev")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("ch_next")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ -- Left timecode
+ geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
+ w = tcW, h = geo.h }
+ lo = add_layout("tc_left")
+ lo.geometry = geo
+ lo.style = osc_styles.timecodesBar
+
+ local sb_l = geo.x + padX
+
+ -- Fullscreen button
+ geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4,
+ w = buttonW, h = geo.h }
+ lo = add_layout("tog_fs")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ -- Volume
+ geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("volume")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ -- Track selection buttons
+ geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
+ lo = add_layout("cy_sub")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("cy_audio")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+
+ -- Right timecode
+ geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an,
+ w = tcW, h = geo.h }
+ lo = add_layout("tc_right")
+ lo.geometry = geo
+ lo.style = osc_styles.timecodesBar
+
+ local sb_r = geo.x - padX
+
+
+ -- Seekbar
+ geo = { x = sb_l, y = geo.y, an = geo.an,
+ w = math.max(0, sb_r - sb_l), h = geo.h }
+ new_element("bgbar1", "box")
+ lo = add_layout("bgbar1")
+
+ lo.geometry = geo
+ lo.layer = 15
+ lo.style = osc_styles.timecodesBar
+ lo.alpha[1] =
+ math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
+ if user_opts["seekbarstyle"] ~= "bar" then
+ lo.box.radius = geo.h / 2
+ lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
+ end
+
+ lo = add_layout("seekbar")
+ lo.geometry = geo
+ lo.style = osc_styles.timecodesBar
+ lo.slider.border = 0
+ lo.slider.gap = 2
+ lo.slider.tooltip_style = osc_styles.timePosBar
+ lo.slider.tooltip_an = 5
+ lo.slider.stype = user_opts["seekbarstyle"]
+ lo.slider.rtype = user_opts["seekrangestyle"]
+
+ if direction < 0 then
+ osc_param.video_margins.b = osc_geo.h / osc_param.playresy
+ else
+ osc_param.video_margins.t = osc_geo.h / osc_param.playresy
+ end
+end
+
+layouts["bottombar"] = function()
+ bar_layout(-1)
+end
+
+layouts["topbar"] = function()
+ bar_layout(1)
+end
+
+-- Validate string type user options
+function validate_user_opts()
+ if layouts[user_opts.layout] == nil then
+ msg.warn("Invalid setting \""..user_opts.layout.."\" for layout")
+ user_opts.layout = "bottombar"
+ end
+
+ if user_opts.seekbarstyle ~= "bar" and
+ user_opts.seekbarstyle ~= "diamond" and
+ user_opts.seekbarstyle ~= "knob" then
+ msg.warn("Invalid setting \"" .. user_opts.seekbarstyle
+ .. "\" for seekbarstyle")
+ user_opts.seekbarstyle = "bar"
+ end
+
+ if user_opts.seekrangestyle ~= "bar" and
+ user_opts.seekrangestyle ~= "line" and
+ user_opts.seekrangestyle ~= "slider" and
+ user_opts.seekrangestyle ~= "inverted" and
+ user_opts.seekrangestyle ~= "none" then
+ msg.warn("Invalid setting \"" .. user_opts.seekrangestyle
+ .. "\" for seekrangestyle")
+ user_opts.seekrangestyle = "inverted"
+ end
+
+ if user_opts.seekrangestyle == "slider" and
+ user_opts.seekbarstyle == "bar" then
+ msg.warn("Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported")
+ user_opts.seekrangestyle = "inverted"
+ end
+
+ if user_opts.windowcontrols ~= "auto" and
+ user_opts.windowcontrols ~= "yes" and
+ user_opts.windowcontrols ~= "no" then
+ msg.warn("windowcontrols cannot be \"" ..
+ user_opts.windowcontrols .. "\". Ignoring.")
+ user_opts.windowcontrols = "auto"
+ end
+ if user_opts.windowcontrols_alignment ~= "right" and
+ user_opts.windowcontrols_alignment ~= "left" then
+ msg.warn("windowcontrols_alignment cannot be \"" ..
+ user_opts.windowcontrols_alignment .. "\". Ignoring.")
+ user_opts.windowcontrols_alignment = "right"
+ end
+end
+
+function update_options(list)
+ validate_user_opts()
+ request_tick()
+ visibility_mode(user_opts.visibility, true)
+ update_duration_watch()
+ request_init()
+end
+
+local UNICODE_MINUS = string.char(0xe2, 0x88, 0x92) -- UTF-8 for U+2212 MINUS SIGN
+
+-- OSC INIT
+function osc_init()
+ msg.debug("osc_init")
+
+ -- set canvas resolution according to display aspect and scaling setting
+ local baseResY = 720
+ local display_w, display_h, display_aspect = mp.get_osd_size()
+ local scale = 1
+
+ if mp.get_property("video") == "no" then -- dummy/forced window
+ scale = user_opts.scaleforcedwindow
+ elseif state.fullscreen then
+ scale = user_opts.scalefullscreen
+ else
+ scale = user_opts.scalewindowed
+ end
+
+ if user_opts.vidscale then
+ osc_param.unscaled_y = baseResY
+ else
+ osc_param.unscaled_y = display_h
+ end
+ osc_param.playresy = osc_param.unscaled_y / scale
+ if display_aspect > 0 then
+ osc_param.display_aspect = display_aspect
+ end
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+
+ -- stop seeking with the slider to prevent skipping files
+ state.active_element = nil
+
+ osc_param.video_margins = {l = 0, r = 0, t = 0, b = 0}
+
+ elements = {}
+
+ -- some often needed stuff
+ local pl_count = mp.get_property_number("playlist-count", 0)
+ local have_pl = (pl_count > 1)
+ local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
+ local have_ch = (mp.get_property_number("chapters", 0) > 0)
+ local loop = mp.get_property("loop-playlist", "no")
+
+ local ne
+
+ -- title
+ ne = new_element("title", "button")
+
+ ne.content = function ()
+ local title = state.forced_title or
+ mp.command_native({"expand-text", user_opts.title})
+ -- escape ASS, and strip newlines and trailing slashes
+ title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
+ return not (title == "") and title or "mpv"
+ end
+
+ ne.eventresponder["mbtn_left_up"] = function ()
+ local title = mp.get_property_osd("media-title")
+ if have_pl then
+ title = string.format("[%d/%d] %s", countone(pl_pos - 1),
+ pl_count, title)
+ end
+ show_message(title)
+ end
+
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(mp.get_property_osd("filename")) end
+
+ -- playlist buttons
+
+ -- prev
+ ne = new_element("pl_prev", "button")
+
+ ne.content = "\238\132\144"
+ ne.enabled = (pl_pos > 1) or (loop ~= "no")
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("playlist-prev", "weak")
+ if user_opts.playlist_osd then
+ show_message(get_playlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_playlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_playlist(), 3) end
+
+ --next
+ ne = new_element("pl_next", "button")
+
+ ne.content = "\238\132\129"
+ ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("playlist-next", "weak")
+ if user_opts.playlist_osd then
+ show_message(get_playlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_playlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_playlist(), 3) end
+
+
+ -- big buttons
+
+ --playpause
+ ne = new_element("playpause", "button")
+
+ ne.content = function ()
+ if mp.get_property("pause") == "yes" then
+ return ("\238\132\129")
+ else
+ return ("\238\128\130")
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "pause") end
+
+ --skipback
+ ne = new_element("skipback", "button")
+
+ ne.softrepeat = true
+ ne.content = "\238\128\132"
+ ne.eventresponder["mbtn_left_down"] =
+ function () mp.commandv("seek", -5, "relative", "keyframes") end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () mp.commandv("frame-back-step") end
+ ne.eventresponder["mbtn_right_down"] =
+ function () mp.commandv("seek", -30, "relative", "keyframes") end
+
+ --skipfrwd
+ ne = new_element("skipfrwd", "button")
+
+ ne.softrepeat = true
+ ne.content = "\238\128\133"
+ ne.eventresponder["mbtn_left_down"] =
+ function () mp.commandv("seek", 10, "relative", "keyframes") end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () mp.commandv("frame-step") end
+ ne.eventresponder["mbtn_right_down"] =
+ function () mp.commandv("seek", 60, "relative", "keyframes") end
+
+ --ch_prev
+ ne = new_element("ch_prev", "button")
+
+ ne.enabled = have_ch
+ ne.content = "\238\132\132"
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("add", "chapter", -1)
+ if user_opts.chapters_osd then
+ show_message(get_chapterlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_chapterlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_chapterlist(), 3) end
+
+ --ch_next
+ ne = new_element("ch_next", "button")
+
+ ne.enabled = have_ch
+ ne.content = "\238\132\133"
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("add", "chapter", 1)
+ if user_opts.chapters_osd then
+ show_message(get_chapterlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_chapterlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_chapterlist(), 3) end
+
+ --
+ update_tracklist()
+
+ --cy_audio
+ ne = new_element("cy_audio", "button")
+
+ ne.enabled = (#tracks_osc.audio > 0)
+ ne.content = function ()
+ local aid = "–"
+ if get_track("audio") ~= 0 then
+ aid = get_track("audio")
+ end
+ return ("\238\132\134" .. osc_styles.smallButtonsLlabel
+ .. " " .. aid .. "/" .. #tracks_osc.audio)
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () set_track("audio", 1) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () set_track("audio", -1) end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () show_message(get_tracklist("audio"), 2) end
+ ne.eventresponder["wheel_down_press"] =
+ function () set_track("audio", 1) end
+ ne.eventresponder["wheel_up_press"] =
+ function () set_track("audio", -1) end
+
+ --cy_sub
+ ne = new_element("cy_sub", "button")
+
+ ne.enabled = (#tracks_osc.sub > 0)
+ ne.content = function ()
+ local sid = "–"
+ if get_track("sub") ~= 0 then
+ sid = get_track("sub")
+ end
+ return ("\238\132\135" .. osc_styles.smallButtonsLlabel
+ .. " " .. sid .. "/" .. #tracks_osc.sub)
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () set_track("sub", 1) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () set_track("sub", -1) end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () show_message(get_tracklist("sub"), 2) end
+ ne.eventresponder["wheel_down_press"] =
+ function () set_track("sub", 1) end
+ ne.eventresponder["wheel_up_press"] =
+ function () set_track("sub", -1) end
+
+ --tog_fs
+ ne = new_element("tog_fs", "button")
+ ne.content = function ()
+ if state.fullscreen then
+ return ("\238\132\137")
+ else
+ return ("\238\132\136")
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "fullscreen") end
+
+ --seekbar
+ ne = new_element("seekbar", "slider")
+
+ ne.enabled = mp.get_property("percent-pos") ~= nil
+ state.slider_element = ne.enabled and ne or nil -- used for forced_title
+ ne.slider.markerF = function ()
+ local duration = mp.get_property_number("duration", nil)
+ if duration ~= nil then
+ local chapters = mp.get_property_native("chapter-list", {})
+ local markers = {}
+ for n = 1, #chapters do
+ markers[n] = (chapters[n].time / duration * 100)
+ end
+ return markers
+ else
+ return {}
+ end
+ end
+ ne.slider.posF =
+ function () return mp.get_property_number("percent-pos", nil) end
+ ne.slider.tooltipF = function (pos)
+ local duration = mp.get_property_number("duration", nil)
+ if duration ~= nil and pos ~= nil then
+ possec = duration * (pos / 100)
+ return mp.format_time(possec)
+ else
+ return ""
+ end
+ end
+ ne.slider.seekRangesF = function()
+ if user_opts.seekrangestyle == "none" then
+ return nil
+ end
+ local cache_state = state.cache_state
+ if not cache_state then
+ return nil
+ end
+ local duration = mp.get_property_number("duration", nil)
+ if duration == nil or duration <= 0 then
+ return nil
+ end
+ local ranges = cache_state["seekable-ranges"]
+ if #ranges == 0 then
+ return nil
+ end
+ local nranges = {}
+ for _, range in pairs(ranges) do
+ nranges[#nranges + 1] = {
+ ["start"] = 100 * range["start"] / duration,
+ ["end"] = 100 * range["end"] / duration,
+ }
+ end
+ return nranges
+ end
+ ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
+ function (element)
+ -- mouse move events may pile up during seeking and may still get
+ -- sent when the user is done seeking, so we need to throw away
+ -- identical seeks
+ local seekto = get_slider_value(element)
+ if element.state.lastseek == nil or
+ element.state.lastseek ~= seekto then
+ local flags = "absolute-percent"
+ if not user_opts.seekbarkeyframes then
+ flags = flags .. "+exact"
+ end
+ mp.commandv("seek", seekto, flags)
+ element.state.lastseek = seekto
+ end
+
+ end
+ ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks
+ function (element) mp.commandv("seek", get_slider_value(element),
+ "absolute-percent", "exact") end
+ ne.eventresponder["reset"] =
+ function (element) element.state.lastseek = nil end
+ ne.eventresponder["wheel_up_press"] =
+ function () mp.commandv("osd-auto", "seek", 10) end
+ ne.eventresponder["wheel_down_press"] =
+ function () mp.commandv("osd-auto", "seek", -10) end
+
+
+ -- tc_left (current pos)
+ ne = new_element("tc_left", "button")
+
+ ne.content = function ()
+ if state.tc_ms then
+ return (mp.get_property_osd("playback-time/full"))
+ else
+ return (mp.get_property_osd("playback-time"))
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] = function ()
+ state.tc_ms = not state.tc_ms
+ request_init()
+ end
+
+ -- tc_right (total/remaining time)
+ ne = new_element("tc_right", "button")
+
+ ne.visible = (mp.get_property_number("duration", 0) > 0)
+ ne.content = function ()
+ if state.rightTC_trem then
+ local minus = user_opts.unicodeminus and UNICODE_MINUS or "-"
+ local property = user_opts.remaining_playtime and "playtime-remaining"
+ or "time-remaining"
+ if state.tc_ms then
+ return (minus..mp.get_property_osd(property .. "/full"))
+ else
+ return (minus..mp.get_property_osd(property))
+ end
+ else
+ if state.tc_ms then
+ return (mp.get_property_osd("duration/full"))
+ else
+ return (mp.get_property_osd("duration"))
+ end
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () state.rightTC_trem = not state.rightTC_trem end
+
+ -- cache
+ ne = new_element("cache", "button")
+
+ ne.content = function ()
+ local cache_state = state.cache_state
+ if not (cache_state and cache_state["seekable-ranges"] and
+ #cache_state["seekable-ranges"] > 0) then
+ -- probably not a network stream
+ return ""
+ end
+ local dmx_cache = cache_state and cache_state["cache-duration"]
+ local thresh = math.min(state.dmx_cache * 0.05, 5) -- 5% or 5s
+ if dmx_cache and math.abs(dmx_cache - state.dmx_cache) >= thresh then
+ state.dmx_cache = dmx_cache
+ else
+ dmx_cache = state.dmx_cache
+ end
+ local min = math.floor(dmx_cache / 60)
+ local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60
+ return "Cache: " .. (min > 0 and
+ string.format("%sm%02.0fs", min, sec) or
+ string.format("%3.0fs", sec))
+ end
+
+ -- volume
+ ne = new_element("volume", "button")
+
+ ne.content = function()
+ local volume = mp.get_property_number("volume", 0)
+ local mute = mp.get_property_native("mute")
+ local volicon = {"\238\132\139", "\238\132\140",
+ "\238\132\141", "\238\132\142"}
+ if volume == 0 or mute then
+ return "\238\132\138"
+ else
+ return volicon[math.min(4,math.ceil(volume / (100/3)))]
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "mute") end
+
+ ne.eventresponder["wheel_up_press"] =
+ function () mp.commandv("osd-auto", "add", "volume", 5) end
+ ne.eventresponder["wheel_down_press"] =
+ function () mp.commandv("osd-auto", "add", "volume", -5) end
+
+
+ -- load layout
+ layouts[user_opts.layout]()
+
+ -- load window controls
+ if window_controls_enabled() then
+ window_controls(user_opts.layout == "topbar")
+ end
+
+ --do something with the elements
+ prepare_elements()
+
+ update_margins()
+end
+
+function reset_margins()
+ if state.using_video_margins then
+ for _, opt in ipairs(margins_opts) do
+ mp.set_property_number(opt[2], 0.0)
+ end
+ state.using_video_margins = false
+ end
+end
+
+function update_margins()
+ local margins = osc_param.video_margins
+
+ -- Don't use margins if it's visible only temporarily.
+ if not state.osc_visible or get_hidetimeout() >= 0 or
+ (state.fullscreen and not user_opts.showfullscreen) or
+ (not state.fullscreen and not user_opts.showwindowed)
+ then
+ margins = {l = 0, r = 0, t = 0, b = 0}
+ end
+
+ if user_opts.boxvideo then
+ -- check whether any margin option has a non-default value
+ local margins_used = false
+
+ if not state.using_video_margins then
+ for _, opt in ipairs(margins_opts) do
+ if mp.get_property_number(opt[2], 0.0) ~= 0.0 then
+ margins_used = true
+ end
+ end
+ end
+
+ if not margins_used then
+ for _, opt in ipairs(margins_opts) do
+ local v = margins[opt[1]]
+ if v ~= 0 or state.using_video_margins then
+ mp.set_property_number(opt[2], v)
+ state.using_video_margins = true
+ end
+ end
+ end
+ else
+ reset_margins()
+ end
+
+ mp.set_property_native("user-data/osc/margins", margins)
+end
+
+function shutdown()
+ reset_margins()
+ mp.del_property("user-data/osc")
+end
+
+--
+-- Other important stuff
+--
+
+
+function show_osc()
+ -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
+ if not state.enabled then return end
+
+ msg.trace("show_osc")
+ --remember last time of invocation (mouse move)
+ state.showtime = mp.get_time()
+
+ osc_visible(true)
+
+ if user_opts.fadeduration > 0 then
+ state.anitype = nil
+ end
+end
+
+function hide_osc()
+ msg.trace("hide_osc")
+ if not state.enabled then
+ -- typically hide happens at render() from tick(), but now tick() is
+ -- no-op and won't render again to remove the osc, so do that manually.
+ state.osc_visible = false
+ render_wipe()
+ elseif user_opts.fadeduration > 0 then
+ if state.osc_visible then
+ state.anitype = "out"
+ request_tick()
+ end
+ else
+ osc_visible(false)
+ end
+end
+
+function osc_visible(visible)
+ if state.osc_visible ~= visible then
+ state.osc_visible = visible
+ update_margins()
+ end
+ request_tick()
+end
+
+function pause_state(name, enabled)
+ state.paused = enabled
+ request_tick()
+end
+
+function cache_state(name, st)
+ state.cache_state = st
+ request_tick()
+end
+
+-- Request that tick() is called (which typically re-renders the OSC).
+-- The tick is then either executed immediately, or rate-limited if it was
+-- called a small time ago.
+function request_tick()
+ if state.tick_timer == nil then
+ state.tick_timer = mp.add_timeout(0, tick)
+ end
+
+ if not state.tick_timer:is_enabled() then
+ local now = mp.get_time()
+ local timeout = tick_delay - (now - state.tick_last_time)
+ if timeout < 0 then
+ timeout = 0
+ end
+ state.tick_timer.timeout = timeout
+ state.tick_timer:resume()
+ end
+end
+
+function mouse_leave()
+ if get_hidetimeout() >= 0 then
+ hide_osc()
+ end
+ -- reset mouse position
+ state.last_mouseX, state.last_mouseY = nil, nil
+ state.mouse_in_window = false
+end
+
+function request_init()
+ state.initREQ = true
+ request_tick()
+end
+
+-- Like request_init(), but also request an immediate update
+function request_init_resize()
+ request_init()
+ -- ensure immediate update
+ state.tick_timer:kill()
+ state.tick_timer.timeout = 0
+ state.tick_timer:resume()
+end
+
+function render_wipe()
+ msg.trace("render_wipe()")
+ state.osd.data = "" -- allows set_osd to immediately update on enable
+ state.osd:remove()
+end
+
+function render()
+ msg.trace("rendering")
+ local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
+ local mouseX, mouseY = get_virt_mouse_pos()
+ local now = mp.get_time()
+
+ -- check if display changed, if so request reinit
+ if state.mp_screen_sizeX ~= current_screen_sizeX
+ or state.mp_screen_sizeY ~= current_screen_sizeY then
+
+ request_init_resize()
+
+ state.mp_screen_sizeX = current_screen_sizeX
+ state.mp_screen_sizeY = current_screen_sizeY
+ end
+
+ -- init management
+ if state.active_element then
+ -- mouse is held down on some element - keep ticking and ignore initReq
+ -- till it's released, or else the mouse-up (click) will misbehave or
+ -- get ignored. that's because osc_init() recreates the osc elements,
+ -- but mouse handling depends on the elements staying unmodified
+ -- between mouse-down and mouse-up (using the index active_element).
+ request_tick()
+ elseif state.initREQ then
+ osc_init()
+ state.initREQ = false
+
+ -- store initial mouse position
+ if (state.last_mouseX == nil or state.last_mouseY == nil)
+ and not (mouseX == nil or mouseY == nil) then
+
+ state.last_mouseX, state.last_mouseY = mouseX, mouseY
+ end
+ end
+
+
+ -- fade animation
+ if state.anitype ~= nil then
+
+ if state.anistart == nil then
+ state.anistart = now
+ end
+
+ if now < state.anistart + (user_opts.fadeduration / 1000) then
+
+ if state.anitype == "in" then --fade in
+ osc_visible(true)
+ state.animation = scale_value(state.anistart,
+ (state.anistart + (user_opts.fadeduration / 1000)),
+ 255, 0, now)
+ elseif state.anitype == "out" then --fade out
+ state.animation = scale_value(state.anistart,
+ (state.anistart + (user_opts.fadeduration / 1000)),
+ 0, 255, now)
+ end
+
+ else
+ if state.anitype == "out" then
+ osc_visible(false)
+ end
+ kill_animation()
+ end
+ else
+ kill_animation()
+ end
+
+ --mouse show/hide area
+ for k,cords in pairs(osc_param.areas["showhide"]) do
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
+ end
+ if osc_param.areas["showhide_wc"] then
+ for k,cords in pairs(osc_param.areas["showhide_wc"]) do
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc")
+ end
+ else
+ set_virt_mouse_area(0, 0, 0, 0, "showhide_wc")
+ end
+ do_enable_keybindings()
+
+ --mouse input area
+ local mouse_over_osc = false
+
+ for _,cords in ipairs(osc_param.areas["input"]) do
+ if state.osc_visible then -- activate only when OSC is actually visible
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
+ end
+ if state.osc_visible ~= state.input_enabled then
+ if state.osc_visible then
+ mp.enable_key_bindings("input")
+ else
+ mp.disable_key_bindings("input")
+ end
+ state.input_enabled = state.osc_visible
+ end
+
+ if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
+ mouse_over_osc = true
+ end
+ end
+
+ if osc_param.areas["window-controls"] then
+ for _,cords in ipairs(osc_param.areas["window-controls"]) do
+ if state.osc_visible then -- activate only when OSC is actually visible
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls")
+ end
+ if state.osc_visible ~= state.windowcontrols_buttons then
+ if state.osc_visible then
+ mp.enable_key_bindings("window-controls")
+ else
+ mp.disable_key_bindings("window-controls")
+ end
+ state.windowcontrols_buttons = state.osc_visible
+ end
+
+ if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
+ mouse_over_osc = true
+ end
+ end
+ end
+
+ if osc_param.areas["window-controls-title"] then
+ for _,cords in ipairs(osc_param.areas["window-controls-title"]) do
+ if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
+ mouse_over_osc = true
+ end
+ end
+ end
+
+ -- autohide
+ if state.showtime ~= nil and get_hidetimeout() >= 0 then
+ local timeout = state.showtime + (get_hidetimeout() / 1000) - now
+ if timeout <= 0 then
+ if state.active_element == nil and not mouse_over_osc then
+ hide_osc()
+ end
+ else
+ -- the timer is only used to recheck the state and to possibly run
+ -- the code above again
+ if not state.hide_timer then
+ state.hide_timer = mp.add_timeout(0, tick)
+ end
+ state.hide_timer.timeout = timeout
+ -- re-arm
+ state.hide_timer:kill()
+ state.hide_timer:resume()
+ end
+ end
+
+
+ -- actual rendering
+ local ass = assdraw.ass_new()
+
+ -- Messages
+ render_message(ass)
+
+ -- actual OSC
+ if state.osc_visible then
+ render_elements(ass)
+ end
+
+ -- submit
+ set_osd(osc_param.playresy * osc_param.display_aspect,
+ osc_param.playresy, ass.text, 1000)
+end
+
+--
+-- Eventhandling
+--
+
+local function element_has_action(element, action)
+ return element and element.eventresponder and
+ element.eventresponder[action]
+end
+
+function process_event(source, what)
+ local action = string.format("%s%s", source,
+ what and ("_" .. what) or "")
+
+ if what == "down" or what == "press" then
+
+ for n = 1, #elements do
+
+ if mouse_hit(elements[n]) and
+ elements[n].eventresponder and
+ (elements[n].eventresponder[source .. "_up"] or
+ elements[n].eventresponder[action]) then
+
+ if what == "down" then
+ state.active_element = n
+ state.active_event_source = source
+ end
+ -- fire the down or press event if the element has one
+ if element_has_action(elements[n], action) then
+ elements[n].eventresponder[action](elements[n])
+ end
+
+ end
+ end
+
+ elseif what == "up" then
+
+ if elements[state.active_element] then
+ local n = state.active_element
+
+ if n == 0 then
+ --click on background (does not work)
+ elseif element_has_action(elements[n], action) and
+ mouse_hit(elements[n]) then
+
+ elements[n].eventresponder[action](elements[n])
+ end
+
+ --reset active element
+ if element_has_action(elements[n], "reset") then
+ elements[n].eventresponder["reset"](elements[n])
+ end
+
+ end
+ state.active_element = nil
+ state.mouse_down_counter = 0
+
+ elseif source == "mouse_move" then
+
+ state.mouse_in_window = true
+
+ local mouseX, mouseY = get_virt_mouse_pos()
+ if user_opts.minmousemove == 0 or
+ ((state.last_mouseX ~= nil and state.last_mouseY ~= nil) and
+ ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
+ or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
+ )
+ ) then
+ show_osc()
+ end
+ state.last_mouseX, state.last_mouseY = mouseX, mouseY
+
+ local n = state.active_element
+ if element_has_action(elements[n], action) then
+ elements[n].eventresponder[action](elements[n])
+ end
+ end
+
+ -- ensure rendering after any (mouse) event - icons could change etc
+ request_tick()
+end
+
+
+local logo_lines = {
+ -- White border
+ "{\\c&HE5E5E5&\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 {\\p0}",
+ -- Purple fill
+ "{\\c&H682167&\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42{\\p0}",
+ -- Darker fill
+ "{\\c&H430142&\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}",
+ -- White fill
+ "{\\c&HDDDBDD&\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}",
+ -- Triangle
+ "{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}",
+}
+
+local santa_hat_lines = {
+ -- Pompoms
+ "{\\c&HC0C0C0&\\p6}m 500 -323 b 491 -322 481 -318 475 -311 465 -312 456 -319 446 -318 434 -314 427 -304 417 -297 410 -290 404 -282 395 -278 390 -274 387 -267 381 -265 377 -261 379 -254 384 -253 397 -244 409 -232 425 -228 437 -228 446 -218 457 -217 462 -216 466 -213 468 -209 471 -205 477 -203 482 -206 491 -211 499 -217 508 -222 532 -235 556 -249 576 -267 584 -272 584 -284 578 -290 569 -305 550 -312 533 -309 523 -310 515 -316 507 -321 505 -323 503 -323 500 -323{\\p0}",
+ "{\\c&HE0E0E0&\\p6}m 315 -260 b 286 -258 259 -240 246 -215 235 -210 222 -215 211 -211 204 -188 177 -176 172 -151 170 -139 163 -128 154 -121 143 -103 141 -81 143 -60 139 -46 125 -34 129 -17 132 -1 134 16 142 30 145 56 161 80 181 96 196 114 210 133 231 144 266 153 303 138 328 115 373 79 401 28 423 -24 446 -73 465 -123 483 -174 487 -199 467 -225 442 -227 421 -232 402 -242 384 -254 364 -259 342 -250 322 -260 320 -260 317 -261 315 -260{\\p0}",
+ -- Main cap
+ "{\\c&H0000F0&\\p6}m 1151 -523 b 1016 -516 891 -458 769 -406 693 -369 624 -319 561 -262 526 -252 465 -235 479 -187 502 -147 551 -135 588 -111 1115 165 1379 232 1909 761 1926 800 1952 834 1987 858 2020 883 2053 912 2065 952 2088 1000 2146 962 2139 919 2162 836 2156 747 2143 662 2131 615 2116 567 2122 517 2120 410 2090 306 2089 199 2092 147 2071 99 2034 64 1987 5 1928 -41 1869 -86 1777 -157 1712 -256 1629 -337 1578 -389 1521 -436 1461 -476 1407 -509 1343 -507 1284 -515 1240 -519 1195 -521 1151 -523{\\p0}",
+ -- Cap shadow
+ "{\\c&H0000AA&\\p6}m 1657 248 b 1658 254 1659 261 1660 267 1669 276 1680 284 1689 293 1695 302 1700 311 1707 320 1716 325 1726 330 1735 335 1744 347 1752 360 1761 371 1753 352 1754 331 1753 311 1751 237 1751 163 1751 90 1752 64 1752 37 1767 14 1778 -3 1785 -24 1786 -45 1786 -60 1786 -77 1774 -87 1760 -96 1750 -78 1751 -65 1748 -37 1750 -8 1750 20 1734 78 1715 134 1699 192 1694 211 1689 231 1676 246 1671 251 1661 255 1657 248 m 1909 541 b 1914 542 1922 549 1917 539 1919 520 1921 502 1919 483 1918 458 1917 433 1915 407 1930 373 1942 338 1947 301 1952 270 1954 238 1951 207 1946 214 1947 229 1945 239 1939 278 1936 318 1924 356 1923 362 1913 382 1912 364 1906 301 1904 237 1891 175 1887 150 1892 126 1892 101 1892 68 1893 35 1888 2 1884 -9 1871 -20 1859 -14 1851 -6 1854 9 1854 20 1855 58 1864 95 1873 132 1883 179 1894 225 1899 273 1908 362 1910 451 1909 541{\\p0}",
+ -- Brim and tip pompom
+ "{\\c&HF8F8F8&\\p6}m 626 -191 b 565 -155 486 -196 428 -151 387 -115 327 -101 304 -47 273 2 267 59 249 113 219 157 217 213 215 265 217 309 260 302 285 283 373 264 465 264 555 257 608 252 655 292 709 287 759 294 816 276 863 298 903 340 972 324 1012 367 1061 394 1125 382 1167 424 1213 462 1268 482 1322 506 1385 546 1427 610 1479 662 1510 690 1534 725 1566 752 1611 796 1664 830 1703 880 1740 918 1747 986 1805 1005 1863 991 1897 932 1916 880 1914 823 1945 777 1961 725 1979 673 1957 622 1938 575 1912 534 1862 515 1836 473 1790 417 1755 351 1697 305 1658 266 1633 216 1593 176 1574 138 1539 116 1497 110 1448 101 1402 77 1371 37 1346 -16 1295 15 1254 6 1211 -27 1170 -62 1121 -86 1072 -104 1027 -128 976 -133 914 -130 851 -137 794 -162 740 -181 679 -168 626 -191 m 2051 917 b 1971 932 1929 1017 1919 1091 1912 1149 1923 1214 1970 1254 2000 1279 2027 1314 2066 1325 2139 1338 2212 1295 2254 1238 2281 1203 2287 1158 2282 1116 2292 1061 2273 1006 2229 970 2206 941 2167 938 2138 918{\\p0}",
+}
+
+-- called by mpv on every frame
+function tick()
+ if state.marginsREQ == true then
+ update_margins()
+ state.marginsREQ = false
+ end
+
+ if not state.enabled then return end
+
+ if state.idle then
+
+ -- render idle message
+ msg.trace("idle message")
+ local _, _, display_aspect = mp.get_osd_size()
+ if display_aspect == 0 then
+ return
+ end
+ local display_h = 360
+ local display_w = display_h * display_aspect
+ -- logo is rendered at 2^(6-1) = 32 times resolution with size 1800x1800
+ local icon_x, icon_y = (display_w - 1800 / 32) / 2, 140
+ local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y)
+
+ local ass = assdraw.ass_new()
+ -- mpv logo
+ if user_opts.idlescreen then
+ for i, line in ipairs(logo_lines) do
+ ass:new_event()
+ ass:append(line_prefix .. line)
+ end
+ end
+
+ -- Santa hat
+ if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then
+ for i, line in ipairs(santa_hat_lines) do
+ ass:new_event()
+ ass:append(line_prefix .. line)
+ end
+ end
+
+ if user_opts.idlescreen then
+ ass:new_event()
+ ass:pos(display_w / 2, icon_y + 65)
+ ass:an(8)
+ ass:append("Drop files or URLs to play here.")
+ end
+ set_osd(display_w, display_h, ass.text, -1000)
+
+ if state.showhide_enabled then
+ mp.disable_key_bindings("showhide")
+ mp.disable_key_bindings("showhide_wc")
+ state.showhide_enabled = false
+ end
+
+
+ elseif state.fullscreen and user_opts.showfullscreen
+ or (not state.fullscreen and user_opts.showwindowed) then
+
+ -- render the OSC
+ render()
+ else
+ -- Flush OSD
+ render_wipe()
+ end
+
+ state.tick_last_time = mp.get_time()
+
+ if state.anitype ~= nil then
+ -- state.anistart can be nil - animation should now start, or it can
+ -- be a timestamp when it started. state.idle has no animation.
+ if not state.idle and
+ (not state.anistart or
+ mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000)
+ then
+ -- animating or starting, or still within 1s past the deadline
+ request_tick()
+ else
+ kill_animation()
+ end
+ end
+end
+
+function do_enable_keybindings()
+ if state.enabled then
+ if not state.showhide_enabled then
+ mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
+ mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor")
+ end
+ state.showhide_enabled = true
+ end
+end
+
+function enable_osc(enable)
+ state.enabled = enable
+ if enable then
+ do_enable_keybindings()
+ else
+ hide_osc() -- acts immediately when state.enabled == false
+ if state.showhide_enabled then
+ mp.disable_key_bindings("showhide")
+ mp.disable_key_bindings("showhide_wc")
+ end
+ state.showhide_enabled = false
+ end
+end
+
+-- duration is observed for the sole purpose of updating chapter markers
+-- positions. live streams with chapters are very rare, and the update is also
+-- expensive (with request_init), so it's only observed when we have chapters
+-- and the user didn't disable the livemarkers option (update_duration_watch).
+function on_duration() request_init() end
+
+local duration_watched = false
+function update_duration_watch()
+ local want_watch = user_opts.livemarkers and
+ (mp.get_property_number("chapters", 0) or 0) > 0 and
+ true or false -- ensure it's a boolean
+
+ if want_watch ~= duration_watched then
+ if want_watch then
+ mp.observe_property("duration", nil, on_duration)
+ else
+ mp.unobserve_property(on_duration)
+ end
+ duration_watched = want_watch
+ end
+end
+
+validate_user_opts()
+update_duration_watch()
+
+mp.register_event("shutdown", shutdown)
+mp.register_event("start-file", request_init)
+mp.observe_property("track-list", nil, request_init)
+mp.observe_property("playlist", nil, request_init)
+mp.observe_property("chapter-list", "native", function(_, list)
+ list = list or {} -- safety, shouldn't return nil
+ table.sort(list, function(a, b) return a.time < b.time end)
+ state.chapter_list = list
+ update_duration_watch()
+ request_init()
+end)
+
+mp.register_script_message("osc-message", show_message)
+mp.register_script_message("osc-chapterlist", function(dur)
+ show_message(get_chapterlist(), dur)
+end)
+mp.register_script_message("osc-playlist", function(dur)
+ show_message(get_playlist(), dur)
+end)
+mp.register_script_message("osc-tracklist", function(dur)
+ local msg = {}
+ for k,v in pairs(nicetypes) do
+ table.insert(msg, get_tracklist(k))
+ end
+ show_message(table.concat(msg, '\n\n'), dur)
+end)
+
+mp.observe_property("fullscreen", "bool",
+ function(name, val)
+ state.fullscreen = val
+ state.marginsREQ = true
+ request_init_resize()
+ end
+)
+mp.observe_property("border", "bool",
+ function(name, val)
+ state.border = val
+ request_init_resize()
+ end
+)
+mp.observe_property("window-maximized", "bool",
+ function(name, val)
+ state.maximized = val
+ request_init_resize()
+ end
+)
+mp.observe_property("idle-active", "bool",
+ function(name, val)
+ state.idle = val
+ request_tick()
+ end
+)
+mp.observe_property("pause", "bool", pause_state)
+mp.observe_property("demuxer-cache-state", "native", cache_state)
+mp.observe_property("vo-configured", "bool", function(name, val)
+ request_tick()
+end)
+mp.observe_property("playback-time", "number", function(name, val)
+ request_tick()
+end)
+mp.observe_property("osd-dimensions", "native", function(name, val)
+ -- (we could use the value instead of re-querying it all the time, but then
+ -- we might have to worry about property update ordering)
+ request_init_resize()
+end)
+
+-- mouse show/hide bindings
+mp.set_key_bindings({
+ {"mouse_move", function(e) process_event("mouse_move", nil) end},
+ {"mouse_leave", mouse_leave},
+}, "showhide", "force")
+mp.set_key_bindings({
+ {"mouse_move", function(e) process_event("mouse_move", nil) end},
+ {"mouse_leave", mouse_leave},
+}, "showhide_wc", "force")
+do_enable_keybindings()
+
+--mouse input bindings
+mp.set_key_bindings({
+ {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
+ function(e) process_event("mbtn_left", "down") end},
+ {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
+ function(e) process_event("shift+mbtn_left", "down") end},
+ {"mbtn_right", function(e) process_event("mbtn_right", "up") end,
+ function(e) process_event("mbtn_right", "down") end},
+ -- alias to shift_mbtn_left for single-handed mouse use
+ {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end,
+ function(e) process_event("shift+mbtn_left", "down") end},
+ {"wheel_up", function(e) process_event("wheel_up", "press") end},
+ {"wheel_down", function(e) process_event("wheel_down", "press") end},
+ {"mbtn_left_dbl", "ignore"},
+ {"shift+mbtn_left_dbl", "ignore"},
+ {"mbtn_right_dbl", "ignore"},
+}, "input", "force")
+mp.enable_key_bindings("input")
+
+mp.set_key_bindings({
+ {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
+ function(e) process_event("mbtn_left", "down") end},
+}, "window-controls", "force")
+mp.enable_key_bindings("window-controls")
+
+function get_hidetimeout()
+ if user_opts.visibility == "always" then
+ return -1 -- disable autohide
+ end
+ return user_opts.hidetimeout
+end
+
+function always_on(val)
+ if state.enabled then
+ if val then
+ show_osc()
+ else
+ hide_osc()
+ end
+ end
+end
+
+-- mode can be auto/always/never/cycle
+-- the modes only affect internal variables and not stored on its own.
+function visibility_mode(mode, no_osd)
+ if mode == "cycle" then
+ if not state.enabled then
+ mode = "auto"
+ elseif user_opts.visibility ~= "always" then
+ mode = "always"
+ else
+ mode = "never"
+ end
+ end
+
+ if mode == "auto" then
+ always_on(false)
+ enable_osc(true)
+ elseif mode == "always" then
+ enable_osc(true)
+ always_on(true)
+ elseif mode == "never" then
+ enable_osc(false)
+ else
+ msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
+ return
+ end
+
+ user_opts.visibility = mode
+ mp.set_property_native("user-data/osc/visibility", mode)
+
+ if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
+ mp.osd_message("OSC visibility: " .. mode)
+ end
+
+ -- Reset the input state on a mode change. The input state will be
+ -- recalculated on the next render cycle, except in 'never' mode where it
+ -- will just stay disabled.
+ mp.disable_key_bindings("input")
+ mp.disable_key_bindings("window-controls")
+ state.input_enabled = false
+
+ update_margins()
+ request_tick()
+end
+
+function idlescreen_visibility(mode, no_osd)
+ if mode == "cycle" then
+ if user_opts.idlescreen then
+ mode = "no"
+ else
+ mode = "yes"
+ end
+ end
+
+ if mode == "yes" then
+ user_opts.idlescreen = true
+ else
+ user_opts.idlescreen = false
+ end
+
+ mp.set_property_native("user-data/osc/idlescreen", user_opts.idlescreen)
+
+ if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
+ mp.osd_message("OSC logo visibility: " .. tostring(mode))
+ end
+
+ request_tick()
+end
+
+visibility_mode(user_opts.visibility, true)
+mp.register_script_message("osc-visibility", visibility_mode)
+mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
+
+mp.register_script_message("osc-idlescreen", idlescreen_visibility)
+
+set_virt_mouse_area(0, 0, 0, 0, "input")
+set_virt_mouse_area(0, 0, 0, 0, "window-controls")
diff --git a/player/lua/stats.lua b/player/lua/stats.lua
new file mode 100644
index 0000000..16e8b68
--- /dev/null
+++ b/player/lua/stats.lua
@@ -0,0 +1,1417 @@
+-- Display some stats.
+--
+-- Please consult the readme for information about usage and configuration:
+-- https://github.com/Argon-/mpv-stats
+--
+-- Please note: not every property is always available and therefore not always
+-- visible.
+
+local mp = require 'mp'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+-- Options
+local o = {
+ -- Default key bindings
+ key_page_1 = "1",
+ key_page_2 = "2",
+ key_page_3 = "3",
+ key_page_4 = "4",
+ key_page_0 = "0",
+ -- For pages which support scrolling
+ key_scroll_up = "UP",
+ key_scroll_down = "DOWN",
+ scroll_lines = 1,
+
+ duration = 4,
+ redraw_delay = 1, -- acts as duration in the toggling case
+ ass_formatting = true,
+ persistent_overlay = false, -- whether the stats can be overwritten by other output
+ print_perfdata_passes = false, -- when true, print the full information about all passes
+ filter_params_max_length = 100, -- a filter list longer than this many characters will be shown one filter per line instead
+ show_frame_info = false, -- whether to show the current frame info
+ debug = false,
+
+ -- Graph options and style
+ plot_perfdata = true,
+ plot_vsync_ratio = true,
+ plot_vsync_jitter = true,
+ plot_tonemapping_lut = false,
+ skip_frames = 5,
+ global_max = true,
+ flush_graph_data = true, -- clear data buffers when toggling
+ plot_bg_border_color = "0000FF",
+ plot_bg_color = "262626",
+ plot_color = "FFFFFF",
+
+ -- Text style
+ font = "sans-serif",
+ font_mono = "monospace", -- monospaced digits are sufficient
+ font_size = 8,
+ font_color = "FFFFFF",
+ border_size = 0.8,
+ border_color = "262626",
+ shadow_x_offset = 0.0,
+ shadow_y_offset = 0.0,
+ shadow_color = "000000",
+ alpha = "11",
+
+ -- Custom header for ASS tags to style the text output.
+ -- Specifying this will ignore the text style values above and just
+ -- use this string instead.
+ custom_header = "",
+
+ -- Text formatting
+ -- With ASS
+ ass_nl = "\\N",
+ ass_indent = "\\h\\h\\h\\h\\h",
+ ass_prefix_sep = "\\h\\h",
+ ass_b1 = "{\\b1}",
+ ass_b0 = "{\\b0}",
+ ass_it1 = "{\\i1}",
+ ass_it0 = "{\\i0}",
+ -- Without ASS
+ no_ass_nl = "\n",
+ no_ass_indent = "\t",
+ no_ass_prefix_sep = " ",
+ no_ass_b1 = "\027[1m",
+ no_ass_b0 = "\027[0m",
+ no_ass_it1 = "\027[3m",
+ no_ass_it0 = "\027[0m",
+
+ bindlist = "no", -- print page 4 to the terminal on startup and quit mpv
+}
+options.read_options(o)
+
+local format = string.format
+local max = math.max
+local min = math.min
+
+-- Function used to record performance data
+local recorder = nil
+-- Timer used for redrawing (toggling) and clearing the screen (oneshot)
+local display_timer = nil
+-- Timer used to update cache stats.
+local cache_recorder_timer = nil
+-- Current page and <page key>:<page function> mappings
+local curr_page = o.key_page_1
+local pages = {}
+local scroll_bound = false
+local tm_viz_prev = nil
+-- Save these sequences locally as we'll need them a lot
+local ass_start = mp.get_property_osd("osd-ass-cc/0")
+local ass_stop = mp.get_property_osd("osd-ass-cc/1")
+-- Ring buffers for the values used to construct a graph.
+-- .pos denotes the current position, .len the buffer length
+-- .max is the max value in the corresponding buffer
+local vsratio_buf, vsjitter_buf
+local function init_buffers()
+ vsratio_buf = {0, pos = 1, len = 50, max = 0}
+ vsjitter_buf = {0, pos = 1, len = 50, max = 0}
+end
+local cache_ahead_buf, cache_speed_buf
+local perf_buffers = {}
+
+local function graph_add_value(graph, value)
+ graph.pos = (graph.pos % graph.len) + 1
+ graph[graph.pos] = value
+ graph.max = max(graph.max, value)
+end
+
+-- "\\<U+2060>" in UTF-8 (U+2060 is WORD-JOINER)
+local ESC_BACKSLASH = "\\" .. string.char(0xE2, 0x81, 0xA0)
+
+local function no_ASS(t)
+ if not o.use_ass then
+ return t
+ elseif not o.persistent_overlay then
+ -- mp.osd_message supports ass-escape using osd-ass-cc/{0|1}
+ return ass_stop .. t .. ass_start
+ else
+ -- mp.set_osd_ass doesn't support ass-escape. roll our own.
+ -- similar to mpv's sub/osd_libass.c:mangle_ass(...), excluding
+ -- space after newlines because no_ASS is not used with multi-line.
+ -- space at the beginning is replaced with "\\h" because it matters
+ -- at the beginning of a line, and we can't know where our output
+ -- ends up. no issue if it ends up at the middle of a line.
+ return tostring(t)
+ :gsub("\\", ESC_BACKSLASH)
+ :gsub("{", "\\{")
+ :gsub("^ ", "\\h")
+ end
+end
+
+
+local function b(t)
+ return o.b1 .. t .. o.b0
+end
+
+
+local function it(t)
+ return o.it1 .. t .. o.it0
+end
+
+
+local function text_style()
+ if not o.use_ass then
+ return ""
+ end
+ if o.custom_header and o.custom_header ~= "" then
+ return o.custom_header
+ else
+ local has_shadow = mp.get_property('osd-back-color'):sub(2, 3) == '00'
+ return format("{\\r\\an7\\fs%d\\fn%s\\bord%f\\3c&H%s&" ..
+ "\\1c&H%s&\\1a&H%s&\\3a&H%s&" ..
+ (has_shadow and "\\4a&H%s&\\xshad%f\\yshad%f\\4c&H%s&}" or "}"),
+ o.font_size, o.font, o.border_size,
+ o.border_color, o.font_color, o.alpha, o.alpha, o.alpha,
+ o.shadow_x_offset, o.shadow_y_offset, o.shadow_color)
+ end
+end
+
+
+local function has_vo_window()
+ return mp.get_property_native("vo-configured") and mp.get_property_native("video-osd")
+end
+
+
+-- Generate a graph from the given values.
+-- Returns an ASS formatted vector drawing as string.
+--
+-- values: Array/table of numbers representing the data. Used like a ring buffer
+-- it will get iterated backwards `len` times starting at position `i`.
+-- i : Index of the latest data value in `values`.
+-- len : The length/amount of numbers in `values`.
+-- v_max : The maximum number in `values`. It is used to scale all data
+-- values to a range of 0 to `v_max`.
+-- v_avg : The average number in `values`. It is used to try and center graphs
+-- if possible. May be left as nil
+-- scale : A value that will be multiplied with all data values.
+-- x_tics: Horizontal width multiplier for the steps
+local function generate_graph(values, i, len, v_max, v_avg, scale, x_tics)
+ -- Check if at least one value exists
+ if not values[i] then
+ return ""
+ end
+
+ local x_max = (len - 1) * x_tics
+ local y_offset = o.border_size
+ local y_max = o.font_size * 0.66
+ local x = 0
+
+ if v_max > 0 then
+ -- try and center the graph if possible, but avoid going above `scale`
+ if v_avg and v_avg > 0 then
+ scale = min(scale, v_max / (2 * v_avg))
+ end
+ scale = scale * y_max / v_max
+ end -- else if v_max==0 then all values are 0 and scale doesn't matter
+
+ local s = {format("m 0 0 n %f %f l ", x, y_max - scale * values[i])}
+ i = ((i - 2) % len) + 1
+
+ for p = 1, len - 1 do
+ if values[i] then
+ x = x - x_tics
+ s[#s+1] = format("%f %f ", x, y_max - scale * values[i])
+ end
+ i = ((i - 2) % len) + 1
+ end
+
+ s[#s+1] = format("%f %f %f %f", x, y_max, 0, y_max)
+
+ local bg_box = format("{\\bord0.5}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0",
+ o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max)
+ return format("%s{\\r}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s",
+ o.prefix_sep, y_offset, bg_box, o.plot_color, table.concat(s), text_style())
+end
+
+
+local function append(s, str, attr)
+ if not str then
+ return false
+ end
+ attr.prefix_sep = attr.prefix_sep or o.prefix_sep
+ attr.indent = attr.indent or o.indent
+ attr.nl = attr.nl or o.nl
+ attr.suffix = attr.suffix or ""
+ attr.prefix = attr.prefix or ""
+ attr.no_prefix_markup = attr.no_prefix_markup or false
+ attr.prefix = attr.no_prefix_markup and attr.prefix or b(attr.prefix)
+ s[#s+1] = format("%s%s%s%s%s%s", attr.nl, attr.indent,
+ attr.prefix, attr.prefix_sep, no_ASS(str), attr.suffix)
+ return true
+end
+
+
+-- Format and append a property.
+-- A property whose value is either `nil` or empty (hereafter called "invalid")
+-- is skipped and not appended.
+-- Returns `false` in case nothing was appended, otherwise `true`.
+--
+-- s : Table containing strings.
+-- prop : The property to query and format (based on its OSD representation).
+-- attr : Optional table to overwrite certain (formatting) attributes for
+-- this property.
+-- exclude: Optional table containing keys which are considered invalid values
+-- for this property. Specifying this will replace empty string as
+-- default invalid value (nil is always invalid).
+local function append_property(s, prop, attr, excluded)
+ excluded = excluded or {[""] = true}
+ local ret = mp.get_property_osd(prop)
+ if not ret or excluded[ret] then
+ if o.debug then
+ print("No value for property: " .. prop)
+ end
+ return false
+ end
+ return append(s, ret, attr)
+end
+
+local function sorted_keys(t, comp_fn)
+ local keys = {}
+ for k,_ in pairs(t) do
+ keys[#keys+1] = k
+ end
+ table.sort(keys, comp_fn)
+ return keys
+end
+
+local function append_perfdata(s, dedicated_page, print_passes)
+ local vo_p = mp.get_property_native("vo-passes")
+ if not vo_p then
+ return
+ end
+
+ local ds = mp.get_property_bool("display-sync-active", false)
+ local target_fps = ds and mp.get_property_number("display-fps", 0)
+ or mp.get_property_number("container-fps", 0)
+ if target_fps > 0 then target_fps = 1 / target_fps * 1e9 end
+
+ -- Sums of all last/avg/peak values
+ local last_s, avg_s, peak_s = {}, {}, {}
+ for frame, data in pairs(vo_p) do
+ last_s[frame], avg_s[frame], peak_s[frame] = 0, 0, 0
+ for _, pass in ipairs(data) do
+ last_s[frame] = last_s[frame] + pass["last"]
+ avg_s[frame] = avg_s[frame] + pass["avg"]
+ peak_s[frame] = peak_s[frame] + pass["peak"]
+ end
+ end
+
+ -- Pretty print measured time
+ local function pp(i)
+ -- rescale to microseconds for a saner display
+ return format("%5d", i / 1000)
+ end
+
+ -- Format n/m with a font weight based on the ratio
+ local function p(n, m)
+ local i = 0
+ if m > 0 then
+ i = tonumber(n) / m
+ end
+ -- Calculate font weight. 100 is minimum, 400 is normal, 700 bold, 900 is max
+ local w = (700 * math.sqrt(i)) + 200
+ return format("{\\b%d}%2d%%{\\b0}", w, i * 100)
+ end
+
+ -- ensure that the fixed title is one element and every scrollable line is
+ -- also one single element.
+ s[#s+1] = format("%s%s%s%s{\\fs%s}%s%s{\\fs%s}",
+ dedicated_page and "" or o.nl, dedicated_page and "" or o.indent,
+ b("Frame Timings:"), o.prefix_sep, o.font_size * 0.66,
+ "(last/average/peak μs)",
+ dedicated_page and " (hint: scroll with ↑↓)" or "", o.font_size)
+
+ for _,frame in ipairs(sorted_keys(vo_p)) do -- ensure fixed display order
+ local data = vo_p[frame]
+ local f = "%s%s%s{\\fn%s}%s / %s / %s %s%s{\\fn%s}%s%s%s"
+
+ if print_passes then
+ s[#s+1] = format("%s%s%s:", o.nl, o.indent,
+ b(frame:gsub("^%l", string.upper)))
+
+ for _, pass in ipairs(data) do
+ s[#s+1] = format(f, o.nl, o.indent, o.indent,
+ o.font_mono, pp(pass["last"]),
+ pp(pass["avg"]), pp(pass["peak"]),
+ o.prefix_sep .. "\\h\\h", p(pass["last"], last_s[frame]),
+ o.font, o.prefix_sep, o.prefix_sep, pass["desc"])
+
+ if o.plot_perfdata and o.use_ass then
+ -- use the same line that was already started for this iteration
+ s[#s] = s[#s] ..
+ generate_graph(pass["samples"], pass["count"],
+ pass["count"], pass["peak"],
+ pass["avg"], 0.9, 0.25)
+ end
+ end
+
+ -- Print sum of timing values as "Total"
+ s[#s+1] = format(f, o.nl, o.indent, o.indent,
+ o.font_mono, pp(last_s[frame]),
+ pp(avg_s[frame]), pp(peak_s[frame]),
+ o.prefix_sep, b("Total"), o.font, "", "", "")
+ else
+ -- for the simplified view, we just print the sum of each pass
+ s[#s+1] = format(f, o.nl, o.indent, o.indent, o.font_mono,
+ pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]),
+ "", "", o.font, o.prefix_sep, o.prefix_sep,
+ frame:gsub("^%l", string.upper))
+ end
+ end
+end
+
+local function ellipsis(s, maxlen)
+ if not maxlen or s:len() <= maxlen then return s end
+ return s:sub(1, maxlen - 3) .. "..."
+end
+
+-- command prefix tokens to strip - includes generic property commands
+local cmd_prefixes = {
+ osd_auto=1, no_osd=1, osd_bar=1, osd_msg=1, osd_msg_bar=1, raw=1, sync=1,
+ async=1, expand_properties=1, repeatable=1, set=1, add=1, multiply=1,
+ toggle=1, cycle=1, cycle_values=1, ["!reverse"]=1, change_list=1,
+}
+-- commands/writable-properties prefix sub-words (followed by -) to strip
+local name_prefixes = {
+ define=1, delete=1, enable=1, disable=1, dump=1, write=1, drop=1, revert=1,
+ ab=1, hr=1, secondary=1, current=1,
+}
+-- extract a command "subject" from a command string, by removing all
+-- generic prefix tokens and then returning the first interesting sub-word
+-- of the next token. For target-script name we also check another token.
+-- The tokenizer works fine for things we care about - valid mpv commands,
+-- properties and script names, possibly quoted, white-space[s]-separated.
+-- It's decent in practice, and worst case is "incorrect" subject.
+local function cmd_subject(cmd)
+ cmd = cmd:gsub(";.*", ""):gsub("%-", "_") -- only first cmd, s/-/_/
+ local TOKEN = '^%s*["\']?([%w_!]*)' -- captures+ends before (maybe) final "
+ local tok, sname, subw
+
+ repeat tok, cmd = cmd:match(TOKEN .. '["\']?(.*)')
+ until not cmd_prefixes[tok]
+ -- tok is the 1st non-generic command/property name token, cmd is the rest
+
+ sname = tok == "script_message_to" and cmd:match(TOKEN)
+ or tok == "script_binding" and cmd:match(TOKEN .. "/")
+ if sname and sname ~= "" then
+ return "script: " .. sname
+ end
+
+ -- return the first sub-word of tok which is not a useless prefix
+ repeat subw, tok = tok:match("([^_]*)_?(.*)")
+ until tok == "" or not name_prefixes[subw]
+ return subw:len() > 1 and subw or "[unknown]"
+end
+
+-- key names are valid UTF-8, ascii7 except maybe the last/only codepoint.
+-- we count codepoints and ignore wcwidth. no need for grapheme clusters.
+-- our error for alignment is at most one cell (if last CP is double-width).
+-- (if k was valid but arbitrary: we'd count all bytes <0x80 or >=0xc0)
+local function keyname_cells(k)
+ local klen = k:len()
+ if klen > 1 and k:byte(klen) >= 0x80 then -- last/only CP is not ascii7
+ repeat klen = klen-1
+ until klen == 1 or k:byte(klen) >= 0xc0 -- last CP begins at klen
+ end
+ return klen
+end
+
+local function get_kbinfo_lines(width)
+ -- active keys: only highest priority of each key, and not our (stats) keys
+ local bindings = mp.get_property_native("input-bindings", {})
+ local active = {} -- map: key-name -> bind-info
+ for _, bind in pairs(bindings) do
+ if bind.priority >= 0 and (
+ not active[bind.key] or
+ (active[bind.key].is_weak and not bind.is_weak) or
+ (bind.is_weak == active[bind.key].is_weak and
+ bind.priority > active[bind.key].priority)
+ ) and not bind.cmd:find("script-binding stats/__forced_", 1, true)
+ then
+ active[bind.key] = bind
+ end
+ end
+
+ -- make an array, find max key len, add sort keys (.subject/.mods[_count])
+ local ordered = {}
+ local kspaces = "" -- as many spaces as the longest key name
+ for _, bind in pairs(active) do
+ bind.subject = cmd_subject(bind.cmd)
+ if bind.subject ~= "ignore" then
+ ordered[#ordered+1] = bind
+ _,_, bind.mods = bind.key:find("(.*)%+.")
+ _, bind.mods_count = bind.key:gsub("%+.", "")
+ if bind.key:len() > kspaces:len() then
+ kspaces = string.rep(" ", bind.key:len())
+ end
+ end
+ end
+
+ local function align_right(key)
+ return kspaces:sub(keyname_cells(key)) .. key
+ end
+
+ -- sort by: subject, mod(ifier)s count, mods, key-len, lowercase-key, key
+ table.sort(ordered, function(a, b)
+ if a.subject ~= b.subject then
+ return a.subject < b.subject
+ elseif a.mods_count ~= b.mods_count then
+ return a.mods_count < b.mods_count
+ elseif a.mods ~= b.mods then
+ return a.mods < b.mods
+ elseif a.key:len() ~= b.key:len() then
+ return a.key:len() < b.key:len()
+ elseif a.key:lower() ~= b.key:lower() then
+ return a.key:lower() < b.key:lower()
+ else
+ return a.key > b.key -- only case differs, lowercase first
+ end
+ end)
+
+ -- key/subject pre/post formatting for terminal/ass.
+ -- key/subject alignment uses spaces (with mono font if ass)
+ -- word-wrapping is disabled for ass, or cut at 79 for the terminal
+ local LTR = string.char(0xE2, 0x80, 0x8E) -- U+200E Left To Right mark
+ local term = not o.use_ass
+ local kpre = term and "" or format("{\\q2\\fn%s}%s", o.font_mono, LTR)
+ local kpost = term and " " or format(" {\\fn%s}", o.font)
+ local spre = term and kspaces .. " "
+ or format("{\\q2\\fn%s}%s {\\fn%s}{\\fs%d\\u1}",
+ o.font_mono, kspaces, o.font, 1.3*o.font_size)
+ local spost = term and "" or format("{\\u0\\fs%d}", o.font_size)
+ local _, itabs = o.indent:gsub("\t", "")
+ local cutoff = term and (width or 79) - o.indent:len() - itabs * 7 - spre:len()
+
+ -- create the display lines
+ local info_lines = {}
+ local subject = nil
+ for _, bind in ipairs(ordered) do
+ if bind.subject ~= subject then -- new subject (title)
+ subject = bind.subject
+ append(info_lines, "", {})
+ append(info_lines, "", { prefix = spre .. subject .. spost })
+ end
+ if bind.comment then
+ bind.cmd = bind.cmd .. " # " .. bind.comment
+ end
+ append(info_lines, ellipsis(bind.cmd, cutoff),
+ { prefix = kpre .. no_ASS(align_right(bind.key)) .. kpost })
+ end
+ return info_lines
+end
+
+local function append_general_perfdata(s, offset)
+ local perf_info = mp.get_property_native("perf-info") or {}
+ local count = 0
+ for _, data in ipairs(perf_info) do
+ count = count + 1
+ end
+ offset = max(1, min((offset or 1), count))
+
+ local i = 0
+ for _, data in ipairs(perf_info) do
+ i = i + 1
+ if i >= offset then
+ append(s, data.text or data.value, {prefix="["..tostring(i).."] "..data.name..":"})
+
+ if o.plot_perfdata and o.use_ass and data.value then
+ buf = perf_buffers[data.name]
+ if not buf then
+ buf = {0, pos = 1, len = 50, max = 0}
+ perf_buffers[data.name] = buf
+ end
+ graph_add_value(buf, data.value)
+ s[#s+1] = generate_graph(buf, buf.pos, buf.len, buf.max, nil, 0.8, 1)
+ end
+ end
+ end
+ return offset
+end
+
+local function append_display_sync(s)
+ if not mp.get_property_bool("display-sync-active", false) then
+ return
+ end
+
+ local vspeed = append_property(s, "video-speed-correction", {prefix="DS:"})
+ if vspeed then
+ append_property(s, "audio-speed-correction",
+ {prefix="/", nl="", indent=" ", prefix_sep=" ", no_prefix_markup=true})
+ else
+ append_property(s, "audio-speed-correction",
+ {prefix="DS:" .. o.prefix_sep .. " - / ", prefix_sep=""})
+ end
+
+ append_property(s, "mistimed-frame-count", {prefix="Mistimed:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep})
+ append_property(s, "vo-delayed-frame-count", {prefix="Delayed:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep})
+
+ -- As we need to plot some graphs we print jitter and ratio on their own lines
+ if not display_timer.oneshot and (o.plot_vsync_ratio or o.plot_vsync_jitter) and o.use_ass then
+ local ratio_graph = ""
+ local jitter_graph = ""
+ if o.plot_vsync_ratio then
+ ratio_graph = generate_graph(vsratio_buf, vsratio_buf.pos, vsratio_buf.len, vsratio_buf.max, nil, 0.8, 1)
+ end
+ if o.plot_vsync_jitter then
+ jitter_graph = generate_graph(vsjitter_buf, vsjitter_buf.pos, vsjitter_buf.len, vsjitter_buf.max, nil, 0.8, 1)
+ end
+ append_property(s, "vsync-ratio", {prefix="VSync Ratio:", suffix=o.prefix_sep .. ratio_graph})
+ append_property(s, "vsync-jitter", {prefix="VSync Jitter:", suffix=o.prefix_sep .. jitter_graph})
+ else
+ -- Since no graph is needed we can print ratio/jitter on the same line and save some space
+ local vr = append_property(s, "vsync-ratio", {prefix="VSync Ratio:"})
+ append_property(s, "vsync-jitter", {prefix="VSync Jitter:",
+ nl=vr and "" or o.nl,
+ indent=vr and o.prefix_sep .. o.prefix_sep})
+ end
+end
+
+
+local function append_filters(s, prop, prefix)
+ local length = 0
+ local filters = {}
+
+ for _,f in ipairs(mp.get_property_native(prop, {})) do
+ local n = f.name
+ if f.enabled ~= nil and not f.enabled then
+ n = n .. " (disabled)"
+ end
+
+ if f.label ~= nil then
+ n = "@" .. f.label .. ": " .. n
+ end
+
+ local p = {}
+ for _,key in ipairs(sorted_keys(f.params)) do
+ p[#p+1] = key .. "=" .. f.params[key]
+ end
+ if #p > 0 then
+ p = " [" .. table.concat(p, " ") .. "]"
+ else
+ p = ""
+ end
+
+ length = length + n:len() + p:len()
+ filters[#filters+1] = no_ASS(n) .. it(no_ASS(p))
+ end
+
+ if #filters > 0 then
+ local ret
+ if length < o.filter_params_max_length then
+ ret = table.concat(filters, ", ")
+ else
+ local sep = o.nl .. o.indent .. o.indent
+ ret = sep .. table.concat(filters, sep)
+ end
+ s[#s+1] = o.nl .. o.indent .. b(prefix) .. o.prefix_sep .. ret
+ end
+end
+
+
+local function add_header(s)
+ s[#s+1] = text_style()
+end
+
+
+local function add_file(s)
+ append(s, "", {prefix="File:", nl="", indent=""})
+ append_property(s, "filename", {prefix_sep="", nl="", indent=""})
+ if not (mp.get_property_osd("filename") == mp.get_property_osd("media-title")) then
+ append_property(s, "media-title", {prefix="Title:"})
+ end
+
+ local editions = mp.get_property_number("editions")
+ local edition = mp.get_property_number("current-edition")
+ local ed_cond = (edition and editions > 1)
+ if ed_cond then
+ append_property(s, "edition-list/" .. tostring(edition) .. "/title",
+ {prefix="Edition:"})
+ append_property(s, "edition-list/count",
+ {prefix="(" .. tostring(edition + 1) .. "/", suffix=")", nl="",
+ indent=" ", prefix_sep=" ", no_prefix_markup=true})
+ end
+
+ local ch_index = mp.get_property_number("chapter")
+ if ch_index and ch_index >= 0 then
+ append_property(s, "chapter-list/" .. tostring(ch_index) .. "/title", {prefix="Chapter:",
+ nl=ed_cond and "" or o.nl})
+ append_property(s, "chapter-list/count",
+ {prefix="(" .. tostring(ch_index + 1) .. " /", suffix=")", nl="",
+ indent=" ", prefix_sep=" ", no_prefix_markup=true})
+ end
+
+ local fs = append_property(s, "file-size", {prefix="Size:"})
+ append_property(s, "file-format", {prefix="Format/Protocol:",
+ nl=fs and "" or o.nl,
+ indent=fs and o.prefix_sep .. o.prefix_sep})
+
+ local demuxer_cache = mp.get_property_native("demuxer-cache-state", {})
+ if demuxer_cache["fw-bytes"] then
+ demuxer_cache = demuxer_cache["fw-bytes"] -- returns bytes
+ else
+ demuxer_cache = 0
+ end
+ local demuxer_secs = mp.get_property_number("demuxer-cache-duration", 0)
+ if demuxer_cache + demuxer_secs > 0 then
+ append(s, utils.format_bytes_humanized(demuxer_cache), {prefix="Total Cache:"})
+ append(s, format("%.1f", demuxer_secs), {prefix="(", suffix=" sec)", nl="",
+ no_prefix_markup=true, prefix_sep="", indent=o.prefix_sep})
+ end
+end
+
+
+local function crop_noop(w, h, r)
+ return r["crop-x"] == 0 and r["crop-y"] == 0 and
+ r["crop-w"] == w and r["crop-h"] == h
+end
+
+
+local function crop_equal(r, ro)
+ return r["crop-x"] == ro["crop-x"] and r["crop-y"] == ro["crop-y"] and
+ r["crop-w"] == ro["crop-w"] and r["crop-h"] == ro["crop-h"]
+end
+
+
+local function append_resolution(s, r, prefix, w_prop, h_prop, video_res)
+ if not r then
+ return
+ end
+ w_prop = w_prop or "w"
+ h_prop = h_prop or "h"
+ if append(s, r[w_prop], {prefix=prefix}) then
+ append(s, r[h_prop], {prefix="x", nl="", indent=" ", prefix_sep=" ",
+ no_prefix_markup=true})
+ if r["aspect"] ~= nil and not video_res then
+ append(s, format("%.2f:1", r["aspect"]), {prefix="", nl="", indent="",
+ no_prefix_markup=true})
+ append(s, r["aspect-name"], {prefix="(", suffix=")", nl="", indent=" ",
+ prefix_sep="", no_prefix_markup=true})
+ end
+ if r["sar"] ~= nil and video_res then
+ append(s, format("%.2f:1", r["sar"]), {prefix="", nl="", indent="",
+ no_prefix_markup=true})
+ append(s, r["sar-name"], {prefix="(", suffix=")", nl="", indent=" ",
+ prefix_sep="", no_prefix_markup=true})
+ end
+ if r["s"] then
+ append(s, format("%.2f", r["s"]), {prefix="(", suffix="x)", nl="",
+ indent=o.prefix_sep, prefix_sep="",
+ no_prefix_markup=true})
+ end
+ -- We can skip crop if it is the same as video decoded resolution
+ if r["crop-w"] and (not video_res or
+ not crop_noop(r[w_prop], r[h_prop], r)) then
+ append(s, format("[x: %d, y: %d, w: %d, h: %d]",
+ r["crop-x"], r["crop-y"], r["crop-w"], r["crop-h"]),
+ {prefix="", nl="", indent="", no_prefix_markup=true})
+ end
+ end
+end
+
+
+local function pq_eotf(x)
+ if not x then
+ return x;
+ end
+
+ local PQ_M1 = 2610.0 / 4096 * 1.0 / 4
+ local PQ_M2 = 2523.0 / 4096 * 128
+ local PQ_C1 = 3424.0 / 4096
+ local PQ_C2 = 2413.0 / 4096 * 32
+ local PQ_C3 = 2392.0 / 4096 * 32
+
+ x = x ^ (1.0 / PQ_M2)
+ x = max(x - PQ_C1, 0.0) / (PQ_C2 - PQ_C3 * x)
+ x = x ^ (1.0 / PQ_M1)
+ x = x * 10000.0
+
+ return x
+end
+
+
+local function append_hdr(s, hdr, video_out)
+ if not hdr then
+ return
+ end
+
+ local function should_show(val)
+ return val and val ~= 203 and val > 0
+ end
+
+ -- If we are printing video out parameters it is just display, not mastering
+ local display_prefix = video_out and "Display:" or "Mastering display:"
+
+ local indent = ""
+
+ if should_show(hdr["max-cll"]) or should_show(hdr["max-luma"]) then
+ append(s, "", {prefix="HDR10:"})
+ if hdr["min-luma"] and should_show(hdr["max-luma"]) then
+ -- libplacebo uses close to zero values as "defined zero"
+ hdr["min-luma"] = hdr["min-luma"] <= 1e-6 and 0 or hdr["min-luma"]
+ append(s, format("%.2g / %.0f", hdr["min-luma"], hdr["max-luma"]),
+ {prefix=display_prefix, suffix=" cd/m²", nl="", indent=indent})
+ indent = o.prefix_sep .. o.prefix_sep
+ end
+ if should_show(hdr["max-cll"]) then
+ append(s, hdr["max-cll"], {prefix="MaxCLL:", suffix=" cd/m²", nl="",
+ indent=indent})
+ indent = o.prefix_sep .. o.prefix_sep
+ end
+ if hdr["max-fall"] and hdr["max-fall"] > 0 then
+ append(s, hdr["max-fall"], {prefix="MaxFALL:", suffix=" cd/m²", nl="",
+ indent=indent})
+ end
+ end
+
+ indent = o.prefix_sep .. o.prefix_sep
+
+ if hdr["scene-max-r"] or hdr["scene-max-g"] or
+ hdr["scene-max-b"] or hdr["scene-avg"] then
+ append(s, "", {prefix="HDR10+:"})
+ append(s, format("%.1f / %.1f / %.1f", hdr["scene-max-r"] or 0,
+ hdr["scene-max-g"] or 0, hdr["scene-max-b"] or 0),
+ {prefix="MaxRGB:", suffix=" cd/m²", nl="", indent=""})
+ append(s, format("%.1f", hdr["scene-avg"] or 0),
+ {prefix="Avg:", suffix=" cd/m²", nl="", indent=indent})
+ end
+
+ if hdr["max-pq-y"] and hdr["avg-pq-y"] then
+ append(s, "", {prefix="PQ(Y):"})
+ append(s, format("%.2f cd/m² (%.2f%% PQ)", pq_eotf(hdr["max-pq-y"]),
+ hdr["max-pq-y"] * 100), {prefix="Max:", nl="",
+ indent=""})
+ append(s, format("%.2f cd/m² (%.2f%% PQ)", pq_eotf(hdr["avg-pq-y"]),
+ hdr["avg-pq-y"] * 100), {prefix="Avg:", nl="",
+ indent=indent})
+ end
+end
+
+
+local function append_img_params(s, r, ro)
+ if not r then
+ return
+ end
+
+ append_resolution(s, r, "Resolution:", "w", "h", true)
+ if ro and (r["w"] ~= ro["dw"] or r["h"] ~= ro["dh"]) then
+ if ro["crop-w"] and (crop_noop(r["w"], r["h"], ro) or crop_equal(r, ro)) then
+ ro["crop-w"] = nil
+ end
+ append_resolution(s, ro, "Output Resolution:", "dw", "dh")
+ end
+
+ local indent = o.prefix_sep .. o.prefix_sep
+
+ local pixel_format = r["hw-pixelformat"] or r["pixelformat"]
+ append(s, pixel_format, {prefix="Format:"})
+ append(s, r["colorlevels"], {prefix="Levels:", nl="", indent=indent})
+ if r["chroma-location"] and r["chroma-location"] ~= "unknown" then
+ append(s, r["chroma-location"], {prefix="Chroma Loc:", nl="", indent=indent})
+ end
+
+ -- Group these together to save vertical space
+ append(s, r["colormatrix"], {prefix="Colormatrix:"})
+ append(s, r["primaries"], {prefix="Primaries:", nl="", indent=indent})
+ append(s, r["gamma"], {prefix="Transfer:", nl="", indent=indent})
+end
+
+
+local function append_fps(s, prop, eprop)
+ local fps = mp.get_property_osd(prop)
+ local efps = mp.get_property_osd(eprop)
+ local single = fps ~= "" and efps ~= "" and fps == efps
+ local unit = prop == "display-fps" and " Hz" or " fps"
+ local suffix = single and "" or " (specified)"
+ local esuffix = single and "" or " (estimated)"
+ local prefix = prop == "display-fps" and "Refresh Rate:" or "Frame rate:"
+ local nl = o.nl
+ local indent = o.indent
+
+ if fps ~= "" and append(s, fps, {prefix=prefix, suffix=unit .. suffix}) then
+ prefix = ""
+ nl = ""
+ indent = ""
+ end
+
+ if not single and efps ~= "" then
+ append(s, efps,
+ {prefix=prefix, suffix=unit .. esuffix, nl=nl, indent=indent})
+ end
+end
+
+
+local function add_video_out(s)
+ local vo = mp.get_property_native("current-vo")
+ if not vo then
+ return
+ end
+
+ append(s, "", {prefix=o.nl .. o.nl .. "Display:", nl="", indent=""})
+ append(s, vo, {prefix_sep="", nl="", indent=""})
+ append_property(s, "display-names", {prefix_sep="", prefix="(", suffix=")",
+ no_prefix_markup=true, nl="", indent=" "})
+ append_property(s, "avsync", {prefix="A-V:"})
+ append_fps(s, "display-fps", "estimated-display-fps")
+ if append_property(s, "decoder-frame-drop-count",
+ {prefix="Dropped Frames:", suffix=" (decoder)"}) then
+ append_property(s, "frame-drop-count", {suffix=" (output)", nl="", indent=""})
+ end
+ append_display_sync(s)
+ append_perfdata(s, false, o.print_perfdata_passes)
+
+ if mp.get_property_native("deinterlace") then
+ append_property(s, "deinterlace", {prefix="Deinterlacing:"})
+ end
+
+ local scale = nil
+ if not mp.get_property_native("fullscreen") then
+ scale = mp.get_property_native("current-window-scale")
+ end
+
+ local r = mp.get_property_native("video-target-params")
+ if not r then
+ local osd_dims = mp.get_property_native("osd-dimensions")
+ local scaled_width = osd_dims["w"] - osd_dims["ml"] - osd_dims["mr"]
+ local scaled_height = osd_dims["h"] - osd_dims["mt"] - osd_dims["mb"]
+ append_resolution(s, {w=scaled_width, h=scaled_height, s=scale},
+ "Resolution:")
+ return
+ end
+
+ -- Add window scale
+ r["s"] = scale
+
+ append_img_params(s, r)
+ append_hdr(s, r, true)
+end
+
+
+local function add_video(s)
+ local r = mp.get_property_native("video-params")
+ local ro = mp.get_property_native("video-out-params")
+ -- in case of e.g. lavfi-complex there can be no input video, only output
+ if not r then
+ r = ro
+ end
+ if not r then
+ return
+ end
+
+ local osd_dims = mp.get_property_native("osd-dimensions")
+ local scaled_width = osd_dims["w"] - osd_dims["ml"] - osd_dims["mr"]
+ local scaled_height = osd_dims["h"] - osd_dims["mt"] - osd_dims["mb"]
+
+ append(s, "", {prefix=o.nl .. o.nl .. "Video:", nl="", indent=""})
+ if append_property(s, "video-codec", {prefix_sep="", nl="", indent=""}) then
+ append_property(s, "hwdec-current", {prefix="HW:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep,
+ no_prefix_markup=false, suffix=""}, {no=true, [""]=true})
+ end
+ local has_prefix = false
+ if o.show_frame_info then
+ if append_property(s, "estimated-frame-number", {prefix="Frame:"}) then
+ append_property(s, "estimated-frame-count", {indent=" / ", nl="",
+ prefix_sep=""})
+ has_prefix = true
+ end
+ local frame_info = mp.get_property_native("video-frame-info")
+ if frame_info and frame_info["picture-type"] then
+ local attrs = has_prefix and {prefix="(", suffix=")", indent=" ", nl="",
+ prefix_sep="", no_prefix_markup=true}
+ or {prefix="Picture Type:"}
+ append(s, frame_info["picture-type"], attrs)
+ has_prefix = true
+ end
+ if frame_info and frame_info["interlaced"] then
+ local attrs = has_prefix and {indent=" ", nl="", prefix_sep=""}
+ or {prefix="Picture Type:"}
+ append(s, "Interlaced", attrs)
+ end
+ end
+
+ if mp.get_property_native("current-tracks/video/image") == false then
+ append_fps(s, "container-fps", "estimated-vf-fps")
+ end
+ append_img_params(s, r, ro)
+ append_hdr(s, ro)
+ append_property(s, "packet-video-bitrate", {prefix="Bitrate:", suffix=" kbps"})
+ append_filters(s, "vf", "Filters:")
+end
+
+
+local function add_audio(s)
+ local r = mp.get_property_native("audio-params")
+ -- in case of e.g. lavfi-complex there can be no input audio, only output
+ if not r then
+ r = mp.get_property_native("audio-out-params")
+ end
+ if not r then
+ return
+ end
+
+ append(s, "", {prefix=o.nl .. o.nl .. "Audio:", nl="", indent=""})
+ append_property(s, "audio-codec", {prefix_sep="", nl="", indent=""})
+ local cc = append(s, r["channel-count"], {prefix="Channels:"})
+ append(s, r["format"], {prefix="Format:", nl=cc and "" or o.nl,
+ indent=cc and o.prefix_sep .. o.prefix_sep})
+ append(s, r["samplerate"], {prefix="Sample Rate:", suffix=" Hz"})
+ append_property(s, "packet-audio-bitrate", {prefix="Bitrate:", suffix=" kbps"})
+ append_filters(s, "af", "Filters:")
+end
+
+
+-- Determine whether ASS formatting shall/can be used and set formatting sequences
+local function eval_ass_formatting()
+ o.use_ass = o.ass_formatting and has_vo_window()
+ if o.use_ass then
+ o.nl = o.ass_nl
+ o.indent = o.ass_indent
+ o.prefix_sep = o.ass_prefix_sep
+ o.b1 = o.ass_b1
+ o.b0 = o.ass_b0
+ o.it1 = o.ass_it1
+ o.it0 = o.ass_it0
+ else
+ o.nl = o.no_ass_nl
+ o.indent = o.no_ass_indent
+ o.prefix_sep = o.no_ass_prefix_sep
+ o.b1 = o.no_ass_b1
+ o.b0 = o.no_ass_b0
+ o.it1 = o.no_ass_it1
+ o.it0 = o.no_ass_it0
+ end
+end
+
+
+-- Returns an ASS string with "normal" stats
+local function default_stats()
+ local stats = {}
+ eval_ass_formatting()
+ add_header(stats)
+ add_file(stats)
+ add_video_out(stats)
+ add_video(stats)
+ add_audio(stats)
+ return table.concat(stats)
+end
+
+local function scroll_vo_stats(stats, fixed_items, offset)
+ local ret = {}
+ local count = #stats - fixed_items
+ offset = max(1, min((offset or 1), count))
+
+ for i, line in pairs(stats) do
+ if i <= fixed_items or i >= fixed_items + offset then
+ ret[#ret+1] = stats[i]
+ end
+ end
+ return ret, offset
+end
+
+-- Returns an ASS string with extended VO stats
+local function vo_stats()
+ local stats = {}
+ eval_ass_formatting()
+ add_header(stats)
+
+ -- first line (title) added next is considered fixed
+ local fixed_items = #stats + 1
+ append_perfdata(stats, true, true)
+
+ local page = pages[o.key_page_2]
+ stats, page.offset = scroll_vo_stats(stats, fixed_items, page.offset)
+ return table.concat(stats)
+end
+
+local kbinfo_lines = nil
+local function keybinding_info(after_scroll)
+ local header = {}
+ local page = pages[o.key_page_4]
+ eval_ass_formatting()
+ add_header(header)
+ append(header, "", {prefix=format("%s: {\\fs%s}%s{\\fs%s}", page.desc,
+ o.font_size * 0.66, "(hint: scroll with ↑↓)", o.font_size), nl="",
+ indent=""})
+
+ if not kbinfo_lines or not after_scroll then
+ kbinfo_lines = get_kbinfo_lines()
+ end
+ -- up to 20 lines for the terminal - so that mpv can also print
+ -- the status line without scrolling, and up to 40 lines for libass
+ -- because it can put a big performance toll on libass to process
+ -- many lines which end up outside (below) the screen.
+ local term = not o.use_ass
+ local nlines = #kbinfo_lines
+ page.offset = max(1, min((page.offset or 1), term and nlines - 20 or nlines))
+ local maxline = min(nlines, page.offset + (term and 20 or 40))
+ return table.concat(header) ..
+ table.concat(kbinfo_lines, "", page.offset, maxline)
+end
+
+local function perf_stats()
+ local stats = {}
+ eval_ass_formatting()
+ add_header(stats)
+ local page = pages[o.key_page_0]
+ append(stats, "", {prefix=page.desc .. ":", nl="", indent=""})
+ page.offset = append_general_perfdata(stats, page.offset)
+ return table.concat(stats)
+end
+
+local function opt_time(t)
+ if type(t) == type(1.1) then
+ return mp.format_time(t)
+ end
+ return "?"
+end
+
+-- Returns an ASS string with stats about the demuxer cache etc.
+local function cache_stats()
+ local stats = {}
+
+ eval_ass_formatting()
+ add_header(stats)
+ append(stats, "", {prefix="Cache info:", nl="", indent=""})
+
+ local info = mp.get_property_native("demuxer-cache-state")
+ if info == nil then
+ append(stats, "Unavailable.", {})
+ return table.concat(stats)
+ end
+
+ local a = info["reader-pts"]
+ local b = info["cache-end"]
+
+ append(stats, opt_time(a) .. " - " .. opt_time(b), {prefix = "Packet queue:"})
+
+ local r = nil
+ if a ~= nil and b ~= nil then
+ r = b - a
+ end
+
+ local r_graph = nil
+ if not display_timer.oneshot and o.use_ass then
+ r_graph = generate_graph(cache_ahead_buf, cache_ahead_buf.pos,
+ cache_ahead_buf.len, cache_ahead_buf.max,
+ nil, 0.8, 1)
+ r_graph = o.prefix_sep .. r_graph
+ end
+ append(stats, opt_time(r), {prefix = "Read-ahead:", suffix = r_graph})
+
+ -- These states are not necessarily exclusive. They're about potentially
+ -- separate mechanisms, whose states may be decoupled.
+ local state = "reading"
+ local seek_ts = info["debug-seeking"]
+ if seek_ts ~= nil then
+ state = "seeking (to " .. mp.format_time(seek_ts) .. ")"
+ elseif info["eof"] == true then
+ state = "eof"
+ elseif info["underrun"] then
+ state = "underrun"
+ elseif info["idle"] == true then
+ state = "inactive"
+ end
+ append(stats, state, {prefix = "State:"})
+
+ local speed = info["raw-input-rate"] or 0
+ local speed_graph = nil
+ if not display_timer.oneshot and o.use_ass then
+ speed_graph = generate_graph(cache_speed_buf, cache_speed_buf.pos,
+ cache_speed_buf.len, cache_speed_buf.max,
+ nil, 0.8, 1)
+ speed_graph = o.prefix_sep .. speed_graph
+ end
+ append(stats, utils.format_bytes_humanized(speed) .. "/s", {prefix="Speed:",
+ suffix=speed_graph})
+
+ append(stats, utils.format_bytes_humanized(info["total-bytes"]),
+ {prefix = "Total RAM:"})
+ append(stats, utils.format_bytes_humanized(info["fw-bytes"]),
+ {prefix = "Forward RAM:"})
+
+ local fc = info["file-cache-bytes"]
+ if fc ~= nil then
+ fc = utils.format_bytes_humanized(fc)
+ else
+ fc = "(disabled)"
+ end
+ append(stats, fc, {prefix = "Disk cache:"})
+
+ append(stats, info["debug-low-level-seeks"], {prefix = "Media seeks:"})
+ append(stats, info["debug-byte-level-seeks"], {prefix = "Stream seeks:"})
+
+ append(stats, "", {prefix=o.nl .. o.nl .. "Ranges:", nl="", indent=""})
+
+ append(stats, info["bof-cached"] and "yes" or "no",
+ {prefix = "Start cached:"})
+ append(stats, info["eof-cached"] and "yes" or "no",
+ {prefix = "End cached:"})
+
+ local ranges = info["seekable-ranges"] or {}
+ for n, r in ipairs(ranges) do
+ append(stats, mp.format_time(r["start"]) .. " - " ..
+ mp.format_time(r["end"]),
+ {prefix = format("Range %s:", n)})
+ end
+
+ return table.concat(stats)
+end
+
+-- Record 1 sample of cache statistics.
+-- (Unlike record_data(), this does not return a function, but runs directly.)
+local function record_cache_stats()
+ local info = mp.get_property_native("demuxer-cache-state")
+ if info == nil then
+ return
+ end
+
+ local a = info["reader-pts"]
+ local b = info["cache-end"]
+ if a ~= nil and b ~= nil then
+ graph_add_value(cache_ahead_buf, b - a)
+ end
+
+ graph_add_value(cache_speed_buf, info["raw-input-rate"] or 0)
+end
+
+cache_recorder_timer = mp.add_periodic_timer(0.25, record_cache_stats)
+cache_recorder_timer:kill()
+
+-- Current page and <page key>:<page function> mapping
+curr_page = o.key_page_1
+pages = {
+ [o.key_page_1] = { f = default_stats, desc = "Default" },
+ [o.key_page_2] = { f = vo_stats, desc = "Extended Frame Timings", scroll = true },
+ [o.key_page_3] = { f = cache_stats, desc = "Cache Statistics" },
+ [o.key_page_4] = { f = keybinding_info, desc = "Active key bindings", scroll = true },
+ [o.key_page_0] = { f = perf_stats, desc = "Internal performance info", scroll = true },
+}
+
+
+-- Returns a function to record vsratio/jitter with the specified `skip` value
+local function record_data(skip)
+ init_buffers()
+ skip = max(skip, 0)
+ local i = skip
+ return function()
+ if i < skip then
+ i = i + 1
+ return
+ else
+ i = 0
+ end
+
+ if o.plot_vsync_jitter then
+ local r = mp.get_property_number("vsync-jitter", nil)
+ if r then
+ vsjitter_buf.pos = (vsjitter_buf.pos % vsjitter_buf.len) + 1
+ vsjitter_buf[vsjitter_buf.pos] = r
+ vsjitter_buf.max = max(vsjitter_buf.max, r)
+ end
+ end
+
+ if o.plot_vsync_ratio then
+ local r = mp.get_property_number("vsync-ratio", nil)
+ if r then
+ vsratio_buf.pos = (vsratio_buf.pos % vsratio_buf.len) + 1
+ vsratio_buf[vsratio_buf.pos] = r
+ vsratio_buf.max = max(vsratio_buf.max, r)
+ end
+ end
+ end
+end
+
+-- Call the function for `page` and print it to OSD
+local function print_page(page, after_scroll)
+ -- the page functions assume we start in ass-enabled mode.
+ -- that's true for mp.set_osd_ass, but not for mp.osd_message.
+ local ass_content = pages[page].f(after_scroll)
+ if o.persistent_overlay then
+ mp.set_osd_ass(0, 0, ass_content)
+ else
+ mp.osd_message((o.use_ass and ass_start or "") .. ass_content,
+ display_timer.oneshot and o.duration or o.redraw_delay + 1)
+ end
+end
+
+
+local function clear_screen()
+ if o.persistent_overlay then mp.set_osd_ass(0, 0, "") else mp.osd_message("", 0) end
+end
+
+local function scroll_delta(d)
+ if display_timer.oneshot then display_timer:kill() ; display_timer:resume() end
+ pages[curr_page].offset = (pages[curr_page].offset or 1) + d
+ print_page(curr_page, true)
+end
+local function scroll_up() scroll_delta(-o.scroll_lines) end
+local function scroll_down() scroll_delta(o.scroll_lines) end
+
+local function reset_scroll_offsets()
+ for _, page in pairs(pages) do
+ page.offset = nil
+ end
+end
+local function bind_scroll()
+ if not scroll_bound then
+ mp.add_forced_key_binding(o.key_scroll_up, "__forced_"..o.key_scroll_up, scroll_up, {repeatable=true})
+ mp.add_forced_key_binding(o.key_scroll_down, "__forced_"..o.key_scroll_down, scroll_down, {repeatable=true})
+ scroll_bound = true
+ end
+end
+local function unbind_scroll()
+ if scroll_bound then
+ mp.remove_key_binding("__forced_"..o.key_scroll_up)
+ mp.remove_key_binding("__forced_"..o.key_scroll_down)
+ scroll_bound = false
+ end
+end
+local function update_scroll_bindings(k)
+ if pages[k].scroll then
+ bind_scroll()
+ else
+ unbind_scroll()
+ end
+end
+
+-- Add keybindings for every page
+local function add_page_bindings()
+ local function a(k)
+ return function()
+ reset_scroll_offsets()
+ update_scroll_bindings(k)
+ curr_page = k
+ print_page(k)
+ if display_timer.oneshot then display_timer:kill() ; display_timer:resume() end
+ end
+ end
+ for k, _ in pairs(pages) do
+ mp.add_forced_key_binding(k, "__forced_"..k, a(k), {repeatable=true})
+ end
+ update_scroll_bindings(curr_page)
+end
+
+
+-- Remove keybindings for every page
+local function remove_page_bindings()
+ for k, _ in pairs(pages) do
+ mp.remove_key_binding("__forced_"..k)
+ end
+ unbind_scroll()
+end
+
+
+local function process_key_binding(oneshot)
+ reset_scroll_offsets()
+ -- Stats are already being displayed
+ if display_timer:is_enabled() then
+ -- Previous and current keys were oneshot -> restart timer
+ if display_timer.oneshot and oneshot then
+ display_timer:kill()
+ print_page(curr_page)
+ display_timer:resume()
+ -- Previous and current keys were toggling -> end toggling
+ elseif not display_timer.oneshot and not oneshot then
+ display_timer:kill()
+ cache_recorder_timer:stop()
+ if tm_viz_prev ~= nil then
+ mp.set_property_native("tone-mapping-visualize", tm_viz_prev)
+ tm_viz_prev = nil
+ end
+ clear_screen()
+ remove_page_bindings()
+ if recorder then
+ mp.unobserve_property(recorder)
+ recorder = nil
+ end
+ end
+ -- No stats are being displayed yet
+ else
+ if not oneshot and (o.plot_vsync_jitter or o.plot_vsync_ratio) then
+ recorder = record_data(o.skip_frames)
+ -- Rely on the fact that "vsync-ratio" is updated at the same time.
+ -- Using "none" to get a sample any time, even if it does not change.
+ -- Will stop working if "vsync-jitter" property change notification
+ -- changes, but it's fine for an internal script.
+ mp.observe_property("vsync-jitter", "none", recorder)
+ end
+ if not oneshot and o.plot_tonemapping_lut then
+ tm_viz_prev = mp.get_property_native("tone-mapping-visualize")
+ mp.set_property_native("tone-mapping-visualize", true)
+ end
+ if not oneshot then
+ cache_ahead_buf = {0, pos = 1, len = 50, max = 0}
+ cache_speed_buf = {0, pos = 1, len = 50, max = 0}
+ cache_recorder_timer:resume()
+ end
+ display_timer:kill()
+ display_timer.oneshot = oneshot
+ display_timer.timeout = oneshot and o.duration or o.redraw_delay
+ add_page_bindings()
+ print_page(curr_page)
+ display_timer:resume()
+ end
+end
+
+
+-- Create the timer used for redrawing (toggling) or clearing the screen (oneshot)
+-- The duration here is not important and always set in process_key_binding()
+display_timer = mp.add_periodic_timer(o.duration,
+ function()
+ if display_timer.oneshot then
+ display_timer:kill() ; clear_screen() ; remove_page_bindings()
+ else
+ print_page(curr_page)
+ end
+ end)
+display_timer:kill()
+
+-- Single invocation key binding
+mp.add_key_binding(nil, "display-stats", function() process_key_binding(true) end,
+ {repeatable=true})
+
+-- Toggling key binding
+mp.add_key_binding(nil, "display-stats-toggle", function() process_key_binding(false) end,
+ {repeatable=false})
+
+-- Single invocation bindings without key, can be used in input.conf to create
+-- bindings for a specific page: "e script-binding stats/display-page-2"
+for k, _ in pairs(pages) do
+ mp.add_key_binding(nil, "display-page-" .. k,
+ function()
+ curr_page = k
+ process_key_binding(true)
+ end, {repeatable=true})
+end
+
+-- Reprint stats immediately when VO was reconfigured, only when toggled
+mp.register_event("video-reconfig",
+ function()
+ if display_timer:is_enabled() then
+ print_page(curr_page)
+ end
+ end)
+
+-- --script-opts=stats-bindlist=[-]{yes|<TERM-WIDTH>}
+if o.bindlist ~= "no" then
+ mp.command("no-osd set really-quiet yes")
+ if o.bindlist:sub(1, 1) == "-" then
+ o.bindlist = o.bindlist:sub(2)
+ o.no_ass_b0 = ""
+ o.no_ass_b1 = ""
+ end
+ local width = max(40, math.floor(tonumber(o.bindlist) or 79))
+ mp.add_timeout(0, function() -- wait for all other scripts to finish init
+ o.ass_formatting = false
+ o.no_ass_indent = " "
+ eval_ass_formatting()
+ io.write(pages[o.key_page_4].desc .. ":" ..
+ table.concat(get_kbinfo_lines(width)) .. "\n")
+ mp.command("quit")
+ end)
+end
diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua
new file mode 100644
index 0000000..3161da6
--- /dev/null
+++ b/player/lua/ytdl_hook.lua
@@ -0,0 +1,1191 @@
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+local options = require 'mp.options'
+
+local o = {
+ exclude = "",
+ try_ytdl_first = false,
+ use_manifests = false,
+ all_formats = false,
+ force_all_formats = true,
+ thumbnails = "none",
+ ytdl_path = "",
+}
+
+local ytdl = {
+ path = "",
+ paths_to_search = {"yt-dlp", "yt-dlp_x86", "youtube-dl"},
+ searched = false,
+ blacklisted = {}
+}
+
+options.read_options(o, nil, function()
+ ytdl.blacklisted = {} -- reparse o.exclude next time
+ ytdl.searched = false
+end)
+
+local chapter_list = {}
+local playlist_cookies = {}
+
+function Set (t)
+ local set = {}
+ for _, v in pairs(t) do set[v] = true end
+ return set
+end
+
+-- ?: surrogate (keep in mind that there is no lazy evaluation)
+function iif(cond, if_true, if_false)
+ if cond then
+ return if_true
+ end
+ return if_false
+end
+
+-- youtube-dl JSON name to mpv tag name
+local tag_list = {
+ ["uploader"] = "uploader",
+ ["channel_url"] = "channel_url",
+ -- these titles tend to be a bit too long, so hide them on the terminal
+ -- (default --display-tags does not include this name)
+ ["description"] = "ytdl_description",
+ -- "title" is handled by force-media-title
+ -- tags don't work with all_formats=yes
+}
+
+local safe_protos = Set {
+ "http", "https", "ftp", "ftps",
+ "rtmp", "rtmps", "rtmpe", "rtmpt", "rtmpts", "rtmpte",
+ "data"
+}
+
+-- For some sites, youtube-dl returns the audio codec (?) only in the "ext" field.
+local ext_map = {
+ ["mp3"] = "mp3",
+ ["opus"] = "opus",
+}
+
+local codec_map = {
+ -- src pattern = mpv codec
+ ["vtt"] = "webvtt",
+ ["opus"] = "opus",
+ ["vp9"] = "vp9",
+ ["avc1%..*"] = "h264",
+ ["av01%..*"] = "av1",
+ ["mp4a%..*"] = "aac",
+}
+
+-- Codec name as reported by youtube-dl mapped to mpv internal codec names.
+-- Fun fact: mpv will not really use the codec, but will still try to initialize
+-- the codec on track selection (just to scrap it), meaning it's only a hint,
+-- but one that may make initialization fail. On the other hand, if the codec
+-- is valid but completely different from the actual media, nothing bad happens.
+local function map_codec_to_mpv(codec)
+ if codec == nil then
+ return nil
+ end
+ for k, v in pairs(codec_map) do
+ local s, e = codec:find(k)
+ if s == 1 and e == #codec then
+ return v
+ end
+ end
+ return nil
+end
+
+local function platform_is_windows()
+ return mp.get_property_native("platform") == "windows"
+end
+
+local function exec(args)
+ msg.debug("Running: " .. table.concat(args, " "))
+
+ return mp.command_native({
+ name = "subprocess",
+ args = args,
+ capture_stdout = true,
+ capture_stderr = true,
+ })
+end
+
+-- return true if it was explicitly set on the command line
+local function option_was_set(name)
+ return mp.get_property_bool("option-info/" ..name.. "/set-from-commandline",
+ false)
+end
+
+-- return true if the option was set locally
+local function option_was_set_locally(name)
+ return mp.get_property_bool("option-info/" ..name.. "/set-locally", false)
+end
+
+-- youtube-dl may set special http headers for some sites (user-agent, cookies)
+local function set_http_headers(http_headers)
+ if not http_headers then
+ return
+ end
+ local headers = {}
+ local useragent = http_headers["User-Agent"]
+ if useragent and not option_was_set("user-agent") then
+ mp.set_property("file-local-options/user-agent", useragent)
+ end
+ local additional_fields = {"Cookie", "Referer", "X-Forwarded-For"}
+ for idx, item in pairs(additional_fields) do
+ local field_value = http_headers[item]
+ if field_value then
+ headers[#headers + 1] = item .. ": " .. field_value
+ end
+ end
+ if #headers > 0 and not option_was_set("http-header-fields") then
+ mp.set_property_native("file-local-options/http-header-fields", headers)
+ end
+end
+
+local special_cookie_field_names = Set {
+ "expires", "max-age", "domain", "path"
+}
+
+-- parse single-line Set-Cookie syntax
+local function parse_cookies(cookies_line)
+ if not cookies_line then
+ return {}
+ end
+ local cookies = {}
+ local cookie = {}
+ for stem in cookies_line:gmatch('[^;]+') do
+ stem = stem:gsub("^%s*(.-)%s*$", "%1")
+ local name, value = stem:match('^(.-)=(.+)$')
+ if name and name ~= "" and value then
+ local cmp_name = name:lower()
+ if special_cookie_field_names[cmp_name] then
+ cookie[cmp_name] = value
+ else
+ if cookie.name and cookie.value then
+ table.insert(cookies, cookie)
+ end
+ cookie = {
+ name = name,
+ value = value,
+ }
+ end
+ end
+ end
+ if cookie.name and cookie.value then
+ local cookie_key = cookie.domain .. ":" .. cookie.name
+ cookies[cookie_key] = cookie
+ end
+ return cookies
+end
+
+-- serialize cookies for avformat
+local function serialize_cookies_for_avformat(cookies)
+ local result = ''
+ for _, cookie in pairs(cookies) do
+ local cookie_str = ('%s=%s; '):format(cookie.name, cookie.value)
+ for k, v in pairs(cookie) do
+ if k ~= "name" and k ~= "value" then
+ cookie_str = cookie_str .. ('%s=%s; '):format(k, v)
+ end
+ end
+ result = result .. cookie_str .. '\r\n'
+ end
+ return result
+end
+
+-- set file-local cookies, preserving existing ones
+local function set_cookies(cookies)
+ if not cookies or cookies == "" then
+ return
+ end
+
+ local option_key = "file-local-options/stream-lavf-o"
+ local stream_opts = mp.get_property_native(option_key, {})
+ local existing_cookies = parse_cookies(stream_opts["cookies"])
+
+ local new_cookies = parse_cookies(cookies)
+ for cookie_key, cookie in pairs(new_cookies) do
+ if not existing_cookies[cookie_key] then
+ existing_cookies[cookie_key] = cookie
+ end
+ end
+
+ stream_opts["cookies"] = serialize_cookies_for_avformat(existing_cookies)
+ mp.set_property_native(option_key, stream_opts)
+end
+
+local function append_libav_opt(props, name, value)
+ if not props then
+ props = {}
+ end
+
+ if name and value and not props[name] then
+ props[name] = value
+ end
+
+ return props
+end
+
+local function edl_escape(url)
+ return "%" .. string.len(url) .. "%" .. url
+end
+
+local function url_is_safe(url)
+ local proto = type(url) == "string" and url:match("^(%a[%w+.-]*):") or nil
+ local safe = proto and safe_protos[proto]
+ if not safe then
+ msg.error(("Ignoring potentially unsafe url: '%s'"):format(url))
+ end
+ return safe
+end
+
+local function time_to_secs(time_string)
+ local ret
+
+ local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
+ if a ~= nil then
+ ret = (a*3600 + b*60 + c)
+ else
+ a, b = time_string:match("(%d%d?):(%d%d)")
+ if a ~= nil then
+ ret = (a*60 + b)
+ end
+ end
+
+ return ret
+end
+
+local function extract_chapters(data, video_length)
+ local ret = {}
+
+ for line in data:gmatch("[^\r\n]+") do
+ local time = time_to_secs(line)
+ if time and (time < video_length) then
+ table.insert(ret, {time = time, title = line})
+ end
+ end
+ table.sort(ret, function(a, b) return a.time < b.time end)
+ return ret
+end
+
+local function is_blacklisted(url)
+ if o.exclude == "" then return false end
+ if #ytdl.blacklisted == 0 then
+ for match in o.exclude:gmatch('%|?([^|]+)') do
+ ytdl.blacklisted[#ytdl.blacklisted + 1] = match
+ end
+ end
+ if #ytdl.blacklisted > 0 then
+ url = url:match('https?://(.+)')
+ for _, exclude in ipairs(ytdl.blacklisted) do
+ if url:match(exclude) then
+ msg.verbose('URL matches excluded substring. Skipping.')
+ return true
+ end
+ end
+ end
+ return false
+end
+
+local function parse_yt_playlist(url, json)
+ -- return 0-based index to use with --playlist-start
+
+ if not json.extractor or
+ (json.extractor ~= "youtube:tab" and
+ json.extractor ~= "youtube:playlist") then
+ return nil
+ end
+
+ local query = url:match("%?.+")
+ if not query then return nil end
+
+ local args = {}
+ for arg, param in query:gmatch("(%a+)=([^&?]+)") do
+ if arg and param then
+ args[arg] = param
+ end
+ end
+
+ local maybe_idx = tonumber(args["index"])
+
+ -- if index matches v param it's probably the requested item
+ if maybe_idx and #json.entries >= maybe_idx and
+ json.entries[maybe_idx].id == args["v"] then
+ msg.debug("index matches requested video")
+ return maybe_idx - 1
+ end
+
+ -- if there's no index or it doesn't match, look for video
+ for i = 1, #json.entries do
+ if json.entries[i].id == args["v"] then
+ msg.debug("found requested video in index " .. (i - 1))
+ return i - 1
+ end
+ end
+
+ msg.debug("requested video not found in playlist")
+ -- if item isn't on the playlist, give up
+ return nil
+end
+
+local function make_absolute_url(base_url, url)
+ if url:find("https?://") == 1 then return url end
+
+ local proto, domain, rest =
+ base_url:match("(https?://)([^/]+/)(.*)/?")
+ local segs = {}
+ rest:gsub("([^/]+)", function(c) table.insert(segs, c) end)
+ url:gsub("([^/]+)", function(c) table.insert(segs, c) end)
+ local resolved_url = {}
+ for i, v in ipairs(segs) do
+ if v == ".." then
+ table.remove(resolved_url)
+ elseif v ~= "." then
+ table.insert(resolved_url, v)
+ end
+ end
+ return proto .. domain ..
+ table.concat(resolved_url, "/")
+end
+
+local function join_url(base_url, fragment)
+ local res = ""
+ if base_url and fragment.path then
+ res = make_absolute_url(base_url, fragment.path)
+ elseif fragment.url then
+ res = fragment.url
+ end
+ return res
+end
+
+local function edl_track_joined(fragments, protocol, is_live, base)
+ if type(fragments) ~= "table" or not fragments[1] then
+ msg.debug("No fragments to join into EDL")
+ return nil
+ end
+
+ local edl = "edl://"
+ local offset = 1
+ local parts = {}
+
+ if protocol == "http_dash_segments" and not is_live then
+ msg.debug("Using dash")
+ local args = ""
+
+ -- assume MP4 DASH initialization segment
+ if not fragments[1].duration and #fragments > 1 then
+ msg.debug("Using init segment")
+ args = args .. ",init=" .. edl_escape(join_url(base, fragments[1]))
+ offset = 2
+ end
+
+ table.insert(parts, "!mp4_dash" .. args)
+
+ -- Check remaining fragments for duration;
+ -- if not available in all, give up.
+ for i = offset, #fragments do
+ if not fragments[i].duration then
+ msg.verbose("EDL doesn't support fragments " ..
+ "without duration with MP4 DASH")
+ return nil
+ end
+ end
+ end
+
+ for i = offset, #fragments do
+ local fragment = fragments[i]
+ if not url_is_safe(join_url(base, fragment)) then
+ return nil
+ end
+ table.insert(parts, edl_escape(join_url(base, fragment)))
+ if fragment.duration then
+ parts[#parts] =
+ parts[#parts] .. ",length="..fragment.duration
+ end
+ end
+ return edl .. table.concat(parts, ";") .. ";"
+end
+
+local function has_native_dash_demuxer()
+ local demuxers = mp.get_property_native("demuxer-lavf-list", {})
+ for _, v in ipairs(demuxers) do
+ if v == "dash" then
+ return true
+ end
+ end
+ return false
+end
+
+local function valid_manifest(json)
+ local reqfmt = json["requested_formats"] and json["requested_formats"][1] or {}
+ if not reqfmt["manifest_url"] and not json["manifest_url"] then
+ return false
+ end
+ local proto = reqfmt["protocol"] or json["protocol"] or ""
+ return (proto == "http_dash_segments" and has_native_dash_demuxer()) or
+ proto:find("^m3u8")
+end
+
+local function as_integer(v, def)
+ def = def or 0
+ local num = math.floor(tonumber(v) or def)
+ if num > -math.huge and num < math.huge then
+ return num
+ end
+ return def
+end
+
+local function tags_to_edl(json)
+ local tags = {}
+ for json_name, mp_name in pairs(tag_list) do
+ local v = json[json_name]
+ if v then
+ tags[#tags + 1] = mp_name .. "=" .. edl_escape(tostring(v))
+ end
+ end
+ if #tags == 0 then
+ return nil
+ end
+ return "!global_tags," .. table.concat(tags, ",")
+end
+
+-- Convert a format list from youtube-dl to an EDL URL, or plain URL.
+-- json: full json blob by youtube-dl
+-- formats: format list by youtube-dl
+-- use_all_formats: if=true, then formats is the full format list, and the
+-- function will attempt to return them as delay-loaded tracks
+-- See res table initialization in the function for result type.
+local function formats_to_edl(json, formats, use_all_formats)
+ local res = {
+ -- the media URL, which may be EDL
+ url = nil,
+ -- for use_all_formats=true: whether any muxed formats are present, and
+ -- at the same time the separate EDL parts don't have both audio/video
+ muxed_needed = false,
+ }
+
+ local default_formats = {}
+ local requested_formats = json["requested_formats"] or json["requested_downloads"]
+ if use_all_formats and requested_formats then
+ for _, track in ipairs(requested_formats) do
+ local id = track["format_id"]
+ if id then
+ default_formats[id] = true
+ end
+ end
+ end
+
+ local duration = as_integer(json["duration"])
+ local single_url = nil
+ local streams = {}
+
+ local tbr_only = true
+ for index, track in ipairs(formats) do
+ tbr_only = tbr_only and track["tbr"] and
+ (not track["abr"]) and (not track["vbr"])
+ end
+
+ local has_requested_video = false
+ local has_requested_audio = false
+ -- Web players with quality selection always show the highest quality
+ -- option at the top. Since tracks are usually listed with the first
+ -- track at the top, that should also be the highest quality track.
+ -- yt-dlp/youtube-dl sorts it's formats from worst to best.
+ -- Iterate in reverse to get best track first.
+ for index = #formats, 1, -1 do
+ local track = formats[index]
+ local edl_track = nil
+ edl_track = edl_track_joined(track.fragments,
+ track.protocol, json.is_live,
+ track.fragment_base_url)
+ if not edl_track and not url_is_safe(track.url) then
+ msg.error("No safe URL or supported fragmented stream available")
+ return nil
+ end
+
+ local is_default = default_formats[track["format_id"]]
+ local tracks = {}
+ -- "none" means it is not a video
+ -- nil means it is unknown
+ if (o.force_all_formats or track.vcodec) and track.vcodec ~= "none" then
+ tracks[#tracks + 1] = {
+ media_type = "video",
+ codec = map_codec_to_mpv(track.vcodec),
+ }
+ if is_default then
+ has_requested_video = true
+ end
+ end
+ if (o.force_all_formats or track.acodec) and track.acodec ~= "none" then
+ tracks[#tracks + 1] = {
+ media_type = "audio",
+ codec = map_codec_to_mpv(track.acodec) or
+ ext_map[track.ext],
+ }
+ if is_default then
+ has_requested_audio = true
+ end
+ end
+
+ local url = edl_track or track.url
+ local hdr = {"!new_stream", "!no_clip", "!no_chapters"}
+ local skip = #tracks == 0
+ local params = ""
+
+ if use_all_formats then
+ for _, sub in ipairs(tracks) do
+ -- A single track that is either audio or video. Delay load it.
+ local props = ""
+ if sub.media_type == "video" then
+ props = props .. ",w=" .. as_integer(track.width)
+ .. ",h=" .. as_integer(track.height)
+ .. ",fps=" .. as_integer(track.fps)
+ elseif sub.media_type == "audio" then
+ props = props .. ",samplerate=" .. as_integer(track.asr)
+ end
+ hdr[#hdr + 1] = "!delay_open,media_type=" .. sub.media_type ..
+ ",codec=" .. (sub.codec or "null") .. props
+
+ -- Add bitrate information etc. for better user selection.
+ local byterate = 0
+ local rates = {"tbr", "vbr", "abr"}
+ if #tracks > 1 then
+ rates = {({video = "vbr", audio = "abr"})[sub.media_type]}
+ end
+ if tbr_only then
+ rates = {"tbr"}
+ end
+ for _, f in ipairs(rates) do
+ local br = as_integer(track[f])
+ if br > 0 then
+ byterate = math.floor(br * 1000 / 8)
+ break
+ end
+ end
+ local title = track.format or track.format_note or ""
+ if #tracks > 1 then
+ if #title > 0 then
+ title = title .. " "
+ end
+ title = title .. "muxed-" .. index
+ end
+ local flags = {}
+ if is_default then
+ flags[#flags + 1] = "default"
+ end
+ hdr[#hdr + 1] = "!track_meta,title=" ..
+ edl_escape(title) .. ",byterate=" .. byterate ..
+ iif(#flags > 0, ",flags=" .. table.concat(flags, "+"), "")
+ end
+
+ if duration > 0 then
+ params = params .. ",length=" .. duration
+ end
+ end
+
+ if not skip then
+ hdr[#hdr + 1] = edl_escape(url) .. params
+
+ streams[#streams + 1] = table.concat(hdr, ";")
+ -- In case there is only 1 of these streams.
+ -- Note: assumes it has no important EDL headers
+ single_url = url
+ end
+ end
+
+ local tags = tags_to_edl(json)
+
+ -- Merge all tracks into a single virtual file, but avoid EDL if it's
+ -- only a single track without metadata (i.e. redundant).
+ if #streams == 1 and single_url and not tags then
+ res.url = single_url
+ elseif #streams > 0 then
+ if tags then
+ -- not a stream; just for the sake of concatenating the EDL string
+ streams[#streams + 1] = tags
+ end
+ res.url = "edl://" .. table.concat(streams, ";")
+ else
+ return nil
+ end
+
+ if has_requested_audio ~= has_requested_video then
+ local not_req_prop = has_requested_video and "aid" or "vid"
+ if mp.get_property(not_req_prop) == "auto" then
+ mp.set_property("file-local-options/" .. not_req_prop, "no")
+ end
+ end
+
+ return res
+end
+
+local function add_single_video(json)
+ local streamurl = ""
+ local format_info = ""
+ local max_bitrate = 0
+ local requested_formats = json["requested_formats"] or json["requested_downloads"]
+ local all_formats = json["formats"]
+ local has_requested_formats = requested_formats and #requested_formats > 0
+ local http_headers = has_requested_formats
+ and requested_formats[1].http_headers
+ or json.http_headers
+ local cookies = has_requested_formats
+ and requested_formats[1].cookies
+ or json.cookies
+
+ if o.use_manifests and valid_manifest(json) then
+ -- prefer manifest_url if present
+ format_info = "manifest"
+
+ local mpd_url = requested_formats and
+ requested_formats[1]["manifest_url"] or json["manifest_url"]
+ if not mpd_url then
+ msg.error("No manifest URL found in JSON data.")
+ return
+ elseif not url_is_safe(mpd_url) then
+ return
+ end
+
+ streamurl = mpd_url
+
+ if requested_formats then
+ for _, track in pairs(requested_formats) do
+ max_bitrate = (track.tbr and track.tbr > max_bitrate) and
+ track.tbr or max_bitrate
+ end
+ elseif json.tbr then
+ max_bitrate = json.tbr > max_bitrate and json.tbr or max_bitrate
+ end
+ end
+
+ if streamurl == "" then
+ -- possibly DASH/split tracks
+ local res = nil
+
+ -- Not having requested_formats usually hints to HLS master playlist
+ -- usage, which we don't want to split off, at least not yet.
+ if (all_formats and o.all_formats) and
+ (has_requested_formats or o.force_all_formats)
+ then
+ format_info = "all_formats (separate)"
+ res = formats_to_edl(json, all_formats, true)
+ -- Note: since we don't delay-load muxed streams, use normal stream
+ -- selection if we have to use muxed streams.
+ if res and res.muxed_needed then
+ res = nil
+ end
+ end
+
+ if not res and has_requested_formats then
+ format_info = "youtube-dl (separate)"
+ res = formats_to_edl(json, requested_formats, false)
+ end
+
+ if res then
+ streamurl = res.url
+ end
+ end
+
+ if streamurl == "" and json.url then
+ format_info = "youtube-dl (single)"
+ local edl_track = nil
+ edl_track = edl_track_joined(json.fragments, json.protocol,
+ json.is_live, json.fragment_base_url)
+
+ if not edl_track and not url_is_safe(json.url) then
+ return
+ end
+ -- normal video or single track
+ streamurl = edl_track or json.url
+ end
+
+ if streamurl == "" then
+ msg.error("No URL found in JSON data.")
+ return
+ end
+
+ set_http_headers(http_headers)
+
+ msg.verbose("format selection: " .. format_info)
+ msg.debug("streamurl: " .. streamurl)
+
+ mp.set_property("stream-open-filename", streamurl:gsub("^data:", "data://", 1))
+
+ if mp.get_property("force-media-title", "") == "" then
+ mp.set_property("file-local-options/force-media-title", json.title)
+ end
+
+ -- set hls-bitrate for dash track selection
+ if max_bitrate > 0 and
+ not option_was_set("hls-bitrate") and
+ not option_was_set_locally("hls-bitrate") then
+ mp.set_property_native('file-local-options/hls-bitrate', max_bitrate*1000)
+ end
+
+ -- add subtitles
+ if json.requested_subtitles ~= nil then
+ local subs = {}
+ for lang, info in pairs(json.requested_subtitles) do
+ subs[#subs + 1] = {lang = lang or "-", info = info}
+ end
+ table.sort(subs, function(a, b) return a.lang < b.lang end)
+ for _, e in ipairs(subs) do
+ local lang, sub_info = e.lang, e.info
+ msg.verbose("adding subtitle ["..lang.."]")
+
+ local sub = nil
+
+ if sub_info.data ~= nil then
+ sub = "memory://"..sub_info.data
+ elseif sub_info.url ~= nil and
+ url_is_safe(sub_info.url) then
+ sub = sub_info.url
+ end
+
+ if sub ~= nil then
+ local edl = "edl://!no_clip;!delay_open,media_type=sub"
+ local codec = map_codec_to_mpv(sub_info.ext)
+ if codec then
+ edl = edl .. ",codec=" .. codec
+ end
+ edl = edl .. ";" .. edl_escape(sub)
+ local title = sub_info.name or sub_info.ext
+ mp.commandv("sub-add", edl, "auto", title, lang)
+ else
+ msg.verbose("No subtitle data/url for ["..lang.."]")
+ end
+ end
+ end
+
+ -- add thumbnails
+ if (o.thumbnails == 'all' or o.thumbnails == 'best') and json.thumbnails ~= nil then
+ local thumb = nil
+ local thumb_height = -1
+ local thumb_preference = nil
+
+ for i = #json.thumbnails, 1, -1 do
+ local thumb_info = json.thumbnails[i]
+ if thumb_info.url ~= nil then
+ if o.thumbnails == 'all' then
+ msg.verbose("adding thumbnail")
+ mp.commandv("video-add", thumb_info.url, "auto")
+ thumb_height = 0
+ elseif (thumb_preference ~= nil and (thumb_info.preference or -math.huge) > thumb_preference) or
+ (thumb_preference == nil and ((thumb_info.height or 0) > thumb_height)) then
+ thumb = thumb_info.url
+ thumb_height = thumb_info.height or 0
+ thumb_preference = thumb_info.preference
+ end
+ end
+ end
+
+ if thumb ~= nil then
+ msg.verbose("adding thumbnail")
+ mp.commandv("video-add", thumb, "auto")
+ elseif thumb_height == -1 then
+ msg.verbose("No thumbnail url")
+ end
+ end
+
+ -- add chapters
+ if json.chapters then
+ msg.debug("Adding pre-parsed chapters")
+ for i = 1, #json.chapters do
+ local chapter = json.chapters[i]
+ local title = chapter.title or ""
+ if title == "" then
+ title = string.format('Chapter %02d', i)
+ end
+ table.insert(chapter_list, {time=chapter.start_time, title=title})
+ end
+ elseif json.description ~= nil and json.duration ~= nil then
+ chapter_list = extract_chapters(json.description, json.duration)
+ end
+
+ -- set start time
+ if json.start_time or json.section_start and
+ not option_was_set("start") and
+ not option_was_set_locally("start") then
+ local start_time = json.start_time or json.section_start
+ msg.debug("Setting start to: " .. start_time .. " secs")
+ mp.set_property("file-local-options/start", start_time)
+ end
+
+ -- set end time
+ if json.end_time or json.section_end and
+ not option_was_set("end") and
+ not option_was_set_locally("end") then
+ local end_time = json.end_time or json.section_end
+ msg.debug("Setting end to: " .. end_time .. " secs")
+ mp.set_property("file-local-options/end", end_time)
+ end
+
+ -- set aspect ratio for anamorphic video
+ if json.stretched_ratio ~= nil and
+ not option_was_set("video-aspect-override") then
+ mp.set_property('file-local-options/video-aspect-override', json.stretched_ratio)
+ end
+
+ local stream_opts = mp.get_property_native("file-local-options/stream-lavf-o", {})
+
+ -- for rtmp
+ if json.protocol == "rtmp" then
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_tcurl", streamurl)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_pageurl", json.page_url)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_playpath", json.play_path)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_swfverify", json.player_url)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_swfurl", json.player_url)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_app", json.app)
+ end
+
+ if json.proxy and json.proxy ~= "" then
+ stream_opts = append_libav_opt(stream_opts,
+ "http_proxy", json.proxy)
+ end
+
+ if cookies and cookies ~= "" then
+ local existing_cookies = parse_cookies(stream_opts["cookies"])
+ local new_cookies = parse_cookies(cookies)
+ for cookie_key, cookie in pairs(new_cookies) do
+ existing_cookies[cookie_key] = cookie
+ end
+ stream_opts["cookies"] = serialize_cookies_for_avformat(existing_cookies)
+ end
+
+ mp.set_property_native("file-local-options/stream-lavf-o", stream_opts)
+end
+
+local function check_version(ytdl_path)
+ local command = {
+ name = "subprocess",
+ capture_stdout = true,
+ args = {ytdl_path, "--version"}
+ }
+ local version_string = mp.command_native(command).stdout
+ local year, month, day = string.match(version_string, "(%d+).(%d+).(%d+)")
+
+ -- sanity check
+ if tonumber(year) < 2000 or tonumber(month) > 12 or
+ tonumber(day) > 31 then
+ return
+ end
+ local version_ts = os.time{year=year, month=month, day=day}
+ if os.difftime(os.time(), version_ts) > 60*60*24*90 then
+ msg.warn("It appears that your youtube-dl version is severely out of date.")
+ end
+end
+
+function run_ytdl_hook(url)
+ local start_time = os.clock()
+
+ -- strip ytdl://
+ if url:find("ytdl://") == 1 then
+ url = url:sub(8)
+ end
+
+ local format = mp.get_property("options/ytdl-format")
+ local raw_options = mp.get_property_native("options/ytdl-raw-options")
+ local allsubs = true
+ local proxy = nil
+ local use_playlist = false
+
+ local command = {
+ ytdl.path, "--no-warnings", "-J", "--flat-playlist",
+ "--sub-format", "ass/srt/best"
+ }
+
+ -- Checks if video option is "no", change format accordingly,
+ -- but only if user didn't explicitly set one
+ if mp.get_property("options/vid") == "no" and #format == 0 then
+ format = "bestaudio/best"
+ msg.verbose("Video disabled. Only using audio")
+ end
+
+ if format == "" then
+ format = "bestvideo+bestaudio/best"
+ end
+
+ if format ~= "ytdl" then
+ table.insert(command, "--format")
+ table.insert(command, format)
+ end
+
+ for param, arg in pairs(raw_options) do
+ table.insert(command, "--" .. param)
+ if arg ~= "" then
+ table.insert(command, arg)
+ end
+ if (param == "sub-lang" or param == "sub-langs" or param == "srt-lang") and (arg ~= "") then
+ allsubs = false
+ elseif param == "proxy" and arg ~= "" then
+ proxy = arg
+ elseif param == "yes-playlist" then
+ use_playlist = true
+ end
+ end
+
+ if allsubs == true then
+ table.insert(command, "--all-subs")
+ end
+ if not use_playlist then
+ table.insert(command, "--no-playlist")
+ end
+ table.insert(command, "--")
+ table.insert(command, url)
+
+ local result
+ if ytdl.searched then
+ result = exec(command)
+ else
+ local separator = platform_is_windows() and ";" or ":"
+ if o.ytdl_path:match("[^" .. separator .. "]") then
+ ytdl.paths_to_search = {}
+ for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
+ table.insert(ytdl.paths_to_search, path)
+ end
+ end
+
+ for _, path in pairs(ytdl.paths_to_search) do
+ -- search for youtube-dl in mpv's config dir
+ local exesuf = platform_is_windows() and not path:lower():match("%.exe$") and ".exe" or ""
+ local ytdl_cmd = mp.find_config_file(path .. exesuf)
+ if ytdl_cmd then
+ msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
+ ytdl.path = ytdl_cmd
+ command[1] = ytdl.path
+ result = exec(command)
+ break
+ else
+ msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
+ command[1] = path
+ result = exec(command)
+ if result.error_string == "init" then
+ msg.verbose("youtube-dl with path " .. path .. " not found in PATH or not enough permissions")
+ else
+ msg.verbose("Found youtube-dl with path " .. path .. " in PATH")
+ ytdl.path = path
+ break
+ end
+ end
+ end
+
+ ytdl.searched = true
+ end
+
+ if result.killed_by_us then
+ return
+ end
+
+ local json = result.stdout
+ local parse_err = nil
+
+ if result.status ~= 0 or json == "" then
+ json = nil
+ elseif json then
+ json, parse_err = utils.parse_json(json)
+ end
+
+ if json == nil then
+ msg.verbose("status:", result.status)
+ msg.verbose("reason:", result.error_string)
+ msg.verbose("stdout:", result.stdout)
+ msg.verbose("stderr:", result.stderr)
+
+ -- trim our stderr to avoid spurious newlines
+ ytdl_err = result.stderr:gsub("^%s*(.-)%s*$", "%1")
+ msg.error(ytdl_err)
+ local err = "youtube-dl failed: "
+ if result.error_string and result.error_string == "init" then
+ err = err .. "not found or not enough permissions"
+ elseif parse_err then
+ err = err .. "failed to parse JSON data: " .. parse_err
+ else
+ err = err .. "unexpected error occurred"
+ end
+ msg.error(err)
+ if parse_err or string.find(ytdl_err, "yt%-dl%.org/bug") then
+ check_version(ytdl.path)
+ end
+ return
+ end
+
+ msg.verbose("youtube-dl succeeded!")
+ msg.debug('ytdl parsing took '..os.clock()-start_time..' seconds')
+
+ json["proxy"] = json["proxy"] or proxy
+
+ -- what did we get?
+ if json["direct"] then
+ -- direct URL, nothing to do
+ msg.verbose("Got direct URL")
+ return
+ elseif json["_type"] == "playlist" or
+ json["_type"] == "multi_video" then
+ -- a playlist
+
+ if #json.entries == 0 then
+ msg.warn("Got empty playlist, nothing to play.")
+ return
+ end
+
+ local self_redirecting_url =
+ json.entries[1]["_type"] ~= "url_transparent" and
+ json.entries[1]["webpage_url"] and
+ json.entries[1]["webpage_url"] == json["webpage_url"]
+
+
+ -- some funky guessing to detect multi-arc videos
+ if self_redirecting_url and #json.entries > 1
+ and json.entries[1].protocol == "m3u8_native"
+ and json.entries[1].url then
+ msg.verbose("multi-arc video detected, building EDL")
+
+ local playlist = edl_track_joined(json.entries)
+
+ msg.debug("EDL: " .. playlist)
+
+ if not playlist then
+ return
+ end
+
+ -- can't change the http headers for each entry, so use the 1st
+ set_http_headers(json.entries[1].http_headers)
+ set_cookies(json.entries[1].cookies or json.cookies)
+
+ mp.set_property("stream-open-filename", playlist)
+ if json.title and mp.get_property("force-media-title", "") == "" then
+ mp.set_property("file-local-options/force-media-title",
+ json.title)
+ end
+
+ -- there might not be subs for the first segment
+ local entry_wsubs = nil
+ for i, entry in pairs(json.entries) do
+ if entry.requested_subtitles ~= nil then
+ entry_wsubs = i
+ break
+ end
+ end
+
+ if entry_wsubs ~= nil and
+ json.entries[entry_wsubs].duration ~= nil then
+ for j, req in pairs(json.entries[entry_wsubs].requested_subtitles) do
+ local subfile = "edl://"
+ for i, entry in pairs(json.entries) do
+ if entry.requested_subtitles ~= nil and
+ entry.requested_subtitles[j] ~= nil and
+ url_is_safe(entry.requested_subtitles[j].url) then
+ subfile = subfile..edl_escape(entry.requested_subtitles[j].url)
+ else
+ subfile = subfile..edl_escape("memory://WEBVTT")
+ end
+ subfile = subfile..",length="..entry.duration..";"
+ end
+ msg.debug(j.." sub EDL: "..subfile)
+ mp.commandv("sub-add", subfile, "auto", req.ext, j)
+ end
+ end
+
+ elseif self_redirecting_url and #json.entries == 1 then
+ msg.verbose("Playlist with single entry detected.")
+ add_single_video(json.entries[1])
+ else
+ local playlist_index = parse_yt_playlist(url, json)
+ local playlist = {"#EXTM3U"}
+ for i, entry in pairs(json.entries) do
+ local site = entry.url
+ local title = entry.title
+
+ if title ~= nil then
+ title = string.gsub(title, '%s+', ' ')
+ table.insert(playlist, "#EXTINF:0," .. title)
+ end
+
+ --[[ some extractors will still return the full info for
+ all clips in the playlist and the URL will point
+ directly to the file in that case, which we don't
+ want so get the webpage URL instead, which is what
+ we want, but only if we aren't going to trigger an
+ infinite loop
+ --]]
+ if entry["webpage_url"] and not self_redirecting_url then
+ site = entry["webpage_url"]
+ end
+
+ local playlist_url = nil
+
+ -- links without protocol as returned by --flat-playlist
+ if not site:find("://") then
+ -- youtube extractor provides only IDs,
+ -- others come prefixed with the extractor name and ":"
+ local prefix = site:find(":") and "ytdl://" or
+ "https://youtu.be/"
+ playlist_url = prefix .. site
+ elseif url_is_safe(site) then
+ playlist_url = site
+ end
+
+ if playlist_url then
+ table.insert(playlist, playlist_url)
+ -- save the cookies in a table for the playlist hook
+ playlist_cookies[playlist_url] = entry.cookies or json.cookies
+ end
+
+ end
+
+ if use_playlist and
+ not option_was_set("playlist-start") and playlist_index then
+ mp.set_property_number("playlist-start", playlist_index)
+ end
+
+ mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
+ end
+
+ else -- probably a video
+ add_single_video(json)
+ end
+ msg.debug('script running time: '..os.clock()-start_time..' seconds')
+end
+
+if not o.try_ytdl_first then
+ mp.add_hook("on_load", 10, function ()
+ msg.verbose('ytdl:// hook')
+ local url = mp.get_property("stream-open-filename", "")
+ if url:find("ytdl://") ~= 1 then
+ msg.verbose('not a ytdl:// url')
+ return
+ end
+ run_ytdl_hook(url)
+ end)
+end
+
+mp.add_hook("on_load", 20, function ()
+ msg.verbose('playlist hook')
+ local url = mp.get_property("stream-open-filename", "")
+ if playlist_cookies[url] then
+ set_cookies(playlist_cookies[url])
+ end
+end)
+
+mp.add_hook(o.try_ytdl_first and "on_load" or "on_load_fail", 10, function()
+ msg.verbose('full hook')
+ local url = mp.get_property("stream-open-filename", "")
+ if url:find("ytdl://") ~= 1 and
+ not ((url:find("https?://") == 1) and not is_blacklisted(url)) then
+ return
+ end
+ run_ytdl_hook(url)
+end)
+
+mp.add_hook("on_preloaded", 10, function ()
+ if next(chapter_list) ~= nil then
+ msg.verbose("Setting chapters")
+
+ mp.set_property_native("chapter-list", chapter_list)
+ chapter_list = {}
+ end
+end)
diff --git a/player/main.c b/player/main.c
new file mode 100644
index 0000000..27cf9b4
--- /dev/null
+++ b/player/main.c
@@ -0,0 +1,467 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+#include <string.h>
+#include <locale.h>
+
+#include "config.h"
+
+#include <libplacebo/config.h>
+
+#include "mpv_talloc.h"
+
+#include "misc/dispatch.h"
+#include "misc/thread_pool.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "osdep/main-fn.h"
+
+#include "common/av_log.h"
+#include "common/codecs.h"
+#include "common/encode.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/m_property.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "common/global.h"
+#include "filters/f_decoder_wrapper.h"
+#include "options/parse_configfile.h"
+#include "options/parse_commandline.h"
+#include "common/playlist.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "input/input.h"
+
+#include "audio/out/ao.h"
+#include "misc/thread_tools.h"
+#include "sub/osd.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "client.h"
+#include "command.h"
+#include "screenshot.h"
+
+static const char def_config[] =
+#include "etc/builtin.conf.inc"
+;
+
+#if HAVE_COCOA
+#include "osdep/macosx_events.h"
+#endif
+
+#ifndef FULLCONFIG
+#define FULLCONFIG "(missing)\n"
+#endif
+
+enum exit_reason {
+ EXIT_NONE,
+ EXIT_NORMAL,
+ EXIT_ERROR,
+};
+
+const char mp_help_text[] =
+"Usage: mpv [options] [url|path/]filename\n"
+"\n"
+"Basic options:\n"
+" --start=<time> seek to given (percent, seconds, or hh:mm:ss) position\n"
+" --no-audio do not play sound\n"
+" --no-video do not play video\n"
+" --fs fullscreen playback\n"
+" --sub-file=<file> specify subtitle file to use\n"
+" --playlist=<file> specify playlist file\n"
+"\n"
+" --list-options list all mpv options\n"
+" --h=<string> print options which contain the given string in their name\n"
+"\n";
+
+static mp_static_mutex terminal_owner_lock = MP_STATIC_MUTEX_INITIALIZER;
+static struct MPContext *terminal_owner;
+
+static bool cas_terminal_owner(struct MPContext *old, struct MPContext *new)
+{
+ mp_mutex_lock(&terminal_owner_lock);
+ bool r = terminal_owner == old;
+ if (r)
+ terminal_owner = new;
+ mp_mutex_unlock(&terminal_owner_lock);
+ return r;
+}
+
+void mp_update_logging(struct MPContext *mpctx, bool preinit)
+{
+ bool had_log_file = mp_msg_has_log_file(mpctx->global);
+
+ mp_msg_update_msglevels(mpctx->global, mpctx->opts);
+
+ bool enable = mpctx->opts->use_terminal;
+ bool enabled = cas_terminal_owner(mpctx, mpctx);
+ if (enable != enabled) {
+ if (enable && cas_terminal_owner(NULL, mpctx)) {
+ terminal_init();
+ enabled = true;
+ } else if (!enable) {
+ terminal_uninit();
+ cas_terminal_owner(mpctx, NULL);
+ }
+ }
+
+ if (mp_msg_has_log_file(mpctx->global) && !had_log_file) {
+ // for log-file=... in config files.
+ // we did flush earlier messages, but they were in a cyclic buffer, so
+ // the version might have been overwritten. ensure we have it.
+ mp_print_version(mpctx->log, false);
+ }
+
+ if (enabled && !preinit && mpctx->opts->consolecontrols)
+ terminal_setup_getch(mpctx->input);
+}
+
+void mp_print_version(struct mp_log *log, int always)
+{
+ int v = always ? MSGL_INFO : MSGL_V;
+ mp_msg(log, v, "%s %s\n", mpv_version, mpv_copyright);
+ if (strcmp(mpv_builddate, "UNKNOWN"))
+ mp_msg(log, v, " built on %s\n", mpv_builddate);
+ mp_msg(log, v, "libplacebo version: %s\n", PL_VERSION);
+ check_library_versions(log, v);
+ mp_msg(log, v, "\n");
+ // Only in verbose mode.
+ if (!always) {
+ mp_msg(log, MSGL_V, "Configuration: " CONFIGURATION "\n");
+ mp_msg(log, MSGL_V, "List of enabled features: %s\n", FULLCONFIG);
+ #ifdef NDEBUG
+ mp_msg(log, MSGL_V, "Built with NDEBUG.\n");
+ #endif
+ }
+}
+
+void mp_destroy(struct MPContext *mpctx)
+{
+ mp_shutdown_clients(mpctx);
+
+ mp_uninit_ipc(mpctx->ipc_ctx);
+ mpctx->ipc_ctx = NULL;
+
+ uninit_audio_out(mpctx);
+ uninit_video_out(mpctx);
+
+ // If it's still set here, it's an error.
+ encode_lavc_free(mpctx->encode_lavc_ctx);
+ mpctx->encode_lavc_ctx = NULL;
+
+ command_uninit(mpctx);
+
+ mp_clients_destroy(mpctx);
+
+ osd_free(mpctx->osd);
+
+#if HAVE_COCOA
+ cocoa_set_input_context(NULL);
+#endif
+
+ if (cas_terminal_owner(mpctx, mpctx)) {
+ terminal_uninit();
+ cas_terminal_owner(mpctx, NULL);
+ }
+
+ mp_input_uninit(mpctx->input);
+
+ uninit_libav(mpctx->global);
+
+ mp_msg_uninit(mpctx->global);
+ assert(!mpctx->num_abort_list);
+ talloc_free(mpctx->abort_list);
+ mp_mutex_destroy(&mpctx->abort_lock);
+ talloc_free(mpctx->mconfig); // destroy before dispatch
+ talloc_free(mpctx);
+}
+
+static bool handle_help_options(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct mp_log *log = mpctx->log;
+ if (opts->ao_opts->audio_device &&
+ strcmp(opts->ao_opts->audio_device, "help") == 0)
+ {
+ ao_print_devices(mpctx->global, log, mpctx->ao);
+ return true;
+ }
+ if (opts->property_print_help) {
+ property_print_help(mpctx);
+ return true;
+ }
+ if (encode_lavc_showhelp(log, opts->encode_opts))
+ return true;
+ return false;
+}
+
+static int cfg_include(void *ctx, char *filename, int flags)
+{
+ struct MPContext *mpctx = ctx;
+ char *fname = mp_get_user_path(NULL, mpctx->global, filename);
+ int r = m_config_parse_config_file(mpctx->mconfig, mpctx->global, fname, NULL, flags);
+ talloc_free(fname);
+ return r;
+}
+
+// We mostly care about LC_NUMERIC, and how "." vs. "," is treated,
+// Other locale stuff might break too, but probably isn't too bad.
+static bool check_locale(void)
+{
+ char *name = setlocale(LC_NUMERIC, NULL);
+ return !name || strcmp(name, "C") == 0 || strcmp(name, "C.UTF-8") == 0;
+}
+
+struct MPContext *mp_create(void)
+{
+ if (!check_locale()) {
+ // Normally, we never print anything (except if the "terminal" option
+ // is enabled), so this is an exception.
+ fprintf(stderr, "Non-C locale detected. This is not supported.\n"
+ "Call 'setlocale(LC_NUMERIC, \"C\");' in your code.\n");
+ return NULL;
+ }
+
+ char *enable_talloc = getenv("MPV_LEAK_REPORT");
+ if (!enable_talloc)
+ enable_talloc = HAVE_TA_LEAK_REPORT ? "1" : "0";
+ if (strcmp(enable_talloc, "1") == 0)
+ talloc_enable_leak_report();
+
+ mp_time_init();
+
+ struct MPContext *mpctx = talloc(NULL, MPContext);
+ *mpctx = (struct MPContext){
+ .last_chapter = -2,
+ .term_osd_contents = talloc_strdup(mpctx, ""),
+ .osd_progbar = { .type = -1 },
+ .playlist = talloc_zero(mpctx, struct playlist),
+ .dispatch = mp_dispatch_create(mpctx),
+ .playback_abort = mp_cancel_new(mpctx),
+ .thread_pool = mp_thread_pool_create(mpctx, 0, 1, 30),
+ .stop_play = PT_NEXT_ENTRY,
+ .play_dir = 1,
+ };
+
+ mp_mutex_init(&mpctx->abort_lock);
+
+ mpctx->global = talloc_zero(mpctx, struct mpv_global);
+
+ stats_global_init(mpctx->global);
+
+ // Nothing must call mp_msg*() and related before this
+ mp_msg_init(mpctx->global);
+ mpctx->log = mp_log_new(mpctx, mpctx->global->log, "!cplayer");
+ mpctx->statusline = mp_log_new(mpctx, mpctx->log, "!statusline");
+
+ mpctx->stats = stats_ctx_create(mpctx, mpctx->global, "main");
+
+ // Create the config context and register the options
+ mpctx->mconfig = m_config_new(mpctx, mpctx->log, &mp_opt_root);
+ mpctx->opts = mpctx->mconfig->optstruct;
+ mpctx->global->config = mpctx->mconfig->shadow;
+ mpctx->mconfig->includefunc = cfg_include;
+ mpctx->mconfig->includefunc_ctx = mpctx;
+ mpctx->mconfig->use_profiles = true;
+ mpctx->mconfig->is_toplevel = true;
+ mpctx->mconfig->global = mpctx->global;
+ m_config_parse(mpctx->mconfig, "", bstr0(def_config), NULL, 0);
+
+ mpctx->input = mp_input_init(mpctx->global, mp_wakeup_core_cb, mpctx);
+ screenshot_init(mpctx);
+ command_init(mpctx);
+ init_libav(mpctx->global);
+ mp_clients_init(mpctx);
+ mpctx->osd = osd_create(mpctx->global);
+
+#if HAVE_COCOA
+ cocoa_set_input_context(mpctx->input);
+#endif
+
+ char *verbose_env = getenv("MPV_VERBOSE");
+ if (verbose_env)
+ mpctx->opts->verbose = atoi(verbose_env);
+
+ mp_cancel_trigger(mpctx->playback_abort);
+
+ return mpctx;
+}
+
+// Finish mpctx initialization. This must be done after setting up all options.
+// Some of the initializations depend on the options, and can't be changed or
+// undone later.
+// If options is not NULL, apply them as command line player arguments.
+// Returns: 0 on success, -1 on error, 1 if exiting normally (e.g. help).
+int mp_initialize(struct MPContext *mpctx, char **options)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ assert(!mpctx->initialized);
+
+ // Preparse the command line, so we can init the terminal early.
+ if (options) {
+ m_config_preparse_command_line(mpctx->mconfig, mpctx->global,
+ &opts->verbose, options);
+ }
+
+ mp_init_paths(mpctx->global, opts);
+ mp_msg_set_early_logging(mpctx->global, true);
+ mp_update_logging(mpctx, true);
+
+ if (options) {
+ MP_VERBOSE(mpctx, "Command line options:");
+ for (int i = 0; options[i]; i++)
+ MP_VERBOSE(mpctx, " '%s'", options[i]);
+ MP_VERBOSE(mpctx, "\n");
+ }
+
+ mp_print_version(mpctx->log, false);
+
+ mp_parse_cfgfiles(mpctx);
+
+ if (options) {
+ int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist,
+ mpctx->global, options);
+ if (r < 0)
+ return r == M_OPT_EXIT ? 1 : -1;
+ }
+
+ if (opts->operation_mode == 1) {
+ m_config_set_profile(mpctx->mconfig, "builtin-pseudo-gui",
+ M_SETOPT_NO_OVERWRITE);
+ m_config_set_profile(mpctx->mconfig, "pseudo-gui", 0);
+ }
+
+ // Backup the default settings, which should not be stored in the resume
+ // config files. This explicitly includes values set by config files and
+ // the command line.
+ m_config_backup_watch_later_opts(mpctx->mconfig);
+
+ mp_input_load_config(mpctx->input);
+
+ // From this point on, all mpctx members are initialized.
+ mpctx->initialized = true;
+ mpctx->mconfig->option_change_callback = mp_option_change_callback;
+ mpctx->mconfig->option_change_callback_ctx = mpctx;
+ m_config_set_update_dispatch_queue(mpctx->mconfig, mpctx->dispatch);
+ // Run all update handlers.
+ mp_option_change_callback(mpctx, NULL, UPDATE_OPTS_MASK, false);
+
+ if (handle_help_options(mpctx))
+ return 1; // help
+
+ check_library_versions(mp_null_log, 0);
+
+ if (!mpctx->playlist->num_entries && !opts->player_idle_mode &&
+ options)
+ {
+ // nothing to play
+ mp_print_version(mpctx->log, true);
+ MP_INFO(mpctx, "%s", mp_help_text);
+ return 1;
+ }
+
+ MP_STATS(mpctx, "start init");
+
+#if HAVE_COCOA
+ mpv_handle *ctx = mp_new_client(mpctx->clients, "osx");
+ cocoa_set_mpv_handle(ctx);
+#endif
+
+ if (opts->encode_opts->file && opts->encode_opts->file[0]) {
+ mpctx->encode_lavc_ctx = encode_lavc_init(mpctx->global);
+ if(!mpctx->encode_lavc_ctx) {
+ MP_INFO(mpctx, "Encoding initialization failed.\n");
+ return -1;
+ }
+ m_config_set_profile(mpctx->mconfig, "encoding", 0);
+ mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE);
+ }
+
+ mp_load_scripts(mpctx);
+
+ if (opts->force_vo == 2 && handle_force_window(mpctx, false) < 0)
+ return -1;
+
+ // Needed to properly enter _initial_ idle mode if playlist empty.
+ if (mpctx->opts->player_idle_mode && !mpctx->playlist->num_entries)
+ mpctx->stop_play = PT_STOP;
+
+ MP_STATS(mpctx, "end init");
+
+ return 0;
+}
+
+int mpv_main(int argc, char *argv[])
+{
+ mp_thread_set_name("mpv");
+ struct MPContext *mpctx = mp_create();
+ if (!mpctx)
+ return 1;
+
+ mpctx->is_cli = true;
+
+ char **options = argv && argv[0] ? argv + 1 : NULL; // skips program name
+ int r = mp_initialize(mpctx, options);
+ if (r == 0)
+ mp_play_files(mpctx);
+
+ int rc = 0;
+ const char *reason = NULL;
+ if (r < 0) {
+ reason = "Fatal error";
+ rc = 1;
+ } else if (r > 0) {
+ // nothing
+ } else if (mpctx->stop_play == PT_QUIT) {
+ reason = "Quit";
+ } else if (mpctx->files_played) {
+ if (mpctx->files_errored || mpctx->files_broken) {
+ reason = "Some errors happened";
+ rc = 3;
+ } else {
+ reason = "End of file";
+ }
+ } else if (mpctx->files_broken && !mpctx->files_errored) {
+ reason = "Errors when loading file";
+ rc = 2;
+ } else if (mpctx->files_errored) {
+ reason = "Interrupted by error";
+ rc = 2;
+ } else {
+ reason = "No files played";
+ }
+
+ if (reason)
+ MP_INFO(mpctx, "Exiting... (%s)\n", reason);
+ if (mpctx->has_quit_custom_rc)
+ rc = mpctx->quit_custom_rc;
+
+ mp_destroy(mpctx);
+ return rc;
+}
diff --git a/player/meson.build b/player/meson.build
new file mode 100644
index 0000000..dc334b8
--- /dev/null
+++ b/player/meson.build
@@ -0,0 +1,10 @@
+subdir('javascript')
+subdir('lua')
+
+# Meson doesn't allow having multiple build targets with the same name in the same file.
+# Just generate the com in here for windows builds.
+if win32 and get_option('cplayer')
+ wrapper_sources= '../osdep/win32-console-wrapper.c'
+ executable('mpv', wrapper_sources, c_args: '-municode', link_args: '-municode',
+ name_suffix: 'com', install: true)
+endif
diff --git a/player/misc.c b/player/misc.c
new file mode 100644
index 0000000..b91d52a
--- /dev/null
+++ b/player/misc.c
@@ -0,0 +1,334 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "osdep/io.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_property.h"
+#include "options/m_config.h"
+#include "common/common.h"
+#include "common/global.h"
+#include "common/encode.h"
+#include "common/playlist.h"
+#include "input/input.h"
+
+#include "audio/out/ao.h"
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+
+const int num_ptracks[STREAM_TYPE_COUNT] = {
+ [STREAM_VIDEO] = 1,
+ [STREAM_AUDIO] = 1,
+ [STREAM_SUB] = 2,
+};
+
+double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t)
+{
+ double length = get_time_length(mpctx);
+ // Relative times are an offset to the start of the file.
+ double start = 0;
+ if (mpctx->demuxer && !mpctx->opts->rebase_start_time)
+ start = mpctx->demuxer->start_time;
+
+ switch (t.type) {
+ case REL_TIME_ABSOLUTE:
+ return t.pos;
+ case REL_TIME_RELATIVE:
+ if (t.pos >= 0) {
+ return start + t.pos;
+ } else {
+ if (length >= 0)
+ return start + MPMAX(length + t.pos, 0.0);
+ }
+ break;
+ case REL_TIME_PERCENT:
+ if (length >= 0)
+ return start + length * (t.pos / 100.0);
+ break;
+ case REL_TIME_CHAPTER:
+ return chapter_start_time(mpctx, t.pos); // already absolute time
+ }
+
+ return MP_NOPTS_VALUE;
+}
+
+static double get_play_end_pts_setting(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ double end = rel_time_to_abs(mpctx, opts->play_end);
+ double length = rel_time_to_abs(mpctx, opts->play_length);
+ if (length != MP_NOPTS_VALUE) {
+ double start = get_play_start_pts(mpctx);
+ if (end == MP_NOPTS_VALUE || start + length < end)
+ end = start + length;
+ }
+ return end;
+}
+
+// Return absolute timestamp against which currently playing media should be
+// clipped. Returns MP_NOPTS_VALUE if no clipping should happen.
+double get_play_end_pts(struct MPContext *mpctx)
+{
+ double end = get_play_end_pts_setting(mpctx);
+ double ab[2];
+ if (mpctx->ab_loop_clip && get_ab_loop_times(mpctx, ab)) {
+ if (end == MP_NOPTS_VALUE || end > ab[1])
+ end = ab[1];
+ }
+ return end;
+}
+
+// Get the absolute PTS at which playback should start.
+// Never returns MP_NOPTS_VALUE.
+double get_play_start_pts(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ double res = rel_time_to_abs(mpctx, opts->play_start);
+ if (res == MP_NOPTS_VALUE)
+ res = get_start_time(mpctx, mpctx->play_dir);
+ return res;
+}
+
+// Get timestamps to use for AB-loop. Returns false iff any of the timestamps
+// are invalid and/or AB-loops are currently disabled, and set t[] to either
+// the user options or NOPTS on best effort basis.
+bool get_ab_loop_times(struct MPContext *mpctx, double t[2])
+{
+ struct MPOpts *opts = mpctx->opts;
+ int dir = mpctx->play_dir;
+
+ t[0] = opts->ab_loop[0];
+ t[1] = opts->ab_loop[1];
+
+ if (!opts->ab_loop_count)
+ return false;
+
+ if (t[0] == MP_NOPTS_VALUE || t[1] == MP_NOPTS_VALUE || t[0] == t[1])
+ return false;
+
+ if (t[0] * dir > t[1] * dir)
+ MPSWAP(double, t[0], t[1]);
+
+ return true;
+}
+
+double get_track_seek_offset(struct MPContext *mpctx, struct track *track)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (track->selected) {
+ if (track->type == STREAM_AUDIO)
+ return -opts->audio_delay;
+ if (track->type == STREAM_SUB)
+ return -opts->subs_rend->sub_delay;
+ }
+ return 0;
+}
+
+void issue_refresh_seek(struct MPContext *mpctx, enum seek_precision min_prec)
+{
+ // let queued seeks execute at a slightly later point
+ if (mpctx->seek.type) {
+ mp_wakeup_core(mpctx);
+ return;
+ }
+ // repeat currently ongoing seeks
+ if (mpctx->current_seek.type) {
+ mpctx->seek = mpctx->current_seek;
+ mp_wakeup_core(mpctx);
+ return;
+ }
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, get_current_time(mpctx), min_prec, 0);
+}
+
+void update_content_type(struct MPContext *mpctx, struct track *track)
+{
+ enum mp_content_type content_type;
+ if (!track || !track->vo_c) {
+ content_type = MP_CONTENT_NONE;
+ } else if (track->image) {
+ content_type = MP_CONTENT_IMAGE;
+ } else {
+ content_type = MP_CONTENT_VIDEO;
+ }
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_CONTENT_TYPE, &content_type);
+}
+
+void update_vo_playback_state(struct MPContext *mpctx)
+{
+ if (mpctx->video_out && mpctx->video_out->config_ok) {
+ struct voctrl_playback_state oldstate = mpctx->vo_playback_state;
+ struct voctrl_playback_state newstate = {
+ .taskbar_progress = mpctx->opts->vo->taskbar_progress,
+ .playing = mpctx->playing,
+ .paused = mpctx->paused,
+ .percent_pos = get_percent_pos(mpctx),
+ };
+
+ if (oldstate.taskbar_progress != newstate.taskbar_progress ||
+ oldstate.playing != newstate.playing ||
+ oldstate.paused != newstate.paused ||
+ oldstate.percent_pos != newstate.percent_pos)
+ {
+ // Don't update progress bar if it was and still is hidden
+ if ((oldstate.playing && oldstate.taskbar_progress) ||
+ (newstate.playing && newstate.taskbar_progress))
+ {
+ vo_control_async(mpctx->video_out,
+ VOCTRL_UPDATE_PLAYBACK_STATE, &newstate);
+ }
+ mpctx->vo_playback_state = newstate;
+ }
+ } else {
+ mpctx->vo_playback_state = (struct voctrl_playback_state){ 0 };
+ }
+}
+
+void update_window_title(struct MPContext *mpctx, bool force)
+{
+ if (!mpctx->video_out && !mpctx->ao) {
+ talloc_free(mpctx->last_window_title);
+ mpctx->last_window_title = NULL;
+ return;
+ }
+ char *title = mp_property_expand_string(mpctx, mpctx->opts->wintitle);
+ if (!mpctx->last_window_title || force ||
+ strcmp(title, mpctx->last_window_title) != 0)
+ {
+ talloc_free(mpctx->last_window_title);
+ mpctx->last_window_title = talloc_steal(mpctx, title);
+
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_UPDATE_WINDOW_TITLE, title);
+
+ if (mpctx->ao) {
+ ao_control(mpctx->ao, AOCONTROL_UPDATE_STREAM_TITLE, title);
+ }
+ } else {
+ talloc_free(title);
+ }
+}
+
+void error_on_track(struct MPContext *mpctx, struct track *track)
+{
+ if (!track || !track->selected)
+ return;
+ mp_deselect_track(mpctx, track);
+ if (track->type == STREAM_AUDIO)
+ MP_INFO(mpctx, "Audio: no audio\n");
+ if (track->type == STREAM_VIDEO)
+ MP_INFO(mpctx, "Video: no video\n");
+ if (mpctx->opts->stop_playback_on_init_failure ||
+ !(mpctx->vo_chain || mpctx->ao_chain))
+ {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_ERROR;
+ if (mpctx->error_playing >= 0)
+ mpctx->error_playing = MPV_ERROR_NOTHING_TO_PLAY;
+ }
+ mp_wakeup_core(mpctx);
+}
+
+int stream_dump(struct MPContext *mpctx, const char *source_filename)
+{
+ struct MPOpts *opts = mpctx->opts;
+ stream_t *stream = stream_create(source_filename,
+ STREAM_ORIGIN_DIRECT | STREAM_READ,
+ mpctx->playback_abort, mpctx->global);
+ if (!stream)
+ return -1;
+
+ int64_t size = stream_get_size(stream);
+
+ FILE *dest = fopen(opts->stream_dump, "wb");
+ if (!dest) {
+ MP_ERR(mpctx, "Error opening dump file: %s\n", mp_strerror(errno));
+ return -1;
+ }
+
+ bool ok = true;
+
+ while (mpctx->stop_play == KEEP_PLAYING && ok) {
+ if (!opts->quiet && ((stream->pos / (1024 * 1024)) % 2) == 1) {
+ uint64_t pos = stream->pos;
+ MP_MSG(mpctx, MSGL_STATUS, "Dumping %lld/%lld...",
+ (long long int)pos, (long long int)size);
+ }
+ uint8_t buf[4096];
+ int len = stream_read(stream, buf, sizeof(buf));
+ if (!len) {
+ ok &= stream->eof;
+ break;
+ }
+ ok &= fwrite(buf, len, 1, dest) == 1;
+ mp_wakeup_core(mpctx); // don't actually sleep
+ mp_idle(mpctx); // but process input
+ }
+
+ ok &= fclose(dest) == 0;
+ free_stream(stream);
+ return ok ? 0 : -1;
+}
+
+void merge_playlist_files(struct playlist *pl)
+{
+ if (!pl->num_entries)
+ return;
+ char *edl = talloc_strdup(NULL, "edl://");
+ for (int n = 0; n < pl->num_entries; n++) {
+ struct playlist_entry *e = pl->entries[n];
+ if (n)
+ edl = talloc_strdup_append_buffer(edl, ";");
+ // Escape if needed
+ if (e->filename[strcspn(e->filename, "=%,;\n")] ||
+ bstr_strip(bstr0(e->filename)).len != strlen(e->filename))
+ {
+ // %length%
+ edl = talloc_asprintf_append_buffer(edl, "%%%zd%%", strlen(e->filename));
+ }
+ edl = talloc_strdup_append_buffer(edl, e->filename);
+ }
+ playlist_clear(pl);
+ playlist_add_file(pl, edl);
+ talloc_free(edl);
+}
+
+const char *mp_status_str(enum playback_status st)
+{
+ switch (st) {
+ case STATUS_SYNCING: return "syncing";
+ case STATUS_READY: return "ready";
+ case STATUS_PLAYING: return "playing";
+ case STATUS_DRAINING: return "draining";
+ case STATUS_EOF: return "eof";
+ default: return "bug";
+ }
+}
diff --git a/player/osd.c b/player/osd.c
new file mode 100644
index 0000000..dc03229
--- /dev/null
+++ b/player/osd.c
@@ -0,0 +1,580 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <math.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "options/m_property.h"
+#include "filters/f_decoder_wrapper.h"
+#include "common/encode.h"
+
+#include "osdep/terminal.h"
+#include "osdep/timer.h"
+
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+
+#define saddf(var, ...) (*(var) = talloc_asprintf_append((*var), __VA_ARGS__))
+
+// append time in the hh:mm:ss format (plus fractions if wanted)
+static void sadd_hhmmssff(char **buf, double time, bool fractions)
+{
+ char *s = mp_format_time(time, fractions);
+ *buf = talloc_strdup_append(*buf, s);
+ talloc_free(s);
+}
+
+static void sadd_percentage(char **buf, int percent) {
+ if (percent >= 0)
+ *buf = talloc_asprintf_append(*buf, " (%d%%)", percent);
+}
+
+static char *join_lines(void *ta_ctx, char **parts, int num_parts)
+{
+ char *res = talloc_strdup(ta_ctx, "");
+ for (int n = 0; n < num_parts; n++)
+ res = talloc_asprintf_append(res, "%s%s", n ? "\n" : "", parts[n]);
+ return res;
+}
+
+static void term_osd_update(struct MPContext *mpctx)
+{
+ int num_parts = 0;
+ char *parts[3] = {0};
+
+ if (!mpctx->opts->use_terminal)
+ return;
+
+ if (mpctx->term_osd_subs && mpctx->term_osd_subs[0])
+ parts[num_parts++] = mpctx->term_osd_subs;
+ if (mpctx->term_osd_text && mpctx->term_osd_text[0])
+ parts[num_parts++] = mpctx->term_osd_text;
+ if (mpctx->term_osd_status && mpctx->term_osd_status[0])
+ parts[num_parts++] = mpctx->term_osd_status;
+
+ char *s = join_lines(mpctx, parts, num_parts);
+
+ if (strcmp(mpctx->term_osd_contents, s) == 0 &&
+ mp_msg_has_status_line(mpctx->global))
+ {
+ talloc_free(s);
+ } else {
+ talloc_free(mpctx->term_osd_contents);
+ mpctx->term_osd_contents = s;
+ mp_msg(mpctx->statusline, MSGL_STATUS, "%s", s);
+ }
+}
+
+static void term_osd_update_title(struct MPContext *mpctx)
+{
+ if (!mpctx->opts->use_terminal)
+ return;
+
+ char *s = mp_property_expand_escaped_string(mpctx, mpctx->opts->term_title);
+ if (bstr_equals(bstr0(s), bstr0(mpctx->term_osd_title))) {
+ talloc_free(s);
+ return;
+ }
+
+ mp_msg_set_term_title(mpctx->statusline, s);
+ mpctx->term_osd_title = talloc_steal(mpctx, s);
+}
+
+void term_osd_set_subs(struct MPContext *mpctx, const char *text)
+{
+ if (mpctx->video_out || !text || !mpctx->opts->subs_rend->sub_visibility)
+ text = ""; // disable
+ if (strcmp(mpctx->term_osd_subs ? mpctx->term_osd_subs : "", text) == 0)
+ return;
+ talloc_free(mpctx->term_osd_subs);
+ mpctx->term_osd_subs = talloc_strdup(mpctx, text);
+ term_osd_update(mpctx);
+}
+
+static void term_osd_set_text_lazy(struct MPContext *mpctx, const char *text)
+{
+ bool video_osd = mpctx->video_out && mpctx->opts->video_osd;
+ if ((video_osd && mpctx->opts->term_osd != 1) || !text)
+ text = ""; // disable
+ talloc_free(mpctx->term_osd_text);
+ mpctx->term_osd_text = talloc_strdup(mpctx, text);
+}
+
+static void term_osd_set_status_lazy(struct MPContext *mpctx, const char *text)
+{
+ talloc_free(mpctx->term_osd_status);
+ mpctx->term_osd_status = talloc_strdup(mpctx, text);
+}
+
+static void add_term_osd_bar(struct MPContext *mpctx, char **line, int width)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (width < 5)
+ return;
+
+ int pos = get_current_pos_ratio(mpctx, false) * (width - 3);
+ pos = MPCLAMP(pos, 0, width - 3);
+
+ bstr chars = bstr0(opts->term_osd_bar_chars);
+ bstr parts[5];
+ for (int n = 0; n < 5; n++)
+ parts[n] = bstr_split_utf8(chars, &chars);
+
+ saddf(line, "\r%.*s", BSTR_P(parts[0]));
+ for (int n = 0; n < pos; n++)
+ saddf(line, "%.*s", BSTR_P(parts[1]));
+ saddf(line, "%.*s", BSTR_P(parts[2]));
+ for (int n = 0; n < width - 3 - pos; n++)
+ saddf(line, "%.*s", BSTR_P(parts[3]));
+ saddf(line, "%.*s", BSTR_P(parts[4]));
+}
+
+static bool is_busy(struct MPContext *mpctx)
+{
+ return !mpctx->restart_complete && mp_time_sec() - mpctx->start_timestamp > 0.3;
+}
+
+static char *get_term_status_msg(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (opts->status_msg)
+ return mp_property_expand_escaped_string(mpctx, opts->status_msg);
+
+ char *line = NULL;
+
+ // Playback status
+ if (is_busy(mpctx)) {
+ saddf(&line, "(...) ");
+ } else if (mpctx->paused_for_cache && !opts->pause) {
+ saddf(&line, "(Buffering) ");
+ } else if (mpctx->paused) {
+ saddf(&line, "(Paused) ");
+ }
+
+ if (mpctx->ao_chain)
+ saddf(&line, "A");
+ if (mpctx->vo_chain)
+ saddf(&line, "V");
+ saddf(&line, ": ");
+
+ // Playback position
+ double speed = opts->term_remaining_playtime ? mpctx->video_speed : 1;
+ sadd_hhmmssff(&line, get_playback_time(mpctx), opts->osd_fractions);
+ saddf(&line, " / ");
+ sadd_hhmmssff(&line, get_time_length(mpctx) / speed, opts->osd_fractions);
+
+ sadd_percentage(&line, get_percent_pos(mpctx));
+
+ // other
+ if (opts->playback_speed != 1)
+ saddf(&line, " x%4.2f", opts->playback_speed);
+
+ // A-V sync
+ if (mpctx->ao_chain && mpctx->vo_chain && !mpctx->vo_chain->is_sparse) {
+ saddf(&line, " A-V:%7.3f", mpctx->last_av_difference);
+ if (fabs(mpctx->total_avsync_change) > 0.05)
+ saddf(&line, " ct:%7.3f", mpctx->total_avsync_change);
+ }
+
+ double position = get_current_pos_ratio(mpctx, true);
+ char lavcbuf[80];
+ if (encode_lavc_getstatus(mpctx->encode_lavc_ctx, lavcbuf, sizeof(lavcbuf),
+ position) >= 0)
+ {
+ // encoding stats
+ saddf(&line, " %s", lavcbuf);
+ } else {
+ // VO stats
+ if (mpctx->vo_chain) {
+ if (mpctx->display_sync_active) {
+ char *r = mp_property_expand_string(mpctx,
+ "${?vsync-ratio:${vsync-ratio}}");
+ if (r[0]) {
+ saddf(&line, " DS: %s/%"PRId64, r,
+ vo_get_delayed_count(mpctx->video_out));
+ }
+ talloc_free(r);
+ }
+ int64_t c = vo_get_drop_count(mpctx->video_out);
+ struct mp_decoder_wrapper *dec = mpctx->vo_chain->track
+ ? mpctx->vo_chain->track->dec : NULL;
+ int dropped_frames =
+ dec ? mp_decoder_wrapper_get_frames_dropped(dec) : 0;
+ if (c > 0 || dropped_frames > 0) {
+ saddf(&line, " Dropped: %"PRId64, c);
+ if (dropped_frames)
+ saddf(&line, "/%d", dropped_frames);
+ }
+ }
+ }
+
+ if (mpctx->demuxer && demux_is_network_cached(mpctx->demuxer)) {
+ saddf(&line, " Cache: ");
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ if (s.ts_duration < 0) {
+ saddf(&line, "???");
+ } else if (s.ts_duration < 10) {
+ saddf(&line, "%2.1fs", s.ts_duration);
+ } else {
+ saddf(&line, "%2ds", (int)s.ts_duration);
+ }
+ int64_t cache_size = s.fw_bytes;
+ if (cache_size > 0) {
+ if (cache_size >= 1024 * 1024) {
+ saddf(&line, "/%lldMB", (long long)(cache_size / 1024 / 1024));
+ } else {
+ saddf(&line, "/%lldKB", (long long)(cache_size / 1024));
+ }
+ }
+ }
+
+ return line;
+}
+
+static void term_osd_print_status_lazy(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ term_osd_update_title(mpctx);
+ update_window_title(mpctx, false);
+ update_vo_playback_state(mpctx);
+
+ if (!opts->use_terminal)
+ return;
+
+ if (opts->quiet || !mpctx->playback_initialized || !mpctx->playing_msg_shown)
+ {
+ if (!mpctx->playing)
+ term_osd_set_status_lazy(mpctx, "");
+ return;
+ }
+
+ char *line = get_term_status_msg(mpctx);
+
+ if (opts->term_osd_bar) {
+ saddf(&line, "\n");
+ int w = 80, h = 24;
+ terminal_get_size(&w, &h);
+ add_term_osd_bar(mpctx, &line, w);
+ }
+
+ term_osd_set_status_lazy(mpctx, line);
+ talloc_free(line);
+}
+
+static bool set_osd_msg_va(struct MPContext *mpctx, int level, int time,
+ const char *fmt, va_list ap)
+{
+ if (level > mpctx->opts->osd_level)
+ return false;
+
+ talloc_free(mpctx->osd_msg_text);
+ mpctx->osd_msg_text = talloc_vasprintf(mpctx, fmt, ap);
+ mpctx->osd_show_pos = false;
+ mpctx->osd_msg_next_duration = time / 1000.0;
+ mpctx->osd_force_update = true;
+ mp_wakeup_core(mpctx);
+ if (mpctx->osd_msg_next_duration <= 0)
+ mpctx->osd_msg_visible = mp_time_sec();
+ return true;
+}
+
+bool set_osd_msg(struct MPContext *mpctx, int level, int time,
+ const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ bool r = set_osd_msg_va(mpctx, level, time, fmt, ap);
+ va_end(ap);
+ return r;
+}
+
+// type: mp_osd_font_codepoints, ASCII, or OSD_BAR_*
+void set_osd_bar(struct MPContext *mpctx, int type,
+ double min, double max, double neutral, double val)
+{
+ struct MPOpts *opts = mpctx->opts;
+ bool video_osd = mpctx->video_out && mpctx->opts->video_osd;
+ if (opts->osd_level < 1 || !opts->osd_bar_visible || !video_osd)
+ return;
+
+ mpctx->osd_visible = mp_time_sec() + opts->osd_duration / 1000.0;
+ mpctx->osd_progbar.type = type;
+ mpctx->osd_progbar.value = (val - min) / (max - min);
+ mpctx->osd_progbar.num_stops = 0;
+ if (neutral > min && neutral < max) {
+ float pos = (neutral - min) / (max - min);
+ MP_TARRAY_APPEND(mpctx, mpctx->osd_progbar.stops,
+ mpctx->osd_progbar.num_stops, pos);
+ }
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ mp_wakeup_core(mpctx);
+}
+
+// Update a currently displayed bar of the same type, without resetting the
+// timer.
+static void update_osd_bar(struct MPContext *mpctx, int type,
+ double min, double max, double val)
+{
+ if (mpctx->osd_progbar.type != type)
+ return;
+
+ float new_value = (val - min) / (max - min);
+ if (new_value != mpctx->osd_progbar.value) {
+ mpctx->osd_progbar.value = new_value;
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ }
+}
+
+void set_osd_bar_chapters(struct MPContext *mpctx, int type)
+{
+ if (mpctx->osd_progbar.type != type)
+ return;
+
+ mpctx->osd_progbar.num_stops = 0;
+ double len = get_time_length(mpctx);
+ if (len > 0) {
+ // Always render the loop points, even if they're incomplete.
+ double ab[2];
+ bool valid = get_ab_loop_times(mpctx, ab);
+ for (int n = 0; n < 2; n++) {
+ if (ab[n] != MP_NOPTS_VALUE) {
+ MP_TARRAY_APPEND(mpctx, mpctx->osd_progbar.stops,
+ mpctx->osd_progbar.num_stops, ab[n] / len);
+ }
+ }
+ if (!valid) {
+ int num = get_chapter_count(mpctx);
+ for (int n = 0; n < num; n++) {
+ double time = chapter_start_time(mpctx, n);
+ if (time >= 0) {
+ float pos = time / len;
+ MP_TARRAY_APPEND(mpctx, mpctx->osd_progbar.stops,
+ mpctx->osd_progbar.num_stops, pos);
+ }
+ }
+ }
+ }
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ mp_wakeup_core(mpctx);
+}
+
+// osd_function is the symbol appearing in the video status, such as OSD_PLAY
+void set_osd_function(struct MPContext *mpctx, int osd_function)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ mpctx->osd_function = osd_function;
+ mpctx->osd_function_visible = mp_time_sec() + opts->osd_duration / 1000.0;
+ mpctx->osd_force_update = true;
+ mp_wakeup_core(mpctx);
+}
+
+void get_current_osd_sym(struct MPContext *mpctx, char *buf, size_t buf_size)
+{
+ int sym = mpctx->osd_function;
+ if (!sym) {
+ if (is_busy(mpctx) || (mpctx->paused_for_cache && !mpctx->opts->pause)) {
+ sym = OSD_CLOCK;
+ } else if (mpctx->paused || mpctx->step_frames) {
+ sym = OSD_PAUSE;
+ } else {
+ sym = OSD_PLAY;
+ }
+ }
+ osd_get_function_sym(buf, buf_size, sym);
+}
+
+static void sadd_osd_status(char **buffer, struct MPContext *mpctx, int level)
+{
+ assert(level >= 0 && level <= 3);
+ if (level == 0)
+ return;
+ char *msg = mpctx->opts->osd_msg[level - 1];
+
+ if (msg && msg[0]) {
+ char *text = mp_property_expand_escaped_string(mpctx, msg);
+ *buffer = talloc_strdup_append(*buffer, text);
+ talloc_free(text);
+ } else if (level >= 2) {
+ bool fractions = mpctx->opts->osd_fractions;
+ char sym[10];
+ get_current_osd_sym(mpctx, sym, sizeof(sym));
+ saddf(buffer, "%s ", sym);
+ char *custom_msg = mpctx->opts->osd_status_msg;
+ if (custom_msg && level == 3) {
+ char *text = mp_property_expand_escaped_string(mpctx, custom_msg);
+ *buffer = talloc_strdup_append(*buffer, text);
+ talloc_free(text);
+ } else {
+ sadd_hhmmssff(buffer, get_playback_time(mpctx), fractions);
+ if (level == 3) {
+ saddf(buffer, " / ");
+ sadd_hhmmssff(buffer, get_time_length(mpctx), fractions);
+ sadd_percentage(buffer, get_percent_pos(mpctx));
+ }
+ }
+ }
+}
+
+// OSD messages initiated by seeking commands are added lazily with this
+// function, because multiple successive seek commands can be coalesced.
+static void add_seek_osd_messages(struct MPContext *mpctx)
+{
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_BAR) {
+ double pos = get_current_pos_ratio(mpctx, false);
+ set_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1, 0, MPCLAMP(pos, 0, 1));
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_TEXT) {
+ // Never in term-osd mode
+ bool video_osd = mpctx->video_out && mpctx->opts->video_osd;
+ if (video_osd && mpctx->opts->term_osd != 1) {
+ if (set_osd_msg(mpctx, 1, mpctx->opts->osd_duration, ""))
+ mpctx->osd_show_pos = true;
+ }
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CHAPTER_TEXT) {
+ char *chapter = chapter_display_name(mpctx, get_current_chapter(mpctx));
+ set_osd_msg(mpctx, 1, mpctx->opts->osd_duration,
+ "Chapter: %s", chapter);
+ talloc_free(chapter);
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CURRENT_FILE) {
+ if (mpctx->filename) {
+ set_osd_msg(mpctx, 1, mpctx->opts->osd_duration, "%s",
+ mpctx->filename);
+ }
+ }
+ mpctx->add_osd_seek_info = 0;
+}
+
+// Update the OSD text (both on VO and terminal status line).
+void update_osd_msg(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct osd_state *osd = mpctx->osd;
+
+ double now = mp_time_sec();
+
+ if (!mpctx->osd_force_update) {
+ // Assume nothing is going on at all.
+ if (!mpctx->osd_idle_update)
+ return;
+
+ double delay = 0.050; // update the OSD at most this often
+ double diff = now - mpctx->osd_last_update;
+ if (diff < delay) {
+ mp_set_timeout(mpctx, delay - diff);
+ return;
+ }
+ }
+ mpctx->osd_force_update = false;
+ mpctx->osd_idle_update = false;
+ mpctx->osd_last_update = now;
+
+ if (mpctx->osd_visible) {
+ double sleep = mpctx->osd_visible - now;
+ if (sleep > 0) {
+ mp_set_timeout(mpctx, sleep);
+ mpctx->osd_idle_update = true;
+ } else {
+ mpctx->osd_visible = 0;
+ mpctx->osd_progbar.type = -1; // disable
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ }
+ }
+
+ if (mpctx->osd_function_visible) {
+ double sleep = mpctx->osd_function_visible - now;
+ if (sleep > 0) {
+ mp_set_timeout(mpctx, sleep);
+ mpctx->osd_idle_update = true;
+ } else {
+ mpctx->osd_function_visible = 0;
+ mpctx->osd_function = 0;
+ }
+ }
+
+ if (mpctx->osd_msg_next_duration > 0) {
+ // This is done to avoid cutting the OSD message short if slow commands
+ // are executed between setting the OSD message and showing it.
+ mpctx->osd_msg_visible = now + mpctx->osd_msg_next_duration;
+ mpctx->osd_msg_next_duration = 0;
+ }
+
+ if (mpctx->osd_msg_visible) {
+ double sleep = mpctx->osd_msg_visible - now;
+ if (sleep > 0) {
+ mp_set_timeout(mpctx, sleep);
+ mpctx->osd_idle_update = true;
+ } else {
+ talloc_free(mpctx->osd_msg_text);
+ mpctx->osd_msg_text = NULL;
+ mpctx->osd_msg_visible = 0;
+ mpctx->osd_show_pos = false;
+ }
+ }
+
+ add_seek_osd_messages(mpctx);
+
+ if (mpctx->osd_progbar.type == OSD_BAR_SEEK) {
+ double pos = get_current_pos_ratio(mpctx, false);
+ update_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1, MPCLAMP(pos, 0, 1));
+ }
+
+ term_osd_set_text_lazy(mpctx, mpctx->osd_msg_text);
+ term_osd_print_status_lazy(mpctx);
+ term_osd_update(mpctx);
+
+ if (!opts->video_osd)
+ return;
+
+ int osd_level = opts->osd_level;
+ if (mpctx->osd_show_pos)
+ osd_level = 3;
+
+ char *text = NULL;
+ sadd_osd_status(&text, mpctx, osd_level);
+ if (mpctx->osd_msg_text && mpctx->osd_msg_text[0]) {
+ text = talloc_asprintf_append(text, "%s%s", text ? "\n" : "",
+ mpctx->osd_msg_text);
+ }
+ osd_set_text(osd, text);
+ talloc_free(text);
+}
diff --git a/player/playloop.c b/player/playloop.c
new file mode 100644
index 0000000..60596da
--- /dev/null
+++ b/player/playloop.c
@@ -0,0 +1,1291 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "client.h"
+#include "command.h"
+#include "core.h"
+#include "mpv_talloc.h"
+#include "screenshot.h"
+
+#include "audio/out/ao.h"
+#include "common/common.h"
+#include "common/encode.h"
+#include "common/msg.h"
+#include "common/playlist.h"
+#include "common/stats.h"
+#include "demux/demux.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+#include "input/input.h"
+#include "misc/dispatch.h"
+#include "options/m_config_frontend.h"
+#include "options/m_property.h"
+#include "options/options.h"
+#include "osdep/terminal.h"
+#include "osdep/timer.h"
+#include "stream/stream.h"
+#include "sub/dec_sub.h"
+#include "sub/osd.h"
+#include "video/out/vo.h"
+
+// Wait until mp_wakeup_core() is called, since the last time
+// mp_wait_events() was called.
+void mp_wait_events(struct MPContext *mpctx)
+{
+ mp_client_send_property_changes(mpctx);
+
+ stats_event(mpctx->stats, "iterations");
+
+ bool sleeping = mpctx->sleeptime > 0;
+ if (sleeping)
+ MP_STATS(mpctx, "start sleep");
+
+ mp_dispatch_queue_process(mpctx->dispatch, mpctx->sleeptime);
+
+ mpctx->sleeptime = INFINITY;
+
+ if (sleeping)
+ MP_STATS(mpctx, "end sleep");
+}
+
+// Set the timeout used when the playloop goes to sleep. This means the
+// playloop will re-run as soon as the timeout elapses (or earlier).
+// mp_set_timeout(c, 0) is essentially equivalent to mp_wakeup_core(c).
+void mp_set_timeout(struct MPContext *mpctx, double sleeptime)
+{
+ if (mpctx->sleeptime > sleeptime) {
+ mpctx->sleeptime = sleeptime;
+ int64_t abstime = mp_time_ns_add(mp_time_ns(), sleeptime);
+ mp_dispatch_adjust_timeout(mpctx->dispatch, abstime);
+ }
+}
+
+// Cause the playloop to run. This can be called from any thread. If called
+// from within the playloop itself, it will be run immediately again, instead
+// of going to sleep in the next mp_wait_events().
+void mp_wakeup_core(struct MPContext *mpctx)
+{
+ mp_dispatch_interrupt(mpctx->dispatch);
+}
+
+// Opaque callback variant of mp_wakeup_core().
+void mp_wakeup_core_cb(void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+ mp_wakeup_core(mpctx);
+}
+
+void mp_core_lock(struct MPContext *mpctx)
+{
+ mp_dispatch_lock(mpctx->dispatch);
+}
+
+void mp_core_unlock(struct MPContext *mpctx)
+{
+ mp_dispatch_unlock(mpctx->dispatch);
+}
+
+// Process any queued user input.
+static void mp_process_input(struct MPContext *mpctx)
+{
+ int processed = 0;
+ for (;;) {
+ mp_cmd_t *cmd = mp_input_read_cmd(mpctx->input);
+ if (!cmd)
+ break;
+ run_command(mpctx, cmd, NULL, NULL, NULL);
+ processed = 1;
+ }
+ mp_set_timeout(mpctx, mp_input_get_delay(mpctx->input));
+ if (processed)
+ mp_notify(mpctx, MP_EVENT_INPUT_PROCESSED, NULL);
+}
+
+double get_relative_time(struct MPContext *mpctx)
+{
+ int64_t new_time = mp_time_ns();
+ int64_t delta = new_time - mpctx->last_time;
+ mpctx->last_time = new_time;
+ return delta * 1e-9;
+}
+
+void update_core_idle_state(struct MPContext *mpctx)
+{
+ bool eof = mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF;
+ bool active = !mpctx->paused && mpctx->restart_complete &&
+ !mpctx->stop_play && mpctx->in_playloop && !eof;
+
+ if (mpctx->playback_active != active) {
+ mpctx->playback_active = active;
+
+ update_screensaver_state(mpctx);
+
+ mp_notify(mpctx, MP_EVENT_CORE_IDLE, NULL);
+ }
+}
+
+bool get_internal_paused(struct MPContext *mpctx)
+{
+ return mpctx->opts->pause || mpctx->paused_for_cache;
+}
+
+// The value passed here is the new value for mpctx->opts->pause
+void set_pause_state(struct MPContext *mpctx, bool user_pause)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ opts->pause = user_pause;
+
+ bool internal_paused = get_internal_paused(mpctx);
+ if (internal_paused != mpctx->paused) {
+ mpctx->paused = internal_paused;
+
+ if (mpctx->ao) {
+ bool eof = mpctx->audio_status == STATUS_EOF;
+ ao_set_paused(mpctx->ao, internal_paused, eof);
+ }
+
+ if (mpctx->video_out)
+ vo_set_paused(mpctx->video_out, internal_paused);
+
+ mpctx->osd_function = 0;
+ mpctx->osd_force_update = true;
+
+ mp_wakeup_core(mpctx);
+
+ if (internal_paused) {
+ mpctx->step_frames = 0;
+ mpctx->time_frame -= get_relative_time(mpctx);
+ } else {
+ (void)get_relative_time(mpctx); // ignore time that passed during pause
+ }
+ }
+
+ update_core_idle_state(mpctx);
+
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->pause);
+}
+
+void update_internal_pause_state(struct MPContext *mpctx)
+{
+ set_pause_state(mpctx, mpctx->opts->pause);
+}
+
+void update_screensaver_state(struct MPContext *mpctx)
+{
+ if (!mpctx->video_out)
+ return;
+
+ bool saver_state = (!mpctx->playback_active || !mpctx->opts->stop_screensaver) &&
+ mpctx->opts->stop_screensaver != 2;
+ vo_control_async(mpctx->video_out, saver_state ? VOCTRL_RESTORE_SCREENSAVER
+ : VOCTRL_KILL_SCREENSAVER, NULL);
+}
+
+void add_step_frame(struct MPContext *mpctx, int dir)
+{
+ if (!mpctx->vo_chain)
+ return;
+ if (dir > 0) {
+ mpctx->step_frames += 1;
+ set_pause_state(mpctx, false);
+ } else if (dir < 0) {
+ if (!mpctx->hrseek_active) {
+ queue_seek(mpctx, MPSEEK_BACKSTEP, 0, MPSEEK_VERY_EXACT, 0);
+ set_pause_state(mpctx, true);
+ }
+ }
+}
+
+// Clear some playback-related fields on file loading or after seeks.
+void reset_playback_state(struct MPContext *mpctx)
+{
+ mp_filter_reset(mpctx->filter_root);
+
+ reset_video_state(mpctx);
+ reset_audio_state(mpctx);
+ reset_subtitle_state(mpctx);
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ // (Often, but not always, this is redundant and also done elsewhere.)
+ if (t->dec)
+ mp_decoder_wrapper_set_play_dir(t->dec, mpctx->play_dir);
+ if (t->d_sub)
+ sub_set_play_dir(t->d_sub, mpctx->play_dir);
+ }
+
+ // May need unpause first
+ if (mpctx->paused_for_cache)
+ update_internal_pause_state(mpctx);
+
+ mpctx->hrseek_active = false;
+ mpctx->hrseek_lastframe = false;
+ mpctx->hrseek_backstep = false;
+ mpctx->current_seek = (struct seek_params){0};
+ mpctx->playback_pts = MP_NOPTS_VALUE;
+ mpctx->step_frames = 0;
+ mpctx->ab_loop_clip = true;
+ mpctx->restart_complete = false;
+ mpctx->paused_for_cache = false;
+ mpctx->cache_buffer = 100;
+ mpctx->cache_update_pts = MP_NOPTS_VALUE;
+
+ encode_lavc_discontinuity(mpctx->encode_lavc_ctx);
+
+ update_internal_pause_state(mpctx);
+ update_core_idle_state(mpctx);
+}
+
+static void mp_seek(MPContext *mpctx, struct seek_params seek)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->demuxer || !seek.type || seek.amount == MP_NOPTS_VALUE)
+ return;
+
+ if (seek.type == MPSEEK_CHAPTER) {
+ mpctx->last_chapter_flag = false;
+ seek.type = MPSEEK_ABSOLUTE;
+ } else {
+ mpctx->last_chapter_seek = -2;
+ }
+
+ bool hr_seek_very_exact = seek.exact == MPSEEK_VERY_EXACT;
+ double current_time = get_playback_time(mpctx);
+ if (current_time == MP_NOPTS_VALUE && seek.type == MPSEEK_RELATIVE)
+ return;
+ if (current_time == MP_NOPTS_VALUE)
+ current_time = 0;
+ double seek_pts = MP_NOPTS_VALUE;
+ int demux_flags = 0;
+
+ switch (seek.type) {
+ case MPSEEK_ABSOLUTE:
+ seek_pts = seek.amount;
+ break;
+ case MPSEEK_BACKSTEP:
+ seek_pts = current_time;
+ hr_seek_very_exact = true;
+ break;
+ case MPSEEK_RELATIVE:
+ demux_flags = seek.amount > 0 ? SEEK_FORWARD : 0;
+ seek_pts = current_time + seek.amount;
+ break;
+ case MPSEEK_FACTOR: ;
+ double len = get_time_length(mpctx);
+ if (len >= 0)
+ seek_pts = seek.amount * len;
+ break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ double demux_pts = seek_pts;
+
+ bool hr_seek = seek.exact != MPSEEK_KEYFRAME && seek_pts != MP_NOPTS_VALUE &&
+ (seek.exact >= MPSEEK_EXACT || opts->hr_seek == 1 ||
+ (opts->hr_seek >= 0 && seek.type == MPSEEK_ABSOLUTE) ||
+ (opts->hr_seek == 2 && (!mpctx->vo_chain || mpctx->vo_chain->is_sparse)));
+
+ // Under certain circumstances, prefer SEEK_FACTOR.
+ if (seek.type == MPSEEK_FACTOR && !hr_seek &&
+ (mpctx->demuxer->ts_resets_possible || seek_pts == MP_NOPTS_VALUE))
+ {
+ demux_pts = seek.amount;
+ demux_flags |= SEEK_FACTOR;
+ }
+
+ int play_dir = opts->play_dir;
+ if (play_dir < 0)
+ demux_flags |= SEEK_SATAN;
+
+ if (hr_seek) {
+ double hr_seek_offset = opts->hr_seek_demuxer_offset;
+ // Always try to compensate for possibly bad demuxers in "special"
+ // situations where we need more robustness from the hr-seek code, even
+ // if the user doesn't use --hr-seek-demuxer-offset.
+ // The value is arbitrary, but should be "good enough" in most situations.
+ if (hr_seek_very_exact)
+ hr_seek_offset = MPMAX(hr_seek_offset, 0.5); // arbitrary
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ double offset = 0;
+ if (!mpctx->tracks[n]->is_external)
+ offset += get_track_seek_offset(mpctx, mpctx->tracks[n]);
+ hr_seek_offset = MPMAX(hr_seek_offset, -offset);
+ }
+ demux_pts -= hr_seek_offset * play_dir;
+ demux_flags = (demux_flags | SEEK_HR) & ~SEEK_FORWARD;
+ // For HR seeks in backward playback mode, the correct seek rounding
+ // direction is forward instead of backward.
+ if (play_dir < 0)
+ demux_flags |= SEEK_FORWARD;
+ }
+
+ if (!mpctx->demuxer->seekable)
+ demux_flags |= SEEK_CACHED;
+
+ demux_flags |= SEEK_BLOCK;
+
+ if (!demux_seek(mpctx->demuxer, demux_pts, demux_flags)) {
+ if (!mpctx->demuxer->seekable) {
+ MP_ERR(mpctx, "Cannot seek in this stream.\n");
+ MP_ERR(mpctx, "You can force it with '--force-seekable=yes'.\n");
+ }
+ return;
+ }
+
+ mpctx->play_dir = play_dir;
+
+ // Seek external, extra files too:
+ for (int t = 0; t < mpctx->num_tracks; t++) {
+ struct track *track = mpctx->tracks[t];
+ if (track->selected && track->is_external && track->demuxer) {
+ double main_new_pos = demux_pts;
+ if (!hr_seek || track->is_external)
+ main_new_pos += get_track_seek_offset(mpctx, track);
+ if (demux_flags & SEEK_FACTOR)
+ main_new_pos = seek_pts;
+ demux_seek(track->demuxer, main_new_pos,
+ demux_flags & (SEEK_SATAN | SEEK_BLOCK));
+ }
+ }
+
+ if (!(seek.flags & MPSEEK_FLAG_NOFLUSH))
+ clear_audio_output_buffers(mpctx);
+
+ reset_playback_state(mpctx);
+
+ demux_block_reading(mpctx->demuxer, false);
+ for (int t = 0; t < mpctx->num_tracks; t++) {
+ struct track *track = mpctx->tracks[t];
+ if (track->selected && track->demuxer)
+ demux_block_reading(track->demuxer, false);
+ }
+
+ /* Use the target time as "current position" for further relative
+ * seeks etc until a new video frame has been decoded */
+ mpctx->last_seek_pts = seek_pts;
+
+ if (hr_seek) {
+ mpctx->hrseek_active = true;
+ mpctx->hrseek_backstep = seek.type == MPSEEK_BACKSTEP;
+ mpctx->hrseek_pts = seek_pts * mpctx->play_dir;
+
+ // allow decoder to drop frames before hrseek_pts
+ bool hrseek_framedrop = !hr_seek_very_exact && opts->hr_seek_framedrop;
+
+ MP_VERBOSE(mpctx, "hr-seek, skipping to %f%s%s\n", mpctx->hrseek_pts,
+ hrseek_framedrop ? "" : " (no framedrop)",
+ mpctx->hrseek_backstep ? " (backstep)" : "");
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ struct mp_decoder_wrapper *dec = track->dec;
+ if (dec && hrseek_framedrop)
+ mp_decoder_wrapper_set_start_pts(dec, mpctx->hrseek_pts);
+ }
+ }
+
+ if (mpctx->stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = KEEP_PLAYING;
+
+ mpctx->start_timestamp = mp_time_sec();
+ mp_wakeup_core(mpctx);
+
+ mp_notify(mpctx, MPV_EVENT_SEEK, NULL);
+ mp_notify(mpctx, MPV_EVENT_TICK, NULL);
+
+ update_ab_loop_clip(mpctx);
+
+ mpctx->current_seek = seek;
+}
+
+// This combines consecutive seek requests.
+void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
+ enum seek_precision exact, int flags)
+{
+ struct seek_params *seek = &mpctx->seek;
+
+ mp_wakeup_core(mpctx);
+
+ if (mpctx->stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = KEEP_PLAYING;
+
+ switch (type) {
+ case MPSEEK_RELATIVE:
+ seek->flags |= flags;
+ if (seek->type == MPSEEK_FACTOR)
+ return; // Well... not common enough to bother doing better
+ seek->amount += amount;
+ seek->exact = MPMAX(seek->exact, exact);
+ if (seek->type == MPSEEK_NONE)
+ seek->exact = exact;
+ if (seek->type == MPSEEK_ABSOLUTE)
+ return;
+ seek->type = MPSEEK_RELATIVE;
+ return;
+ case MPSEEK_ABSOLUTE:
+ case MPSEEK_FACTOR:
+ case MPSEEK_BACKSTEP:
+ case MPSEEK_CHAPTER:
+ *seek = (struct seek_params) {
+ .type = type,
+ .amount = amount,
+ .exact = exact,
+ .flags = flags,
+ };
+ return;
+ case MPSEEK_NONE:
+ *seek = (struct seek_params){ 0 };
+ return;
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+void execute_queued_seek(struct MPContext *mpctx)
+{
+ if (mpctx->seek.type) {
+ bool queued_hr_seek = mpctx->seek.exact != MPSEEK_KEYFRAME;
+ // Let explicitly imprecise seeks cancel precise seeks:
+ if (mpctx->hrseek_active && !queued_hr_seek)
+ mpctx->start_timestamp = -1e9;
+ // If the user seeks continuously (keeps arrow key down) try to finish
+ // showing a frame from one location before doing another seek (instead
+ // of never updating the screen).
+ if ((mpctx->seek.flags & MPSEEK_FLAG_DELAY) &&
+ mp_time_sec() - mpctx->start_timestamp < 0.3)
+ {
+ // Wait until a video frame is available and has been shown.
+ if (mpctx->video_status < STATUS_PLAYING)
+ return;
+ // On A/V hr-seeks, always wait for the full result, to avoid corner
+ // cases when seeking past EOF (we want it to determine that EOF
+ // actually happened, instead of overwriting it with the new seek).
+ if (mpctx->hrseek_active && queued_hr_seek && mpctx->vo_chain &&
+ mpctx->ao_chain && !mpctx->restart_complete)
+ return;
+ }
+ mp_seek(mpctx, mpctx->seek);
+ mpctx->seek = (struct seek_params){0};
+ }
+}
+
+// NOPTS (i.e. <0) if unknown
+double get_time_length(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ return demuxer && demuxer->duration >= 0 ? demuxer->duration : MP_NOPTS_VALUE;
+}
+
+// Return approximate PTS of first frame played. This can be completely wrong
+// for a number of reasons in a number of situations.
+double get_start_time(struct MPContext *mpctx, int dir)
+{
+ double res = 0;
+ if (mpctx->demuxer) {
+ if (!mpctx->opts->rebase_start_time)
+ res += mpctx->demuxer->start_time;
+ if (dir < 0)
+ res += MPMAX(mpctx->demuxer->duration, 0);
+ }
+ return res;
+}
+
+double get_current_time(struct MPContext *mpctx)
+{
+ if (!mpctx->demuxer)
+ return MP_NOPTS_VALUE;
+ if (mpctx->playback_pts != MP_NOPTS_VALUE)
+ return mpctx->playback_pts * mpctx->play_dir;
+ return mpctx->last_seek_pts;
+}
+
+double get_playback_time(struct MPContext *mpctx)
+{
+ double cur = get_current_time(mpctx);
+ // During seeking, the time corresponds to the last seek time - apply some
+ // cosmetics to it.
+ if (cur != MP_NOPTS_VALUE && mpctx->playback_pts == MP_NOPTS_VALUE) {
+ double length = get_time_length(mpctx);
+ if (length >= 0)
+ cur = MPCLAMP(cur, 0, length);
+ }
+ return cur;
+}
+
+// Return playback position in 0.0-1.0 ratio, or -1 if unknown.
+double get_current_pos_ratio(struct MPContext *mpctx, bool use_range)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return -1;
+ double ans = -1;
+ double start = 0;
+ double len = get_time_length(mpctx);
+ if (use_range) {
+ double startpos = get_play_start_pts(mpctx);
+ double endpos = get_play_end_pts(mpctx);
+ if (endpos > MPMAX(0, len))
+ endpos = MPMAX(0, len);
+ if (endpos < startpos)
+ endpos = startpos;
+ start = startpos;
+ len = endpos - startpos;
+ }
+ double pos = get_current_time(mpctx);
+ if (len > 0)
+ ans = MPCLAMP((pos - start) / len, 0, 1);
+ if (ans < 0) {
+ int64_t size = demuxer->filesize;
+ if (size > 0 && demuxer->filepos >= 0)
+ ans = MPCLAMP(demuxer->filepos / (double)size, 0, 1);
+ }
+ if (use_range) {
+ if (mpctx->opts->play_frames > 0)
+ ans = MPMAX(ans, 1.0 -
+ mpctx->max_frames / (double) mpctx->opts->play_frames);
+ }
+ return ans;
+}
+
+// 0-100, -1 if unknown
+int get_percent_pos(struct MPContext *mpctx)
+{
+ double pos = get_current_pos_ratio(mpctx, false);
+ return pos < 0 ? -1 : (int)round(pos * 100);
+}
+
+// -2 is no chapters, -1 is before first chapter
+int get_current_chapter(struct MPContext *mpctx)
+{
+ if (!mpctx->num_chapters)
+ return -2;
+ double current_pts = get_current_time(mpctx);
+ int i;
+ for (i = 0; i < mpctx->num_chapters; i++)
+ if (current_pts < mpctx->chapters[i].pts)
+ break;
+ return mpctx->last_chapter_flag ?
+ mpctx->last_chapter_seek : MPMAX(mpctx->last_chapter_seek, i - 1);
+}
+
+char *chapter_display_name(struct MPContext *mpctx, int chapter)
+{
+ char *name = chapter_name(mpctx, chapter);
+ char *dname = NULL;
+ if (name) {
+ dname = talloc_asprintf(NULL, "(%d) %s", chapter + 1, name);
+ } else if (chapter < -1) {
+ dname = talloc_strdup(NULL, "(unavailable)");
+ } else {
+ int chapter_count = get_chapter_count(mpctx);
+ if (chapter_count <= 0)
+ dname = talloc_asprintf(NULL, "(%d)", chapter + 1);
+ else
+ dname = talloc_asprintf(NULL, "(%d) of %d", chapter + 1,
+ chapter_count);
+ }
+ return dname;
+}
+
+// returns NULL if chapter name unavailable
+char *chapter_name(struct MPContext *mpctx, int chapter)
+{
+ if (chapter < 0 || chapter >= mpctx->num_chapters)
+ return NULL;
+ return mp_tags_get_str(mpctx->chapters[chapter].metadata, "title");
+}
+
+// returns the start of the chapter in seconds (NOPTS if unavailable)
+double chapter_start_time(struct MPContext *mpctx, int chapter)
+{
+ if (chapter == -1)
+ return 0;
+ if (chapter >= 0 && chapter < mpctx->num_chapters)
+ return mpctx->chapters[chapter].pts;
+ return MP_NOPTS_VALUE;
+}
+
+int get_chapter_count(struct MPContext *mpctx)
+{
+ return mpctx->num_chapters;
+}
+
+// If the current playback position (or seek target) falls before the B
+// position, actually make playback loop when reaching the B point. The
+// intention is that you can seek out of the ab-loop range.
+void update_ab_loop_clip(struct MPContext *mpctx)
+{
+ double pts = get_current_time(mpctx);
+ double ab[2];
+ mpctx->ab_loop_clip = pts != MP_NOPTS_VALUE &&
+ get_ab_loop_times(mpctx, ab) &&
+ pts * mpctx->play_dir <= ab[1] * mpctx->play_dir;
+}
+
+static void handle_osd_redraw(struct MPContext *mpctx)
+{
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ return;
+ // If we're playing normally, let OSD be redrawn naturally as part of
+ // video display.
+ if (!mpctx->paused) {
+ if (mpctx->sleeptime < 0.1 && mpctx->video_status == STATUS_PLAYING)
+ return;
+ }
+ // Don't redraw immediately during a seek (makes it significantly slower).
+ bool use_video = mpctx->vo_chain && !mpctx->vo_chain->is_sparse;
+ if (use_video && mp_time_sec() - mpctx->start_timestamp < 0.1) {
+ mp_set_timeout(mpctx, 0.1);
+ return;
+ }
+ bool want_redraw = osd_query_and_reset_want_redraw(mpctx->osd) ||
+ vo_want_redraw(mpctx->video_out);
+ if (!want_redraw)
+ return;
+ vo_redraw(mpctx->video_out);
+}
+
+static void clear_underruns(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain && mpctx->ao_chain->underrun) {
+ mpctx->ao_chain->underrun = false;
+ mp_wakeup_core(mpctx);
+ }
+
+ if (mpctx->vo_chain && mpctx->vo_chain->underrun) {
+ mpctx->vo_chain->underrun = false;
+ mp_wakeup_core(mpctx);
+ }
+}
+
+static void handle_update_cache(struct MPContext *mpctx)
+{
+ bool force_update = false;
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->demuxer || mpctx->encode_lavc_ctx) {
+ clear_underruns(mpctx);
+ return;
+ }
+
+ double now = mp_time_sec();
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ mpctx->demux_underrun |= s.underrun;
+
+ int cache_buffer = 100;
+ bool use_pause_on_low_cache = opts->cache_pause && mpctx->play_dir > 0;
+
+ if (!mpctx->restart_complete) {
+ // Audio or video is restarting, and initial buffering is enabled. Make
+ // sure we actually restart them in paused mode, so no audio gets
+ // dropped and video technically doesn't start yet.
+ use_pause_on_low_cache &= opts->cache_pause_initial &&
+ (mpctx->video_status == STATUS_READY ||
+ mpctx->audio_status == STATUS_READY);
+ }
+
+ bool is_low = use_pause_on_low_cache && !s.idle &&
+ s.ts_duration < opts->cache_pause_wait;
+
+ // Enter buffering state only if there actually was an underrun (or if
+ // initial caching before playback restart is used).
+ bool need_wait = is_low;
+ if (is_low && !mpctx->paused_for_cache && mpctx->restart_complete) {
+ // Wait only if an output underrun was registered. (Or if there is no
+ // underrun detection.)
+ bool output_underrun = false;
+
+ if (mpctx->ao_chain)
+ output_underrun |= mpctx->ao_chain->underrun;
+ if (mpctx->vo_chain)
+ output_underrun |= mpctx->vo_chain->underrun;
+
+ // Output underruns could be sporadic (unrelated to demuxer buffer state
+ // and for example caused by slow decoding), so use a past demuxer
+ // underrun as indication that the underrun was possibly due to a
+ // demuxer underrun.
+ need_wait = mpctx->demux_underrun && output_underrun;
+ }
+
+ // Let the underrun flag "stick" around until the cache has fully recovered.
+ // See logic where demux_underrun is used.
+ if (!is_low)
+ mpctx->demux_underrun = false;
+
+ if (mpctx->paused_for_cache != need_wait) {
+ mpctx->paused_for_cache = need_wait;
+ update_internal_pause_state(mpctx);
+ force_update = true;
+ if (mpctx->paused_for_cache)
+ mpctx->cache_stop_time = now;
+ }
+
+ if (!mpctx->paused_for_cache)
+ clear_underruns(mpctx);
+
+ if (mpctx->paused_for_cache) {
+ cache_buffer =
+ 100 * MPCLAMP(s.ts_duration / opts->cache_pause_wait, 0, 0.99);
+ mp_set_timeout(mpctx, 0.2);
+ }
+
+ // Also update cache properties.
+ bool busy = !s.idle;
+ if (fabs(mpctx->cache_update_pts - mpctx->playback_pts) >= 1.0)
+ busy = true;
+ if (busy || mpctx->next_cache_update > 0) {
+ if (mpctx->next_cache_update <= now) {
+ mpctx->next_cache_update = busy ? now + 0.25 : 0;
+ force_update = true;
+ }
+ if (mpctx->next_cache_update > 0)
+ mp_set_timeout(mpctx, mpctx->next_cache_update - now);
+ }
+
+ if (mpctx->cache_buffer != cache_buffer) {
+ if ((mpctx->cache_buffer == 100) != (cache_buffer == 100)) {
+ if (cache_buffer < 100) {
+ MP_VERBOSE(mpctx, "Enter buffering (buffer went from %d%% -> %d%%) [%fs].\n",
+ mpctx->cache_buffer, cache_buffer, s.ts_duration);
+ } else {
+ double t = now - mpctx->cache_stop_time;
+ MP_VERBOSE(mpctx, "End buffering (waited %f secs) [%fs].\n",
+ t, s.ts_duration);
+ }
+ } else {
+ MP_VERBOSE(mpctx, "Still buffering (buffer went from %d%% -> %d%%) [%fs].\n",
+ mpctx->cache_buffer, cache_buffer, s.ts_duration);
+ }
+ mpctx->cache_buffer = cache_buffer;
+ force_update = true;
+ }
+
+ if (s.eof && !busy)
+ prefetch_next(mpctx);
+
+ if (force_update) {
+ mpctx->cache_update_pts = mpctx->playback_pts;
+ mp_notify(mpctx, MP_EVENT_CACHE_UPDATE, NULL);
+ }
+}
+
+int get_cache_buffering_percentage(struct MPContext *mpctx)
+{
+ return mpctx->demuxer ? mpctx->cache_buffer : -1;
+}
+
+static void handle_cursor_autohide(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo *vo = mpctx->video_out;
+
+ if (!vo)
+ return;
+
+ bool mouse_cursor_visible = mpctx->mouse_cursor_visible;
+ double now = mp_time_sec();
+
+ unsigned mouse_event_ts = mp_input_get_mouse_event_counter(mpctx->input);
+ if (mpctx->mouse_event_ts != mouse_event_ts) {
+ mpctx->mouse_event_ts = mouse_event_ts;
+ mpctx->mouse_timer = now + opts->cursor_autohide_delay / 1000.0;
+ mouse_cursor_visible = true;
+ }
+
+ if (mpctx->mouse_timer > now) {
+ mp_set_timeout(mpctx, mpctx->mouse_timer - now);
+ } else {
+ mouse_cursor_visible = false;
+ }
+
+ if (opts->cursor_autohide_delay == -1)
+ mouse_cursor_visible = true;
+
+ if (opts->cursor_autohide_delay == -2)
+ mouse_cursor_visible = false;
+
+ if (opts->cursor_autohide_fs && !opts->vo->fullscreen)
+ mouse_cursor_visible = true;
+
+ if (mouse_cursor_visible != mpctx->mouse_cursor_visible)
+ vo_control(vo, VOCTRL_SET_CURSOR_VISIBILITY, &mouse_cursor_visible);
+ mpctx->mouse_cursor_visible = mouse_cursor_visible;
+}
+
+static void handle_vo_events(struct MPContext *mpctx)
+{
+ struct vo *vo = mpctx->video_out;
+ int events = vo ? vo_query_and_reset_events(vo, VO_EVENTS_USER) : 0;
+ if (events & VO_EVENT_RESIZE)
+ mp_notify(mpctx, MP_EVENT_WIN_RESIZE, NULL);
+ if (events & VO_EVENT_WIN_STATE)
+ mp_notify(mpctx, MP_EVENT_WIN_STATE, NULL);
+ if (events & VO_EVENT_DPI)
+ mp_notify(mpctx, MP_EVENT_WIN_STATE2, NULL);
+ if (events & VO_EVENT_FOCUS)
+ mp_notify(mpctx, MP_EVENT_FOCUS, NULL);
+}
+
+static void handle_sstep(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (mpctx->stop_play || !mpctx->restart_complete)
+ return;
+
+ if (opts->step_sec > 0 && !mpctx->paused) {
+ set_osd_function(mpctx, OSD_FFW);
+ queue_seek(mpctx, MPSEEK_RELATIVE, opts->step_sec, MPSEEK_DEFAULT, 0);
+ }
+
+ if (mpctx->video_status >= STATUS_EOF) {
+ if (mpctx->max_frames >= 0 && !mpctx->stop_play)
+ mpctx->stop_play = AT_END_OF_FILE; // force EOF even if audio left
+ if (mpctx->step_frames > 0 && !mpctx->paused)
+ set_pause_state(mpctx, true);
+ }
+}
+
+static void handle_loop_file(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->stop_play != AT_END_OF_FILE)
+ return;
+
+ double target = MP_NOPTS_VALUE;
+ enum seek_precision prec = MPSEEK_DEFAULT;
+
+ double ab[2];
+ if (get_ab_loop_times(mpctx, ab) && mpctx->ab_loop_clip) {
+ if (opts->ab_loop_count > 0) {
+ opts->ab_loop_count--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->ab_loop_count);
+ }
+ target = ab[0];
+ prec = MPSEEK_EXACT;
+ } else if (opts->loop_file) {
+ if (opts->loop_file > 0) {
+ opts->loop_file--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->loop_file);
+ }
+ target = get_start_time(mpctx, mpctx->play_dir);
+ }
+
+ if (target != MP_NOPTS_VALUE) {
+ if (!mpctx->shown_aframes && !mpctx->shown_vframes) {
+ MP_WARN(mpctx, "No media data to loop.\n");
+ return;
+ }
+
+ mpctx->stop_play = KEEP_PLAYING;
+ set_osd_function(mpctx, OSD_FFW);
+ mark_seek(mpctx);
+
+ // Assumes execute_queued_seek() happens before next audio/video is
+ // attempted to be decoded or filtered.
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, target, prec, MPSEEK_FLAG_NOFLUSH);
+ }
+}
+
+void seek_to_last_frame(struct MPContext *mpctx)
+{
+ if (!mpctx->vo_chain)
+ return;
+ if (mpctx->hrseek_lastframe) // exit if we already tried this
+ return;
+ MP_VERBOSE(mpctx, "seeking to last frame...\n");
+ // Approximately seek close to the end of the file.
+ // Usually, it will seek some seconds before end.
+ double end = MP_NOPTS_VALUE;
+ if (mpctx->play_dir > 0) {
+ end = get_play_end_pts(mpctx);
+ if (end == MP_NOPTS_VALUE)
+ end = get_time_length(mpctx);
+ } else {
+ end = get_start_time(mpctx, 1);
+ }
+ mp_seek(mpctx, (struct seek_params){
+ .type = MPSEEK_ABSOLUTE,
+ .amount = end,
+ .exact = MPSEEK_VERY_EXACT,
+ });
+ // Make it exact: stop seek only if last frame was reached.
+ if (mpctx->hrseek_active) {
+ mpctx->hrseek_pts = INFINITY * mpctx->play_dir;
+ mpctx->hrseek_lastframe = true;
+ }
+}
+
+static void handle_keep_open(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->keep_open && mpctx->stop_play == AT_END_OF_FILE &&
+ (opts->keep_open == 2 ||
+ (!playlist_get_next(mpctx->playlist, 1) && opts->loop_times == 1)))
+ {
+ mpctx->stop_play = KEEP_PLAYING;
+ if (mpctx->vo_chain) {
+ if (!vo_has_frame(mpctx->video_out)) { // EOF not reached normally
+ seek_to_last_frame(mpctx);
+ mpctx->audio_status = STATUS_EOF;
+ mpctx->video_status = STATUS_EOF;
+ }
+ }
+ if (opts->keep_open_pause) {
+ if (mpctx->ao && ao_is_playing(mpctx->ao))
+ return;
+ set_pause_state(mpctx, true);
+ }
+ }
+}
+
+static void handle_chapter_change(struct MPContext *mpctx)
+{
+ int chapter = get_current_chapter(mpctx);
+ if (chapter != mpctx->last_chapter) {
+ mpctx->last_chapter = chapter;
+ mp_notify(mpctx, MP_EVENT_CHAPTER_CHANGE, NULL);
+ }
+}
+
+// Execute a forceful refresh of the VO window. This clears the window from
+// the previous video. It also creates/destroys the VO on demand.
+// It tries to make the change only in situations where the window is
+// definitely needed or not needed, or if the force parameter is set (the
+// latter also decides whether to clear an existing window, because there's
+// no way to know if this has already been done or not).
+int handle_force_window(struct MPContext *mpctx, bool force)
+{
+ // True if we're either in idle mode, or loading of the file has finished.
+ // It's also set via force in some stages during file loading.
+ bool act = mpctx->stop_play || mpctx->playback_initialized || force;
+
+ // On the other hand, if a video track is selected, but no video is ever
+ // decoded on it, then create the window.
+ bool stalled_video = mpctx->playback_initialized && mpctx->restart_complete &&
+ mpctx->video_status == STATUS_EOF && mpctx->vo_chain &&
+ !mpctx->video_out->config_ok;
+
+ // Don't interfere with real video playback
+ if (mpctx->vo_chain && !stalled_video)
+ return 0;
+
+ if (!mpctx->opts->force_vo) {
+ if (act && !mpctx->vo_chain)
+ uninit_video_out(mpctx);
+ return 0;
+ }
+
+ if (mpctx->opts->force_vo != 2 && !act)
+ return 0;
+
+ if (!mpctx->video_out) {
+ struct vo_extra ex = {
+ .input_ctx = mpctx->input,
+ .osd = mpctx->osd,
+ .encode_lavc_ctx = mpctx->encode_lavc_ctx,
+ .wakeup_cb = mp_wakeup_core_cb,
+ .wakeup_ctx = mpctx,
+ };
+ mpctx->video_out = init_best_video_out(mpctx->global, &ex);
+ if (!mpctx->video_out)
+ goto err;
+ mpctx->mouse_cursor_visible = true;
+ }
+
+ if (!mpctx->video_out->config_ok || force) {
+ struct vo *vo = mpctx->video_out;
+ // Pick whatever works
+ int config_format = 0;
+ uint8_t fmts[IMGFMT_END - IMGFMT_START] = {0};
+ vo_query_formats(vo, fmts);
+ for (int fmt = IMGFMT_START; fmt < IMGFMT_END; fmt++) {
+ if (fmts[fmt - IMGFMT_START]) {
+ config_format = fmt;
+ break;
+ }
+ }
+ int w = 960;
+ int h = 480;
+ struct mp_image_params p = {
+ .imgfmt = config_format,
+ .w = w, .h = h,
+ .p_w = 1, .p_h = 1,
+ .force_window = true,
+ };
+ if (vo_reconfig(vo, &p) < 0)
+ goto err;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ update_content_type(mpctx, track);
+ update_screensaver_state(mpctx);
+ vo_set_paused(vo, true);
+ vo_redraw(vo);
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+
+ return 0;
+
+err:
+ mpctx->opts->force_vo = 0;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &mpctx->opts->force_vo);
+ uninit_video_out(mpctx);
+ MP_FATAL(mpctx, "Error opening/initializing the VO window.\n");
+ return -1;
+}
+
+// Potentially needed by some Lua scripts, which assume TICK always comes.
+static void handle_dummy_ticks(struct MPContext *mpctx)
+{
+ if ((mpctx->video_status != STATUS_PLAYING &&
+ mpctx->video_status != STATUS_DRAINING) ||
+ mpctx->paused)
+ {
+ if (mp_time_sec() - mpctx->last_idle_tick > 0.050) {
+ mpctx->last_idle_tick = mp_time_sec();
+ mp_notify(mpctx, MPV_EVENT_TICK, NULL);
+ }
+ }
+}
+
+// Update current playback time.
+static void handle_playback_time(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain &&
+ !mpctx->vo_chain->is_sparse &&
+ mpctx->video_status >= STATUS_PLAYING &&
+ mpctx->video_status < STATUS_EOF)
+ {
+ mpctx->playback_pts = mpctx->video_pts;
+ } else if (mpctx->audio_status >= STATUS_PLAYING &&
+ mpctx->audio_status < STATUS_EOF)
+ {
+ mpctx->playback_pts = playing_audio_pts(mpctx);
+ } else if (mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF)
+ {
+ double apts = playing_audio_pts(mpctx);
+ double vpts = mpctx->video_pts;
+ double mpts = MP_PTS_MAX(apts, vpts);
+ if (mpts != MP_NOPTS_VALUE)
+ mpctx->playback_pts = mpts;
+ }
+}
+
+// We always make sure audio and video buffers are filled before actually
+// starting playback. This code handles starting them at the same time.
+static void handle_playback_restart(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->audio_status < STATUS_READY ||
+ mpctx->video_status < STATUS_READY)
+ return;
+
+ handle_update_cache(mpctx);
+
+ if (mpctx->video_status == STATUS_READY) {
+ mpctx->video_status = STATUS_PLAYING;
+ get_relative_time(mpctx);
+ mp_wakeup_core(mpctx);
+ MP_DBG(mpctx, "starting video playback\n");
+ }
+
+ if (mpctx->audio_status == STATUS_READY) {
+ // If a new seek is queued while the current one finishes, don't
+ // actually play the audio, but resume seeking immediately.
+ if (mpctx->seek.type && mpctx->video_status == STATUS_PLAYING) {
+ handle_playback_time(mpctx);
+ mpctx->seek.flags &= ~MPSEEK_FLAG_DELAY; // immediately
+ execute_queued_seek(mpctx);
+ return;
+ }
+
+ audio_start_ao(mpctx);
+ }
+
+ if (!mpctx->restart_complete) {
+ mpctx->hrseek_active = false;
+ mpctx->restart_complete = true;
+ mpctx->current_seek = (struct seek_params){0};
+ handle_playback_time(mpctx);
+ mp_notify(mpctx, MPV_EVENT_PLAYBACK_RESTART, NULL);
+ update_core_idle_state(mpctx);
+ if (!mpctx->playing_msg_shown) {
+ if (opts->playing_msg && opts->playing_msg[0]) {
+ char *msg =
+ mp_property_expand_escaped_string(mpctx, opts->playing_msg);
+ struct mp_log *log = mp_log_new(NULL, mpctx->log, "!term-msg");
+ mp_info(log, "%s\n", msg);
+ talloc_free(log);
+ talloc_free(msg);
+ }
+ if (opts->osd_playing_msg && opts->osd_playing_msg[0]) {
+ char *msg =
+ mp_property_expand_escaped_string(mpctx, opts->osd_playing_msg);
+ set_osd_msg(mpctx, 1, opts->osd_playing_msg_duration ?
+ opts->osd_playing_msg_duration : opts->osd_duration,
+ "%s", msg);
+ talloc_free(msg);
+ }
+ }
+ mpctx->playing_msg_shown = true;
+ mp_wakeup_core(mpctx);
+ update_ab_loop_clip(mpctx);
+ MP_VERBOSE(mpctx, "playback restart complete @ %f, audio=%s, video=%s%s\n",
+ mpctx->playback_pts, mp_status_str(mpctx->audio_status),
+ mp_status_str(mpctx->video_status),
+ get_internal_paused(mpctx) ? " (paused)" : "");
+
+ // To avoid strange effects when using relative seeks, especially if
+ // there are no proper audio & video timestamps (seeks after EOF).
+ double length = get_time_length(mpctx);
+ if (mpctx->last_seek_pts != MP_NOPTS_VALUE && length >= 0)
+ mpctx->last_seek_pts = MPCLAMP(mpctx->last_seek_pts, 0, length);
+
+ // Continuous seeks past EOF => treat as EOF instead of repeating seek.
+ if (mpctx->seek.type == MPSEEK_RELATIVE && mpctx->seek.amount > 0 &&
+ mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF)
+ mpctx->seek = (struct seek_params){0};
+ }
+}
+
+static void handle_eof(struct MPContext *mpctx)
+{
+ if (mpctx->seek.type)
+ return; // for proper keep-open operation
+
+ /* Don't quit while paused and we're displaying the last video frame. On the
+ * other hand, if we don't have a video frame, then the user probably seeked
+ * outside of the video, and we do want to quit. */
+ bool prevent_eof =
+ mpctx->paused && mpctx->video_out && vo_has_frame(mpctx->video_out);
+ /* It's possible for the user to simultaneously switch both audio
+ * and video streams to "disabled" at runtime. Handle this by waiting
+ * rather than immediately stopping playback due to EOF.
+ */
+ if ((mpctx->ao_chain || mpctx->vo_chain) && !prevent_eof &&
+ mpctx->audio_status == STATUS_EOF &&
+ mpctx->video_status == STATUS_EOF &&
+ !mpctx->stop_play)
+ {
+ mpctx->stop_play = AT_END_OF_FILE;
+ }
+}
+
+void run_playloop(struct MPContext *mpctx)
+{
+ if (encode_lavc_didfail(mpctx->encode_lavc_ctx)) {
+ mpctx->stop_play = PT_ERROR;
+ return;
+ }
+
+ update_demuxer_properties(mpctx);
+
+ handle_cursor_autohide(mpctx);
+ handle_vo_events(mpctx);
+ handle_command_updates(mpctx);
+
+ if (mpctx->lavfi && mp_filter_has_failed(mpctx->lavfi))
+ mpctx->stop_play = AT_END_OF_FILE;
+
+ fill_audio_out_buffers(mpctx);
+ write_video(mpctx);
+
+ handle_playback_restart(mpctx);
+
+ handle_playback_time(mpctx);
+
+ handle_dummy_ticks(mpctx);
+
+ update_osd_msg(mpctx);
+ if (mpctx->video_status == STATUS_EOF)
+ update_subtitles(mpctx, mpctx->playback_pts);
+
+ handle_each_frame_screenshot(mpctx);
+
+ handle_eof(mpctx);
+
+ handle_loop_file(mpctx);
+
+ handle_keep_open(mpctx);
+
+ handle_sstep(mpctx);
+
+ update_core_idle_state(mpctx);
+
+ execute_queued_seek(mpctx);
+
+ if (mpctx->stop_play)
+ return;
+
+ handle_osd_redraw(mpctx);
+
+ if (mp_filter_graph_run(mpctx->filter_root))
+ mp_wakeup_core(mpctx);
+
+ mp_wait_events(mpctx);
+
+ handle_update_cache(mpctx);
+
+ mp_process_input(mpctx);
+
+ handle_chapter_change(mpctx);
+
+ handle_force_window(mpctx, false);
+}
+
+void mp_idle(struct MPContext *mpctx)
+{
+ handle_dummy_ticks(mpctx);
+ mp_wait_events(mpctx);
+ mp_process_input(mpctx);
+ handle_command_updates(mpctx);
+ handle_update_cache(mpctx);
+ handle_cursor_autohide(mpctx);
+ handle_vo_events(mpctx);
+ update_osd_msg(mpctx);
+ handle_osd_redraw(mpctx);
+}
+
+// Waiting for the slave master to send us a new file to play.
+void idle_loop(struct MPContext *mpctx)
+{
+ // ================= idle loop (STOP state) =========================
+ bool need_reinit = true;
+ while (mpctx->opts->player_idle_mode && mpctx->stop_play == PT_STOP) {
+ if (need_reinit) {
+ uninit_audio_out(mpctx);
+ handle_force_window(mpctx, true);
+ mp_wakeup_core(mpctx);
+ mp_notify(mpctx, MPV_EVENT_IDLE, NULL);
+ need_reinit = false;
+ }
+ mp_idle(mpctx);
+ }
+}
diff --git a/player/screenshot.c b/player/screenshot.c
new file mode 100644
index 0000000..e4d0912
--- /dev/null
+++ b/player/screenshot.c
@@ -0,0 +1,611 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <libavcodec/avcodec.h>
+
+#include "common/global.h"
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+#include "screenshot.h"
+#include "core.h"
+#include "command.h"
+#include "input/cmd.h"
+#include "misc/bstr.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "misc/thread_tools.h"
+#include "common/msg.h"
+#include "options/path.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+#include "video/out/vo.h"
+#include "video/image_writer.h"
+#include "video/sws_utils.h"
+#include "sub/osd.h"
+
+#include "video/csputils.h"
+
+#define MODE_FULL_WINDOW 1
+#define MODE_SUBTITLES 2
+
+typedef struct screenshot_ctx {
+ struct MPContext *mpctx;
+ struct mp_log *log;
+
+ // Command to repeat in each-frame mode.
+ struct mp_cmd *each_frame;
+
+ int frameno;
+ uint64_t last_frame_count;
+} screenshot_ctx;
+
+void screenshot_init(struct MPContext *mpctx)
+{
+ mpctx->screenshot_ctx = talloc(mpctx, screenshot_ctx);
+ *mpctx->screenshot_ctx = (screenshot_ctx) {
+ .mpctx = mpctx,
+ .frameno = 1,
+ .log = mp_log_new(mpctx, mpctx->log, "screenshot")
+ };
+}
+
+static char *stripext(void *talloc_ctx, const char *s)
+{
+ const char *end = strrchr(s, '.');
+ if (!end)
+ end = s + strlen(s);
+ return talloc_asprintf(talloc_ctx, "%.*s", (int)(end - s), s);
+}
+
+static bool write_screenshot(struct mp_cmd_ctx *cmd, struct mp_image *img,
+ const char *filename, struct image_writer_opts *opts)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ struct image_writer_opts *gopts = mpctx->opts->screenshot_image_opts;
+ struct image_writer_opts opts_copy = opts ? *opts : *gopts;
+
+ mp_cmd_msg(cmd, MSGL_V, "Starting screenshot: '%s'", filename);
+
+ mp_core_unlock(mpctx);
+
+ bool ok = img && write_image(img, &opts_copy, filename, mpctx->global,
+ mpctx->screenshot_ctx->log);
+
+ mp_core_lock(mpctx);
+
+ if (ok) {
+ mp_cmd_msg(cmd, MSGL_INFO, "Screenshot: '%s'", filename);
+ } else {
+ mp_cmd_msg(cmd, MSGL_ERR, "Error writing screenshot!");
+ }
+ return ok;
+}
+
+#ifdef _WIN32
+#define ILLEGAL_FILENAME_CHARS "?\"/\\<>*|:"
+#else
+#define ILLEGAL_FILENAME_CHARS "/"
+#endif
+
+// Replace all characters disallowed in filenames with '_' and return the newly
+// allocated result string.
+static char *sanitize_filename(void *talloc_ctx, const char *s)
+{
+ char *res = talloc_strdup(talloc_ctx, s);
+ char *cur = res;
+ while (*cur) {
+ if (strchr(ILLEGAL_FILENAME_CHARS, *cur) || ((unsigned char)*cur) < 32)
+ *cur = '_';
+ cur++;
+ }
+ return res;
+}
+
+static void append_filename(char **s, const char *f)
+{
+ char *append = sanitize_filename(NULL, f);
+ *s = talloc_strdup_append(*s, append);
+ talloc_free(append);
+}
+
+static char *create_fname(struct MPContext *mpctx, char *template,
+ const char *file_ext, int *sequence, int *frameno)
+{
+ char *res = talloc_strdup(NULL, ""); //empty string, non-NULL context
+
+ time_t raw_time = time(NULL);
+ struct tm *local_time = localtime(&raw_time);
+
+ if (!template || *template == '\0')
+ return NULL;
+
+ for (;;) {
+ char *next = strchr(template, '%');
+ if (!next)
+ break;
+ res = talloc_strndup_append(res, template, next - template);
+ template = next + 1;
+ char fmt = *template++;
+ switch (fmt) {
+ case '#':
+ case '0':
+ case 'n': {
+ int digits = '4';
+ if (fmt == '#') {
+ if (!*sequence) {
+ *frameno = 1;
+ }
+ fmt = *template++;
+ }
+ if (fmt == '0') {
+ digits = *template++;
+ if (digits < '0' || digits > '9')
+ goto error_exit;
+ fmt = *template++;
+ }
+ if (fmt != 'n')
+ goto error_exit;
+ char fmtstr[] = {'%', '0', digits, 'd', '\0'};
+ res = talloc_asprintf_append(res, fmtstr, *frameno);
+ if (*frameno < 100000 - 1) {
+ (*frameno) += 1;
+ (*sequence) += 1;
+ }
+ break;
+ }
+ case 'f':
+ case 'F': {
+ char *video_file = NULL;
+ if (mpctx->filename)
+ video_file = mp_basename(mpctx->filename);
+
+ if (!video_file)
+ video_file = "NO_FILE";
+
+ char *name = video_file;
+ if (fmt == 'F')
+ name = stripext(res, video_file);
+ append_filename(&res, name);
+ break;
+ }
+ case 'x':
+ case 'X': {
+ char *fallback = "";
+ if (fmt == 'X') {
+ if (template[0] != '{')
+ goto error_exit;
+ char *end = strchr(template, '}');
+ if (!end)
+ goto error_exit;
+ fallback = talloc_strndup(res, template + 1, end - template - 1);
+ template = end + 1;
+ }
+ if (!mpctx->filename || mp_is_url(bstr0(mpctx->filename))) {
+ res = talloc_strdup_append(res, fallback);
+ } else {
+ bstr dir = mp_dirname(mpctx->filename);
+ if (!bstr_equals0(dir, "."))
+ res = talloc_asprintf_append(res, "%.*s", BSTR_P(dir));
+ }
+ break;
+ }
+ case 'p':
+ case 'P': {
+ char *t = mp_format_time(get_playback_time(mpctx), fmt == 'P');
+ append_filename(&res, t);
+ talloc_free(t);
+ break;
+ }
+ case 'w': {
+ char tfmt = *template;
+ if (!tfmt)
+ goto error_exit;
+ template++;
+ char fmtstr[] = {'%', tfmt, '\0'};
+ char *s = mp_format_time_fmt(fmtstr, get_playback_time(mpctx));
+ if (!s)
+ goto error_exit;
+ append_filename(&res, s);
+ talloc_free(s);
+ break;
+ }
+ case 't': {
+ char tfmt = *template;
+ if (!tfmt)
+ goto error_exit;
+ template++;
+ char fmtstr[] = {'%', tfmt, '\0'};
+ char buffer[80];
+ if (strftime(buffer, sizeof(buffer), fmtstr, local_time) == 0)
+ buffer[0] = '\0';
+ append_filename(&res, buffer);
+ break;
+ }
+ case '{': {
+ char *end = strchr(template, '}');
+ if (!end)
+ goto error_exit;
+ struct bstr prop = bstr_splice(bstr0(template), 0, end - template);
+ char *tmp = talloc_asprintf(NULL, "${%.*s}", BSTR_P(prop));
+ char *s = mp_property_expand_string(mpctx, tmp);
+ talloc_free(tmp);
+ if (s)
+ append_filename(&res, s);
+ talloc_free(s);
+ template = end + 1;
+ break;
+ }
+ case '%':
+ res = talloc_strdup_append(res, "%");
+ break;
+ default:
+ goto error_exit;
+ }
+ }
+
+ res = talloc_strdup_append(res, template);
+ res = talloc_asprintf_append(res, ".%s", file_ext);
+ char *fname = mp_get_user_path(NULL, mpctx->global, res);
+ talloc_free(res);
+ return fname;
+
+error_exit:
+ talloc_free(res);
+ return NULL;
+}
+
+static char *gen_fname(struct mp_cmd_ctx *cmd, const char *file_ext)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ int sequence = 0;
+ for (;;) {
+ int prev_sequence = sequence;
+ char *fname = create_fname(ctx->mpctx,
+ ctx->mpctx->opts->screenshot_template,
+ file_ext,
+ &sequence,
+ &ctx->frameno);
+
+ if (!fname) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Invalid screenshot filename "
+ "template! Fix or remove the --screenshot-template "
+ "option.");
+ return NULL;
+ }
+
+ char *dir = ctx->mpctx->opts->screenshot_dir;
+ if (dir && dir[0]) {
+ void *t = fname;
+ dir = mp_get_user_path(t, ctx->mpctx->global, dir);
+ fname = mp_path_join(NULL, dir, fname);
+
+ mp_mkdirp(dir);
+
+ talloc_free(t);
+ }
+
+ char *full_dir = bstrto0(fname, mp_dirname(fname));
+ if (!mp_path_exists(full_dir)) {
+ mp_mkdirp(full_dir);
+ }
+
+ if (!mp_path_exists(fname))
+ return fname;
+
+ if (sequence == prev_sequence) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Can't save screenshot, file '%s' "
+ "already exists!", fname);
+ talloc_free(fname);
+ return NULL;
+ }
+
+ talloc_free(fname);
+ }
+}
+
+static void add_osd(struct MPContext *mpctx, struct mp_image *image, int mode)
+{
+ bool window = mode == MODE_FULL_WINDOW;
+ struct mp_osd_res res = window ? osd_get_vo_res(mpctx->video_out->osd) :
+ osd_res_from_image_params(&image->params);
+ if (mode == MODE_SUBTITLES || window) {
+ osd_draw_on_image(mpctx->osd, res, mpctx->video_pts,
+ OSD_DRAW_SUB_ONLY, image);
+ }
+ if (window) {
+ osd_draw_on_image(mpctx->osd, res, mpctx->video_pts,
+ OSD_DRAW_OSD_ONLY, image);
+ }
+}
+
+static struct mp_image *screenshot_get(struct MPContext *mpctx, int mode,
+ bool high_depth)
+{
+ struct mp_image *image = NULL;
+ const struct image_writer_opts *imgopts = mpctx->opts->screenshot_image_opts;
+ if (mode == MODE_SUBTITLES && osd_get_render_subs_in_filter(mpctx->osd))
+ mode = 0;
+
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ return NULL;
+
+ vo_wait_frame(mpctx->video_out); // important for each-frame mode
+
+ bool use_sw = mpctx->opts->screenshot_sw;
+ bool window = mode == MODE_FULL_WINDOW;
+ struct voctrl_screenshot ctrl = {
+ .scaled = window,
+ .subs = mode != 0,
+ .osd = window,
+ .high_bit_depth = high_depth && imgopts->high_bit_depth,
+ .native_csp = image_writer_flexible_csp(imgopts),
+ };
+ if (!use_sw)
+ vo_control(mpctx->video_out, VOCTRL_SCREENSHOT, &ctrl);
+ image = ctrl.res;
+
+ if (!use_sw && !image && window)
+ vo_control(mpctx->video_out, VOCTRL_SCREENSHOT_WIN, &image);
+
+ if (!image) {
+ use_sw = true;
+ MP_VERBOSE(mpctx->screenshot_ctx, "Falling back to software screenshot.\n");
+ image = vo_get_current_frame(mpctx->video_out);
+ }
+
+ // vo_get_current_frame() can return a hardware frame, which we have to download first.
+ if (image && image->fmt.flags & MP_IMGFLAG_HWACCEL) {
+ struct mp_image *nimage = mp_image_hw_download(image, NULL);
+ talloc_free(image);
+ if (!nimage)
+ return NULL;
+ image = nimage;
+ }
+
+ if (use_sw && image && window) {
+ if (mp_image_crop_valid(&image->params) &&
+ (mp_rect_w(image->params.crop) != image->w ||
+ mp_rect_h(image->params.crop) != image->h))
+ {
+ struct mp_image *nimage = mp_image_new_ref(image);
+ if (!nimage) {
+ MP_ERR(mpctx->screenshot_ctx, "mp_image_new_ref failed!\n");
+ return NULL;
+ }
+ mp_image_crop_rc(nimage, image->params.crop);
+ talloc_free(image);
+ image = nimage;
+ }
+ struct mp_osd_res res = osd_get_vo_res(mpctx->video_out->osd);
+ struct mp_osd_res image_res = osd_res_from_image_params(&image->params);
+ if (!osd_res_equals(res, image_res)) {
+ struct mp_image *nimage = mp_image_alloc(image->imgfmt, res.w, res.h);
+ if (!nimage) {
+ talloc_free(image);
+ return NULL;
+ }
+ struct mp_sws_context *sws = mp_sws_alloc(NULL);
+ mp_sws_scale(sws, nimage, image);
+ talloc_free(image);
+ talloc_free(sws);
+ image = nimage;
+ }
+ }
+
+ if (!image)
+ return NULL;
+
+ if (use_sw && mode != 0)
+ add_osd(mpctx, image, mode);
+ mp_image_params_guess_csp(&image->params);
+ return image;
+}
+
+struct mp_image *convert_image(struct mp_image *image, int destfmt,
+ struct mpv_global *global, struct mp_log *log)
+{
+ int d_w, d_h;
+ mp_image_params_get_dsize(&image->params, &d_w, &d_h);
+
+ struct mp_image_params p = {
+ .imgfmt = destfmt,
+ .w = d_w,
+ .h = d_h,
+ .p_w = 1,
+ .p_h = 1,
+ };
+ mp_image_params_guess_csp(&p);
+
+ if (mp_image_params_equal(&p, &image->params))
+ return mp_image_new_ref(image);
+
+ struct mp_image *dst = mp_image_alloc(p.imgfmt, p.w, p.h);
+ if (!dst) {
+ mp_err(log, "Out of memory.\n");
+ return NULL;
+ }
+ mp_image_copy_attributes(dst, image);
+
+ dst->params = p;
+
+ struct mp_sws_context *sws = mp_sws_alloc(NULL);
+ sws->log = log;
+ if (global)
+ mp_sws_enable_cmdline_opts(sws, global);
+ bool ok = mp_sws_scale(sws, dst, image) >= 0;
+ talloc_free(sws);
+
+ if (!ok) {
+ mp_err(log, "Error when converting image.\n");
+ talloc_free(dst);
+ return NULL;
+ }
+
+ return dst;
+}
+
+// mode is the same as in screenshot_get()
+static struct mp_image *screenshot_get_rgb(struct MPContext *mpctx, int mode)
+{
+ struct mp_image *mpi = screenshot_get(mpctx, mode, false);
+ if (!mpi)
+ return NULL;
+ struct mp_image *res = convert_image(mpi, IMGFMT_BGR0, mpctx->global,
+ mpctx->log);
+ talloc_free(mpi);
+ return res;
+}
+
+void cmd_screenshot_to_file(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ const char *filename = cmd->args[0].v.s;
+ int mode = cmd->args[1].v.i;
+ struct image_writer_opts opts = *mpctx->opts->screenshot_image_opts;
+
+ char *ext = mp_splitext(filename, NULL);
+ int format = image_writer_format_from_ext(ext);
+ if (format)
+ opts.format = format;
+ bool high_depth = image_writer_high_depth(&opts);
+ struct mp_image *image = screenshot_get(mpctx, mode, high_depth);
+ if (!image) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Taking screenshot failed.");
+ cmd->success = false;
+ return;
+ }
+ cmd->success = write_screenshot(cmd, image, filename, &opts);
+ talloc_free(image);
+}
+
+void cmd_screenshot(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct mpv_node *res = &cmd->result;
+ int mode = cmd->args[0].v.i & 3;
+ bool each_frame_toggle = (cmd->args[0].v.i | cmd->args[1].v.i) & 8;
+ bool each_frame_mode = cmd->args[0].v.i & 16;
+
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ if (mode == MODE_SUBTITLES && osd_get_render_subs_in_filter(mpctx->osd))
+ mode = 0;
+
+ if (!each_frame_mode) {
+ if (each_frame_toggle) {
+ if (ctx->each_frame) {
+ TA_FREEP(&ctx->each_frame);
+ return;
+ }
+ ctx->each_frame = talloc_steal(ctx, mp_cmd_clone(cmd->cmd));
+ ctx->each_frame->args[0].v.i |= 16;
+ } else {
+ TA_FREEP(&ctx->each_frame);
+ }
+ }
+
+ cmd->success = false;
+
+ struct image_writer_opts *opts = mpctx->opts->screenshot_image_opts;
+ bool high_depth = image_writer_high_depth(opts);
+
+ struct mp_image *image = screenshot_get(mpctx, mode, high_depth);
+
+ if (image) {
+ char *filename = gen_fname(cmd, image_writer_file_ext(opts));
+ if (filename) {
+ cmd->success = write_screenshot(cmd, image, filename, NULL);
+ if (cmd->success) {
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_string(res, "filename", filename);
+ }
+ }
+ talloc_free(filename);
+ } else {
+ mp_cmd_msg(cmd, MSGL_ERR, "Taking screenshot failed.");
+ }
+
+ talloc_free(image);
+}
+
+void cmd_screenshot_raw(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct mpv_node *res = &cmd->result;
+
+ struct mp_image *img = screenshot_get_rgb(mpctx, cmd->args[0].v.i);
+ if (!img) {
+ cmd->success = false;
+ return;
+ }
+
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "w", img->w);
+ node_map_add_int64(res, "h", img->h);
+ node_map_add_int64(res, "stride", img->stride[0]);
+ node_map_add_string(res, "format", "bgr0");
+ struct mpv_byte_array *ba =
+ node_map_add(res, "data", MPV_FORMAT_BYTE_ARRAY)->u.ba;
+ *ba = (struct mpv_byte_array){
+ .data = img->planes[0],
+ .size = img->stride[0] * img->h,
+ };
+ talloc_steal(ba, img);
+}
+
+static void screenshot_fin(struct mp_cmd_ctx *cmd)
+{
+ void **a = cmd->on_completion_priv;
+ struct MPContext *mpctx = a[0];
+ struct mp_waiter *waiter = a[1];
+
+ mp_waiter_wakeup(waiter, 0);
+ mp_wakeup_core(mpctx);
+}
+
+void handle_each_frame_screenshot(struct MPContext *mpctx)
+{
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ if (!ctx->each_frame)
+ return;
+
+ if (ctx->last_frame_count == mpctx->shown_vframes)
+ return;
+ ctx->last_frame_count = mpctx->shown_vframes;
+
+ struct mp_waiter wait = MP_WAITER_INITIALIZER;
+ void *a[] = {mpctx, &wait};
+ run_command(mpctx, mp_cmd_clone(ctx->each_frame), NULL, screenshot_fin, a);
+
+ // Block (in a reentrant way) until the screenshot was written. Otherwise,
+ // we could pile up screenshot requests forever.
+ while (!mp_waiter_poll(&wait))
+ mp_idle(mpctx);
+
+ mp_waiter_wait(&wait);
+}
diff --git a/player/screenshot.h b/player/screenshot.h
new file mode 100644
index 0000000..97abc79
--- /dev/null
+++ b/player/screenshot.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_SCREENSHOT_H
+#define MPLAYER_SCREENSHOT_H
+
+#include <stdbool.h>
+
+struct MPContext;
+struct mp_image;
+struct mp_log;
+struct mpv_global;
+
+// One time initialization at program start.
+void screenshot_init(struct MPContext *mpctx);
+
+// Called by the playback core on each iteration.
+void handle_each_frame_screenshot(struct MPContext *mpctx);
+
+/* Return the image converted to the given format. If the pixel aspect ratio is
+ * not 1:1, the image is scaled as well. Returns NULL on failure.
+ * If global!=NULL, use command line scaler options etc.
+ */
+struct mp_image *convert_image(struct mp_image *image, int destfmt,
+ struct mpv_global *global, struct mp_log *log);
+
+// Handlers for the user-facing commands.
+void cmd_screenshot(void *p);
+void cmd_screenshot_to_file(void *p);
+void cmd_screenshot_raw(void *p);
+
+#endif /* MPLAYER_SCREENSHOT_H */
diff --git a/player/scripting.c b/player/scripting.c
new file mode 100644
index 0000000..0b20081
--- /dev/null
+++ b/player/scripting.c
@@ -0,0 +1,462 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+#include "osdep/threads.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "options/parse_configfile.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "core.h"
+#include "client.h"
+#include "libmpv/client.h"
+#include "libmpv/render.h"
+#include "libmpv/stream_cb.h"
+
+extern const struct mp_scripting mp_scripting_lua;
+extern const struct mp_scripting mp_scripting_cplugin;
+extern const struct mp_scripting mp_scripting_js;
+extern const struct mp_scripting mp_scripting_run;
+
+static const struct mp_scripting *const scripting_backends[] = {
+#if HAVE_LUA
+ &mp_scripting_lua,
+#endif
+#if HAVE_CPLUGINS
+ &mp_scripting_cplugin,
+#endif
+#if HAVE_JAVASCRIPT
+ &mp_scripting_js,
+#endif
+ &mp_scripting_run,
+ NULL
+};
+
+static char *script_name_from_filename(void *talloc_ctx, const char *fname)
+{
+ fname = mp_basename(fname);
+ if (fname[0] == '@')
+ fname += 1;
+ char *name = talloc_strdup(talloc_ctx, fname);
+ // Drop file extension
+ char *dot = strrchr(name, '.');
+ if (dot)
+ *dot = '\0';
+ // Turn it into a safe identifier - this is used with e.g. dispatching
+ // input via: "send scriptname ..."
+ for (int n = 0; name[n]; n++) {
+ char c = name[n];
+ if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') &&
+ !(c >= '0' && c <= '9'))
+ name[n] = '_';
+ }
+ return talloc_asprintf(talloc_ctx, "%s", name);
+}
+
+static void run_script(struct mp_script_args *arg)
+{
+ char *name = talloc_asprintf(NULL, "%s/%s", arg->backend->name,
+ mpv_client_name(arg->client));
+ mp_thread_set_name(name);
+ talloc_free(name);
+
+ if (arg->backend->load(arg) < 0)
+ MP_ERR(arg, "Could not load %s script %s\n", arg->backend->name, arg->filename);
+
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+}
+
+static MP_THREAD_VOID script_thread(void *p)
+{
+ struct mp_script_args *arg = p;
+ run_script(arg);
+
+ MP_THREAD_RETURN();
+}
+
+static int64_t mp_load_script(struct MPContext *mpctx, const char *fname)
+{
+ char *ext = mp_splitext(fname, NULL);
+ if (ext && strcasecmp(ext, "disable") == 0)
+ return 0;
+
+ void *tmp = talloc_new(NULL);
+
+ const char *path = NULL;
+ char *script_name = NULL;
+ const struct mp_scripting *backend = NULL;
+
+ struct stat s;
+ if (!stat(fname, &s) && S_ISDIR(s.st_mode)) {
+ path = fname;
+ fname = NULL;
+
+ for (int n = 0; scripting_backends[n]; n++) {
+ const struct mp_scripting *b = scripting_backends[n];
+ char *filename = mp_tprintf(80, "main.%s", b->file_ext);
+ fname = mp_path_join(tmp, path, filename);
+ if (!stat(fname, &s) && S_ISREG(s.st_mode)) {
+ backend = b;
+ break;
+ }
+ talloc_free((void *)fname);
+ fname = NULL;
+ }
+
+ if (!fname) {
+ MP_ERR(mpctx, "Cannot find main.* for any supported scripting "
+ "backend in: %s\n", path);
+ talloc_free(tmp);
+ return -1;
+ }
+
+ script_name = talloc_strdup(tmp, path);
+ mp_path_strip_trailing_separator(script_name);
+ script_name = mp_basename(script_name);
+ } else {
+ for (int n = 0; scripting_backends[n]; n++) {
+ const struct mp_scripting *b = scripting_backends[n];
+ if (ext && strcasecmp(ext, b->file_ext) == 0) {
+ backend = b;
+ break;
+ }
+ }
+ script_name = script_name_from_filename(tmp, fname);
+ }
+
+ if (!backend) {
+ MP_ERR(mpctx, "Can't load unknown script: %s\n", fname);
+ talloc_free(tmp);
+ return -1;
+ }
+
+ struct mp_script_args *arg = talloc_ptrtype(NULL, arg);
+ *arg = (struct mp_script_args){
+ .mpctx = mpctx,
+ .filename = talloc_strdup(arg, fname),
+ .path = talloc_strdup(arg, path),
+ .backend = backend,
+ // Create the client before creating the thread; otherwise a race
+ // condition could happen, where MPContext is destroyed while the
+ // thread tries to create the client.
+ .client = mp_new_client(mpctx->clients, script_name),
+ };
+
+ talloc_free(tmp);
+ fname = NULL; // might have been freed so don't touch anymore
+
+ if (!arg->client) {
+ MP_ERR(mpctx, "Failed to create client for script: %s\n", arg->filename);
+ talloc_free(arg);
+ return -1;
+ }
+
+ mp_client_set_weak(arg->client);
+ arg->log = mp_client_get_log(arg->client);
+ int64_t id = mpv_client_id(arg->client);
+
+ MP_DBG(arg, "Loading %s script %s...\n", backend->name, arg->filename);
+
+ if (backend->no_thread) {
+ run_script(arg);
+ } else {
+ mp_thread thread;
+ if (mp_thread_create(&thread, script_thread, arg)) {
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+ return -1;
+ }
+ mp_thread_detach(thread);
+ }
+
+ return id;
+}
+
+int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname)
+{
+ char *path = mp_get_user_path(NULL, mpctx->global, fname);
+ int64_t ret = mp_load_script(mpctx, path);
+ talloc_free(path);
+ return ret;
+}
+
+static int compare_filename(const void *pa, const void *pb)
+{
+ char *a = (char *)pa;
+ char *b = (char *)pb;
+ return strcmp(a, b);
+}
+
+static char **list_script_files(void *talloc_ctx, char *path)
+{
+ char **files = NULL;
+ int count = 0;
+ DIR *dp = opendir(path);
+ if (!dp)
+ return NULL;
+ struct dirent *ep;
+ while ((ep = readdir(dp))) {
+ if (ep->d_name[0] != '.') {
+ char *fname = mp_path_join(talloc_ctx, path, ep->d_name);
+ struct stat s;
+ if (!stat(fname, &s) && (S_ISREG(s.st_mode) || S_ISDIR(s.st_mode)))
+ MP_TARRAY_APPEND(talloc_ctx, files, count, fname);
+ }
+ }
+ closedir(dp);
+ if (files)
+ qsort(files, count, sizeof(char *), compare_filename);
+ MP_TARRAY_APPEND(talloc_ctx, files, count, NULL);
+ return files;
+}
+
+static void load_builtin_script(struct MPContext *mpctx, int slot, bool enable,
+ const char *fname)
+{
+ assert(slot < MP_ARRAY_SIZE(mpctx->builtin_script_ids));
+ int64_t *pid = &mpctx->builtin_script_ids[slot];
+ if (*pid > 0 && !mp_client_id_exists(mpctx, *pid))
+ *pid = 0; // died
+ if ((*pid > 0) != enable) {
+ if (enable) {
+ *pid = mp_load_script(mpctx, fname);
+ } else {
+ char *name = mp_tprintf(22, "@%"PRIi64, *pid);
+ mp_client_send_event(mpctx, name, 0, MPV_EVENT_SHUTDOWN, NULL);
+ }
+ }
+}
+
+void mp_load_builtin_scripts(struct MPContext *mpctx)
+{
+ load_builtin_script(mpctx, 0, mpctx->opts->lua_load_osc, "@osc.lua");
+ load_builtin_script(mpctx, 1, mpctx->opts->lua_load_ytdl, "@ytdl_hook.lua");
+ load_builtin_script(mpctx, 2, mpctx->opts->lua_load_stats, "@stats.lua");
+ load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua");
+ load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles,
+ "@auto_profiles.lua");
+}
+
+bool mp_load_scripts(struct MPContext *mpctx)
+{
+ bool ok = true;
+
+ // Load scripts from options
+ char **files = mpctx->opts->script_files;
+ for (int n = 0; files && files[n]; n++) {
+ if (files[n][0])
+ ok &= mp_load_user_script(mpctx, files[n]) >= 0;
+ }
+ if (!mpctx->opts->auto_load_scripts)
+ return ok;
+
+ // Load all scripts
+ void *tmp = talloc_new(NULL);
+ char **scriptsdir = mp_find_all_config_files(tmp, mpctx->global, "scripts");
+ for (int i = 0; scriptsdir && scriptsdir[i]; i++) {
+ files = list_script_files(tmp, scriptsdir[i]);
+ for (int n = 0; files && files[n]; n++)
+ ok &= mp_load_script(mpctx, files[n]) >= 0;
+ }
+ talloc_free(tmp);
+
+ return ok;
+}
+
+#if HAVE_CPLUGINS
+
+#if !HAVE_WIN32
+#include <dlfcn.h>
+#endif
+
+#define MPV_DLOPEN_FN "mpv_open_cplugin"
+typedef int (*mpv_open_cplugin)(mpv_handle *handle);
+
+static void init_sym_table(struct mp_script_args *args, void *lib) {
+#define INIT_SYM(name) \
+ { \
+ void **sym = (void **)dlsym(lib, "pfn_" #name); \
+ if (sym) { \
+ if (*sym && *sym != &name) \
+ MP_ERR(args, "Overriding already set function " #name "\n"); \
+ *sym = &name; \
+ } \
+ }
+
+ INIT_SYM(mpv_client_api_version);
+ INIT_SYM(mpv_error_string);
+ INIT_SYM(mpv_free);
+ INIT_SYM(mpv_client_name);
+ INIT_SYM(mpv_client_id);
+ INIT_SYM(mpv_create);
+ INIT_SYM(mpv_initialize);
+ INIT_SYM(mpv_destroy);
+ INIT_SYM(mpv_terminate_destroy);
+ INIT_SYM(mpv_create_client);
+ INIT_SYM(mpv_create_weak_client);
+ INIT_SYM(mpv_load_config_file);
+ INIT_SYM(mpv_get_time_us);
+ INIT_SYM(mpv_free_node_contents);
+ INIT_SYM(mpv_set_option);
+ INIT_SYM(mpv_set_option_string);
+ INIT_SYM(mpv_command);
+ INIT_SYM(mpv_command_node);
+ INIT_SYM(mpv_command_ret);
+ INIT_SYM(mpv_command_string);
+ INIT_SYM(mpv_command_async);
+ INIT_SYM(mpv_command_node_async);
+ INIT_SYM(mpv_abort_async_command);
+ INIT_SYM(mpv_set_property);
+ INIT_SYM(mpv_set_property_string);
+ INIT_SYM(mpv_del_property);
+ INIT_SYM(mpv_set_property_async);
+ INIT_SYM(mpv_get_property);
+ INIT_SYM(mpv_get_property_string);
+ INIT_SYM(mpv_get_property_osd_string);
+ INIT_SYM(mpv_get_property_async);
+ INIT_SYM(mpv_observe_property);
+ INIT_SYM(mpv_unobserve_property);
+ INIT_SYM(mpv_event_name);
+ INIT_SYM(mpv_event_to_node);
+ INIT_SYM(mpv_request_event);
+ INIT_SYM(mpv_request_log_messages);
+ INIT_SYM(mpv_wait_event);
+ INIT_SYM(mpv_wakeup);
+ INIT_SYM(mpv_set_wakeup_callback);
+ INIT_SYM(mpv_wait_async_requests);
+ INIT_SYM(mpv_hook_add);
+ INIT_SYM(mpv_hook_continue);
+ INIT_SYM(mpv_get_wakeup_pipe);
+
+ INIT_SYM(mpv_render_context_create);
+ INIT_SYM(mpv_render_context_set_parameter);
+ INIT_SYM(mpv_render_context_get_info);
+ INIT_SYM(mpv_render_context_set_update_callback);
+ INIT_SYM(mpv_render_context_update);
+ INIT_SYM(mpv_render_context_render);
+ INIT_SYM(mpv_render_context_report_swap);
+ INIT_SYM(mpv_render_context_free);
+
+ INIT_SYM(mpv_stream_cb_add_ro);
+
+#undef INIT_SYM
+}
+
+static int load_cplugin(struct mp_script_args *args)
+{
+ void *lib = dlopen(args->filename, RTLD_NOW | RTLD_LOCAL);
+ if (!lib)
+ goto error;
+ // Note: once loaded, we never unload, as unloading the libraries linked to
+ // the plugin can cause random serious problems.
+ mpv_open_cplugin sym = (mpv_open_cplugin)dlsym(lib, MPV_DLOPEN_FN);
+ if (!sym)
+ goto error;
+
+ init_sym_table(args, lib);
+
+ return sym(args->client) ? -1 : 0;
+error: ;
+ char *err = dlerror();
+ if (err)
+ MP_ERR(args, "C plugin error: '%s'\n", err);
+ return -1;
+}
+
+const struct mp_scripting mp_scripting_cplugin = {
+ .name = "cplugin",
+ #if HAVE_WIN32
+ .file_ext = "dll",
+ #else
+ .file_ext = "so",
+ #endif
+ .load = load_cplugin,
+};
+
+#endif
+
+static int load_run(struct mp_script_args *args)
+{
+ // The arg->client object might die and with it args->log, so duplicate it.
+ args->log = mp_log_new(args, args->log, NULL);
+
+ int fds[2];
+ if (!mp_ipc_start_anon_client(args->mpctx->ipc_ctx, args->client, fds))
+ return -1;
+ args->client = NULL; // ownership lost
+
+ char *fdopt = fds[1] >= 0 ? mp_tprintf(80, "--mpv-ipc-fd=%d:%d", fds[0], fds[1])
+ : mp_tprintf(80, "--mpv-ipc-fd=%d", fds[0]);
+
+ struct mp_subprocess_opts opts = {
+ .exe = (char *)args->filename,
+ .args = (char *[]){(char *)args->filename, fdopt, NULL},
+ .fds = {
+ // Keep terminal stuff
+ {.fd = 0, .src_fd = 0,},
+ {.fd = 1, .src_fd = 1,},
+ {.fd = 2, .src_fd = 2,},
+ // Just hope these don't step over each other (e.g. fds[1] could be
+ // below 4, if the std FDs are missing).
+ {.fd = fds[0], .src_fd = fds[0], },
+ {.fd = fds[1], .src_fd = fds[1], },
+ },
+ .num_fds = fds[1] >= 0 ? 5 : 4,
+ .detach = true,
+ };
+ struct mp_subprocess_result res;
+ mp_subprocess2(&opts, &res);
+
+ // Closing these will (probably) make the client exit, if it really died.
+ // They _should_ be CLOEXEC, but are not, because
+ // posix_spawn_file_actions_adddup2() may not clear the CLOEXEC flag
+ // properly if by coincidence fd==src_fd.
+ close(fds[0]);
+ if (fds[1] >= 0)
+ close(fds[1]);
+
+ if (res.error < 0) {
+ MP_ERR(args, "Starting '%s' failed: %s\n", args->filename,
+ mp_subprocess_err_str(res.error));
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct mp_scripting mp_scripting_run = {
+ .name = "ipc",
+ .file_ext = "run",
+ .no_thread = true,
+ .load = load_run,
+};
diff --git a/player/sub.c b/player/sub.c
new file mode 100644
index 0000000..f3e42fe
--- /dev/null
+++ b/player/sub.c
@@ -0,0 +1,214 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "common/global.h"
+
+#include "stream/stream.h"
+#include "sub/dec_sub.h"
+#include "demux/demux.h"
+#include "video/mp_image.h"
+
+#include "core.h"
+
+// 0: primary sub, 1: secondary sub, -1: not selected
+static int get_order(struct MPContext *mpctx, struct track *track)
+{
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++) {
+ if (mpctx->current_track[n][STREAM_SUB] == track)
+ return n;
+ }
+ return -1;
+}
+
+static void reset_subtitles(struct MPContext *mpctx, struct track *track)
+{
+ if (track->d_sub) {
+ sub_reset(track->d_sub);
+ sub_set_play_dir(track->d_sub, mpctx->play_dir);
+ }
+ term_osd_set_subs(mpctx, NULL);
+}
+
+void reset_subtitle_state(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ reset_subtitles(mpctx, mpctx->tracks[n]);
+ term_osd_set_subs(mpctx, NULL);
+}
+
+void uninit_sub(struct MPContext *mpctx, struct track *track)
+{
+ if (track && track->d_sub) {
+ reset_subtitles(mpctx, track);
+ sub_select(track->d_sub, false);
+ int order = get_order(mpctx, track);
+ osd_set_sub(mpctx->osd, order, NULL);
+ sub_destroy(track->d_sub);
+ track->d_sub = NULL;
+ }
+}
+
+void uninit_sub_all(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ uninit_sub(mpctx, mpctx->tracks[n]);
+}
+
+static bool update_subtitle(struct MPContext *mpctx, double video_pts,
+ struct track *track)
+{
+ struct dec_sub *dec_sub = track ? track->d_sub : NULL;
+
+ if (!dec_sub || video_pts == MP_NOPTS_VALUE)
+ return true;
+
+ if (mpctx->vo_chain) {
+ struct mp_image_params params = mpctx->vo_chain->filter->input_params;
+ if (params.imgfmt)
+ sub_control(dec_sub, SD_CTRL_SET_VIDEO_PARAMS, &params);
+ }
+
+ if (track->demuxer->fully_read && sub_can_preload(dec_sub)) {
+ // Assume fully_read implies no interleaved audio/video streams.
+ // (Reading packets will change the demuxer position.)
+ demux_seek(track->demuxer, 0, 0);
+ sub_preload(dec_sub);
+ }
+
+ if (!sub_read_packets(dec_sub, video_pts, mpctx->paused))
+ return false;
+
+ // Handle displaying subtitles on terminal; never done for secondary subs
+ if (mpctx->current_track[0][STREAM_SUB] == track && !mpctx->video_out) {
+ char *text = sub_get_text(dec_sub, video_pts, SD_TEXT_TYPE_PLAIN);
+ term_osd_set_subs(mpctx, text);
+ talloc_free(text);
+ }
+
+ // Handle displaying subtitles on VO with no video being played. This is
+ // quite different, because normally subtitles are redrawn on new video
+ // frames, using the video frames' timestamps.
+ if (mpctx->video_out && mpctx->video_status == STATUS_EOF &&
+ (mpctx->opts->subs_rend->sub_past_video_end ||
+ !mpctx->current_track[0][STREAM_VIDEO] ||
+ mpctx->current_track[0][STREAM_VIDEO]->image)) {
+ if (osd_get_force_video_pts(mpctx->osd) != video_pts) {
+ osd_set_force_video_pts(mpctx->osd, video_pts);
+ osd_query_and_reset_want_redraw(mpctx->osd);
+ vo_redraw(mpctx->video_out);
+ // Force an arbitrary minimum FPS
+ mp_set_timeout(mpctx, 0.1);
+ }
+ }
+
+ return true;
+}
+
+// Return true if the subtitles for the given PTS are ready; false if the player
+// should wait for new demuxer data, and then should retry.
+bool update_subtitles(struct MPContext *mpctx, double video_pts)
+{
+ bool ok = true;
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++)
+ ok &= update_subtitle(mpctx, video_pts, mpctx->current_track[n][STREAM_SUB]);
+ return ok;
+}
+
+static struct attachment_list *get_all_attachments(struct MPContext *mpctx)
+{
+ struct attachment_list *list = talloc_zero(NULL, struct attachment_list);
+ struct demuxer *prev_demuxer = NULL;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ if (!t->demuxer || prev_demuxer == t->demuxer)
+ continue;
+ prev_demuxer = t->demuxer;
+ for (int i = 0; i < t->demuxer->num_attachments; i++) {
+ struct demux_attachment *att = &t->demuxer->attachments[i];
+ struct demux_attachment copy = {
+ .name = talloc_strdup(list, att->name),
+ .type = talloc_strdup(list, att->type),
+ .data = talloc_memdup(list, att->data, att->data_size),
+ .data_size = att->data_size,
+ };
+ MP_TARRAY_APPEND(list, list->entries, list->num_entries, copy);
+ }
+ }
+ return list;
+}
+
+static bool init_subdec(struct MPContext *mpctx, struct track *track)
+{
+ assert(!track->d_sub);
+
+ if (!track->demuxer || !track->stream)
+ return false;
+
+ track->d_sub = sub_create(mpctx->global, track,
+ get_all_attachments(mpctx),
+ get_order(mpctx, track));
+ if (!track->d_sub)
+ return false;
+
+ struct track *vtrack = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_codec_params *v_c =
+ vtrack && vtrack->stream ? vtrack->stream->codec : NULL;
+ double fps = v_c ? v_c->fps : 25;
+ sub_control(track->d_sub, SD_CTRL_SET_VIDEO_DEF_FPS, &fps);
+
+ return true;
+}
+
+void reinit_sub(struct MPContext *mpctx, struct track *track)
+{
+ if (!track || !track->stream || track->stream->type != STREAM_SUB)
+ return;
+
+ assert(!track->d_sub);
+
+ if (!init_subdec(mpctx, track)) {
+ error_on_track(mpctx, track);
+ return;
+ }
+
+ sub_select(track->d_sub, true);
+ int order = get_order(mpctx, track);
+ osd_set_sub(mpctx->osd, order, track->d_sub);
+ sub_control(track->d_sub, SD_CTRL_SET_TOP, &order);
+
+ // When paused we have to wait for packets to be available.
+ // So just retry until we get a packet in this case.
+ if (mpctx->playback_initialized)
+ while (!update_subtitles(mpctx, mpctx->playback_pts) && mpctx->paused);
+}
+
+void reinit_sub_all(struct MPContext *mpctx)
+{
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++)
+ reinit_sub(mpctx, mpctx->current_track[n][STREAM_SUB]);
+}
diff --git a/player/video.c b/player/video.c
new file mode 100644
index 0000000..48a3165
--- /dev/null
+++ b/player/video.c
@@ -0,0 +1,1324 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "common/common.h"
+#include "common/encode.h"
+#include "options/m_property.h"
+#include "osdep/timer.h"
+
+#include "audio/out/ao.h"
+#include "audio/format.h"
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+#include "video/hwdec.h"
+#include "filters/f_decoder_wrapper.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+#include "screenshot.h"
+
+enum {
+ // update_video() - code also uses: <0 error, 0 eof, >0 progress
+ VD_ERROR = -1,
+ VD_EOF = 0, // end of file - no new output
+ VD_PROGRESS = 1, // progress, but no output; repeat call with no waiting
+ VD_NEW_FRAME = 2, // the call produced a new frame
+ VD_WAIT = 3, // no EOF, but no output; wait until wakeup
+};
+
+static const char av_desync_help_text[] =
+"\n"
+"Audio/Video desynchronisation detected! Possible reasons include too slow\n"
+"hardware, temporary CPU spikes, broken drivers, and broken files. Audio\n"
+"position will not match to the video (see A-V status field).\n"
+"Consider trying `--profile=fast` and/or `--hwdec=auto-safe` as they may help.\n"
+"\n";
+
+static bool recreate_video_filters(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ assert(vo_c);
+
+ return mp_output_chain_update_filters(vo_c->filter, opts->vf_settings);
+}
+
+int reinit_video_filters(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c)
+ return 0;
+
+ if (!recreate_video_filters(mpctx))
+ return -1;
+
+ mp_force_video_refresh(mpctx);
+
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+
+ return 0;
+}
+
+static void vo_chain_reset_state(struct vo_chain *vo_c)
+{
+ vo_seek_reset(vo_c->vo);
+ vo_c->underrun = false;
+ vo_c->underrun_signaled = false;
+}
+
+void reset_video_state(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain) {
+ vo_chain_reset_state(mpctx->vo_chain);
+ struct track *t = mpctx->vo_chain->track;
+ if (t && t->dec)
+ mp_decoder_wrapper_set_play_dir(t->dec, mpctx->play_dir);
+ }
+
+ for (int n = 0; n < mpctx->num_next_frames; n++)
+ mp_image_unrefp(&mpctx->next_frames[n]);
+ mpctx->num_next_frames = 0;
+ mp_image_unrefp(&mpctx->saved_frame);
+
+ mpctx->delay = 0;
+ mpctx->time_frame = 0;
+ mpctx->video_pts = MP_NOPTS_VALUE;
+ mpctx->last_frame_duration = 0;
+ mpctx->num_past_frames = 0;
+ mpctx->total_avsync_change = 0;
+ mpctx->last_av_difference = 0;
+ mpctx->mistimed_frames_total = 0;
+ mpctx->drop_message_shown = 0;
+ mpctx->display_sync_drift_dir = 0;
+ mpctx->display_sync_error = 0;
+
+ mpctx->video_status = mpctx->vo_chain ? STATUS_SYNCING : STATUS_EOF;
+}
+
+void uninit_video_out(struct MPContext *mpctx)
+{
+ uninit_video_chain(mpctx);
+ if (mpctx->video_out) {
+ vo_destroy(mpctx->video_out);
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+ mpctx->video_out = NULL;
+}
+
+static void vo_chain_uninit(struct vo_chain *vo_c)
+{
+ struct track *track = vo_c->track;
+ if (track) {
+ assert(track->vo_c == vo_c);
+ track->vo_c = NULL;
+ if (vo_c->dec_src)
+ assert(track->dec->f->pins[0] == vo_c->dec_src);
+ talloc_free(track->dec->f);
+ track->dec = NULL;
+ }
+
+ if (vo_c->filter_src)
+ mp_pin_disconnect(vo_c->filter_src);
+
+ talloc_free(vo_c->filter->f);
+ talloc_free(vo_c);
+ // this does not free the VO
+}
+
+void uninit_video_chain(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain) {
+ reset_video_state(mpctx);
+ vo_chain_uninit(mpctx->vo_chain);
+ mpctx->vo_chain = NULL;
+
+ mpctx->video_status = STATUS_EOF;
+
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+}
+
+int init_video_decoder(struct MPContext *mpctx, struct track *track)
+{
+ assert(!track->dec);
+ if (!track->stream)
+ goto err_out;
+
+ struct mp_filter *parent = mpctx->filter_root;
+
+ // If possible, set this as parent so the decoder gets the hwdec and DR
+ // interfaces.
+ // Note: We rely on being able to get rid of all references to the VO by
+ // destroying the VO chain. Thus, decoders not linked to vo_chain
+ // must not use the hwdec context.
+ if (track->vo_c)
+ parent = track->vo_c->filter->f;
+
+ track->dec = mp_decoder_wrapper_create(parent, track->stream);
+ if (!track->dec)
+ goto err_out;
+
+ if (!mp_decoder_wrapper_reinit(track->dec))
+ goto err_out;
+
+ return 1;
+
+err_out:
+ if (track->sink)
+ mp_pin_disconnect(track->sink);
+ track->sink = NULL;
+ error_on_track(mpctx, track);
+ return 0;
+}
+
+void reinit_video_chain(struct MPContext *mpctx)
+{
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ if (!track || !track->stream) {
+ error_on_track(mpctx, track);
+ return;
+ }
+ reinit_video_chain_src(mpctx, track);
+}
+
+static void filter_update_subtitles(void *ctx, double pts)
+{
+ struct MPContext *mpctx = ctx;
+
+ if (osd_get_render_subs_in_filter(mpctx->osd))
+ update_subtitles(mpctx, pts);
+}
+
+// (track=NULL creates a blank chain, used for lavfi-complex)
+void reinit_video_chain_src(struct MPContext *mpctx, struct track *track)
+{
+ assert(!mpctx->vo_chain);
+
+ if (!mpctx->video_out) {
+ struct vo_extra ex = {
+ .input_ctx = mpctx->input,
+ .osd = mpctx->osd,
+ .encode_lavc_ctx = mpctx->encode_lavc_ctx,
+ .wakeup_cb = mp_wakeup_core_cb,
+ .wakeup_ctx = mpctx,
+ };
+ mpctx->video_out = init_best_video_out(mpctx->global, &ex);
+ if (!mpctx->video_out) {
+ MP_FATAL(mpctx, "Error opening/initializing "
+ "the selected video_out (--vo) device.\n");
+ mpctx->error_playing = MPV_ERROR_VO_INIT_FAILED;
+ goto err_out;
+ }
+ mpctx->mouse_cursor_visible = true;
+ }
+
+ update_window_title(mpctx, true);
+
+ struct vo_chain *vo_c = talloc_zero(NULL, struct vo_chain);
+ mpctx->vo_chain = vo_c;
+ vo_c->log = mpctx->log;
+ vo_c->vo = mpctx->video_out;
+ vo_c->filter =
+ mp_output_chain_create(mpctx->filter_root, MP_OUTPUT_CHAIN_VIDEO);
+ mp_output_chain_set_vo(vo_c->filter, vo_c->vo);
+ vo_c->filter->update_subtitles = filter_update_subtitles;
+ vo_c->filter->update_subtitles_ctx = mpctx;
+
+ if (track) {
+ vo_c->track = track;
+ track->vo_c = vo_c;
+ if (!init_video_decoder(mpctx, track))
+ goto err_out;
+
+ vo_c->dec_src = track->dec->f->pins[0];
+ vo_c->filter->container_fps =
+ mp_decoder_wrapper_get_container_fps(track->dec);
+ vo_c->is_coverart = !!track->attached_picture;
+ vo_c->is_sparse = track->stream->still_image || vo_c->is_coverart;
+
+ if (vo_c->is_coverart)
+ mp_decoder_wrapper_set_coverart_flag(track->dec, true);
+
+ track->vo_c = vo_c;
+ vo_c->track = track;
+
+ mp_pin_connect(vo_c->filter->f->pins[0], vo_c->dec_src);
+ }
+
+ if (!recreate_video_filters(mpctx))
+ goto err_out;
+
+ update_content_type(mpctx, track);
+ update_screensaver_state(mpctx);
+
+ vo_set_paused(vo_c->vo, get_internal_paused(mpctx));
+
+ reset_video_state(mpctx);
+ term_osd_set_subs(mpctx, NULL);
+
+ return;
+
+err_out:
+ uninit_video_chain(mpctx);
+ error_on_track(mpctx, track);
+ handle_force_window(mpctx, true);
+}
+
+// Try to refresh the video by doing a precise seek to the currently displayed
+// frame. This can go wrong in all sorts of ways, so use sparingly.
+void mp_force_video_refresh(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c)
+ return;
+
+ // If not paused, the next frame should come soon enough.
+ if (opts->pause || mpctx->time_frame >= 0.5 ||
+ mpctx->video_status == STATUS_EOF)
+ {
+ issue_refresh_seek(mpctx, MPSEEK_VERY_EXACT);
+ }
+}
+
+static void check_framedrop(struct MPContext *mpctx, struct vo_chain *vo_c)
+{
+ struct MPOpts *opts = mpctx->opts;
+ // check for frame-drop:
+ if (mpctx->video_status == STATUS_PLAYING && !mpctx->paused &&
+ mpctx->audio_status == STATUS_PLAYING && !ao_untimed(mpctx->ao) &&
+ vo_c->track && vo_c->track->dec && (opts->frame_dropping & 2))
+ {
+ float fps = vo_c->filter->container_fps;
+ // it's a crappy heuristic; avoid getting upset by incorrect fps
+ if (fps <= 20 || fps >= 500)
+ return;
+ double frame_time = 1.0 / fps;
+ // try to drop as many frames as we appear to be behind
+ mp_decoder_wrapper_set_frame_drops(vo_c->track->dec,
+ MPCLAMP((mpctx->last_av_difference - 0.010) / frame_time, 0, 100));
+ }
+}
+
+/* Modify video timing to match the audio timeline. There are two main
+ * reasons this is needed. First, video and audio can start from different
+ * positions at beginning of file or after a seek (MPlayer starts both
+ * immediately even if they have different pts). Second, the file can have
+ * audio timestamps that are inconsistent with the duration of the audio
+ * packets, for example two consecutive timestamp values differing by
+ * one second but only a packet with enough samples for half a second
+ * of playback between them.
+ */
+static void adjust_sync(struct MPContext *mpctx, double v_pts, double frame_time)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->audio_status == STATUS_EOF)
+ return;
+
+ mpctx->delay -= frame_time;
+ double a_pts = written_audio_pts(mpctx) + opts->audio_delay - mpctx->delay;
+ double av_delay = a_pts - v_pts;
+
+ double change = av_delay * 0.1;
+ double factor = fabs(av_delay) < 0.3 ? 0.1 : 0.4;
+ double max_change = opts->default_max_pts_correction >= 0 ?
+ opts->default_max_pts_correction : frame_time * factor;
+ if (change < -max_change)
+ change = -max_change;
+ else if (change > max_change)
+ change = max_change;
+ mpctx->delay += change;
+ mpctx->total_avsync_change += change;
+
+ if (mpctx->display_sync_active)
+ mpctx->total_avsync_change = 0;
+}
+
+// Make the frame at position 0 "known" to the playback logic. This must happen
+// only once for each frame, so this function has to be called carefully.
+// Generally, if position 0 gets a new frame, this must be called.
+static void handle_new_frame(struct MPContext *mpctx)
+{
+ assert(mpctx->num_next_frames >= 1);
+
+ double frame_time = 0;
+ double pts = mpctx->next_frames[0]->pts;
+ bool is_sparse = mpctx->vo_chain && mpctx->vo_chain->is_sparse;
+
+ if (mpctx->video_pts != MP_NOPTS_VALUE) {
+ frame_time = pts - mpctx->video_pts;
+ double tolerance = mpctx->demuxer->ts_resets_possible &&
+ !is_sparse ? 5 : 1e4;
+ if (frame_time <= 0 || frame_time >= tolerance) {
+ // Assume a discontinuity.
+ MP_WARN(mpctx, "Invalid video timestamp: %f -> %f\n",
+ mpctx->video_pts, pts);
+ frame_time = 0;
+ }
+ }
+ mpctx->time_frame += frame_time / mpctx->video_speed;
+ if (frame_time)
+ adjust_sync(mpctx, pts, frame_time);
+ MP_TRACE(mpctx, "frametime=%5.3f\n", frame_time);
+}
+
+// Remove the first frame in mpctx->next_frames
+static void shift_frames(struct MPContext *mpctx)
+{
+ if (mpctx->num_next_frames < 1)
+ return;
+ talloc_free(mpctx->next_frames[0]);
+ for (int n = 0; n < mpctx->num_next_frames - 1; n++)
+ mpctx->next_frames[n] = mpctx->next_frames[n + 1];
+ mpctx->num_next_frames -= 1;
+}
+
+static bool use_video_lookahead(struct MPContext *mpctx)
+{
+ return mpctx->video_out &&
+ !(mpctx->video_out->driver->caps & VO_CAP_NORETAIN) &&
+ !(mpctx->opts->untimed || mpctx->video_out->driver->untimed) &&
+ !mpctx->opts->video_latency_hacks;
+}
+
+static int get_req_frames(struct MPContext *mpctx, bool eof)
+{
+ // On EOF, drain all frames.
+ if (eof)
+ return 1;
+
+ if (!use_video_lookahead(mpctx))
+ return 1;
+
+ if (mpctx->vo_chain && mpctx->vo_chain->is_sparse)
+ return 1;
+
+ // Normally require at least 2 frames, so we can compute a frame duration.
+ int min = 2;
+
+ // On the first frame, output a new frame as quickly as possible.
+ if (mpctx->video_pts == MP_NOPTS_VALUE)
+ return min;
+
+ int req = vo_get_num_req_frames(mpctx->video_out);
+ return MPCLAMP(req, min, MP_ARRAY_SIZE(mpctx->next_frames) - 1);
+}
+
+// Whether it's fine to call add_new_frame() now.
+static bool needs_new_frame(struct MPContext *mpctx)
+{
+ return mpctx->num_next_frames < get_req_frames(mpctx, false);
+}
+
+// Queue a frame to mpctx->next_frames[]. Call only if needs_new_frame() signals ok.
+static void add_new_frame(struct MPContext *mpctx, struct mp_image *frame)
+{
+ assert(mpctx->num_next_frames < MP_ARRAY_SIZE(mpctx->next_frames));
+ assert(frame);
+ mpctx->next_frames[mpctx->num_next_frames++] = frame;
+ if (mpctx->num_next_frames == 1)
+ handle_new_frame(mpctx);
+}
+
+// Enough video filtered already to push one frame to the VO?
+// Set eof to true if no new frames are to be expected.
+static bool have_new_frame(struct MPContext *mpctx, bool eof)
+{
+ return mpctx->num_next_frames >= get_req_frames(mpctx, eof);
+}
+
+// Fill mpctx->next_frames[] with a newly filtered or decoded image.
+// logical_eof: is set to true if there is EOF after currently queued frames
+// returns VD_* code
+static int video_output_image(struct MPContext *mpctx, bool *logical_eof)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ bool hrseek = false;
+ double hrseek_pts = mpctx->hrseek_pts;
+ double tolerance = mpctx->hrseek_backstep ? 0 : .005;
+ if (mpctx->video_status == STATUS_SYNCING) {
+ hrseek = mpctx->hrseek_active;
+ // playback_pts is normally only set when audio and video have started
+ // playing normally. If video is in syncing mode, then this must mean
+ // video was just enabled via track switching - skip to current time.
+ if (!hrseek && mpctx->playback_pts != MP_NOPTS_VALUE) {
+ hrseek = true;
+ hrseek_pts = mpctx->playback_pts;
+ }
+ }
+
+ if (vo_c->is_coverart) {
+ *logical_eof = true;
+ if (vo_has_frame(mpctx->video_out))
+ return VD_EOF;
+ hrseek = false;
+ }
+
+ if (have_new_frame(mpctx, false))
+ return VD_NEW_FRAME;
+
+ // Get a new frame if we need one.
+ int r = VD_PROGRESS;
+ if (needs_new_frame(mpctx)) {
+ // Filter a new frame.
+ struct mp_image *img = NULL;
+ struct mp_frame frame = mp_pin_out_read(vo_c->filter->f->pins[1]);
+ if (frame.type == MP_FRAME_NONE) {
+ r = vo_c->filter->got_output_eof ? VD_EOF : VD_WAIT;
+ } else if (frame.type == MP_FRAME_EOF) {
+ r = VD_EOF;
+ } else if (frame.type == MP_FRAME_VIDEO) {
+ img = frame.data;
+ } else {
+ MP_ERR(mpctx, "unexpected frame type %s\n",
+ mp_frame_type_str(frame.type));
+ mp_frame_unref(&frame);
+ return VD_ERROR;
+ }
+ if (img) {
+ double endpts = get_play_end_pts(mpctx);
+ if (endpts != MP_NOPTS_VALUE)
+ endpts *= mpctx->play_dir;
+ if ((endpts != MP_NOPTS_VALUE && img->pts >= endpts) ||
+ mpctx->max_frames == 0)
+ {
+ mp_pin_out_unread(vo_c->filter->f->pins[1], frame);
+ img = NULL;
+ r = VD_EOF;
+ } else if (hrseek && (img->pts < hrseek_pts - tolerance ||
+ mpctx->hrseek_lastframe))
+ {
+ /* just skip - but save in case it was the last frame */
+ mp_image_setrefp(&mpctx->saved_frame, img);
+ } else {
+ if (hrseek && mpctx->hrseek_backstep) {
+ if (mpctx->saved_frame) {
+ add_new_frame(mpctx, mpctx->saved_frame);
+ mpctx->saved_frame = NULL;
+ } else {
+ MP_WARN(mpctx, "Backstep failed.\n");
+ }
+ mpctx->hrseek_backstep = false;
+ }
+ mp_image_unrefp(&mpctx->saved_frame);
+ add_new_frame(mpctx, img);
+ img = NULL;
+ }
+ talloc_free(img);
+ }
+ }
+
+ if (!hrseek)
+ mp_image_unrefp(&mpctx->saved_frame);
+
+ if (r == VD_EOF) {
+ // If hr-seek went past EOF, use the last frame.
+ if (mpctx->saved_frame)
+ add_new_frame(mpctx, mpctx->saved_frame);
+ mpctx->saved_frame = NULL;
+ *logical_eof = true;
+ }
+
+ return have_new_frame(mpctx, r <= 0) ? VD_NEW_FRAME : r;
+}
+
+static bool check_for_hwdec_fallback(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c->filter->failed_output_conversion || !vo_c->track || !vo_c->track->dec)
+ return false;
+
+ if (mp_decoder_wrapper_control(vo_c->track->dec,
+ VDCTRL_FORCE_HWDEC_FALLBACK, NULL) != CONTROL_OK)
+ return false;
+
+ mp_output_chain_reset_harder(vo_c->filter);
+ return true;
+}
+
+static bool check_for_forced_eof(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c->track || !vo_c->track->dec)
+ return false;
+
+ struct mp_decoder_wrapper *dec = vo_c->track->dec;
+ bool forced_eof = false;
+
+ mp_decoder_wrapper_control(dec, VDCTRL_CHECK_FORCED_EOF, &forced_eof);
+ return forced_eof;
+}
+
+/* Update avsync before a new video frame is displayed. Actually, this can be
+ * called arbitrarily often before the actual display.
+ * This adjusts the time of the next video frame */
+static void update_avsync_before_frame(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo *vo = mpctx->video_out;
+
+ if (mpctx->video_status < STATUS_READY) {
+ mpctx->time_frame = 0;
+ } else if (mpctx->display_sync_active || vo->opts->video_sync == VS_NONE) {
+ // don't touch the timing
+ } else if (mpctx->audio_status == STATUS_PLAYING &&
+ mpctx->video_status == STATUS_PLAYING &&
+ !ao_untimed(mpctx->ao))
+ {
+ double buffered_audio = ao_get_delay(mpctx->ao);
+
+ double predicted = mpctx->delay / mpctx->video_speed +
+ mpctx->time_frame;
+ double difference = buffered_audio - predicted;
+ MP_STATS(mpctx, "value %f audio-diff", difference);
+
+ if (opts->autosync) {
+ /* Smooth reported playback position from AO by averaging
+ * it with the value expected based on previous value and
+ * time elapsed since then. May help smooth video timing
+ * with audio output that have inaccurate position reporting.
+ * This is badly implemented; the behavior of the smoothing
+ * now undesirably depends on how often this code runs
+ * (mainly depends on video frame rate). */
+ buffered_audio = predicted + difference / opts->autosync;
+ }
+
+ mpctx->time_frame = buffered_audio - mpctx->delay / mpctx->video_speed;
+ } else {
+ /* If we're more than 200 ms behind the right playback
+ * position, don't try to speed up display of following
+ * frames to catch up; continue with default speed from
+ * the current frame instead.
+ * If untimed is set always output frames immediately
+ * without sleeping.
+ */
+ if (mpctx->time_frame < -0.2 || opts->untimed || vo->driver->untimed)
+ mpctx->time_frame = 0;
+ }
+}
+
+// Update the A/V sync difference when a new video frame is being shown.
+static void update_av_diff(struct MPContext *mpctx, double offset)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ mpctx->last_av_difference = 0;
+
+ if (mpctx->audio_status != STATUS_PLAYING ||
+ mpctx->video_status != STATUS_PLAYING)
+ return;
+
+ if (mpctx->vo_chain && mpctx->vo_chain->is_sparse)
+ return;
+
+ double a_pos = playing_audio_pts(mpctx);
+ if (a_pos != MP_NOPTS_VALUE && mpctx->video_pts != MP_NOPTS_VALUE) {
+ mpctx->last_av_difference = a_pos - mpctx->video_pts
+ + opts->audio_delay + offset;
+ }
+
+ if (fabs(mpctx->last_av_difference) > 0.5 && !mpctx->drop_message_shown) {
+ MP_WARN(mpctx, "%s", av_desync_help_text);
+ mpctx->drop_message_shown = true;
+ }
+}
+
+double calc_average_frame_duration(struct MPContext *mpctx)
+{
+ double total = 0;
+ int num = 0;
+ for (int n = 0; n < mpctx->num_past_frames; n++) {
+ double dur = mpctx->past_frames[n].approx_duration;
+ if (dur <= 0)
+ continue;
+ total += dur;
+ num += 1;
+ }
+ return num > 0 ? total / num : 0;
+}
+
+// Find a speed factor such that the display FPS is an integer multiple of the
+// effective video FPS. If this is not possible, try to do it for multiples,
+// which still leads to an improved end result.
+// Both parameters are durations in seconds.
+static double calc_best_speed(double vsync, double frame,
+ double max_change, int max_factor)
+{
+ double ratio = frame / vsync;
+ for (int factor = 1; factor <= max_factor; factor++) {
+ double scale = ratio * factor / rint(ratio * factor);
+ if (fabs(scale - 1) <= max_change)
+ return scale;
+ }
+ return -1;
+}
+
+static double find_best_speed(struct MPContext *mpctx, double vsync)
+{
+ double total = 0;
+ int num = 0;
+ for (int n = 0; n < mpctx->num_past_frames; n++) {
+ double dur = mpctx->past_frames[n].approx_duration;
+ if (dur <= 0)
+ continue;
+ double best = calc_best_speed(vsync, dur / mpctx->opts->playback_speed,
+ mpctx->opts->sync_max_video_change / 100,
+ mpctx->opts->sync_max_factor);
+ if (best <= 0)
+ continue;
+ total += best;
+ num++;
+ }
+ // If it doesn't work, play at normal speed.
+ return num > 0 ? total / num : 1;
+}
+
+static bool using_spdif_passthrough(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain && mpctx->ao_chain->ao) {
+ int samplerate;
+ int format;
+ struct mp_chmap channels;
+ ao_get_format(mpctx->ao_chain->ao, &samplerate, &format, &channels);
+ return !af_fmt_is_pcm(format);
+ }
+ return false;
+}
+
+// Compute the relative audio speed difference by taking A/V dsync into account.
+static double compute_audio_drift(struct MPContext *mpctx, double vsync)
+{
+ // Least-squares linear regression, using relative real time for x, and
+ // audio desync for y. Assume speed didn't change for the frames we're
+ // looking at for simplicity. This also should actually use the realtime
+ // (minus paused time) for x, but use vsync scheduling points instead.
+ if (mpctx->num_past_frames <= 10)
+ return NAN;
+ int num = mpctx->num_past_frames - 1;
+ double sum_x = 0, sum_y = 0, sum_xy = 0, sum_xx = 0;
+ double x = 0;
+ for (int n = 0; n < num; n++) {
+ struct frame_info *frame = &mpctx->past_frames[n + 1];
+ if (frame->num_vsyncs < 0)
+ return NAN;
+ double y = frame->av_diff;
+ sum_x += x;
+ sum_y += y;
+ sum_xy += x * y;
+ sum_xx += x * x;
+ x -= frame->num_vsyncs * vsync;
+ }
+ return (sum_x * sum_y - num * sum_xy) / (sum_x * sum_x - num * sum_xx);
+}
+
+static void adjust_audio_drift_compensation(struct MPContext *mpctx, double vsync)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int mode = mpctx->video_out->opts->video_sync;
+
+ if ((mode != VS_DISP_RESAMPLE && mode != VS_DISP_TEMPO) ||
+ mpctx->audio_status != STATUS_PLAYING)
+ {
+ mpctx->speed_factor_a = mpctx->speed_factor_v;
+ return;
+ }
+
+ // Try to smooth out audio timing drifts. This can happen if either
+ // video isn't playing at expected speed, or audio is not playing at
+ // the requested speed. Both are unavoidable.
+ // The audio desync is made up of 2 parts: 1. drift due to rounding
+ // errors and imperfect information, and 2. an offset, due to
+ // unaligned audio/video start, or disruptive events halting audio
+ // or video for a small time.
+ // Instead of trying to be clever, just apply an awfully dumb drift
+ // compensation with a constant factor, which does what we want. In
+ // theory we could calculate the exact drift compensation needed,
+ // but it likely would be wrong anyway, and we'd run into the same
+ // issues again, except with more complex code.
+ // 1 means drifts to positive, -1 means drifts to negative
+ double max_drift = vsync / 2;
+ double av_diff = mpctx->last_av_difference;
+ int new = mpctx->display_sync_drift_dir;
+ if (av_diff * -mpctx->display_sync_drift_dir >= 0)
+ new = 0;
+ if (fabs(av_diff) > max_drift)
+ new = av_diff >= 0 ? 1 : -1;
+
+ bool change = mpctx->display_sync_drift_dir != new;
+ if (new || change) {
+ if (change)
+ MP_VERBOSE(mpctx, "Change display sync audio drift: %d\n", new);
+ mpctx->display_sync_drift_dir = new;
+
+ double max_correct = opts->sync_max_audio_change / 100;
+ double audio_factor = 1 + max_correct * -mpctx->display_sync_drift_dir;
+
+ if (new == 0) {
+ // If we're resetting, actually try to be clever and pick a speed
+ // which compensates the general drift we're getting.
+ double drift = compute_audio_drift(mpctx, vsync);
+ if (isnormal(drift)) {
+ // other = will be multiplied with audio_factor for final speed
+ double other = mpctx->opts->playback_speed * mpctx->speed_factor_v;
+ audio_factor = (mpctx->audio_speed - drift) / other;
+ MP_VERBOSE(mpctx, "Compensation factor: %f\n", audio_factor);
+ }
+ }
+
+ audio_factor = MPCLAMP(audio_factor, 1 - max_correct, 1 + max_correct);
+ mpctx->speed_factor_a = audio_factor * mpctx->speed_factor_v;
+ }
+}
+
+// Manipulate frame timing for display sync, or do nothing for normal timing.
+static void handle_display_sync_frame(struct MPContext *mpctx,
+ struct vo_frame *frame)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo *vo = mpctx->video_out;
+ int mode = vo->opts->video_sync;
+
+ if (!mpctx->display_sync_active) {
+ mpctx->display_sync_error = 0.0;
+ mpctx->display_sync_drift_dir = 0;
+ }
+
+ mpctx->display_sync_active = false;
+
+ if (!VS_IS_DISP(mode))
+ return;
+ bool resample = mode == VS_DISP_RESAMPLE || mode == VS_DISP_RESAMPLE_VDROP ||
+ mode == VS_DISP_RESAMPLE_NONE;
+ bool drop = mode == VS_DISP_VDROP || mode == VS_DISP_RESAMPLE ||
+ mode == VS_DISP_ADROP || mode == VS_DISP_RESAMPLE_VDROP ||
+ mode == VS_DISP_TEMPO;
+ drop &= frame->can_drop;
+
+ if (resample && using_spdif_passthrough(mpctx))
+ return;
+
+ double vsync = vo_get_vsync_interval(vo) / 1e9;
+ if (vsync <= 0)
+ return;
+
+ double approx_duration = MPMAX(0, mpctx->past_frames[0].approx_duration);
+ double adjusted_duration = approx_duration / opts->playback_speed;
+ if (adjusted_duration > 0.5)
+ return;
+
+ mpctx->speed_factor_v = 1.0;
+ if (mode != VS_DISP_VDROP)
+ mpctx->speed_factor_v = find_best_speed(mpctx, vsync);
+
+ // Determine for how many vsyncs a frame should be displayed. This can be
+ // e.g. 2 for 30hz on a 60hz display. It can also be 0 if the video
+ // framerate is higher than the display framerate.
+ // We use the speed-adjusted (i.e. real) frame duration for this.
+ double frame_duration = adjusted_duration / mpctx->speed_factor_v;
+ double ratio = (frame_duration + mpctx->display_sync_error) / vsync;
+ int num_vsyncs = MPMAX(lrint(ratio), 0);
+ double prev_error = mpctx->display_sync_error;
+ mpctx->display_sync_error += frame_duration - num_vsyncs * vsync;
+
+ MP_TRACE(mpctx, "s=%f vsyncs=%d dur=%f ratio=%f err=%.20f (%f/%f)\n",
+ mpctx->speed_factor_v, num_vsyncs, adjusted_duration, ratio,
+ mpctx->display_sync_error, mpctx->display_sync_error / vsync,
+ mpctx->display_sync_error / frame_duration);
+
+ double av_diff = mpctx->last_av_difference;
+ MP_STATS(mpctx, "value %f avdiff", av_diff);
+
+ // Intended number of additional display frames to drop (<0) or repeat (>0)
+ int drop_repeat = 0;
+
+ // If we are too far ahead/behind, attempt to drop/repeat frames.
+ // Tolerate some desync to avoid frame dropping due to jitter.
+ if (drop && fabs(av_diff) >= 0.020 && fabs(av_diff) / vsync >= 1)
+ drop_repeat = -av_diff / vsync; // round towards 0
+
+ // We can only drop all frames at most. We can repeat much more frames,
+ // but we still limit it to 10 times the original frames to avoid that
+ // corner cases or exceptional situations cause too much havoc.
+ drop_repeat = MPCLAMP(drop_repeat, -num_vsyncs, num_vsyncs * 10);
+ num_vsyncs += drop_repeat;
+
+ // Always show the first frame.
+ if (mpctx->num_past_frames <= 1 && num_vsyncs < 1)
+ num_vsyncs = 1;
+
+ // Estimate the video position, so we can calculate a good A/V difference
+ // value below. This is used to estimate A/V drift.
+ double time_left = vo_get_delay(vo);
+
+ // We also know that the timing is (necessarily) off, because we have to
+ // align frame timings on the vsync boundaries. This is unavoidable, and
+ // for the sake of the A/V sync calculations we pretend it's perfect.
+ time_left += prev_error;
+ // Likewise, we know sync is off, but is going to be compensated.
+ time_left += drop_repeat * vsync;
+
+ // If syncing took too long, disregard timing of the first frame.
+ if (mpctx->num_past_frames == 2 && time_left < 0) {
+ vo_discard_timing_info(vo);
+ time_left = 0;
+ }
+
+ if (drop_repeat) {
+ mpctx->mistimed_frames_total += 1;
+ MP_STATS(mpctx, "mistimed");
+ }
+
+ mpctx->total_avsync_change = 0;
+ update_av_diff(mpctx, time_left * opts->playback_speed);
+
+ mpctx->past_frames[0].num_vsyncs = num_vsyncs;
+ mpctx->past_frames[0].av_diff = mpctx->last_av_difference;
+
+ if (resample || mode == VS_DISP_ADROP || mode == VS_DISP_TEMPO) {
+ adjust_audio_drift_compensation(mpctx, vsync);
+ } else {
+ mpctx->speed_factor_a = 1.0;
+ }
+
+ // A bad guess, only needed when reverting to audio sync.
+ mpctx->time_frame = time_left;
+
+ frame->vsync_interval = vsync;
+ frame->vsync_offset = -prev_error;
+ frame->ideal_frame_duration = frame_duration;
+ frame->ideal_frame_vsync = (-prev_error / frame_duration) * approx_duration;
+ frame->ideal_frame_vsync_duration = (vsync / frame_duration) * approx_duration;
+ frame->num_vsyncs = num_vsyncs;
+ frame->display_synced = true;
+ frame->approx_duration = approx_duration;
+
+ // Adjust frame virtual vsyncs by the repeat count
+ if (drop_repeat > 0)
+ frame->ideal_frame_vsync_duration /= drop_repeat;
+
+ mpctx->display_sync_active = true;
+ // Try to avoid audio underruns that may occur if we update
+ // the playback speed while in the STATUS_SYNCING state.
+ if (mpctx->video_status != STATUS_SYNCING)
+ update_playback_speed(mpctx);
+
+ MP_STATS(mpctx, "value %f aspeed", mpctx->speed_factor_a - 1);
+ MP_STATS(mpctx, "value %f vspeed", mpctx->speed_factor_v - 1);
+}
+
+static void schedule_frame(struct MPContext *mpctx, struct vo_frame *frame)
+{
+ handle_display_sync_frame(mpctx, frame);
+
+ if (mpctx->num_past_frames > 1 &&
+ ((mpctx->past_frames[1].num_vsyncs >= 0) != mpctx->display_sync_active))
+ {
+ MP_VERBOSE(mpctx, "Video sync mode %s.\n",
+ mpctx->display_sync_active ? "enabled" : "disabled");
+ }
+
+ if (!mpctx->display_sync_active) {
+ mpctx->speed_factor_a = 1.0;
+ mpctx->speed_factor_v = 1.0;
+ update_playback_speed(mpctx);
+
+ update_av_diff(mpctx, mpctx->time_frame > 0 ?
+ mpctx->time_frame * mpctx->video_speed : 0);
+ }
+}
+
+// Determine the mpctx->past_frames[0] frame duration.
+static void calculate_frame_duration(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ assert(mpctx->num_past_frames >= 1 && mpctx->num_next_frames >= 1);
+
+ double demux_duration = vo_c->filter->container_fps > 0
+ ? 1.0 / vo_c->filter->container_fps : -1;
+ double duration = demux_duration;
+
+ if (mpctx->num_next_frames >= 2) {
+ double pts0 = mpctx->next_frames[0]->pts;
+ double pts1 = mpctx->next_frames[1]->pts;
+ if (pts0 != MP_NOPTS_VALUE && pts1 != MP_NOPTS_VALUE && pts1 >= pts0)
+ duration = pts1 - pts0;
+ }
+
+ // The following code tries to compensate for rounded Matroska timestamps
+ // by "unrounding" frame durations, or if not possible, approximating them.
+ // These formats usually round on 1ms. Some muxers do this incorrectly,
+ // and might go off by 1ms more, and compensate for it later by an equal
+ // rounding error into the opposite direction.
+ double tolerance = 0.001 * 3 + 0.0001;
+
+ double total = 0;
+ int num_dur = 0;
+ for (int n = 1; n < mpctx->num_past_frames; n++) {
+ // Eliminate likely outliers using a really dumb heuristic.
+ double dur = mpctx->past_frames[n].duration;
+ if (dur <= 0 || fabs(dur - duration) >= tolerance)
+ break;
+ total += dur;
+ num_dur += 1;
+ }
+ double approx_duration = num_dur > 0 ? total / num_dur : duration;
+
+ // Try if the demuxer frame rate fits - if so, just take it.
+ if (demux_duration > 0) {
+ // Note that even if each timestamp is within rounding tolerance, it
+ // could literally not add up (e.g. if demuxer FPS is rounded itself).
+ if (fabs(duration - demux_duration) < tolerance &&
+ fabs(total - demux_duration * num_dur) < tolerance &&
+ (num_dur >= 16 || num_dur >= mpctx->num_past_frames - 4))
+ {
+ approx_duration = demux_duration;
+ }
+ }
+
+ mpctx->past_frames[0].duration = duration;
+ mpctx->past_frames[0].approx_duration = approx_duration;
+
+ MP_STATS(mpctx, "value %f frame-duration", MPMAX(0, duration));
+ MP_STATS(mpctx, "value %f frame-duration-approx", MPMAX(0, approx_duration));
+}
+
+static void apply_video_crop(struct MPContext *mpctx, struct vo *vo)
+{
+ for (int n = 0; n < mpctx->num_next_frames; n++) {
+ struct m_geometry *gm = &vo->opts->video_crop;
+ struct mp_image_params p = mpctx->next_frames[n]->params;
+ if (gm->xy_valid || (gm->wh_valid && (gm->w > 0 || gm->h > 0)))
+ {
+ m_rect_apply(&p.crop, p.w, p.h, gm);
+ }
+
+ if (p.crop.x1 == 0 && p.crop.y1 == 0)
+ return;
+
+ if (!mp_image_crop_valid(&p)) {
+ char *str = m_option_type_rect.print(NULL, gm);
+ MP_WARN(vo, "Ignoring invalid --video-crop=%s for %dx%d image\n",
+ str, p.w, p.h);
+ talloc_free(str);
+ *gm = (struct m_geometry){0};
+ mp_property_do("video-crop", M_PROPERTY_SET, gm, mpctx);
+ return;
+ }
+ mpctx->next_frames[n]->params.crop = p.crop;
+ }
+}
+
+static bool video_reconfig_needed(const struct mp_image_params *p1,
+ const struct mp_image_params *p2)
+{
+ return p1->imgfmt != p2->imgfmt ||
+ p1->hw_subfmt != p2->hw_subfmt ||
+ p1->w != p2->w || p1->h != p2->h ||
+ p1->p_w != p2->p_w || p1->p_h != p2->p_h ||
+ p1->force_window != p2->force_window ||
+ p1->rotate != p2->rotate ||
+ p1->stereo3d != p2->stereo3d ||
+ !mp_rect_equals(&p1->crop, &p2->crop);
+}
+
+void write_video(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->vo_chain)
+ return;
+ struct track *track = mpctx->vo_chain->track;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ struct vo *vo = vo_c->vo;
+
+ if (vo_c->filter->reconfig_happened) {
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ vo_c->filter->reconfig_happened = false;
+ }
+
+ // Actual playback starts when both audio and video are ready.
+ if (mpctx->video_status == STATUS_READY)
+ return;
+
+ if (mpctx->paused && mpctx->video_status >= STATUS_READY)
+ return;
+
+ bool logical_eof = false;
+ int r = video_output_image(mpctx, &logical_eof);
+ MP_TRACE(mpctx, "video_output_image: r=%d/eof=%d/st=%s\n", r, logical_eof,
+ mp_status_str(mpctx->video_status));
+
+ if (r < 0)
+ goto error;
+
+ if (r == VD_WAIT) {
+ // Heuristic to detect underruns.
+ if (mpctx->video_status == STATUS_PLAYING && !vo_still_displaying(vo) &&
+ !vo_c->underrun_signaled)
+ {
+ vo_c->underrun = true;
+ vo_c->underrun_signaled = true;
+ }
+ // Demuxer will wake us up for more packets to decode.
+ return;
+ }
+
+ if (r == VD_EOF) {
+ if (check_for_hwdec_fallback(mpctx))
+ return;
+ if (check_for_forced_eof(mpctx)) {
+ uninit_video_chain(mpctx);
+ handle_force_window(mpctx, true);
+ return;
+ }
+ if (vo_c->filter->failed_output_conversion)
+ goto error;
+
+ mpctx->delay = 0;
+ mpctx->last_av_difference = 0;
+
+ if (mpctx->video_status <= STATUS_PLAYING) {
+ mpctx->video_status = STATUS_DRAINING;
+ get_relative_time(mpctx);
+ if (vo_c->is_sparse && !mpctx->ao_chain) {
+ MP_VERBOSE(mpctx, "assuming this is an image\n");
+ mpctx->time_frame += opts->image_display_duration;
+ } else if (mpctx->last_frame_duration > 0) {
+ MP_VERBOSE(mpctx, "using demuxer frame duration for last frame\n");
+ mpctx->time_frame += mpctx->last_frame_duration;
+ } else {
+ mpctx->time_frame = 0;
+ }
+ // Encode mode can't honor this; it'll only delay finishing.
+ if (mpctx->encode_lavc_ctx)
+ mpctx->time_frame = 0;
+ }
+
+ // Wait for the VO to signal actual EOF, then exit if the frame timer
+ // has expired.
+ bool has_frame = vo_has_frame(vo); // maybe not configured
+ if (mpctx->video_status == STATUS_DRAINING &&
+ (vo_is_ready_for_frame(vo, -1) || !has_frame))
+ {
+ mpctx->time_frame -= get_relative_time(mpctx);
+ mp_set_timeout(mpctx, mpctx->time_frame);
+ if (mpctx->time_frame <= 0 || !has_frame) {
+ MP_VERBOSE(mpctx, "video EOF reached\n");
+ mpctx->video_status = STATUS_EOF;
+ }
+ }
+
+ // Avoid pointlessly spamming the logs every frame.
+ if (!vo_c->is_sparse || !vo_c->sparse_eof_signalled) {
+ MP_DBG(mpctx, "video EOF (status=%d)\n", mpctx->video_status);
+ vo_c->sparse_eof_signalled = vo_c->is_sparse;
+ }
+ return;
+ }
+
+ if (mpctx->video_status > STATUS_PLAYING)
+ mpctx->video_status = STATUS_PLAYING;
+
+ if (r != VD_NEW_FRAME) {
+ mp_wakeup_core(mpctx); // Decode more in next iteration.
+ return;
+ }
+
+ if (logical_eof && !mpctx->num_past_frames && mpctx->num_next_frames == 1 &&
+ use_video_lookahead(mpctx) && !vo_c->is_sparse)
+ {
+ // Too much danger to accidentally mark video as sparse when e.g.
+ // seeking exactly to the last frame, so as a heuristic, do this only
+ // if it looks like the "first" video frame (unreliable, but often
+ // works out well). Helps with seeking with single-image video tracks,
+ // as well as detecting whether as video track is really an image.
+ if (mpctx->next_frames[0]->pts == 0) {
+ MP_VERBOSE(mpctx, "assuming single-image video stream\n");
+ vo_c->is_sparse = true;
+ }
+ }
+
+ // Inject vo crop to notify and reconfig if needed
+ apply_video_crop(mpctx, vo);
+
+ // Filter output is different from VO input?
+ struct mp_image_params *p = &mpctx->next_frames[0]->params;
+ if (!vo->params || video_reconfig_needed(p, vo->params)) {
+ // Changing config deletes the current frame; wait until it's finished.
+ if (vo_still_displaying(vo))
+ return;
+
+ const struct vo_driver *info = mpctx->video_out->driver;
+ char extra[20] = {0};
+ if (p->p_w != p->p_h) {
+ int d_w, d_h;
+ mp_image_params_get_dsize(p, &d_w, &d_h);
+ snprintf(extra, sizeof(extra), " => %dx%d", d_w, d_h);
+ }
+ char sfmt[20] = {0};
+ if (p->hw_subfmt)
+ snprintf(sfmt, sizeof(sfmt), "[%s]", mp_imgfmt_to_name(p->hw_subfmt));
+ MP_INFO(mpctx, "VO: [%s] %dx%d%s %s%s\n",
+ info->name, p->w, p->h, extra, mp_imgfmt_to_name(p->imgfmt), sfmt);
+ MP_VERBOSE(mpctx, "VO: Description: %s\n", info->description);
+
+ int vo_r = vo_reconfig2(vo, mpctx->next_frames[0]);
+ if (vo_r < 0) {
+ mpctx->error_playing = MPV_ERROR_VO_INIT_FAILED;
+ goto error;
+ }
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+
+ mpctx->time_frame -= get_relative_time(mpctx);
+ update_avsync_before_frame(mpctx);
+
+ // Enforce timing subtitles to video frames.
+ osd_set_force_video_pts(mpctx->osd, MP_NOPTS_VALUE);
+
+ if (!update_subtitles(mpctx, mpctx->next_frames[0]->pts)) {
+ MP_VERBOSE(mpctx, "Video frame delayed due to waiting on subtitles.\n");
+ return;
+ }
+
+ double time_frame = MPMAX(mpctx->time_frame, -1);
+ int64_t pts = mp_time_ns() + (int64_t)(time_frame * 1e9);
+
+ // wait until VO wakes us up to get more frames
+ // (NB: in theory, the 1st frame after display sync mode change uses the
+ // wrong waiting mode)
+ if (!vo_is_ready_for_frame(vo, mpctx->display_sync_active ? -1 : pts))
+ return;
+
+ assert(mpctx->num_next_frames >= 1);
+
+ if (mpctx->num_past_frames >= MAX_NUM_VO_PTS)
+ mpctx->num_past_frames--;
+ MP_TARRAY_INSERT_AT(mpctx, mpctx->past_frames, mpctx->num_past_frames, 0,
+ (struct frame_info){0});
+ mpctx->past_frames[0] = (struct frame_info){
+ .pts = mpctx->next_frames[0]->pts,
+ .num_vsyncs = -1,
+ };
+ calculate_frame_duration(mpctx);
+
+ int req = vo_get_num_req_frames(mpctx->video_out);
+ assert(req >= 1 && req <= VO_MAX_REQ_FRAMES);
+ struct vo_frame dummy = {
+ .pts = pts,
+ .duration = -1,
+ .still = mpctx->step_frames > 0,
+ .can_drop = opts->frame_dropping & 1,
+ .num_frames = MPMIN(mpctx->num_next_frames, req),
+ .num_vsyncs = 1,
+ };
+ for (int n = 0; n < dummy.num_frames; n++)
+ dummy.frames[n] = mpctx->next_frames[n];
+ struct vo_frame *frame = vo_frame_ref(&dummy);
+
+ double diff = mpctx->past_frames[0].approx_duration;
+ if (opts->untimed || vo->driver->untimed)
+ diff = -1; // disable frame dropping and aspects of frame timing
+ if (diff >= 0) {
+ // expected A/V sync correction is ignored
+ diff /= mpctx->video_speed;
+ if (mpctx->time_frame < 0)
+ diff += mpctx->time_frame;
+ frame->duration = MPCLAMP(diff, 0, 10) * 1e9;
+ }
+
+ mpctx->video_pts = mpctx->next_frames[0]->pts;
+ mpctx->last_frame_duration =
+ mpctx->next_frames[0]->pkt_duration / mpctx->video_speed;
+
+ shift_frames(mpctx);
+
+ schedule_frame(mpctx, frame);
+
+ mpctx->osd_force_update = true;
+ update_osd_msg(mpctx);
+
+ vo_queue_frame(vo, frame);
+
+ check_framedrop(mpctx, vo_c);
+
+ // The frames were shifted down; "initialize" the new first entry.
+ if (mpctx->num_next_frames >= 1)
+ handle_new_frame(mpctx);
+
+ mpctx->shown_vframes++;
+ if (mpctx->video_status < STATUS_PLAYING) {
+ mpctx->video_status = STATUS_READY;
+ // After a seek, make sure to wait until the first frame is visible.
+ if (!opts->video_latency_hacks) {
+ vo_wait_frame(vo);
+ MP_VERBOSE(mpctx, "first video frame after restart shown\n");
+ }
+ }
+
+ mp_notify(mpctx, MPV_EVENT_TICK, NULL);
+
+ // hr-seek past EOF -> returns last frame, but terminates playback. The
+ // early EOF is needed to trigger the exit before the next seek is executed.
+ // Always using early EOF breaks other cases, like images.
+ if (logical_eof && !mpctx->num_next_frames && mpctx->ao_chain)
+ mpctx->video_status = STATUS_EOF;
+
+ if (mpctx->video_status != STATUS_EOF) {
+ if (mpctx->step_frames > 0) {
+ mpctx->step_frames--;
+ if (!mpctx->step_frames)
+ set_pause_state(mpctx, true);
+ }
+ if (mpctx->max_frames == 0 && !mpctx->stop_play)
+ mpctx->stop_play = AT_END_OF_FILE;
+ if (mpctx->max_frames > 0)
+ mpctx->max_frames--;
+ }
+
+ vo_c->underrun_signaled = false;
+
+ if (mpctx->video_status == STATUS_EOF || mpctx->stop_play)
+ mp_wakeup_core(mpctx);
+ return;
+
+error:
+ MP_FATAL(mpctx, "Could not initialize video chain.\n");
+ uninit_video_chain(mpctx);
+ error_on_track(mpctx, track);
+ handle_force_window(mpctx, true);
+ mp_wakeup_core(mpctx);
+}
diff --git a/stream/cookies.c b/stream/cookies.c
new file mode 100644
index 0000000..fe61c0e
--- /dev/null
+++ b/stream/cookies.c
@@ -0,0 +1,138 @@
+/*
+ * HTTP Cookies
+ * Reads Netscape and Mozilla cookies.txt files
+ *
+ * Copyright (c) 2003 Dave Lambley <mplayer@davel.me.uk>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <inttypes.h>
+
+#include "stream/stream.h"
+#include "options/options.h"
+#include "cookies.h"
+#include "common/msg.h"
+
+#define MAX_COOKIES 20
+
+typedef struct cookie_list_type {
+ char *name;
+ char *value;
+ char *domain;
+ char *path;
+
+ int secure;
+
+ struct cookie_list_type *next;
+} cookie_list_t;
+
+/* Like strdup, but stops at anything <31. */
+static char *col_dup(void *talloc_ctx, const char *src)
+{
+ int length = 0;
+ while (src[length] > 31)
+ length++;
+
+ return talloc_strndup(talloc_ctx, src, length);
+}
+
+/* Finds the start of all the columns */
+static int parse_line(char **ptr, char *cols[7])
+{
+ int col;
+ cols[0] = *ptr;
+
+ for (col = 1; col < 7; col++) {
+ for (; (**ptr) > 31; (*ptr)++);
+ if (**ptr == 0)
+ return 0;
+ (*ptr)++;
+ if ((*ptr)[-1] != 9)
+ return 0;
+ cols[col] = (*ptr);
+ }
+
+ return 1;
+}
+
+/* Loads a cookies.txt file into a linked list. */
+static struct cookie_list_type *load_cookies_from(void *ctx,
+ struct mpv_global *global,
+ struct mp_log *log,
+ const char *filename)
+{
+ mp_verbose(log, "Loading cookie file: %s\n", filename);
+ bstr data = stream_read_file(filename, ctx, global, 1000000);
+ if (!data.start) {
+ mp_verbose(log, "Error reading\n");
+ return NULL;
+ }
+
+ bstr_xappend(ctx, &data, (struct bstr){"", 1}); // null-terminate
+ char *ptr = data.start;
+
+ struct cookie_list_type *list = NULL;
+ while (*ptr) {
+ char *cols[7];
+ if (parse_line(&ptr, cols)) {
+ struct cookie_list_type *new;
+ new = talloc_zero(ctx, cookie_list_t);
+ new->name = col_dup(new, cols[5]);
+ new->value = col_dup(new, cols[6]);
+ new->path = col_dup(new, cols[2]);
+ new->domain = col_dup(new, cols[0]);
+ new->secure = (*(cols[3]) == 't') || (*(cols[3]) == 'T');
+ new->next = list;
+ list = new;
+ }
+ }
+
+ return list;
+}
+
+// Return a cookies string as expected by lavf (libavformat/http.c). The format
+// is like a Set-Cookie header (http://curl.haxx.se/rfc/cookie_spec.html),
+// separated by newlines.
+char *cookies_lavf(void *talloc_ctx,
+ struct mpv_global *global,
+ struct mp_log *log,
+ const char *file)
+{
+ void *tmp = talloc_new(NULL);
+ struct cookie_list_type *list = NULL;
+ if (file && file[0])
+ list = load_cookies_from(tmp, global, log, file);
+
+ char *res = talloc_strdup(talloc_ctx, "");
+
+ while (list) {
+ res = talloc_asprintf_append_buffer(res,
+ "%s=%s; path=%s; domain=%s; %s\n", list->name, list->value,
+ list->path, list->domain, list->secure ? "secure" : "");
+ list = list->next;
+ }
+
+ talloc_free(tmp);
+ return res;
+}
diff --git a/stream/cookies.h b/stream/cookies.h
new file mode 100644
index 0000000..491b87d
--- /dev/null
+++ b/stream/cookies.h
@@ -0,0 +1,31 @@
+/*
+ * HTTP Cookies
+ * Reads Netscape and Mozilla cookies.txt files
+ *
+ * Copyright (c) 2003 Dave Lambley <mplayer@davel.me.uk>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_COOKIES_H
+#define MPLAYER_COOKIES_H
+
+char *cookies_lavf(void *talloc_ctx,
+ struct mpv_global *global,
+ struct mp_log *log,
+ const char *file);
+
+#endif /* MPLAYER_COOKIES_H */
diff --git a/stream/dvb_tune.c b/stream/dvb_tune.c
new file mode 100644
index 0000000..d18f70f
--- /dev/null
+++ b/stream/dvb_tune.c
@@ -0,0 +1,652 @@
+/* dvbtune - tune.c
+
+ Copyright (C) Dave Chapman 2001,2002
+ Copyright (C) Rozhuk Ivan <rozhuk.im@gmail.com> 2016 - 2017
+
+ Modified for use with MPlayer, for details see the changelog at
+ http://svn.mplayerhq.hu/mplayer/trunk/
+ $Id$
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ Or, point your browser to http://www.gnu.org/copyleft/gpl.html
+
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <poll.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <linux/dvb/dmx.h>
+#include <linux/dvb/frontend.h>
+
+#include "osdep/io.h"
+#include "osdep/timer.h"
+#include "dvbin.h"
+#include "dvb_tune.h"
+#include "common/msg.h"
+
+/* Keep in sync with enum fe_delivery_system. */
+static const char *dvb_delsys_str[] = {
+ "UNDEFINED",
+ "DVB-C ANNEX A",
+ "DVB-C ANNEX B",
+ "DVB-T",
+ "DSS",
+ "DVB-S",
+ "DVB-S2",
+ "DVB-H",
+ "ISDBT",
+ "ISDBS",
+ "ISDBC",
+ "ATSC",
+ "ATSCMH",
+ "DTMB",
+ "CMMB",
+ "DAB",
+ "DVB-T2",
+ "TURBO",
+ "DVB-C ANNEX C",
+ NULL
+};
+
+const char *get_dvb_delsys(unsigned int delsys)
+{
+ if (SYS_DVB__COUNT__ <= delsys)
+ return dvb_delsys_str[0];
+ return dvb_delsys_str[delsys];
+}
+
+unsigned int dvb_get_tuner_delsys_mask(int fe_fd, struct mp_log *log)
+{
+ unsigned int ret_mask = 0, delsys;
+ struct dtv_property prop[1];
+ struct dtv_properties cmdseq = {.num = 1, .props = prop};
+
+ prop[0].cmd = DTV_ENUM_DELSYS;
+ if (ioctl(fe_fd, FE_GET_PROPERTY, &cmdseq) < 0) {
+ mp_err(log, "DVBv5: FE_GET_PROPERTY(DTV_ENUM_DELSYS) error: %d\n", errno);
+ return ret_mask;
+ }
+ unsigned int delsys_count = prop[0].u.buffer.len;
+ if (delsys_count == 0) {
+ mp_err(log, "DVBv5: Frontend returned no delivery systems!\n");
+ return ret_mask;
+ }
+ mp_verbose(log, "DVBv5: Number of supported delivery systems: %d\n", delsys_count);
+ for (unsigned int i = 0; i < delsys_count; i++) {
+ delsys = (unsigned int)prop[0].u.buffer.data[i];
+ DELSYS_SET(ret_mask, delsys);
+ mp_verbose(log, " %s\n", get_dvb_delsys(delsys));
+ }
+
+ return ret_mask;
+}
+
+int dvb_open_devices(dvb_priv_t *priv, unsigned int adapter,
+ unsigned int frontend, unsigned int demux_cnt)
+{
+ dvb_state_t *state = priv->state;
+
+ char frontend_dev[100], dvr_dev[100], demux_dev[100];
+ snprintf(frontend_dev, sizeof(frontend_dev), "/dev/dvb/adapter%u/frontend%u", adapter, frontend);
+ snprintf(dvr_dev, sizeof(dvr_dev), "/dev/dvb/adapter%u/dvr0", adapter);
+ snprintf(demux_dev, sizeof(demux_dev), "/dev/dvb/adapter%u/demux0", adapter);
+
+ MP_VERBOSE(priv, "Opening frontend device %s\n", frontend_dev);
+ state->fe_fd = open(frontend_dev, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (state->fe_fd < 0) {
+ MP_ERR(priv, "Error opening frontend device: %d\n", errno);
+ return 0;
+ }
+
+ state->demux_fds_cnt = 0;
+ MP_VERBOSE(priv, "Opening %d demuxers\n", demux_cnt);
+ for (unsigned int i = 0; i < demux_cnt; i++) {
+ state->demux_fds[i] = open(demux_dev, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (state->demux_fds[i] < 0) {
+ MP_ERR(priv, "Error opening demux0: %d\n", errno);
+ return 0;
+ }
+ state->demux_fds_cnt++;
+ }
+
+ state->dvr_fd = open(dvr_dev, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (state->dvr_fd < 0) {
+ MP_ERR(priv, "Error opening dvr device %s: %d\n", dvr_dev, errno);
+ return 0;
+ }
+
+ return 1;
+}
+
+int dvb_fix_demuxes(dvb_priv_t *priv, unsigned int cnt)
+{
+ dvb_state_t *state = priv->state;
+
+ char demux_dev[100];
+ snprintf(demux_dev, sizeof(demux_dev), "/dev/dvb/adapter%d/demux0",
+ state->adapters[state->cur_adapter].devno);
+
+ MP_VERBOSE(priv, "Changing demuxer count %d -> %d\n", state->demux_fds_cnt, cnt);
+ if (state->demux_fds_cnt >= cnt) {
+ for (int i = state->demux_fds_cnt - 1; i >= (int)cnt; i--) {
+ close(state->demux_fds[i]);
+ }
+ state->demux_fds_cnt = cnt;
+ } else {
+ for (int i = state->demux_fds_cnt; i < cnt; i++) {
+ state->demux_fds[i] = open(demux_dev,
+ O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (state->demux_fds[i] < 0) {
+ MP_ERR(priv, "Error opening demux0: %d\n", errno);
+ return 0;
+ }
+ state->demux_fds_cnt++;
+ }
+ }
+
+ return 1;
+}
+
+int dvb_set_ts_filt(dvb_priv_t *priv, int fd, uint16_t pid,
+ dmx_pes_type_t pestype)
+{
+ int i;
+ struct dmx_pes_filter_params pesFilterParams;
+
+ pesFilterParams.pid = pid;
+ pesFilterParams.input = DMX_IN_FRONTEND;
+ pesFilterParams.output = DMX_OUT_TS_TAP;
+ pesFilterParams.pes_type = pestype;
+ pesFilterParams.flags = DMX_IMMEDIATE_START;
+
+ {
+ int buffersize = 256 * 1024;
+ if (ioctl(fd, DMX_SET_BUFFER_SIZE, buffersize) < 0)
+ MP_ERR(priv, "Error in DMX_SET_BUFFER_SIZE %i: errno=%d\n",
+ pid, errno);
+ }
+
+ errno = 0;
+ if ((i = ioctl(fd, DMX_SET_PES_FILTER, &pesFilterParams)) < 0) {
+ MP_ERR(priv, "Error in DMX_SET_PES_FILTER %i: errno=%d\n",
+ pid, errno);
+ return 0;
+ }
+
+ return 1;
+}
+
+int dvb_get_pmt_pid(dvb_priv_t *priv, int devno, int service_id)
+{
+ /* We need special filters on the demux,
+ so open one locally, and close also here. */
+ char demux_dev[100];
+ snprintf(demux_dev, sizeof(demux_dev), "/dev/dvb/adapter%d/demux0", devno);
+
+ struct dmx_sct_filter_params fparams = {0};
+ fparams.pid = 0;
+ fparams.filter.filter[0] = 0x00;
+ fparams.filter.mask[0] = 0xff;
+ fparams.timeout = 0;
+ fparams.flags = DMX_IMMEDIATE_START | DMX_CHECK_CRC;
+
+ int pat_fd;
+ if ((pat_fd = open(demux_dev, O_RDWR)) < 0) {
+ MP_ERR(priv, "Opening PAT demux failed: %d", errno);
+ return -1;
+ }
+
+ if (ioctl(pat_fd, DMX_SET_FILTER, &fparams) < 0) {
+ MP_ERR(priv, "ioctl DMX_SET_FILTER failed: %d", errno);
+ close(pat_fd);
+ return -1;
+ }
+
+ int bytes_read;
+ unsigned char buft[4096];
+ unsigned char *bufptr = buft;
+
+ int pmt_pid = -1;
+
+ bool pat_read = false;
+ while (!pat_read) {
+ bytes_read = read(pat_fd, bufptr, sizeof(buft));
+ if (bytes_read < 0 && errno == EOVERFLOW)
+ bytes_read = read(pat_fd, bufptr, sizeof(buft));
+ if (bytes_read < 0) {
+ MP_ERR(priv, "PAT: read error: %d", errno);
+ close(pat_fd);
+ return -1;
+ }
+
+ int section_length = ((bufptr[1] & 0x0f) << 8) | bufptr[2];
+ if (bytes_read != section_length + 3)
+ continue;
+
+ bufptr += 8;
+ section_length -= 8;
+
+ /* assumes one section contains the whole pat */
+ pat_read = true;
+ while (section_length > 0) {
+ int this_service_id = (bufptr[0] << 8) | bufptr[1];
+ if (this_service_id == service_id) {
+ pmt_pid = ((bufptr[2] & 0x1f) << 8) | bufptr[3];
+ section_length = 0;
+ }
+ bufptr += 4;
+ section_length -= 4;
+ }
+ }
+ close(pat_fd);
+
+ return pmt_pid;
+}
+
+static void print_status(dvb_priv_t *priv, fe_status_t festatus)
+{
+ MP_VERBOSE(priv, "FE_STATUS:");
+ if (festatus & FE_HAS_SIGNAL)
+ MP_VERBOSE(priv, " FE_HAS_SIGNAL");
+ if (festatus & FE_TIMEDOUT)
+ MP_VERBOSE(priv, " FE_TIMEDOUT");
+ if (festatus & FE_HAS_LOCK)
+ MP_VERBOSE(priv, " FE_HAS_LOCK");
+ if (festatus & FE_HAS_CARRIER)
+ MP_VERBOSE(priv, " FE_HAS_CARRIER");
+ if (festatus & FE_HAS_VITERBI)
+ MP_VERBOSE(priv, " FE_HAS_VITERBI");
+ if (festatus & FE_HAS_SYNC)
+ MP_VERBOSE(priv, " FE_HAS_SYNC");
+ MP_VERBOSE(priv, "\n");
+}
+
+static int check_status(dvb_priv_t *priv, int fd_frontend, int tmout)
+{
+ fe_status_t festatus;
+ bool ok = false;
+ int locks = 0;
+
+ struct pollfd pfd[1];
+ pfd[0].fd = fd_frontend;
+ pfd[0].events = POLLPRI;
+
+ MP_VERBOSE(priv, "Getting frontend status\n");
+ int tm1 = (int)mp_time_sec();
+ while (!ok) {
+ festatus = 0;
+ if (poll(pfd, 1, tmout * 1000) > 0) {
+ if (pfd[0].revents & POLLPRI) {
+ if (ioctl(fd_frontend, FE_READ_STATUS, &festatus) >= 0) {
+ if (festatus & FE_HAS_LOCK)
+ locks++;
+ }
+ }
+ }
+ usleep(10000);
+ int tm2 = (int)mp_time_sec();
+ if ((festatus & FE_TIMEDOUT) || (locks >= 2) || (tm2 - tm1 >= tmout))
+ ok = true;
+ }
+
+ if (!(festatus & FE_HAS_LOCK)) {
+ MP_ERR(priv, "Not able to lock to the signal on the given frequency, "
+ "timeout: %d\n", tmout);
+ return -1;
+ }
+
+ int32_t strength = 0;
+ if (ioctl(fd_frontend, FE_READ_BER, &strength) >= 0)
+ MP_VERBOSE(priv, "Bit error rate: %d\n", strength);
+
+ strength = 0;
+ if (ioctl(fd_frontend, FE_READ_SIGNAL_STRENGTH, &strength) >= 0)
+ MP_VERBOSE(priv, "Signal strength: %d\n", strength);
+
+ strength = 0;
+ if (ioctl(fd_frontend, FE_READ_SNR, &strength) >= 0)
+ MP_VERBOSE(priv, "SNR: %d\n", strength);
+
+ strength = 0;
+ if (ioctl(fd_frontend, FE_READ_UNCORRECTED_BLOCKS, &strength) >= 0)
+ MP_VERBOSE(priv, "UNC: %d\n", strength);
+
+ print_status(priv, festatus);
+
+ return 0;
+}
+
+struct diseqc_cmd {
+ struct dvb_diseqc_master_cmd cmd;
+ uint32_t wait;
+};
+
+static int diseqc_send_msg(int fd, fe_sec_voltage_t v, struct diseqc_cmd *cmd,
+ fe_sec_tone_mode_t t, fe_sec_mini_cmd_t b)
+{
+ if (ioctl(fd, FE_SET_TONE, SEC_TONE_OFF) < 0)
+ return -1;
+ if (ioctl(fd, FE_SET_VOLTAGE, v) < 0)
+ return -1;
+ usleep(15 * 1000);
+ if (ioctl(fd, FE_DISEQC_SEND_MASTER_CMD, &cmd->cmd) < 0)
+ return -1;
+ usleep(cmd->wait * 1000);
+ usleep(15 * 1000);
+ if (ioctl(fd, FE_DISEQC_SEND_BURST, b) < 0)
+ return -1;
+ usleep(15 * 1000);
+ if (ioctl(fd, FE_SET_TONE, t) < 0)
+ return -1;
+ usleep(100000);
+
+ return 0;
+}
+
+/* digital satellite equipment control,
+ * specification is available from http://www.eutelsat.com/
+ */
+static int do_diseqc(int secfd, int sat_no, int polv, int hi_lo)
+{
+ struct diseqc_cmd cmd = { {{0xe0, 0x10, 0x38, 0xf0, 0x00, 0x00}, 4}, 0 };
+
+ /* param: high nibble: reset bits, low nibble set bits,
+ * bits are: option, position, polarizaion, band
+ */
+ cmd.cmd.msg[3] = 0xf0 | (((sat_no * 4) & 0x0f) | (hi_lo ? 1 : 0) | (polv ? 0 : 2));
+
+ return diseqc_send_msg(secfd, polv ? SEC_VOLTAGE_13 : SEC_VOLTAGE_18,
+ &cmd, hi_lo ? SEC_TONE_ON : SEC_TONE_OFF,
+ ((sat_no / 4) % 2) ? SEC_MINI_B : SEC_MINI_A);
+}
+
+static int dvbv5_tune(dvb_priv_t *priv, int fd_frontend,
+ unsigned int delsys, struct dtv_properties* cmdseq)
+{
+ MP_VERBOSE(priv, "Dumping raw tuning commands and values:\n");
+ for (int i = 0; i < cmdseq->num; ++i) {
+ MP_VERBOSE(priv, " %02d: 0x%x(%d) => 0x%x(%d)\n",
+ i, cmdseq->props[i].cmd, cmdseq->props[i].cmd,
+ cmdseq->props[i].u.data, cmdseq->props[i].u.data);
+ }
+ if (ioctl(fd_frontend, FE_SET_PROPERTY, cmdseq) < 0) {
+ MP_ERR(priv, "Error tuning channel\n");
+ return -1;
+ }
+ return 0;
+}
+
+static int tune_it(dvb_priv_t *priv, int fd_frontend, unsigned int delsys,
+ unsigned int freq, unsigned int srate, char pol,
+ int stream_id,
+ fe_spectral_inversion_t specInv, unsigned int diseqc,
+ fe_modulation_t modulation,
+ fe_code_rate_t HP_CodeRate,
+ fe_transmit_mode_t TransmissionMode,
+ fe_guard_interval_t guardInterval,
+ fe_bandwidth_t bandwidth,
+ fe_code_rate_t LP_CodeRate, fe_hierarchy_t hier,
+ int timeout)
+{
+ dvb_state_t *state = priv->state;
+
+ MP_VERBOSE(priv, "tune_it: fd_frontend %d, %s freq %lu, srate %lu, "
+ "pol %c, diseqc %u\n", fd_frontend,
+ get_dvb_delsys(delsys),
+ (long unsigned int)freq, (long unsigned int)srate,
+ (pol > ' ' ? pol : '-'), diseqc);
+
+ MP_VERBOSE(priv, "Using %s adapter %d\n",
+ get_dvb_delsys(delsys),
+ state->adapters[state->cur_adapter].devno);
+
+ {
+ /* discard stale QPSK events */
+ struct dvb_frontend_event ev;
+ while (true) {
+ if (ioctl(fd_frontend, FE_GET_EVENT, &ev) < 0)
+ break;
+ }
+ }
+
+ /* Prepare params, be verbose. */
+ int hi_lo = 0, bandwidth_hz = 0;
+ switch (delsys) {
+ case SYS_DVBT2:
+ case SYS_DVBT:
+ case SYS_ISDBT:
+ if (freq < 1000000)
+ freq *= 1000UL;
+ switch (bandwidth) {
+ case BANDWIDTH_5_MHZ:
+ bandwidth_hz = 5000000;
+ break;
+ case BANDWIDTH_6_MHZ:
+ bandwidth_hz = 6000000;
+ break;
+ case BANDWIDTH_7_MHZ:
+ bandwidth_hz = 7000000;
+ break;
+ case BANDWIDTH_8_MHZ:
+ bandwidth_hz = 8000000;
+ break;
+ case BANDWIDTH_10_MHZ:
+ bandwidth_hz = 10000000;
+ break;
+ case BANDWIDTH_AUTO:
+ if (freq < 474000000) {
+ bandwidth_hz = 7000000;
+ } else {
+ bandwidth_hz = 8000000;
+ }
+ break;
+ default:
+ bandwidth_hz = 0;
+ break;
+ }
+
+ MP_VERBOSE(priv, "tuning %s to %d Hz, bandwidth: %d\n",
+ get_dvb_delsys(delsys), freq, bandwidth_hz);
+ break;
+ case SYS_DVBS2:
+ case SYS_DVBS:
+ if (freq > 2200000) {
+ // this must be an absolute frequency
+ if (freq < SLOF) {
+ freq -= LOF1;
+ hi_lo = 0;
+ } else {
+ freq -= LOF2;
+ hi_lo = 1;
+ }
+ }
+ MP_VERBOSE(priv, "tuning %s to Freq: %u, Pol: %c Srate: %d, "
+ "22kHz: %s, LNB: %d\n", get_dvb_delsys(delsys), freq,
+ pol, srate, hi_lo ? "on" : "off", diseqc);
+
+ if (do_diseqc(fd_frontend, diseqc, (pol == 'V' ? 1 : 0), hi_lo) == 0) {
+ MP_VERBOSE(priv, "DISEQC setting succeeded\n");
+ } else {
+ MP_ERR(priv, "DISEQC setting failed\n");
+ return -1;
+ }
+
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ MP_VERBOSE(priv, "tuning %s to %d, srate=%d\n",
+ get_dvb_delsys(delsys), freq, srate);
+ break;
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ MP_VERBOSE(priv, "tuning %s to %d, modulation=%d\n",
+ get_dvb_delsys(delsys), freq, modulation);
+ break;
+ default:
+ MP_VERBOSE(priv, "Unknown FE type, aborting.\n");
+ return 0;
+ }
+
+ /* S2API is the DVB API new since 2.6.28.
+ * It is needed to tune to new delivery systems, e.g. DVB-S2.
+ * It takes a struct with a list of pairs of command + parameter.
+ */
+
+ /* Reset before tune. */
+ struct dtv_property p_clear[] = {
+ { .cmd = DTV_CLEAR },
+ };
+ struct dtv_properties cmdseq_clear = {
+ .num = 1,
+ .props = p_clear
+ };
+ if (ioctl(fd_frontend, FE_SET_PROPERTY, &cmdseq_clear) < 0) {
+ MP_ERR(priv, "DTV_CLEAR failed\n");
+ }
+
+ /* Tune. */
+ switch (delsys) {
+ case SYS_DVBS:
+ case SYS_DVBS2:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_SYMBOL_RATE, .u.data = srate },
+ { .cmd = DTV_INNER_FEC, .u.data = HP_CodeRate },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_ROLLOFF, .u.data = ROLLOFF_AUTO },
+ { .cmd = DTV_PILOT, .u.data = PILOT_AUTO },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_SYMBOL_RATE, .u.data = srate },
+ { .cmd = DTV_CODE_RATE_HP, .u.data = HP_CodeRate },
+ { .cmd = DTV_CODE_RATE_LP, .u.data = LP_CodeRate },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_BANDWIDTH_HZ, .u.data = bandwidth_hz },
+ { .cmd = DTV_TRANSMISSION_MODE, .u.data = TransmissionMode },
+ { .cmd = DTV_GUARD_INTERVAL, .u.data = guardInterval },
+ { .cmd = DTV_HIERARCHY, .u.data = hier },
+ { .cmd = DTV_STREAM_ID, .u.data = stream_id },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_SYMBOL_RATE, .u.data = srate },
+ { .cmd = DTV_INNER_FEC, .u.data = HP_CodeRate },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ }
+
+ int tune_status = check_status(priv, fd_frontend, timeout);
+ if (tune_status != 0) {
+ MP_ERR(priv, "Error locking to channel\n");
+ }
+ return tune_status;
+
+error_tune:
+ MP_ERR(priv, "Error tuning channel\n");
+ return -1;
+}
+
+int dvb_tune(dvb_priv_t *priv, unsigned int delsys,
+ int freq, char pol, int srate, int diseqc,
+ int stream_id, fe_spectral_inversion_t specInv,
+ fe_modulation_t modulation, fe_guard_interval_t guardInterval,
+ fe_transmit_mode_t TransmissionMode, fe_bandwidth_t bandWidth,
+ fe_code_rate_t HP_CodeRate,
+ fe_code_rate_t LP_CodeRate, fe_hierarchy_t hier,
+ int timeout)
+{
+ MP_INFO(priv, "Tuning to %s frequency %lu Hz\n",
+ get_dvb_delsys(delsys), (long unsigned int) freq);
+
+ dvb_state_t *state = priv->state;
+
+ int ris = tune_it(priv, state->fe_fd, delsys, freq, srate, pol,
+ stream_id, specInv, diseqc, modulation,
+ HP_CodeRate, TransmissionMode, guardInterval,
+ bandWidth, LP_CodeRate, hier, timeout);
+
+ if (ris != 0)
+ MP_INFO(priv, "Tuning failed\n");
+
+ return ris == 0;
+}
diff --git a/stream/dvb_tune.h b/stream/dvb_tune.h
new file mode 100644
index 0000000..d7a7901
--- /dev/null
+++ b/stream/dvb_tune.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPLAYER_DVB_TUNE_H
+#define MPLAYER_DVB_TUNE_H
+
+#include "dvbin.h"
+
+struct mp_log;
+
+
+const char *get_dvb_delsys(unsigned int delsys);
+unsigned int dvb_get_tuner_delsys_mask(int fe_fd, struct mp_log *log);
+int dvb_open_devices(dvb_priv_t *priv, unsigned int adapter,
+ unsigned int frontend, unsigned int demux_cnt);
+int dvb_fix_demuxes(dvb_priv_t *priv, unsigned int cnt);
+int dvb_set_ts_filt(dvb_priv_t *priv, int fd, uint16_t pid, dmx_pes_type_t pestype);
+int dvb_get_pmt_pid(dvb_priv_t *priv, int card, int service_id);
+int dvb_tune(dvb_priv_t *priv, unsigned int delsys,
+ int freq, char pol, int srate, int diseqc,
+ int stream_id, fe_spectral_inversion_t specInv,
+ fe_modulation_t modulation, fe_guard_interval_t guardInterval,
+ fe_transmit_mode_t TransmissionMode, fe_bandwidth_t bandWidth,
+ fe_code_rate_t HP_CodeRate, fe_code_rate_t LP_CodeRate,
+ fe_hierarchy_t hier, int timeout);
+
+#endif /* MPLAYER_DVB_TUNE_H */
diff --git a/stream/dvbin.h b/stream/dvbin.h
new file mode 100644
index 0000000..3daf747
--- /dev/null
+++ b/stream/dvbin.h
@@ -0,0 +1,143 @@
+/* Imported from the dvbstream project
+ *
+ * Modified for use with MPlayer, for details see the changelog at
+ * http://svn.mplayerhq.hu/mplayer/trunk/
+ * $Id$
+ */
+
+#ifndef MPLAYER_DVBIN_H
+#define MPLAYER_DVBIN_H
+
+#include "config.h"
+#include "stream.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define SLOF (11700 * 1000UL)
+#define LOF1 (9750 * 1000UL)
+#define LOF2 (10600 * 1000UL)
+
+#include <inttypes.h>
+#include <linux/dvb/dmx.h>
+#include <linux/dvb/frontend.h>
+#include <linux/dvb/version.h>
+
+#define MAX_ADAPTERS 16
+#define MAX_FRONTENDS 8
+
+#if DVB_API_VERSION < 5 || DVB_API_VERSION_MINOR < 8
+#error DVB support requires a non-ancient kernel
+#endif
+
+#define DVB_CHANNEL_LOWER -1
+#define DVB_CHANNEL_HIGHER 1
+
+#ifndef DMX_FILTER_SIZE
+#define DMX_FILTER_SIZE 32
+#endif
+
+typedef struct {
+ char *name;
+ unsigned int freq, srate, diseqc;
+ char pol;
+ unsigned int tpid, dpid1, dpid2, progid, ca, pids[DMX_FILTER_SIZE], pids_cnt;
+ bool is_dvb_x2; /* Used only in dvb_get_channels() and parse_vdr_par_string(), use delsys. */
+ unsigned int frontend;
+ unsigned int delsys;
+ unsigned int stream_id;
+ unsigned int service_id;
+ fe_spectral_inversion_t inv;
+ fe_modulation_t mod;
+ fe_transmit_mode_t trans;
+ fe_bandwidth_t bw;
+ fe_guard_interval_t gi;
+ fe_code_rate_t cr, cr_lp;
+ fe_hierarchy_t hier;
+} dvb_channel_t;
+
+typedef struct {
+ unsigned int NUM_CHANNELS;
+ unsigned int current;
+ dvb_channel_t *channels;
+} dvb_channels_list_t;
+
+typedef struct {
+ int devno;
+ unsigned int delsys_mask[MAX_FRONTENDS];
+ dvb_channels_list_t *list;
+} dvb_adapter_config_t;
+
+typedef struct {
+ unsigned int adapters_count;
+ dvb_adapter_config_t *adapters;
+ unsigned int cur_adapter;
+ unsigned int cur_frontend;
+
+ int fe_fd;
+ int dvr_fd;
+ int demux_fd[3], demux_fds[DMX_FILTER_SIZE], demux_fds_cnt;
+
+ bool is_on;
+ int retry;
+ unsigned int last_freq;
+ bool switching_channel;
+ bool stream_used;
+} dvb_state_t;
+
+typedef struct {
+ char *cfg_prog;
+ int cfg_devno;
+ int cfg_timeout;
+ char *cfg_file;
+ bool cfg_full_transponder;
+ int cfg_channel_switch_offset;
+} dvb_opts_t;
+
+typedef struct {
+ struct mp_log *log;
+
+ dvb_state_t *state;
+
+ char *prog;
+ int devno;
+
+ int opts_check_time;
+ dvb_opts_t *opts;
+ struct m_config_cache *opts_cache;
+} dvb_priv_t;
+
+
+/* Keep in sync with enum fe_delivery_system. */
+#define SYS_DVB__COUNT__ (SYS_DVBC_ANNEX_C + 1)
+
+
+#define DELSYS_BIT(__bit) (((unsigned int)1) << (__bit))
+
+#define DELSYS_SET(__mask, __bit) \
+ (__mask) |= DELSYS_BIT((__bit))
+
+#define DELSYS_IS_SET(__mask, __bit) \
+ (0 != ((__mask) & DELSYS_BIT((__bit))))
+
+
+#define DELSYS_SUPP_MASK \
+ ( \
+ DELSYS_BIT(SYS_DVBC_ANNEX_A) | \
+ DELSYS_BIT(SYS_DVBT) | \
+ DELSYS_BIT(SYS_DVBS) | \
+ DELSYS_BIT(SYS_DVBS2) | \
+ DELSYS_BIT(SYS_ATSC) | \
+ DELSYS_BIT(SYS_DVBC_ANNEX_B) | \
+ DELSYS_BIT(SYS_DVBT2) | \
+ DELSYS_BIT(SYS_ISDBT) | \
+ DELSYS_BIT(SYS_DVBC_ANNEX_C) \
+ )
+
+void dvb_update_config(stream_t *);
+int dvb_parse_path(stream_t *);
+int dvb_set_channel(stream_t *, unsigned int, unsigned int);
+dvb_state_t *dvb_get_state(stream_t *);
+
+#endif /* MPLAYER_DVBIN_H */
diff --git a/stream/stream.c b/stream/stream.c
new file mode 100644
index 0000000..dd67825
--- /dev/null
+++ b/stream/stream.c
@@ -0,0 +1,900 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+
+#include <strings.h>
+#include <assert.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "demux/demux.h"
+#include "misc/bstr.h"
+#include "misc/thread_tools.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "osdep/timer.h"
+#include "stream.h"
+
+#include "options/m_option.h"
+#include "options/m_config.h"
+
+extern const stream_info_t stream_info_cdda;
+extern const stream_info_t stream_info_dvb;
+extern const stream_info_t stream_info_null;
+extern const stream_info_t stream_info_memory;
+extern const stream_info_t stream_info_mf;
+extern const stream_info_t stream_info_ffmpeg;
+extern const stream_info_t stream_info_ffmpeg_unsafe;
+extern const stream_info_t stream_info_avdevice;
+extern const stream_info_t stream_info_file;
+extern const stream_info_t stream_info_slice;
+extern const stream_info_t stream_info_fd;
+extern const stream_info_t stream_info_ifo_dvdnav;
+extern const stream_info_t stream_info_dvdnav;
+extern const stream_info_t stream_info_bdmv_dir;
+extern const stream_info_t stream_info_bluray;
+extern const stream_info_t stream_info_bdnav;
+extern const stream_info_t stream_info_edl;
+extern const stream_info_t stream_info_libarchive;
+extern const stream_info_t stream_info_cb;
+
+static const stream_info_t *const stream_list[] = {
+#if HAVE_CDDA
+ &stream_info_cdda,
+#endif
+ &stream_info_ffmpeg,
+ &stream_info_ffmpeg_unsafe,
+ &stream_info_avdevice,
+#if HAVE_DVBIN
+ &stream_info_dvb,
+#endif
+#if HAVE_DVDNAV
+ &stream_info_ifo_dvdnav,
+ &stream_info_dvdnav,
+#endif
+#if HAVE_LIBBLURAY
+ &stream_info_bdmv_dir,
+ &stream_info_bluray,
+ &stream_info_bdnav,
+#endif
+#if HAVE_LIBARCHIVE
+ &stream_info_libarchive,
+#endif
+ &stream_info_memory,
+ &stream_info_null,
+ &stream_info_mf,
+ &stream_info_edl,
+ &stream_info_file,
+ &stream_info_slice,
+ &stream_info_fd,
+ &stream_info_cb,
+};
+
+// Because of guarantees documented on STREAM_BUFFER_SIZE.
+// Half the buffer is used as forward buffer, the other for seek-back.
+#define STREAM_MIN_BUFFER_SIZE (STREAM_BUFFER_SIZE * 2)
+// Sort of arbitrary; keep *2 of it comfortably within integer limits.
+// Must be power of 2.
+#define STREAM_MAX_BUFFER_SIZE (512 * 1024 * 1024)
+
+struct stream_opts {
+ int64_t buffer_size;
+ bool load_unsafe_playlists;
+};
+
+#define OPT_BASE_STRUCT struct stream_opts
+
+const struct m_sub_options stream_conf = {
+ .opts = (const struct m_option[]){
+ {"stream-buffer-size", OPT_BYTE_SIZE(buffer_size),
+ M_RANGE(STREAM_MIN_BUFFER_SIZE, STREAM_MAX_BUFFER_SIZE)},
+ {"load-unsafe-playlists", OPT_BOOL(load_unsafe_playlists)},
+ {0}
+ },
+ .size = sizeof(struct stream_opts),
+ .defaults = &(const struct stream_opts){
+ .buffer_size = 128 * 1024,
+ },
+};
+
+// return -1 if not hex char
+static int hex2dec(char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'A' && c <= 'F')
+ return 10 + c - 'A';
+ if (c >= 'a' && c <= 'f')
+ return 10 + c - 'a';
+ return -1;
+}
+
+// Replace escape sequences in an URL (or a part of an URL)
+void mp_url_unescape_inplace(char *url)
+{
+ for (int len = strlen(url), i = 0, o = 0; i <= len;) {
+ if ((url[i] != '%') || (i > len - 3)) { // %NN can't start after len-3
+ url[o++] = url[i++];
+ continue;
+ }
+
+ int msd = hex2dec(url[i + 1]),
+ lsd = hex2dec(url[i + 2]);
+
+ if (msd >= 0 && lsd >= 0) {
+ url[o++] = 16 * msd + lsd;
+ i += 3;
+ } else {
+ url[o++] = url[i++];
+ url[o++] = url[i++];
+ url[o++] = url[i++];
+ }
+ }
+}
+
+static const char hex_digits[] = "0123456789ABCDEF";
+
+
+static const char url_default_ok[] = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "-._~";
+
+// Escape according to http://tools.ietf.org/html/rfc3986#section-2.1
+// Only unreserved characters are not escaped.
+// The argument ok (if not NULL) is as follows:
+// ok[0] != '~': additional characters that are not escaped
+// ok[0] == '~': do not escape anything but these characters
+// (can't override the unreserved characters, which are
+// never escaped)
+char *mp_url_escape(void *talloc_ctx, const char *url, const char *ok)
+{
+ char *rv = talloc_size(talloc_ctx, strlen(url) * 3 + 1);
+ char *out = rv;
+ bool negate = ok && ok[0] == '~';
+
+ for (char c; (c = *url); url++) {
+ bool as_is = negate ? !strchr(ok + 1, c)
+ : (strchr(url_default_ok, c) || (ok && strchr(ok, c)));
+ if (as_is) {
+ *out++ = c;
+ } else {
+ unsigned char v = c;
+ *out++ = '%';
+ *out++ = hex_digits[v / 16];
+ *out++ = hex_digits[v % 16];
+ }
+ }
+
+ *out = 0;
+ return rv;
+}
+
+static const char *match_proto(const char *url, const char *proto)
+{
+ int l = strlen(proto);
+ if (l > 0) {
+ if (strncasecmp(url, proto, l) == 0 && strncmp("://", url + l, 3) == 0)
+ return url + l + 3;
+ } else if (!mp_is_url(bstr0(url))) {
+ return url; // pure filenames
+ }
+ return NULL;
+}
+
+// src and new are both STREAM_ORIGIN_* values. This checks whether a stream
+// with flags "new" can be opened from the "src". On success, return
+// new origin, on incompatibility return 0.
+static int check_origin(int src, int new)
+{
+ switch (src) {
+ case STREAM_ORIGIN_DIRECT:
+ case STREAM_ORIGIN_UNSAFE:
+ // Allow anything, but constrain it to the new origin.
+ return new;
+ case STREAM_ORIGIN_FS:
+ // From unix FS, allow all but unsafe.
+ if (new == STREAM_ORIGIN_FS || new == STREAM_ORIGIN_NET)
+ return new;
+ break;
+ case STREAM_ORIGIN_NET:
+ // Allow only other network links.
+ if (new == STREAM_ORIGIN_NET)
+ return new;
+ break;
+ }
+ return 0;
+}
+
+// Read len bytes from the start position, and wrap around as needed. Limit the
+// actually read data to the size of the buffer. Return amount of copied bytes.
+// len: max bytes to copy to dst
+// pos: index into s->buffer[], e.g. s->buf_start is byte 0
+// returns: bytes copied to dst (limited by len and available buffered data)
+static int ring_copy(struct stream *s, void *dst, int len, int pos)
+{
+ assert(len >= 0);
+
+ if (pos < s->buf_start || pos > s->buf_end)
+ return 0;
+
+ int copied = 0;
+ len = MPMIN(len, s->buf_end - pos);
+
+ if (len && pos <= s->buffer_mask) {
+ int copy = MPMIN(len, s->buffer_mask + 1 - pos);
+ memcpy(dst, &s->buffer[pos], copy);
+ copied += copy;
+ len -= copy;
+ pos += copy;
+ }
+
+ if (len) {
+ memcpy((char *)dst + copied, &s->buffer[pos & s->buffer_mask], len);
+ copied += len;
+ }
+
+ return copied;
+}
+
+// Resize the current stream buffer. Uses a larger size if needed to keep data.
+// Does nothing if the size is adequate. Calling this with 0 ensures it uses the
+// default buffer size if possible.
+// The caller must check whether enough data was really allocated.
+// keep: keep at least [buf_end-keep, buf_end] (used for assert()s only)
+// new: new total size of buffer
+// returns: false if buffer allocation failed, true if reallocated or size ok
+static bool stream_resize_buffer(struct stream *s, int keep, int new)
+{
+ assert(keep >= s->buf_end - s->buf_cur);
+ assert(keep <= new);
+
+ new = MPMAX(new, s->requested_buffer_size);
+ new = MPMIN(new, STREAM_MAX_BUFFER_SIZE);
+ new = mp_round_next_power_of_2(new);
+
+ assert(keep <= new); // can't fail (if old buffer size was valid)
+
+ if (new == s->buffer_mask + 1)
+ return true;
+
+ int old_pos = s->buf_cur - s->buf_start;
+ int old_used_len = s->buf_end - s->buf_start;
+ int skip = old_used_len > new ? old_used_len - new : 0;
+
+ MP_DBG(s, "resize stream to %d bytes, drop %d bytes\n", new, skip);
+
+ void *nbuf = ta_alloc_size(s, new);
+ if (!nbuf)
+ return false; // oom; tolerate it, caller needs to check if required
+
+ int new_len = 0;
+ if (s->buffer)
+ new_len = ring_copy(s, nbuf, new, s->buf_start + skip);
+ assert(new_len == old_used_len - skip);
+ assert(old_pos >= skip); // "keep" too low
+ assert(old_pos - skip <= new_len);
+ s->buf_start = 0;
+ s->buf_cur = old_pos - skip;
+ s->buf_end = new_len;
+
+ ta_free(s->buffer);
+
+ s->buffer = nbuf;
+ s->buffer_mask = new - 1;
+
+ return true;
+}
+
+static int stream_create_instance(const stream_info_t *sinfo,
+ struct stream_open_args *args,
+ struct stream **ret)
+{
+ const char *url = args->url;
+ int flags = args->flags;
+
+ *ret = NULL;
+
+ const char *path = url;
+
+ if (flags & STREAM_LOCAL_FS_ONLY) {
+ if (!sinfo->local_fs)
+ return STREAM_NO_MATCH;
+ } else {
+ for (int n = 0; sinfo->protocols && sinfo->protocols[n]; n++) {
+ path = match_proto(url, sinfo->protocols[n]);
+ if (path)
+ break;
+ }
+
+ if (!path)
+ return STREAM_NO_MATCH;
+ }
+
+ stream_t *s = talloc_zero(NULL, stream_t);
+ s->global = args->global;
+ struct stream_opts *opts = mp_get_config_group(s, s->global, &stream_conf);
+ if (flags & STREAM_SILENT) {
+ s->log = mp_null_log;
+ } else {
+ s->log = mp_log_new(s, s->global->log, sinfo->name);
+ }
+ s->info = sinfo;
+ s->cancel = args->cancel;
+ s->url = talloc_strdup(s, url);
+ s->path = talloc_strdup(s, path);
+ s->mode = flags & (STREAM_READ | STREAM_WRITE);
+ s->requested_buffer_size = opts->buffer_size;
+
+ if (flags & STREAM_LESS_NOISE)
+ mp_msg_set_max_level(s->log, MSGL_WARN);
+
+ struct demux_opts *demux_opts = mp_get_config_group(s, s->global, &demux_conf);
+ s->access_references = demux_opts->access_references;
+ talloc_free(demux_opts);
+
+ MP_VERBOSE(s, "Opening %s\n", url);
+
+ if (strlen(url) > INT_MAX / 8) {
+ MP_ERR(s, "URL too large.\n");
+ talloc_free(s);
+ return STREAM_ERROR;
+ }
+
+ if ((s->mode & STREAM_WRITE) && !sinfo->can_write) {
+ MP_DBG(s, "No write access implemented.\n");
+ talloc_free(s);
+ return STREAM_NO_MATCH;
+ }
+
+ s->stream_origin = flags & STREAM_ORIGIN_MASK; // pass through by default
+ if (opts->load_unsafe_playlists) {
+ s->stream_origin = STREAM_ORIGIN_DIRECT;
+ } else if (sinfo->stream_origin) {
+ s->stream_origin = check_origin(s->stream_origin, sinfo->stream_origin);
+ }
+
+ if (!s->stream_origin) {
+ talloc_free(s);
+ return STREAM_UNSAFE;
+ }
+
+ int r = STREAM_UNSUPPORTED;
+ if (sinfo->open2) {
+ r = sinfo->open2(s, args);
+ } else if (!args->special_arg) {
+ r = (sinfo->open)(s);
+ }
+ if (r != STREAM_OK) {
+ talloc_free(s);
+ return r;
+ }
+
+ if (!stream_resize_buffer(s, 0, 0)) {
+ free_stream(s);
+ return STREAM_ERROR;
+ }
+
+ assert(s->seekable == !!s->seek);
+
+ if (s->mime_type)
+ MP_VERBOSE(s, "Mime-type: '%s'\n", s->mime_type);
+
+ MP_DBG(s, "Stream opened successfully.\n");
+
+ *ret = s;
+ return STREAM_OK;
+}
+
+int stream_create_with_args(struct stream_open_args *args, struct stream **ret)
+
+{
+ assert(args->url);
+
+ int r = STREAM_NO_MATCH;
+ *ret = NULL;
+
+ // Open stream proper
+ if (args->sinfo) {
+ r = stream_create_instance(args->sinfo, args, ret);
+ } else {
+ for (int i = 0; i < MP_ARRAY_SIZE(stream_list); i++) {
+ r = stream_create_instance(stream_list[i], args, ret);
+ if (r == STREAM_OK)
+ break;
+ if (r == STREAM_NO_MATCH || r == STREAM_UNSUPPORTED)
+ continue;
+ if (r == STREAM_UNSAFE)
+ continue;
+ break;
+ }
+ }
+
+ if (!*ret && !(args->flags & STREAM_SILENT) && !mp_cancel_test(args->cancel))
+ {
+ struct mp_log *log = mp_log_new(NULL, args->global->log, "!stream");
+
+ if (r == STREAM_UNSAFE) {
+ mp_err(log, "\nRefusing to load potentially unsafe URL from a playlist.\n"
+ "Use the --load-unsafe-playlists option to load it anyway.\n\n");
+ } else if (r == STREAM_NO_MATCH || r == STREAM_UNSUPPORTED) {
+ mp_err(log, "No protocol handler found to open URL %s\n", args->url);
+ mp_err(log, "The protocol is either unsupported, or was disabled "
+ "at compile-time.\n");
+ } else {
+ mp_err(log, "Failed to open %s.\n", args->url);
+ }
+
+ talloc_free(log);
+ }
+
+ return r;
+}
+
+struct stream *stream_create(const char *url, int flags,
+ struct mp_cancel *c, struct mpv_global *global)
+{
+ struct stream_open_args args = {
+ .global = global,
+ .cancel = c,
+ .flags = flags,
+ .url = url,
+ };
+ struct stream *s;
+ stream_create_with_args(&args, &s);
+ return s;
+}
+
+stream_t *open_output_stream(const char *filename, struct mpv_global *global)
+{
+ return stream_create(filename, STREAM_ORIGIN_DIRECT | STREAM_WRITE,
+ NULL, global);
+}
+
+// Read function bypassing the local stream buffer. This will not write into
+// s->buffer, but into buf[0..len] instead.
+// Returns 0 on error or EOF, and length of bytes read on success.
+// Partial reads are possible, even if EOF is not reached.
+static int stream_read_unbuffered(stream_t *s, void *buf, int len)
+{
+ assert(len >= 0);
+ if (len <= 0)
+ return 0;
+
+ int res = 0;
+ // we will retry even if we already reached EOF previously.
+ if (s->fill_buffer && !mp_cancel_test(s->cancel))
+ res = s->fill_buffer(s, buf, len);
+ if (res <= 0) {
+ s->eof = 1;
+ return 0;
+ }
+ assert(res <= len);
+ // When reading succeeded we are obviously not at eof.
+ s->eof = 0;
+ s->pos += res;
+ s->total_unbuffered_read_bytes += res;
+ return res;
+}
+
+// Ask for having at most "forward" bytes ready to read in the buffer.
+// To read everything, you may have to call this in a loop.
+// forward: desired amount of bytes in buffer after s->cur_pos
+// returns: progress (false on EOF or on OOM or if enough data was available)
+static bool stream_read_more(struct stream *s, int forward)
+{
+ assert(forward >= 0);
+
+ int forward_avail = s->buf_end - s->buf_cur;
+ if (forward_avail >= forward)
+ return false;
+
+ // Avoid that many small reads will lead to many low-level read calls.
+ forward = MPMAX(forward, s->requested_buffer_size / 2);
+ assert(forward_avail < forward);
+
+ // Keep guaranteed seek-back.
+ int buf_old = MPMIN(s->buf_cur - s->buf_start, s->requested_buffer_size / 2);
+
+ if (!stream_resize_buffer(s, buf_old + forward_avail, buf_old + forward))
+ return false;
+
+ int buf_alloc = s->buffer_mask + 1;
+
+ assert(s->buf_start <= s->buf_cur);
+ assert(s->buf_cur <= s->buf_end);
+ assert(s->buf_cur < buf_alloc * 2);
+ assert(s->buf_end < buf_alloc * 2);
+ assert(s->buf_start < buf_alloc);
+
+ // Note: read as much as possible, even if forward is much smaller. Do
+ // this because the stream buffer is supposed to set an approx. minimum
+ // read size on it.
+ int read = buf_alloc - (buf_old + forward_avail); // free buffer past end
+
+ int pos = s->buf_end & s->buffer_mask;
+ read = MPMIN(read, buf_alloc - pos);
+
+ // Note: if wrap-around happens, we need to make two calls. This may
+ // affect latency (e.g. waiting for new data on a socket), so do only
+ // 1 read call always.
+ read = stream_read_unbuffered(s, &s->buffer[pos], read);
+
+ s->buf_end += read;
+
+ // May have overwritten old data.
+ if (s->buf_end - s->buf_start >= buf_alloc) {
+ assert(s->buf_end >= buf_alloc);
+
+ s->buf_start = s->buf_end - buf_alloc;
+
+ assert(s->buf_start <= s->buf_cur);
+ assert(s->buf_cur <= s->buf_end);
+
+ if (s->buf_start >= buf_alloc) {
+ s->buf_start -= buf_alloc;
+ s->buf_cur -= buf_alloc;
+ s->buf_end -= buf_alloc;
+ }
+ }
+
+ // Must not have overwritten guaranteed old data.
+ assert(s->buf_cur - s->buf_start >= buf_old);
+
+ if (s->buf_cur < s->buf_end)
+ s->eof = 0;
+
+ return !!read;
+}
+
+// Read between 1..buf_size bytes of data, return how much data has been read.
+// Return 0 on EOF, error, or if buf_size was 0.
+int stream_read_partial(stream_t *s, void *buf, int buf_size)
+{
+ assert(s->buf_cur <= s->buf_end);
+ assert(buf_size >= 0);
+ if (s->buf_cur == s->buf_end && buf_size > 0) {
+ if (buf_size > (s->buffer_mask + 1) / 2) {
+ // Direct read if the buffer is too small anyway.
+ stream_drop_buffers(s);
+ return stream_read_unbuffered(s, buf, buf_size);
+ }
+ stream_read_more(s, 1);
+ }
+ int res = ring_copy(s, buf, buf_size, s->buf_cur);
+ s->buf_cur += res;
+ return res;
+}
+
+// Slow version of stream_read_char(); called by it if the buffer is empty.
+int stream_read_char_fallback(stream_t *s)
+{
+ uint8_t c;
+ return stream_read_partial(s, &c, 1) ? c : -256;
+}
+
+int stream_read(stream_t *s, void *mem, int total)
+{
+ int len = total;
+ while (len > 0) {
+ int read = stream_read_partial(s, mem, len);
+ if (read <= 0)
+ break; // EOF
+ mem = (char *)mem + read;
+ len -= read;
+ }
+ total -= len;
+ return total;
+}
+
+// Read ahead so that at least forward_size bytes are readable ahead. Returns
+// the actual forward amount available (restricted by EOF or buffer limits).
+int stream_peek(stream_t *s, int forward_size)
+{
+ while (stream_read_more(s, forward_size)) {}
+ return s->buf_end - s->buf_cur;
+}
+
+// Like stream_read(), but do not advance the current position. This may resize
+// the buffer to satisfy the read request.
+int stream_read_peek(stream_t *s, void *buf, int buf_size)
+{
+ stream_peek(s, buf_size);
+ return ring_copy(s, buf, buf_size, s->buf_cur);
+}
+
+int stream_write_buffer(stream_t *s, void *buf, int len)
+{
+ if (!s->write_buffer)
+ return -1;
+ int orig_len = len;
+ while (len) {
+ int w = s->write_buffer(s, buf, len);
+ if (w <= 0)
+ return -1;
+ s->pos += w;
+ buf = (char *)buf + w;
+ len -= w;
+ }
+ return orig_len;
+}
+
+// Drop len bytes form input, possibly reading more until all is skipped. If
+// EOF or an error was encountered before all could be skipped, return false,
+// otherwise return true.
+static bool stream_skip_read(struct stream *s, int64_t len)
+{
+ while (len > 0) {
+ unsigned int left = s->buf_end - s->buf_cur;
+ if (!left) {
+ if (!stream_read_more(s, 1))
+ return false;
+ continue;
+ }
+ unsigned skip = MPMIN(len, left);
+ s->buf_cur += skip;
+ len -= skip;
+ }
+ return true;
+}
+
+// Drop the internal buffer. Note that this will advance the stream position
+// (as seen by stream_tell()), because the real stream position is ahead of the
+// logical stream position by the amount of buffered but not yet read data.
+void stream_drop_buffers(stream_t *s)
+{
+ s->pos = stream_tell(s);
+ s->buf_start = s->buf_cur = s->buf_end = 0;
+ s->eof = 0;
+ stream_resize_buffer(s, 0, 0);
+}
+
+// Seek function bypassing the local stream buffer.
+static bool stream_seek_unbuffered(stream_t *s, int64_t newpos)
+{
+ if (newpos != s->pos) {
+ MP_VERBOSE(s, "stream level seek from %" PRId64 " to %" PRId64 "\n",
+ s->pos, newpos);
+
+ s->total_stream_seeks++;
+
+ if (newpos > s->pos && !s->seekable) {
+ MP_ERR(s, "Cannot seek forward in this stream\n");
+ return false;
+ }
+ if (newpos < s->pos && !s->seekable) {
+ MP_ERR(s, "Cannot seek backward in linear streams!\n");
+ return false;
+ }
+ if (s->seek(s, newpos) <= 0) {
+ int level = mp_cancel_test(s->cancel) ? MSGL_V : MSGL_ERR;
+ MP_MSG(s, level, "Seek failed (to %lld, size %lld)\n",
+ (long long)newpos, (long long)stream_get_size(s));
+ return false;
+ }
+ stream_drop_buffers(s);
+ s->pos = newpos;
+ }
+ return true;
+}
+
+bool stream_seek(stream_t *s, int64_t pos)
+{
+ MP_TRACE(s, "seek request from %" PRId64 " to %" PRId64 "\n",
+ stream_tell(s), pos);
+
+ s->eof = 0; // eof should be set only on read; seeking always clears it
+
+ if (pos < 0) {
+ MP_ERR(s, "Invalid seek to negative position %lld!\n", (long long)pos);
+ pos = 0;
+ }
+
+ if (pos <= s->pos) {
+ int64_t x = pos - (s->pos - (int)s->buf_end);
+ if (x >= (int)s->buf_start) {
+ s->buf_cur = x;
+ assert(s->buf_cur >= s->buf_start);
+ assert(s->buf_cur <= s->buf_end);
+ return true;
+ }
+ }
+
+ if (s->mode == STREAM_WRITE)
+ return s->seekable && s->seek(s, pos);
+
+ // Skip data instead of performing a seek in some cases.
+ if (pos >= s->pos &&
+ ((!s->seekable && s->fast_skip) ||
+ pos - s->pos <= s->requested_buffer_size))
+ {
+ return stream_skip_read(s, pos - stream_tell(s));
+ }
+
+ return stream_seek_unbuffered(s, pos);
+}
+
+// Like stream_seek(), but strictly prefer skipping data instead of failing, if
+// it's a forward-seek.
+bool stream_seek_skip(stream_t *s, int64_t pos)
+{
+ uint64_t cur_pos = stream_tell(s);
+
+ if (cur_pos == pos)
+ return true;
+
+ return !s->seekable && pos > cur_pos
+ ? stream_skip_read(s, pos - cur_pos)
+ : stream_seek(s, pos);
+}
+
+int stream_control(stream_t *s, int cmd, void *arg)
+{
+ return s->control ? s->control(s, cmd, arg) : STREAM_UNSUPPORTED;
+}
+
+// Return the current size of the stream, or a negative value if unknown.
+int64_t stream_get_size(stream_t *s)
+{
+ return s->get_size ? s->get_size(s) : -1;
+}
+
+void free_stream(stream_t *s)
+{
+ if (!s)
+ return;
+
+ if (s->close)
+ s->close(s);
+ talloc_free(s);
+}
+
+static const char *const bom[3] = {"\xEF\xBB\xBF", "\xFF\xFE", "\xFE\xFF"};
+
+// Return utf16 argument for stream_read_line
+int stream_skip_bom(struct stream *s)
+{
+ char buf[4];
+ int len = stream_read_peek(s, buf, sizeof(buf));
+ bstr data = {buf, len};
+ for (int n = 0; n < 3; n++) {
+ if (bstr_startswith0(data, bom[n])) {
+ stream_seek_skip(s, stream_tell(s) + strlen(bom[n]));
+ return n;
+ }
+ }
+ return -1; // default to 8 bit codepages
+}
+
+// Read the rest of the stream into memory (current pos to EOF), and return it.
+// talloc_ctx: used as talloc parent for the returned allocation
+// max_size: must be set to >0. If the file is larger than that, it is treated
+// as error. This is a minor robustness measure.
+// returns: stream contents, or .start/.len set to NULL on error
+// If the file was empty, but no error happened, .start will be non-NULL and
+// .len will be 0.
+// For convenience, the returned buffer is padded with a 0 byte. The padding
+// is not included in the returned length.
+struct bstr stream_read_complete(struct stream *s, void *talloc_ctx,
+ int max_size)
+{
+ if (max_size > 1000000000)
+ abort();
+
+ int bufsize;
+ int total_read = 0;
+ int padding = 1;
+ char *buf = NULL;
+ int64_t size = stream_get_size(s) - stream_tell(s);
+ if (size > max_size)
+ return (struct bstr){NULL, 0};
+ if (size > 0)
+ bufsize = size + padding;
+ else
+ bufsize = 1000;
+ while (1) {
+ buf = talloc_realloc_size(talloc_ctx, buf, bufsize);
+ int readsize = stream_read(s, buf + total_read, bufsize - total_read);
+ total_read += readsize;
+ if (total_read < bufsize)
+ break;
+ if (bufsize > max_size) {
+ talloc_free(buf);
+ return (struct bstr){NULL, 0};
+ }
+ bufsize = MPMIN(bufsize + (bufsize >> 1), max_size + padding);
+ }
+ buf = talloc_realloc_size(talloc_ctx, buf, total_read + padding);
+ memset(&buf[total_read], 0, padding);
+ return (struct bstr){buf, total_read};
+}
+
+struct bstr stream_read_file(const char *filename, void *talloc_ctx,
+ struct mpv_global *global, int max_size)
+{
+ struct bstr res = {0};
+ int flags = STREAM_ORIGIN_DIRECT | STREAM_READ | STREAM_LOCAL_FS_ONLY |
+ STREAM_LESS_NOISE;
+ stream_t *s = stream_create(filename, flags, NULL, global);
+ if (s) {
+ res = stream_read_complete(s, talloc_ctx, max_size);
+ free_stream(s);
+ }
+ return res;
+}
+
+char **stream_get_proto_list(void)
+{
+ char **list = NULL;
+ int num = 0;
+ for (int i = 0; i < MP_ARRAY_SIZE(stream_list); i++) {
+ const stream_info_t *stream_info = stream_list[i];
+
+ if (!stream_info->protocols)
+ continue;
+
+ for (int j = 0; stream_info->protocols[j]; j++) {
+ if (*stream_info->protocols[j] == '\0')
+ continue;
+
+ MP_TARRAY_APPEND(NULL, list, num,
+ talloc_strdup(NULL, stream_info->protocols[j]));
+ }
+ }
+ MP_TARRAY_APPEND(NULL, list, num, NULL);
+ return list;
+}
+
+void stream_print_proto_list(struct mp_log *log)
+{
+ int count = 0;
+
+ mp_info(log, "Protocols:\n\n");
+ char **list = stream_get_proto_list();
+ for (int i = 0; list[i]; i++) {
+ mp_info(log, " %s://\n", list[i]);
+ count++;
+ talloc_free(list[i]);
+ }
+ talloc_free(list);
+ mp_info(log, "\nTotal: %d protocols\n", count);
+}
+
+bool stream_has_proto(const char *proto)
+{
+ for (int i = 0; i < MP_ARRAY_SIZE(stream_list); i++) {
+ const stream_info_t *stream_info = stream_list[i];
+
+ for (int j = 0; stream_info->protocols && stream_info->protocols[j]; j++) {
+ if (strcmp(stream_info->protocols[j], proto) == 0)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/stream/stream.h b/stream/stream.h
new file mode 100644
index 0000000..423ba12
--- /dev/null
+++ b/stream/stream.h
@@ -0,0 +1,269 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_STREAM_H
+#define MPLAYER_STREAM_H
+
+#include "common/msg.h"
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+#include "misc/bstr.h"
+
+// Minimum guaranteed buffer and seek-back size. For any reads <= of this size,
+// it's guaranteed that you can seek back by <= of this size again.
+#define STREAM_BUFFER_SIZE 2048
+
+// flags for stream_open_ext (this includes STREAM_READ and STREAM_WRITE)
+
+// stream->mode
+#define STREAM_READ 0
+#define STREAM_WRITE (1 << 0)
+
+#define STREAM_SILENT (1 << 1)
+
+// Origin value for "security". This is an integer within the flags bit-field.
+#define STREAM_ORIGIN_DIRECT (1 << 2) // passed from cmdline or loadfile
+#define STREAM_ORIGIN_FS (2 << 2) // referenced from playlist on unix FS
+#define STREAM_ORIGIN_NET (3 << 2) // referenced from playlist on network
+#define STREAM_ORIGIN_UNSAFE (4 << 2) // from a grotesque source
+
+#define STREAM_ORIGIN_MASK (7 << 2) // for extracting origin value from flags
+
+#define STREAM_LOCAL_FS_ONLY (1 << 5) // stream_file only, no URLs
+#define STREAM_LESS_NOISE (1 << 6) // try to log errors only
+
+// end flags for stream_open_ext (the naming convention sucks)
+
+#define STREAM_UNSAFE -3
+#define STREAM_NO_MATCH -2
+#define STREAM_UNSUPPORTED -1
+#define STREAM_ERROR 0
+#define STREAM_OK 1
+
+enum stream_ctrl {
+ // Certain network protocols
+ STREAM_CTRL_AVSEEK,
+ STREAM_CTRL_HAS_AVSEEK,
+ STREAM_CTRL_GET_METADATA,
+
+ // Optical discs (internal interface between streams and demux_disc)
+ STREAM_CTRL_GET_TIME_LENGTH,
+ STREAM_CTRL_GET_DVD_INFO,
+ STREAM_CTRL_GET_DISC_NAME,
+ STREAM_CTRL_GET_NUM_CHAPTERS,
+ STREAM_CTRL_GET_CURRENT_TIME,
+ STREAM_CTRL_GET_CHAPTER_TIME,
+ STREAM_CTRL_SEEK_TO_TIME,
+ STREAM_CTRL_GET_ASPECT_RATIO,
+ STREAM_CTRL_GET_NUM_ANGLES,
+ STREAM_CTRL_GET_ANGLE,
+ STREAM_CTRL_SET_ANGLE,
+ STREAM_CTRL_GET_NUM_TITLES,
+ STREAM_CTRL_GET_TITLE_LENGTH, // double* (in: title number, out: len)
+ STREAM_CTRL_GET_LANG,
+ STREAM_CTRL_GET_CURRENT_TITLE,
+ STREAM_CTRL_SET_CURRENT_TITLE,
+};
+
+struct stream_lang_req {
+ int type; // STREAM_AUDIO, STREAM_SUB
+ int id;
+ char name[50];
+};
+
+struct stream_dvd_info_req {
+ unsigned int palette[16];
+ int num_subs;
+};
+
+// for STREAM_CTRL_AVSEEK
+struct stream_avseek {
+ int stream_index;
+ int64_t timestamp;
+ int flags;
+};
+
+struct stream;
+struct stream_open_args;
+typedef struct stream_info_st {
+ const char *name;
+ // opts is set from ->opts
+ int (*open)(struct stream *st);
+ // Alternative to open(). Only either open() or open2() can be set.
+ int (*open2)(struct stream *st, const struct stream_open_args *args);
+ const char *const *protocols;
+ bool can_write; // correctly checks for READ/WRITE modes
+ bool local_fs; // supports STREAM_LOCAL_FS_ONLY
+ int stream_origin; // 0 or set of STREAM_ORIGIN_*; if 0, the same origin
+ // is set, or the stream's open() function handles it
+} stream_info_t;
+
+typedef struct stream {
+ const struct stream_info_st *info;
+
+ // Read
+ int (*fill_buffer)(struct stream *s, void *buffer, int max_len);
+ // Write
+ int (*write_buffer)(struct stream *s, void *buffer, int len);
+ // Seek
+ int (*seek)(struct stream *s, int64_t pos);
+ // Total stream size in bytes (negative if unavailable)
+ int64_t (*get_size)(struct stream *s);
+ // Control
+ int (*control)(struct stream *s, int cmd, void *arg);
+ // Close
+ void (*close)(struct stream *s);
+
+ int64_t pos;
+ int eof; // valid only after read calls that returned a short result
+ int mode; //STREAM_READ or STREAM_WRITE
+ int stream_origin; // any STREAM_ORIGIN_*
+ void *priv; // used for DVD, TV, RTSP etc
+ char *url; // filename/url (possibly including protocol prefix)
+ char *path; // filename (url without protocol prefix)
+ char *mime_type; // when HTTP streaming is used
+ char *demuxer; // request demuxer to be used
+ char *lavf_type; // name of expected demuxer type for lavf
+ bool streaming : 1; // known to be a network stream if true
+ bool seekable : 1; // presence of general byte seeking support
+ bool fast_skip : 1; // consider stream fast enough to fw-seek by skipping
+ bool is_network : 1; // I really don't know what this is for
+ bool is_local_file : 1; // from the filesystem
+ bool is_directory : 1; // directory on the filesystem
+ bool access_references : 1; // open other streams
+ struct mp_log *log;
+ struct mpv_global *global;
+
+ struct mp_cancel *cancel; // cancellation notification
+
+ // Read statistic for fill_buffer calls. All bytes read by fill_buffer() are
+ // added to this. The user can reset this as needed.
+ uint64_t total_unbuffered_read_bytes;
+ // Seek statistics. The user can reset this as needed.
+ uint64_t total_stream_seeks;
+
+ // Buffer size requested by user; s->buffer may have a different size
+ int requested_buffer_size;
+
+ // This is a ring buffer. It is reset only on seeks (or when buffers are
+ // dropped). Otherwise old contents always stay valid.
+ // The valid buffer is from buf_start to buf_end; buf_end can be larger
+ // than the buffer size (requires wrap around). buf_cur is a value in the
+ // range [buf_start, buf_end].
+ // When reading more data from the stream, buf_start is advanced as old
+ // data is overwritten with new data.
+ // Example:
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+ // +===========================+---------------------------+
+ // + 05 06 07 08 | 01 02 03 04 + 05 06 07 08 | 01 02 03 04 +
+ // +===========================+---------------------------+
+ // ^ buf_start (4) | |
+ // | ^ buf_end (12 % 8 => 4)
+ // ^ buf_cur (9 % 8 => 1)
+ // Here, the entire 8 byte buffer is filled, i.e. buf_end - buf_start = 8.
+ // buffer_mask == 7, so (x & buffer_mask) == (x % buffer_size)
+ unsigned int buf_start; // index of oldest byte in buffer (is <= buffer_mask)
+ unsigned int buf_cur; // current read pos (can be > buffer_mask)
+ unsigned int buf_end; // end position (can be > buffer_mask)
+
+ unsigned int buffer_mask; // buffer_size-1, where buffer_size == 2**n
+ uint8_t *buffer;
+} stream_t;
+
+// Non-inline version of stream_read_char().
+int stream_read_char_fallback(stream_t *s);
+
+int stream_write_buffer(stream_t *s, void *buf, int len);
+
+inline static int stream_read_char(stream_t *s)
+{
+ return s->buf_cur < s->buf_end
+ ? s->buffer[(s->buf_cur++) & s->buffer_mask]
+ : stream_read_char_fallback(s);
+}
+
+int stream_skip_bom(struct stream *s);
+
+inline static int64_t stream_tell(stream_t *s)
+{
+ return s->pos + s->buf_cur - s->buf_end;
+}
+
+bool stream_seek_skip(stream_t *s, int64_t pos);
+bool stream_seek(stream_t *s, int64_t pos);
+int stream_read(stream_t *s, void *mem, int total);
+int stream_read_partial(stream_t *s, void *buf, int buf_size);
+int stream_peek(stream_t *s, int forward_size);
+int stream_read_peek(stream_t *s, void *buf, int buf_size);
+void stream_drop_buffers(stream_t *s);
+int64_t stream_get_size(stream_t *s);
+
+struct mpv_global;
+
+struct bstr stream_read_complete(struct stream *s, void *talloc_ctx,
+ int max_size);
+struct bstr stream_read_file(const char *filename, void *talloc_ctx,
+ struct mpv_global *global, int max_size);
+
+int stream_control(stream_t *s, int cmd, void *arg);
+void free_stream(stream_t *s);
+
+struct stream_open_args {
+ struct mpv_global *global;
+ struct mp_cancel *cancel; // aborting stream access (used directly)
+ const char *url;
+ int flags; // STREAM_READ etc.
+ const stream_info_t *sinfo; // NULL = autoprobe, otherwise force stream impl.
+ void *special_arg; // specific to impl., use only with sinfo
+};
+
+int stream_create_with_args(struct stream_open_args *args, struct stream **ret);
+struct stream *stream_create(const char *url, int flags,
+ struct mp_cancel *c, struct mpv_global *global);
+stream_t *open_output_stream(const char *filename, struct mpv_global *global);
+
+void mp_url_unescape_inplace(char *buf);
+char *mp_url_escape(void *talloc_ctx, const char *s, const char *ok);
+
+// stream_memory.c
+struct stream *stream_memory_open(struct mpv_global *global, void *data, int len);
+
+// stream_concat.c
+struct stream *stream_concat_open(struct mpv_global *global, struct mp_cancel *c,
+ struct stream **streams, int num_streams);
+
+// stream_file.c
+char *mp_file_url_to_filename(void *talloc_ctx, bstr url);
+char *mp_file_get_path(void *talloc_ctx, bstr url);
+
+// stream_lavf.c
+struct AVDictionary;
+void mp_setup_av_network_options(struct AVDictionary **dict,
+ const char *target_fmt,
+ struct mpv_global *global,
+ struct mp_log *log);
+
+void stream_print_proto_list(struct mp_log *log);
+char **stream_get_proto_list(void);
+bool stream_has_proto(const char *proto);
+
+#endif /* MPLAYER_STREAM_H */
diff --git a/stream/stream_avdevice.c b/stream/stream_avdevice.c
new file mode 100644
index 0000000..5046b21
--- /dev/null
+++ b/stream/stream_avdevice.c
@@ -0,0 +1,32 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "stream.h"
+
+static int open_f(stream_t *stream)
+{
+ stream->demuxer = "lavf";
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_avdevice = {
+ .name = "avdevice",
+ .open = open_f,
+ .protocols = (const char*const[]){ "avdevice", "av", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c
new file mode 100644
index 0000000..7771375
--- /dev/null
+++ b/stream/stream_bluray.c
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2010 Benjamin Zores <ben@geexbox.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Blu-ray parser/reader using libbluray
+ * Use 'git clone git://git.videolan.org/libbluray' to get it.
+ *
+ * TODO:
+ * - Add descrambled keys database support (KEYDB.cfg)
+ *
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <assert.h>
+
+#include <libbluray/bluray.h>
+#include <libbluray/meta_data.h>
+#include <libbluray/overlay.h>
+#include <libbluray/keys.h>
+#include <libbluray/bluray-version.h>
+#include <libbluray/log_control.h>
+#include <libavutil/common.h>
+
+#include "config.h"
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "stream.h"
+#include "osdep/timer.h"
+#include "sub/osd.h"
+#include "sub/img_convert.h"
+#include "video/mp_image.h"
+
+#define BLURAY_SECTOR_SIZE 6144
+
+#define BLURAY_DEFAULT_ANGLE 0
+#define BLURAY_DEFAULT_CHAPTER 0
+#define BLURAY_PLAYLIST_TITLE -3
+#define BLURAY_DEFAULT_TITLE -2
+#define BLURAY_MENU_TITLE -1
+
+// 90khz ticks
+#define BD_TIMEBASE (90000)
+#define BD_TIME_TO_MP(x) ((x) / (double)(BD_TIMEBASE))
+#define BD_TIME_FROM_MP(x) ((uint64_t)(x * BD_TIMEBASE))
+
+// copied from aacs.h in libaacs
+#define AACS_ERROR_CORRUPTED_DISC -1 /* opening or reading of AACS files failed */
+#define AACS_ERROR_NO_CONFIG -2 /* missing config file */
+#define AACS_ERROR_NO_PK -3 /* no matching processing key */
+#define AACS_ERROR_NO_CERT -4 /* no valid certificate */
+#define AACS_ERROR_CERT_REVOKED -5 /* certificate has been revoked */
+#define AACS_ERROR_MMC_OPEN -6 /* MMC open failed (no MMC drive ?) */
+#define AACS_ERROR_MMC_FAILURE -7 /* MMC failed */
+#define AACS_ERROR_NO_DK -8 /* no matching device key */
+
+
+struct bluray_opts {
+ char *bluray_device;
+};
+
+#define OPT_BASE_STRUCT struct bluray_opts
+const struct m_sub_options stream_bluray_conf = {
+ .opts = (const struct m_option[]) {
+ {"device", OPT_STRING(bluray_device), .flags = M_OPT_FILE},
+ {0},
+ },
+ .size = sizeof(struct bluray_opts),
+};
+
+struct bluray_priv_s {
+ BLURAY *bd;
+ BLURAY_TITLE_INFO *title_info;
+ int num_titles;
+ int current_angle;
+ int current_title;
+ int current_playlist;
+
+ int cfg_title;
+ int cfg_playlist;
+ char *cfg_device;
+
+ bool use_nav;
+ struct bluray_opts *opts;
+ struct m_config_cache *opts_cache;
+};
+
+static void destruct(struct bluray_priv_s *priv)
+{
+ if (priv->title_info)
+ bd_free_title_info(priv->title_info);
+ bd_close(priv->bd);
+}
+
+inline static int play_playlist(struct bluray_priv_s *priv, int playlist)
+{
+ return bd_select_playlist(priv->bd, playlist);
+}
+
+inline static int play_title(struct bluray_priv_s *priv, int title)
+{
+ return bd_select_title(priv->bd, title);
+}
+
+static void bluray_stream_close(stream_t *s)
+{
+ destruct(s->priv);
+}
+
+static void handle_event(stream_t *s, const BD_EVENT *ev)
+{
+ struct bluray_priv_s *b = s->priv;
+ switch (ev->event) {
+ case BD_EVENT_MENU:
+ break;
+ case BD_EVENT_STILL:
+ break;
+ case BD_EVENT_STILL_TIME:
+ bd_read_skip_still(b->bd);
+ break;
+ case BD_EVENT_END_OF_TITLE:
+ break;
+ case BD_EVENT_PLAYLIST:
+ b->current_playlist = ev->param;
+ b->current_title = bd_get_current_title(b->bd);
+ if (b->title_info)
+ bd_free_title_info(b->title_info);
+ b->title_info = bd_get_playlist_info(b->bd, b->current_playlist,
+ b->current_angle);
+ break;
+ case BD_EVENT_TITLE:
+ if (ev->param == BLURAY_TITLE_FIRST_PLAY) {
+ b->current_title = bd_get_current_title(b->bd);
+ } else
+ b->current_title = ev->param;
+ if (b->title_info) {
+ bd_free_title_info(b->title_info);
+ b->title_info = NULL;
+ }
+ break;
+ case BD_EVENT_ANGLE:
+ b->current_angle = ev->param;
+ if (b->title_info) {
+ bd_free_title_info(b->title_info);
+ b->title_info = bd_get_playlist_info(b->bd, b->current_playlist,
+ b->current_angle);
+ }
+ break;
+ case BD_EVENT_POPUP:
+ break;
+#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0)
+ case BD_EVENT_DISCONTINUITY:
+ break;
+#endif
+ default:
+ MP_TRACE(s, "Unhandled event: %d %d\n", ev->event, ev->param);
+ break;
+ }
+}
+
+static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len)
+{
+ struct bluray_priv_s *b = s->priv;
+ BD_EVENT event;
+ while (bd_get_event(b->bd, &event))
+ handle_event(s, &event);
+ return bd_read(b->bd, buf, len);
+}
+
+static int bluray_stream_control(stream_t *s, int cmd, void *arg)
+{
+ struct bluray_priv_s *b = s->priv;
+
+ switch (cmd) {
+ case STREAM_CTRL_GET_NUM_CHAPTERS: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ *((unsigned int *) arg) = ti->chapter_count;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CHAPTER_TIME: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ int chapter = *(double *)arg;
+ double time = MP_NOPTS_VALUE;
+ if (chapter >= 0 || chapter < ti->chapter_count)
+ time = BD_TIME_TO_MP(ti->chapters[chapter].start);
+ if (time == MP_NOPTS_VALUE)
+ return STREAM_ERROR;
+ *(double *)arg = time;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SET_CURRENT_TITLE: {
+ const uint32_t title = *((unsigned int*)arg);
+ if (title >= b->num_titles || !play_title(b, title))
+ return STREAM_UNSUPPORTED;
+ b->current_title = title;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CURRENT_TITLE: {
+ *((unsigned int *) arg) = b->current_title;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_NUM_TITLES: {
+ *((unsigned int *)arg) = b->num_titles;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_TIME_LENGTH: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ *((double *) arg) = BD_TIME_TO_MP(ti->duration);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CURRENT_TIME: {
+ *((double *) arg) = BD_TIME_TO_MP(bd_tell_time(b->bd));
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SEEK_TO_TIME: {
+ double pts = *((double *) arg);
+ bd_seek_time(b->bd, BD_TIME_FROM_MP(pts));
+ stream_drop_buffers(s);
+ // API makes it hard to determine seeking success
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_NUM_ANGLES: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ *((int *) arg) = ti->angle_count;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_ANGLE: {
+ *((int *) arg) = b->current_angle;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SET_ANGLE: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ int angle = *((int *) arg);
+ if (angle < 0 || angle > ti->angle_count)
+ return STREAM_UNSUPPORTED;
+ b->current_angle = angle;
+ bd_seamless_angle_change(b->bd, angle);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_LANG: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (ti && ti->clip_count) {
+ struct stream_lang_req *req = arg;
+ BLURAY_STREAM_INFO *si = NULL;
+ int count = 0;
+ switch (req->type) {
+ case STREAM_AUDIO:
+ count = ti->clips[0].audio_stream_count;
+ si = ti->clips[0].audio_streams;
+ break;
+ case STREAM_SUB:
+ count = ti->clips[0].pg_stream_count;
+ si = ti->clips[0].pg_streams;
+ break;
+ }
+ for (int n = 0; n < count; n++) {
+ BLURAY_STREAM_INFO *i = &si[n];
+ if (i->pid == req->id) {
+ snprintf(req->name, sizeof(req->name), "%.4s", i->lang);
+ return STREAM_OK;
+ }
+ }
+ }
+ return STREAM_ERROR;
+ }
+ case STREAM_CTRL_GET_DISC_NAME: {
+ const struct meta_dl *meta = bd_get_meta(b->bd);
+ if (!meta || !meta->di_name || !meta->di_name[0])
+ break;
+ *(char**)arg = talloc_strdup(NULL, meta->di_name);
+ return STREAM_OK;
+ }
+ default:
+ break;
+ }
+
+ return STREAM_UNSUPPORTED;
+}
+
+static const char *aacs_strerr(int err)
+{
+ switch (err) {
+ case AACS_ERROR_CORRUPTED_DISC: return "opening or reading of AACS files failed";
+ case AACS_ERROR_NO_CONFIG: return "missing config file";
+ case AACS_ERROR_NO_PK: return "no matching processing key";
+ case AACS_ERROR_NO_CERT: return "no valid certificate";
+ case AACS_ERROR_CERT_REVOKED: return "certificate has been revoked";
+ case AACS_ERROR_MMC_OPEN: return "MMC open failed (maybe no MMC drive?)";
+ case AACS_ERROR_MMC_FAILURE: return "MMC failed";
+ case AACS_ERROR_NO_DK: return "no matching device key";
+ default: return "unknown error";
+ }
+}
+
+static bool check_disc_info(stream_t *s)
+{
+ struct bluray_priv_s *b = s->priv;
+ const BLURAY_DISC_INFO *info = bd_get_disc_info(b->bd);
+
+ // check Blu-ray
+ if (!info->bluray_detected) {
+ MP_ERR(s, "Given stream is not a Blu-ray.\n");
+ return false;
+ }
+
+ // check AACS
+ if (info->aacs_detected) {
+ if (!info->libaacs_detected) {
+ MP_ERR(s, "AACS encryption detected but cannot find libaacs.\n");
+ return false;
+ }
+ if (!info->aacs_handled) {
+ MP_ERR(s, "AACS error: %s\n", aacs_strerr(info->aacs_error_code));
+ return false;
+ }
+ }
+
+ // check BD+
+ if (info->bdplus_detected) {
+ if (!info->libbdplus_detected) {
+ MP_ERR(s, "BD+ encryption detected but cannot find libbdplus.\n");
+ return false;
+ }
+ if (!info->bdplus_handled) {
+ MP_ERR(s, "Cannot decrypt BD+ encryption.\n");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void select_initial_title(stream_t *s, int title_guess) {
+ struct bluray_priv_s *b = s->priv;
+
+ if (b->cfg_title == BLURAY_PLAYLIST_TITLE) {
+ if (!play_playlist(b, b->cfg_playlist))
+ MP_WARN(s, "Couldn't start playlist '%05d'.\n", b->cfg_playlist);
+ b->current_title = bd_get_current_title(b->bd);
+ } else {
+ int title = -1;
+ if (b->cfg_title != BLURAY_DEFAULT_TITLE )
+ title = b->cfg_title;
+ else
+ title = title_guess;
+ if (title < 0)
+ return;
+
+ if (play_title(b, title))
+ b->current_title = title;
+ else {
+ MP_WARN(s, "Couldn't start title '%d'.\n", title);
+ b->current_title = bd_get_current_title(b->bd);
+ }
+ }
+}
+
+static int bluray_stream_open_internal(stream_t *s)
+{
+ struct bluray_priv_s *b = s->priv;
+
+ char *device = NULL;
+ /* find the requested device */
+ if (b->cfg_device && b->cfg_device[0]) {
+ device = b->cfg_device;
+ } else {
+ device = b->opts->bluray_device;
+ }
+
+ if (!device || !device[0]) {
+ MP_ERR(s, "No Blu-ray device/location was specified ...\n");
+ return STREAM_UNSUPPORTED;
+ }
+
+ if (!mp_msg_test(s->log, MSGL_DEBUG))
+ bd_set_debug_mask(0);
+
+ /* open device */
+ BLURAY *bd = bd_open(device, NULL);
+ if (!bd) {
+ MP_ERR(s, "Couldn't open Blu-ray device: %s\n", device);
+ return STREAM_UNSUPPORTED;
+ }
+ b->bd = bd;
+
+ if (!check_disc_info(s)) {
+ destruct(b);
+ return STREAM_UNSUPPORTED;
+ }
+
+ int title_guess = BLURAY_DEFAULT_TITLE;
+ if (b->use_nav) {
+ MP_FATAL(s, "BluRay menu support has been removed.\n");
+ return STREAM_ERROR;
+ } else {
+ /* check for available titles on disc */
+ b->num_titles = bd_get_titles(bd, TITLES_RELEVANT, 0);
+ if (!b->num_titles) {
+ MP_ERR(s, "Can't find any Blu-ray-compatible title here.\n");
+ destruct(b);
+ return STREAM_UNSUPPORTED;
+ }
+
+ MP_INFO(s, "List of available titles:\n");
+
+ /* parse titles information */
+ uint64_t max_duration = 0;
+ for (int i = 0; i < b->num_titles; i++) {
+ BLURAY_TITLE_INFO *ti = bd_get_title_info(bd, i, 0);
+ if (!ti)
+ continue;
+
+ char *time = mp_format_time(ti->duration / 90000, false);
+ MP_INFO(s, "idx: %3d duration: %s (playlist: %05d.mpls)\n",
+ i, time, ti->playlist);
+ talloc_free(time);
+
+ /* try to guess which title may contain the main movie */
+ if (ti->duration > max_duration) {
+ max_duration = ti->duration;
+ title_guess = i;
+ }
+
+ bd_free_title_info(ti);
+ }
+ }
+
+ // these should be set before any callback
+ b->current_angle = -1;
+ b->current_title = -1;
+
+ // initialize libbluray event queue
+ bd_get_event(bd, NULL);
+
+ select_initial_title(s, title_guess);
+
+ s->fill_buffer = bluray_stream_fill_buffer;
+ s->close = bluray_stream_close;
+ s->control = bluray_stream_control;
+ s->priv = b;
+ s->demuxer = "+disc";
+
+ MP_VERBOSE(s, "Blu-ray successfully opened.\n");
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_bdnav;
+
+static int bluray_stream_open(stream_t *s)
+{
+ struct bluray_priv_s *b = talloc_zero(s, struct bluray_priv_s);
+ s->priv = b;
+
+ struct m_config_cache *opts_cache =
+ m_config_cache_alloc(s, s->global, &stream_bluray_conf);
+
+ b->opts_cache = opts_cache;
+ b->opts = opts_cache->opts;
+
+ b->use_nav = s->info == &stream_info_bdnav;
+
+ bstr title, bdevice, rest = { .len = 0 };
+ bstr_split_tok(bstr0(s->path), "/", &title, &bdevice);
+
+ b->cfg_title = BLURAY_DEFAULT_TITLE;
+
+ if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) {
+ b->cfg_title = BLURAY_DEFAULT_TITLE;
+ } else if (bstr_equals0(title, "menu")) {
+ b->cfg_title = BLURAY_MENU_TITLE;
+ } else if (bstr_equals0(title, "mpls")) {
+ bstr_split_tok(bdevice, "/", &title, &bdevice);
+ long long pl = bstrtoll(title, &rest, 10);
+ if (rest.len) {
+ MP_ERR(s, "number expected: '%.*s'\n", BSTR_P(rest));
+ return STREAM_ERROR;
+ } else if (pl < 0 || 99999 < pl) {
+ MP_ERR(s, "invalid playlist: '%.*s', must be in the range 0-99999\n",
+ BSTR_P(title));
+ return STREAM_ERROR;
+ }
+ b->cfg_playlist = pl;
+ b->cfg_title = BLURAY_PLAYLIST_TITLE;
+ } else if (title.len) {
+ long long t = bstrtoll(title, &rest, 10);
+ if (rest.len) {
+ MP_ERR(s, "number expected: '%.*s'\n", BSTR_P(rest));
+ return STREAM_ERROR;
+ } else if (t < 0 || 99999 < t) {
+ MP_ERR(s, "invalid title: '%.*s', must be in the range 0-99999\n",
+ BSTR_P(title));
+ return STREAM_ERROR;
+ }
+ b->cfg_title = t;
+ }
+
+ b->cfg_device = bstrto0(b, bdevice);
+
+ return bluray_stream_open_internal(s);
+}
+
+const stream_info_t stream_info_bluray = {
+ .name = "bd",
+ .open = bluray_stream_open,
+ .protocols = (const char*const[]){ "bd", "br", "bluray", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
+
+const stream_info_t stream_info_bdnav = {
+ .name = "bdnav",
+ .open = bluray_stream_open,
+ .protocols = (const char*const[]){ "bdnav", "brnav", "bluraynav", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
+
+static bool check_bdmv(const char *path)
+{
+ if (strcasecmp(mp_basename(path), "MovieObject.bdmv"))
+ return false;
+
+ FILE *temp = fopen(path, "rb");
+ if (!temp)
+ return false;
+
+ char data[50] = {0};
+
+ fread(data, 50, 1, temp);
+ fclose(temp);
+
+ bstr bdata = {data, 50};
+
+ return bstr_startswith0(bdata, "MOBJ0100") || // AVCHD
+ bstr_startswith0(bdata, "MOBJ0200") || // Blu-ray
+ bstr_startswith0(bdata, "MOBJ0300"); // UHD BD
+}
+
+// Destructively remove the current trailing path component.
+static void remove_prefix(char *path)
+{
+ size_t len = strlen(path);
+#if HAVE_DOS_PATHS
+ const char *seps = "/\\";
+#else
+ const char *seps = "/";
+#endif
+ while (len > 0 && !strchr(seps, path[len - 1]))
+ len--;
+ while (len > 0 && strchr(seps, path[len - 1]))
+ len--;
+ path[len] = '\0';
+}
+
+static int bdmv_dir_stream_open(stream_t *stream)
+{
+ struct bluray_priv_s *priv = talloc_ptrtype(stream, priv);
+ stream->priv = priv;
+ *priv = (struct bluray_priv_s){
+ .cfg_title = BLURAY_DEFAULT_TITLE,
+ };
+
+ if (!stream->access_references)
+ goto unsupported;
+
+ char *path = mp_file_get_path(priv, bstr0(stream->url));
+ if (!path)
+ goto unsupported;
+
+ // We allow the path to point to a directory containing BDMV/, a
+ // directory containing MovieObject.bdmv, or that file itself.
+ if (!check_bdmv(path)) {
+ // On UNIX, just assume the filename has always this case.
+ char *npath = mp_path_join(priv, path, "MovieObject.bdmv");
+ if (!check_bdmv(npath)) {
+ npath = mp_path_join(priv, path, "BDMV/MovieObject.bdmv");
+ if (!check_bdmv(npath))
+ goto unsupported;
+ }
+ path = npath;
+ }
+
+ // Go up by 2 levels.
+ remove_prefix(path);
+ remove_prefix(path);
+ priv->cfg_device = path;
+ if (strlen(priv->cfg_device) <= 1)
+ goto unsupported;
+
+ MP_INFO(stream, "BDMV detected. Redirecting to bluray://\n");
+ return bluray_stream_open_internal(stream);
+
+unsupported:
+ talloc_free(priv);
+ stream->priv = NULL;
+ return STREAM_UNSUPPORTED;
+}
+
+const stream_info_t stream_info_bdmv_dir = {
+ .name = "bdmv/bluray",
+ .open = bdmv_dir_stream_open,
+ .protocols = (const char*const[]){ "file", "", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_cb.c b/stream/stream_cb.c
new file mode 100644
index 0000000..29e5563
--- /dev/null
+++ b/stream/stream_cb.c
@@ -0,0 +1,108 @@
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "stream.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "player/client.h"
+#include "libmpv/stream_cb.h"
+#include "misc/thread_tools.h"
+
+struct priv {
+ mpv_stream_cb_info info;
+ struct mp_cancel *cancel;
+};
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ struct priv *p = s->priv;
+ return (int)p->info.read_fn(p->info.cookie, buffer, (size_t)max_len);
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ return p->info.seek_fn(p->info.cookie, newpos) >= 0;
+}
+
+static int64_t get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+
+ if (p->info.size_fn) {
+ int64_t size = p->info.size_fn(p->info.cookie);
+ if (size >= 0)
+ return size;
+ }
+
+ return -1;
+}
+
+static void s_close(stream_t *s)
+{
+ struct priv *p = s->priv;
+ p->info.close_fn(p->info.cookie);
+}
+
+static int open_cb(stream_t *stream)
+{
+ struct priv *p = talloc_ptrtype(stream, p);
+ stream->priv = p;
+
+ bstr bproto = mp_split_proto(bstr0(stream->url), NULL);
+ char *proto = bstrto0(stream, bproto);
+
+ void *user_data;
+ mpv_stream_cb_open_ro_fn open_fn;
+
+ if (!mp_streamcb_lookup(stream->global, proto, &user_data, &open_fn))
+ return STREAM_UNSUPPORTED;
+
+ mpv_stream_cb_info info = {0};
+
+ int r = open_fn(user_data, stream->url, &info);
+ if (r < 0) {
+ if (r != MPV_ERROR_LOADING_FAILED)
+ MP_WARN(stream, "unknown error from user callback\n");
+ return STREAM_ERROR;
+ }
+
+ if (!info.read_fn || !info.close_fn) {
+ MP_FATAL(stream, "required read_fn or close_fn callbacks not set.\n");
+ return STREAM_ERROR;
+ }
+
+ p->info = info;
+
+ if (p->info.seek_fn && p->info.seek_fn(p->info.cookie, 0) >= 0) {
+ stream->seek = seek;
+ stream->seekable = true;
+ }
+ stream->fast_skip = true;
+ stream->fill_buffer = fill_buffer;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ if (p->info.cancel_fn && stream->cancel) {
+ p->cancel = mp_cancel_new(p);
+ mp_cancel_set_parent(p->cancel, stream->cancel);
+ mp_cancel_set_cb(p->cancel, p->info.cancel_fn, p->info.cookie);
+ }
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_cb = {
+ .name = "stream_callback",
+ .open = open_cb,
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_cdda.c b/stream/stream_cdda.c
new file mode 100644
index 0000000..71ae461
--- /dev/null
+++ b/stream/stream_cdda.c
@@ -0,0 +1,369 @@
+/*
+ * Original author: Albeu
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <cdio/cdio.h>
+
+// For cdio_cddap_version
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wstrict-prototypes"
+#include <cdio/paranoia/cdda.h>
+#include <cdio/paranoia/paranoia.h>
+#pragma GCC diagnostic pop
+
+#include "common/msg.h"
+#include "config.h"
+#include "mpv_talloc.h"
+
+#include "stream.h"
+#include "options/m_option.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+typedef struct cdda_params {
+ cdrom_drive_t *cd;
+ cdrom_paranoia_t *cdp;
+ int sector;
+ int start_sector;
+ int end_sector;
+ uint8_t *data;
+ size_t data_pos;
+
+ // options
+ char *cdda_device;
+ int speed;
+ int paranoia_mode;
+ int sector_size;
+ int search_overlap;
+ int toc_bias;
+ int toc_offset;
+ bool skip;
+ char *device;
+ int span[2];
+ bool cdtext;
+} cdda_priv;
+
+#define OPT_BASE_STRUCT struct cdda_params
+const struct m_sub_options stream_cdda_conf = {
+ .opts = (const m_option_t[]) {
+ {"device", OPT_STRING(cdda_device), .flags = M_OPT_FILE},
+ {"speed", OPT_INT(speed), M_RANGE(1, 100)},
+ {"paranoia", OPT_INT(paranoia_mode), M_RANGE(0, 2)},
+ {"sector-size", OPT_INT(sector_size), M_RANGE(1, 100)},
+ {"overlap", OPT_INT(search_overlap), M_RANGE(0, 75)},
+ {"toc-bias", OPT_INT(toc_bias),
+ .deprecation_message = "toc-bias is no longer used"},
+ {"toc-offset", OPT_INT(toc_offset)},
+ {"skip", OPT_BOOL(skip)},
+ {"span-a", OPT_INT(span[0])},
+ {"span-b", OPT_INT(span[1])},
+ {"cdtext", OPT_BOOL(cdtext)},
+ {0}
+ },
+ .size = sizeof(struct cdda_params),
+ .defaults = &(const struct cdda_params){
+ .search_overlap = -1,
+ .skip = true,
+ },
+};
+
+static const char *const cdtext_name[] = {
+ [CDTEXT_FIELD_ARRANGER] = "Arranger",
+ [CDTEXT_FIELD_COMPOSER] = "Composer",
+ [CDTEXT_FIELD_MESSAGE] = "Message",
+ [CDTEXT_FIELD_ISRC] = "ISRC",
+ [CDTEXT_FIELD_PERFORMER] = "Performer",
+ [CDTEXT_FIELD_SONGWRITER] = "Songwriter",
+ [CDTEXT_FIELD_TITLE] = "Title",
+ [CDTEXT_FIELD_UPC_EAN] = "UPC_EAN",
+};
+
+static void print_cdtext(stream_t *s, int track)
+{
+ cdda_priv* p = (cdda_priv*)s->priv;
+ if (!p->cdtext)
+ return;
+ cdtext_t *text = cdio_get_cdtext(p->cd->p_cdio);
+ int header = 0;
+ if (text) {
+ for (int i = 0; i < sizeof(cdtext_name) / sizeof(cdtext_name[0]); i++) {
+ const char *name = cdtext_name[i];
+ const char *value = cdtext_get_const(text, i, track);
+ if (name && value) {
+ if (!header)
+ MP_INFO(s, "CD-Text (%s):\n", track ? "track" : "CD");
+ header = 1;
+ MP_INFO(s, " %s: '%s'\n", name, value);
+ }
+ }
+ }
+}
+
+static void print_track_info(stream_t *s, int track)
+{
+ MP_INFO(s, "Switched to track %d\n", track);
+ print_cdtext(s, track);
+}
+
+static void cdparanoia_callback(long int inpos, paranoia_cb_mode_t function)
+{
+}
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ cdda_priv *p = (cdda_priv *)s->priv;
+
+ if (!p->data || p->data_pos >= CDIO_CD_FRAMESIZE_RAW) {
+ if ((p->sector < p->start_sector) || (p->sector > p->end_sector))
+ return 0;
+
+ p->data_pos = 0;
+ p->data = (uint8_t *)paranoia_read(p->cdp, cdparanoia_callback);
+ if (!p->data)
+ return 0;
+
+ p->sector++;
+ }
+
+ size_t copy = MPMIN(CDIO_CD_FRAMESIZE_RAW - p->data_pos, max_len);
+ memcpy(buffer, p->data + p->data_pos, copy);
+ p->data_pos += copy;
+ return copy;
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ cdda_priv *p = (cdda_priv *)s->priv;
+ int sec;
+ int current_track = 0, seeked_track = 0;
+ int seek_to_track = 0;
+ int i;
+
+ newpos += p->start_sector * CDIO_CD_FRAMESIZE_RAW;
+
+ sec = newpos / CDIO_CD_FRAMESIZE_RAW;
+ if (newpos < 0 || sec > p->end_sector) {
+ p->sector = p->end_sector + 1;
+ return 0;
+ }
+
+ for (i = 0; i < p->cd->tracks; i++) {
+ if (p->sector >= p->cd->disc_toc[i].dwStartSector
+ && p->sector < p->cd->disc_toc[i + 1].dwStartSector)
+ current_track = i;
+ if (sec >= p->cd->disc_toc[i].dwStartSector
+ && sec < p->cd->disc_toc[i + 1].dwStartSector)
+ {
+ seeked_track = i;
+ seek_to_track = sec == p->cd->disc_toc[i].dwStartSector;
+ }
+ }
+ if (current_track != seeked_track && !seek_to_track)
+ print_track_info(s, seeked_track + 1);
+
+ p->sector = sec;
+
+ paranoia_seek(p->cdp, sec, SEEK_SET);
+ return 1;
+}
+
+static void close_cdda(stream_t *s)
+{
+ cdda_priv *p = (cdda_priv *)s->priv;
+ paranoia_free(p->cdp);
+ cdda_close(p->cd);
+}
+
+static int get_track_by_sector(cdda_priv *p, unsigned int sector)
+{
+ int i;
+ for (i = p->cd->tracks; i >= 0; --i)
+ if (p->cd->disc_toc[i].dwStartSector <= sector)
+ break;
+ return i;
+}
+
+static int control(stream_t *stream, int cmd, void *arg)
+{
+ cdda_priv *p = stream->priv;
+ switch (cmd) {
+ case STREAM_CTRL_GET_NUM_CHAPTERS:
+ {
+ int start_track = get_track_by_sector(p, p->start_sector);
+ int end_track = get_track_by_sector(p, p->end_sector);
+ if (start_track == -1 || end_track == -1)
+ return STREAM_ERROR;
+ *(unsigned int *)arg = end_track + 1 - start_track;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CHAPTER_TIME:
+ {
+ int track = *(double *)arg;
+ int start_track = get_track_by_sector(p, p->start_sector);
+ int end_track = get_track_by_sector(p, p->end_sector);
+ track += start_track;
+ if (track > end_track)
+ return STREAM_ERROR;
+ int64_t sector = p->cd->disc_toc[track].dwStartSector;
+ int64_t pos = sector * CDIO_CD_FRAMESIZE_RAW;
+ // Assume standard audio CD: 44.1khz, 2 channels, s16 samples
+ *(double *)arg = pos / (44100.0 * 2 * 2);
+ return STREAM_OK;
+ }
+ }
+ return STREAM_UNSUPPORTED;
+}
+
+static int64_t get_size(stream_t *st)
+{
+ cdda_priv *p = st->priv;
+ return (p->end_sector + 1 - p->start_sector) * CDIO_CD_FRAMESIZE_RAW;
+}
+
+static int open_cdda(stream_t *st)
+{
+ st->priv = mp_get_config_group(st, st->global, &stream_cdda_conf);
+ cdda_priv *priv = st->priv;
+ cdda_priv *p = priv;
+ int mode = p->paranoia_mode;
+ int offset = p->toc_offset;
+ cdrom_drive_t *cdd = NULL;
+ int last_track;
+
+ if (st->path[0]) {
+ p->device = st->path;
+ } else if (p->cdda_device && p->cdda_device[0]) {
+ p->device = p->cdda_device;
+ } else {
+ p->device = DEFAULT_CDROM_DEVICE;
+ }
+
+#if defined(__NetBSD__)
+ cdd = cdda_identify_scsi(p->device, p->device, 0, NULL);
+#else
+ cdd = cdda_identify(p->device, 0, NULL);
+#endif
+
+ if (!cdd) {
+ MP_ERR(st, "Can't open CDDA device.\n");
+ return STREAM_ERROR;
+ }
+
+ cdda_verbose_set(cdd, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
+
+ if (p->sector_size)
+ cdd->nsectors = p->sector_size;
+
+ if (cdda_open(cdd) != 0) {
+ MP_ERR(st, "Can't open disc.\n");
+ cdda_close(cdd);
+ return STREAM_ERROR;
+ }
+
+ priv->cd = cdd;
+
+ offset -= cdda_track_firstsector(cdd, 1);
+ if (offset) {
+ for (int n = 0; n < cdd->tracks + 1; n++)
+ cdd->disc_toc[n].dwStartSector += offset;
+ }
+
+ if (p->speed > 0)
+ cdda_speed_set(cdd, p->speed);
+
+ last_track = cdda_tracks(cdd);
+ if (p->span[0] > last_track)
+ p->span[0] = last_track;
+ if (p->span[1] < p->span[0])
+ p->span[1] = p->span[0];
+ if (p->span[1] > last_track)
+ p->span[1] = last_track;
+ if (p->span[0])
+ priv->start_sector = cdda_track_firstsector(cdd, p->span[0]);
+ else
+ priv->start_sector = cdda_disc_firstsector(cdd);
+
+ if (p->span[1])
+ priv->end_sector = cdda_track_lastsector(cdd, p->span[1]);
+ else
+ priv->end_sector = cdda_disc_lastsector(cdd);
+
+ priv->cdp = paranoia_init(cdd);
+ if (priv->cdp == NULL) {
+ cdda_close(cdd);
+ free(priv);
+ return STREAM_ERROR;
+ }
+
+ if (mode == 0)
+ mode = PARANOIA_MODE_DISABLE;
+ else if (mode == 1)
+ mode = PARANOIA_MODE_OVERLAP;
+ else
+ mode = PARANOIA_MODE_FULL;
+
+ if (p->skip)
+ mode &= ~PARANOIA_MODE_NEVERSKIP;
+ else
+ mode |= PARANOIA_MODE_NEVERSKIP;
+
+ if (p->search_overlap > 0)
+ mode |= PARANOIA_MODE_OVERLAP;
+ else if (p->search_overlap == 0)
+ mode &= ~PARANOIA_MODE_OVERLAP;
+
+ paranoia_modeset(priv->cdp, mode);
+
+ if (p->search_overlap > 0)
+ paranoia_overlapset(priv->cdp, p->search_overlap);
+
+ paranoia_seek(priv->cdp, priv->start_sector, SEEK_SET);
+ priv->sector = priv->start_sector;
+
+ st->priv = priv;
+
+ st->fill_buffer = fill_buffer;
+ st->seek = seek;
+ st->seekable = true;
+ st->control = control;
+ st->get_size = get_size;
+ st->close = close_cdda;
+
+ st->streaming = true;
+
+ st->demuxer = "+disc";
+
+ print_cdtext(st, 0);
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_cdda = {
+ .name = "cdda",
+ .open = open_cdda,
+ .protocols = (const char*const[]){"cdda", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_concat.c b/stream/stream_concat.c
new file mode 100644
index 0000000..d06bd4a
--- /dev/null
+++ b/stream/stream_concat.c
@@ -0,0 +1,179 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/common.h>
+
+#include "common/common.h"
+#include "stream.h"
+
+struct priv {
+ struct stream **streams;
+ int num_streams;
+
+ int64_t size;
+
+ int cur; // streams[cur] is the stream for current stream.pos
+};
+
+static int fill_buffer(struct stream *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+
+ while (1) {
+ int res = stream_read_partial(p->streams[p->cur], buffer, len);
+ if (res || p->cur == p->num_streams - 1)
+ return res;
+
+ p->cur += 1;
+ if (s->seekable)
+ stream_seek(p->streams[p->cur], 0);
+ }
+}
+
+static int seek(struct stream *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+
+ int64_t next_pos = 0;
+ int64_t base_pos = 0;
+
+ // Look for the last stream whose start position is <= newpos.
+ // Note that the last stream's size is essentially ignored. The last
+ // stream is allowed to have an unknown size.
+ for (int n = 0; n < p->num_streams; n++) {
+ if (next_pos > newpos)
+ break;
+
+ base_pos = next_pos;
+ p->cur = n;
+
+ int64_t size = stream_get_size(p->streams[n]);
+ if (size < 0)
+ break;
+
+ next_pos = base_pos + size;
+ }
+
+ int ok = stream_seek(p->streams[p->cur], newpos - base_pos);
+ s->pos = base_pos + stream_tell(p->streams[p->cur]);
+ return ok;
+}
+
+static int64_t get_size(struct stream *s)
+{
+ struct priv *p = s->priv;
+ return p->size;
+}
+
+static void s_close(struct stream *s)
+{
+ struct priv *p = s->priv;
+
+ for (int n = 0; n < p->num_streams; n++)
+ free_stream(p->streams[n]);
+}
+
+// Return the "worst" origin value of the two. cur can be 0 to mean unset.
+static int combine_origin(int cur, int new)
+{
+ if (cur == STREAM_ORIGIN_UNSAFE || new == STREAM_ORIGIN_UNSAFE)
+ return STREAM_ORIGIN_UNSAFE;
+ if (cur == STREAM_ORIGIN_NET || new == STREAM_ORIGIN_NET)
+ return STREAM_ORIGIN_NET;
+ if (cur == STREAM_ORIGIN_FS || new == STREAM_ORIGIN_FS)
+ return STREAM_ORIGIN_FS;
+ return new; // including cur==0
+}
+
+static int open2(struct stream *stream, const struct stream_open_args *args)
+{
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ stream->fill_buffer = fill_buffer;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ stream->seekable = true;
+
+ struct priv *list = args->special_arg;
+ if (!list || !list->num_streams) {
+ MP_FATAL(stream, "No sub-streams.\n");
+ return STREAM_ERROR;
+ }
+
+ stream->stream_origin = 0;
+
+ for (int n = 0; n < list->num_streams; n++) {
+ struct stream *sub = list->streams[n];
+
+ int64_t size = stream_get_size(sub);
+ if (n != list->num_streams - 1 && size < 0) {
+ MP_WARN(stream, "Sub stream %d has unknown size.\n", n);
+ stream->seekable = false;
+ p->size = -1;
+ } else if (size >= 0 && p->size >= 0) {
+ p->size += size;
+ }
+
+ if (!sub->seekable)
+ stream->seekable = false;
+
+ stream->stream_origin =
+ combine_origin(stream->stream_origin, sub->stream_origin);
+
+ MP_TARRAY_APPEND(p, p->streams, p->num_streams, sub);
+ }
+
+ if (stream->seekable)
+ stream->seek = seek;
+
+ return STREAM_OK;
+}
+
+static const stream_info_t stream_info_concat = {
+ .name = "concat",
+ .open2 = open2,
+ .protocols = (const char*const[]){ "concat", NULL },
+};
+
+// Create a stream with a concatenated view on streams[]. Copies the streams
+// array. Takes over ownership of every stream passed to it (it will free them
+// if the concat stream is closed).
+// If an error happens, NULL is returned, and the streams are not freed.
+struct stream *stream_concat_open(struct mpv_global *global, struct mp_cancel *c,
+ struct stream **streams, int num_streams)
+{
+ // (struct priv is blatantly abused to pass in the stream list)
+ struct priv arg = {
+ .streams = streams,
+ .num_streams = num_streams,
+ };
+
+ struct stream_open_args sargs = {
+ .global = global,
+ .cancel = c,
+ .url = "concat://",
+ .flags = STREAM_READ | STREAM_SILENT | STREAM_ORIGIN_DIRECT,
+ .sinfo = &stream_info_concat,
+ .special_arg = &arg,
+ };
+
+ struct stream *s = NULL;
+ stream_create_with_args(&sargs, &s);
+ return s;
+}
diff --git a/stream/stream_dvb.c b/stream/stream_dvb.c
new file mode 100644
index 0000000..215e82c
--- /dev/null
+++ b/stream/stream_dvb.c
@@ -0,0 +1,1161 @@
+/*
+
+ dvbstream
+ (C) Dave Chapman <dave@dchapman.com> 2001, 2002.
+ (C) Rozhuk Ivan <rozhuk.im@gmail.com> 2016 - 2017
+
+ Original authors: Nico, probably Arpi
+
+ Some code based on dvbstream, 0.4.3-pre3 (CVS checkout),
+ http://sourceforge.net/projects/dvbtools/
+
+ Modified for use with MPlayer, for details see the changelog at
+ http://svn.mplayerhq.hu/mplayer/trunk/
+ $Id$
+
+ Copyright notice:
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+#include "config.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <poll.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "osdep/io.h"
+#include "misc/ctype.h"
+#include "osdep/timer.h"
+
+#include "stream.h"
+#include "common/tags.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "osdep/threads.h"
+
+#include "dvbin.h"
+#include "dvb_tune.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define CHANNEL_LINE_LEN 256
+
+#define OPT_BASE_STRUCT dvb_opts_t
+
+static dvb_state_t *global_dvb_state = NULL;
+static mp_static_mutex global_dvb_state_lock = MP_STATIC_MUTEX_INITIALIZER;
+
+const struct m_sub_options stream_dvb_conf = {
+ .opts = (const m_option_t[]) {
+ {"prog", OPT_STRING(cfg_prog), .flags = UPDATE_DVB_PROG},
+ {"card", OPT_INT(cfg_devno), M_RANGE(0, MAX_ADAPTERS-1)},
+ {"timeout", OPT_INT(cfg_timeout), M_RANGE(1, 30)},
+ {"file", OPT_STRING(cfg_file), .flags = M_OPT_FILE},
+ {"full-transponder", OPT_BOOL(cfg_full_transponder)},
+ {"channel-switch-offset", OPT_INT(cfg_channel_switch_offset),
+ .flags = UPDATE_DVB_PROG},
+ {0}
+ },
+ .size = sizeof(dvb_opts_t),
+ .defaults = &(const dvb_opts_t){
+ .cfg_prog = NULL,
+ .cfg_timeout = 30,
+ },
+};
+
+void dvbin_close(stream_t *stream);
+
+static fe_code_rate_t parse_fec(const char *cr)
+{
+ if (!strcmp(cr, "FEC_1_2")) {
+ return FEC_1_2;
+ } else if (!strcmp(cr, "FEC_2_3")) {
+ return FEC_2_3;
+ } else if (!strcmp(cr, "FEC_3_4")) {
+ return FEC_3_4;
+ } else if (!strcmp(cr, "FEC_4_5")) {
+ return FEC_4_5;
+ } else if (!strcmp(cr, "FEC_5_6")) {
+ return FEC_5_6;
+ } else if (!strcmp(cr, "FEC_6_7")) {
+ return FEC_6_7;
+ } else if (!strcmp(cr, "FEC_7_8")) {
+ return FEC_7_8;
+ } else if (!strcmp(cr, "FEC_8_9")) {
+ return FEC_8_9;
+ } else if (!strcmp(cr, "FEC_NONE")) {
+ return FEC_NONE;
+ }
+ return FEC_NONE;
+}
+
+static fe_modulation_t parse_vdr_modulation(const char** modstring)
+{
+ const static struct { const char *s; fe_modulation_t v; } table[] = {
+ { "16", QAM_16 },
+ { "32", QAM_32 },
+ { "64", QAM_64 },
+ { "128", QAM_128 },
+ { "256", QAM_256 },
+ { "998", QAM_AUTO },
+ { "2", QPSK },
+ { "5", PSK_8 },
+ { "6", APSK_16 },
+ { "7", APSK_32 },
+ { "10", VSB_8 },
+ { "11", VSB_16 },
+ { "12", DQPSK },
+ };
+ for (int i = 0; i < MP_ARRAY_SIZE(table); i++) {
+ if (!strncmp(*modstring, table[i].s, strlen(table[i].s))) {
+ *modstring += strlen(table[i].s);
+ return table[i].v;
+ }
+ }
+ return QAM_AUTO;
+}
+
+static void parse_vdr_par_string(const char *vdr_par_str, dvb_channel_t *ptr)
+{
+ //FIXME: There is more information in this parameter string, especially related
+ // to non-DVB-S reception.
+ if (!vdr_par_str[0])
+ return;
+ const char *vdr_par = &vdr_par_str[0];
+ while (vdr_par && *vdr_par) {
+ switch (mp_toupper(*vdr_par)) {
+ case 'H':
+ ptr->pol = 'H';
+ vdr_par++;
+ break;
+ case 'V':
+ ptr->pol = 'V';
+ vdr_par++;
+ break;
+ case 'S':
+ vdr_par++;
+ ptr->is_dvb_x2 = *vdr_par == '1';
+ vdr_par++;
+ break;
+ case 'P':
+ vdr_par++;
+ char *endptr = NULL;
+ errno = 0;
+ int n = strtol(vdr_par, &endptr, 10);
+ if (!errno && endptr != vdr_par) {
+ ptr->stream_id = n;
+ vdr_par = endptr;
+ }
+ break;
+ case 'I':
+ vdr_par++;
+ ptr->inv = (*vdr_par == '1') ? INVERSION_ON : INVERSION_OFF;
+ vdr_par++;
+ break;
+ case 'M':
+ vdr_par++;
+ ptr->mod = parse_vdr_modulation(&vdr_par);
+ break;
+ default:
+ vdr_par++;
+ }
+ }
+}
+
+static char *dvb_strtok_r(char *s, const char *sep, char **p)
+{
+ if (!s && !(s = *p))
+ return NULL;
+
+ /* Skip leading separators. */
+ s += strspn(s, sep);
+
+ /* s points at first non-separator, or end of string. */
+ if (!*s)
+ return *p = 0;
+
+ /* Move *p to next separator. */
+ *p = s + strcspn(s, sep);
+ if (**p) {
+ *(*p)++ = 0;
+ } else {
+ *p = 0;
+ }
+ return s;
+}
+
+static bool parse_pid_string(struct mp_log *log, char *pid_string,
+ dvb_channel_t *ptr)
+{
+ if (!pid_string[0])
+ return false;
+ int pcnt = 0;
+ /* These tokens also catch vdr-style PID lists.
+ * They can contain 123=deu@3,124=eng+jap@4;125
+ * 3 and 4 are codes for codec type, =langLeft+langRight is allowed,
+ * and ; may separate a dolby channel.
+ * With the numChars-test and the full token-list, all is handled
+ * gracefully.
+ */
+ const char *tokens = "+,;";
+ char *pidPart;
+ char *savePtr = NULL;
+ pidPart = dvb_strtok_r(pid_string, tokens, &savePtr);
+ while (pidPart != NULL) {
+ if (ptr->pids_cnt >= DMX_FILTER_SIZE - 1) {
+ mp_verbose(log, "Maximum number of PIDs for one channel "
+ "reached, ignoring further ones!\n");
+ break;
+ }
+ int numChars = 0;
+ int pid = 0;
+ pcnt += sscanf(pidPart, "%d%n", &pid, &numChars);
+ if (numChars > 0) {
+ ptr->pids[ptr->pids_cnt] = pid;
+ ptr->pids_cnt++;
+ }
+ pidPart = dvb_strtok_r(NULL, tokens, &savePtr);
+ }
+ return pcnt > 0;
+}
+
+static dvb_channels_list_t *dvb_get_channels(struct mp_log *log,
+ dvb_channels_list_t *list_add,
+ int cfg_full_transponder,
+ char *filename,
+ unsigned int frontend,
+ int delsys, unsigned int delsys_mask)
+{
+ dvb_channels_list_t *list = list_add;
+
+ if (!filename)
+ return list;
+
+ const char *cbl_conf =
+ "%d:%255[^:]:%d:%255[^:]:%255[^:]:%255[^:]:%255[^:]\n";
+ const char *sat_conf = "%d:%c:%d:%d:%255[^:]:%255[^:]\n";
+ const char *ter_conf =
+ "%d:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]\n";
+ const char *atsc_conf = "%d:%255[^:]:%255[^:]:%255[^:]\n";
+ const char *vdr_conf =
+ "%d:%255[^:]:%255[^:]:%d:%255[^:]:%255[^:]:%255[^:]:%*255[^:]:%d:%*d:%*d:%*d\n%n";
+
+ mp_verbose(log, "Reading config file %s for type %s\n",
+ filename, get_dvb_delsys(delsys));
+ FILE *f = fopen(filename, "r");
+ if (!f) {
+ mp_fatal(log, "Can't open file %s\n", filename);
+ return list;
+ }
+
+ if (!list)
+ list = talloc_zero(NULL, dvb_channels_list_t);
+
+ while (!feof(f)) {
+ char line[CHANNEL_LINE_LEN];
+ if (!fgets(line, CHANNEL_LINE_LEN, f))
+ continue;
+
+ if (line[0] == '#' || strlen(line) == 0)
+ continue;
+
+ dvb_channel_t chn = {0};
+ dvb_channel_t *ptr = &chn;
+
+ char tmp_lcr[256], tmp_hier[256], inv[256], bw[256], cr[256], mod[256],
+ transm[256], gi[256], vpid_str[256], apid_str[256], tpid_str[256],
+ vdr_par_str[256], vdr_loc_str[256];
+
+ vpid_str[0] = apid_str[0] = tpid_str[0] = 0;
+ vdr_loc_str[0] = vdr_par_str[0] = 0;
+
+ char *colon = strchr(line, ':');
+ if (!colon)
+ continue;
+ int k = colon - line;
+ if (!k)
+ continue;
+ // In some modern VDR-style configs, channel name also has bouquet after ;.
+ // Parse that off, we ignore it.
+ char *bouquet_sep = strchr(line, ';');
+ {
+ int namelen = k;
+ if (bouquet_sep && bouquet_sep < colon)
+ namelen = bouquet_sep - line;
+ ptr->name = talloc_strndup(list, line, namelen);
+ }
+
+ k++;
+
+ ptr->pids_cnt = 0;
+ ptr->freq = 0;
+ ptr->service_id = -1;
+ ptr->is_dvb_x2 = false;
+ ptr->frontend = frontend;
+ ptr->delsys = delsys;
+ ptr->diseqc = 0;
+ ptr->stream_id = NO_STREAM_ID_FILTER;
+ ptr->inv = INVERSION_AUTO;
+ ptr->bw = BANDWIDTH_AUTO;
+ ptr->cr = FEC_AUTO;
+ ptr->cr_lp = FEC_AUTO;
+ ptr->mod = QAM_AUTO;
+ ptr->hier = HIERARCHY_AUTO;
+ ptr->gi = GUARD_INTERVAL_AUTO;
+ ptr->trans = TRANSMISSION_MODE_AUTO;
+
+ // Check if VDR-type channels.conf-line - then full line is consumed by the scan.
+ int fields, num_chars = 0;
+ fields = sscanf(&line[k], vdr_conf,
+ &ptr->freq, vdr_par_str, vdr_loc_str, &ptr->srate,
+ vpid_str, apid_str, tpid_str, &ptr->service_id,
+ &num_chars);
+
+ bool is_vdr_conf = (num_chars == strlen(&line[k]));
+
+ // Special case: DVB-T style ZAP config also has 13 columns.
+ // Most columns should have non-numeric content, but some channel list generators insert 0
+ // if a value is not used. However, INVERSION_* should always set after frequency.
+ if (is_vdr_conf && !strncmp(vdr_par_str, "INVERSION_", 10)) {
+ is_vdr_conf = false;
+ }
+
+ if (is_vdr_conf) {
+ // Modulation parsed here, not via old xine-parsing path.
+ mod[0] = '\0';
+ // It's a VDR-style config line.
+ parse_vdr_par_string(vdr_par_str, ptr);
+ // Frequency in VDR-style config files is in MHz for DVB-S,
+ // and may be in MHz, kHz or Hz for DVB-C and DVB-T.
+ // General rule to get useful units is to multiply by 1000 until value is larger than 1000000.
+ while (ptr->freq < 1000000U) {
+ ptr->freq *= 1000U;
+ }
+ // Symbol rate in VDR-style config files is divided by 1000.
+ ptr->srate *= 1000U;
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ /* Fix delsys value. */
+ if (ptr->is_dvb_x2) {
+ ptr->delsys = delsys = SYS_DVBT2;
+ } else {
+ ptr->delsys = delsys = SYS_DVBT;
+ }
+ if (!DELSYS_IS_SET(delsys_mask, delsys))
+ continue; /* Skip channel. */
+ mp_verbose(log, "VDR, %s, NUM: %d, NUM_FIELDS: %d, NAME: %s, "
+ "FREQ: %d, SRATE: %d, T2: %s\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields,
+ ptr->name, ptr->freq, ptr->srate,
+ (delsys == SYS_DVBT2) ? "yes" : "no");
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ case SYS_ISDBT:
+ mp_verbose(log, "VDR, %s, NUM: %d, NUM_FIELDS: %d, NAME: %s, "
+ "FREQ: %d, SRATE: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields,
+ ptr->name, ptr->freq, ptr->srate);
+ break;
+ case SYS_DVBS:
+ case SYS_DVBS2:
+ /* Fix delsys value. */
+ if (ptr->is_dvb_x2) {
+ ptr->delsys = delsys = SYS_DVBS2;
+ } else {
+ ptr->delsys = delsys = SYS_DVBS;
+ }
+ if (!DELSYS_IS_SET(delsys_mask, delsys))
+ continue; /* Skip channel. */
+
+ if (vdr_loc_str[0]) {
+ // In older vdr config format, this field contained the DISEQc information.
+ // If it is numeric, assume that's it.
+ int diseqc_info = 0;
+ int valid_digits = 0;
+ if (sscanf(vdr_loc_str, "%d%n", &diseqc_info,
+ &valid_digits) == 1)
+ {
+ if (valid_digits == strlen(vdr_loc_str)) {
+ ptr->diseqc = (unsigned int)diseqc_info;
+ if (ptr->diseqc > 4)
+ continue;
+ if (ptr->diseqc > 0)
+ ptr->diseqc--;
+ }
+ }
+ }
+
+ mp_verbose(log, "VDR, %s, NUM: %d, NUM_FIELDS: %d, NAME: %s, "
+ "FREQ: %d, SRATE: %d, POL: %c, DISEQC: %d, S2: %s, "
+ "StreamID: %d, SID: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS,
+ fields, ptr->name, ptr->freq, ptr->srate, ptr->pol,
+ ptr->diseqc, (delsys == SYS_DVBS2) ? "yes" : "no",
+ ptr->stream_id, ptr->service_id);
+ break;
+ default:
+ break;
+ }
+ } else {
+ // ZAP style channel config file.
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ fields = sscanf(&line[k], ter_conf,
+ &ptr->freq, inv, bw, cr, tmp_lcr, mod,
+ transm, gi, tmp_hier, vpid_str, apid_str);
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d\n",
+ get_dvb_delsys(delsys), list->NUM_CHANNELS,
+ fields, ptr->name, ptr->freq);
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ fields = sscanf(&line[k], cbl_conf,
+ &ptr->freq, inv, &ptr->srate,
+ cr, mod, vpid_str, apid_str);
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d, "
+ "SRATE: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields, ptr->name,
+ ptr->freq, ptr->srate);
+ break;
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ fields = sscanf(&line[k], atsc_conf,
+ &ptr->freq, mod, vpid_str, apid_str);
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d\n",
+ get_dvb_delsys(delsys), list->NUM_CHANNELS,
+ fields, ptr->name, ptr->freq);
+ break;
+ case SYS_DVBS:
+ case SYS_DVBS2:
+ fields = sscanf(&line[k], sat_conf,
+ &ptr->freq, &ptr->pol, &ptr->diseqc, &ptr->srate,
+ vpid_str,
+ apid_str);
+ ptr->pol = mp_toupper(ptr->pol);
+ ptr->freq *= 1000U;
+ ptr->srate *= 1000U;
+ if (ptr->diseqc > 4)
+ continue;
+ if (ptr->diseqc > 0)
+ ptr->diseqc--;
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d, "
+ "SRATE: %d, POL: %c, DISEQC: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields, ptr->name, ptr->freq,
+ ptr->srate, ptr->pol, ptr->diseqc);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (parse_pid_string(log, vpid_str, ptr))
+ fields++;
+ if (parse_pid_string(log, apid_str, ptr))
+ fields++;
+ /* If we do not know the service_id, PMT can not be extracted.
+ Teletext decoding will fail without PMT. */
+ if (ptr->service_id != -1) {
+ if (parse_pid_string(log, tpid_str, ptr))
+ fields++;
+ }
+
+
+ if (fields < 2 || ptr->pids_cnt == 0 || ptr->freq == 0 ||
+ strlen(ptr->name) == 0)
+ continue;
+
+ /* Add some PIDs which are mandatory in DVB,
+ * and contain human-readable helpful data. */
+
+ /* This is the STD, the service description table.
+ * It contains service names and such, ffmpeg decodes it. */
+ ptr->pids[ptr->pids_cnt] = 0x0011;
+ ptr->pids_cnt++;
+
+ /* This is the EIT, which contains EPG data.
+ * ffmpeg can not decode it (yet), but e.g. VLC
+ * shows what was recorded. */
+ ptr->pids[ptr->pids_cnt] = 0x0012;
+ ptr->pids_cnt++;
+
+ if (ptr->service_id != -1) {
+ /* We have the PMT-PID in addition.
+ This will be found later, when we tune to the channel.
+ Push back here to create the additional demux. */
+ ptr->pids[ptr->pids_cnt] = -1; // Placeholder.
+ ptr->pids_cnt++;
+ }
+
+ bool has8192 = false, has0 = false;
+ for (int cnt = 0; cnt < ptr->pids_cnt; cnt++) {
+ if (ptr->pids[cnt] == 8192)
+ has8192 = true;
+ if (ptr->pids[cnt] == 0)
+ has0 = true;
+ }
+
+ /* 8192 is the pseudo-PID for full TP dump,
+ enforce that if requested. */
+ if (!has8192 && cfg_full_transponder)
+ has8192 = 1;
+ if (has8192) {
+ ptr->pids[0] = 8192;
+ ptr->pids_cnt = 1;
+ } else if (!has0) {
+ ptr->pids[ptr->pids_cnt] = 0; //PID 0 is the PAT
+ ptr->pids_cnt++;
+ }
+
+ mp_verbose(log, " PIDS: ");
+ for (int cnt = 0; cnt < ptr->pids_cnt; cnt++)
+ mp_verbose(log, " %d ", ptr->pids[cnt]);
+ mp_verbose(log, "\n");
+
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ if (!strcmp(inv, "INVERSION_ON")) {
+ ptr->inv = INVERSION_ON;
+ } else if (!strcmp(inv, "INVERSION_OFF")) {
+ ptr->inv = INVERSION_OFF;
+ }
+
+ ptr->cr = parse_fec(cr);
+ }
+
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ if (!strcmp(mod, "QAM_128")) {
+ ptr->mod = QAM_128;
+ } else if (!strcmp(mod, "QAM_256")) {
+ ptr->mod = QAM_256;
+ } else if (!strcmp(mod, "QAM_64")) {
+ ptr->mod = QAM_64;
+ } else if (!strcmp(mod, "QAM_32")) {
+ ptr->mod = QAM_32;
+ } else if (!strcmp(mod, "QAM_16")) {
+ ptr->mod = QAM_16;
+ } else if (!strcmp(mod, "VSB_8") || !strcmp(mod, "8VSB")) {
+ ptr->mod = VSB_8;
+ } else if (!strcmp(mod, "VSB_16") || !strcmp(mod, "16VSB")) {
+ ptr->mod = VSB_16;
+ }
+ }
+
+ /* Modulation defines real delsys for ATSC:
+ Terrestrial (VSB) is SYS_ATSC, Cable (QAM) is SYS_DVBC_ANNEX_B. */
+ if (delsys == SYS_ATSC || delsys == SYS_DVBC_ANNEX_B) {
+ if (ptr->mod == VSB_8 || ptr->mod == VSB_16) {
+ delsys = SYS_ATSC;
+ } else {
+ delsys = SYS_DVBC_ANNEX_B;
+ }
+ if (!DELSYS_IS_SET(delsys_mask, delsys))
+ continue; /* Skip channel. */
+ mp_verbose(log, "Switched to delivery system for ATSC: %s (guessed from modulation)\n",
+ get_dvb_delsys(delsys));
+ }
+
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ if (!strcmp(bw, "BANDWIDTH_5_MHZ")) {
+ ptr->bw = BANDWIDTH_5_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_6_MHZ")) {
+ ptr->bw = BANDWIDTH_6_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_7_MHZ")) {
+ ptr->bw = BANDWIDTH_7_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_8_MHZ")) {
+ ptr->bw = BANDWIDTH_8_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_10_MHZ")) {
+ ptr->bw = BANDWIDTH_10_MHZ;
+ }
+
+
+ if (!strcmp(transm, "TRANSMISSION_MODE_2K")) {
+ ptr->trans = TRANSMISSION_MODE_2K;
+ } else if (!strcmp(transm, "TRANSMISSION_MODE_8K")) {
+ ptr->trans = TRANSMISSION_MODE_8K;
+ } else if (!strcmp(transm, "TRANSMISSION_MODE_AUTO")) {
+ ptr->trans = TRANSMISSION_MODE_AUTO;
+ }
+
+ if (!strcmp(gi, "GUARD_INTERVAL_1_32")) {
+ ptr->gi = GUARD_INTERVAL_1_32;
+ } else if (!strcmp(gi, "GUARD_INTERVAL_1_16")) {
+ ptr->gi = GUARD_INTERVAL_1_16;
+ } else if (!strcmp(gi, "GUARD_INTERVAL_1_8")) {
+ ptr->gi = GUARD_INTERVAL_1_8;
+ } else if (!strcmp(gi, "GUARD_INTERVAL_1_4")) {
+ ptr->gi = GUARD_INTERVAL_1_4;
+ }
+
+ ptr->cr_lp = parse_fec(tmp_lcr);
+
+ if (!strcmp(tmp_hier, "HIERARCHY_1")) {
+ ptr->hier = HIERARCHY_1;
+ } else if (!strcmp(tmp_hier, "HIERARCHY_2")) {
+ ptr->hier = HIERARCHY_2;
+ } else if (!strcmp(tmp_hier, "HIERARCHY_4")) {
+ ptr->hier = HIERARCHY_4;
+ } else if (!strcmp(tmp_hier, "HIERARCHY_NONE")) {
+ ptr->hier = HIERARCHY_NONE;
+ }
+ }
+
+ MP_TARRAY_APPEND(list, list->channels, list->NUM_CHANNELS, *ptr);
+ }
+
+ fclose(f);
+ if (list->NUM_CHANNELS == 0)
+ TA_FREEP(&list);
+
+ return list;
+}
+
+static int dvb_streaming_read(stream_t *stream, void *buffer, int size)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+ int pos = 0;
+ int tries = state->retry;
+ const int fd = state->dvr_fd;
+
+ MP_TRACE(stream, "dvb_streaming_read(%d)\n", size);
+
+ struct pollfd pfds[1];
+ pfds[0].fd = fd;
+ pfds[0].events = POLLIN | POLLPRI;
+
+ while (pos < size) {
+ int rk = read(fd, (char *)buffer + pos, (size - pos));
+ if (rk <= 0) {
+ if (pos || tries == 0)
+ break;
+ tries--;
+ if (poll(pfds, 1, 2000) <= 0) {
+ MP_ERR(stream, "dvb_streaming_read: failed with "
+ "errno %d when reading %d bytes\n", errno, size - pos);
+ errno = 0;
+ break;
+ }
+ continue;
+ }
+ pos += rk;
+ MP_TRACE(stream, "got %d bytes\n", pos);
+ }
+
+ if (!pos)
+ MP_ERR(stream, "dvb_streaming_read: returning 0 bytes\n");
+
+ // Check if config parameters have been updated.
+ dvb_update_config(stream);
+
+ return pos;
+}
+
+int dvb_set_channel(stream_t *stream, unsigned int adapter, unsigned int n)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ assert(adapter < state->adapters_count);
+ int devno = state->adapters[adapter].devno;
+ dvb_channels_list_t *new_list = state->adapters[adapter].list;
+ assert(n < new_list->NUM_CHANNELS);
+ dvb_channel_t *channel = &(new_list->channels[n]);
+
+ if (state->is_on) { //the fds are already open and we have to stop the demuxers
+ /* Remove all demuxes. */
+ dvb_fix_demuxes(priv, 0);
+
+ state->retry = 0;
+ // empty both the stream's and driver's buffer
+ char buf[4096];
+ while (dvb_streaming_read(stream, buf, sizeof(buf)) > 0) {}
+
+ if (state->cur_adapter != adapter ||
+ state->cur_frontend != channel->frontend) {
+ dvbin_close(stream);
+ if (!dvb_open_devices(priv, devno, channel->frontend, channel->pids_cnt)) {
+ MP_ERR(stream, "dvb_set_channel: couldn't open devices of adapter "
+ "%d\n", devno);
+ return 0;
+ }
+ } else {
+ // close all demux_fds with pos > pids required for the new channel
+ // or open other demux_fds if we have too few
+ if (!dvb_fix_demuxes(priv, channel->pids_cnt))
+ return 0;
+ }
+ } else {
+ if (!dvb_open_devices(priv, devno, channel->frontend, channel->pids_cnt)) {
+ MP_ERR(stream, "dvb_set_channel: couldn't open devices of adapter "
+ "%d\n", devno);
+ return 0;
+ }
+ }
+
+ state->retry = 5;
+ new_list->current = n;
+ MP_VERBOSE(stream, "dvb_set_channel: new channel name=\"%s\", adapter: %d, "
+ "channel: %d\n", channel->name, devno, n);
+
+ if (channel->freq != state->last_freq) {
+ if (!dvb_tune(priv, channel->delsys, channel->freq,
+ channel->pol, channel->srate, channel->diseqc,
+ channel->stream_id, channel->inv,
+ channel->mod, channel->gi,
+ channel->trans, channel->bw, channel->cr, channel->cr_lp,
+ channel->hier, priv->opts->cfg_timeout))
+ return 0;
+ }
+
+ state->is_on = true;
+ state->last_freq = channel->freq;
+ state->cur_adapter = adapter;
+ state->cur_frontend = channel->frontend;
+
+ if (channel->service_id != -1) {
+ /* We need the PMT-PID in addition.
+ If it has not yet beem resolved, do it now. */
+ for (int i = 0; i < channel->pids_cnt; i++) {
+ if (channel->pids[i] == -1) {
+ MP_VERBOSE(stream, "dvb_set_channel: PMT-PID for service %d "
+ "not resolved yet, parsing PAT...\n",
+ channel->service_id);
+ int pmt_pid = dvb_get_pmt_pid(priv, adapter, channel->service_id);
+ MP_VERBOSE(stream, "found PMT-PID: %d\n", pmt_pid);
+ channel->pids[i] = pmt_pid;
+ break;
+ }
+ }
+ }
+
+ // sets demux filters and restart the stream
+ for (int i = 0; i < channel->pids_cnt; i++) {
+ if (channel->pids[i] == -1) {
+ // In case PMT was not resolved, skip it here.
+ MP_ERR(stream, "dvb_set_channel: PMT-PID not found, "
+ "teletext decoding may fail.\n");
+ continue;
+ }
+ if (!dvb_set_ts_filt(priv, state->demux_fds[i], channel->pids[i],
+ DMX_PES_OTHER))
+ return 0;
+ }
+
+ return 1;
+}
+
+static int dvbin_stream_control(struct stream *s, int cmd, void *arg)
+{
+ dvb_priv_t *priv = s->priv;
+ dvb_state_t *state = priv->state;
+
+ if (state->cur_adapter >= state->adapters_count)
+ return STREAM_ERROR;
+ dvb_channels_list_t *list = state->adapters[state->cur_adapter].list;
+
+ switch (cmd) {
+ case STREAM_CTRL_GET_METADATA: {
+ struct mp_tags *metadata = talloc_zero(NULL, struct mp_tags);
+ char *progname = list->channels[list->current].name;
+ mp_tags_set_str(metadata, "title", progname);
+ *(struct mp_tags **)arg = metadata;
+ return STREAM_OK;
+ }
+ }
+ return STREAM_UNSUPPORTED;
+}
+
+void dvbin_close(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ if (state->switching_channel && state->is_on) {
+ // Prevent state destruction, reset channel-switch.
+ state->switching_channel = false;
+ mp_mutex_lock(&global_dvb_state_lock);
+ global_dvb_state->stream_used = false;
+ mp_mutex_unlock(&global_dvb_state_lock);
+ return;
+ }
+
+ for (int i = state->demux_fds_cnt - 1; i >= 0; i--) {
+ state->demux_fds_cnt--;
+ close(state->demux_fds[i]);
+ }
+ close(state->dvr_fd);
+ close(state->fe_fd);
+ state->fe_fd = state->dvr_fd = -1;
+
+ state->is_on = false;
+ state->cur_adapter = -1;
+ state->cur_frontend = -1;
+
+ mp_mutex_lock(&global_dvb_state_lock);
+ TA_FREEP(&global_dvb_state);
+ mp_mutex_unlock(&global_dvb_state_lock);
+}
+
+static int dvb_streaming_start(stream_t *stream, char *progname)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ if (!progname)
+ return 0;
+
+ dvb_channels_list_t *list = state->adapters[state->cur_adapter].list;
+ dvb_channel_t *channel = NULL;
+ int i;
+ for (i = 0; i < list->NUM_CHANNELS; i ++) {
+ if (!strcmp(list->channels[i].name, progname)) {
+ channel = &(list->channels[i]);
+ break;
+ }
+ }
+
+ if (!channel) {
+ MP_ERR(stream, "no such channel \"%s\"\n", progname);
+ return 0;
+ }
+ list->current = i;
+
+ // When switching channels, cfg_channel_switch_offset
+ // keeps the offset to the initially chosen channel.
+ list->current = (list->NUM_CHANNELS + list->current +
+ priv->opts->cfg_channel_switch_offset) % list->NUM_CHANNELS;
+ channel = &(list->channels[list->current]);
+ MP_INFO(stream, "Tuning to channel \"%s\"...\n", channel->name);
+ MP_VERBOSE(stream, "Program number %d: name=\"%s\", freq=%u\n", i,
+ channel->name, channel->freq);
+
+ if (!dvb_set_channel(stream, state->cur_adapter, list->current)) {
+ dvbin_close(stream);
+ return 0;
+ }
+
+ return 1;
+}
+
+void dvb_update_config(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ // Throttle the check to at maximum once every 0.1 s.
+ int now = (int)(mp_time_sec()*10);
+ if (now == priv->opts_check_time)
+ return;
+ priv->opts_check_time = now;
+
+ if (!m_config_cache_update(priv->opts_cache))
+ return;
+
+ // Re-parse stream path, if we have cfg parameters now,
+ // these should be preferred.
+ if (!dvb_parse_path(stream)) {
+ MP_ERR(stream, "error parsing DVB config, not tuning.");
+ return;
+ }
+
+ int r = dvb_streaming_start(stream, priv->prog);
+ if (r) {
+ // Stream will be pulled down after channel switch,
+ // persist state.
+ state->switching_channel = true;
+ }
+}
+
+static int dvb_open(stream_t *stream)
+{
+ dvb_priv_t *priv = NULL;
+
+ mp_mutex_lock(&global_dvb_state_lock);
+ if (global_dvb_state && global_dvb_state->stream_used) {
+ MP_ERR(stream, "DVB stream already in use, only one DVB stream can exist at a time!\n");
+ mp_mutex_unlock(&global_dvb_state_lock);
+ goto err_out;
+ }
+
+ // Need to re-get config in any case, not part of global state.
+ stream->priv = talloc_zero(stream, dvb_priv_t);
+ priv = stream->priv;
+ priv->opts_cache = m_config_cache_alloc(stream, stream->global, &stream_dvb_conf);
+ priv->opts = priv->opts_cache->opts;
+
+ dvb_state_t *state = dvb_get_state(stream);
+
+ priv->state = state;
+ priv->log = stream->log;
+ if (!state) {
+ MP_ERR(stream, "DVB configuration is empty\n");
+ mp_mutex_unlock(&global_dvb_state_lock);
+ goto err_out;
+ }
+
+ if (!dvb_parse_path(stream)) {
+ mp_mutex_unlock(&global_dvb_state_lock);
+ goto err_out;
+ }
+
+ state->stream_used = true;
+ mp_mutex_unlock(&global_dvb_state_lock);
+
+ if (!state->is_on) {
+ // State could be already initialized, for example, we just did a channel switch.
+ // The following setup only has to be done once.
+
+ state->cur_frontend = -1;
+
+ if (!dvb_streaming_start(stream, priv->prog))
+ goto err_out;
+ }
+
+ stream->fill_buffer = dvb_streaming_read;
+ stream->close = dvbin_close;
+ stream->control = dvbin_stream_control;
+ stream->streaming = true;
+ stream->demuxer = "lavf";
+ stream->lavf_type = "mpegts";
+
+ return STREAM_OK;
+
+err_out:
+ talloc_free(priv);
+ stream->priv = NULL;
+ return STREAM_ERROR;
+}
+
+int dvb_parse_path(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ // Parse stream path. Common rule: cfg wins over stream path,
+ // since cfg may be changed at runtime.
+ bstr prog, devno;
+ if (!bstr_split_tok(bstr0(stream->path), "@", &devno, &prog)) {
+ prog = devno;
+ devno.len = 0;
+ }
+
+ if (priv->opts->cfg_devno != 0) {
+ priv->devno = priv->opts->cfg_devno;
+ } else if (devno.len) {
+ bstr r;
+ priv->devno = bstrtoll(devno, &r, 0);
+ if (r.len || priv->devno < 0 || priv->devno >= MAX_ADAPTERS) {
+ MP_ERR(stream, "invalid devno: '%.*s'\n", BSTR_P(devno));
+ return 0;
+ }
+ } else {
+ // Default to the default of cfg_devno.
+ priv->devno = priv->opts->cfg_devno;
+ }
+
+ // Current adapter is derived from devno.
+ state->cur_adapter = -1;
+ for (int i = 0; i < state->adapters_count; i++) {
+ if (state->adapters[i].devno == priv->devno) {
+ state->cur_adapter = i;
+ break;
+ }
+ }
+
+ if (state->cur_adapter == -1) {
+ MP_ERR(stream, "No configuration found for adapter %d!\n",
+ priv->devno);
+ return 0;
+ }
+
+ char *new_prog = NULL;
+ if (priv->opts->cfg_prog != NULL && strlen(priv->opts->cfg_prog) > 0) {
+ new_prog = talloc_strdup(priv, priv->opts->cfg_prog);
+ } else if (prog.len) {
+ new_prog = bstrto0(priv, prog);
+ } else {
+ // We use the first program from the channel list.
+ dvb_channels_list_t *list = state->adapters[state->cur_adapter].list;
+ if (!list) {
+ MP_ERR(stream, "No channel list available for adapter %d!\n", priv->devno);
+ return 0;
+ }
+ new_prog = talloc_strdup(priv, list->channels[0].name);
+ }
+ talloc_free(priv->prog);
+ priv->prog = new_prog;
+
+ MP_VERBOSE(stream, "dvb_config: prog=\"%s\", devno=%d\n",
+ priv->prog, priv->devno);
+ return 1;
+}
+
+dvb_state_t *dvb_get_state(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ if (global_dvb_state)
+ return global_dvb_state;
+
+ struct mp_log *log = stream->log;
+ struct mpv_global *global = stream->global;
+
+ dvb_state_t *state = talloc_zero(NULL, dvb_state_t);
+ state->switching_channel = false;
+ state->is_on = false;
+ state->stream_used = true;
+ state->fe_fd = state->dvr_fd = -1;
+
+ for (unsigned int i = 0; i < MAX_ADAPTERS; i++) {
+ dvb_channels_list_t *list = NULL;
+ unsigned int delsys_mask[MAX_FRONTENDS];
+ for (unsigned int f = 0; f < MAX_FRONTENDS; f++) {
+ char filename[100];
+ snprintf(filename, sizeof(filename), "/dev/dvb/adapter%u/frontend%u", i, f);
+ int fd = open(filename, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (fd < 0)
+ continue;
+
+ delsys_mask[f] = dvb_get_tuner_delsys_mask(fd, log);
+ delsys_mask[f] &= DELSYS_SUPP_MASK; /* Filter unsupported delivery systems. */
+ close(fd);
+ if (delsys_mask[f] == 0) {
+ mp_verbose(log, "Frontend device %s has no supported delivery systems.\n",
+ filename);
+ continue; /* Skip tuner. */
+ }
+
+ /* Create channel list for adapter. */
+ for (unsigned int delsys = 0; delsys < SYS_DVB__COUNT__; delsys++) {
+ if (!DELSYS_IS_SET(delsys_mask[f], delsys))
+ continue; /* Skip unsupported. */
+
+ mp_verbose(log, "Searching channel list for delivery system %s\n", get_dvb_delsys(delsys));
+ const char *conf_file_name;
+ switch (delsys) {
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ conf_file_name = "channels.conf.cbl";
+ break;
+ case SYS_ATSC:
+ conf_file_name = "channels.conf.atsc";
+ break;
+ case SYS_DVBT:
+ if (DELSYS_IS_SET(delsys_mask[f], SYS_DVBT2))
+ continue; /* Add all channels later with T2. */
+ conf_file_name = "channels.conf.ter";
+ break;
+ case SYS_DVBT2:
+ conf_file_name = "channels.conf.ter";
+ break;
+ case SYS_ISDBT:
+ conf_file_name = "channels.conf.isdbt";
+ break;
+ case SYS_DVBS:
+ if (DELSYS_IS_SET(delsys_mask[f], SYS_DVBS2))
+ continue; /* Add all channels later with S2. */
+ conf_file_name = "channels.conf.sat";
+ break;
+ case SYS_DVBS2:
+ conf_file_name = "channels.conf.sat";
+ break;
+ default:
+ continue;
+ }
+
+ void *talloc_ctx = NULL;
+ char *conf_file;
+ if (priv->opts->cfg_file && priv->opts->cfg_file[0]) {
+ conf_file = priv->opts->cfg_file;
+ } else {
+ talloc_ctx = talloc_new(NULL);
+ conf_file = mp_find_config_file(talloc_ctx, global, conf_file_name);
+ if (conf_file) {
+ mp_verbose(log, "Ignoring other channels.conf files.\n");
+ } else {
+ conf_file = mp_find_config_file(talloc_ctx, global,
+ "channels.conf");
+ }
+ }
+
+ list = dvb_get_channels(log, list, priv->opts->cfg_full_transponder,
+ conf_file, f, delsys, delsys_mask[f]);
+ talloc_free(talloc_ctx);
+ }
+ }
+ /* Add adapter with non zero channel list. */
+ if (!list)
+ continue;
+
+ dvb_adapter_config_t tmp = {
+ .devno = i,
+ .list = talloc_steal(state, list),
+ };
+ memcpy(&tmp.delsys_mask, delsys_mask, sizeof(delsys_mask));
+
+ MP_TARRAY_APPEND(state, state->adapters, state->adapters_count, tmp);
+
+ mp_verbose(log, "Added adapter with channels to state list, now %d.\n",
+ state->adapters_count);
+ }
+
+ if (state->adapters_count == 0)
+ TA_FREEP(&state);
+
+ global_dvb_state = state;
+ return state;
+}
+
+const stream_info_t stream_info_dvb = {
+ .name = "dvbin",
+ .open = dvb_open,
+ .protocols = (const char *const[]){ "dvb", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c
new file mode 100644
index 0000000..d858c51
--- /dev/null
+++ b/stream/stream_dvdnav.c
@@ -0,0 +1,719 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <strings.h>
+#include <errno.h>
+#include <assert.h>
+
+#ifdef __linux__
+#include <linux/cdrom.h>
+#include <scsi/sg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#endif
+
+#include <dvdnav/dvdnav.h>
+#include <libavutil/common.h>
+#include <libavutil/intreadwrite.h>
+
+#include "osdep/io.h"
+
+#include "options/options.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "osdep/timer.h"
+#include "stream.h"
+#include "demux/demux.h"
+#include "video/out/vo.h"
+
+#define TITLE_MENU -1
+#define TITLE_LONGEST -2
+
+struct priv {
+ dvdnav_t *dvdnav; // handle to libdvdnav stuff
+ char *filename; // path
+ unsigned int duration; // in milliseconds
+ int mousex, mousey;
+ int title;
+ uint32_t spu_clut[16];
+ bool spu_clut_valid;
+ bool had_initial_vts;
+
+ int dvd_speed;
+
+ int track;
+ char *device;
+
+ struct dvd_opts *opts;
+};
+
+#define DNE(e) [e] = # e
+static const char *const mp_dvdnav_events[] = {
+ DNE(DVDNAV_BLOCK_OK),
+ DNE(DVDNAV_NOP),
+ DNE(DVDNAV_STILL_FRAME),
+ DNE(DVDNAV_SPU_STREAM_CHANGE),
+ DNE(DVDNAV_AUDIO_STREAM_CHANGE),
+ DNE(DVDNAV_VTS_CHANGE),
+ DNE(DVDNAV_CELL_CHANGE),
+ DNE(DVDNAV_NAV_PACKET),
+ DNE(DVDNAV_STOP),
+ DNE(DVDNAV_HIGHLIGHT),
+ DNE(DVDNAV_SPU_CLUT_CHANGE),
+ DNE(DVDNAV_HOP_CHANNEL),
+ DNE(DVDNAV_WAIT),
+};
+
+#define LOOKUP_NAME(array, i) \
+ (((i) >= 0 && (i) < MP_ARRAY_SIZE(array)) ? array[(i)] : "?")
+
+static void dvd_set_speed(stream_t *stream, char *device, unsigned speed)
+{
+#if defined(__linux__) && defined(SG_IO) && defined(GPCMD_SET_STREAMING)
+ int fd;
+ unsigned char buffer[28];
+ unsigned char cmd[12];
+ struct sg_io_hdr sghdr;
+ struct stat st;
+
+ memset(&st, 0, sizeof(st));
+
+ if (stat(device, &st) == -1) return;
+
+ if (!S_ISBLK(st.st_mode)) return; /* not a block device */
+
+ switch (speed) {
+ case 0: /* don't touch speed setting */
+ return;
+ case -1: /* restore default value */
+ MP_INFO(stream, "Restoring DVD speed... ");
+ break;
+ default: /* limit to <speed> KB/s */
+ // speed < 100 is multiple of DVD single speed (1350KB/s)
+ if (speed < 100)
+ speed *= 1350;
+ MP_INFO(stream, "Limiting DVD speed to %dKB/s... ", speed);
+ break;
+ }
+
+ memset(&sghdr, 0, sizeof(sghdr));
+ sghdr.interface_id = 'S';
+ sghdr.timeout = 5000;
+ sghdr.dxfer_direction = SG_DXFER_TO_DEV;
+ sghdr.dxfer_len = sizeof(buffer);
+ sghdr.dxferp = buffer;
+ sghdr.cmd_len = sizeof(cmd);
+ sghdr.cmdp = cmd;
+
+ memset(cmd, 0, sizeof(cmd));
+ cmd[0] = GPCMD_SET_STREAMING;
+ cmd[10] = sizeof(buffer);
+
+ memset(buffer, 0, sizeof(buffer));
+ /* first sector 0, last sector 0xffffffff */
+ AV_WB32(buffer + 8, 0xffffffff);
+ if (speed == -1)
+ buffer[0] = 4; /* restore default */
+ else {
+ /* <speed> kilobyte */
+ AV_WB32(buffer + 12, speed);
+ AV_WB32(buffer + 20, speed);
+ }
+ /* 1 second */
+ AV_WB16(buffer + 18, 1000);
+ AV_WB16(buffer + 26, 1000);
+
+ fd = open(device, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (fd == -1) {
+ MP_INFO(stream, "Couldn't open DVD device for writing, changing DVD speed needs write access.\n");
+ return;
+ }
+
+ if (ioctl(fd, SG_IO, &sghdr) < 0)
+ MP_INFO(stream, "failed\n");
+ else
+ MP_INFO(stream, "successful\n");
+
+ close(fd);
+#endif
+}
+
+// Check if this is likely to be an .ifo or similar file.
+static int dvd_probe(const char *path, const char *ext, const char *sig)
+{
+ if (!bstr_case_endswith(bstr0(path), bstr0(ext)))
+ return false;
+
+ FILE *temp = fopen(path, "rb");
+ if (!temp)
+ return false;
+
+ bool r = false;
+
+ char data[50];
+
+ assert(strlen(sig) <= sizeof(data));
+
+ if (fread(data, 50, 1, temp) == 1) {
+ if (memcmp(data, sig, strlen(sig)) == 0)
+ r = true;
+ }
+
+ fclose(temp);
+ return r;
+}
+
+/**
+ * \brief mp_dvdnav_lang_from_aid() returns the language corresponding to audio id 'aid'
+ * \param stream: - stream pointer
+ * \param sid: physical subtitle id
+ * \return 0 on error, otherwise language id
+ */
+static int mp_dvdnav_lang_from_aid(stream_t *stream, int aid)
+{
+ uint8_t lg;
+ uint16_t lang;
+ struct priv *priv = stream->priv;
+
+ if (aid < 0)
+ return 0;
+ lg = dvdnav_get_audio_logical_stream(priv->dvdnav, aid & 0x7);
+ if (lg == 0xff)
+ return 0;
+ lang = dvdnav_audio_stream_to_lang(priv->dvdnav, lg);
+ if (lang == 0xffff)
+ return 0;
+ return lang;
+}
+
+/**
+ * \brief mp_dvdnav_lang_from_sid() returns the language corresponding to subtitle id 'sid'
+ * \param stream: - stream pointer
+ * \param sid: physical subtitle id
+ * \return 0 on error, otherwise language id
+ */
+static int mp_dvdnav_lang_from_sid(stream_t *stream, int sid)
+{
+ uint8_t k;
+ uint16_t lang;
+ struct priv *priv = stream->priv;
+ if (sid < 0)
+ return 0;
+ for (k = 0; k < 32; k++)
+ if (dvdnav_get_spu_logical_stream(priv->dvdnav, k) == sid)
+ break;
+ if (k == 32)
+ return 0;
+ lang = dvdnav_spu_stream_to_lang(priv->dvdnav, k);
+ if (lang == 0xffff)
+ return 0;
+ return lang;
+}
+
+/**
+ * \brief mp_dvdnav_number_of_subs() returns the count of available subtitles
+ * \param stream: - stream pointer
+ * \return 0 on error, something meaningful otherwise
+ */
+static int mp_dvdnav_number_of_subs(stream_t *stream)
+{
+ struct priv *priv = stream->priv;
+ uint8_t lg, k, n = 0;
+
+ for (k = 0; k < 32; k++) {
+ lg = dvdnav_get_spu_logical_stream(priv->dvdnav, k);
+ if (lg == 0xff)
+ continue;
+ if (lg >= n)
+ n = lg + 1;
+ }
+ return n;
+}
+
+static int fill_buffer(stream_t *s, void *buf, int max_len)
+{
+ struct priv *priv = s->priv;
+ dvdnav_t *dvdnav = priv->dvdnav;
+
+ if (max_len < 2048) {
+ MP_FATAL(s, "Short read size. Data corruption will follow. Please "
+ "provide a patch.\n");
+ return -1;
+ }
+
+ while (1) {
+ int len = -1;
+ int event = DVDNAV_NOP;
+ if (dvdnav_get_next_block(dvdnav, buf, &event, &len) != DVDNAV_STATUS_OK)
+ {
+ MP_ERR(s, "Error getting next block from DVD %d (%s)\n",
+ event, dvdnav_err_to_string(dvdnav));
+ return 0;
+ }
+ if (event != DVDNAV_BLOCK_OK) {
+ const char *name = LOOKUP_NAME(mp_dvdnav_events, event);
+ MP_TRACE(s, "DVDNAV: event %s (%d).\n", name, event);
+ }
+ switch (event) {
+ case DVDNAV_BLOCK_OK:
+ return len;
+ case DVDNAV_STOP:
+ return 0;
+ case DVDNAV_NAV_PACKET: {
+ pci_t *pnavpci = dvdnav_get_current_nav_pci(dvdnav);
+ uint32_t start_pts = pnavpci->pci_gi.vobu_s_ptm;
+ MP_TRACE(s, "start pts = %"PRIu32"\n", start_pts);
+ break;
+ }
+ case DVDNAV_STILL_FRAME:
+ dvdnav_still_skip(dvdnav);
+ return 0;
+ case DVDNAV_WAIT:
+ dvdnav_wait_skip(dvdnav);
+ return 0;
+ case DVDNAV_HIGHLIGHT:
+ break;
+ case DVDNAV_VTS_CHANGE: {
+ int tit = 0, part = 0;
+ dvdnav_vts_change_event_t *vts_event =
+ (dvdnav_vts_change_event_t *)s->buffer;
+ MP_INFO(s, "DVDNAV, switched to title: %d\n",
+ vts_event->new_vtsN);
+ if (!priv->had_initial_vts) {
+ // dvdnav sends an initial VTS change before any data; don't
+ // cause a blocking wait for the player, because the player in
+ // turn can't initialize the demuxer without data.
+ priv->had_initial_vts = true;
+ break;
+ }
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) == DVDNAV_STATUS_OK)
+ {
+ MP_VERBOSE(s, "DVDNAV, NEW TITLE %d\n", tit);
+ if (priv->title > 0 && tit != priv->title)
+ MP_WARN(s, "Requested title not found\n");
+ }
+ break;
+ }
+ case DVDNAV_CELL_CHANGE: {
+ dvdnav_cell_change_event_t *ev = (dvdnav_cell_change_event_t *)buf;
+
+ if (ev->pgc_length)
+ priv->duration = ev->pgc_length / 90;
+
+ break;
+ }
+ case DVDNAV_SPU_CLUT_CHANGE: {
+ memcpy(priv->spu_clut, buf, 16 * sizeof(uint32_t));
+ priv->spu_clut_valid = true;
+ break;
+ }
+ }
+ }
+ return 0;
+}
+
+static int control(stream_t *stream, int cmd, void *arg)
+{
+ struct priv *priv = stream->priv;
+ dvdnav_t *dvdnav = priv->dvdnav;
+ int tit, part;
+
+ switch (cmd) {
+ case STREAM_CTRL_GET_NUM_CHAPTERS: {
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ if (dvdnav_get_number_of_parts(dvdnav, tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ if (!part)
+ break;
+ *(unsigned int *)arg = part;
+ return 1;
+ }
+ case STREAM_CTRL_GET_CHAPTER_TIME: {
+ double *ch = arg;
+ int chapter = *ch;
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ uint64_t *parts = NULL, duration = 0;
+ int n = dvdnav_describe_title_chapters(dvdnav, tit, &parts, &duration);
+ if (!parts)
+ break;
+ if (chapter < 0 || chapter + 1 > n)
+ break;
+ *ch = chapter > 0 ? parts[chapter - 1] / 90000.0 : 0;
+ free(parts);
+ return 1;
+ }
+ case STREAM_CTRL_GET_TIME_LENGTH: {
+ if (priv->duration) {
+ *(double *)arg = (double)priv->duration / 1000.0;
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_GET_ASPECT_RATIO: {
+ uint8_t ar = dvdnav_get_video_aspect(dvdnav);
+ *(double *)arg = !ar ? 4.0 / 3.0 : 16.0 / 9.0;
+ return 1;
+ }
+ case STREAM_CTRL_GET_CURRENT_TIME: {
+ double tm;
+ tm = dvdnav_get_current_time(dvdnav) / 90000.0f;
+ if (tm != -1) {
+ *(double *)arg = tm;
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_GET_NUM_TITLES: {
+ int32_t num_titles = 0;
+ if (dvdnav_get_number_of_titles(dvdnav, &num_titles) != DVDNAV_STATUS_OK)
+ break;
+ *((unsigned int*)arg)= num_titles;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_TITLE_LENGTH: {
+ int t = *(double *)arg;
+ int32_t num_titles = 0;
+ if (dvdnav_get_number_of_titles(dvdnav, &num_titles) != DVDNAV_STATUS_OK)
+ break;
+ if (t < 0 || t >= num_titles)
+ break;
+ uint64_t duration = 0;
+ uint64_t *parts = NULL;
+ dvdnav_describe_title_chapters(dvdnav, t + 1, &parts, &duration);
+ if (!parts)
+ break;
+ free(parts);
+ *(double *)arg = duration / 90000.0;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CURRENT_TITLE: {
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ *((unsigned int *) arg) = tit - 1;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SET_CURRENT_TITLE: {
+ int title = *((unsigned int *) arg);
+ if (dvdnav_title_play(priv->dvdnav, title + 1) != DVDNAV_STATUS_OK)
+ break;
+ stream_drop_buffers(stream);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SEEK_TO_TIME: {
+ double *args = arg;
+ double d = args[0]; // absolute target timestamp
+ int flags = args[1]; // from SEEK_* flags (demux.h)
+ if (flags & SEEK_HR)
+ d -= 10; // fudge offset; it's a hack, because fuck libdvd*
+ int64_t tm = (int64_t)(d * 90000);
+ if (tm < 0)
+ tm = 0;
+ if (priv->duration && tm >= (priv->duration * 90))
+ tm = priv->duration * 90 - 1;
+ uint32_t pos, len;
+ if (dvdnav_get_position(dvdnav, &pos, &len) != DVDNAV_STATUS_OK)
+ break;
+ MP_VERBOSE(stream, "seek to PTS %f (%"PRId64")\n", d, tm);
+ if (dvdnav_time_search(dvdnav, tm) != DVDNAV_STATUS_OK)
+ break;
+ stream_drop_buffers(stream);
+ d = dvdnav_get_current_time(dvdnav) / 90000.0f;
+ MP_VERBOSE(stream, "landed at: %f\n", d);
+ if (dvdnav_get_position(dvdnav, &pos, &len) == DVDNAV_STATUS_OK)
+ MP_VERBOSE(stream, "block: %lu\n", (unsigned long)pos);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_NUM_ANGLES: {
+ uint32_t curr, angles;
+ if (dvdnav_get_angle_info(dvdnav, &curr, &angles) != DVDNAV_STATUS_OK)
+ break;
+ *(int *)arg = angles;
+ return 1;
+ }
+ case STREAM_CTRL_GET_ANGLE: {
+ uint32_t curr, angles;
+ if (dvdnav_get_angle_info(dvdnav, &curr, &angles) != DVDNAV_STATUS_OK)
+ break;
+ *(int *)arg = curr;
+ return 1;
+ }
+ case STREAM_CTRL_SET_ANGLE: {
+ uint32_t curr, angles;
+ int new_angle = *(int *)arg;
+ if (dvdnav_get_angle_info(dvdnav, &curr, &angles) != DVDNAV_STATUS_OK)
+ break;
+ if (new_angle > angles || new_angle < 1)
+ break;
+ if (dvdnav_angle_change(dvdnav, new_angle) != DVDNAV_STATUS_OK)
+ return 1;
+ break;
+ }
+ case STREAM_CTRL_GET_LANG: {
+ struct stream_lang_req *req = arg;
+ int lang = 0;
+ switch (req->type) {
+ case STREAM_AUDIO:
+ lang = mp_dvdnav_lang_from_aid(stream, req->id);
+ break;
+ case STREAM_SUB:
+ lang = mp_dvdnav_lang_from_sid(stream, req->id);
+ break;
+ }
+ if (!lang)
+ break;
+ snprintf(req->name, sizeof(req->name), "%c%c", lang >> 8, lang);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_DVD_INFO: {
+ struct stream_dvd_info_req *req = arg;
+ memset(req, 0, sizeof(*req));
+ req->num_subs = mp_dvdnav_number_of_subs(stream);
+ assert(sizeof(uint32_t) == sizeof(unsigned int));
+ memcpy(req->palette, priv->spu_clut, sizeof(req->palette));
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_DISC_NAME: {
+ const char *volume = NULL;
+ if (dvdnav_get_title_string(dvdnav, &volume) != DVDNAV_STATUS_OK)
+ break;
+ if (!volume || !volume[0])
+ break;
+ *(char**)arg = talloc_strdup(NULL, volume);
+ return STREAM_OK;
+ }
+ }
+
+ return STREAM_UNSUPPORTED;
+}
+
+static void stream_dvdnav_close(stream_t *s)
+{
+ struct priv *priv = s->priv;
+ dvdnav_close(priv->dvdnav);
+ priv->dvdnav = NULL;
+ if (priv->dvd_speed)
+ dvd_set_speed(s, priv->filename, -1);
+ if (priv->filename)
+ free(priv->filename);
+}
+
+static struct priv *new_dvdnav_stream(stream_t *stream, char *filename)
+{
+ struct priv *priv = stream->priv;
+ const char *title_str;
+
+ if (!filename)
+ return NULL;
+
+ if (!(priv->filename = strdup(filename)))
+ return NULL;
+
+ priv->dvd_speed = priv->opts->speed;
+ dvd_set_speed(stream, priv->filename, priv->dvd_speed);
+
+ if (dvdnav_open(&(priv->dvdnav), priv->filename) != DVDNAV_STATUS_OK) {
+ free(priv->filename);
+ priv->filename = NULL;
+ return NULL;
+ }
+
+ if (!priv->dvdnav)
+ return NULL;
+
+ dvdnav_set_readahead_flag(priv->dvdnav, 1);
+ if (dvdnav_set_PGC_positioning_flag(priv->dvdnav, 1) != DVDNAV_STATUS_OK)
+ MP_ERR(stream, "stream_dvdnav, failed to set PGC positioning\n");
+ /* report the title?! */
+ dvdnav_get_title_string(priv->dvdnav, &title_str);
+
+ return priv;
+}
+
+static int open_s_internal(stream_t *stream)
+{
+ struct priv *priv, *p;
+ priv = p = stream->priv;
+ char *filename;
+
+ p->opts = mp_get_config_group(stream, stream->global, &dvd_conf);
+
+ if (p->device && p->device[0])
+ filename = p->device;
+ else if (p->opts->device && p->opts->device[0])
+ filename = p->opts->device;
+ else
+ filename = DEFAULT_DVD_DEVICE;
+ if (!new_dvdnav_stream(stream, filename)) {
+ MP_ERR(stream, "Couldn't open DVD device: %s\n",
+ filename);
+ return STREAM_ERROR;
+ }
+
+ if (p->track == TITLE_LONGEST) { // longest
+ dvdnav_t *dvdnav = priv->dvdnav;
+ uint64_t best_length = 0;
+ int best_title = -1;
+ int32_t num_titles;
+ if (dvdnav_get_number_of_titles(dvdnav, &num_titles) == DVDNAV_STATUS_OK) {
+ MP_VERBOSE(stream, "List of available titles:\n");
+ for (int n = 1; n <= num_titles; n++) {
+ uint64_t *parts = NULL, duration = 0;
+ dvdnav_describe_title_chapters(dvdnav, n, &parts, &duration);
+ if (parts) {
+ if (duration > best_length) {
+ best_length = duration;
+ best_title = n;
+ }
+ if (duration > 90000) { // arbitrarily ignore <1s titles
+ char *time = mp_format_time(duration / 90000, false);
+ MP_VERBOSE(stream, "title: %3d duration: %s\n",
+ n - 1, time);
+ talloc_free(time);
+ }
+ free(parts);
+ }
+ }
+ }
+ p->track = best_title - 1;
+ MP_INFO(stream, "Selecting title %d.\n", p->track);
+ }
+
+ if (p->track >= 0) {
+ priv->title = p->track;
+ if (dvdnav_title_play(priv->dvdnav, p->track + 1) != DVDNAV_STATUS_OK) {
+ MP_FATAL(stream, "dvdnav_stream, couldn't select title %d, error '%s'\n",
+ p->track, dvdnav_err_to_string(priv->dvdnav));
+ return STREAM_UNSUPPORTED;
+ }
+ } else {
+ MP_FATAL(stream, "DVD menu support has been removed.\n");
+ return STREAM_ERROR;
+ }
+ if (p->opts->angle > 1)
+ dvdnav_angle_change(priv->dvdnav, p->opts->angle);
+
+ stream->fill_buffer = fill_buffer;
+ stream->control = control;
+ stream->close = stream_dvdnav_close;
+ stream->demuxer = "+disc";
+ stream->lavf_type = "mpeg";
+
+ return STREAM_OK;
+}
+
+static int open_s(stream_t *stream)
+{
+ struct priv *priv = talloc_zero(stream, struct priv);
+ stream->priv = priv;
+
+ bstr title, bdevice;
+ bstr_split_tok(bstr0(stream->path), "/", &title, &bdevice);
+
+ priv->track = TITLE_LONGEST;
+
+ if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) {
+ priv->track = TITLE_LONGEST;
+ } else if (bstr_equals0(title, "menu")) {
+ priv->track = TITLE_MENU;
+ } else if (title.len) {
+ bstr rest;
+ priv->track = bstrtoll(title, &rest, 10);
+ if (rest.len) {
+ MP_ERR(stream, "number expected: '%.*s'\n", BSTR_P(rest));
+ return STREAM_ERROR;
+ }
+ }
+
+ priv->device = bstrto0(priv, bdevice);
+
+ return open_s_internal(stream);
+}
+
+const stream_info_t stream_info_dvdnav = {
+ .name = "dvdnav",
+ .open = open_s,
+ .protocols = (const char*const[]){ "dvd", "dvdnav", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
+
+static bool check_ifo(const char *path)
+{
+ if (strcasecmp(mp_basename(path), "video_ts.ifo"))
+ return false;
+
+ return dvd_probe(path, ".ifo", "DVDVIDEO-VMG");
+}
+
+static int ifo_dvdnav_stream_open(stream_t *stream)
+{
+ struct priv *priv = talloc_zero(stream, struct priv);
+ stream->priv = priv;
+
+ if (!stream->access_references)
+ goto unsupported;
+
+ priv->track = TITLE_LONGEST;
+
+ char *path = mp_file_get_path(priv, bstr0(stream->url));
+ if (!path)
+ goto unsupported;
+
+ // We allow the path to point to a directory containing VIDEO_TS/, a
+ // directory containing VIDEO_TS.IFO, or that file itself.
+ if (!check_ifo(path)) {
+ // On UNIX, just assume the filename is always uppercase.
+ char *npath = mp_path_join(priv, path, "VIDEO_TS.IFO");
+ if (!check_ifo(npath)) {
+ npath = mp_path_join(priv, path, "VIDEO_TS/VIDEO_TS.IFO");
+ if (!check_ifo(npath))
+ goto unsupported;
+ }
+ path = npath;
+ }
+
+ priv->device = bstrto0(priv, mp_dirname(path));
+
+ MP_INFO(stream, ".IFO detected. Redirecting to dvd://\n");
+ return open_s_internal(stream);
+
+unsupported:
+ talloc_free(priv);
+ stream->priv = NULL;
+ return STREAM_UNSUPPORTED;
+}
+
+const stream_info_t stream_info_ifo_dvdnav = {
+ .name = "ifo_dvdnav",
+ .open = ifo_dvdnav_stream_open,
+ .protocols = (const char*const[]){ "file", "", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_edl.c b/stream/stream_edl.c
new file mode 100644
index 0000000..94bbe58
--- /dev/null
+++ b/stream/stream_edl.c
@@ -0,0 +1,17 @@
+// Dummy stream implementation to enable demux_edl, which is in turn a
+// dummy demuxer implementation to enable tl_edl.
+
+#include "stream.h"
+
+static int s_open (struct stream *stream)
+{
+ stream->demuxer = "edl";
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_edl = {
+ .name = "edl",
+ .open = s_open,
+ .protocols = (const char*const[]){"edl", NULL},
+};
diff --git a/stream/stream_file.c b/stream/stream_file.c
new file mode 100644
index 0000000..4895a83
--- /dev/null
+++ b/stream/stream_file.c
@@ -0,0 +1,377 @@
+/*
+ * Original authors: Albeu, probably Arpi
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+
+#ifndef __MINGW32__
+#include <poll.h>
+#endif
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/thread_tools.h"
+#include "stream.h"
+#include "options/m_option.h"
+#include "options/path.h"
+
+#if HAVE_BSD_FSTATFS
+#include <sys/param.h>
+#include <sys/mount.h>
+#endif
+
+#if HAVE_LINUX_FSTATFS
+#include <sys/vfs.h>
+#endif
+
+#ifdef _WIN32
+#include <windows.h>
+#include <winternl.h>
+#include <io.h>
+
+#ifndef FILE_REMOTE_DEVICE
+#define FILE_REMOTE_DEVICE (0x10)
+#endif
+#endif
+
+struct priv {
+ int fd;
+ bool close;
+ bool use_poll;
+ bool regular_file;
+ bool appending;
+ int64_t orig_size;
+ struct mp_cancel *cancel;
+};
+
+// Total timeout = RETRY_TIMEOUT * MAX_RETRIES
+#define RETRY_TIMEOUT 0.2
+#define MAX_RETRIES 10
+
+static int64_t get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+ struct stat st;
+ if (fstat(p->fd, &st) == 0) {
+ if (st.st_size <= 0 && !s->seekable)
+ st.st_size = -1;
+ if (st.st_size >= 0)
+ return st.st_size;
+ }
+ return -1;
+}
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ struct priv *p = s->priv;
+
+#ifndef __MINGW32__
+ if (p->use_poll) {
+ int c = mp_cancel_get_fd(p->cancel);
+ struct pollfd fds[2] = {
+ {.fd = p->fd, .events = POLLIN},
+ {.fd = c, .events = POLLIN},
+ };
+ poll(fds, c >= 0 ? 2 : 1, -1);
+ if (fds[1].revents & POLLIN)
+ return -1;
+ }
+#endif
+
+ for (int retries = 0; retries < MAX_RETRIES; retries++) {
+ int r = read(p->fd, buffer, max_len);
+ if (r > 0)
+ return r;
+
+ // Try to detect and handle files being appended during playback.
+ int64_t size = get_size(s);
+ if (p->regular_file && size > p->orig_size && !p->appending) {
+ MP_WARN(s, "File is apparently being appended to, will keep "
+ "retrying with timeouts.\n");
+ p->appending = true;
+ }
+
+ if (!p->appending || p->use_poll)
+ break;
+
+ if (mp_cancel_wait(p->cancel, RETRY_TIMEOUT))
+ break;
+ }
+
+ return 0;
+}
+
+static int write_buffer(stream_t *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+ return write(p->fd, buffer, len);
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ return lseek(p->fd, newpos, SEEK_SET) != (off_t)-1;
+}
+
+static void s_close(stream_t *s)
+{
+ struct priv *p = s->priv;
+ if (p->close)
+ close(p->fd);
+}
+
+// If url is a file:// URL, return the local filename, otherwise return NULL.
+char *mp_file_url_to_filename(void *talloc_ctx, bstr url)
+{
+ bstr proto = mp_split_proto(url, &url);
+ if (bstrcasecmp0(proto, "file") != 0)
+ return NULL;
+ char *filename = bstrto0(talloc_ctx, url);
+ mp_url_unescape_inplace(filename);
+#if HAVE_DOS_PATHS
+ // extract '/' from '/x:/path'
+ if (filename[0] == '/' && filename[1] && filename[2] == ':')
+ memmove(filename, filename + 1, strlen(filename)); // including \0
+#endif
+ return filename;
+}
+
+// Return talloc_strdup's filesystem path if local, otherwise NULL.
+// Unlike mp_file_url_to_filename(), doesn't return NULL if already local.
+char *mp_file_get_path(void *talloc_ctx, bstr url)
+{
+ if (mp_split_proto(url, &(bstr){0}).len) {
+ return mp_file_url_to_filename(talloc_ctx, url);
+ } else {
+ return bstrto0(talloc_ctx, url);
+ }
+}
+
+#if HAVE_BSD_FSTATFS
+static bool check_stream_network(int fd)
+{
+ struct statfs fs;
+ const char *stypes[] = { "afpfs", "nfs", "smbfs", "webdav", "osxfusefs",
+ "fuse", "fusefs.sshfs", "macfuse", NULL };
+ if (fstatfs(fd, &fs) == 0)
+ for (int i=0; stypes[i]; i++)
+ if (strcmp(stypes[i], fs.f_fstypename) == 0)
+ return true;
+ return false;
+
+}
+#elif HAVE_LINUX_FSTATFS
+static bool check_stream_network(int fd)
+{
+ struct statfs fs;
+ const uint32_t stypes[] = {
+ 0x5346414F /*AFS*/, 0x61756673 /*AUFS*/, 0x00C36400 /*CEPH*/,
+ 0xFF534D42 /*CIFS*/, 0x73757245 /*CODA*/, 0x19830326 /*FHGFS*/,
+ 0x65735546 /*FUSEBLK*/,0x65735543 /*FUSECTL*/,0x1161970 /*GFS*/,
+ 0x47504653 /*GPFS*/, 0x6B414653 /*KAFS*/, 0x0BD00BD0 /*LUSTRE*/,
+ 0x564C /*NCP*/, 0x6969 /*NFS*/, 0x6E667364 /*NFSD*/,
+ 0xAAD7AAEA /*PANFS*/, 0x50495045 /*PIPEFS*/, 0x517B /*SMB*/,
+ 0xBEEFDEAD /*SNFS*/, 0xBACBACBC /*VMHGFS*/, 0x7461636f /*OCFS2*/,
+ 0xFE534D42 /*SMB2*/, 0x61636673 /*ACFS*/, 0x013111A8 /*IBRIX*/,
+ 0
+ };
+ if (fstatfs(fd, &fs) == 0) {
+ for (int i=0; stypes[i]; i++) {
+ if (stypes[i] == fs.f_type)
+ return true;
+ }
+ }
+ return false;
+
+}
+#elif defined(_WIN32)
+static bool check_stream_network(int fd)
+{
+ NTSTATUS (NTAPI *pNtQueryVolumeInformationFile)(HANDLE,
+ PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS) = NULL;
+
+ // NtQueryVolumeInformationFile is an internal Windows function. It has
+ // been present since Windows XP, however this code should fail gracefully
+ // if it's removed from a future version of Windows.
+ HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
+ pNtQueryVolumeInformationFile = (NTSTATUS (NTAPI*)(HANDLE,
+ PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS))
+ GetProcAddress(ntdll, "NtQueryVolumeInformationFile");
+
+ if (!pNtQueryVolumeInformationFile)
+ return false;
+
+ HANDLE h = (HANDLE)_get_osfhandle(fd);
+ if (h == INVALID_HANDLE_VALUE)
+ return false;
+
+ FILE_FS_DEVICE_INFORMATION info = { 0 };
+ IO_STATUS_BLOCK io;
+ NTSTATUS status = pNtQueryVolumeInformationFile(h, &io, &info,
+ sizeof(info), FileFsDeviceInformation);
+ if (!NT_SUCCESS(status))
+ return false;
+
+ return info.DeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM ||
+ (info.Characteristics & FILE_REMOTE_DEVICE);
+}
+#else
+static bool check_stream_network(int fd)
+{
+ return false;
+}
+#endif
+
+static int open_f(stream_t *stream, const struct stream_open_args *args)
+{
+ struct priv *p = talloc_ptrtype(stream, p);
+ *p = (struct priv) {
+ .fd = -1,
+ };
+ stream->priv = p;
+ stream->is_local_file = true;
+
+ bool strict_fs = args->flags & STREAM_LOCAL_FS_ONLY;
+ bool write = stream->mode == STREAM_WRITE;
+ int m = O_CLOEXEC | (write ? O_RDWR | O_CREAT | O_TRUNC : O_RDONLY);
+
+ char *filename = stream->path;
+ char *url = "";
+ if (!strict_fs) {
+ char *fn = mp_file_url_to_filename(stream, bstr0(stream->url));
+ if (fn)
+ filename = stream->path = fn;
+ url = stream->url;
+ }
+
+ bool is_fdclose = strncmp(url, "fdclose://", 10) == 0;
+ if (strncmp(url, "fd://", 5) == 0 || is_fdclose) {
+ char *begin = strstr(stream->url, "://") + 3, *end = NULL;
+ p->fd = strtol(begin, &end, 0);
+ if (!end || end == begin || end[0]) {
+ MP_ERR(stream, "Invalid FD: %s\n", stream->url);
+ return STREAM_ERROR;
+ }
+ if (is_fdclose)
+ p->close = true;
+ } else if (!strict_fs && !strcmp(filename, "-")) {
+ if (!write) {
+ MP_INFO(stream, "Reading from stdin...\n");
+ p->fd = 0;
+ } else {
+ MP_INFO(stream, "Writing to stdout...\n");
+ p->fd = 1;
+ }
+ } else {
+ if (bstr_startswith0(bstr0(stream->url), "appending://"))
+ p->appending = true;
+
+ mode_t openmode = S_IRUSR | S_IWUSR;
+#ifndef __MINGW32__
+ openmode |= S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
+ if (!write)
+ m |= O_NONBLOCK;
+#endif
+ p->fd = open(filename, m | O_BINARY, openmode);
+ if (p->fd < 0) {
+ MP_ERR(stream, "Cannot open file '%s': %s\n",
+ filename, mp_strerror(errno));
+ return STREAM_ERROR;
+ }
+ p->close = true;
+ }
+
+ struct stat st;
+ if (fstat(p->fd, &st) == 0) {
+ if (S_ISDIR(st.st_mode)) {
+ stream->is_directory = true;
+ if (!(args->flags & STREAM_LESS_NOISE))
+ MP_INFO(stream, "This is a directory - adding to playlist.\n");
+ } else if (S_ISREG(st.st_mode)) {
+ p->regular_file = true;
+#ifndef __MINGW32__
+ // O_NONBLOCK has weird semantics on file locks; remove it.
+ int val = fcntl(p->fd, F_GETFL) & ~(unsigned)O_NONBLOCK;
+ fcntl(p->fd, F_SETFL, val);
+#endif
+ } else {
+ p->use_poll = true;
+ }
+ }
+
+#ifdef __MINGW32__
+ setmode(p->fd, O_BINARY);
+#endif
+
+ off_t len = lseek(p->fd, 0, SEEK_END);
+ lseek(p->fd, 0, SEEK_SET);
+ if (len != (off_t)-1) {
+ stream->seek = seek;
+ stream->seekable = true;
+ }
+
+ stream->fast_skip = true;
+ stream->fill_buffer = fill_buffer;
+ stream->write_buffer = write_buffer;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ if (check_stream_network(p->fd)) {
+ stream->streaming = true;
+#if HAVE_COCOA
+ if (fcntl(p->fd, F_RDAHEAD, 0) < 0) {
+ MP_VERBOSE(stream, "Cannot disable read ahead on file '%s': %s\n",
+ filename, mp_strerror(errno));
+ }
+#endif
+ }
+
+ p->orig_size = get_size(stream);
+
+ p->cancel = mp_cancel_new(p);
+ if (stream->cancel)
+ mp_cancel_set_parent(p->cancel, stream->cancel);
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_file = {
+ .name = "file",
+ .open2 = open_f,
+ .protocols = (const char*const[]){ "file", "", "appending", NULL },
+ .can_write = true,
+ .local_fs = true,
+ .stream_origin = STREAM_ORIGIN_FS,
+};
+
+const stream_info_t stream_info_fd = {
+ .name = "fd",
+ .open2 = open_f,
+ .protocols = (const char*const[]){ "fd", "fdclose", NULL },
+ .can_write = true,
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_lavf.c b/stream/stream_lavf.c
new file mode 100644
index 0000000..c153ddd
--- /dev/null
+++ b/stream/stream_lavf.c
@@ -0,0 +1,457 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavformat/avformat.h>
+#include <libavformat/avio.h>
+#include <libavutil/opt.h>
+
+#include "options/path.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/tags.h"
+#include "common/av_common.h"
+#include "demux/demux.h"
+#include "misc/charset_conv.h"
+#include "misc/thread_tools.h"
+#include "stream.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+
+#include "cookies.h"
+
+#include "misc/bstr.h"
+#include "mpv_talloc.h"
+
+#define OPT_BASE_STRUCT struct stream_lavf_params
+struct stream_lavf_params {
+ char **avopts;
+ bool cookies_enabled;
+ char *cookies_file;
+ char *useragent;
+ char *referrer;
+ char **http_header_fields;
+ bool tls_verify;
+ char *tls_ca_file;
+ char *tls_cert_file;
+ char *tls_key_file;
+ double timeout;
+ char *http_proxy;
+};
+
+const struct m_sub_options stream_lavf_conf = {
+ .opts = (const m_option_t[]) {
+ {"stream-lavf-o", OPT_KEYVALUELIST(avopts)},
+ {"http-header-fields", OPT_STRINGLIST(http_header_fields)},
+ {"user-agent", OPT_STRING(useragent)},
+ {"referrer", OPT_STRING(referrer)},
+ {"cookies", OPT_BOOL(cookies_enabled)},
+ {"cookies-file", OPT_STRING(cookies_file), .flags = M_OPT_FILE},
+ {"tls-verify", OPT_BOOL(tls_verify)},
+ {"tls-ca-file", OPT_STRING(tls_ca_file), .flags = M_OPT_FILE},
+ {"tls-cert-file", OPT_STRING(tls_cert_file), .flags = M_OPT_FILE},
+ {"tls-key-file", OPT_STRING(tls_key_file), .flags = M_OPT_FILE},
+ {"network-timeout", OPT_DOUBLE(timeout), M_RANGE(0, DBL_MAX)},
+ {"http-proxy", OPT_STRING(http_proxy)},
+ {0}
+ },
+ .size = sizeof(struct stream_lavf_params),
+ .defaults = &(const struct stream_lavf_params){
+ .useragent = "libmpv",
+ .timeout = 60,
+ },
+};
+
+static const char *const http_like[] =
+ {"http", "https", "mmsh", "mmshttp", "httproxy", NULL};
+
+static int open_f(stream_t *stream);
+static struct mp_tags *read_icy(stream_t *stream);
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ AVIOContext *avio = s->priv;
+ int r = avio_read_partial(avio, buffer, max_len);
+ return (r <= 0) ? -1 : r;
+}
+
+static int write_buffer(stream_t *s, void *buffer, int len)
+{
+ AVIOContext *avio = s->priv;
+ avio_write(avio, buffer, len);
+ avio_flush(avio);
+ if (avio->error)
+ return -1;
+ return len;
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ AVIOContext *avio = s->priv;
+ if (avio_seek(avio, newpos, SEEK_SET) < 0) {
+ return 0;
+ }
+ return 1;
+}
+
+static int64_t get_size(stream_t *s)
+{
+ AVIOContext *avio = s->priv;
+ return avio_size(avio);
+}
+
+static void close_f(stream_t *stream)
+{
+ AVIOContext *avio = stream->priv;
+ /* NOTE: As of 2011 write streams must be manually flushed before close.
+ * Currently write_buffer() always flushes them after writing.
+ * avio_close() could return an error, but we have no way to return that
+ * with the current stream API.
+ */
+ if (avio)
+ avio_close(avio);
+}
+
+static int control(stream_t *s, int cmd, void *arg)
+{
+ AVIOContext *avio = s->priv;
+ switch(cmd) {
+ case STREAM_CTRL_AVSEEK: {
+ struct stream_avseek *c = arg;
+ int64_t r = avio_seek_time(avio, c->stream_index, c->timestamp, c->flags);
+ if (r >= 0) {
+ stream_drop_buffers(s);
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_HAS_AVSEEK: {
+ // Starting at some point, read_seek is always available, and runtime
+ // behavior decides whether it exists or not. FFmpeg's API doesn't
+ // return anything helpful to determine seekability upfront, so here's
+ // a hardcoded whitelist. Not our fault.
+ // In addition we also have to jump through ridiculous hoops just to
+ // get the fucking protocol name.
+ const char *proto = NULL;
+ if (avio->av_class && avio->av_class->child_next) {
+ // This usually yields the URLContext (why does it even exist?),
+ // which holds the name of the actual protocol implementation.
+ void *child = avio->av_class->child_next(avio, NULL);
+ AVClass *cl = *(AVClass **)child;
+ if (cl && cl->item_name)
+ proto = cl->item_name(child);
+ }
+ static const char *const has_read_seek[] = {
+ "rtmp", "rtmpt", "rtmpe", "rtmpte", "rtmps", "rtmpts", "mmsh", 0};
+ for (int n = 0; has_read_seek[n]; n++) {
+ if (avio->read_seek && proto && strcmp(proto, has_read_seek[n]) == 0)
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_GET_METADATA: {
+ *(struct mp_tags **)arg = read_icy(s);
+ if (!*(struct mp_tags **)arg)
+ break;
+ return 1;
+ }
+ }
+ return STREAM_UNSUPPORTED;
+}
+
+static int interrupt_cb(void *ctx)
+{
+ struct stream *stream = ctx;
+ return mp_cancel_test(stream->cancel);
+}
+
+static const char * const prefix[] = { "lavf://", "ffmpeg://" };
+
+void mp_setup_av_network_options(AVDictionary **dict, const char *target_fmt,
+ struct mpv_global *global, struct mp_log *log)
+{
+ void *temp = talloc_new(NULL);
+ struct stream_lavf_params *opts =
+ mp_get_config_group(temp, global, &stream_lavf_conf);
+
+ // HTTP specific options (other protocols ignore them)
+ if (opts->useragent)
+ av_dict_set(dict, "user_agent", opts->useragent, 0);
+ if (opts->cookies_enabled) {
+ char *file = opts->cookies_file;
+ if (file && file[0])
+ file = mp_get_user_path(temp, global, file);
+ char *cookies = cookies_lavf(temp, global, log, file);
+ if (cookies && cookies[0])
+ av_dict_set(dict, "cookies", cookies, 0);
+ }
+ av_dict_set(dict, "tls_verify", opts->tls_verify ? "1" : "0", 0);
+ if (opts->tls_ca_file)
+ av_dict_set(dict, "ca_file", opts->tls_ca_file, 0);
+ if (opts->tls_cert_file)
+ av_dict_set(dict, "cert_file", opts->tls_cert_file, 0);
+ if (opts->tls_key_file)
+ av_dict_set(dict, "key_file", opts->tls_key_file, 0);
+ char *cust_headers = talloc_strdup(temp, "");
+ if (opts->referrer) {
+ cust_headers = talloc_asprintf_append(cust_headers, "Referer: %s\r\n",
+ opts->referrer);
+ }
+ if (opts->http_header_fields) {
+ for (int n = 0; opts->http_header_fields[n]; n++) {
+ cust_headers = talloc_asprintf_append(cust_headers, "%s\r\n",
+ opts->http_header_fields[n]);
+ }
+ }
+ if (strlen(cust_headers))
+ av_dict_set(dict, "headers", cust_headers, 0);
+ av_dict_set(dict, "icy", "1", 0);
+ // So far, every known protocol uses microseconds for this
+ // Except rtsp.
+ if (opts->timeout > 0) {
+ if (target_fmt && strcmp(target_fmt, "rtsp") == 0) {
+ mp_verbose(log, "Broken FFmpeg RTSP API => not setting timeout.\n");
+ } else {
+ char buf[80];
+ snprintf(buf, sizeof(buf), "%lld", (long long)(opts->timeout * 1e6));
+ av_dict_set(dict, "timeout", buf, 0);
+ }
+ }
+ if (opts->http_proxy && opts->http_proxy[0])
+ av_dict_set(dict, "http_proxy", opts->http_proxy, 0);
+
+ mp_set_avdict(dict, opts->avopts);
+
+ talloc_free(temp);
+}
+
+// Escape http URLs with unescaped, invalid characters in them.
+// libavformat's http protocol does not do this, and a patch to add this
+// in a 100% safe case (spaces only) was rejected.
+static char *normalize_url(void *ta_parent, const char *filename)
+{
+ bstr proto = mp_split_proto(bstr0(filename), NULL);
+ for (int n = 0; http_like[n]; n++) {
+ if (bstr_equals0(proto, http_like[n]))
+ // Escape everything but reserved characters.
+ // Also don't double-scape, so include '%'.
+ return mp_url_escape(ta_parent, filename, ":/?#[]@!$&'()*+,;=%");
+ }
+ return (char *)filename;
+}
+
+static int open_f(stream_t *stream)
+{
+ AVIOContext *avio = NULL;
+ int res = STREAM_ERROR;
+ AVDictionary *dict = NULL;
+ void *temp = talloc_new(NULL);
+
+ stream->seek = NULL;
+ stream->seekable = false;
+
+ int flags = stream->mode == STREAM_WRITE ? AVIO_FLAG_WRITE : AVIO_FLAG_READ;
+
+ const char *filename = stream->url;
+ if (!filename) {
+ MP_ERR(stream, "No URL\n");
+ goto out;
+ }
+ for (int i = 0; i < sizeof(prefix) / sizeof(prefix[0]); i++)
+ if (!strncmp(filename, prefix[i], strlen(prefix[i])))
+ filename += strlen(prefix[i]);
+ if (!strncmp(filename, "rtsp:", 5) || !strncmp(filename, "rtsps:", 6)) {
+ /* This is handled as a special demuxer, without a separate
+ * stream layer. demux_lavf will do all the real work. Note
+ * that libavformat doesn't even provide a protocol entry for
+ * this (the rtsp demuxer's probe function checks for a "rtsp:"
+ * filename prefix), so it has to be handled specially here.
+ */
+ stream->demuxer = "lavf";
+ stream->lavf_type = "rtsp";
+ talloc_free(temp);
+ return STREAM_OK;
+ }
+
+ // Replace "mms://" with "mmsh://", so that most mms:// URLs just work.
+ // Replace "dav://" or "webdav://" with "http://" and "davs://" or "webdavs://" with "https://"
+ bstr b_filename = bstr0(filename);
+ if (bstr_eatstart0(&b_filename, "mms://") ||
+ bstr_eatstart0(&b_filename, "mmshttp://"))
+ {
+ filename = talloc_asprintf(temp, "mmsh://%.*s", BSTR_P(b_filename));
+ } else if (bstr_eatstart0(&b_filename, "dav://") || bstr_eatstart0(&b_filename, "webdav://"))
+ {
+ filename = talloc_asprintf(temp, "http://%.*s", BSTR_P(b_filename));
+ } else if (bstr_eatstart0(&b_filename, "davs://") || bstr_eatstart0(&b_filename, "webdavs://"))
+ {
+ filename = talloc_asprintf(temp, "https://%.*s", BSTR_P(b_filename));
+ }
+
+ av_dict_set(&dict, "reconnect", "1", 0);
+ av_dict_set(&dict, "reconnect_delay_max", "7", 0);
+
+ mp_setup_av_network_options(&dict, NULL, stream->global, stream->log);
+
+ AVIOInterruptCB cb = {
+ .callback = interrupt_cb,
+ .opaque = stream,
+ };
+
+ filename = normalize_url(stream, filename);
+
+ if (strncmp(filename, "rtmp", 4) == 0) {
+ stream->demuxer = "lavf";
+ stream->lavf_type = "flv";
+ // Setting timeout enables listen mode - force it to disabled.
+ av_dict_set(&dict, "timeout", "0", 0);
+ }
+
+ int err = avio_open2(&avio, filename, flags, &cb, &dict);
+ if (err < 0) {
+ if (err == AVERROR_PROTOCOL_NOT_FOUND)
+ MP_ERR(stream, "Protocol not found. Make sure"
+ " ffmpeg/Libav is compiled with networking support.\n");
+ goto out;
+ }
+
+ mp_avdict_print_unset(stream->log, MSGL_V, dict);
+
+ if (avio->av_class) {
+ uint8_t *mt = NULL;
+ if (av_opt_get(avio, "mime_type", AV_OPT_SEARCH_CHILDREN, &mt) >= 0) {
+ stream->mime_type = talloc_strdup(stream, mt);
+ av_free(mt);
+ }
+ }
+
+ stream->priv = avio;
+ stream->seekable = avio->seekable & AVIO_SEEKABLE_NORMAL;
+ stream->seek = stream->seekable ? seek : NULL;
+ stream->fill_buffer = fill_buffer;
+ stream->write_buffer = write_buffer;
+ stream->get_size = get_size;
+ stream->control = control;
+ stream->close = close_f;
+ // enable cache (should be avoided for files, but no way to detect this)
+ stream->streaming = true;
+ if (stream->info->stream_origin == STREAM_ORIGIN_NET)
+ stream->is_network = true;
+ res = STREAM_OK;
+
+out:
+ av_dict_free(&dict);
+ talloc_free(temp);
+ return res;
+}
+
+static struct mp_tags *read_icy(stream_t *s)
+{
+ AVIOContext *avio = s->priv;
+
+ if (!avio->av_class)
+ return NULL;
+
+ uint8_t *icy_header = NULL;
+ if (av_opt_get(avio, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN,
+ &icy_header) < 0)
+ icy_header = NULL;
+
+ uint8_t *icy_packet;
+ if (av_opt_get(avio, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN,
+ &icy_packet) < 0)
+ icy_packet = NULL;
+
+ // Send a metadata update only 1. on start, and 2. on a new metadata packet.
+ // To detect new packages, set the icy_metadata_packet to "-" once we've
+ // read it (a bit hacky, but works).
+
+ struct mp_tags *res = NULL;
+ if ((!icy_header || !icy_header[0]) && (!icy_packet || !icy_packet[0]))
+ goto done;
+
+ bstr packet = bstr0(icy_packet);
+ if (bstr_equals0(packet, "-"))
+ goto done;
+
+ res = talloc_zero(NULL, struct mp_tags);
+
+ bstr header = bstr0(icy_header);
+ while (header.len) {
+ bstr line = bstr_strip_linebreaks(bstr_getline(header, &header));
+ bstr name, val;
+ if (bstr_split_tok(line, ": ", &name, &val))
+ mp_tags_set_bstr(res, name, val);
+ }
+
+ bstr head = bstr0("StreamTitle='");
+ int i = bstr_find(packet, head);
+ if (i >= 0) {
+ packet = bstr_cut(packet, i + head.len);
+ int end = bstr_find(packet, bstr0("\';"));
+ packet = bstr_splice(packet, 0, end);
+
+ bool allocated = false;
+ struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf);
+ const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0);
+ if (charset && !mp_charset_is_utf8(charset)) {
+ bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0);
+ if (conv.start && conv.start != packet.start) {
+ allocated = true;
+ packet = conv;
+ }
+ }
+ mp_tags_set_bstr(res, bstr0("icy-title"), packet);
+ talloc_free(opts);
+ if (allocated)
+ talloc_free(packet.start);
+ }
+
+ av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN);
+
+done:
+ av_free(icy_header);
+ av_free(icy_packet);
+ return res;
+}
+
+const stream_info_t stream_info_ffmpeg = {
+ .name = "ffmpeg",
+ .open = open_f,
+ .protocols = (const char *const[]){
+ "rtmp", "rtsp", "rtsps", "http", "https", "mms", "mmst", "mmsh", "mmshttp",
+ "rtp", "httpproxy", "rtmpe", "rtmps", "rtmpt", "rtmpte", "rtmpts", "srt",
+ "rist", "srtp", "gopher", "gophers", "data", "ipfs", "ipns", "dav",
+ "davs", "webdav", "webdavs",
+ NULL },
+ .can_write = true,
+ .stream_origin = STREAM_ORIGIN_NET,
+};
+
+// Unlike above, this is not marked as safe, and can contain protocols which
+// may do insecure things. (Such as "ffmpeg", which can access the "lavfi"
+// pseudo-demuxer, which in turn gives access to filters that can access the
+// local filesystem.)
+const stream_info_t stream_info_ffmpeg_unsafe = {
+ .name = "ffmpeg",
+ .open = open_f,
+ .protocols = (const char *const[]){
+ "lavf", "ffmpeg", "udp", "ftp", "tcp", "tls", "unix", "sftp", "md5",
+ "concat", "smb",
+ NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+ .can_write = true,
+};
diff --git a/stream/stream_libarchive.c b/stream/stream_libarchive.c
new file mode 100644
index 0000000..ff2d512
--- /dev/null
+++ b/stream/stream_libarchive.c
@@ -0,0 +1,623 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <archive.h>
+#include <archive_entry.h>
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "misc/thread_tools.h"
+#include "stream.h"
+
+#include "stream_libarchive.h"
+
+#define MP_ARCHIVE_FLAG_MAYBE_ZIP (MP_ARCHIVE_FLAG_PRIV << 0)
+#define MP_ARCHIVE_FLAG_MAYBE_RAR (MP_ARCHIVE_FLAG_PRIV << 1)
+#define MP_ARCHIVE_FLAG_MAYBE_VOLUMES (MP_ARCHIVE_FLAG_PRIV << 2)
+
+struct mp_archive_volume {
+ struct mp_archive *mpa;
+ int index; // volume number (starting with 0, mp_archive.primary_src)
+ struct stream *src; // NULL => not current volume, or 0 sized dummy stream
+ int64_t seek_to;
+ char *url;
+};
+
+static bool probe_rar(struct stream *s)
+{
+ static uint8_t rar_sig[] = {0x52, 0x61, 0x72, 0x21, 0x1a, 0x07};
+ uint8_t buf[6];
+ if (stream_read_peek(s, buf, sizeof(buf)) != sizeof(buf))
+ return false;
+ return memcmp(buf, rar_sig, 6) == 0;
+}
+
+static bool probe_multi_rar(struct stream *s)
+{
+ uint8_t hdr[14];
+ if (stream_read_peek(s, hdr, sizeof(hdr)) == sizeof(hdr)) {
+ // Look for rar mark head & main head (assume they're in order).
+ if (hdr[6] == 0x00 && hdr[7 + 2] == 0x73) {
+ int rflags = hdr[7 + 3] | (hdr[7 + 4] << 8);
+ return rflags & 0x100;
+ }
+ }
+ return false;
+}
+
+static bool probe_zip(struct stream *s)
+{
+ uint8_t p[4];
+ if (stream_read_peek(s, p, sizeof(p)) != sizeof(p))
+ return false;
+ // Lifted from libarchive, BSD license.
+ if (p[0] == 'P' && p[1] == 'K') {
+ if ((p[2] == '\001' && p[3] == '\002') ||
+ (p[2] == '\003' && p[3] == '\004') ||
+ (p[2] == '\005' && p[3] == '\006') ||
+ (p[2] == '\006' && p[3] == '\006') ||
+ (p[2] == '\007' && p[3] == '\010') ||
+ (p[2] == '0' && p[3] == '0'))
+ return true;
+ }
+ return false;
+}
+
+static int mp_archive_probe(struct stream *src)
+{
+ int flags = 0;
+ assert(stream_tell(src) == 0);
+ if (probe_zip(src))
+ flags |= MP_ARCHIVE_FLAG_MAYBE_ZIP;
+
+ if (probe_rar(src)) {
+ flags |= MP_ARCHIVE_FLAG_MAYBE_RAR;
+ if (probe_multi_rar(src))
+ flags |= MP_ARCHIVE_FLAG_MAYBE_VOLUMES;
+ }
+ return flags;
+}
+
+static bool volume_seek(struct mp_archive_volume *vol)
+{
+ if (!vol->src || vol->seek_to < 0)
+ return true;
+ bool r = stream_seek(vol->src, vol->seek_to);
+ vol->seek_to = -1;
+ return r;
+}
+
+static ssize_t read_cb(struct archive *arch, void *priv, const void **buffer)
+{
+ struct mp_archive_volume *vol = priv;
+ if (!vol->src)
+ return 0;
+ if (!volume_seek(vol))
+ return -1;
+ int res = stream_read_partial(vol->src, vol->mpa->buffer,
+ sizeof(vol->mpa->buffer));
+ *buffer = vol->mpa->buffer;
+ return MPMAX(res, 0);
+}
+
+// lazy seek to avoid problems with end seeking over http
+static int64_t seek_cb(struct archive *arch, void *priv,
+ int64_t offset, int whence)
+{
+ struct mp_archive_volume *vol = priv;
+ if (!vol->src)
+ return 0;
+ switch (whence) {
+ case SEEK_SET:
+ vol->seek_to = offset;
+ break;
+ case SEEK_CUR:
+ if (vol->seek_to < 0)
+ vol->seek_to = stream_tell(vol->src);
+ vol->seek_to += offset;
+ break;
+ case SEEK_END: ;
+ int64_t size = stream_get_size(vol->src);
+ if (size < 0)
+ return -1;
+ vol->seek_to = size + offset;
+ break;
+ default:
+ return -1;
+ }
+ return vol->seek_to;
+}
+
+static int64_t skip_cb(struct archive *arch, void *priv, int64_t request)
+{
+ struct mp_archive_volume *vol = priv;
+ if (!vol->src)
+ return request;
+ if (!volume_seek(vol))
+ return -1;
+ int64_t old = stream_tell(vol->src);
+ stream_seek_skip(vol->src, old + request);
+ return stream_tell(vol->src) - old;
+}
+
+static int open_cb(struct archive *arch, void *priv)
+{
+ struct mp_archive_volume *vol = priv;
+ vol->seek_to = -1;
+ if (!vol->src) {
+ // Avoid annoying warnings/latency for known dummy volumes.
+ if (vol->index >= vol->mpa->num_volumes)
+ return ARCHIVE_OK;
+ MP_INFO(vol->mpa, "Opening volume '%s'...\n", vol->url);
+ vol->src = stream_create(vol->url,
+ STREAM_READ |
+ vol->mpa->primary_src->stream_origin,
+ vol->mpa->primary_src->cancel,
+ vol->mpa->primary_src->global);
+ // We pretend that failure to open a stream means it was not found,
+ // we assume in turn means that the volume doesn't exist (since
+ // libarchive builds volumes as some sort of abstraction on top of its
+ // stream layer, and its rar code cannot access volumes or signal
+ // anything related to this). libarchive also encounters a fatal error
+ // when a volume could not be opened. However, due to the way volume
+ // support works, it is fine with 0-sized volumes, which we simulate
+ // whenever vol->src==NULL for an opened volume.
+ if (!vol->src) {
+ vol->mpa->num_volumes = MPMIN(vol->mpa->num_volumes, vol->index);
+ MP_INFO(vol->mpa, "Assuming the volume above was not needed.\n");
+ }
+ return ARCHIVE_OK;
+ }
+
+ // just rewind the primary stream
+ return stream_seek(vol->src, 0) ? ARCHIVE_OK : ARCHIVE_FATAL;
+}
+
+static void volume_close(struct mp_archive_volume *vol)
+{
+ // don't close the primary stream
+ if (vol->src && vol->src != vol->mpa->primary_src) {
+ free_stream(vol->src);
+ vol->src = NULL;
+ }
+}
+
+static int close_cb(struct archive *arch, void *priv)
+{
+ struct mp_archive_volume *vol = priv;
+ volume_close(vol);
+ return ARCHIVE_OK;
+}
+
+static void mp_archive_close(struct mp_archive *mpa)
+{
+ if (mpa && mpa->arch) {
+ archive_read_close(mpa->arch);
+ archive_read_free(mpa->arch);
+ mpa->arch = NULL;
+ }
+}
+
+// Supposedly we're not allowed to continue reading on FATAL returns. Otherwise
+// crashes and other UB is possible. Assume calling the close/free functions is
+// still ok. Return true if it was fatal and the archive was closed.
+static bool mp_archive_check_fatal(struct mp_archive *mpa, int r)
+{
+ if (r > ARCHIVE_FATAL)
+ return false;
+ MP_FATAL(mpa, "fatal error received - closing archive\n");
+ mp_archive_close(mpa);
+ return true;
+}
+
+void mp_archive_free(struct mp_archive *mpa)
+{
+ mp_archive_close(mpa);
+ if (mpa && mpa->locale)
+ freelocale(mpa->locale);
+ talloc_free(mpa);
+}
+
+static bool add_volume(struct mp_archive *mpa, struct stream *src,
+ const char* url, int index)
+{
+ struct mp_archive_volume *vol = talloc_zero(mpa, struct mp_archive_volume);
+ vol->index = index;
+ vol->mpa = mpa;
+ vol->src = src;
+ vol->url = talloc_strdup(vol, url);
+ locale_t oldlocale = uselocale(mpa->locale);
+ bool res = archive_read_append_callback_data(mpa->arch, vol) == ARCHIVE_OK;
+ uselocale(oldlocale);
+ return res;
+}
+
+static char *standard_volume_url(void *ctx, const char *format,
+ struct bstr base, int index)
+{
+ return talloc_asprintf(ctx, format, BSTR_P(base), index);
+}
+
+static char *old_rar_volume_url(void *ctx, const char *format,
+ struct bstr base, int index)
+{
+ return talloc_asprintf(ctx, format, BSTR_P(base),
+ 'r' + index / 100, index % 100);
+}
+
+struct file_pattern {
+ const char *match;
+ const char *format;
+ char *(*volume_url)(void *ctx, const char *format,
+ struct bstr base, int index);
+ int start;
+ int stop;
+ bool legacy;
+};
+
+static const struct file_pattern patterns[] = {
+ { ".part1.rar", "%.*s.part%.1d.rar", standard_volume_url, 2, 9 },
+ { ".part01.rar", "%.*s.part%.2d.rar", standard_volume_url, 2, 99 },
+ { ".part001.rar", "%.*s.part%.3d.rar", standard_volume_url, 2, 999 },
+ { ".part0001.rar", "%.*s.part%.4d.rar", standard_volume_url, 2, 9999 },
+ { ".rar", "%.*s.%c%.2d", old_rar_volume_url, 0, 99, true },
+ { ".001", "%.*s.%.3d", standard_volume_url, 2, 9999 },
+ { NULL, NULL, NULL, 0, 0 },
+};
+
+static bool find_volumes(struct mp_archive *mpa, int flags)
+{
+ struct bstr primary_url = bstr0(mpa->primary_src->url);
+
+ const struct file_pattern *pattern = patterns;
+ while (pattern->match) {
+ if (bstr_endswith0(primary_url, pattern->match))
+ break;
+ pattern++;
+ }
+
+ if (!pattern->match)
+ return true;
+ if (pattern->legacy && !(flags & MP_ARCHIVE_FLAG_MAYBE_VOLUMES))
+ return true;
+
+ struct bstr base = bstr_splice(primary_url, 0, -(int)strlen(pattern->match));
+ for (int i = pattern->start; i <= pattern->stop; i++) {
+ char* url = pattern->volume_url(mpa, pattern->format, base, i);
+
+ if (!add_volume(mpa, NULL, url, i + 1))
+ return false;
+ }
+
+ MP_WARN(mpa, "This appears to be a multi-volume archive.\n"
+ "Support is not very good due to libarchive limitations.\n"
+ "There are known cases of libarchive crashing mpv on these.\n"
+ "This is also an excessively inefficient and stupid way to distribute\n"
+ "media files. People creating them should rethink this.\n");
+
+ return true;
+}
+
+static struct mp_archive *mp_archive_new_raw(struct mp_log *log,
+ struct stream *src,
+ int flags, int max_volumes)
+{
+ struct mp_archive *mpa = talloc_zero(NULL, struct mp_archive);
+ mpa->log = log;
+ mpa->locale = newlocale(LC_CTYPE_MASK, "C.UTF-8", (locale_t)0);
+ if (!mpa->locale) {
+ mpa->locale = newlocale(LC_CTYPE_MASK, "", (locale_t)0);
+ if (!mpa->locale)
+ goto err;
+ }
+ mpa->arch = archive_read_new();
+ mpa->primary_src = src;
+ if (!mpa->arch)
+ goto err;
+
+ mpa->flags = flags;
+ mpa->num_volumes = max_volumes ? max_volumes : INT_MAX;
+
+ // first volume is the primary stream
+ if (!add_volume(mpa, src, src->url, 0))
+ goto err;
+
+ if (!(flags & MP_ARCHIVE_FLAG_NO_VOLUMES)) {
+ // try to open other volumes
+ if (!find_volumes(mpa, flags))
+ goto err;
+ }
+
+ locale_t oldlocale = uselocale(mpa->locale);
+
+ archive_read_support_format_rar(mpa->arch);
+ archive_read_support_format_rar5(mpa->arch);
+
+ // Exclude other formats if it's probably RAR, because other formats may
+ // behave suboptimal with multiple volumes exposed, such as opening every
+ // single volume by seeking at the end of the file.
+ if (!(flags & MP_ARCHIVE_FLAG_MAYBE_RAR)) {
+ archive_read_support_format_7zip(mpa->arch);
+ archive_read_support_format_iso9660(mpa->arch);
+ archive_read_support_filter_bzip2(mpa->arch);
+ archive_read_support_filter_gzip(mpa->arch);
+ archive_read_support_filter_xz(mpa->arch);
+ archive_read_support_format_zip_streamable(mpa->arch);
+
+ // This zip reader is normally preferable. However, it seeks to the end
+ // of the file, which may be annoying (HTTP reconnect, volume skipping),
+ // so use it only as last resort, or if it's relatively likely that it's
+ // really zip.
+ if (flags & (MP_ARCHIVE_FLAG_UNSAFE | MP_ARCHIVE_FLAG_MAYBE_ZIP))
+ archive_read_support_format_zip_seekable(mpa->arch);
+ }
+
+ archive_read_set_read_callback(mpa->arch, read_cb);
+ archive_read_set_skip_callback(mpa->arch, skip_cb);
+ archive_read_set_open_callback(mpa->arch, open_cb);
+ // Allow it to close a volume.
+ archive_read_set_close_callback(mpa->arch, close_cb);
+ if (mpa->primary_src->seekable)
+ archive_read_set_seek_callback(mpa->arch, seek_cb);
+ bool fail = archive_read_open1(mpa->arch) < ARCHIVE_OK;
+
+ uselocale(oldlocale);
+
+ if (fail)
+ goto err;
+
+ return mpa;
+
+err:
+ mp_archive_free(mpa);
+ return NULL;
+}
+
+struct mp_archive *mp_archive_new(struct mp_log *log, struct stream *src,
+ int flags, int max_volumes)
+{
+ flags |= mp_archive_probe(src);
+ return mp_archive_new_raw(log, src, flags, max_volumes);
+}
+
+// Iterate entries. The first call establishes the first entry. Returns false
+// if no entry found, otherwise returns true and sets mpa->entry/entry_filename.
+bool mp_archive_next_entry(struct mp_archive *mpa)
+{
+ mpa->entry = NULL;
+ talloc_free(mpa->entry_filename);
+ mpa->entry_filename = NULL;
+
+ if (!mpa->arch)
+ return false;
+
+ locale_t oldlocale = uselocale(mpa->locale);
+ bool success = false;
+
+ while (!mp_cancel_test(mpa->primary_src->cancel)) {
+ struct archive_entry *entry;
+ int r = archive_read_next_header(mpa->arch, &entry);
+ if (r == ARCHIVE_EOF)
+ break;
+ if (r < ARCHIVE_OK)
+ MP_ERR(mpa, "%s\n", archive_error_string(mpa->arch));
+ if (r < ARCHIVE_WARN) {
+ MP_FATAL(mpa, "could not read archive entry\n");
+ mp_archive_check_fatal(mpa, r);
+ break;
+ }
+ if (archive_entry_filetype(entry) != AE_IFREG)
+ continue;
+ // Some archives may have no filenames, or libarchive won't return some.
+ const char *fn = archive_entry_pathname(entry);
+ char buf[64];
+ if (!fn || bstr_validate_utf8(bstr0(fn)) < 0) {
+ snprintf(buf, sizeof(buf), "mpv_unknown#%d", mpa->entry_num);
+ fn = buf;
+ }
+ mpa->entry = entry;
+ mpa->entry_filename = talloc_strdup(mpa, fn);
+ mpa->entry_num += 1;
+ success = true;
+ break;
+ }
+
+ uselocale(oldlocale);
+
+ return success;
+}
+
+struct priv {
+ struct mp_archive *mpa;
+ bool broken_seek;
+ struct stream *src;
+ int64_t entry_size;
+ char *entry_name;
+};
+
+static int reopen_archive(stream_t *s)
+{
+ struct priv *p = s->priv;
+ s->pos = 0;
+ if (!p->mpa) {
+ p->mpa = mp_archive_new(s->log, p->src, MP_ARCHIVE_FLAG_UNSAFE, 0);
+ } else {
+ int flags = p->mpa->flags;
+ int num_volumes = p->mpa->num_volumes;
+ mp_archive_free(p->mpa);
+ p->mpa = mp_archive_new_raw(s->log, p->src, flags, num_volumes);
+ }
+
+ if (!p->mpa)
+ return STREAM_ERROR;
+
+ // Follows the same logic as demux_libarchive.c.
+ struct mp_archive *mpa = p->mpa;
+ while (mp_archive_next_entry(mpa)) {
+ if (strcmp(p->entry_name, mpa->entry_filename) == 0) {
+ locale_t oldlocale = uselocale(mpa->locale);
+ p->entry_size = -1;
+ if (archive_entry_size_is_set(mpa->entry))
+ p->entry_size = archive_entry_size(mpa->entry);
+ uselocale(oldlocale);
+ return STREAM_OK;
+ }
+ }
+
+ mp_archive_free(p->mpa);
+ p->mpa = NULL;
+ MP_ERR(s, "archive entry not found. '%s'\n", p->entry_name);
+ return STREAM_ERROR;
+}
+
+static int archive_entry_fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ struct priv *p = s->priv;
+ if (!p->mpa)
+ return 0;
+ locale_t oldlocale = uselocale(p->mpa->locale);
+ int r = archive_read_data(p->mpa->arch, buffer, max_len);
+ if (r < 0) {
+ MP_ERR(s, "%s\n", archive_error_string(p->mpa->arch));
+ if (mp_archive_check_fatal(p->mpa, r)) {
+ mp_archive_free(p->mpa);
+ p->mpa = NULL;
+ }
+ }
+ uselocale(oldlocale);
+ return r;
+}
+
+static int archive_entry_seek(stream_t *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ if (p->mpa && !p->broken_seek) {
+ locale_t oldlocale = uselocale(p->mpa->locale);
+ int r = archive_seek_data(p->mpa->arch, newpos, SEEK_SET);
+ uselocale(oldlocale);
+ if (r >= 0)
+ return 1;
+ MP_WARN(s, "possibly unsupported seeking - switching to reopening\n");
+ p->broken_seek = true;
+ if (reopen_archive(s) < STREAM_OK)
+ return -1;
+ }
+ // libarchive can't seek in most formats.
+ if (newpos < s->pos) {
+ // Hack seeking backwards into working by reopening the archive and
+ // starting over.
+ MP_VERBOSE(s, "trying to reopen archive for performing seek\n");
+ if (reopen_archive(s) < STREAM_OK)
+ return -1;
+ }
+ if (newpos > s->pos) {
+ if (!p->mpa && reopen_archive(s) < STREAM_OK)
+ return -1;
+ // For seeking forwards, just keep reading data (there's no libarchive
+ // skip function either).
+ char buffer[4096];
+ while (newpos > s->pos) {
+ if (mp_cancel_test(s->cancel))
+ return -1;
+
+ int size = MPMIN(newpos - s->pos, sizeof(buffer));
+ locale_t oldlocale = uselocale(p->mpa->locale);
+ int r = archive_read_data(p->mpa->arch, buffer, size);
+ if (r <= 0) {
+ if (r == 0 && newpos > p->entry_size) {
+ MP_ERR(s, "demuxer trying to seek beyond end of archive "
+ "entry\n");
+ } else if (r == 0) {
+ MP_ERR(s, "end of archive entry reached while seeking\n");
+ } else {
+ MP_ERR(s, "%s\n", archive_error_string(p->mpa->arch));
+ }
+ uselocale(oldlocale);
+ if (mp_archive_check_fatal(p->mpa, r)) {
+ mp_archive_free(p->mpa);
+ p->mpa = NULL;
+ }
+ return -1;
+ }
+ uselocale(oldlocale);
+ s->pos += r;
+ }
+ }
+ return 1;
+}
+
+static void archive_entry_close(stream_t *s)
+{
+ struct priv *p = s->priv;
+ mp_archive_free(p->mpa);
+ free_stream(p->src);
+}
+
+static int64_t archive_entry_get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+ return p->entry_size;
+}
+
+static int archive_entry_open(stream_t *stream)
+{
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ if (!strchr(stream->path, '|'))
+ return STREAM_ERROR;
+
+ char *base = talloc_strdup(p, stream->path);
+ char *name = strchr(base, '|');
+ if (!name)
+ return STREAM_ERROR;
+ *name++ = '\0';
+ if (name[0] == '/')
+ name += 1;
+ p->entry_name = name;
+ mp_url_unescape_inplace(base);
+
+ p->src = stream_create(base, STREAM_READ | stream->stream_origin,
+ stream->cancel, stream->global);
+ if (!p->src) {
+ archive_entry_close(stream);
+ return STREAM_ERROR;
+ }
+
+ int r = reopen_archive(stream);
+ if (r < STREAM_OK) {
+ archive_entry_close(stream);
+ return r;
+ }
+
+ stream->fill_buffer = archive_entry_fill_buffer;
+ if (p->src->seekable) {
+ stream->seek = archive_entry_seek;
+ stream->seekable = true;
+ }
+ stream->close = archive_entry_close;
+ stream->get_size = archive_entry_get_size;
+ stream->streaming = true;
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_libarchive = {
+ .name = "libarchive",
+ .open = archive_entry_open,
+ .protocols = (const char*const[]){ "archive", NULL },
+};
diff --git a/stream/stream_libarchive.h b/stream/stream_libarchive.h
new file mode 100644
index 0000000..151e4ee
--- /dev/null
+++ b/stream/stream_libarchive.h
@@ -0,0 +1,35 @@
+#include <locale.h>
+#include "osdep/io.h"
+
+#ifdef __APPLE__
+# include <string.h>
+# include <xlocale.h>
+#endif
+
+struct mp_log;
+
+struct mp_archive {
+ locale_t locale;
+ struct mp_log *log;
+ struct archive *arch;
+ struct stream *primary_src;
+ char buffer[4096];
+ int flags;
+ int num_volumes; // INT_MAX if unknown (initial state)
+
+ // Current entry, as set by mp_archive_next_entry().
+ struct archive_entry *entry;
+ char *entry_filename;
+ int entry_num;
+};
+
+void mp_archive_free(struct mp_archive *mpa);
+
+#define MP_ARCHIVE_FLAG_UNSAFE (1 << 0)
+#define MP_ARCHIVE_FLAG_NO_VOLUMES (1 << 1)
+#define MP_ARCHIVE_FLAG_PRIV (1 << 2)
+
+struct mp_archive *mp_archive_new(struct mp_log *log, struct stream *src,
+ int flags, int max_volumes);
+
+bool mp_archive_next_entry(struct mp_archive *mpa);
diff --git a/stream/stream_memory.c b/stream/stream_memory.c
new file mode 100644
index 0000000..e4696a7
--- /dev/null
+++ b/stream/stream_memory.c
@@ -0,0 +1,100 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common/common.h"
+#include "stream.h"
+
+struct priv {
+ bstr data;
+};
+
+static int fill_buffer(stream_t *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+ bstr data = p->data;
+ if (s->pos < 0 || s->pos > data.len)
+ return 0;
+ len = MPMIN(len, data.len - s->pos);
+ memcpy(buffer, data.start + s->pos, len);
+ return len;
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ return 1;
+}
+
+static int64_t get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+ return p->data.len;
+}
+
+static int open2(stream_t *stream, const struct stream_open_args *args)
+{
+ stream->fill_buffer = fill_buffer;
+ stream->seek = seek;
+ stream->seekable = true;
+ stream->get_size = get_size;
+
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ // Initial data
+ bstr data = bstr0(stream->url);
+ bool use_hex = bstr_eatstart0(&data, "hex://");
+ if (!use_hex)
+ bstr_eatstart0(&data, "memory://");
+
+ if (args->special_arg)
+ data = *(bstr *)args->special_arg;
+
+ p->data = bstrdup(stream, data);
+
+ if (use_hex && !bstr_decode_hex(stream, p->data, &p->data)) {
+ MP_FATAL(stream, "Invalid data.\n");
+ return STREAM_ERROR;
+ }
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_memory = {
+ .name = "memory",
+ .open2 = open2,
+ .protocols = (const char*const[]){ "memory", "hex", NULL },
+};
+
+// The data is copied.
+// Caller may need to set stream.stream_origin correctly.
+struct stream *stream_memory_open(struct mpv_global *global, void *data, int len)
+{
+ assert(len >= 0);
+
+ struct stream_open_args sargs = {
+ .global = global,
+ .url = "memory://",
+ .flags = STREAM_READ | STREAM_SILENT | STREAM_ORIGIN_DIRECT,
+ .sinfo = &stream_info_memory,
+ .special_arg = &(bstr){data, len},
+ };
+
+ struct stream *s = NULL;
+ stream_create_with_args(&sargs, &s);
+ MP_HANDLE_OOM(s);
+ return s;
+}
diff --git a/stream/stream_mf.c b/stream/stream_mf.c
new file mode 100644
index 0000000..0160d7c
--- /dev/null
+++ b/stream/stream_mf.c
@@ -0,0 +1,41 @@
+/*
+ * stream layer for multiple files input, based on previous work from Albeu
+ *
+ * Copyright (C) 2006 Benjamin Zores
+ * Original author: Albeu
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "stream.h"
+
+static int
+mf_stream_open (stream_t *stream)
+{
+ stream->demuxer = "mf";
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_mf = {
+ .name = "mf",
+ .open = mf_stream_open,
+ .protocols = (const char*const[]){ "mf", NULL },
+ .stream_origin = STREAM_ORIGIN_FS,
+};
diff --git a/stream/stream_null.c b/stream/stream_null.c
new file mode 100644
index 0000000..4027c1b
--- /dev/null
+++ b/stream/stream_null.c
@@ -0,0 +1,35 @@
+/*
+ * Original author: Albeu
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "stream.h"
+
+static int open_s(stream_t *stream)
+{
+ return 1;
+}
+
+const stream_info_t stream_info_null = {
+ .name = "null",
+ .open = open_s,
+ .protocols = (const char*const[]){ "null", NULL },
+ .can_write = true,
+};
diff --git a/stream/stream_slice.c b/stream/stream_slice.c
new file mode 100644
index 0000000..c0dbeeb
--- /dev/null
+++ b/stream/stream_slice.c
@@ -0,0 +1,181 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/common.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "common/common.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "stream.h"
+
+struct priv {
+ int64_t slice_start;
+ int64_t slice_max_end; // 0 for no limit
+ struct stream *inner;
+};
+
+static int fill_buffer(struct stream *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+
+ if (p->slice_max_end) {
+ // We don't simply use (s->pos >= size) to avoid early return
+ // if the file is still being appended to.
+ if (s->pos + p->slice_start >= p->slice_max_end)
+ return -1;
+ // Avoid rading beyond p->slice_max_end
+ len = MPMIN(len, p->slice_max_end - s->pos);
+ }
+
+ return stream_read_partial(p->inner, buffer, len);
+}
+
+static int seek(struct stream *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ return stream_seek(p->inner, newpos + p->slice_start);
+}
+
+static int64_t get_size(struct stream *s)
+{
+ struct priv *p = s->priv;
+ int64_t size = stream_get_size(p->inner);
+ if (size <= 0)
+ return size;
+ if (size <= p->slice_start)
+ return 0;
+ if (p->slice_max_end)
+ size = MPMIN(size, p->slice_max_end);
+ return size - p->slice_start;
+}
+
+static void s_close(struct stream *s)
+{
+ struct priv *p = s->priv;
+ free_stream(p->inner);
+}
+
+static int parse_slice_range(stream_t *stream)
+{
+ struct priv *p = stream->priv;
+
+ struct bstr b_url = bstr0(stream->url);
+ struct bstr proto_with_range, inner_url;
+
+ bool has_at = bstr_split_tok(b_url, "@", &proto_with_range, &inner_url);
+
+ if (!has_at) {
+ MP_ERR(stream, "Expected slice://start[-end]@URL: '%s'\n", stream->url);
+ return STREAM_ERROR;
+ }
+
+ if (!inner_url.len) {
+ MP_ERR(stream, "URL expected to follow 'slice://start[-end]@': '%s'.\n", stream->url);
+ return STREAM_ERROR;
+ }
+ stream->path = bstrto0(stream, inner_url);
+
+ mp_split_proto(proto_with_range, &proto_with_range);
+ struct bstr range = proto_with_range;
+
+ struct bstr start, end;
+ bool has_end = bstr_split_tok(range, "-", &start, &end);
+
+ if (!start.len) {
+ MP_ERR(stream, "The byte range must have a start, and it can't be negative: '%s'\n", stream->url);
+ return STREAM_ERROR;
+ }
+
+ if (has_end && !end.len) {
+ MP_ERR(stream, "The byte range end can be omitted, but it can't be empty: '%s'\n", stream->url);
+ return STREAM_ERROR;
+ }
+
+ const struct m_option opt = {
+ .type = &m_option_type_byte_size,
+ };
+
+ if (m_option_parse(stream->log, &opt, bstr0("slice_start"), start, &p->slice_start) < 0)
+ return STREAM_ERROR;
+
+ bool max_end_is_offset = bstr_startswith0(end, "+");
+ if (has_end) {
+ if (m_option_parse(stream->log, &opt, bstr0("slice_max_end"), end, &p->slice_max_end) < 0)
+ return STREAM_ERROR;
+ }
+
+ if (max_end_is_offset)
+ p->slice_max_end += p->slice_start;
+
+ if (p->slice_max_end && p->slice_max_end < p->slice_start) {
+ MP_ERR(stream, "The byte range end (%"PRId64") can't be smaller than the start (%"PRId64"): '%s'\n",
+ p->slice_max_end,
+ p->slice_start,
+ stream->url);
+ return STREAM_ERROR;
+ }
+
+ return STREAM_OK;
+}
+
+static int open2(struct stream *stream, const struct stream_open_args *args)
+{
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ stream->fill_buffer = fill_buffer;
+ stream->seek = seek;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ int parse_ret = parse_slice_range(stream);
+ if (parse_ret != STREAM_OK) {
+ return parse_ret;
+ }
+
+ struct stream_open_args args2 = *args;
+ args2.url = stream->path;
+ int inner_ret = stream_create_with_args(&args2, &p->inner);
+ if (inner_ret != STREAM_OK) {
+ return inner_ret;
+ }
+
+ if (!p->inner->seekable) {
+ MP_FATAL(stream, "Non-seekable stream '%s' can't be used with 'slice://'\n", p->inner->url);
+ free_stream(p->inner);
+ return STREAM_ERROR;
+ }
+
+ stream->seekable = 1;
+ stream->stream_origin = p->inner->stream_origin;
+
+ if (p->slice_start)
+ seek(stream, 0);
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_slice = {
+ .name = "slice",
+ .open2 = open2,
+ .protocols = (const char*const[]){ "slice", NULL },
+ .can_write = false,
+};
diff --git a/sub/ass_mp.c b/sub/ass_mp.c
new file mode 100644
index 0000000..634681f
--- /dev/null
+++ b/sub/ass_mp.c
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <math.h>
+
+#include <ass/ass.h>
+#include <ass/ass_types.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "options/path.h"
+#include "ass_mp.h"
+#include "img_convert.h"
+#include "osd.h"
+#include "stream/stream.h"
+#include "options/options.h"
+#include "video/out/bitmap_packer.h"
+#include "video/mp_image.h"
+
+// res_y should be track->PlayResY
+// It determines scaling of font sizes and more.
+void mp_ass_set_style(ASS_Style *style, double res_y,
+ const struct osd_style_opts *opts)
+{
+ if (!style)
+ return;
+
+ if (opts->font) {
+ if (!style->FontName || strcmp(style->FontName, opts->font) != 0) {
+ free(style->FontName);
+ style->FontName = strdup(opts->font);
+ }
+ }
+
+ // libass_font_size = FontSize * (window_height / res_y)
+ // scale translates parameters from PlayResY=720 to res_y
+ double scale = res_y / 720.0;
+
+ style->FontSize = opts->font_size * scale;
+ style->PrimaryColour = MP_ASS_COLOR(opts->color);
+ style->SecondaryColour = style->PrimaryColour;
+ style->OutlineColour = MP_ASS_COLOR(opts->border_color);
+ if (opts->back_color.a) {
+ style->BackColour = MP_ASS_COLOR(opts->back_color);
+ style->BorderStyle = 4; // opaque box
+ } else {
+ style->BackColour = MP_ASS_COLOR(opts->shadow_color);
+ style->BorderStyle = 1; // outline
+ }
+ style->Outline = opts->border_size * scale;
+ style->Shadow = opts->shadow_offset * scale;
+ style->Spacing = opts->spacing * scale;
+ style->MarginL = opts->margin_x * scale;
+ style->MarginR = style->MarginL;
+ style->MarginV = opts->margin_y * scale;
+ style->ScaleX = 1.;
+ style->ScaleY = 1.;
+ style->Alignment = 1 + (opts->align_x + 1) + (opts->align_y + 2) % 3 * 4;
+#ifdef ASS_JUSTIFY_LEFT
+ style->Justify = opts->justify;
+#endif
+ style->Blur = opts->blur;
+ style->Bold = opts->bold;
+ style->Italic = opts->italic;
+}
+
+void mp_ass_configure_fonts(ASS_Renderer *priv, struct osd_style_opts *opts,
+ struct mpv_global *global, struct mp_log *log)
+{
+ void *tmp = talloc_new(NULL);
+ char *default_font = mp_find_config_file(tmp, global, "subfont.ttf");
+ char *config = mp_find_config_file(tmp, global, "fonts.conf");
+
+ if (default_font && !mp_path_exists(default_font))
+ default_font = NULL;
+
+ int font_provider = ASS_FONTPROVIDER_AUTODETECT;
+ if (opts->font_provider == 1)
+ font_provider = ASS_FONTPROVIDER_NONE;
+ if (opts->font_provider == 2)
+ font_provider = ASS_FONTPROVIDER_FONTCONFIG;
+
+ mp_verbose(log, "Setting up fonts...\n");
+ ass_set_fonts(priv, default_font, opts->font, font_provider, config, 1);
+ mp_verbose(log, "Done.\n");
+
+ talloc_free(tmp);
+}
+
+static const int map_ass_level[] = {
+ MSGL_ERR, // 0 "FATAL errors"
+ MSGL_WARN,
+ MSGL_INFO,
+ MSGL_V,
+ MSGL_V,
+ MSGL_DEBUG, // 5 application recommended level
+ MSGL_TRACE,
+ MSGL_TRACE, // 7 "verbose DEBUG"
+};
+
+static void message_callback(int level, const char *format, va_list va, void *ctx)
+{
+ struct mp_log *log = ctx;
+ if (!log)
+ return;
+ level = map_ass_level[level];
+ mp_msg_va(log, level, format, va);
+ // libass messages lack trailing \n
+ mp_msg(log, level, "\n");
+}
+
+ASS_Library *mp_ass_init(struct mpv_global *global,
+ struct osd_style_opts *opts, struct mp_log *log)
+{
+ char *path = opts->fonts_dir && opts->fonts_dir[0] ?
+ mp_get_user_path(NULL, global, opts->fonts_dir) :
+ mp_find_config_file(NULL, global, "fonts");
+ mp_dbg(log, "ASS library version: 0x%x (runtime 0x%x)\n",
+ (unsigned)LIBASS_VERSION, ass_library_version());
+ ASS_Library *priv = ass_library_init();
+ if (!priv)
+ abort();
+ ass_set_message_cb(priv, message_callback, log);
+ if (path)
+ ass_set_fonts_dir(priv, path);
+ talloc_free(path);
+ return priv;
+}
+
+void mp_ass_flush_old_events(ASS_Track *track, long long ts)
+{
+ int n = 0;
+ for (; n < track->n_events; n++) {
+ if ((track->events[n].Start + track->events[n].Duration) >= ts)
+ break;
+ ass_free_event(track, n);
+ track->n_events--;
+ }
+ for (int i = 0; n > 0 && i < track->n_events; i++) {
+ track->events[i] = track->events[i+n];
+ }
+}
+
+static void draw_ass_rgba(unsigned char *src, int src_w, int src_h,
+ int src_stride, unsigned char *dst, size_t dst_stride,
+ int dst_x, int dst_y, uint32_t color)
+{
+ const unsigned int r = (color >> 24) & 0xff;
+ const unsigned int g = (color >> 16) & 0xff;
+ const unsigned int b = (color >> 8) & 0xff;
+ const unsigned int a = 0xff - (color & 0xff);
+
+ dst += dst_y * dst_stride + dst_x * 4;
+
+ for (int y = 0; y < src_h; y++, dst += dst_stride, src += src_stride) {
+ uint32_t *dstrow = (uint32_t *) dst;
+ for (int x = 0; x < src_w; x++) {
+ const unsigned int v = src[x];
+ int rr = (r * a * v);
+ int gg = (g * a * v);
+ int bb = (b * a * v);
+ int aa = a * v;
+ uint32_t dstpix = dstrow[x];
+ unsigned int dstb = dstpix & 0xFF;
+ unsigned int dstg = (dstpix >> 8) & 0xFF;
+ unsigned int dstr = (dstpix >> 16) & 0xFF;
+ unsigned int dsta = (dstpix >> 24) & 0xFF;
+ dstb = (bb + dstb * (255 * 255 - aa)) / (255 * 255);
+ dstg = (gg + dstg * (255 * 255 - aa)) / (255 * 255);
+ dstr = (rr + dstr * (255 * 255 - aa)) / (255 * 255);
+ dsta = (aa * 255 + dsta * (255 * 255 - aa)) / (255 * 255);
+ dstrow[x] = dstb | (dstg << 8) | (dstr << 16) | (dsta << 24);
+ }
+ }
+}
+
+struct mp_ass_packer {
+ struct sub_bitmap *cached_parts; // only for the array memory
+ struct mp_image *cached_img;
+ struct sub_bitmaps cached_subs;
+ bool cached_subs_valid;
+ struct sub_bitmap rgba_imgs[MP_SUB_BB_LIST_MAX];
+ struct bitmap_packer *packer;
+};
+
+// Free with talloc_free().
+struct mp_ass_packer *mp_ass_packer_alloc(void *ta_parent)
+{
+ struct mp_ass_packer *p = talloc_zero(ta_parent, struct mp_ass_packer);
+ p->packer = talloc_zero(p, struct bitmap_packer);
+ return p;
+}
+
+static bool pack(struct mp_ass_packer *p, struct sub_bitmaps *res, int imgfmt)
+{
+ packer_set_size(p->packer, res->num_parts);
+
+ for (int n = 0; n < res->num_parts; n++)
+ p->packer->in[n] = (struct pos){res->parts[n].w, res->parts[n].h};
+
+ if (p->packer->count == 0 || packer_pack(p->packer) < 0)
+ return false;
+
+ struct pos bb[2];
+ packer_get_bb(p->packer, bb);
+
+ res->packed_w = bb[1].x;
+ res->packed_h = bb[1].y;
+
+ if (!p->cached_img || p->cached_img->w < res->packed_w ||
+ p->cached_img->h < res->packed_h ||
+ p->cached_img->imgfmt != imgfmt)
+ {
+ talloc_free(p->cached_img);
+ p->cached_img = mp_image_alloc(imgfmt, p->packer->w, p->packer->h);
+ if (!p->cached_img) {
+ packer_reset(p->packer);
+ return false;
+ }
+ talloc_steal(p, p->cached_img);
+ }
+
+ if (!mp_image_make_writeable(p->cached_img)) {
+ packer_reset(p->packer);
+ return false;
+ }
+
+ res->packed = p->cached_img;
+
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *b = &res->parts[n];
+ struct pos pos = p->packer->result[n];
+
+ b->src_x = pos.x;
+ b->src_y = pos.y;
+ }
+
+ return true;
+}
+
+static bool pack_libass(struct mp_ass_packer *p, struct sub_bitmaps *res)
+{
+ if (!pack(p, res, IMGFMT_Y8))
+ return false;
+
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *b = &res->parts[n];
+
+ int stride = res->packed->stride[0];
+ void *pdata =
+ (uint8_t *)res->packed->planes[0] + b->src_y * stride + b->src_x;
+ memcpy_pic(pdata, b->bitmap, b->w, b->h, stride, b->stride);
+
+ b->bitmap = pdata;
+ b->stride = stride;
+ }
+
+ return true;
+}
+
+static bool pack_rgba(struct mp_ass_packer *p, struct sub_bitmaps *res)
+{
+ struct mp_rect bb_list[MP_SUB_BB_LIST_MAX];
+ int num_bb = mp_get_sub_bb_list(res, bb_list, MP_SUB_BB_LIST_MAX);
+
+ struct sub_bitmaps imgs = {
+ .change_id = res->change_id,
+ .format = SUBBITMAP_BGRA,
+ .parts = p->rgba_imgs,
+ .num_parts = num_bb,
+ };
+
+ for (int n = 0; n < imgs.num_parts; n++) {
+ imgs.parts[n].w = bb_list[n].x1 - bb_list[n].x0;
+ imgs.parts[n].h = bb_list[n].y1 - bb_list[n].y0;
+ }
+
+ if (!pack(p, &imgs, IMGFMT_BGRA))
+ return false;
+
+ for (int n = 0; n < num_bb; n++) {
+ struct mp_rect bb = bb_list[n];
+ struct sub_bitmap *b = &imgs.parts[n];
+
+ b->x = bb.x0;
+ b->y = bb.y0;
+ b->w = b->dw = bb.x1 - bb.x0;
+ b->h = b->dh = bb.y1 - bb.y0;
+ b->stride = imgs.packed->stride[0];
+ b->bitmap = (uint8_t *)imgs.packed->planes[0] +
+ b->stride * b->src_y + b->src_x * 4;
+
+ memset_pic(b->bitmap, 0, b->w * 4, b->h, b->stride);
+
+ for (int i = 0; i < res->num_parts; i++) {
+ struct sub_bitmap *s = &res->parts[i];
+
+ // Assume mp_get_sub_bb_list() never splits sub bitmaps
+ // So we don't clip/adjust the size of the sub bitmap
+ if (s->x > bb.x1 || s->x + s->w < bb.x0 ||
+ s->y > bb.y1 || s->y + s->h < bb.y0)
+ continue;
+
+ draw_ass_rgba(s->bitmap, s->w, s->h, s->stride,
+ b->bitmap, b->stride,
+ s->x - bb.x0, s->y - bb.y0,
+ s->libass.color);
+ }
+ }
+
+ *res = imgs;
+ return true;
+}
+
+// Pack the contents of image_lists[0] to image_lists[num_image_lists-1] into
+// a single image, and make *out point to it. *out is completely overwritten.
+// If libass reported any change, image_lists_changed must be set (it then
+// repacks all images). preferred_osd_format can be set to a desired
+// sub_bitmap_format. Currently, only SUBBITMAP_LIBASS is supported.
+void mp_ass_packer_pack(struct mp_ass_packer *p, ASS_Image **image_lists,
+ int num_image_lists, bool image_lists_changed,
+ int preferred_osd_format, struct sub_bitmaps *out)
+{
+ int format = preferred_osd_format == SUBBITMAP_BGRA ? SUBBITMAP_BGRA
+ : SUBBITMAP_LIBASS;
+
+ if (p->cached_subs_valid && !image_lists_changed &&
+ p->cached_subs.format == format)
+ {
+ *out = p->cached_subs;
+ return;
+ }
+
+ *out = (struct sub_bitmaps){.change_id = 1};
+ p->cached_subs_valid = false;
+
+ struct sub_bitmaps res = {
+ .change_id = image_lists_changed,
+ .format = SUBBITMAP_LIBASS,
+ .parts = p->cached_parts,
+ };
+
+ for (int n = 0; n < num_image_lists; n++) {
+ for (struct ass_image *img = image_lists[n]; img; img = img->next) {
+ if (img->w == 0 || img->h == 0)
+ continue;
+ MP_TARRAY_GROW(p, p->cached_parts, res.num_parts);
+ res.parts = p->cached_parts;
+ struct sub_bitmap *b = &res.parts[res.num_parts];
+ b->bitmap = img->bitmap;
+ b->stride = img->stride;
+ b->libass.color = img->color;
+ b->dw = b->w = img->w;
+ b->dh = b->h = img->h;
+ b->x = img->dst_x;
+ b->y = img->dst_y;
+ res.num_parts++;
+ }
+ }
+
+ bool r = false;
+ if (format == SUBBITMAP_BGRA) {
+ r = pack_rgba(p, &res);
+ } else {
+ r = pack_libass(p, &res);
+ }
+
+ if (!r)
+ return;
+
+ *out = res;
+ p->cached_subs = res;
+ p->cached_subs.change_id = 0;
+ p->cached_subs_valid = true;
+}
+
+// Set *out_rc to [x0, y0, x1, y1] of the graphical bounding box in script
+// coordinates.
+// Set it to [inf, inf, -inf, -inf] if empty.
+void mp_ass_get_bb(ASS_Image *image_list, ASS_Track *track,
+ struct mp_osd_res *res, double *out_rc)
+{
+ double rc[4] = {INFINITY, INFINITY, -INFINITY, -INFINITY};
+
+ for (ASS_Image *img = image_list; img; img = img->next) {
+ if (img->w == 0 || img->h == 0)
+ continue;
+ rc[0] = MPMIN(rc[0], img->dst_x);
+ rc[1] = MPMIN(rc[1], img->dst_y);
+ rc[2] = MPMAX(rc[2], img->dst_x + img->w);
+ rc[3] = MPMAX(rc[3], img->dst_y + img->h);
+ }
+
+ double scale = track->PlayResY / (double)MPMAX(res->h, 1);
+ if (scale > 0) {
+ for (int i = 0; i < 4; i++)
+ out_rc[i] = rc[i] * scale;
+ }
+}
diff --git a/sub/ass_mp.h b/sub/ass_mp.h
new file mode 100644
index 0000000..dc83e31
--- /dev/null
+++ b/sub/ass_mp.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_ASS_MP_H
+#define MPLAYER_ASS_MP_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <ass/ass.h>
+#include <ass/ass_types.h>
+
+// These PlayResX and PlayResY values are arbitrary and taken from lavc.
+// lavc assumes these values when converting to ass generally. Moreover, these
+// values are also used by default in VSFilter, so it isn't that arbitrary.
+#define MP_ASS_FONT_PLAYRESX 384
+#define MP_ASS_FONT_PLAYRESY 288
+
+#define MP_ASS_RGBA(r, g, b, a) \
+ (((unsigned)(r) << 24) | ((g) << 16) | ((b) << 8) | (0xFF - (a)))
+
+// m_color argument
+#define MP_ASS_COLOR(c) MP_ASS_RGBA((c).r, (c).g, (c).b, (c).a)
+
+struct MPOpts;
+struct mpv_global;
+struct mp_osd_res;
+struct osd_style_opts;
+struct mp_log;
+
+void mp_ass_flush_old_events(ASS_Track *track, long long ts);
+void mp_ass_set_style(ASS_Style *style, double res_y,
+ const struct osd_style_opts *opts);
+
+void mp_ass_configure_fonts(ASS_Renderer *priv, struct osd_style_opts *opts,
+ struct mpv_global *global, struct mp_log *log);
+ASS_Library *mp_ass_init(struct mpv_global *global,
+ struct osd_style_opts *opts, struct mp_log *log);
+
+struct sub_bitmaps;
+struct mp_ass_packer;
+struct mp_ass_packer *mp_ass_packer_alloc(void *ta_parent);
+void mp_ass_packer_pack(struct mp_ass_packer *p, ASS_Image **image_lists,
+ int num_image_lists, bool changed,
+ int preferred_osd_format, struct sub_bitmaps *out);
+void mp_ass_get_bb(ASS_Image *image_list, ASS_Track *track,
+ struct mp_osd_res *res, double *out_rc);
+
+#endif /* MPLAYER_ASS_MP_H */
diff --git a/sub/dec_sub.c b/sub/dec_sub.c
new file mode 100644
index 0000000..18d826e
--- /dev/null
+++ b/sub/dec_sub.c
@@ -0,0 +1,498 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "demux/demux.h"
+#include "sd.h"
+#include "dec_sub.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/recorder.h"
+#include "misc/dispatch.h"
+#include "osdep/threads.h"
+
+extern const struct sd_functions sd_ass;
+extern const struct sd_functions sd_lavc;
+
+static const struct sd_functions *const sd_list[] = {
+ &sd_lavc,
+ &sd_ass,
+ NULL
+};
+
+struct dec_sub {
+ mp_mutex lock;
+
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct mp_subtitle_opts *opts;
+ struct m_config_cache *opts_cache;
+
+ struct mp_recorder_sink *recorder_sink;
+
+ struct attachment_list *attachments;
+
+ struct sh_stream *sh;
+ int play_dir;
+ int order;
+ double last_pkt_pts;
+ bool preload_attempted;
+ double video_fps;
+ double sub_speed;
+
+ struct mp_codec_params *codec;
+ double start, end;
+
+ double last_vo_pts;
+ struct sd *sd;
+
+ struct demux_packet *new_segment;
+ struct demux_packet *cached_pkts[2];
+};
+
+static void update_subtitle_speed(struct dec_sub *sub)
+{
+ struct mp_subtitle_opts *opts = sub->opts;
+ sub->sub_speed = 1.0;
+
+ if (sub->video_fps > 0 && sub->codec->frame_based > 0) {
+ MP_VERBOSE(sub, "Frame based format, dummy FPS: %f, video FPS: %f\n",
+ sub->codec->frame_based, sub->video_fps);
+ sub->sub_speed *= sub->codec->frame_based / sub->video_fps;
+ }
+
+ if (opts->sub_fps && sub->video_fps)
+ sub->sub_speed *= opts->sub_fps / sub->video_fps;
+
+ sub->sub_speed *= opts->sub_speed;
+}
+
+// Return the subtitle PTS used for a given video PTS.
+static double pts_to_subtitle(struct dec_sub *sub, double pts)
+{
+ struct mp_subtitle_opts *opts = sub->opts;
+
+ if (pts != MP_NOPTS_VALUE)
+ pts = (pts * sub->play_dir - opts->sub_delay) / sub->sub_speed;
+
+ return pts;
+}
+
+static double pts_from_subtitle(struct dec_sub *sub, double pts)
+{
+ struct mp_subtitle_opts *opts = sub->opts;
+
+ if (pts != MP_NOPTS_VALUE)
+ pts = (pts * sub->sub_speed + opts->sub_delay) * sub->play_dir;
+
+ return pts;
+}
+
+static void wakeup_demux(void *ctx)
+{
+ struct mp_dispatch_queue *q = ctx;
+ mp_dispatch_interrupt(q);
+}
+
+void sub_destroy(struct dec_sub *sub)
+{
+ if (!sub)
+ return;
+ demux_set_stream_wakeup_cb(sub->sh, NULL, NULL);
+ if (sub->sd) {
+ sub_reset(sub);
+ sub->sd->driver->uninit(sub->sd);
+ }
+ talloc_free(sub->sd);
+ mp_mutex_destroy(&sub->lock);
+ talloc_free(sub);
+}
+
+static struct sd *init_decoder(struct dec_sub *sub)
+{
+ for (int n = 0; sd_list[n]; n++) {
+ const struct sd_functions *driver = sd_list[n];
+ struct sd *sd = talloc(NULL, struct sd);
+ *sd = (struct sd){
+ .global = sub->global,
+ .log = mp_log_new(sd, sub->log, driver->name),
+ .opts = sub->opts,
+ .driver = driver,
+ .attachments = sub->attachments,
+ .codec = sub->codec,
+ .preload_ok = true,
+ };
+
+ if (sd->driver->init(sd) >= 0)
+ return sd;
+
+ talloc_free(sd);
+ }
+
+ MP_ERR(sub, "Could not find subtitle decoder for format '%s'.\n",
+ sub->codec->codec);
+ return NULL;
+}
+
+// Thread-safety of the returned object: all functions are thread-safe,
+// except sub_get_bitmaps() and sub_get_text(). Decoder backends (sd_*)
+// do not need to acquire locks.
+// Ownership of attachments goes to the callee, and is released with
+// talloc_free() (even on failure).
+struct dec_sub *sub_create(struct mpv_global *global, struct track *track,
+ struct attachment_list *attachments, int order)
+{
+ assert(track->stream && track->stream->type == STREAM_SUB);
+
+ struct dec_sub *sub = talloc(NULL, struct dec_sub);
+ *sub = (struct dec_sub){
+ .log = mp_log_new(sub, global->log, "sub"),
+ .global = global,
+ .opts_cache = m_config_cache_alloc(sub, global, &mp_subtitle_sub_opts),
+ .sh = track->stream,
+ .codec = track->stream->codec,
+ .attachments = talloc_steal(sub, attachments),
+ .play_dir = 1,
+ .order = order,
+ .last_pkt_pts = MP_NOPTS_VALUE,
+ .last_vo_pts = MP_NOPTS_VALUE,
+ .start = MP_NOPTS_VALUE,
+ .end = MP_NOPTS_VALUE,
+ };
+ sub->opts = sub->opts_cache->opts;
+ mp_mutex_init_type(&sub->lock, MP_MUTEX_RECURSIVE);
+
+ sub->sd = init_decoder(sub);
+ if (sub->sd) {
+ update_subtitle_speed(sub);
+ return sub;
+ }
+
+ sub_destroy(sub);
+ return NULL;
+}
+
+// Called locked.
+static void update_segment(struct dec_sub *sub)
+{
+ if (sub->new_segment && sub->last_vo_pts != MP_NOPTS_VALUE &&
+ sub->last_vo_pts >= sub->new_segment->start)
+ {
+ MP_VERBOSE(sub, "Switch segment: %f at %f\n", sub->new_segment->start,
+ sub->last_vo_pts);
+
+ sub->codec = sub->new_segment->codec;
+ sub->start = sub->new_segment->start;
+ sub->end = sub->new_segment->end;
+ struct sd *new = init_decoder(sub);
+ if (new) {
+ sub->sd->driver->uninit(sub->sd);
+ talloc_free(sub->sd);
+ sub->sd = new;
+ update_subtitle_speed(sub);
+ sub_control(sub, SD_CTRL_SET_TOP, &sub->order);
+ } else {
+ // We'll just keep the current decoder, and feed it possibly
+ // invalid data (not our fault if it crashes or something).
+ MP_ERR(sub, "Can't change to new codec.\n");
+ }
+ sub->sd->driver->decode(sub->sd, sub->new_segment);
+ talloc_free(sub->new_segment);
+ sub->new_segment = NULL;
+ }
+}
+
+bool sub_can_preload(struct dec_sub *sub)
+{
+ bool r;
+ mp_mutex_lock(&sub->lock);
+ r = sub->sd->driver->accept_packets_in_advance && !sub->preload_attempted;
+ mp_mutex_unlock(&sub->lock);
+ return r;
+}
+
+void sub_preload(struct dec_sub *sub)
+{
+ mp_mutex_lock(&sub->lock);
+
+ struct mp_dispatch_queue *demux_waiter = mp_dispatch_create(NULL);
+ demux_set_stream_wakeup_cb(sub->sh, wakeup_demux, demux_waiter);
+
+ sub->preload_attempted = true;
+
+ for (;;) {
+ struct demux_packet *pkt = NULL;
+ int r = demux_read_packet_async(sub->sh, &pkt);
+ if (r == 0) {
+ mp_dispatch_queue_process(demux_waiter, INFINITY);
+ continue;
+ }
+ if (!pkt)
+ break;
+ sub->sd->driver->decode(sub->sd, pkt);
+ talloc_free(pkt);
+ }
+
+ demux_set_stream_wakeup_cb(sub->sh, NULL, NULL);
+ talloc_free(demux_waiter);
+
+ mp_mutex_unlock(&sub->lock);
+}
+
+static bool is_new_segment(struct dec_sub *sub, struct demux_packet *p)
+{
+ return p->segmented &&
+ (p->start != sub->start || p->end != sub->end || p->codec != sub->codec);
+}
+
+// Read packets from the demuxer stream passed to sub_create(). Return true if
+// enough packets were read, false if the player should wait until the demuxer
+// signals new packets available (and then should retry).
+bool sub_read_packets(struct dec_sub *sub, double video_pts, bool force)
+{
+ bool r = true;
+ mp_mutex_lock(&sub->lock);
+ video_pts = pts_to_subtitle(sub, video_pts);
+ while (1) {
+ bool read_more = true;
+ if (sub->sd->driver->accepts_packet)
+ read_more = sub->sd->driver->accepts_packet(sub->sd, video_pts);
+
+ if (!read_more)
+ break;
+
+ if (sub->new_segment && sub->new_segment->start < video_pts) {
+ sub->last_vo_pts = video_pts;
+ update_segment(sub);
+ }
+
+ if (sub->new_segment)
+ break;
+
+ // (Use this mechanism only if sub_delay matters to avoid corner cases.)
+ double min_pts = sub->opts->sub_delay < 0 || force ? video_pts : MP_NOPTS_VALUE;
+
+ struct demux_packet *pkt;
+ int st = demux_read_packet_async_until(sub->sh, min_pts, &pkt);
+ // Note: "wait" (st==0) happens with non-interleaved streams only, and
+ // then we should stop the playloop until a new enough packet has been
+ // seen (or the subtitle decoder's queue is full). This usually does not
+ // happen for interleaved subtitle streams, which never return "wait"
+ // when reading, unless min_pts is set.
+ if (st <= 0) {
+ r = st < 0 || (sub->last_pkt_pts != MP_NOPTS_VALUE &&
+ sub->last_pkt_pts > video_pts);
+ break;
+ }
+
+ if (sub->recorder_sink)
+ mp_recorder_feed_packet(sub->recorder_sink, pkt);
+
+
+ // Update cached packets
+ if (sub->cached_pkts[0]) {
+ if (sub->cached_pkts[1])
+ talloc_free(sub->cached_pkts[1]);
+ sub->cached_pkts[1] = sub->cached_pkts[0];
+ }
+ sub->cached_pkts[0] = pkt;
+
+ sub->last_pkt_pts = pkt->pts;
+
+ if (is_new_segment(sub, pkt)) {
+ sub->new_segment = demux_copy_packet(pkt);
+ // Note that this can be delayed to a much later point in time.
+ update_segment(sub);
+ break;
+ }
+
+ if (!(sub->preload_attempted && sub->sd->preload_ok))
+ sub->sd->driver->decode(sub->sd, pkt);
+ }
+ mp_mutex_unlock(&sub->lock);
+ return r;
+}
+
+// Redecode both cached packets if needed.
+// Used with UPDATE_SUB_HARD and UPDATE_SUB_FILT.
+void sub_redecode_cached_packets(struct dec_sub *sub)
+{
+ mp_mutex_lock(&sub->lock);
+ if (sub->cached_pkts[0])
+ sub->sd->driver->decode(sub->sd, sub->cached_pkts[0]);
+ if (sub->cached_pkts[1])
+ sub->sd->driver->decode(sub->sd, sub->cached_pkts[1]);
+ mp_mutex_unlock(&sub->lock);
+}
+
+// Unref sub_bitmaps.rc to free the result. May return NULL.
+struct sub_bitmaps *sub_get_bitmaps(struct dec_sub *sub, struct mp_osd_res dim,
+ int format, double pts)
+{
+ mp_mutex_lock(&sub->lock);
+
+ pts = pts_to_subtitle(sub, pts);
+
+ sub->last_vo_pts = pts;
+ update_segment(sub);
+
+ struct sub_bitmaps *res = NULL;
+
+ if (!(sub->end != MP_NOPTS_VALUE && pts >= sub->end) &&
+ sub->sd->driver->get_bitmaps)
+ res = sub->sd->driver->get_bitmaps(sub->sd, dim, format, pts);
+
+ mp_mutex_unlock(&sub->lock);
+ return res;
+}
+
+// The returned string is talloc'ed.
+char *sub_get_text(struct dec_sub *sub, double pts, enum sd_text_type type)
+{
+ mp_mutex_lock(&sub->lock);
+ char *text = NULL;
+
+ pts = pts_to_subtitle(sub, pts);
+
+ sub->last_vo_pts = pts;
+ update_segment(sub);
+
+ if (sub->sd->driver->get_text)
+ text = sub->sd->driver->get_text(sub->sd, pts, type);
+ mp_mutex_unlock(&sub->lock);
+ return text;
+}
+
+char *sub_ass_get_extradata(struct dec_sub *sub)
+{
+ if (strcmp(sub->sd->codec->codec, "ass") != 0)
+ return NULL;
+ char *extradata = sub->sd->codec->extradata;
+ int extradata_size = sub->sd->codec->extradata_size;
+ return talloc_strndup(NULL, extradata, extradata_size);
+}
+
+struct sd_times sub_get_times(struct dec_sub *sub, double pts)
+{
+ mp_mutex_lock(&sub->lock);
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+
+ pts = pts_to_subtitle(sub, pts);
+
+ sub->last_vo_pts = pts;
+ update_segment(sub);
+
+ if (sub->sd->driver->get_times)
+ res = sub->sd->driver->get_times(sub->sd, pts);
+
+ mp_mutex_unlock(&sub->lock);
+ return res;
+}
+
+void sub_reset(struct dec_sub *sub)
+{
+ mp_mutex_lock(&sub->lock);
+ if (sub->sd->driver->reset)
+ sub->sd->driver->reset(sub->sd);
+ sub->last_pkt_pts = MP_NOPTS_VALUE;
+ sub->last_vo_pts = MP_NOPTS_VALUE;
+ TA_FREEP(&sub->cached_pkts[0]);
+ TA_FREEP(&sub->cached_pkts[1]);
+ TA_FREEP(&sub->new_segment);
+ mp_mutex_unlock(&sub->lock);
+}
+
+void sub_select(struct dec_sub *sub, bool selected)
+{
+ mp_mutex_lock(&sub->lock);
+ if (sub->sd->driver->select)
+ sub->sd->driver->select(sub->sd, selected);
+ mp_mutex_unlock(&sub->lock);
+}
+
+int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg)
+{
+ int r = CONTROL_UNKNOWN;
+ mp_mutex_lock(&sub->lock);
+ bool propagate = false;
+ switch (cmd) {
+ case SD_CTRL_SET_VIDEO_DEF_FPS:
+ sub->video_fps = *(double *)arg;
+ update_subtitle_speed(sub);
+ break;
+ case SD_CTRL_SUB_STEP: {
+ double *a = arg;
+ double arg2[2] = {a[0], a[1]};
+ arg2[0] = pts_to_subtitle(sub, arg2[0]);
+ if (sub->sd->driver->control)
+ r = sub->sd->driver->control(sub->sd, cmd, arg2);
+ if (r == CONTROL_OK)
+ a[0] = pts_from_subtitle(sub, arg2[0]);
+ break;
+ }
+ case SD_CTRL_UPDATE_OPTS: {
+ int flags = (uintptr_t)arg;
+ if (m_config_cache_update(sub->opts_cache))
+ update_subtitle_speed(sub);
+ propagate = true;
+ if (flags & UPDATE_SUB_HARD) {
+ // forget about the previous preload because
+ // UPDATE_SUB_HARD will cause a sub reinit
+ // that clears all preloaded sub packets
+ sub->preload_attempted = false;
+ }
+ break;
+ }
+ default:
+ propagate = true;
+ }
+ if (propagate && sub->sd->driver->control)
+ r = sub->sd->driver->control(sub->sd, cmd, arg);
+ mp_mutex_unlock(&sub->lock);
+ return r;
+}
+
+void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink)
+{
+ mp_mutex_lock(&sub->lock);
+ sub->recorder_sink = sink;
+ mp_mutex_unlock(&sub->lock);
+}
+
+void sub_set_play_dir(struct dec_sub *sub, int dir)
+{
+ mp_mutex_lock(&sub->lock);
+ sub->play_dir = dir;
+ mp_mutex_unlock(&sub->lock);
+}
+
+bool sub_is_primary_visible(struct dec_sub *sub)
+{
+ return !!sub->opts->sub_visibility;
+}
+
+bool sub_is_secondary_visible(struct dec_sub *sub)
+{
+ return !!sub->opts->sec_sub_visibility;
+}
diff --git a/sub/dec_sub.h b/sub/dec_sub.h
new file mode 100644
index 0000000..9de6760
--- /dev/null
+++ b/sub/dec_sub.h
@@ -0,0 +1,62 @@
+#ifndef MPLAYER_DEC_SUB_H
+#define MPLAYER_DEC_SUB_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "player/core.h"
+#include "osd.h"
+
+struct sh_stream;
+struct mpv_global;
+struct demux_packet;
+struct mp_recorder_sink;
+struct dec_sub;
+struct sd;
+
+enum sd_ctrl {
+ SD_CTRL_SUB_STEP,
+ SD_CTRL_SET_VIDEO_PARAMS,
+ SD_CTRL_SET_TOP,
+ SD_CTRL_SET_VIDEO_DEF_FPS,
+ SD_CTRL_UPDATE_OPTS,
+};
+
+enum sd_text_type {
+ SD_TEXT_TYPE_PLAIN,
+ SD_TEXT_TYPE_ASS,
+};
+
+struct sd_times {
+ double start;
+ double end;
+};
+
+struct attachment_list {
+ struct demux_attachment *entries;
+ int num_entries;
+};
+
+struct dec_sub *sub_create(struct mpv_global *global, struct track *track,
+ struct attachment_list *attachments, int order);
+void sub_destroy(struct dec_sub *sub);
+
+bool sub_can_preload(struct dec_sub *sub);
+void sub_preload(struct dec_sub *sub);
+void sub_redecode_cached_packets(struct dec_sub *sub);
+bool sub_read_packets(struct dec_sub *sub, double video_pts, bool force);
+struct sub_bitmaps *sub_get_bitmaps(struct dec_sub *sub, struct mp_osd_res dim,
+ int format, double pts);
+char *sub_get_text(struct dec_sub *sub, double pts, enum sd_text_type type);
+char *sub_ass_get_extradata(struct dec_sub *sub);
+struct sd_times sub_get_times(struct dec_sub *sub, double pts);
+void sub_reset(struct dec_sub *sub);
+void sub_select(struct dec_sub *sub, bool selected);
+void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink);
+void sub_set_play_dir(struct dec_sub *sub, int dir);
+bool sub_is_primary_visible(struct dec_sub *sub);
+bool sub_is_secondary_visible(struct dec_sub *sub);
+
+int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg);
+
+#endif
diff --git a/sub/draw_bmp.c b/sub/draw_bmp.c
new file mode 100644
index 0000000..58db162
--- /dev/null
+++ b/sub/draw_bmp.c
@@ -0,0 +1,1035 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <math.h>
+#include <inttypes.h>
+
+#include "common/common.h"
+#include "draw_bmp.h"
+#include "img_convert.h"
+#include "video/mp_image.h"
+#include "video/repack.h"
+#include "video/sws_utils.h"
+#include "video/img_format.h"
+#include "video/csputils.h"
+
+const bool mp_draw_sub_formats[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_LIBASS] = true,
+ [SUBBITMAP_BGRA] = true,
+};
+
+struct part {
+ int change_id;
+ // Sub-bitmaps scaled to final sizes.
+ int num_imgs;
+ struct mp_image **imgs;
+};
+
+// Must be a power of 2. Height is 1, but mark_rect() effectively operates on
+// multiples of chroma sized macro-pixels. (E.g. 4:2:0 -> every second line is
+// the same as the previous one, and x0%2==x1%2==0.)
+#define SLICE_W 256u
+
+// Whether to scale in tiles. Faster, but can't use correct chroma position.
+// Should be a runtime option. SLICE_W is used as tile width. The tile size
+// should probably be small; too small or too big will cause overhead when
+// scaling.
+#define SCALE_IN_TILES 1
+#define TILE_H 4u
+
+struct slice {
+ uint16_t x0, x1;
+};
+
+struct mp_draw_sub_cache
+{
+ struct mpv_global *global;
+
+ // Possibly cached parts. Also implies what's in the video_overlay.
+ struct part parts[MAX_OSD_PARTS];
+ int64_t change_id;
+
+ struct mp_image_params params; // target image params
+
+ int w, h; // like params.w/h, but rounded up to chroma
+ unsigned align_x, align_y; // alignment for all video pixels
+
+ struct mp_image *rgba_overlay; // all OSD in RGBA
+ struct mp_image *video_overlay; // rgba_overlay converted to video colorspace
+ struct mp_image *alpha_overlay; // alpha plane ref. to video_overlay
+ struct mp_image *calpha_overlay; // alpha_overlay scaled to chroma plane size
+
+ unsigned s_w; // number of slices per line
+ struct slice *slices; // slices[y * s_w + x / SLICE_W]
+ bool any_osd;
+
+ struct mp_sws_context *rgba_to_overlay; // scaler for rgba -> video csp.
+ struct mp_sws_context *alpha_to_calpha; // scaler for overlay -> calpha
+ bool scale_in_tiles;
+
+ struct mp_sws_context *sub_scale; // scaler for SUBBITMAP_BGRA
+
+ struct mp_repack *overlay_to_f32; // convert video_overlay to float
+ struct mp_image *overlay_tmp; // slice in float32
+
+ struct mp_repack *calpha_to_f32; // convert video_overlay to float
+ struct mp_image *calpha_tmp; // slice in float32
+
+ struct mp_repack *video_to_f32; // convert video to float
+ struct mp_repack *video_from_f32; // convert float back to video
+ struct mp_image *video_tmp; // slice in float32
+
+ struct mp_sws_context *premul; // video -> premultiplied video
+ struct mp_sws_context *unpremul; // reverse
+ struct mp_image *premul_tmp;
+
+ // Function that works on the _f32 data.
+ void (*blend_line)(void *dst, void *src, void *src_a, int w);
+
+ struct mp_image res_overlay; // returned by mp_draw_sub_overlay()
+};
+
+static void blend_line_f32(void *dst, void *src, void *src_a, int w)
+{
+ float *dst_f = dst;
+ float *src_f = src;
+ float *src_a_f = src_a;
+
+ for (int x = 0; x < w; x++)
+ dst_f[x] = src_f[x] + dst_f[x] * (1.0f - src_a_f[x]);
+}
+
+static void blend_line_u8(void *dst, void *src, void *src_a, int w)
+{
+ uint8_t *dst_i = dst;
+ uint8_t *src_i = src;
+ uint8_t *src_a_i = src_a;
+
+ for (int x = 0; x < w; x++)
+ dst_i[x] = src_i[x] + dst_i[x] * (255u - src_a_i[x]) / 255u;
+}
+
+static void blend_slice(struct mp_draw_sub_cache *p)
+{
+ struct mp_image *ov = p->overlay_tmp;
+ struct mp_image *ca = p->calpha_tmp;
+ struct mp_image *vid = p->video_tmp;
+
+ for (int plane = 0; plane < vid->num_planes; plane++) {
+ int xs = vid->fmt.xs[plane];
+ int ys = vid->fmt.ys[plane];
+ int h = (1 << vid->fmt.chroma_ys) - (1 << ys) + 1;
+ int cw = mp_chroma_div_up(vid->w, xs);
+ for (int y = 0; y < h; y++) {
+ p->blend_line(mp_image_pixel_ptr_ny(vid, plane, 0, y),
+ mp_image_pixel_ptr_ny(ov, plane, 0, y),
+ xs || ys ? mp_image_pixel_ptr_ny(ca, 0, 0, y)
+ : mp_image_pixel_ptr_ny(ov, ov->num_planes - 1, 0, y),
+ cw);
+ }
+ }
+}
+
+static bool blend_overlay_with_video(struct mp_draw_sub_cache *p,
+ struct mp_image *dst)
+{
+ if (!repack_config_buffers(p->video_to_f32, 0, p->video_tmp, 0, dst, NULL))
+ return false;
+ if (!repack_config_buffers(p->video_from_f32, 0, dst, 0, p->video_tmp, NULL))
+ return false;
+
+ int xs = dst->fmt.chroma_xs;
+ int ys = dst->fmt.chroma_ys;
+
+ for (int y = 0; y < dst->h; y += p->align_y) {
+ struct slice *line = &p->slices[y * p->s_w];
+
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &line[sx];
+
+ int w = s->x1 - s->x0;
+ if (w <= 0)
+ continue;
+ int x = sx * SLICE_W + s->x0;
+
+ assert(MP_IS_ALIGNED(x, p->align_x));
+ assert(MP_IS_ALIGNED(w, p->align_x));
+ assert(x + w <= p->w);
+
+ repack_line(p->overlay_to_f32, 0, 0, x, y, w);
+ repack_line(p->video_to_f32, 0, 0, x, y, w);
+ if (p->calpha_to_f32)
+ repack_line(p->calpha_to_f32, 0, 0, x >> xs, y >> ys, w >> xs);
+
+ blend_slice(p);
+
+ repack_line(p->video_from_f32, x, y, 0, 0, w);
+ }
+ }
+
+ return true;
+}
+
+static bool convert_overlay_part(struct mp_draw_sub_cache *p,
+ int x0, int y0, int w, int h)
+{
+ struct mp_image src = *p->rgba_overlay;
+ struct mp_image dst = *p->video_overlay;
+
+ mp_image_crop(&src, x0, y0, x0 + w, y0 + h);
+ mp_image_crop(&dst, x0, y0, x0 + w, y0 + h);
+
+ if (mp_sws_scale(p->rgba_to_overlay, &dst, &src) < 0)
+ return false;
+
+ if (p->calpha_overlay) {
+ src = *p->alpha_overlay;
+ dst = *p->calpha_overlay;
+
+ int xs = p->video_overlay->fmt.chroma_xs;
+ int ys = p->video_overlay->fmt.chroma_ys;
+ mp_image_crop(&src, x0, y0, x0 + w, y0 + h);
+ mp_image_crop(&dst, x0 >> xs, y0 >> ys, (x0 + w) >> xs, (y0 + h) >> ys);
+
+ if (mp_sws_scale(p->alpha_to_calpha, &dst, &src) < 0)
+ return false;
+ }
+
+ return true;
+}
+
+static bool convert_to_video_overlay(struct mp_draw_sub_cache *p)
+{
+ if (!p->video_overlay)
+ return true;
+
+ if (p->scale_in_tiles) {
+ int t_h = p->rgba_overlay->h / TILE_H;
+ for (int ty = 0; ty < t_h; ty++) {
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &p->slices[ty * TILE_H * p->s_w + sx];
+ bool pixels_set = false;
+ for (int y = 0; y < TILE_H; y++) {
+ if (s[0].x0 < s[0].x1) {
+ pixels_set = true;
+ break;
+ }
+ s += p->s_w;
+ }
+ if (!pixels_set)
+ continue;
+ if (!convert_overlay_part(p, sx * SLICE_W, ty * TILE_H,
+ SLICE_W, TILE_H))
+ return false;
+ }
+ }
+ } else {
+ if (!convert_overlay_part(p, 0, 0, p->rgba_overlay->w, p->rgba_overlay->h))
+ return false;
+ }
+
+ return true;
+}
+
+// Mark the given rectangle of pixels as possibly non-transparent.
+// The rectangle must have been pre-clipped.
+static void mark_rect(struct mp_draw_sub_cache *p, int x0, int y0, int x1, int y1)
+{
+ x0 = MP_ALIGN_DOWN(x0, p->align_x);
+ y0 = MP_ALIGN_DOWN(y0, p->align_y);
+ x1 = MP_ALIGN_UP(x1, p->align_x);
+ y1 = MP_ALIGN_UP(y1, p->align_y);
+
+ assert(x0 >= 0 && x0 <= x1 && x1 <= p->w);
+ assert(y0 >= 0 && y0 <= y1 && y1 <= p->h);
+
+ const int sx0 = x0 / SLICE_W;
+ const int sx1 = MPMIN(x1 / SLICE_W, p->s_w - 1);
+
+ for (int y = y0; y < y1; y++) {
+ struct slice *line = &p->slices[y * p->s_w];
+
+ struct slice *s0 = &line[sx0];
+ struct slice *s1 = &line[sx1];
+
+ s0->x0 = MPMIN(s0->x0, x0 % SLICE_W);
+ s1->x1 = MPMAX(s1->x1, ((x1 - 1) % SLICE_W) + 1);
+
+ if (s0 != s1) {
+ s0->x1 = SLICE_W;
+ s1->x0 = 0;
+
+ for (int x = sx0 + 1; x < sx1; x++) {
+ struct slice *s = &line[x];
+ s->x0 = 0;
+ s->x1 = SLICE_W;
+ }
+ }
+
+ // Ensure the very last slice does not extend
+ // beyond the total width.
+ struct slice *last_s = &line[p->s_w - 1];
+ last_s->x1 = MPMIN(p->w - ((p->s_w - 1) * SLICE_W), last_s->x1);
+
+ p->any_osd = true;
+ }
+}
+
+static void draw_ass_rgba(uint8_t *dst, ptrdiff_t dst_stride,
+ uint8_t *src, ptrdiff_t src_stride,
+ int w, int h, uint32_t color)
+{
+ const unsigned int r = (color >> 24) & 0xff;
+ const unsigned int g = (color >> 16) & 0xff;
+ const unsigned int b = (color >> 8) & 0xff;
+ const unsigned int a = 0xff - (color & 0xff);
+
+ for (int y = 0; y < h; y++) {
+ uint32_t *dstrow = (uint32_t *) dst;
+ for (int x = 0; x < w; x++) {
+ const unsigned int v = src[x];
+ unsigned int aa = a * v;
+ uint32_t dstpix = dstrow[x];
+ unsigned int dstb = dstpix & 0xFF;
+ unsigned int dstg = (dstpix >> 8) & 0xFF;
+ unsigned int dstr = (dstpix >> 16) & 0xFF;
+ unsigned int dsta = (dstpix >> 24) & 0xFF;
+ dstb = (v * b * a + dstb * (255 * 255 - aa)) / (255 * 255);
+ dstg = (v * g * a + dstg * (255 * 255 - aa)) / (255 * 255);
+ dstr = (v * r * a + dstr * (255 * 255 - aa)) / (255 * 255);
+ dsta = (aa * 255 + dsta * (255 * 255 - aa)) / (255 * 255);
+ dstrow[x] = dstb | (dstg << 8) | (dstr << 16) | (dsta << 24);
+ }
+ dst += dst_stride;
+ src += src_stride;
+ }
+}
+
+static void render_ass(struct mp_draw_sub_cache *p, struct sub_bitmaps *sb)
+{
+ assert(sb->format == SUBBITMAP_LIBASS);
+
+ for (int i = 0; i < sb->num_parts; i++) {
+ struct sub_bitmap *s = &sb->parts[i];
+
+ draw_ass_rgba(mp_image_pixel_ptr(p->rgba_overlay, 0, s->x, s->y),
+ p->rgba_overlay->stride[0], s->bitmap, s->stride,
+ s->w, s->h, s->libass.color);
+
+ mark_rect(p, s->x, s->y, s->x + s->w, s->y + s->h);
+ }
+}
+
+static void draw_rgba(uint8_t *dst, ptrdiff_t dst_stride,
+ uint8_t *src, ptrdiff_t src_stride, int w, int h)
+{
+ for (int y = 0; y < h; y++) {
+ uint32_t *srcrow = (uint32_t *)src;
+ uint32_t *dstrow = (uint32_t *)dst;
+ for (int x = 0; x < w; x++) {
+ uint32_t srcpix = srcrow[x];
+ uint32_t dstpix = dstrow[x];
+ unsigned int srcb = srcpix & 0xFF;
+ unsigned int srcg = (srcpix >> 8) & 0xFF;
+ unsigned int srcr = (srcpix >> 16) & 0xFF;
+ unsigned int srca = (srcpix >> 24) & 0xFF;
+ unsigned int dstb = dstpix & 0xFF;
+ unsigned int dstg = (dstpix >> 8) & 0xFF;
+ unsigned int dstr = (dstpix >> 16) & 0xFF;
+ unsigned int dsta = (dstpix >> 24) & 0xFF;
+ dstb = srcb + dstb * (255 * 255 - srca) / (255 * 255);
+ dstg = srcg + dstg * (255 * 255 - srca) / (255 * 255);
+ dstr = srcr + dstr * (255 * 255 - srca) / (255 * 255);
+ dsta = srca + dsta * (255 * 255 - srca) / (255 * 255);
+ dstrow[x] = dstb | (dstg << 8) | (dstr << 16) | (dsta << 24);
+ }
+ dst += dst_stride;
+ src += src_stride;
+ }
+}
+
+static bool render_rgba(struct mp_draw_sub_cache *p, struct part *part,
+ struct sub_bitmaps *sb)
+{
+ assert(sb->format == SUBBITMAP_BGRA);
+
+ if (part->change_id != sb->change_id) {
+ for (int n = 0; n < part->num_imgs; n++)
+ talloc_free(part->imgs[n]);
+ part->num_imgs = sb->num_parts;
+ MP_TARRAY_GROW(p, part->imgs, part->num_imgs);
+ for (int n = 0; n < part->num_imgs; n++)
+ part->imgs[n] = NULL;
+
+ part->change_id = sb->change_id;
+ }
+
+ for (int i = 0; i < sb->num_parts; i++) {
+ struct sub_bitmap *s = &sb->parts[i];
+
+ // Clipping is rare but necessary.
+ int sx0 = s->x;
+ int sy0 = s->y;
+ int sx1 = s->x + s->dw;
+ int sy1 = s->y + s->dh;
+
+ int x0 = MPCLAMP(sx0, 0, p->w);
+ int y0 = MPCLAMP(sy0, 0, p->h);
+ int x1 = MPCLAMP(sx1, 0, p->w);
+ int y1 = MPCLAMP(sy1, 0, p->h);
+
+ int dw = x1 - x0;
+ int dh = y1 - y0;
+ if (dw <= 0 || dh <= 0)
+ continue;
+
+ // We clip the source instead of the scaled image, because that might
+ // avoid excessive memory usage when applying a ridiculous scale factor,
+ // even if that stretches it to up to 1 pixel due to integer rounding.
+ int sx = 0;
+ int sy = 0;
+ int sw = s->w;
+ int sh = s->h;
+ if (x0 != sx0 || y0 != sy0 || x1 != sx1 || y1 != sy1) {
+ double fx = s->dw / (double)s->w;
+ double fy = s->dh / (double)s->h;
+ sx = MPCLAMP((x0 - sx0) / fx, 0, s->w);
+ sy = MPCLAMP((y0 - sy0) / fy, 0, s->h);
+ sw = MPCLAMP(dw / fx, 1, s->w);
+ sh = MPCLAMP(dh / fy, 1, s->h);
+ }
+
+ assert(sx >= 0 && sw > 0 && sx + sw <= s->w);
+ assert(sy >= 0 && sh > 0 && sy + sh <= s->h);
+
+ ptrdiff_t s_stride = s->stride;
+ void *s_ptr = (char *)s->bitmap + s_stride * sy + sx * 4;
+
+ if (dw != sw || dh != sh) {
+ struct mp_image *scaled = part->imgs[i];
+
+ if (!scaled) {
+ struct mp_image src_img = {0};
+ mp_image_setfmt(&src_img, IMGFMT_BGRA);
+ mp_image_set_size(&src_img, sw, sh);
+ src_img.planes[0] = s_ptr;
+ src_img.stride[0] = s_stride;
+ src_img.params.alpha = MP_ALPHA_PREMUL;
+
+ scaled = mp_image_alloc(IMGFMT_BGRA, dw, dh);
+ if (!scaled)
+ return false;
+ part->imgs[i] = talloc_steal(p, scaled);
+ mp_image_copy_attributes(scaled, &src_img);
+
+ if (mp_sws_scale(p->sub_scale, scaled, &src_img) < 0)
+ return false;
+ }
+
+ assert(scaled->w == dw);
+ assert(scaled->h == dh);
+
+ s_stride = scaled->stride[0];
+ s_ptr = scaled->planes[0];
+ }
+
+ draw_rgba(mp_image_pixel_ptr(p->rgba_overlay, 0, x0, y0),
+ p->rgba_overlay->stride[0], s_ptr, s_stride, dw, dh);
+
+ mark_rect(p, x0, y0, x1, y1);
+ }
+
+ return true;
+}
+
+static bool render_sb(struct mp_draw_sub_cache *p, struct sub_bitmaps *sb)
+{
+ struct part *part = &p->parts[sb->render_index];
+
+ switch (sb->format) {
+ case SUBBITMAP_LIBASS:
+ render_ass(p, sb);
+ return true;
+ case SUBBITMAP_BGRA:
+ return render_rgba(p, part, sb);
+ }
+
+ return false;
+}
+
+static void clear_rgba_overlay(struct mp_draw_sub_cache *p)
+{
+ assert(p->rgba_overlay->imgfmt == IMGFMT_BGRA);
+
+ for (int y = 0; y < p->rgba_overlay->h; y++) {
+ uint32_t *px = mp_image_pixel_ptr(p->rgba_overlay, 0, 0, y);
+ struct slice *line = &p->slices[y * p->s_w];
+
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &line[sx];
+
+ // Ensure this final slice doesn't extend beyond the width of p->s_w
+ if (s->x1 == SLICE_W && sx == p->s_w - 1 && y == p->rgba_overlay->h - 1)
+ s->x1 = MPMIN(p->w - ((p->s_w - 1) * SLICE_W), s->x1);
+
+ if (s->x0 <= s->x1) {
+ memset(px + s->x0, 0, (s->x1 - s->x0) * 4);
+ *s = (struct slice){SLICE_W, 0};
+ }
+
+ px += SLICE_W;
+ }
+ }
+
+ p->any_osd = false;
+}
+
+static struct mp_sws_context *alloc_scaler(struct mp_draw_sub_cache *p)
+{
+ struct mp_sws_context *s = mp_sws_alloc(p);
+ mp_sws_enable_cmdline_opts(s, p->global);
+ return s;
+}
+
+static void init_general(struct mp_draw_sub_cache *p)
+{
+ p->sub_scale = alloc_scaler(p);
+
+ p->s_w = MP_ALIGN_UP(p->rgba_overlay->w, SLICE_W) / SLICE_W;
+
+ p->slices = talloc_zero_array(p, struct slice, p->s_w * p->rgba_overlay->h);
+
+ mp_image_clear(p->rgba_overlay, 0, 0, p->w, p->h);
+ clear_rgba_overlay(p);
+}
+
+static bool reinit_to_video(struct mp_draw_sub_cache *p)
+{
+ struct mp_image_params *params = &p->params;
+ mp_image_params_guess_csp(params);
+
+ bool need_premul = params->alpha != MP_ALPHA_PREMUL &&
+ (mp_imgfmt_get_desc(params->imgfmt).flags & MP_IMGFLAG_ALPHA);
+
+ // Intermediate format for video_overlay. Requirements:
+ // - same subsampling as video
+ // - uses video colorspace
+ // - has alpha
+ // - repacker support (to the format used in p->blend_line)
+ // - probably 8 bit per component rather than something wasteful or strange
+ struct mp_regular_imgfmt vfdesc = {0};
+
+ int rflags = REPACK_CREATE_EXPAND_8BIT;
+ bool use_shortcut = false;
+
+ p->video_to_f32 = mp_repack_create_planar(params->imgfmt, false, rflags);
+ talloc_steal(p, p->video_to_f32);
+ if (!p->video_to_f32)
+ return false;
+ mp_get_regular_imgfmt(&vfdesc, mp_repack_get_format_dst(p->video_to_f32));
+ assert(vfdesc.num_planes); // must have succeeded
+
+ if (params->color.space == MP_CSP_RGB && vfdesc.num_planes >= 3) {
+ use_shortcut = true;
+
+ if (vfdesc.component_type == MP_COMPONENT_TYPE_UINT &&
+ vfdesc.component_size == 1 && vfdesc.component_pad == 0)
+ p->blend_line = blend_line_u8;
+ }
+
+ // If no special blender is available, blend in float.
+ if (!p->blend_line) {
+ TA_FREEP(&p->video_to_f32);
+
+ rflags |= REPACK_CREATE_PLANAR_F32;
+
+ p->video_to_f32 = mp_repack_create_planar(params->imgfmt, false, rflags);
+ talloc_steal(p, p->video_to_f32);
+ if (!p->video_to_f32)
+ return false;
+
+ mp_get_regular_imgfmt(&vfdesc, mp_repack_get_format_dst(p->video_to_f32));
+ assert(vfdesc.component_type == MP_COMPONENT_TYPE_FLOAT);
+
+ p->blend_line = blend_line_f32;
+ }
+
+ p->scale_in_tiles = SCALE_IN_TILES;
+
+ int vid_f32_fmt = mp_repack_get_format_dst(p->video_to_f32);
+
+ p->video_from_f32 = mp_repack_create_planar(params->imgfmt, true, rflags);
+ talloc_steal(p, p->video_from_f32);
+ if (!p->video_from_f32)
+ return false;
+
+ assert(mp_repack_get_format_dst(p->video_to_f32) ==
+ mp_repack_get_format_src(p->video_from_f32));
+
+ int overlay_fmt = 0;
+ if (use_shortcut) {
+ // No point in doing anything fancy.
+ overlay_fmt = IMGFMT_BGRA;
+ p->scale_in_tiles = false;
+ } else {
+ struct mp_regular_imgfmt odesc = vfdesc;
+ // Just use 8 bit as well (should be fine, may use less memory).
+ odesc.component_type = MP_COMPONENT_TYPE_UINT;
+ odesc.component_size = 1;
+ odesc.component_pad = 0;
+
+ // Ensure there's alpha.
+ if (odesc.planes[odesc.num_planes - 1].components[0] != 4) {
+ if (odesc.num_planes >= 4)
+ return false; // wat
+ odesc.planes[odesc.num_planes++] =
+ (struct mp_regular_imgfmt_plane){1, {4}};
+ }
+
+ overlay_fmt = mp_find_regular_imgfmt(&odesc);
+ p->scale_in_tiles = odesc.chroma_xs || odesc.chroma_ys;
+ }
+ if (!overlay_fmt)
+ return false;
+
+ p->overlay_to_f32 = mp_repack_create_planar(overlay_fmt, false, rflags);
+ talloc_steal(p, p->overlay_to_f32);
+ if (!p->overlay_to_f32)
+ return false;
+
+ int render_fmt = mp_repack_get_format_dst(p->overlay_to_f32);
+
+ struct mp_regular_imgfmt ofdesc = {0};
+ mp_get_regular_imgfmt(&ofdesc, render_fmt);
+
+ if (ofdesc.planes[ofdesc.num_planes - 1].components[0] != 4)
+ return false;
+
+ // The formats must be the same, minus possible lack of alpha in vfdesc.
+ if (ofdesc.num_planes != vfdesc.num_planes &&
+ ofdesc.num_planes - 1 != vfdesc.num_planes)
+ return false;
+ for (int n = 0; n < vfdesc.num_planes; n++) {
+ if (vfdesc.planes[n].components[0] != ofdesc.planes[n].components[0])
+ return false;
+ }
+
+ p->align_x = mp_repack_get_align_x(p->video_to_f32);
+ p->align_y = mp_repack_get_align_y(p->video_to_f32);
+
+ assert(p->align_x >= mp_repack_get_align_x(p->overlay_to_f32));
+ assert(p->align_y >= mp_repack_get_align_y(p->overlay_to_f32));
+
+ if (p->align_x > SLICE_W || p->align_y > TILE_H)
+ return false;
+
+ p->w = MP_ALIGN_UP(params->w, p->align_x);
+ int slice_h = p->align_y;
+ p->h = MP_ALIGN_UP(params->h, slice_h);
+
+ // Size of the overlay. If scaling in tiles, round up to tiles, so we don't
+ // need to reinit the scale for right/bottom tiles.
+ int w = p->w;
+ int h = p->h;
+ if (p->scale_in_tiles) {
+ w = MP_ALIGN_UP(w, SLICE_W);
+ h = MP_ALIGN_UP(h, TILE_H);
+ }
+
+ p->rgba_overlay = talloc_steal(p, mp_image_alloc(IMGFMT_BGRA, w, h));
+ p->overlay_tmp = talloc_steal(p, mp_image_alloc(render_fmt, SLICE_W, slice_h));
+ p->video_tmp = talloc_steal(p, mp_image_alloc(vid_f32_fmt, SLICE_W, slice_h));
+ if (!p->rgba_overlay || !p->overlay_tmp || !p->video_tmp)
+ return false;
+
+ mp_image_params_guess_csp(&p->rgba_overlay->params);
+ p->rgba_overlay->params.alpha = MP_ALPHA_PREMUL;
+
+ p->overlay_tmp->params.color = params->color;
+ p->video_tmp->params.color = params->color;
+
+ if (p->rgba_overlay->imgfmt == overlay_fmt) {
+ if (!repack_config_buffers(p->overlay_to_f32, 0, p->overlay_tmp,
+ 0, p->rgba_overlay, NULL))
+ return false;
+ } else {
+ // Generally non-RGB.
+ p->video_overlay = talloc_steal(p, mp_image_alloc(overlay_fmt, w, h));
+ if (!p->video_overlay)
+ return false;
+
+ p->video_overlay->params.color = params->color;
+ p->video_overlay->params.chroma_location = params->chroma_location;
+ p->video_overlay->params.alpha = MP_ALPHA_PREMUL;
+
+ if (p->scale_in_tiles)
+ p->video_overlay->params.chroma_location = MP_CHROMA_CENTER;
+
+ p->rgba_to_overlay = alloc_scaler(p);
+ p->rgba_to_overlay->allow_zimg = true;
+ if (!mp_sws_supports_formats(p->rgba_to_overlay,
+ p->video_overlay->imgfmt, p->rgba_overlay->imgfmt))
+ return false;
+
+ if (!repack_config_buffers(p->overlay_to_f32, 0, p->overlay_tmp,
+ 0, p->video_overlay, NULL))
+ return false;
+
+ // Setup a scaled alpha plane if chroma-subsampling is present.
+ int xs = p->video_overlay->fmt.chroma_xs;
+ int ys = p->video_overlay->fmt.chroma_ys;
+ if (xs || ys) {
+ // Require float so format selection becomes simpler (maybe).
+ assert(rflags & REPACK_CREATE_PLANAR_F32);
+
+ // For extracting the alpha plane, construct a gray format that is
+ // compatible with the alpha one.
+ struct mp_regular_imgfmt odesc = {0};
+ mp_get_regular_imgfmt(&odesc, overlay_fmt);
+ assert(odesc.component_size);
+ int aplane = odesc.num_planes - 1;
+ assert(odesc.planes[aplane].num_components == 1);
+ assert(odesc.planes[aplane].components[0] == 4);
+ struct mp_regular_imgfmt cadesc = odesc;
+ cadesc.num_planes = 1;
+ cadesc.planes[0] = (struct mp_regular_imgfmt_plane){1, {1}};
+ cadesc.chroma_xs = cadesc.chroma_ys = 0;
+
+ int calpha_fmt = mp_find_regular_imgfmt(&cadesc);
+ if (!calpha_fmt)
+ return false;
+
+ // Unscaled alpha plane from p->video_overlay.
+ p->alpha_overlay = talloc_zero(p, struct mp_image);
+ mp_image_setfmt(p->alpha_overlay, calpha_fmt);
+ mp_image_set_size(p->alpha_overlay, w, h);
+ p->alpha_overlay->planes[0] = p->video_overlay->planes[aplane];
+ p->alpha_overlay->stride[0] = p->video_overlay->stride[aplane];
+
+ // Full range gray always has the same range as alpha.
+ p->alpha_overlay->params.color.levels = MP_CSP_LEVELS_PC;
+ mp_image_params_guess_csp(&p->alpha_overlay->params);
+
+ p->calpha_overlay =
+ talloc_steal(p, mp_image_alloc(calpha_fmt, w >> xs, h >> ys));
+ if (!p->calpha_overlay)
+ return false;
+ p->calpha_overlay->params.color = p->alpha_overlay->params.color;
+
+ p->calpha_to_f32 = mp_repack_create_planar(calpha_fmt, false, rflags);
+ talloc_steal(p, p->calpha_to_f32);
+ if (!p->calpha_to_f32)
+ return false;
+
+ int af32_fmt = mp_repack_get_format_dst(p->calpha_to_f32);
+ p->calpha_tmp = talloc_steal(p, mp_image_alloc(af32_fmt, SLICE_W, 1));
+ if (!p->calpha_tmp)
+ return false;
+
+ if (!repack_config_buffers(p->calpha_to_f32, 0, p->calpha_tmp,
+ 0, p->calpha_overlay, NULL))
+ return false;
+
+ p->alpha_to_calpha = alloc_scaler(p);
+ if (!mp_sws_supports_formats(p->alpha_to_calpha,
+ calpha_fmt, calpha_fmt))
+ return false;
+ }
+ }
+
+ if (need_premul) {
+ p->premul = alloc_scaler(p);
+ p->unpremul = alloc_scaler(p);
+ p->premul_tmp = mp_image_alloc(params->imgfmt, params->w, params->h);
+ talloc_steal(p, p->premul_tmp);
+ if (!p->premul_tmp)
+ return false;
+ mp_image_set_params(p->premul_tmp, params);
+ p->premul_tmp->params.alpha = MP_ALPHA_PREMUL;
+
+ // Only zimg supports this.
+ p->premul->force_scaler = MP_SWS_ZIMG;
+ p->unpremul->force_scaler = MP_SWS_ZIMG;
+ }
+
+ init_general(p);
+
+ return true;
+}
+
+static bool reinit_to_overlay(struct mp_draw_sub_cache *p)
+{
+ p->align_x = 1;
+ p->align_y = 1;
+
+ p->w = p->params.w;
+ p->h = p->params.h;
+
+ p->rgba_overlay = talloc_steal(p, mp_image_alloc(IMGFMT_BGRA, p->w, p->h));
+ if (!p->rgba_overlay)
+ return false;
+
+ mp_image_params_guess_csp(&p->rgba_overlay->params);
+ p->rgba_overlay->params.alpha = MP_ALPHA_PREMUL;
+
+ // Some non-sense with the intention to somewhat isolate the returned image.
+ mp_image_setfmt(&p->res_overlay, p->rgba_overlay->imgfmt);
+ mp_image_set_size(&p->res_overlay, p->rgba_overlay->w, p->rgba_overlay->h);
+ mp_image_copy_attributes(&p->res_overlay, p->rgba_overlay);
+ p->res_overlay.planes[0] = p->rgba_overlay->planes[0];
+ p->res_overlay.stride[0] = p->rgba_overlay->stride[0];
+
+ init_general(p);
+
+ // Mark all dirty (for full reinit of user state).
+ for (int y = 0; y < p->rgba_overlay->h; y++) {
+ for (int sx = 0; sx < p->s_w; sx++)
+ p->slices[y * p->s_w + sx] = (struct slice){0, SLICE_W};
+ }
+
+ return true;
+}
+
+static bool check_reinit(struct mp_draw_sub_cache *p,
+ struct mp_image_params *params, bool to_video)
+{
+ if (!mp_image_params_equal(&p->params, params) || !p->rgba_overlay) {
+ talloc_free_children(p);
+ *p = (struct mp_draw_sub_cache){.global = p->global, .params = *params};
+ if (!(to_video ? reinit_to_video(p) : reinit_to_overlay(p))) {
+ talloc_free_children(p);
+ *p = (struct mp_draw_sub_cache){.global = p->global};
+ return false;
+ }
+ }
+ return true;
+}
+
+char *mp_draw_sub_get_dbg_info(struct mp_draw_sub_cache *p)
+{
+ assert(p);
+
+ return talloc_asprintf(NULL,
+ "align=%d:%d ov=%-7s, ov_f=%s, v_f=%s, a=%s, ca=%s, ca_f=%s",
+ p->align_x, p->align_y,
+ mp_imgfmt_to_name(p->video_overlay ? p->video_overlay->imgfmt : 0),
+ mp_imgfmt_to_name(p->overlay_tmp->imgfmt),
+ mp_imgfmt_to_name(p->video_tmp->imgfmt),
+ mp_imgfmt_to_name(p->alpha_overlay ? p->alpha_overlay->imgfmt : 0),
+ mp_imgfmt_to_name(p->calpha_overlay ? p->calpha_overlay->imgfmt : 0),
+ mp_imgfmt_to_name(p->calpha_tmp ? p->calpha_tmp->imgfmt : 0));
+}
+
+struct mp_draw_sub_cache *mp_draw_sub_alloc(void *ta_parent, struct mpv_global *g)
+{
+ struct mp_draw_sub_cache *c = talloc_zero(ta_parent, struct mp_draw_sub_cache);
+ c->global = g;
+ return c;
+}
+
+// For tests.
+struct mp_draw_sub_cache *mp_draw_sub_alloc_test(struct mp_image *dst)
+{
+ struct mp_draw_sub_cache *c = talloc_zero(NULL, struct mp_draw_sub_cache);
+ reinit_to_video(c);
+ return c;
+}
+
+bool mp_draw_sub_bitmaps(struct mp_draw_sub_cache *p, struct mp_image *dst,
+ struct sub_bitmap_list *sbs_list)
+{
+ bool ok = false;
+
+ // dst must at least be as large as the bounding box, or you may get memory
+ // corruption.
+ assert(dst->w >= sbs_list->w);
+ assert(dst->h >= sbs_list->h);
+
+ if (!check_reinit(p, &dst->params, true))
+ return false;
+
+ if (p->change_id != sbs_list->change_id) {
+ p->change_id = sbs_list->change_id;
+
+ clear_rgba_overlay(p);
+
+ for (int n = 0; n < sbs_list->num_items; n++) {
+ if (!render_sb(p, sbs_list->items[n]))
+ goto done;
+ }
+
+ if (!convert_to_video_overlay(p))
+ goto done;
+ }
+
+ if (p->any_osd) {
+ struct mp_image *target = dst;
+ if (p->premul_tmp) {
+ if (mp_sws_scale(p->premul, p->premul_tmp, dst) < 0)
+ goto done;
+ target = p->premul_tmp;
+ }
+
+ if (!blend_overlay_with_video(p, target))
+ goto done;
+
+ if (target != dst) {
+ if (mp_sws_scale(p->unpremul, dst, p->premul_tmp) < 0)
+ goto done;
+ }
+ }
+
+ ok = true;
+
+done:
+ return ok;
+}
+
+// Bounding boxes for mp_draw_sub_overlay() API. For simplicity, each rectangle
+// covers a fixed tile on the screen, starts out empty, but is not extended
+// beyond the tile. In the simplest case, there's only 1 rect/tile for everything.
+struct rc_grid {
+ unsigned w, h; // size in grid tiles
+ unsigned r_w, r_h; // size of a grid tile in pixels
+ struct mp_rect *rcs; // rcs[x * w + y]
+};
+
+static void init_rc_grid(struct rc_grid *gr, struct mp_draw_sub_cache *p,
+ struct mp_rect *rcs, int max_rcs)
+{
+ *gr = (struct rc_grid){ .w = max_rcs ? 1 : 0, .h = max_rcs ? 1 : 0,
+ .rcs = rcs, .r_w = p->s_w * SLICE_W, .r_h = p->h, };
+
+ // Dumb iteration to figure out max. size because I'm stupid.
+ bool more = true;
+ while (more) {
+ more = false;
+ if (gr->r_h >= 128) {
+ if (gr->w * gr->h * 2 > max_rcs)
+ break;
+ gr->h *= 2;
+ gr->r_h = (p->h + gr->h - 1) / gr->h;
+ more = true;
+ }
+ if (gr->r_w >= SLICE_W * 2) {
+ if (gr->w * gr->h * 2 > max_rcs)
+ break;
+ gr->w *= 2;
+ gr->r_w = (p->s_w + gr->w - 1) / gr->w * SLICE_W;
+ more = true;
+ }
+ }
+
+ assert(gr->r_h * gr->h >= p->h);
+ assert(!(gr->r_w & (SLICE_W - 1)));
+ assert(gr->r_w * gr->w >= p->w);
+
+ // Init with empty (degenerate) rectangles.
+ for (int y = 0; y < gr->h; y++) {
+ for (int x = 0; x < gr->w; x++) {
+ struct mp_rect *rc = &gr->rcs[y * gr->w + x];
+ rc->x1 = x * gr->r_w;
+ rc->y1 = y * gr->r_h;
+ rc->x0 = rc->x1 + gr->r_w;
+ rc->y0 = rc->y1 + gr->r_h;
+ }
+ }
+}
+
+// Extend given grid with contents of p->slices.
+static void mark_rcs(struct mp_draw_sub_cache *p, struct rc_grid *gr)
+{
+ for (int y = 0; y < p->h; y++) {
+ struct slice *line = &p->slices[y * p->s_w];
+ struct mp_rect *rcs = &gr->rcs[y / gr->r_h * gr->w];
+
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &line[sx];
+ if (s->x0 < s->x1) {
+ unsigned xpos = sx * SLICE_W;
+ struct mp_rect *rc = &rcs[xpos / gr->r_w];
+ rc->y0 = MPMIN(rc->y0, y);
+ rc->y1 = MPMAX(rc->y1, y + 1);
+ rc->x0 = MPMIN(rc->x0, xpos + s->x0);
+ // Ensure this does not extend beyond the total width
+ rc->x1 = MPCLAMP(xpos + s->x1, rc->x1, p->w);
+ }
+ }
+ }
+}
+
+// Remove empty RCs, and return rc count.
+static int return_rcs(struct rc_grid *gr)
+{
+ int num = 0, cnt = gr->w * gr->h;
+ for (int n = 0; n < cnt; n++) {
+ struct mp_rect *rc = &gr->rcs[n];
+ if (rc->x0 < rc->x1 && rc->y0 < rc->y1)
+ gr->rcs[num++] = *rc;
+ }
+ return num;
+}
+
+struct mp_image *mp_draw_sub_overlay(struct mp_draw_sub_cache *p,
+ struct sub_bitmap_list *sbs_list,
+ struct mp_rect *act_rcs,
+ int max_act_rcs,
+ int *num_act_rcs,
+ struct mp_rect *mod_rcs,
+ int max_mod_rcs,
+ int *num_mod_rcs)
+{
+ *num_act_rcs = 0;
+ *num_mod_rcs = 0;
+
+ struct mp_image_params params = {.w = sbs_list->w, .h = sbs_list->h};
+ if (!check_reinit(p, &params, false))
+ return NULL;
+
+ struct rc_grid gr_act, gr_mod;
+ init_rc_grid(&gr_act, p, act_rcs, max_act_rcs);
+ init_rc_grid(&gr_mod, p, mod_rcs, max_mod_rcs);
+
+ if (p->change_id != sbs_list->change_id) {
+ p->change_id = sbs_list->change_id;
+
+ mark_rcs(p, &gr_mod);
+
+ clear_rgba_overlay(p);
+
+ for (int n = 0; n < sbs_list->num_items; n++) {
+ if (!render_sb(p, sbs_list->items[n])) {
+ p->change_id = 0;
+ return NULL;
+ }
+ }
+
+ mark_rcs(p, &gr_mod);
+ }
+
+ mark_rcs(p, &gr_act);
+
+ *num_act_rcs = return_rcs(&gr_act);
+ *num_mod_rcs = return_rcs(&gr_mod);
+
+ return &p->res_overlay;
+}
+
+// vim: ts=4 sw=4 et tw=80
diff --git a/sub/draw_bmp.h b/sub/draw_bmp.h
new file mode 100644
index 0000000..fda7797
--- /dev/null
+++ b/sub/draw_bmp.h
@@ -0,0 +1,63 @@
+#ifndef MPLAYER_DRAW_BMP_H
+#define MPLAYER_DRAW_BMP_H
+
+#include "osd.h"
+
+struct mp_rect;
+struct mp_image;
+struct mpv_global;
+struct mp_draw_sub_cache;
+
+struct mp_draw_sub_cache *mp_draw_sub_alloc(void *ta_parent, struct mpv_global *g);
+
+// Only for use in tests.
+struct mp_draw_sub_cache *mp_draw_sub_alloc_test(struct mp_image *dst);
+
+// Render the sub-bitmaps in sbs_list to dst. sbs_list must have been rendered
+// for an OSD resolution equivalent to dst's size (UB if not).
+// Warning: if dst is a format with alpha, and dst is not set to MP_ALPHA_PREMUL
+// (not done by default), this will be extremely slow.
+// Warning: the caller is responsible for ensuring that dst is writable.
+// cache: allocated instance; caches non-changing OSD parts etc.
+// dst: image to draw to
+// sbs_list: source sub-bitmaps
+// returns: success
+bool mp_draw_sub_bitmaps(struct mp_draw_sub_cache *cache, struct mp_image *dst,
+ struct sub_bitmap_list *sbs_list);
+
+char *mp_draw_sub_get_dbg_info(struct mp_draw_sub_cache *c);
+
+// Return a RGBA overlay with subtitles. The returned image uses IMGFMT_BGRA and
+// premultiplied alpha, and the size specified by sbs_list.w/h.
+// This can return a list of active (act_) and modified (mod_) rectangles.
+// Active rectangles are regions that contain visible OSD pixels. Modified
+// rectangles are regions that were changed since the last call. This function
+// always makes the act region a subset of the mod region. Rectangles within a
+// list never overlap with rectangles within the same list.
+// If num_mod_rcs==0 is returned, this function guarantees that the act region
+// did not change since the last call.
+// If the user-provided lists are too small (max_*_rcs too small), multiple
+// rectangles are merged until they fit in the list.
+// You can pass max_act_rcs=0, which implies you render the whole overlay.
+// cache: allocated instance; keeps track of changed regions
+// sbs_list: source sub-bitmaps
+// act_rcs: caller allocated list of non-transparent rectangles
+// max_act_rcs: number of allocated items in act_rcs
+// num_act_rcs: set to the number of valid items in act_rcs
+// mod_rcs, max_mod_rcs, num_mod_rcs: modified rectangles
+// returns: internal OSD overlay owned by cache, NULL on error
+// read only, valid until the next call on cache
+struct mp_image *mp_draw_sub_overlay(struct mp_draw_sub_cache *cache,
+ struct sub_bitmap_list *sbs_list,
+ struct mp_rect *act_rcs,
+ int max_act_rcs,
+ int *num_act_rcs,
+ struct mp_rect *mod_rcs,
+ int max_mod_rcs,
+ int *num_mod_rcs);
+
+extern const bool mp_draw_sub_formats[SUBBITMAP_COUNT];
+
+#endif /* MPLAYER_DRAW_BMP_H */
+
+// vim: ts=4 sw=4 et tw=80
diff --git a/sub/filter_jsre.c b/sub/filter_jsre.c
new file mode 100644
index 0000000..f956000
--- /dev/null
+++ b/sub/filter_jsre.c
@@ -0,0 +1,140 @@
+#include <stdio.h>
+#include <sys/types.h>
+
+#include <mujs.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "sd.h"
+
+
+// p_NAME are protected functions (never throw) which interact with the JS VM.
+// return 0 on successful interaction, not-0 on (caught) js-error.
+// on error: stack is the same as on entry + an error value
+
+// js: global[n] = new RegExp(str, flags)
+static int p_regcomp(js_State *J, int n, const char *str, int flags)
+{
+ if (js_try(J))
+ return 1;
+
+ js_pushnumber(J, n); // n
+ js_newregexp(J, str, flags); // n regex
+ js_setglobal(J, js_tostring(J, -2)); // n (and global[n] is the regex)
+ js_pop(J, 1);
+
+ js_endtry(J);
+ return 0;
+}
+
+// js: found = global[n].test(text)
+static int p_regexec(js_State *J, int n, const char *text, int *found)
+{
+ if (js_try(J))
+ return 1;
+
+ js_pushnumber(J, n); // n
+ js_getglobal(J, js_tostring(J, -1)); // n global[n]
+ js_getproperty(J, -1, "test"); // n global[n] global[n].test
+ js_rot2(J); // n global[n].test global[n] (n, test(), and its `this')
+ js_pushstring(J, text); // n global[n].test global[n] text
+ js_call(J, 1); // n test-result
+ *found = js_toboolean(J, -1);
+ js_pop(J, 2); // the result and n
+
+ js_endtry(J);
+ return 0;
+}
+
+// protected. caller should pop the error after using the result string.
+static const char *get_err(js_State *J)
+{
+ return js_trystring(J, -1, "unknown error");
+}
+
+
+struct priv {
+ js_State *J;
+ int num_regexes;
+ int offset;
+};
+
+static void destruct_priv(void *p)
+{
+ js_freestate(((struct priv *)p)->J);
+}
+
+static bool jsre_init(struct sd_filter *ft)
+{
+ if (strcmp(ft->codec, "ass") != 0)
+ return false;
+
+ if (!ft->opts->rf_enable)
+ return false;
+
+ if (!(ft->opts->jsre_items && ft->opts->jsre_items[0]))
+ return false;
+
+ struct priv *p = talloc_zero(ft, struct priv);
+ ft->priv = p;
+
+ p->J = js_newstate(0, 0, JS_STRICT);
+ if (!p->J) {
+ MP_ERR(ft, "jsre: VM init error\n");
+ return false;
+ }
+ talloc_set_destructor(p, destruct_priv);
+
+ for (int n = 0; ft->opts->jsre_items[n]; n++) {
+ char *item = ft->opts->jsre_items[n];
+
+ int err = p_regcomp(p->J, p->num_regexes, item, JS_REGEXP_I | JS_REGEXP_M);
+ if (err) {
+ MP_ERR(ft, "jsre: %s -- '%s'\n", get_err(p->J), item);
+ js_pop(p->J, 1);
+ continue;
+ }
+
+ p->num_regexes += 1;
+ }
+
+ if (!p->num_regexes)
+ return false;
+
+ p->offset = sd_ass_fmt_offset(ft->event_format);
+ return true;
+}
+
+static struct demux_packet *jsre_filter(struct sd_filter *ft,
+ struct demux_packet *pkt)
+{
+ struct priv *p = ft->priv;
+ char *text = bstrto0(NULL, sd_ass_pkt_text(ft, pkt, p->offset));
+ bool drop = false;
+
+ if (ft->opts->rf_plain)
+ sd_ass_to_plaintext(text, strlen(text), text);
+
+ for (int n = 0; n < p->num_regexes; n++) {
+ int found, err = p_regexec(p->J, n, text, &found);
+ if (err == 0 && found) {
+ int level = ft->opts->rf_warn ? MSGL_WARN : MSGL_V;
+ MP_MSG(ft, level, "jsre: regex %d => drop: '%s'\n", n, text);
+ drop = true;
+ break;
+ } else if (err) {
+ MP_WARN(ft, "jsre: test regex %d: %s.\n", n, get_err(p->J));
+ js_pop(p->J, 1);
+ }
+ }
+
+ talloc_free(text);
+ return drop ? NULL : pkt;
+}
+
+const struct sd_filter_functions sd_filter_jsre = {
+ .init = jsre_init,
+ .filter = jsre_filter,
+};
diff --git a/sub/filter_regex.c b/sub/filter_regex.c
new file mode 100644
index 0000000..8e29991
--- /dev/null
+++ b/sub/filter_regex.c
@@ -0,0 +1,89 @@
+#include <regex.h>
+#include <sys/types.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "sd.h"
+
+struct priv {
+ int offset;
+ regex_t *regexes;
+ int num_regexes;
+};
+
+static bool rf_init(struct sd_filter *ft)
+{
+ if (strcmp(ft->codec, "ass") != 0)
+ return false;
+
+ if (!ft->opts->rf_enable)
+ return false;
+
+ struct priv *p = talloc_zero(ft, struct priv);
+ ft->priv = p;
+
+ for (int n = 0; ft->opts->rf_items && ft->opts->rf_items[n]; n++) {
+ char *item = ft->opts->rf_items[n];
+
+ MP_TARRAY_GROW(p, p->regexes, p->num_regexes);
+ regex_t *preg = &p->regexes[p->num_regexes];
+
+ int err = regcomp(preg, item, REG_ICASE | REG_EXTENDED | REG_NOSUB | REG_NEWLINE);
+ if (err) {
+ char errbuf[512];
+ regerror(err, preg, errbuf, sizeof(errbuf));
+ MP_ERR(ft, "Regular expression error: '%s'\n", errbuf);
+ continue;
+ }
+
+ p->num_regexes += 1;
+ }
+
+ if (!p->num_regexes)
+ return false;
+
+ p->offset = sd_ass_fmt_offset(ft->event_format);
+ return true;
+}
+
+static void rf_uninit(struct sd_filter *ft)
+{
+ struct priv *p = ft->priv;
+
+ for (int n = 0; n < p->num_regexes; n++)
+ regfree(&p->regexes[n]);
+}
+
+static struct demux_packet *rf_filter(struct sd_filter *ft,
+ struct demux_packet *pkt)
+{
+ struct priv *p = ft->priv;
+ char *text = bstrto0(NULL, sd_ass_pkt_text(ft, pkt, p->offset));
+ bool drop = false;
+
+ if (ft->opts->rf_plain)
+ sd_ass_to_plaintext(text, strlen(text), text);
+
+ for (int n = 0; n < p->num_regexes; n++) {
+ int err = regexec(&p->regexes[n], text, 0, NULL, 0);
+ if (err == 0) {
+ int level = ft->opts->rf_warn ? MSGL_WARN : MSGL_V;
+ MP_MSG(ft, level, "Matching regex %d => drop: '%s'\n", n, text);
+ drop = true;
+ break;
+ } else if (err != REG_NOMATCH) {
+ MP_WARN(ft, "Error on regexec() on regex %d.\n", n);
+ }
+ }
+
+ talloc_free(text);
+ return drop ? NULL : pkt;
+}
+
+const struct sd_filter_functions sd_filter_regex = {
+ .init = rf_init,
+ .uninit = rf_uninit,
+ .filter = rf_filter,
+};
diff --git a/sub/filter_sdh.c b/sub/filter_sdh.c
new file mode 100644
index 0000000..69fca9f
--- /dev/null
+++ b/sub/filter_sdh.c
@@ -0,0 +1,482 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <stddef.h>
+
+#include "misc/ctype.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "sd.h"
+
+// Filter for removing subtitle additions for deaf or hard-of-hearing (SDH)
+// This is for English, but may in part work for others too.
+// The intention is that it can always be active so may not remove
+// all SDH parts.
+// It is for filtering ASS encoded subtitles
+
+struct buffer {
+ char *string;
+ int length;
+ int pos;
+};
+
+static void init_buf(struct buffer *buf, int length)
+{
+ buf->string = talloc_size(NULL, length);
+ buf->pos = 0;
+ buf->length = length;
+}
+
+static inline int append(struct sd_filter *sd, struct buffer *buf, char c)
+{
+ if (buf->pos >= 0 && buf->pos < buf->length) {
+ buf->string[buf->pos++] = c;
+ } else {
+ // ensure that terminating \0 is always written
+ if (c == '\0')
+ buf->string[buf->length - 1] = c;
+ }
+ return c;
+}
+
+
+// copy ass override tags, if they exist att current position,
+// from source string to destination buffer stopping at first
+// character following last sequence of '{text}'
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// on return the read pointer is updated to the position after
+// the tags.
+static void copy_ass(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ char *rp = *rpp;
+
+ while (rp[0] == '{') {
+ while (*rp) {
+ char tmp = append(sd, buf, rp[0]);
+ rp++;
+ if (tmp == '}')
+ break;
+ }
+ }
+ *rpp = rp;
+
+ return;
+}
+
+static bool skip_bracketed(struct sd_filter *sd, char **rpp, struct buffer *buf);
+
+// check for speaker label, like MAN:
+// normal subtitles may include mixed case text with : after so
+// only upper case is accepted and lower case l which for some
+// looks like upper case I unless filter_harder - then
+// lower case is also acceptable
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// scan in source string and copy ass tags to destination string
+// skipping speaker label if it exists
+//
+// if no label was found read pointer and write position in buffer
+// will be unchanged
+// otherwise they point to next position after label and next write position
+static void skip_speaker_label(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ int filter_harder = sd->opts->sub_filter_SDH_harder;
+ char *rp = *rpp;
+ int old_pos = buf->pos;
+
+ copy_ass(sd, &rp, buf);
+ // copy any leading "- "
+ if (rp[0] == '-') {
+ append(sd, buf, rp[0]);
+ rp++;
+ }
+ copy_ass(sd, &rp, buf);
+ while (rp[0] == ' ') {
+ append(sd, buf, rp[0]);
+ rp++;
+ copy_ass(sd, &rp, buf);
+ }
+ // skip past valid data searching for :
+ while (*rp && rp[0] != ':') {
+ if (rp[0] == '{') {
+ copy_ass(sd, &rp, buf);
+ } else if (rp[0] == '[') {
+ // not uncommon with [xxxx]: which should also be skipped
+ if (!skip_bracketed(sd, &rp, buf)) {
+ buf->pos = old_pos;
+ return;
+ }
+ } else if ((mp_isalpha(rp[0]) &&
+ (filter_harder || mp_isupper(rp[0]) || rp[0] == 'l')) ||
+ mp_isdigit(rp[0]) ||
+ rp[0] == ' ' || rp[0] == '\'' ||
+ (filter_harder && (rp[0] == '(' || rp[0] == ')')) ||
+ rp[0] == '#' || rp[0] == '.' || rp[0] == ',') {
+ rp++;
+ } else {
+ buf->pos = old_pos;
+ return;
+ }
+ }
+ if (!*rp) {
+ // : was not found
+ buf->pos = old_pos;
+ return;
+ }
+ rp++; // skip :
+ copy_ass(sd, &rp, buf);
+ if (!*rp) {
+ // end of data
+ } else if (rp[0] == '\\' && rp[1] == 'N') {
+ // line end follows - skip it as line is empty
+ rp += 2;
+ } else if (rp[0] == ' ') {
+ while (rp[0] == ' ') {
+ rp++;
+ }
+ if (rp[0] == '\\' && rp[1] == 'N') {
+ // line end follows - skip it as line is empty
+ rp += 2;
+ }
+ } else {
+ // non space follows - no speaker label
+ buf->pos = old_pos;
+ return;
+ }
+ *rpp = rp;
+
+ return;
+}
+
+// check for bracketed text, like [SOUND]
+// and skip it while preserving ass tags
+// any characters are allowed, brackets are seldom used in normal text
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// scan in source string
+// the first character in source string must by the starting '['
+// and copy ass tags to destination string but
+// skipping bracketed text if it looks like SDH
+//
+// return true if bracketed text was removed.
+// if not valid SDH read pointer and write buffer position will be unchanged
+// otherwise they point to next position after text and next write position
+static bool skip_bracketed(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ char *rp = *rpp;
+ int old_pos = buf->pos;
+
+ rp++; // skip past '['
+ // skip past valid data searching for ]
+ while (*rp && rp[0] != ']') {
+ if (rp[0] == '{') {
+ copy_ass(sd, &rp, buf);
+ } else {
+ rp++;
+ }
+ }
+ if (!*rp) {
+ // ] was not found
+ buf->pos = old_pos;
+ return false;
+ }
+ rp++; // skip ]
+ // skip trailing spaces
+ while (rp[0] == ' ') {
+ rp++;
+ }
+ *rpp = rp;
+
+ return true;
+}
+
+// check for parenthesized text, like (SOUND)
+// and skip it while preserving ass tags
+// normal subtitles may include mixed case text in parentheses so
+// only upper case is accepted and lower case l which for some
+// looks like upper case I but if requested harder filtering
+// both upper and lower case is accepted
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// scan in source string
+// the first character in source string must be the starting '('
+// and copy ass tags to destination string but
+// skipping parenthesized text if it looks like SDH
+//
+// return true if parenthesized text was removed.
+// if not valid SDH read pointer and write buffer position will be unchanged
+// otherwise they point to next position after text and next write position
+static bool skip_parenthesized(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ int filter_harder = sd->opts->sub_filter_SDH_harder;
+ char *rp = *rpp;
+ int old_pos = buf->pos;
+
+ rp++; // skip past '('
+ // skip past valid data searching for )
+ bool only_digits = true;
+ while (*rp && rp[0] != ')') {
+ if (rp[0] == '{') {
+ copy_ass(sd, &rp, buf);
+ } else if ((mp_isalpha(rp[0]) &&
+ (filter_harder || mp_isupper(rp[0]) || rp[0] == 'l')) ||
+ mp_isdigit(rp[0]) ||
+ rp[0] == ' ' || rp[0] == '\'' || rp[0] == '#' ||
+ rp[0] == '.' || rp[0] == ',' ||
+ rp[0] == '-' || rp[0] == '"' || rp[0] == '\\') {
+ if (!mp_isdigit(rp[0]))
+ only_digits = false;
+ rp++;
+ } else {
+ buf->pos = old_pos;
+ return false;
+ }
+ }
+ if (!*rp) {
+ // ) was not found
+ buf->pos = old_pos;
+ return false;
+ }
+ if (only_digits) {
+ // number within parentheses is probably not SDH
+ buf->pos = old_pos;
+ return false;
+ }
+ rp++; // skip )
+ // skip trailing spaces
+ while (rp[0] == ' ') {
+ rp++;
+ }
+ *rpp = rp;
+
+ return true;
+}
+
+// remove leading hyphen and following spaces in write buffer
+//
+// Parameters:
+// start_pos start position i buffer
+// buf buffer to remove in
+//
+// when removing characters the following are moved back
+//
+static void remove_leading_hyphen_space(struct sd_filter *sd, int start_pos,
+ struct buffer *buf)
+{
+ int old_pos = buf->pos;
+ if (start_pos < 0 || start_pos >= old_pos)
+ return;
+ append(sd, buf, '\0'); // \0 terminate for reading
+
+ // move past leading ass tags
+ while (buf->string[start_pos] == '{') {
+ while (buf->string[start_pos] && buf->string[start_pos] != '}') {
+ start_pos++;
+ }
+ if (buf->string[start_pos])
+ start_pos++; // skip past '}'
+ }
+
+ // if there is not a leading '-' no removing will be done
+ if (buf->string[start_pos] != '-') {
+ buf->pos = old_pos;
+ return;
+ }
+
+ char *rp = &buf->string[start_pos]; // read from here
+ buf->pos = start_pos; // start writing here
+ rp++; // skip '-'
+ copy_ass(sd, &rp, buf);
+ while (rp[0] == ' ') {
+ rp++; // skip ' '
+ copy_ass(sd, &rp, buf);
+ }
+ while (*rp) {
+ // copy the rest
+ append(sd, buf, rp[0]);
+ rp++;
+ }
+}
+
+// Filter ASS formatted string for SDH
+//
+// Parameters:
+// data ASS line
+// length length of ASS line
+// toff Text offset from data. required: 0 <= toff <= length
+//
+// Returns a talloc allocated string with filtered ASS data (may be the same
+// content as original if no SDH was found) which must be released
+// by caller using talloc_free.
+//
+// Returns NULL if filtering resulted in all of ASS data being removed so no
+// subtitle should be output
+static char *filter_SDH(struct sd_filter *sd, char *data, int length, ptrdiff_t toff)
+{
+ struct buffer writebuf;
+ struct buffer *buf = &writebuf;
+ init_buf(buf, length + 1); // with room for terminating '\0'
+
+ // pre-text headers into buf, rp is the (null-terminated) remaining text
+ char *ass = talloc_strndup(NULL, data, length), *rp = ass;
+ while (rp - ass < toff)
+ append(sd, buf, *rp++);
+
+ bool contains_text = false; // true if non SDH text was found
+ bool line_with_text = false; // if last line contained text
+ int wp_line_start = buf->pos; // write pos to start of last line
+ int wp_line_end = buf->pos; // write pos to end of previous line with text (\N)
+
+ // go through the lines in the text
+ // they are separated by \N
+ while (*rp) {
+ line_with_text = false;
+ wp_line_start = buf->pos;
+
+ // skip any speaker label
+ skip_speaker_label(sd, &rp, buf);
+
+ // go through the rest of the line looking for SDH in () or []
+ while (*rp && !(rp[0] == '\\' && rp[1] == 'N')) {
+ copy_ass(sd, &rp, buf);
+ if (rp[0] == '[') {
+ if (!skip_bracketed(sd, &rp, buf)) {
+ append(sd, buf, rp[0]);
+ rp++;
+ line_with_text = true;
+ }
+ } else if (rp[0] == '(') {
+ if (!skip_parenthesized(sd, &rp, buf)) {
+ append(sd, buf, rp[0]);
+ rp++;
+ line_with_text = true;
+ }
+ } else if (*rp && rp[0] != '\\') {
+ if ((rp[0] > 32 && rp[0] < 127 && rp[0] != '-') ||
+ (unsigned char)rp[0] >= 0xC0)
+ {
+ line_with_text = true;
+ }
+ append(sd, buf, rp[0]);
+ rp++;
+ } else if (rp[0] == '\\' && rp[1] != 'N') {
+ append(sd, buf, rp[0]);
+ rp++;
+ }
+ }
+ // either end of data or ASS line end defined by separating \N
+ if (*rp) {
+ // ASS line end
+ if (line_with_text) {
+ contains_text = true;
+ wp_line_end = buf->pos;
+ append(sd, buf, rp[0]); // copy backslash
+ append(sd, buf, rp[1]); // copy N
+ rp += 2; // move read pointer past \N
+ } else {
+ // no text in line, remove leading hyphen and spaces
+ remove_leading_hyphen_space(sd, wp_line_start, buf);
+ // and join with next line
+ rp += 2; // move read pointer past \N
+ }
+ }
+ }
+ // if no normal text in last line - remove last line
+ // by moving write pointer to start of last line
+ if (!line_with_text) {
+ buf->pos = wp_line_end;
+ } else {
+ contains_text = true;
+ }
+ talloc_free(ass);
+
+ if (contains_text) {
+ // the ASS data contained normal text after filtering
+ append(sd, buf, '\0'); // '\0' terminate
+ return buf->string;
+ } else {
+ // all data removed by filtering
+ talloc_free(buf->string);
+ return NULL;
+ }
+}
+
+static bool sdh_init(struct sd_filter *ft)
+{
+ if (strcmp(ft->codec, "ass") != 0)
+ return false;
+
+ if (!ft->opts->sub_filter_SDH)
+ return false;
+
+ if (!ft->event_format) {
+ MP_VERBOSE(ft, "SDH filtering not possible - format missing\n");
+ return false;
+ }
+
+ return true;
+}
+
+static struct demux_packet *sdh_filter(struct sd_filter *ft,
+ struct demux_packet *pkt)
+{
+ bstr text = sd_ass_pkt_text(ft, pkt, sd_ass_fmt_offset(ft->event_format));
+ if (!text.start || !text.len || pkt->len >= INT_MAX)
+ return pkt; // we don't touch it
+
+ ptrdiff_t toff = text.start - pkt->buffer;
+ char *line = filter_SDH(ft, (char *)pkt->buffer, (int)pkt->len, toff);
+ if (!line)
+ return NULL;
+ if (0 == bstrcmp0((bstr){(char *)pkt->buffer, pkt->len}, line)) {
+ talloc_free(line);
+ return pkt; // unmodified, no need to allocate new packet
+ }
+
+ // Stupidly, this copies it again. One could possibly allocate the packet
+ // for writing in the first place (new_demux_packet()) and use
+ // demux_packet_shorten().
+ struct demux_packet *npkt = new_demux_packet_from(line, strlen(line));
+ if (npkt)
+ demux_packet_copy_attribs(npkt, pkt);
+
+ talloc_free(line);
+ return npkt;
+}
+
+const struct sd_filter_functions sd_filter_sdh = {
+ .init = sdh_init,
+ .filter = sdh_filter,
+};
diff --git a/sub/img_convert.c b/sub/img_convert.c
new file mode 100644
index 0000000..a70bb0a
--- /dev/null
+++ b/sub/img_convert.c
@@ -0,0 +1,128 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <assert.h>
+#include <limits.h>
+
+#include "mpv_talloc.h"
+
+#include "common/common.h"
+#include "img_convert.h"
+#include "osd.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+
+void mp_blur_rgba_sub_bitmap(struct sub_bitmap *d, double gblur)
+{
+ struct mp_image *tmp1 = mp_image_alloc(IMGFMT_BGRA, d->w, d->h);
+ if (tmp1) { // on OOM, skip region
+ struct mp_image s = {0};
+ mp_image_setfmt(&s, IMGFMT_BGRA);
+ mp_image_set_size(&s, d->w, d->h);
+ s.stride[0] = d->stride;
+ s.planes[0] = d->bitmap;
+
+ mp_image_copy(tmp1, &s);
+
+ mp_image_sw_blur_scale(&s, tmp1, gblur);
+ }
+ talloc_free(tmp1);
+}
+
+bool mp_sub_bitmaps_bb(struct sub_bitmaps *imgs, struct mp_rect *out_bb)
+{
+ struct mp_rect bb = {INT_MAX, INT_MAX, INT_MIN, INT_MIN};
+ for (int n = 0; n < imgs->num_parts; n++) {
+ struct sub_bitmap *p = &imgs->parts[n];
+ bb.x0 = MPMIN(bb.x0, p->x);
+ bb.y0 = MPMIN(bb.y0, p->y);
+ bb.x1 = MPMAX(bb.x1, p->x + p->dw);
+ bb.y1 = MPMAX(bb.y1, p->y + p->dh);
+ }
+
+ // avoid degenerate bounding box if empty
+ bb.x0 = MPMIN(bb.x0, bb.x1);
+ bb.y0 = MPMIN(bb.y0, bb.y1);
+
+ *out_bb = bb;
+
+ return bb.x0 < bb.x1 && bb.y0 < bb.y1;
+}
+
+// Merge bounding rectangles if they're closer than the given amount of pixels.
+// Avoids having too many rectangles due to spacing between letter.
+#define MERGE_RC_PIXELS 50
+
+static void remove_intersecting_rcs(struct mp_rect *list, int *count)
+{
+ int M = MERGE_RC_PIXELS;
+ bool changed = true;
+ while (changed) {
+ changed = false;
+ for (int a = 0; a < *count; a++) {
+ struct mp_rect *rc_a = &list[a];
+ for (int b = *count - 1; b > a; b--) {
+ struct mp_rect *rc_b = &list[b];
+ if (rc_a->x0 - M <= rc_b->x1 && rc_a->x1 + M >= rc_b->x0 &&
+ rc_a->y0 - M <= rc_b->y1 && rc_a->y1 + M >= rc_b->y0)
+ {
+ mp_rect_union(rc_a, rc_b);
+ MP_TARRAY_REMOVE_AT(list, *count, b);
+ changed = true;
+ }
+ }
+ }
+ }
+}
+
+// Cluster the given subrectangles into a small numbers of bounding rectangles,
+// and store them into list. E.g. when subtitles and toptitles are visible at
+// the same time, there should be two bounding boxes, so that the video between
+// the text is left untouched (need to resample less pixels -> faster).
+// Returns number of rectangles added to out_rc_list (<= rc_list_count)
+// NOTE: some callers assume that sub bitmaps are never split or partially
+// covered by returned rectangles.
+int mp_get_sub_bb_list(struct sub_bitmaps *sbs, struct mp_rect *out_rc_list,
+ int rc_list_count)
+{
+ int M = MERGE_RC_PIXELS;
+ int num_rc = 0;
+ for (int n = 0; n < sbs->num_parts; n++) {
+ struct sub_bitmap *sb = &sbs->parts[n];
+ struct mp_rect bb = {sb->x, sb->y, sb->x + sb->dw, sb->y + sb->dh};
+ bool intersects = false;
+ for (int r = 0; r < num_rc; r++) {
+ struct mp_rect *rc = &out_rc_list[r];
+ if ((bb.x0 - M <= rc->x1 && bb.x1 + M >= rc->x0 &&
+ bb.y0 - M <= rc->y1 && bb.y1 + M >= rc->y0) ||
+ num_rc == rc_list_count)
+ {
+ mp_rect_union(rc, &bb);
+ intersects = true;
+ break;
+ }
+ }
+ if (!intersects) {
+ out_rc_list[num_rc++] = bb;
+ remove_intersecting_rcs(out_rc_list, &num_rc);
+ }
+ }
+ remove_intersecting_rcs(out_rc_list, &num_rc);
+ return num_rc;
+}
diff --git a/sub/img_convert.h b/sub/img_convert.h
new file mode 100644
index 0000000..e03c155
--- /dev/null
+++ b/sub/img_convert.h
@@ -0,0 +1,23 @@
+#ifndef MPLAYER_SUB_IMG_CONVERT_H
+#define MPLAYER_SUB_IMG_CONVERT_H
+
+#include <stdbool.h>
+
+struct sub_bitmaps;
+struct sub_bitmap;
+struct mp_rect;
+
+// Sub postprocessing
+void mp_blur_rgba_sub_bitmap(struct sub_bitmap *d, double gblur);
+
+bool mp_sub_bitmaps_bb(struct sub_bitmaps *imgs, struct mp_rect *out_bb);
+
+// Intentionally limit the maximum number of bounding rects to something low.
+// This prevents the algorithm from degrading to O(N^2).
+// Most subtitles yield a very low number of bounding rects (<5).
+#define MP_SUB_BB_LIST_MAX 15
+
+int mp_get_sub_bb_list(struct sub_bitmaps *sbs, struct mp_rect *out_rc_list,
+ int rc_list_count);
+
+#endif
diff --git a/sub/lavc_conv.c b/sub/lavc_conv.c
new file mode 100644
index 0000000..532e91d
--- /dev/null
+++ b/sub/lavc_conv.c
@@ -0,0 +1,293 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/common.h>
+#include <libavutil/opt.h>
+
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "common/av_common.h"
+#include "demux/stheader.h"
+#include "misc/bstr.h"
+#include "sd.h"
+
+struct lavc_conv {
+ struct mp_log *log;
+ AVCodecContext *avctx;
+ AVPacket *avpkt;
+ AVPacket *avpkt_vtt;
+ char *codec;
+ char *extradata;
+ AVSubtitle cur;
+ char **cur_list;
+};
+
+static const char *get_lavc_format(const char *format)
+{
+ // For the hack involving parse_webvtt().
+ if (format && strcmp(format, "webvtt-webm") == 0)
+ format = "webvtt";
+ // Most text subtitles are srt/html style anyway.
+ if (format && strcmp(format, "text") == 0)
+ format = "subrip";
+ return format;
+}
+
+// Disable style definitions generated by the libavcodec converter.
+// We always want the user defined style instead.
+static void disable_styles(bstr header)
+{
+ bstr style = bstr0("\nStyle: ");
+ while (header.len) {
+ int n = bstr_find(header, style);
+ if (n < 0)
+ break;
+ header.start[n + 1] = '#'; // turn into a comment
+ header = bstr_cut(header, n + style.len);
+ }
+}
+
+struct lavc_conv *lavc_conv_create(struct mp_log *log,
+ const struct mp_codec_params *mp_codec)
+{
+ struct lavc_conv *priv = talloc_zero(NULL, struct lavc_conv);
+ priv->log = log;
+ priv->cur_list = talloc_array(priv, char*, 0);
+ priv->codec = talloc_strdup(priv, mp_codec->codec);
+ AVCodecContext *avctx = NULL;
+ AVDictionary *opts = NULL;
+ const char *fmt = get_lavc_format(priv->codec);
+ const AVCodec *codec = avcodec_find_decoder(mp_codec_to_av_codec_id(fmt));
+ if (!codec)
+ goto error;
+ avctx = avcodec_alloc_context3(codec);
+ if (!avctx)
+ goto error;
+ if (mp_set_avctx_codec_headers(avctx, mp_codec) < 0)
+ goto error;
+
+ priv->avpkt = av_packet_alloc();
+ priv->avpkt_vtt = av_packet_alloc();
+ if (!priv->avpkt || !priv->avpkt_vtt)
+ goto error;
+
+#if LIBAVCODEC_VERSION_MAJOR < 59
+ av_dict_set(&opts, "sub_text_format", "ass", 0);
+#endif
+ av_dict_set(&opts, "flags2", "+ass_ro_flush_noop", 0);
+ if (strcmp(priv->codec, "eia_608") == 0)
+ av_dict_set(&opts, "real_time", "1", 0);
+ if (avcodec_open2(avctx, codec, &opts) < 0)
+ goto error;
+ av_dict_free(&opts);
+ // Documented as "set by libavcodec", but there is no other way
+ avctx->time_base = (AVRational) {1, 1000};
+ avctx->pkt_timebase = avctx->time_base;
+ avctx->sub_charenc_mode = FF_SUB_CHARENC_MODE_IGNORE;
+ priv->avctx = avctx;
+ priv->extradata = talloc_strndup(priv, avctx->subtitle_header,
+ avctx->subtitle_header_size);
+ disable_styles(bstr0(priv->extradata));
+ return priv;
+
+ error:
+ MP_FATAL(priv, "Could not open libavcodec subtitle converter\n");
+ av_dict_free(&opts);
+ av_free(avctx);
+ mp_free_av_packet(&priv->avpkt);
+ mp_free_av_packet(&priv->avpkt_vtt);
+ talloc_free(priv);
+ return NULL;
+}
+
+char *lavc_conv_get_extradata(struct lavc_conv *priv)
+{
+ return priv->extradata;
+}
+
+// FFmpeg WebVTT packets are pre-parsed in some way. The FFmpeg Matroska
+// demuxer does this on its own. In order to free our demuxer_mkv.c from
+// codec-specific crud, we do this here.
+// Copied from libavformat/matroskadec.c (FFmpeg 818ebe9 / 2013-08-19)
+// License: LGPL v2.1 or later
+// Author header: The FFmpeg Project
+// Modified in some ways.
+static int parse_webvtt(AVPacket *in, AVPacket *pkt)
+{
+ uint8_t *id, *settings, *text, *buf;
+ int id_len, settings_len, text_len;
+ uint8_t *p, *q;
+ int err;
+
+ uint8_t *data = in->data;
+ int data_len = in->size;
+
+ if (data_len <= 0)
+ return AVERROR_INVALIDDATA;
+
+ p = data;
+ q = data + data_len;
+
+ id = p;
+ id_len = -1;
+ while (p < q) {
+ if (*p == '\r' || *p == '\n') {
+ id_len = p - id;
+ if (*p == '\r')
+ p++;
+ break;
+ }
+ p++;
+ }
+
+ if (p >= q || *p != '\n')
+ return AVERROR_INVALIDDATA;
+ p++;
+
+ settings = p;
+ settings_len = -1;
+ while (p < q) {
+ if (*p == '\r' || *p == '\n') {
+ settings_len = p - settings;
+ if (*p == '\r')
+ p++;
+ break;
+ }
+ p++;
+ }
+
+ if (p >= q || *p != '\n')
+ return AVERROR_INVALIDDATA;
+ p++;
+
+ text = p;
+ text_len = q - p;
+ while (text_len > 0) {
+ const int len = text_len - 1;
+ const uint8_t c = p[len];
+ if (c != '\r' && c != '\n')
+ break;
+ text_len = len;
+ }
+
+ if (text_len <= 0)
+ return AVERROR_INVALIDDATA;
+
+ err = av_new_packet(pkt, text_len);
+ if (err < 0)
+ return AVERROR(err);
+
+ memcpy(pkt->data, text, text_len);
+
+ if (id_len > 0) {
+ buf = av_packet_new_side_data(pkt,
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+ id_len);
+ if (buf == NULL) {
+ av_packet_unref(pkt);
+ return AVERROR(ENOMEM);
+ }
+ memcpy(buf, id, id_len);
+ }
+
+ if (settings_len > 0) {
+ buf = av_packet_new_side_data(pkt,
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+ settings_len);
+ if (buf == NULL) {
+ av_packet_unref(pkt);
+ return AVERROR(ENOMEM);
+ }
+ memcpy(buf, settings, settings_len);
+ }
+
+ pkt->pts = in->pts;
+ pkt->duration = in->duration;
+ return 0;
+}
+
+// Return a NULL-terminated list of ASS event lines and have
+// the AVSubtitle display PTS and duration set to input
+// double variables.
+char **lavc_conv_decode(struct lavc_conv *priv, struct demux_packet *packet,
+ double *sub_pts, double *sub_duration)
+{
+ AVCodecContext *avctx = priv->avctx;
+ AVPacket *curr_pkt = priv->avpkt;
+ int ret, got_sub;
+ int num_cur = 0;
+
+ avsubtitle_free(&priv->cur);
+
+ mp_set_av_packet(priv->avpkt, packet, &avctx->time_base);
+ if (priv->avpkt->pts < 0)
+ priv->avpkt->pts = 0;
+
+ if (strcmp(priv->codec, "webvtt-webm") == 0) {
+ if (parse_webvtt(priv->avpkt, priv->avpkt_vtt) < 0) {
+ MP_ERR(priv, "Error parsing subtitle\n");
+ goto done;
+ }
+ curr_pkt = priv->avpkt_vtt;
+ }
+
+ ret = avcodec_decode_subtitle2(avctx, &priv->cur, &got_sub, curr_pkt);
+ if (ret < 0) {
+ MP_ERR(priv, "Error decoding subtitle\n");
+ } else if (got_sub) {
+ *sub_pts = packet->pts + mp_pts_from_av(priv->cur.start_display_time,
+ &avctx->time_base);
+ *sub_duration = priv->cur.end_display_time == UINT32_MAX ?
+ UINT32_MAX :
+ mp_pts_from_av(priv->cur.end_display_time -
+ priv->cur.start_display_time,
+ &avctx->time_base);
+
+ for (int i = 0; i < priv->cur.num_rects; i++) {
+ if (priv->cur.rects[i]->w > 0 && priv->cur.rects[i]->h > 0)
+ MP_WARN(priv, "Ignoring bitmap subtitle.\n");
+ char *ass_line = priv->cur.rects[i]->ass;
+ if (!ass_line)
+ continue;
+ MP_TARRAY_APPEND(priv, priv->cur_list, num_cur, ass_line);
+ }
+ }
+
+done:
+ av_packet_unref(priv->avpkt_vtt);
+ MP_TARRAY_APPEND(priv, priv->cur_list, num_cur, NULL);
+ return priv->cur_list;
+}
+
+void lavc_conv_reset(struct lavc_conv *priv)
+{
+ avcodec_flush_buffers(priv->avctx);
+}
+
+void lavc_conv_uninit(struct lavc_conv *priv)
+{
+ avsubtitle_free(&priv->cur);
+ avcodec_free_context(&priv->avctx);
+ mp_free_av_packet(&priv->avpkt);
+ mp_free_av_packet(&priv->avpkt_vtt);
+ talloc_free(priv);
+}
diff --git a/sub/meson.build b/sub/meson.build
new file mode 100644
index 0000000..867f218
--- /dev/null
+++ b/sub/meson.build
@@ -0,0 +1,6 @@
+osd_font = custom_target('osd_font.otf',
+ input: join_paths(source_root, 'sub', 'osd_font.otf'),
+ output: 'osd_font.otf.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+)
+sources += osd_font
diff --git a/sub/osd.c b/sub/osd.c
new file mode 100644
index 0000000..9d6926d
--- /dev/null
+++ b/sub/osd.c
@@ -0,0 +1,559 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "common/common.h"
+
+#include "stream/stream.h"
+
+#include "osdep/timer.h"
+
+#include "mpv_talloc.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/stats.h"
+#include "player/client.h"
+#include "player/command.h"
+#include "osd.h"
+#include "osd_state.h"
+#include "dec_sub.h"
+#include "img_convert.h"
+#include "draw_bmp.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+
+#define OPT_BASE_STRUCT struct osd_style_opts
+static const m_option_t style_opts[] = {
+ {"font", OPT_STRING(font)},
+ {"font-size", OPT_FLOAT(font_size), M_RANGE(1, 9000)},
+ {"color", OPT_COLOR(color)},
+ {"border-color", OPT_COLOR(border_color)},
+ {"shadow-color", OPT_COLOR(shadow_color)},
+ {"back-color", OPT_COLOR(back_color)},
+ {"border-size", OPT_FLOAT(border_size)},
+ {"shadow-offset", OPT_FLOAT(shadow_offset)},
+ {"spacing", OPT_FLOAT(spacing), M_RANGE(-10, 10)},
+ {"margin-x", OPT_INT(margin_x), M_RANGE(0, 300)},
+ {"margin-y", OPT_INT(margin_y), M_RANGE(0, 600)},
+ {"align-x", OPT_CHOICE(align_x,
+ {"left", -1}, {"center", 0}, {"right", +1})},
+ {"align-y", OPT_CHOICE(align_y,
+ {"top", -1}, {"center", 0}, {"bottom", +1})},
+ {"blur", OPT_FLOAT(blur), M_RANGE(0, 20)},
+ {"bold", OPT_BOOL(bold)},
+ {"italic", OPT_BOOL(italic)},
+ {"justify", OPT_CHOICE(justify,
+ {"auto", 0}, {"left", 1}, {"center", 2}, {"right", 3})},
+ {"font-provider", OPT_CHOICE(font_provider,
+ {"auto", 0}, {"none", 1}, {"fontconfig", 2}), .flags = UPDATE_SUB_HARD},
+ {"fonts-dir", OPT_STRING(fonts_dir),
+ .flags = M_OPT_FILE | UPDATE_SUB_HARD},
+ {0}
+};
+
+const struct m_sub_options osd_style_conf = {
+ .opts = style_opts,
+ .size = sizeof(struct osd_style_opts),
+ .defaults = &(const struct osd_style_opts){
+ .font = "sans-serif",
+ .font_size = 55,
+ .color = {255, 255, 255, 255},
+ .border_color = {0, 0, 0, 255},
+ .shadow_color = {240, 240, 240, 128},
+ .border_size = 3,
+ .shadow_offset = 0,
+ .margin_x = 25,
+ .margin_y = 22,
+ .align_x = -1,
+ .align_y = -1,
+ },
+ .change_flags = UPDATE_OSD,
+};
+
+const struct m_sub_options sub_style_conf = {
+ .opts = style_opts,
+ .size = sizeof(struct osd_style_opts),
+ .defaults = &(const struct osd_style_opts){
+ .font = "sans-serif",
+ .font_size = 55,
+ .color = {255, 255, 255, 255},
+ .border_color = {0, 0, 0, 255},
+ .shadow_color = {240, 240, 240, 128},
+ .border_size = 3,
+ .shadow_offset = 0,
+ .margin_x = 25,
+ .margin_y = 22,
+ .align_x = 0,
+ .align_y = 1,
+ },
+ .change_flags = UPDATE_OSD,
+};
+
+bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b)
+{
+ return a.w == b.w && a.h == b.h && a.ml == b.ml && a.mt == b.mt
+ && a.mr == b.mr && a.mb == b.mb
+ && a.display_par == b.display_par;
+}
+
+struct osd_state *osd_create(struct mpv_global *global)
+{
+ assert(MAX_OSD_PARTS >= OSDTYPE_COUNT);
+
+ struct osd_state *osd = talloc_zero(NULL, struct osd_state);
+ *osd = (struct osd_state) {
+ .opts_cache = m_config_cache_alloc(osd, global, &mp_osd_render_sub_opts),
+ .global = global,
+ .log = mp_log_new(osd, global->log, "osd"),
+ .force_video_pts = MP_NOPTS_VALUE,
+ .stats = stats_ctx_create(osd, global, "osd"),
+ };
+ mp_mutex_init(&osd->lock);
+ osd->opts = osd->opts_cache->opts;
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct osd_object *obj = talloc(osd, struct osd_object);
+ *obj = (struct osd_object) {
+ .type = n,
+ .text = talloc_strdup(obj, ""),
+ .progbar_state = {.type = -1},
+ .vo_change_id = 1,
+ };
+ osd->objs[n] = obj;
+ }
+
+ osd->objs[OSDTYPE_SUB]->is_sub = true;
+ osd->objs[OSDTYPE_SUB2]->is_sub = true;
+
+ osd_init_backend(osd);
+ return osd;
+}
+
+void osd_free(struct osd_state *osd)
+{
+ if (!osd)
+ return;
+ osd_destroy_backend(osd);
+ talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->external2);
+ mp_mutex_destroy(&osd->lock);
+ talloc_free(osd);
+}
+
+void osd_set_text(struct osd_state *osd, const char *text)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *osd_obj = osd->objs[OSDTYPE_OSD];
+ if (!text)
+ text = "";
+ if (strcmp(osd_obj->text, text) != 0) {
+ talloc_free(osd_obj->text);
+ osd_obj->text = talloc_strdup(osd_obj, text);
+ osd_obj->osd_changed = true;
+ osd->want_redraw_notification = true;
+ }
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_sub(struct osd_state *osd, int index, struct dec_sub *dec_sub)
+{
+ mp_mutex_lock(&osd->lock);
+ if (index >= 0 && index < 2) {
+ struct osd_object *obj = osd->objs[OSDTYPE_SUB + index];
+ obj->sub = dec_sub;
+ obj->vo_change_id += 1;
+ }
+ osd->want_redraw_notification = true;
+ mp_mutex_unlock(&osd->lock);
+}
+
+bool osd_get_render_subs_in_filter(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ bool r = osd->render_subs_in_filter;
+ mp_mutex_unlock(&osd->lock);
+ return r;
+}
+
+void osd_set_render_subs_in_filter(struct osd_state *osd, bool s)
+{
+ mp_mutex_lock(&osd->lock);
+ if (osd->render_subs_in_filter != s) {
+ osd->render_subs_in_filter = s;
+
+ int change_id = 0;
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ change_id = MPMAX(change_id, osd->objs[n]->vo_change_id);
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ osd->objs[n]->vo_change_id = change_id + 1;
+ }
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_force_video_pts(struct osd_state *osd, double video_pts)
+{
+ atomic_store(&osd->force_video_pts, video_pts);
+}
+
+double osd_get_force_video_pts(struct osd_state *osd)
+{
+ return atomic_load(&osd->force_video_pts);
+}
+
+void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *osd_obj = osd->objs[OSDTYPE_OSD];
+ osd_obj->progbar_state.type = s->type;
+ osd_obj->progbar_state.value = s->value;
+ osd_obj->progbar_state.num_stops = s->num_stops;
+ MP_TARRAY_GROW(osd_obj, osd_obj->progbar_state.stops, s->num_stops);
+ if (s->num_stops) {
+ memcpy(osd_obj->progbar_state.stops, s->stops,
+ sizeof(osd_obj->progbar_state.stops[0]) * s->num_stops);
+ }
+ osd_obj->osd_changed = true;
+ osd->want_redraw_notification = true;
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL2];
+ talloc_free(obj->external2);
+ obj->external2 = sub_bitmaps_copy(NULL, imgs);
+ obj->vo_change_id += 1;
+ osd->want_redraw_notification = true;
+ mp_mutex_unlock(&osd->lock);
+}
+
+static void check_obj_resize(struct osd_state *osd, struct mp_osd_res res,
+ struct osd_object *obj)
+{
+ if (!osd_res_equals(res, obj->vo_res)) {
+ obj->vo_res = res;
+ obj->osd_changed = true;
+ mp_client_broadcast_event_external(osd->global->client_api,
+ MP_EVENT_WIN_RESIZE, NULL);
+ }
+}
+
+// Optional. Can be called for faster reaction of OSD-generating scripts like
+// osc.lua. This can achieve that the resize happens first, so that the OSD is
+// generated at the correct resolution the first time the resized frame is
+// rendered. Since the OSD doesn't (and can't) wait for the script, this
+// increases the time in which the script can react, and also gets rid of the
+// unavoidable redraw delay (though it will still be racy).
+// Unnecessary for anything else.
+void osd_resize(struct osd_state *osd, struct mp_osd_res res)
+{
+ mp_mutex_lock(&osd->lock);
+ int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, -1};
+ for (int n = 0; types[n] >= 0; n++)
+ check_obj_resize(osd, res, osd->objs[types[n]]);
+ mp_mutex_unlock(&osd->lock);
+}
+
+static struct sub_bitmaps *render_object(struct osd_state *osd,
+ struct osd_object *obj,
+ struct mp_osd_res osdres, double video_pts,
+ const bool sub_formats[SUBBITMAP_COUNT])
+{
+ int format = SUBBITMAP_LIBASS;
+ if (!sub_formats[format] || osd->opts->force_rgba_osd)
+ format = SUBBITMAP_BGRA;
+
+ struct sub_bitmaps *res = NULL;
+
+ check_obj_resize(osd, osdres, obj);
+
+ if (obj->type == OSDTYPE_SUB) {
+ if (obj->sub && sub_is_primary_visible(obj->sub))
+ res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts);
+ } else if (obj->type == OSDTYPE_SUB2) {
+ if (obj->sub && sub_is_secondary_visible(obj->sub))
+ res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts);
+ } else if (obj->type == OSDTYPE_EXTERNAL2) {
+ if (obj->external2 && obj->external2->format) {
+ res = sub_bitmaps_copy(NULL, obj->external2); // need to be owner
+ obj->external2->change_id = 0;
+ }
+ } else {
+ res = osd_object_get_bitmaps(osd, obj, format);
+ }
+
+ if (obj->vo_had_output != !!res) {
+ obj->vo_had_output = !!res;
+ obj->vo_change_id += 1;
+ }
+
+ if (res) {
+ obj->vo_change_id += res->change_id;
+
+ res->render_index = obj->type;
+ res->change_id = obj->vo_change_id;
+ }
+
+ return res;
+}
+
+// Render OSD to a list of bitmap and return it. The returned object is
+// refcounted. Typically you should hold it only for a short time, and then
+// release it.
+// draw_flags is a bit field of OSD_DRAW_* constants
+struct sub_bitmap_list *osd_render(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT])
+{
+ mp_mutex_lock(&osd->lock);
+
+ struct sub_bitmap_list *list = talloc_zero(NULL, struct sub_bitmap_list);
+ list->change_id = 1;
+ list->w = res.w;
+ list->h = res.h;
+
+ double force_video_pts = atomic_load(&osd->force_video_pts);
+ if (force_video_pts != MP_NOPTS_VALUE)
+ video_pts = force_video_pts;
+
+ if (draw_flags & OSD_DRAW_SUB_FILTER)
+ draw_flags |= OSD_DRAW_SUB_ONLY;
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct osd_object *obj = osd->objs[n];
+
+ // Object is drawn into the video frame itself; don't draw twice
+ if (osd->render_subs_in_filter && obj->is_sub &&
+ !(draw_flags & OSD_DRAW_SUB_FILTER))
+ continue;
+ if ((draw_flags & OSD_DRAW_SUB_ONLY) && !obj->is_sub)
+ continue;
+ if ((draw_flags & OSD_DRAW_OSD_ONLY) && obj->is_sub)
+ continue;
+
+ char *stat_type_render = obj->is_sub ? "sub-render" : "osd-render";
+ stats_time_start(osd->stats, stat_type_render);
+
+ struct sub_bitmaps *imgs =
+ render_object(osd, obj, res, video_pts, formats);
+
+ stats_time_end(osd->stats, stat_type_render);
+
+ if (imgs && imgs->num_parts > 0) {
+ if (formats[imgs->format]) {
+ talloc_steal(list, imgs);
+ MP_TARRAY_APPEND(list, list->items, list->num_items, imgs);
+ imgs = NULL;
+ } else {
+ MP_ERR(osd, "Can't render OSD part %d (format %d).\n",
+ obj->type, imgs->format);
+ }
+ }
+
+ list->change_id += obj->vo_change_id;
+
+ talloc_free(imgs);
+ }
+
+ // If this is called with OSD_DRAW_SUB_ONLY or OSD_DRAW_OSD_ONLY set, assume
+ // it will always draw the complete OSD by doing multiple osd_draw() calls.
+ // OSD_DRAW_SUB_FILTER on the other hand is an evil special-case, and we
+ // must not reset the flag when it happens.
+ if (!(draw_flags & OSD_DRAW_SUB_FILTER))
+ osd->want_redraw_notification = false;
+
+ mp_mutex_unlock(&osd->lock);
+ return list;
+}
+
+// Warning: this function should be considered legacy. Use osd_render() instead.
+void osd_draw(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT],
+ void (*cb)(void *ctx, struct sub_bitmaps *imgs), void *cb_ctx)
+{
+ struct sub_bitmap_list *list =
+ osd_render(osd, res, video_pts, draw_flags, formats);
+
+ stats_time_start(osd->stats, "draw");
+
+ for (int n = 0; n < list->num_items; n++)
+ cb(cb_ctx, list->items[n]);
+
+ stats_time_end(osd->stats, "draw");
+
+ talloc_free(list);
+}
+
+// Calls mp_image_make_writeable() on the dest image if something is drawn.
+// draw_flags as in osd_render().
+void osd_draw_on_image(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags, struct mp_image *dest)
+{
+ osd_draw_on_image_p(osd, res, video_pts, draw_flags, NULL, dest);
+}
+
+// Like osd_draw_on_image(), but if dest needs to be copied to make it
+// writeable, allocate images from the given pool. (This is a minor
+// optimization to reduce "real" image sized memory allocations.)
+void osd_draw_on_image_p(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ struct mp_image_pool *pool, struct mp_image *dest)
+{
+ struct sub_bitmap_list *list =
+ osd_render(osd, res, video_pts, draw_flags, mp_draw_sub_formats);
+
+ if (!list->num_items) {
+ talloc_free(list);
+ return;
+ }
+
+ if (!mp_image_pool_make_writeable(pool, dest))
+ return; // on OOM, skip
+
+ // Need to lock for the dumb osd->draw_cache thing.
+ mp_mutex_lock(&osd->lock);
+
+ if (!osd->draw_cache)
+ osd->draw_cache = mp_draw_sub_alloc(osd, osd->global);
+
+ stats_time_start(osd->stats, "draw-bmp");
+
+ if (!mp_draw_sub_bitmaps(osd->draw_cache, dest, list))
+ MP_WARN(osd, "Failed rendering OSD.\n");
+ talloc_steal(osd, osd->draw_cache);
+
+ stats_time_end(osd->stats, "draw-bmp");
+
+ mp_mutex_unlock(&osd->lock);
+
+ talloc_free(list);
+}
+
+// Setup the OSD resolution to render into an image with the given parameters.
+// The interesting part about this is that OSD has to compensate the aspect
+// ratio if the image does not have a 1:1 pixel aspect ratio.
+struct mp_osd_res osd_res_from_image_params(const struct mp_image_params *p)
+{
+ return (struct mp_osd_res) {
+ .w = p->w,
+ .h = p->h,
+ .display_par = p->p_h / (double)p->p_w,
+ };
+}
+
+// Typically called to react to OSD style changes.
+void osd_changed(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ osd->objs[OSDTYPE_OSD]->osd_changed = true;
+ osd->want_redraw_notification = true;
+ // Done here for a lack of a better place.
+ m_config_cache_update(osd->opts_cache);
+ mp_mutex_unlock(&osd->lock);
+}
+
+bool osd_query_and_reset_want_redraw(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ bool r = osd->want_redraw_notification;
+ osd->want_redraw_notification = false;
+ mp_mutex_unlock(&osd->lock);
+ return r;
+}
+
+struct mp_osd_res osd_get_vo_res(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ // Any OSDTYPE is fine; but it mustn't be a subtitle one (can have lower res.)
+ struct mp_osd_res res = osd->objs[OSDTYPE_OSD]->vo_res;
+ mp_mutex_unlock(&osd->lock);
+ return res;
+}
+
+// Position the subbitmaps in imgs on the screen. Basically, this fits the
+// subtitle canvas (of size frame_w x frame_h) onto the screen, such that it
+// fills the whole video area (especially if the video is magnified, e.g. on
+// fullscreen). If compensate_par is >0, adjust the way the subtitles are
+// "stretched" on the screen, and letter-box the result. If compensate_par
+// is <0, strictly letter-box the subtitles. If it is 0, stretch them.
+void osd_rescale_bitmaps(struct sub_bitmaps *imgs, int frame_w, int frame_h,
+ struct mp_osd_res res, double compensate_par)
+{
+ int vidw = res.w - res.ml - res.mr;
+ int vidh = res.h - res.mt - res.mb;
+ double xscale = (double)vidw / frame_w;
+ double yscale = (double)vidh / frame_h;
+ if (compensate_par < 0) {
+ assert(res.display_par);
+ compensate_par = xscale / yscale / res.display_par;
+ }
+ if (compensate_par > 0)
+ xscale /= compensate_par;
+ int cx = vidw / 2 - (int)(frame_w * xscale) / 2;
+ int cy = vidh / 2 - (int)(frame_h * yscale) / 2;
+ for (int i = 0; i < imgs->num_parts; i++) {
+ struct sub_bitmap *bi = &imgs->parts[i];
+ bi->x = (int)(bi->x * xscale) + cx + res.ml;
+ bi->y = (int)(bi->y * yscale) + cy + res.mt;
+ bi->dw = (int)(bi->w * xscale + 0.5);
+ bi->dh = (int)(bi->h * yscale + 0.5);
+ }
+}
+
+// Copy *in and return a new allocation of it. Free with talloc_free(). This
+// will contain a refcounted copy of the image data.
+//
+// in->packed must be set and must be a refcounted image, unless there is no
+// data (num_parts==0).
+//
+// p_cache: if not NULL, then this points to a struct sub_bitmap_copy_cache*
+// variable. The function may set this to an allocation and may later
+// read it. You have to free it with talloc_free() when done.
+// in: valid struct, or NULL (in this case it also returns NULL)
+// returns: new copy, or NULL if there was no data in the input
+struct sub_bitmaps *sub_bitmaps_copy(struct sub_bitmap_copy_cache **p_cache,
+ struct sub_bitmaps *in)
+{
+ if (!in || !in->num_parts)
+ return NULL;
+
+ struct sub_bitmaps *res = talloc(NULL, struct sub_bitmaps);
+ *res = *in;
+
+ // Note: the p_cache thing is a lie and unused.
+
+ // The bitmaps being refcounted is essential for performance, and for
+ // not invalidating in->parts[*].bitmap pointers.
+ assert(in->packed && in->packed->bufs[0]);
+
+ res->packed = mp_image_new_ref(res->packed);
+ talloc_steal(res, res->packed);
+
+ res->parts = NULL;
+ MP_RESIZE_ARRAY(res, res->parts, res->num_parts);
+ memcpy(res->parts, in->parts, sizeof(res->parts[0]) * res->num_parts);
+
+ return res;
+}
diff --git a/sub/osd.h b/sub/osd.h
new file mode 100644
index 0000000..39a88ea
--- /dev/null
+++ b/sub/osd.h
@@ -0,0 +1,247 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_SUB_H
+#define MPLAYER_SUB_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "options/m_option.h"
+
+// NOTE: VOs must support at least SUBBITMAP_BGRA.
+enum sub_bitmap_format {
+ SUBBITMAP_EMPTY = 0,// no bitmaps; always has num_parts==0
+ SUBBITMAP_LIBASS, // A8, with a per-surface blend color (libass.color)
+ SUBBITMAP_BGRA, // IMGFMT_BGRA (MSB=A, LSB=B), scaled, premultiplied alpha
+
+ SUBBITMAP_COUNT
+};
+
+struct sub_bitmap {
+ void *bitmap;
+ int stride;
+ // Note: not clipped, going outside the screen area is allowed
+ // (except for SUBBITMAP_LIBASS, which is always clipped)
+ int w, h;
+ int x, y;
+ int dw, dh;
+
+ // If the containing struct sub_bitmaps has the packed field set, then this
+ // is the position within the source. (Strictly speaking this is redundant
+ // with the bitmap pointer.)
+ int src_x, src_y;
+
+ struct {
+ uint32_t color;
+ } libass;
+};
+
+struct sub_bitmaps {
+ // For VO cache state (limited by MAX_OSD_PARTS)
+ int render_index;
+
+ enum sub_bitmap_format format;
+
+ struct sub_bitmap *parts;
+ int num_parts;
+
+ // Packed representation of the bitmap data. If non-NULL, then the
+ // parts[].bitmap pointer points into the image data here (and stride will
+ // correspond to packed->stride[0]).
+ // SUBBITMAP_BGRA: IMGFMT_BGRA (exact match)
+ // SUBBITMAP_LIBASS: IMGFMT_Y8 (not the same, but compatible layout)
+ // Other formats have this set to NULL.
+ struct mp_image *packed;
+
+ // Bounding box for the packed image. All parts will be within the bounding
+ // box. (The origin of the box is at (0,0).)
+ int packed_w, packed_h;
+
+ int change_id; // Incremented on each change (0 is never used)
+};
+
+struct sub_bitmap_list {
+ // Combined change_id - of any of the existing items change (even if they
+ // e.g. go away and are removed from items[]), this is incremented.
+ int64_t change_id;
+
+ // Bounding box for rendering. It's notable that SUBBITMAP_LIBASS images are
+ // always within these bounds, while SUBBITMAP_BGRA is not necessarily.
+ int w, h;
+
+ // Sorted by sub_bitmaps.render_index. Unused parts are not in the array,
+ // and you cannot index items[] with render_index.
+ struct sub_bitmaps **items;
+ int num_items;
+};
+
+struct sub_bitmap_copy_cache;
+struct sub_bitmaps *sub_bitmaps_copy(struct sub_bitmap_copy_cache **cache,
+ struct sub_bitmaps *in);
+
+struct mp_osd_res {
+ int w, h; // screen dimensions, including black borders
+ int mt, mb, ml, mr; // borders (top, bottom, left, right)
+ double display_par;
+};
+
+bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b);
+
+// 0 <= sub_bitmaps.render_index < MAX_OSD_PARTS
+#define MAX_OSD_PARTS 5
+
+// Start of OSD symbols in osd_font.pfb
+#define OSD_CODEPOINTS 0xE000
+
+// OSD symbols. osd_font.pfb has them starting from codepoint OSD_CODEPOINTS.
+// Symbols with a value >= 32 are normal unicode codepoints.
+enum mp_osd_font_codepoints {
+ OSD_PLAY = 0x01,
+ OSD_PAUSE = 0x02,
+ OSD_STOP = 0x03,
+ OSD_REW = 0x04,
+ OSD_FFW = 0x05,
+ OSD_CLOCK = 0x06,
+ OSD_CONTRAST = 0x07,
+ OSD_SATURATION = 0x08,
+ OSD_VOLUME = 0x09,
+ OSD_BRIGHTNESS = 0x0A,
+ OSD_HUE = 0x0B,
+ OSD_BALANCE = 0x0C,
+ OSD_PANSCAN = 0x50,
+
+ OSD_PB_START = 0x10,
+ OSD_PB_0 = 0x11,
+ OSD_PB_END = 0x12,
+ OSD_PB_1 = 0x13,
+};
+
+
+// Never valid UTF-8, so we expect it's free for use.
+// Specially interpreted by osd_libass.c, in order to allow/escape ASS tags.
+#define OSD_ASS_0 "\xFD"
+#define OSD_ASS_1 "\xFE"
+
+struct osd_style_opts {
+ char *font;
+ float font_size;
+ struct m_color color;
+ struct m_color border_color;
+ struct m_color shadow_color;
+ struct m_color back_color;
+ float border_size;
+ float shadow_offset;
+ float spacing;
+ int margin_x;
+ int margin_y;
+ int align_x;
+ int align_y;
+ float blur;
+ bool bold;
+ bool italic;
+ int justify;
+ int font_provider;
+ char *fonts_dir;
+};
+
+extern const struct m_sub_options osd_style_conf;
+extern const struct m_sub_options sub_style_conf;
+
+struct osd_state;
+struct osd_object;
+struct mpv_global;
+struct dec_sub;
+
+struct osd_state *osd_create(struct mpv_global *global);
+void osd_changed(struct osd_state *osd);
+void osd_free(struct osd_state *osd);
+
+bool osd_query_and_reset_want_redraw(struct osd_state *osd);
+
+void osd_set_text(struct osd_state *osd, const char *text);
+void osd_set_sub(struct osd_state *osd, int index, struct dec_sub *dec_sub);
+
+bool osd_get_render_subs_in_filter(struct osd_state *osd);
+void osd_set_render_subs_in_filter(struct osd_state *osd, bool s);
+void osd_set_force_video_pts(struct osd_state *osd, double video_pts);
+double osd_get_force_video_pts(struct osd_state *osd);
+
+struct osd_progbar_state {
+ int type; // <0: disabled, 1-255: symbol, else: no symbol
+ float value; // range 0.0-1.0
+ float *stops; // used for chapter indicators (0.0-1.0 each)
+ int num_stops;
+};
+void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s);
+
+void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs);
+
+enum mp_osd_draw_flags {
+ OSD_DRAW_SUB_FILTER = (1 << 0),
+ OSD_DRAW_SUB_ONLY = (1 << 1),
+ OSD_DRAW_OSD_ONLY = (1 << 2),
+};
+
+void osd_draw(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT],
+ void (*cb)(void *ctx, struct sub_bitmaps *imgs), void *cb_ctx);
+
+struct sub_bitmap_list *osd_render(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT]);
+
+struct mp_image;
+void osd_draw_on_image(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags, struct mp_image *dest);
+
+struct mp_image_pool;
+void osd_draw_on_image_p(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ struct mp_image_pool *pool, struct mp_image *dest);
+
+void osd_resize(struct osd_state *osd, struct mp_osd_res res);
+
+struct mp_image_params;
+struct mp_osd_res osd_res_from_image_params(const struct mp_image_params *p);
+
+struct mp_osd_res osd_get_vo_res(struct osd_state *osd);
+
+void osd_rescale_bitmaps(struct sub_bitmaps *imgs, int frame_w, int frame_h,
+ struct mp_osd_res res, double compensate_par);
+
+struct osd_external_ass {
+ void *owner; // unique pointer (NULL is also allowed)
+ int64_t id;
+ int format;
+ char *data;
+ int res_x, res_y;
+ int z;
+ bool hidden;
+
+ double *out_rc; // hack to pass boundary rect, [x0, y0, x1, y1]
+};
+
+// defined in osd_libass.c and osd_dummy.c
+void osd_set_external(struct osd_state *osd, struct osd_external_ass *ov);
+void osd_set_external_remove_owner(struct osd_state *osd, void *owner);
+void osd_get_text_size(struct osd_state *osd, int *out_screen_h, int *out_font_h);
+void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function);
+
+#endif /* MPLAYER_SUB_H */
diff --git a/sub/osd_font.otf b/sub/osd_font.otf
new file mode 100644
index 0000000..70b9b21
--- /dev/null
+++ b/sub/osd_font.otf
Binary files differ
diff --git a/sub/osd_libass.c b/sub/osd_libass.c
new file mode 100644
index 0000000..a3b19c9
--- /dev/null
+++ b/sub/osd_libass.c
@@ -0,0 +1,691 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "osd.h"
+#include "osd_state.h"
+
+static const char osd_font_pfb[] =
+#include "sub/osd_font.otf.inc"
+;
+
+#include "sub/ass_mp.h"
+#include "options/options.h"
+
+
+#define ASS_USE_OSD_FONT "{\\fnmpv-osd-symbols}"
+
+static void append_ass(struct ass_state *ass, struct mp_osd_res *res,
+ ASS_Image **img_list, bool *changed);
+
+void osd_init_backend(struct osd_state *osd)
+{
+}
+
+static void create_ass_renderer(struct osd_state *osd, struct ass_state *ass)
+{
+ if (ass->render)
+ return;
+
+ ass->log = mp_log_new(NULL, osd->log, "libass");
+ ass->library = mp_ass_init(osd->global, osd->opts->osd_style, ass->log);
+ ass_add_font(ass->library, "mpv-osd-symbols", (void *)osd_font_pfb,
+ sizeof(osd_font_pfb) - 1);
+
+ ass->render = ass_renderer_init(ass->library);
+ if (!ass->render)
+ abort();
+
+ mp_ass_configure_fonts(ass->render, osd->opts->osd_style,
+ osd->global, ass->log);
+ ass_set_pixel_aspect(ass->render, 1.0);
+}
+
+static void destroy_ass_renderer(struct ass_state *ass)
+{
+ if (ass->track)
+ ass_free_track(ass->track);
+ ass->track = NULL;
+ if (ass->render)
+ ass_renderer_done(ass->render);
+ ass->render = NULL;
+ if (ass->library)
+ ass_library_done(ass->library);
+ ass->library = NULL;
+ talloc_free(ass->log);
+ ass->log = NULL;
+}
+
+static void destroy_external(struct osd_external *ext)
+{
+ destroy_ass_renderer(&ext->ass);
+ talloc_free(ext);
+}
+
+void osd_destroy_backend(struct osd_state *osd)
+{
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct osd_object *obj = osd->objs[n];
+ destroy_ass_renderer(&obj->ass);
+ for (int i = 0; i < obj->num_externals; i++)
+ destroy_external(obj->externals[i]);
+ obj->num_externals = 0;
+ }
+}
+
+static void update_playres(struct ass_state *ass, struct mp_osd_res *vo_res)
+{
+ ASS_Track *track = ass->track;
+ int old_res_x = track->PlayResX;
+ int old_res_y = track->PlayResY;
+
+ ass->vo_res = *vo_res;
+
+ double aspect = 1.0 * vo_res->w / MPMAX(vo_res->h, 1);
+ if (vo_res->display_par > 0)
+ aspect = aspect / vo_res->display_par;
+
+ track->PlayResY = ass->res_y ? ass->res_y : MP_ASS_FONT_PLAYRESY;
+ track->PlayResX = ass->res_x ? ass->res_x : track->PlayResY * aspect;
+
+ // Force libass to clear its internal cache - it doesn't check for
+ // PlayRes changes itself.
+ if (old_res_x != track->PlayResX || old_res_y != track->PlayResY)
+ ass_set_frame_size(ass->render, 1, 1);
+}
+
+static void create_ass_track(struct osd_state *osd, struct osd_object *obj,
+ struct ass_state *ass)
+{
+ create_ass_renderer(osd, ass);
+
+ ASS_Track *track = ass->track;
+ if (!track)
+ track = ass->track = ass_new_track(ass->library);
+
+ track->track_type = TRACK_TYPE_ASS;
+ track->Timer = 100.;
+ track->WrapStyle = 1; // end-of-line wrapping instead of smart wrapping
+ track->Kerning = true;
+ track->ScaledBorderAndShadow = true;
+#if LIBASS_VERSION >= 0x01600010
+ ass_track_set_feature(track, ASS_FEATURE_WRAP_UNICODE, 1);
+#endif
+ update_playres(ass, &obj->vo_res);
+}
+
+static int find_style(ASS_Track *track, const char *name, int def)
+{
+ for (int n = 0; n < track->n_styles; n++) {
+ if (track->styles[n].Name && strcmp(track->styles[n].Name, name) == 0)
+ return n;
+ }
+ return def;
+}
+
+// Find a given style, or add it if it's missing.
+static ASS_Style *get_style(struct ass_state *ass, char *name)
+{
+ ASS_Track *track = ass->track;
+ if (!track)
+ return NULL;
+
+ int sid = find_style(track, name, -1);
+ if (sid >= 0)
+ return &track->styles[sid];
+
+ sid = ass_alloc_style(track);
+ ASS_Style *style = &track->styles[sid];
+ style->Name = strdup(name);
+ // Set to neutral base direction, as opposed to VSFilter LTR default
+ style->Encoding = -1;
+ return style;
+}
+
+static ASS_Event *add_osd_ass_event(ASS_Track *track, const char *style,
+ const char *text)
+{
+ int n = ass_alloc_event(track);
+ ASS_Event *event = track->events + n;
+ event->Start = 0;
+ event->Duration = 100;
+ event->Style = find_style(track, style, 0);
+ event->ReadOrder = n;
+ assert(event->Text == NULL);
+ if (text)
+ event->Text = strdup(text);
+ return event;
+}
+
+static void clear_ass(struct ass_state *ass)
+{
+ if (ass->track)
+ ass_flush_events(ass->track);
+}
+
+void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function)
+{
+ // 0xFF is never valid UTF-8, so we can use it to escape OSD symbols.
+ // (Same trick as OSD_ASS_0/OSD_ASS_1.)
+ snprintf(buffer, buffer_size, "\xFF%c", osd_function);
+}
+
+static void mangle_ass(bstr *dst, const char *in)
+{
+ const char *start = in;
+ bool escape_ass = true;
+ while (*in) {
+ // As used by osd_get_function_sym().
+ if (in[0] == '\xFF' && in[1]) {
+ bstr_xappend(NULL, dst, bstr0(ASS_USE_OSD_FONT));
+ mp_append_utf8_bstr(NULL, dst, OSD_CODEPOINTS + in[1]);
+ bstr_xappend(NULL, dst, bstr0("{\\r}"));
+ in += 2;
+ continue;
+ }
+ if (*in == OSD_ASS_0[0] || *in == OSD_ASS_1[0]) {
+ escape_ass = *in == OSD_ASS_1[0];
+ in += 1;
+ continue;
+ }
+ if (escape_ass && *in == '{')
+ bstr_xappend(NULL, dst, bstr0("\\"));
+ // Libass will strip leading whitespace
+ if (in[0] == ' ' && (in == start || in[-1] == '\n')) {
+ bstr_xappend(NULL, dst, bstr0("\\h"));
+ in += 1;
+ continue;
+ }
+ bstr_xappend(NULL, dst, (bstr){(char *)in, 1});
+ // Break ASS escapes with U+2060 WORD JOINER
+ if (escape_ass && *in == '\\')
+ mp_append_utf8_bstr(NULL, dst, 0x2060);
+ in++;
+ }
+}
+
+static ASS_Event *add_osd_ass_event_escaped(ASS_Track *track, const char *style,
+ const char *text)
+{
+ bstr buf = {0};
+ mangle_ass(&buf, text);
+ ASS_Event *e = add_osd_ass_event(track, style, buf.start);
+ talloc_free(buf.start);
+ return e;
+}
+
+static ASS_Style *prepare_osd_ass(struct osd_state *osd, struct osd_object *obj)
+{
+ struct mp_osd_render_opts *opts = osd->opts;
+
+ create_ass_track(osd, obj, &obj->ass);
+
+ struct osd_style_opts font = *opts->osd_style;
+ font.font_size *= opts->osd_scale;
+
+ double playresy = obj->ass.track->PlayResY;
+ // Compensate for libass and mp_ass_set_style scaling the font etc.
+ if (!opts->osd_scale_by_window)
+ playresy *= 720.0 / obj->vo_res.h;
+
+ ASS_Style *style = get_style(&obj->ass, "OSD");
+ mp_ass_set_style(style, playresy, &font);
+ return style;
+}
+
+static void update_osd_text(struct osd_state *osd, struct osd_object *obj)
+{
+
+ if (!obj->text[0])
+ return;
+
+ prepare_osd_ass(osd, obj);
+ add_osd_ass_event_escaped(obj->ass.track, "OSD", obj->text);
+}
+
+void osd_get_text_size(struct osd_state *osd, int *out_screen_h, int *out_font_h)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_OSD];
+ ASS_Style *style = prepare_osd_ass(osd, obj);
+ *out_screen_h = obj->ass.track->PlayResY - style->MarginV;
+ *out_font_h = style->FontSize;
+ mp_mutex_unlock(&osd->lock);
+}
+
+// align: -1 .. +1
+// frame: size of the containing area
+// obj: size of the object that should be positioned inside the area
+// margin: min. distance from object to frame (as long as -1 <= align <= +1)
+static float get_align(float align, float frame, float obj, float margin)
+{
+ frame -= margin * 2;
+ return margin + frame / 2 - obj / 2 + (frame - obj) / 2 * align;
+}
+
+struct ass_draw {
+ int scale;
+ char *text;
+};
+
+static void ass_draw_start(struct ass_draw *d)
+{
+ d->scale = MPMAX(d->scale, 1);
+ d->text = talloc_asprintf_append(d->text, "{\\p%d}", d->scale);
+}
+
+static void ass_draw_stop(struct ass_draw *d)
+{
+ d->text = talloc_strdup_append(d->text, "{\\p0}");
+}
+
+static void ass_draw_c(struct ass_draw *d, float x, float y)
+{
+ int ix = round(x * (1 << (d->scale - 1)));
+ int iy = round(y * (1 << (d->scale - 1)));
+ d->text = talloc_asprintf_append(d->text, " %d %d", ix, iy);
+}
+
+static void ass_draw_append(struct ass_draw *d, const char *t)
+{
+ d->text = talloc_strdup_append(d->text, t);
+}
+
+static void ass_draw_move_to(struct ass_draw *d, float x, float y)
+{
+ ass_draw_append(d, " m");
+ ass_draw_c(d, x, y);
+}
+
+static void ass_draw_line_to(struct ass_draw *d, float x, float y)
+{
+ ass_draw_append(d, " l");
+ ass_draw_c(d, x, y);
+}
+
+static void ass_draw_rect_ccw(struct ass_draw *d, float x0, float y0,
+ float x1, float y1)
+{
+ ass_draw_move_to(d, x0, y0);
+ ass_draw_line_to(d, x0, y1);
+ ass_draw_line_to(d, x1, y1);
+ ass_draw_line_to(d, x1, y0);
+}
+
+static void ass_draw_rect_cw(struct ass_draw *d, float x0, float y0,
+ float x1, float y1)
+{
+ ass_draw_move_to(d, x0, y0);
+ ass_draw_line_to(d, x1, y0);
+ ass_draw_line_to(d, x1, y1);
+ ass_draw_line_to(d, x0, y1);
+}
+
+static void ass_draw_reset(struct ass_draw *d)
+{
+ talloc_free(d->text);
+ d->text = NULL;
+}
+
+static void get_osd_bar_box(struct osd_state *osd, struct osd_object *obj,
+ float *o_x, float *o_y, float *o_w, float *o_h,
+ float *o_border)
+{
+ struct mp_osd_render_opts *opts = osd->opts;
+
+ create_ass_track(osd, obj, &obj->ass);
+ ASS_Track *track = obj->ass.track;
+
+ ASS_Style *style = get_style(&obj->ass, "progbar");
+ if (!style) {
+ *o_x = *o_y = *o_w = *o_h = *o_border = 0;
+ return;
+ }
+
+ mp_ass_set_style(style, track->PlayResY, opts->osd_style);
+
+ if (osd->opts->osd_style->back_color.a) {
+ // override the default osd opaque-box into plain outline. Otherwise
+ // the opaque box is not aligned with the bar (even without shadow),
+ // and each bar ass event gets its own opaque box - breaking the bar.
+ style->BackColour = MP_ASS_COLOR(opts->osd_style->shadow_color);
+ style->BorderStyle = 1; // outline
+ }
+
+ *o_w = track->PlayResX * (opts->osd_bar_w / 100.0);
+ *o_h = track->PlayResY * (opts->osd_bar_h / 100.0);
+
+ float base_size = 0.03125;
+ style->Outline *= *o_h / track->PlayResY / base_size;
+ // So that the chapter marks have space between them
+ style->Outline = MPMIN(style->Outline, *o_h / 5.2);
+ // So that the border is not 0
+ style->Outline = MPMAX(style->Outline, *o_h / 32.0);
+ // Rendering with shadow is broken (because there's more than one shape)
+ style->Shadow = 0;
+
+ style->Alignment = 5;
+
+ *o_border = style->Outline;
+
+ *o_x = get_align(opts->osd_bar_align_x, track->PlayResX, *o_w, *o_border);
+ *o_y = get_align(opts->osd_bar_align_y, track->PlayResY, *o_h, *o_border);
+}
+
+static void update_progbar(struct osd_state *osd, struct osd_object *obj)
+{
+ if (obj->progbar_state.type < 0)
+ return;
+
+ float px, py, width, height, border;
+ get_osd_bar_box(osd, obj, &px, &py, &width, &height, &border);
+
+ ASS_Track *track = obj->ass.track;
+
+ float sx = px - border * 2 - height / 4; // includes additional spacing
+ float sy = py + height / 2;
+
+ bstr buf = bstr0(talloc_asprintf(NULL, "{\\an6\\pos(%f,%f)}", sx, sy));
+
+ if (obj->progbar_state.type == 0 || obj->progbar_state.type >= 256) {
+ // no sym
+ } else if (obj->progbar_state.type >= 32) {
+ mp_append_utf8_bstr(NULL, &buf, obj->progbar_state.type);
+ } else {
+ bstr_xappend(NULL, &buf, bstr0(ASS_USE_OSD_FONT));
+ mp_append_utf8_bstr(NULL, &buf, OSD_CODEPOINTS + obj->progbar_state.type);
+ bstr_xappend(NULL, &buf, bstr0("{\\r}"));
+ }
+
+ add_osd_ass_event(track, "progbar", buf.start);
+ talloc_free(buf.start);
+
+ struct ass_draw *d = &(struct ass_draw) { .scale = 4 };
+
+ if (osd->opts->osd_style->back_color.a) {
+ // the bar style always ignores the --osd-back-color config - it messes
+ // up the bar. draw an artificial box at the original back color.
+ struct m_color bc = osd->opts->osd_style->back_color;
+ d->text = talloc_asprintf_append(d->text,
+ "{\\pos(%f,%f)\\bord0\\1a&H%02X\\1c&H%02X%02X%02X&}",
+ px, py, 255 - bc.a, (int)bc.b, (int)bc.g, (int)bc.r);
+
+ ass_draw_start(d);
+ ass_draw_rect_cw(d, -border, -border, width + border, height + border);
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+ }
+
+ // filled area
+ d->text = talloc_asprintf_append(d->text, "{\\bord0\\pos(%f,%f)}", px, py);
+ ass_draw_start(d);
+ float pos = obj->progbar_state.value * width - border / 2;
+ ass_draw_rect_cw(d, 0, 0, pos, height);
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+
+ // position marker
+ d->text = talloc_asprintf_append(d->text, "{\\bord%f\\pos(%f,%f)}",
+ border / 2, px, py);
+ ass_draw_start(d);
+ ass_draw_move_to(d, pos + border / 2, 0);
+ ass_draw_line_to(d, pos + border / 2, height);
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+
+ d->text = talloc_asprintf_append(d->text, "{\\pos(%f,%f)}", px, py);
+ ass_draw_start(d);
+
+ // the box
+ ass_draw_rect_cw(d, -border, -border, width + border, height + border);
+
+ // the "hole"
+ ass_draw_rect_ccw(d, 0, 0, width, height);
+
+ // chapter marks
+ for (int n = 0; n < obj->progbar_state.num_stops; n++) {
+ float s = obj->progbar_state.stops[n] * width;
+ float dent = border * 1.3;
+
+ if (s > dent && s < width - dent) {
+ ass_draw_move_to(d, s + dent, 0);
+ ass_draw_line_to(d, s, dent);
+ ass_draw_line_to(d, s - dent, 0);
+
+ ass_draw_move_to(d, s - dent, height);
+ ass_draw_line_to(d, s, height - dent);
+ ass_draw_line_to(d, s + dent, height);
+ }
+ }
+
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+}
+
+static void update_osd(struct osd_state *osd, struct osd_object *obj)
+{
+ obj->osd_changed = false;
+ clear_ass(&obj->ass);
+ update_osd_text(osd, obj);
+ update_progbar(osd, obj);
+}
+
+static void update_external(struct osd_state *osd, struct osd_object *obj,
+ struct osd_external *ext)
+{
+ bstr t = bstr0(ext->ov.data);
+ ext->ass.res_x = ext->ov.res_x;
+ ext->ass.res_y = ext->ov.res_y;
+ create_ass_track(osd, obj, &ext->ass);
+
+ clear_ass(&ext->ass);
+
+ int resy = ext->ass.track->PlayResY;
+ mp_ass_set_style(get_style(&ext->ass, "OSD"), resy, osd->opts->osd_style);
+
+ // Some scripts will reference this style name with \r tags.
+ const struct osd_style_opts *def = osd_style_conf.defaults;
+ mp_ass_set_style(get_style(&ext->ass, "Default"), resy, def);
+
+ while (t.len) {
+ bstr line;
+ bstr_split_tok(t, "\n", &line, &t);
+ if (line.len) {
+ char *tmp = bstrdup0(NULL, line);
+ add_osd_ass_event(ext->ass.track, "OSD", tmp);
+ talloc_free(tmp);
+ }
+ }
+}
+
+static int cmp_zorder(const void *pa, const void *pb)
+{
+ const struct osd_external *a = *(struct osd_external **)pa;
+ const struct osd_external *b = *(struct osd_external **)pb;
+ return a->ov.z == b->ov.z ? 0 : (a->ov.z > b->ov.z ? 1 : -1);
+}
+
+void osd_set_external(struct osd_state *osd, struct osd_external_ass *ov)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL];
+ bool zorder_changed = false;
+ int index = -1;
+
+ for (int n = 0; n < obj->num_externals; n++) {
+ struct osd_external *e = obj->externals[n];
+ if (e->ov.id == ov->id && e->ov.owner == ov->owner) {
+ index = n;
+ break;
+ }
+ }
+
+ if (index < 0) {
+ if (!ov->format)
+ goto done;
+ struct osd_external *new = talloc_zero(NULL, struct osd_external);
+ new->ov.owner = ov->owner;
+ new->ov.id = ov->id;
+ MP_TARRAY_APPEND(obj, obj->externals, obj->num_externals, new);
+ index = obj->num_externals - 1;
+ zorder_changed = true;
+ }
+
+ struct osd_external *entry = obj->externals[index];
+
+ if (!ov->format) {
+ if (!entry->ov.hidden) {
+ obj->changed = true;
+ osd->want_redraw_notification = true;
+ }
+ destroy_external(entry);
+ MP_TARRAY_REMOVE_AT(obj->externals, obj->num_externals, index);
+ goto done;
+ }
+
+ if (!entry->ov.hidden || !ov->hidden) {
+ obj->changed = true;
+ osd->want_redraw_notification = true;
+ }
+
+ entry->ov.format = ov->format;
+ if (!entry->ov.data)
+ entry->ov.data = talloc_strdup(entry, "");
+ entry->ov.data[0] = '\0'; // reuse memory allocation
+ entry->ov.data = talloc_strdup_append(entry->ov.data, ov->data);
+ entry->ov.res_x = ov->res_x;
+ entry->ov.res_y = ov->res_y;
+ zorder_changed |= entry->ov.z != ov->z;
+ entry->ov.z = ov->z;
+ entry->ov.hidden = ov->hidden;
+
+ update_external(osd, obj, entry);
+
+ if (zorder_changed) {
+ qsort(obj->externals, obj->num_externals, sizeof(obj->externals[0]),
+ cmp_zorder);
+ }
+
+ if (ov->out_rc) {
+ struct mp_osd_res vo_res = entry->ass.vo_res;
+ // Defined fallback if VO has not drawn this yet
+ if (vo_res.w < 1 || vo_res.h < 1) {
+ vo_res = (struct mp_osd_res){
+ .w = entry->ov.res_x,
+ .h = entry->ov.res_y,
+ .display_par = 1,
+ };
+ // According to osd-overlay command description.
+ if (vo_res.w < 1)
+ vo_res.w = 1280;
+ if (vo_res.h < 1)
+ vo_res.h = 720;
+ }
+
+ ASS_Image *img_list = NULL;
+ append_ass(&entry->ass, &vo_res, &img_list, NULL);
+
+ mp_ass_get_bb(img_list, entry->ass.track, &vo_res, ov->out_rc);
+ }
+
+done:
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_external_remove_owner(struct osd_state *osd, void *owner)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL];
+ for (int n = obj->num_externals - 1; n >= 0; n--) {
+ struct osd_external *e = obj->externals[n];
+ if (e->ov.owner == owner) {
+ destroy_external(e);
+ MP_TARRAY_REMOVE_AT(obj->externals, obj->num_externals, n);
+ obj->changed = true;
+ osd->want_redraw_notification = true;
+ }
+ }
+ mp_mutex_unlock(&osd->lock);
+}
+
+static void append_ass(struct ass_state *ass, struct mp_osd_res *res,
+ ASS_Image **img_list, bool *changed)
+{
+ if (!ass->render || !ass->track) {
+ *img_list = NULL;
+ return;
+ }
+
+ update_playres(ass, res);
+
+ ass_set_frame_size(ass->render, res->w, res->h);
+ ass_set_pixel_aspect(ass->render, res->display_par);
+
+ int ass_changed;
+ *img_list = ass_render_frame(ass->render, ass->track, 0, &ass_changed);
+
+ ass->changed |= ass_changed;
+
+ if (changed) {
+ *changed |= ass->changed;
+ ass->changed = false;
+ }
+}
+
+struct sub_bitmaps *osd_object_get_bitmaps(struct osd_state *osd,
+ struct osd_object *obj, int format)
+{
+ if (obj->type == OSDTYPE_OSD && obj->osd_changed)
+ update_osd(osd, obj);
+
+ if (!obj->ass_packer)
+ obj->ass_packer = mp_ass_packer_alloc(obj);
+
+ MP_TARRAY_GROW(obj, obj->ass_imgs, obj->num_externals + 1);
+
+ append_ass(&obj->ass, &obj->vo_res, &obj->ass_imgs[0], &obj->changed);
+ for (int n = 0; n < obj->num_externals; n++) {
+ if (obj->externals[n]->ov.hidden) {
+ update_playres(&obj->externals[n]->ass, &obj->vo_res);
+ obj->ass_imgs[n + 1] = NULL;
+ } else {
+ append_ass(&obj->externals[n]->ass, &obj->vo_res,
+ &obj->ass_imgs[n + 1], &obj->changed);
+ }
+ }
+
+ struct sub_bitmaps out_imgs = {0};
+ mp_ass_packer_pack(obj->ass_packer, obj->ass_imgs, obj->num_externals + 1,
+ obj->changed, format, &out_imgs);
+
+ obj->changed = false;
+
+ return sub_bitmaps_copy(&obj->copy_cache, &out_imgs);
+}
diff --git a/sub/osd_state.h b/sub/osd_state.h
new file mode 100644
index 0000000..9bb48f8
--- /dev/null
+++ b/sub/osd_state.h
@@ -0,0 +1,94 @@
+#ifndef MP_OSD_STATE_H_
+#define MP_OSD_STATE_H_
+
+#include <stdatomic.h>
+
+#include "osd.h"
+#include "osdep/threads.h"
+
+enum mp_osdtype {
+ OSDTYPE_SUB,
+ OSDTYPE_SUB2, // IDs must be numerically successive
+
+ OSDTYPE_OSD,
+
+ OSDTYPE_EXTERNAL,
+ OSDTYPE_EXTERNAL2,
+
+ OSDTYPE_COUNT
+};
+
+struct ass_state {
+ struct mp_log *log;
+ struct ass_track *track;
+ struct ass_renderer *render;
+ struct ass_library *library;
+ int res_x, res_y;
+ bool changed;
+ struct mp_osd_res vo_res; // last known value
+};
+
+struct osd_object {
+ int type; // OSDTYPE_*
+ bool is_sub;
+
+ // OSDTYPE_OSD
+ bool osd_changed;
+ char *text;
+ struct osd_progbar_state progbar_state;
+
+ // OSDTYPE_SUB/OSDTYPE_SUB2
+ struct dec_sub *sub;
+
+ // OSDTYPE_EXTERNAL
+ struct osd_external **externals;
+ int num_externals;
+
+ // OSDTYPE_EXTERNAL2
+ struct sub_bitmaps *external2;
+
+ // VO cache state
+ int vo_change_id;
+ struct mp_osd_res vo_res;
+ bool vo_had_output;
+
+ // Internally used by osd_libass.c
+ bool changed;
+ struct ass_state ass;
+ struct mp_ass_packer *ass_packer;
+ struct sub_bitmap_copy_cache *copy_cache;
+ struct ass_image **ass_imgs;
+};
+
+struct osd_external {
+ struct osd_external_ass ov;
+ struct ass_state ass;
+};
+
+struct osd_state {
+ mp_mutex lock;
+
+ struct osd_object *objs[MAX_OSD_PARTS];
+
+ bool render_subs_in_filter;
+ _Atomic double force_video_pts;
+
+ bool want_redraw;
+ bool want_redraw_notification;
+
+ struct m_config_cache *opts_cache;
+ struct mp_osd_render_opts *opts;
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct stats_ctx *stats;
+
+ struct mp_draw_sub_cache *draw_cache;
+};
+
+// defined in osd_libass.c
+struct sub_bitmaps *osd_object_get_bitmaps(struct osd_state *osd,
+ struct osd_object *obj, int format);
+void osd_init_backend(struct osd_state *osd);
+void osd_destroy_backend(struct osd_state *osd);
+
+#endif
diff --git a/sub/sd.h b/sub/sd.h
new file mode 100644
index 0000000..11a90fe
--- /dev/null
+++ b/sub/sd.h
@@ -0,0 +1,111 @@
+#ifndef MPLAYER_SD_H
+#define MPLAYER_SD_H
+
+#include "dec_sub.h"
+#include "demux/packet.h"
+#include "misc/bstr.h"
+
+// up to 210 ms overlaps or gaps are removed
+#define SUB_GAP_THRESHOLD 0.210
+// don't change timings if durations are smaller
+#define SUB_GAP_KEEP 0.4
+// slight offset when sub seeking or sub stepping
+#define SUB_SEEK_OFFSET 0.01
+
+struct sd {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct mp_subtitle_opts *opts;
+
+ const struct sd_functions *driver;
+ void *priv;
+
+ struct attachment_list *attachments;
+ struct mp_codec_params *codec;
+
+ // Set to false as soon as the decoder discards old subtitle events.
+ // (only needed if sd_functions.accept_packets_in_advance == false)
+ bool preload_ok;
+};
+
+struct sd_functions {
+ const char *name;
+ bool accept_packets_in_advance;
+ int (*init)(struct sd *sd);
+ void (*decode)(struct sd *sd, struct demux_packet *packet);
+ void (*reset)(struct sd *sd);
+ void (*select)(struct sd *sd, bool selected);
+ void (*uninit)(struct sd *sd);
+
+ bool (*accepts_packet)(struct sd *sd, double pts); // implicit default if NULL: true
+ int (*control)(struct sd *sd, enum sd_ctrl cmd, void *arg);
+
+ struct sub_bitmaps *(*get_bitmaps)(struct sd *sd, struct mp_osd_res dim,
+ int format, double pts);
+ char *(*get_text)(struct sd *sd, double pts, enum sd_text_type type);
+ struct sd_times (*get_times)(struct sd *sd, double pts);
+};
+
+// lavc_conv.c
+struct lavc_conv;
+struct lavc_conv *lavc_conv_create(struct mp_log *log,
+ const struct mp_codec_params *mp_codec);
+char *lavc_conv_get_extradata(struct lavc_conv *priv);
+char **lavc_conv_decode(struct lavc_conv *priv, struct demux_packet *packet,
+ double *sub_pts, double *sub_duration);
+void lavc_conv_reset(struct lavc_conv *priv);
+void lavc_conv_uninit(struct lavc_conv *priv);
+
+struct sd_filter {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct mp_sub_filter_opts *opts;
+ const struct sd_filter_functions *driver;
+
+ void *priv;
+
+ // Static codec parameters. Set by sd; cannot be changed by filter.
+ char *codec;
+ char *event_format;
+};
+
+struct sd_filter_functions {
+ bool (*init)(struct sd_filter *ft);
+
+ // Filter an ASS event (usually in the Matroska format, but event_format
+ // can be used to determine details).
+ // Returning NULL is interpreted as dropping the event completely.
+ // Returning pkt makes it no-op.
+ // If the returned packet is not pkt or NULL, it must have been properly
+ // allocated.
+ // pkt is owned by the caller (and freed by the caller when needed).
+ // Note: as by normal demux_packet rules, you must not modify any fields in
+ // it, or the data referenced by it. You must create a new demux_packet
+ // when modifying data.
+ struct demux_packet *(*filter)(struct sd_filter *ft,
+ struct demux_packet *pkt);
+
+ void (*uninit)(struct sd_filter *ft);
+};
+
+extern const struct sd_filter_functions sd_filter_sdh;
+extern const struct sd_filter_functions sd_filter_regex;
+extern const struct sd_filter_functions sd_filter_jsre;
+
+
+// convenience utils for filters with ass codec
+
+// num commas to skip at an ass-event before the "Text" field (always last)
+// (doesn't change, can be retrieved once on filter init)
+int sd_ass_fmt_offset(const char *event_format);
+
+// the event (pkt->buffer) "Text" content according to the calculated offset.
+// on malformed event: warns and returns (bstr){NULL,0}
+bstr sd_ass_pkt_text(struct sd_filter *ft, struct demux_packet *pkt, int offset);
+
+// convert \0-terminated "Text" (ass) content to plaintext, possibly in-place.
+// result.start is out, result.len is MIN(out_siz, strlen(in)) or smaller.
+// if there's room: out[result.len] is set to \0. out == in is allowed.
+bstr sd_ass_to_plaintext(char *out, size_t out_siz, const char *in);
+
+#endif
diff --git a/sub/sd_ass.c b/sub/sd_ass.c
new file mode 100644
index 0000000..6742f6f
--- /dev/null
+++ b/sub/sd_ass.c
@@ -0,0 +1,1035 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+#include <math.h>
+#include <limits.h>
+
+#include <libavutil/common.h>
+#include <ass/ass.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "demux/demux.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "dec_sub.h"
+#include "ass_mp.h"
+#include "sd.h"
+
+struct sd_ass_priv {
+ struct ass_library *ass_library;
+ struct ass_renderer *ass_renderer;
+ struct ass_track *ass_track;
+ struct ass_track *shadow_track; // for --sub-ass=no rendering
+ bool ass_configured;
+ bool is_converted;
+ struct lavc_conv *converter;
+ struct sd_filter **filters;
+ int num_filters;
+ bool clear_once;
+ bool on_top;
+ struct mp_ass_packer *packer;
+ struct sub_bitmap_copy_cache *copy_cache;
+ char last_text[500];
+ struct mp_image_params video_params;
+ struct mp_image_params last_params;
+ struct mp_osd_res osd;
+ int64_t *seen_packets;
+ int num_seen_packets;
+ bool duration_unknown;
+};
+
+static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts);
+static void fill_plaintext(struct sd *sd, double pts);
+
+static const struct sd_filter_functions *const filters[] = {
+ // Note: list order defines filter order.
+ &sd_filter_sdh,
+#if HAVE_POSIX
+ &sd_filter_regex,
+#endif
+#if HAVE_JAVASCRIPT
+ &sd_filter_jsre,
+#endif
+ NULL,
+};
+
+// Add default styles, if the track does not have any styles yet.
+// Apply style overrides if the user provides any.
+static void mp_ass_add_default_styles(ASS_Track *track, struct mp_subtitle_opts *opts)
+{
+ if (opts->ass_styles_file && opts->ass_style_override)
+ ass_read_styles(track, opts->ass_styles_file, NULL);
+
+ if (track->n_styles == 0) {
+ if (!track->PlayResY) {
+ track->PlayResX = MP_ASS_FONT_PLAYRESX;
+ track->PlayResY = MP_ASS_FONT_PLAYRESY;
+ }
+ track->Kerning = true;
+ int sid = ass_alloc_style(track);
+ track->default_style = sid;
+ ASS_Style *style = track->styles + sid;
+ style->Name = strdup("Default");
+ mp_ass_set_style(style, track->PlayResY, opts->sub_style);
+ }
+
+ if (opts->ass_style_override)
+ ass_process_force_style(track);
+}
+
+static const char *const font_mimetypes[] = {
+ "application/x-truetype-font",
+ "application/vnd.ms-opentype",
+ "application/x-font-ttf",
+ "application/x-font", // probably incorrect
+ "application/font-sfnt",
+ "font/collection",
+ "font/otf",
+ "font/sfnt",
+ "font/ttf",
+ NULL
+};
+
+static const char *const font_exts[] = {".ttf", ".ttc", ".otf", ".otc", NULL};
+
+static bool attachment_is_font(struct mp_log *log, struct demux_attachment *f)
+{
+ if (!f->name || !f->type || !f->data || !f->data_size)
+ return false;
+ for (int n = 0; font_mimetypes[n]; n++) {
+ if (strcmp(font_mimetypes[n], f->type) == 0)
+ return true;
+ }
+ // fallback: match against file extension
+ char *ext = strlen(f->name) > 4 ? f->name + strlen(f->name) - 4 : "";
+ for (int n = 0; font_exts[n]; n++) {
+ if (strcasecmp(ext, font_exts[n]) == 0) {
+ mp_warn(log, "Loading font attachment '%s' with MIME type %s. "
+ "Assuming this is a broken Matroska file, which was "
+ "muxed without setting a correct font MIME type.\n",
+ f->name, f->type);
+ return true;
+ }
+ }
+ return false;
+}
+
+static void add_subtitle_fonts(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+ if (!opts->ass_enabled || !opts->use_embedded_fonts || !sd->attachments)
+ return;
+ for (int i = 0; i < sd->attachments->num_entries; i++) {
+ struct demux_attachment *f = &sd->attachments->entries[i];
+ if (attachment_is_font(sd->log, f))
+ ass_add_font(ctx->ass_library, f->name, f->data, f->data_size);
+ }
+}
+
+static void filters_destroy(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ for (int n = 0; n < ctx->num_filters; n++) {
+ struct sd_filter *ft = ctx->filters[n];
+ if (ft->driver->uninit)
+ ft->driver->uninit(ft);
+ talloc_free(ft);
+ }
+ ctx->num_filters = 0;
+}
+
+static void filters_init(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ filters_destroy(sd);
+
+ for (int n = 0; filters[n]; n++) {
+ struct sd_filter *ft = talloc_ptrtype(ctx, ft);
+ *ft = (struct sd_filter){
+ .global = sd->global,
+ .log = sd->log,
+ .opts = mp_get_config_group(ft, sd->global, &mp_sub_filter_opts),
+ .driver = filters[n],
+ .codec = "ass",
+ .event_format = ctx->ass_track->event_format,
+ };
+ if (ft->driver->init(ft)) {
+ MP_TARRAY_APPEND(ctx, ctx->filters, ctx->num_filters, ft);
+ } else {
+ talloc_free(ft);
+ }
+ }
+}
+
+static void enable_output(struct sd *sd, bool enable)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ if (enable == !!ctx->ass_renderer)
+ return;
+ if (ctx->ass_renderer) {
+ ass_renderer_done(ctx->ass_renderer);
+ ctx->ass_renderer = NULL;
+ } else {
+ ctx->ass_renderer = ass_renderer_init(ctx->ass_library);
+
+ mp_ass_configure_fonts(ctx->ass_renderer, sd->opts->sub_style,
+ sd->global, sd->log);
+ }
+}
+
+static void assobjects_init(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+
+ ctx->ass_library = mp_ass_init(sd->global, sd->opts->sub_style, sd->log);
+ ass_set_extract_fonts(ctx->ass_library, opts->use_embedded_fonts);
+
+ add_subtitle_fonts(sd);
+
+ if (opts->ass_style_override)
+ ass_set_style_overrides(ctx->ass_library, opts->ass_style_override_list);
+
+ ctx->ass_track = ass_new_track(ctx->ass_library);
+ ctx->ass_track->track_type = TRACK_TYPE_ASS;
+
+ ctx->shadow_track = ass_new_track(ctx->ass_library);
+ ctx->shadow_track->PlayResX = MP_ASS_FONT_PLAYRESX;
+ ctx->shadow_track->PlayResY = MP_ASS_FONT_PLAYRESY;
+ mp_ass_add_default_styles(ctx->shadow_track, opts);
+
+ char *extradata = sd->codec->extradata;
+ int extradata_size = sd->codec->extradata_size;
+ if (ctx->converter) {
+ extradata = lavc_conv_get_extradata(ctx->converter);
+ extradata_size = extradata ? strlen(extradata) : 0;
+ }
+ if (extradata)
+ ass_process_codec_private(ctx->ass_track, extradata, extradata_size);
+
+ mp_ass_add_default_styles(ctx->ass_track, opts);
+
+#if LIBASS_VERSION >= 0x01302000
+ ass_set_check_readorder(ctx->ass_track, sd->opts->sub_clear_on_seek ? 0 : 1);
+#endif
+
+ enable_output(sd, true);
+}
+
+static void assobjects_destroy(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ ass_free_track(ctx->ass_track);
+ ass_free_track(ctx->shadow_track);
+ enable_output(sd, false);
+ ass_library_done(ctx->ass_library);
+}
+
+static int init(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = talloc_zero(sd, struct sd_ass_priv);
+ sd->priv = ctx;
+
+ // Note: accept "null" as alias for "ass", so EDL delay_open subtitle
+ // streams work.
+ if (strcmp(sd->codec->codec, "ass") != 0 &&
+ strcmp(sd->codec->codec, "null") != 0)
+ {
+ ctx->is_converted = true;
+ ctx->converter = lavc_conv_create(sd->log, sd->codec);
+ if (!ctx->converter)
+ return -1;
+
+ if (strcmp(sd->codec->codec, "eia_608") == 0)
+ ctx->duration_unknown = 1;
+ }
+
+ assobjects_init(sd);
+ filters_init(sd);
+
+ ctx->packer = mp_ass_packer_alloc(ctx);
+
+ return 0;
+}
+
+// Note: pkt is not necessarily a fully valid refcounted packet.
+static void filter_and_add(struct sd *sd, struct demux_packet *pkt)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct demux_packet *orig_pkt = pkt;
+
+ for (int n = 0; n < ctx->num_filters; n++) {
+ struct sd_filter *ft = ctx->filters[n];
+ struct demux_packet *npkt = ft->driver->filter(ft, pkt);
+ if (pkt != npkt && pkt != orig_pkt)
+ talloc_free(pkt);
+ pkt = npkt;
+ if (!pkt)
+ return;
+ }
+
+ ass_process_chunk(ctx->ass_track, pkt->buffer, pkt->len,
+ llrint(pkt->pts * 1000),
+ llrint(pkt->duration * 1000));
+
+ if (pkt != orig_pkt)
+ talloc_free(pkt);
+}
+
+// Test if the packet with the given file position (used as unique ID) was
+// already consumed. Return false if the packet is new (and add it to the
+// internal list), and return true if it was already seen.
+static bool check_packet_seen(struct sd *sd, int64_t pos)
+{
+ struct sd_ass_priv *priv = sd->priv;
+ int a = 0;
+ int b = priv->num_seen_packets;
+ while (a < b) {
+ int mid = a + (b - a) / 2;
+ int64_t val = priv->seen_packets[mid];
+ if (pos == val)
+ return true;
+ if (pos > val) {
+ a = mid + 1;
+ } else {
+ b = mid;
+ }
+ }
+ MP_TARRAY_INSERT_AT(priv, priv->seen_packets, priv->num_seen_packets, a, pos);
+ return false;
+}
+
+#define UNKNOWN_DURATION (INT_MAX / 1000)
+
+static void decode(struct sd *sd, struct demux_packet *packet)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->ass_track;
+ if (ctx->converter) {
+ if (!sd->opts->sub_clear_on_seek && packet->pos >= 0 &&
+ check_packet_seen(sd, packet->pos))
+ return;
+
+ double sub_pts = 0;
+ double sub_duration = 0;
+ char **r = lavc_conv_decode(ctx->converter, packet, &sub_pts,
+ &sub_duration);
+ if (sd->opts->sub_stretch_durations ||
+ packet->duration < 0 || sub_duration == UINT32_MAX) {
+ if (!ctx->duration_unknown) {
+ MP_WARN(sd, "Subtitle with unknown duration.\n");
+ ctx->duration_unknown = true;
+ }
+ sub_duration = UNKNOWN_DURATION;
+ }
+
+ for (int n = 0; r && r[n]; n++) {
+ struct demux_packet pkt2 = {
+ .pts = sub_pts,
+ .duration = sub_duration,
+ .buffer = r[n],
+ .len = strlen(r[n]),
+ };
+ filter_and_add(sd, &pkt2);
+ }
+ if (ctx->duration_unknown) {
+ for (int n = track->n_events - 2; n >= 0; n--) {
+ if (track->events[n].Duration == UNKNOWN_DURATION * 1000) {
+ if (track->events[n].Start != track->events[n + 1].Start) {
+ track->events[n].Duration = track->events[n + 1].Start -
+ track->events[n].Start;
+ } else {
+ track->events[n].Duration = track->events[n + 1].Duration;
+ }
+ }
+ }
+ }
+ } else {
+ // Note that for this packet format, libass has an internal mechanism
+ // for discarding duplicate (already seen) packets.
+ filter_and_add(sd, packet);
+ }
+}
+
+static void configure_ass(struct sd *sd, struct mp_osd_res *dim,
+ bool converted, ASS_Track *track)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Renderer *priv = ctx->ass_renderer;
+
+ ass_set_frame_size(priv, dim->w, dim->h);
+ ass_set_margins(priv, dim->mt, dim->mb, dim->ml, dim->mr);
+
+ bool set_use_margins = false;
+ float set_sub_pos = 0.0f;
+ float set_line_spacing = 0;
+ float set_font_scale = 1;
+ int set_hinting = 0;
+ bool set_scale_with_window = false;
+ bool set_scale_by_window = true;
+ bool total_override = false;
+ // With forced overrides, apply the --sub-* specific options
+ if (converted || opts->ass_style_override == 3) { // 'force'
+ set_scale_with_window = opts->sub_scale_with_window;
+ set_use_margins = opts->sub_use_margins;
+ set_scale_by_window = opts->sub_scale_by_window;
+ total_override = true;
+ } else {
+ set_scale_with_window = opts->ass_scale_with_window;
+ set_use_margins = opts->ass_use_margins;
+ }
+ if (converted || opts->ass_style_override) {
+ set_sub_pos = 100.0f - opts->sub_pos;
+ set_line_spacing = opts->ass_line_spacing;
+ set_hinting = opts->ass_hinting;
+ set_font_scale = opts->sub_scale;
+ }
+ if (set_scale_with_window) {
+ int vidh = dim->h - (dim->mt + dim->mb);
+ set_font_scale *= dim->h / (float)MPMAX(vidh, 1);
+ }
+ if (!set_scale_by_window) {
+ double factor = dim->h / 720.0;
+ if (factor != 0.0)
+ set_font_scale /= factor;
+ }
+ ass_set_use_margins(priv, set_use_margins);
+ ass_set_line_position(priv, set_sub_pos);
+ ass_set_shaper(priv, opts->ass_shaper);
+ int set_force_flags = 0;
+ if (total_override)
+ set_force_flags |= ASS_OVERRIDE_BIT_STYLE | ASS_OVERRIDE_BIT_SELECTIVE_FONT_SCALE;
+ if (opts->ass_style_override == 4) // 'scale'
+ set_force_flags |= ASS_OVERRIDE_BIT_SELECTIVE_FONT_SCALE;
+ if (converted)
+ set_force_flags |= ASS_OVERRIDE_BIT_ALIGNMENT;
+#ifdef ASS_JUSTIFY_AUTO
+ if ((converted || opts->ass_style_override) && opts->ass_justify)
+ set_force_flags |= ASS_OVERRIDE_BIT_JUSTIFY;
+#endif
+ ass_set_selective_style_override_enabled(priv, set_force_flags);
+ ASS_Style style = {0};
+ mp_ass_set_style(&style, MP_ASS_FONT_PLAYRESY, opts->sub_style);
+ ass_set_selective_style_override(priv, &style);
+ free(style.FontName);
+ if (converted && track->default_style < track->n_styles) {
+ mp_ass_set_style(track->styles + track->default_style,
+ track->PlayResY, opts->sub_style);
+ }
+ ass_set_font_scale(priv, set_font_scale);
+ ass_set_hinting(priv, set_hinting);
+ ass_set_line_spacing(priv, set_line_spacing);
+#if LIBASS_VERSION >= 0x01600010
+ if (converted)
+ ass_track_set_feature(track, ASS_FEATURE_WRAP_UNICODE, 1);
+#endif
+ if (converted) {
+ bool override_playres = true;
+ char **ass_style_override_list = opts->ass_style_override_list;
+ for (int i = 0; ass_style_override_list && ass_style_override_list[i]; i++) {
+ if (bstr_find0(bstr0(ass_style_override_list[i]), "PlayResX") >= 0)
+ override_playres = false;
+ }
+
+ // srt to ass conversion from ffmpeg has fixed PlayResX of 384 with an
+ // aspect of 4:3. Starting with libass f08f8ea5 (pre 0.17) PlayResX
+ // affects shadow and border widths, among others, so to render borders
+ // and shadows correctly, we adjust PlayResX according to the DAR.
+ // But PlayResX also affects margins, so we adjust those too.
+ // This should ensure basic srt-to-ass ffmpeg conversion has correct
+ // borders, but there could be other issues with some srt extensions
+ // and/or different source formats which would be exposed over time.
+ // Make these adjustments only if the user didn't set PlayResX.
+ if (override_playres) {
+ int vidw = dim->w - (dim->ml + dim->mr);
+ int vidh = dim->h - (dim->mt + dim->mb);
+ track->PlayResX = track->PlayResY * (double)vidw / MPMAX(vidh, 1);
+ // ffmpeg and mpv use a default PlayResX of 384 when it is not known,
+ // this comes from VSFilter.
+ double fix_margins = track->PlayResX / (double)MP_ASS_FONT_PLAYRESX;
+ track->styles->MarginL = round(track->styles->MarginL * fix_margins);
+ track->styles->MarginR = round(track->styles->MarginR * fix_margins);
+ }
+ }
+}
+
+static bool has_overrides(char *s)
+{
+ if (!s)
+ return false;
+ return strstr(s, "\\pos") || strstr(s, "\\move") || strstr(s, "\\clip") ||
+ strstr(s, "\\iclip") || strstr(s, "\\org") || strstr(s, "\\p");
+}
+
+#define END(ev) ((ev)->Start + (ev)->Duration)
+
+static long long find_timestamp(struct sd *sd, double pts)
+{
+ struct sd_ass_priv *priv = sd->priv;
+ if (pts == MP_NOPTS_VALUE)
+ return 0;
+
+ long long ts = llrint(pts * 1000);
+
+ if (!sd->opts->sub_fix_timing || sd->opts->ass_style_override == 0)
+ return ts;
+
+ // Try to fix small gaps and overlaps.
+ ASS_Track *track = priv->ass_track;
+ int threshold = SUB_GAP_THRESHOLD * 1000;
+ int keep = SUB_GAP_KEEP * 1000;
+
+ // Find the "current" event.
+ ASS_Event *ev[2] = {0};
+ int n_ev = 0;
+ for (int n = 0; n < track->n_events; n++) {
+ ASS_Event *event = &track->events[n];
+ if (ts >= event->Start - threshold && ts <= END(event) + threshold) {
+ if (n_ev >= MP_ARRAY_SIZE(ev))
+ return ts; // multiple overlaps - give up (probably complex subs)
+ ev[n_ev++] = event;
+ }
+ }
+
+ if (n_ev != 2)
+ return ts;
+
+ // Simple/minor heuristic against destroying typesetting.
+ if (ev[0]->Style != ev[1]->Style || has_overrides(ev[0]->Text) ||
+ has_overrides(ev[1]->Text))
+ return ts;
+
+ // Sort by start timestamps.
+ if (ev[0]->Start > ev[1]->Start)
+ MPSWAP(ASS_Event*, ev[0], ev[1]);
+
+ // We want to fix partial overlaps only.
+ if (END(ev[0]) >= END(ev[1]))
+ return ts;
+
+ if (ev[0]->Duration < keep || ev[1]->Duration < keep)
+ return ts;
+
+ // Gap between the events -> move ts to show the end of the first event.
+ if (ts >= END(ev[0]) && ts < ev[1]->Start && END(ev[0]) < ev[1]->Start &&
+ END(ev[0]) + threshold >= ev[1]->Start)
+ return END(ev[0]) - 1;
+
+ // Overlap -> move ts to the (exclusive) end of the first event.
+ // Relies on the fact that the ASS_Renderer has no overlap registered, even
+ // if there is one. This happens to work because we never render the
+ // overlapped state, and libass never resolves a collision.
+ if (ts >= ev[1]->Start && ts <= END(ev[0]) && END(ev[0]) > ev[1]->Start &&
+ END(ev[0]) <= ev[1]->Start + threshold)
+ return END(ev[0]);
+
+ return ts;
+}
+
+#undef END
+
+static struct sub_bitmaps *get_bitmaps(struct sd *sd, struct mp_osd_res dim,
+ int format, double pts)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+ bool no_ass = !opts->ass_enabled || ctx->on_top ||
+ opts->ass_style_override == 5;
+ bool converted = ctx->is_converted || no_ass;
+ ASS_Track *track = no_ass ? ctx->shadow_track : ctx->ass_track;
+ ASS_Renderer *renderer = ctx->ass_renderer;
+ struct sub_bitmaps *res = &(struct sub_bitmaps){0};
+
+ // Always update the osd_res
+ struct mp_osd_res old_osd = ctx->osd;
+ ctx->osd = dim;
+
+ if (pts == MP_NOPTS_VALUE || !renderer)
+ goto done;
+
+ // Currently no supported text sub formats support a distinction between forced
+ // and unforced lines, so we just assume everything's unforced and discard everything.
+ // If we ever see a format that makes this distinction, we can add support here.
+ if (opts->sub_forced_events_only)
+ goto done;
+
+ double scale = dim.display_par;
+ if (!converted && (!opts->ass_style_override ||
+ opts->ass_vsfilter_aspect_compat))
+ {
+ // Let's use the original video PAR for vsfilter compatibility:
+ double par = ctx->video_params.p_w / (double)ctx->video_params.p_h;
+ if (isnormal(par))
+ scale *= par;
+ }
+ if (!ctx->ass_configured || !osd_res_equals(old_osd, ctx->osd)) {
+ configure_ass(sd, &dim, converted, track);
+ ctx->ass_configured = true;
+ }
+ ass_set_pixel_aspect(renderer, scale);
+ if (!converted && (!opts->ass_style_override ||
+ opts->ass_vsfilter_blur_compat))
+ {
+ ass_set_storage_size(renderer, ctx->video_params.w, ctx->video_params.h);
+ } else {
+ ass_set_storage_size(renderer, 0, 0);
+ }
+ long long ts = find_timestamp(sd, pts);
+ if (ctx->duration_unknown && pts != MP_NOPTS_VALUE) {
+ mp_ass_flush_old_events(track, ts);
+ ctx->num_seen_packets = 0;
+ sd->preload_ok = false;
+ }
+
+ if (no_ass)
+ fill_plaintext(sd, pts);
+
+ int changed;
+ ASS_Image *imgs = ass_render_frame(renderer, track, ts, &changed);
+ mp_ass_packer_pack(ctx->packer, &imgs, 1, changed, format, res);
+
+done:
+ // mangle_colors() modifies the color field, so copy the thing _before_.
+ res = sub_bitmaps_copy(&ctx->copy_cache, res);
+
+ if (!converted && res)
+ mangle_colors(sd, res);
+
+ return res;
+}
+
+struct buf {
+ char *start;
+ int size;
+ int len;
+};
+
+static void append(struct buf *b, char c)
+{
+ if (b->len < b->size) {
+ b->start[b->len] = c;
+ b->len++;
+ }
+}
+
+static void ass_to_plaintext(struct buf *b, const char *in)
+{
+ bool in_tag = false;
+ const char *open_tag_pos = NULL;
+ bool in_drawing = false;
+ while (*in) {
+ if (in_tag) {
+ if (in[0] == '}') {
+ in += 1;
+ in_tag = false;
+ } else if (in[0] == '\\' && in[1] == 'p') {
+ in += 2;
+ // Skip text between \pN and \p0 tags. A \p without a number
+ // is the same as \p0, and leading 0s are also allowed.
+ in_drawing = false;
+ while (in[0] >= '0' && in[0] <= '9') {
+ if (in[0] != '0')
+ in_drawing = true;
+ in += 1;
+ }
+ } else {
+ in += 1;
+ }
+ } else {
+ if (in[0] == '\\' && (in[1] == 'N' || in[1] == 'n')) {
+ in += 2;
+ append(b, '\n');
+ } else if (in[0] == '\\' && in[1] == 'h') {
+ in += 2;
+ append(b, ' ');
+ } else if (in[0] == '{') {
+ open_tag_pos = in;
+ in += 1;
+ in_tag = true;
+ } else {
+ if (!in_drawing)
+ append(b, in[0]);
+ in += 1;
+ }
+ }
+ }
+ // A '{' without a closing '}' is always visible.
+ if (in_tag) {
+ while (*open_tag_pos)
+ append(b, *open_tag_pos++);
+ }
+}
+
+// Empty string counts as whitespace. Reads s[len-1] even if there are \0s.
+static bool is_whitespace_only(char *s, int len)
+{
+ for (int n = 0; n < len; n++) {
+ if (s[n] != ' ' && s[n] != '\t')
+ return false;
+ }
+ return true;
+}
+
+static char *get_text_buf(struct sd *sd, double pts, enum sd_text_type type)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->ass_track;
+
+ if (pts == MP_NOPTS_VALUE)
+ return NULL;
+ long long ipts = find_timestamp(sd, pts);
+
+ struct buf b = {ctx->last_text, sizeof(ctx->last_text) - 1};
+
+ for (int i = 0; i < track->n_events; ++i) {
+ ASS_Event *event = track->events + i;
+ if (ipts >= event->Start && ipts < event->Start + event->Duration) {
+ if (event->Text) {
+ int start = b.len;
+ if (type == SD_TEXT_TYPE_PLAIN) {
+ ass_to_plaintext(&b, event->Text);
+ } else {
+ char *t = event->Text;
+ while (*t)
+ append(&b, *t++);
+ }
+ if (is_whitespace_only(&b.start[start], b.len - start)) {
+ b.len = start;
+ } else {
+ append(&b, '\n');
+ }
+ }
+ }
+ }
+
+ b.start[b.len] = '\0';
+
+ if (b.len > 0 && b.start[b.len - 1] == '\n')
+ b.start[b.len - 1] = '\0';
+
+ return ctx->last_text;
+}
+
+static char *get_text(struct sd *sd, double pts, enum sd_text_type type)
+{
+ return talloc_strdup(NULL, get_text_buf(sd, pts, type));
+}
+
+static struct sd_times get_times(struct sd *sd, double pts)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->ass_track;
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+
+ if (pts == MP_NOPTS_VALUE)
+ return res;
+
+ long long ipts = find_timestamp(sd, pts);
+
+ for (int i = 0; i < track->n_events; ++i) {
+ ASS_Event *event = track->events + i;
+ if (ipts >= event->Start && ipts < event->Start + event->Duration) {
+ double start = event->Start / 1000.0;
+ double end = event->Duration == UNKNOWN_DURATION ?
+ MP_NOPTS_VALUE : (event->Start + event->Duration) / 1000.0;
+
+ if (res.start == MP_NOPTS_VALUE || res.start > start)
+ res.start = start;
+
+ if (res.end == MP_NOPTS_VALUE || res.end < end)
+ res.end = end;
+ }
+ }
+
+ return res;
+}
+
+static void fill_plaintext(struct sd *sd, double pts)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->shadow_track;
+
+ ass_flush_events(track);
+
+ char *text = get_text_buf(sd, pts, SD_TEXT_TYPE_PLAIN);
+ if (!text)
+ return;
+
+ bstr dst = {0};
+
+ if (ctx->on_top)
+ bstr_xappend(NULL, &dst, bstr0("{\\a6}"));
+
+ while (*text) {
+ if (*text == '{')
+ bstr_xappend(NULL, &dst, bstr0("\\"));
+ bstr_xappend(NULL, &dst, (bstr){text, 1});
+ // Break ASS escapes with U+2060 WORD JOINER
+ if (*text == '\\')
+ mp_append_utf8_bstr(NULL, &dst, 0x2060);
+ text++;
+ }
+
+ if (!dst.start)
+ return;
+
+ int n = ass_alloc_event(track);
+ ASS_Event *event = track->events + n;
+ event->Start = 0;
+ event->Duration = INT_MAX;
+ event->Style = track->default_style;
+ event->Text = strdup(dst.start);
+
+ talloc_free(dst.start);
+}
+
+static void reset(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ if (sd->opts->sub_clear_on_seek || ctx->duration_unknown || ctx->clear_once) {
+ ass_flush_events(ctx->ass_track);
+ ctx->num_seen_packets = 0;
+ sd->preload_ok = false;
+ ctx->clear_once = false;
+ }
+ if (ctx->converter)
+ lavc_conv_reset(ctx->converter);
+}
+
+static void uninit(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ filters_destroy(sd);
+ if (ctx->converter)
+ lavc_conv_uninit(ctx->converter);
+ assobjects_destroy(sd);
+ talloc_free(ctx->copy_cache);
+}
+
+static int control(struct sd *sd, enum sd_ctrl cmd, void *arg)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ switch (cmd) {
+ case SD_CTRL_SUB_STEP: {
+ double *a = arg;
+ long long ts = llrint(a[0] * 1000.0);
+ long long res = ass_step_sub(ctx->ass_track, ts, a[1]);
+ if (!res)
+ return false;
+ // Try to account for overlapping durations
+ a[0] += res / 1000.0 + SUB_SEEK_OFFSET;
+ return true;
+ }
+ case SD_CTRL_SET_VIDEO_PARAMS:
+ ctx->video_params = *(struct mp_image_params *)arg;
+ return CONTROL_OK;
+ case SD_CTRL_SET_TOP:
+ ctx->on_top = *(bool *)arg;
+ return CONTROL_OK;
+ case SD_CTRL_UPDATE_OPTS: {
+ int flags = (uintptr_t)arg;
+ if (flags & UPDATE_SUB_FILT) {
+ filters_destroy(sd);
+ filters_init(sd);
+ ctx->clear_once = true; // allow reloading on seeks
+ reset(sd);
+ }
+ if (flags & UPDATE_SUB_HARD) {
+ // ass_track will be recreated, so clear duplicate cache
+ ctx->clear_once = true;
+ reset(sd);
+ assobjects_destroy(sd);
+ assobjects_init(sd);
+ }
+ ctx->ass_configured = false; // ass always needs to be reconfigured
+ return CONTROL_OK;
+ }
+ default:
+ return CONTROL_UNKNOWN;
+ }
+}
+
+const struct sd_functions sd_ass = {
+ .name = "ass",
+ .accept_packets_in_advance = true,
+ .init = init,
+ .decode = decode,
+ .get_bitmaps = get_bitmaps,
+ .get_text = get_text,
+ .get_times = get_times,
+ .control = control,
+ .reset = reset,
+ .select = enable_output,
+ .uninit = uninit,
+};
+
+// Disgusting hack for (xy-)vsfilter color compatibility.
+static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_ass_priv *ctx = sd->priv;
+ enum mp_csp csp = 0;
+ enum mp_csp_levels levels = 0;
+ if (opts->ass_vsfilter_color_compat == 0) // "no"
+ return;
+ bool force_601 = opts->ass_vsfilter_color_compat == 3;
+ ASS_Track *track = ctx->ass_track;
+ static const int ass_csp[] = {
+ [YCBCR_BT601_TV] = MP_CSP_BT_601,
+ [YCBCR_BT601_PC] = MP_CSP_BT_601,
+ [YCBCR_BT709_TV] = MP_CSP_BT_709,
+ [YCBCR_BT709_PC] = MP_CSP_BT_709,
+ [YCBCR_SMPTE240M_TV] = MP_CSP_SMPTE_240M,
+ [YCBCR_SMPTE240M_PC] = MP_CSP_SMPTE_240M,
+ };
+ static const int ass_levels[] = {
+ [YCBCR_BT601_TV] = MP_CSP_LEVELS_TV,
+ [YCBCR_BT601_PC] = MP_CSP_LEVELS_PC,
+ [YCBCR_BT709_TV] = MP_CSP_LEVELS_TV,
+ [YCBCR_BT709_PC] = MP_CSP_LEVELS_PC,
+ [YCBCR_SMPTE240M_TV] = MP_CSP_LEVELS_TV,
+ [YCBCR_SMPTE240M_PC] = MP_CSP_LEVELS_PC,
+ };
+ int trackcsp = track->YCbCrMatrix;
+ if (force_601)
+ trackcsp = YCBCR_BT601_TV;
+ // NONE is a bit random, but the intention is: don't modify colors.
+ if (trackcsp == YCBCR_NONE)
+ return;
+ if (trackcsp < sizeof(ass_csp) / sizeof(ass_csp[0]))
+ csp = ass_csp[trackcsp];
+ if (trackcsp < sizeof(ass_levels) / sizeof(ass_levels[0]))
+ levels = ass_levels[trackcsp];
+ if (trackcsp == YCBCR_DEFAULT) {
+ csp = MP_CSP_BT_601;
+ levels = MP_CSP_LEVELS_TV;
+ }
+ // Unknown colorspace (either YCBCR_UNKNOWN, or a valid value unknown to us)
+ if (!csp || !levels)
+ return;
+
+ struct mp_image_params params = ctx->video_params;
+
+ if (force_601) {
+ params.color = (struct mp_colorspace){
+ .space = MP_CSP_BT_709,
+ .levels = MP_CSP_LEVELS_TV,
+ };
+ }
+
+ if ((csp == params.color.space && levels == params.color.levels) ||
+ params.color.space == MP_CSP_RGB) // Even VSFilter doesn't mangle on RGB video
+ return;
+
+ bool basic_conv = params.color.space == MP_CSP_BT_709 &&
+ params.color.levels == MP_CSP_LEVELS_TV &&
+ csp == MP_CSP_BT_601 &&
+ levels == MP_CSP_LEVELS_TV;
+
+ // With "basic", only do as much as needed for basic compatibility.
+ if (opts->ass_vsfilter_color_compat == 1 && !basic_conv)
+ return;
+
+ if (params.color.space != ctx->last_params.color.space ||
+ params.color.levels != ctx->last_params.color.levels)
+ {
+ int msgl = basic_conv ? MSGL_V : MSGL_WARN;
+ ctx->last_params = params;
+ MP_MSG(sd, msgl, "mangling colors like vsfilter: "
+ "RGB -> %s %s -> %s %s -> RGB\n",
+ m_opt_choice_str(mp_csp_names, csp),
+ m_opt_choice_str(mp_csp_levels_names, levels),
+ m_opt_choice_str(mp_csp_names, params.color.space),
+ m_opt_choice_str(mp_csp_names, params.color.levels));
+ }
+
+ // Conversion that VSFilter would use
+ struct mp_csp_params vs_params = MP_CSP_PARAMS_DEFAULTS;
+ vs_params.color.space = csp;
+ vs_params.color.levels = levels;
+ struct mp_cmat vs_yuv2rgb, vs_rgb2yuv;
+ mp_get_csp_matrix(&vs_params, &vs_yuv2rgb);
+ mp_invert_cmat(&vs_rgb2yuv, &vs_yuv2rgb);
+
+ // Proper conversion to RGB
+ struct mp_csp_params rgb_params = MP_CSP_PARAMS_DEFAULTS;
+ rgb_params.color = params.color;
+ struct mp_cmat vs2rgb;
+ mp_get_csp_matrix(&rgb_params, &vs2rgb);
+
+ for (int n = 0; n < parts->num_parts; n++) {
+ struct sub_bitmap *sb = &parts->parts[n];
+ uint32_t color = sb->libass.color;
+ int r = (color >> 24u) & 0xff;
+ int g = (color >> 16u) & 0xff;
+ int b = (color >> 8u) & 0xff;
+ int a = 0xff - (color & 0xff);
+ int rgb[3] = {r, g, b}, yuv[3];
+ mp_map_fixp_color(&vs_rgb2yuv, 8, rgb, 8, yuv);
+ mp_map_fixp_color(&vs2rgb, 8, yuv, 8, rgb);
+ sb->libass.color = MP_ASS_RGBA(rgb[0], rgb[1], rgb[2], a);
+ }
+}
+
+int sd_ass_fmt_offset(const char *evt_fmt)
+{
+ // "Text" is always last (as it's arbitrary content in buf), e.g. format:
+ // "Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
+ int n = 0;
+ while (evt_fmt && (evt_fmt = strchr(evt_fmt, ',')))
+ evt_fmt++, n++;
+ return n-1; // buffer is without the format's Start/End, with ReadOrder
+}
+
+bstr sd_ass_pkt_text(struct sd_filter *ft, struct demux_packet *pkt, int offset)
+{
+ // e.g. pkt->buffer ("4" is ReadOrder): "4,0,Default,,0,0,0,,fifth line"
+ bstr txt = {(char *)pkt->buffer, pkt->len}, t0 = txt;
+ while (offset-- > 0) {
+ int n = bstrchr(txt, ',');
+ if (n < 0) { // shouldn't happen
+ MP_WARN(ft, "Malformed event '%.*s'\n", BSTR_P(t0));
+ return (bstr){NULL, 0};
+ }
+ txt = bstr_cut(txt, n+1);
+ }
+ return txt;
+}
+
+bstr sd_ass_to_plaintext(char *out, size_t out_siz, const char *in)
+{
+ struct buf b = {out, out_siz, 0};
+ ass_to_plaintext(&b, in);
+ if (b.len < out_siz)
+ out[b.len] = 0;
+ return (bstr){out, b.len};
+}
diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c
new file mode 100644
index 0000000..30aa641
--- /dev/null
+++ b/sub/sd_lavc.c
@@ -0,0 +1,676 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+#include <math.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/common.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/opt.h>
+
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "common/av_common.h"
+#include "demux/stheader.h"
+#include "options/options.h"
+#include "video/mp_image.h"
+#include "video/out/bitmap_packer.h"
+#include "img_convert.h"
+#include "sd.h"
+#include "dec_sub.h"
+
+#define MAX_QUEUE 4
+
+struct sub {
+ bool valid;
+ AVSubtitle avsub;
+ struct sub_bitmap *inbitmaps;
+ int count;
+ struct mp_image *data;
+ int bound_w, bound_h;
+ int src_w, src_h;
+ double pts;
+ double endpts;
+ int64_t id;
+};
+
+struct seekpoint {
+ double pts;
+ double endpts;
+};
+
+struct sd_lavc_priv {
+ AVCodecContext *avctx;
+ AVPacket *avpkt;
+ AVRational pkt_timebase;
+ struct sub subs[MAX_QUEUE]; // most recent event first
+ struct sub_bitmap *outbitmaps;
+ struct sub_bitmap *prevret;
+ int prevret_num;
+ int64_t displayed_id;
+ int64_t new_id;
+ struct mp_image_params video_params;
+ double current_pts;
+ struct seekpoint *seekpoints;
+ int num_seekpoints;
+ struct bitmap_packer *packer;
+};
+
+static int init(struct sd *sd)
+{
+ enum AVCodecID cid = mp_codec_to_av_codec_id(sd->codec->codec);
+
+ // Supported codecs must be known to decode to paletted bitmaps
+ switch (cid) {
+ case AV_CODEC_ID_DVB_SUBTITLE:
+ case AV_CODEC_ID_DVB_TELETEXT:
+ case AV_CODEC_ID_HDMV_PGS_SUBTITLE:
+ case AV_CODEC_ID_XSUB:
+ case AV_CODEC_ID_DVD_SUBTITLE:
+ break;
+ default:
+ return -1;
+ }
+
+ struct sd_lavc_priv *priv = talloc_zero(NULL, struct sd_lavc_priv);
+ AVCodecContext *ctx = NULL;
+ const AVCodec *sub_codec = avcodec_find_decoder(cid);
+ if (!sub_codec)
+ goto error;
+ ctx = avcodec_alloc_context3(sub_codec);
+ if (!ctx)
+ goto error;
+ priv->avpkt = av_packet_alloc();
+ if (!priv->avpkt)
+ goto error;
+ if (mp_set_avctx_codec_headers(ctx, sd->codec) < 0)
+ goto error;
+ priv->pkt_timebase = mp_get_codec_timebase(sd->codec);
+ ctx->pkt_timebase = priv->pkt_timebase;
+ if (avcodec_open2(ctx, sub_codec, NULL) < 0)
+ goto error;
+ priv->avctx = ctx;
+ sd->priv = priv;
+ priv->displayed_id = -1;
+ priv->current_pts = MP_NOPTS_VALUE;
+ priv->packer = talloc_zero(priv, struct bitmap_packer);
+ return 0;
+
+ error:
+ MP_FATAL(sd, "Could not open libavcodec subtitle decoder\n");
+ avcodec_free_context(&ctx);
+ mp_free_av_packet(&priv->avpkt);
+ talloc_free(priv);
+ return -1;
+}
+
+static void clear_sub(struct sub *sub)
+{
+ sub->count = 0;
+ sub->pts = MP_NOPTS_VALUE;
+ sub->endpts = MP_NOPTS_VALUE;
+ if (sub->valid)
+ avsubtitle_free(&sub->avsub);
+ sub->valid = false;
+}
+
+static void alloc_sub(struct sd_lavc_priv *priv)
+{
+ clear_sub(&priv->subs[MAX_QUEUE - 1]);
+ struct sub tmp = priv->subs[MAX_QUEUE - 1];
+ for (int n = MAX_QUEUE - 1; n > 0; n--)
+ priv->subs[n] = priv->subs[n - 1];
+ priv->subs[0] = tmp;
+ // clear only some fields; the memory allocs can be reused
+ priv->subs[0].valid = false;
+ priv->subs[0].count = 0;
+ priv->subs[0].src_w = 0;
+ priv->subs[0].src_h = 0;
+ priv->subs[0].id = priv->new_id++;
+}
+
+static void convert_pal(uint32_t *colors, size_t count, bool gray)
+{
+ for (int n = 0; n < count; n++) {
+ uint32_t c = colors[n];
+ uint32_t b = c & 0xFF;
+ uint32_t g = (c >> 8) & 0xFF;
+ uint32_t r = (c >> 16) & 0xFF;
+ uint32_t a = (c >> 24) & 0xFF;
+ if (gray)
+ r = g = b = (r + g + b) / 3;
+ // from straight to pre-multiplied alpha
+ b = b * a / 255;
+ g = g * a / 255;
+ r = r * a / 255;
+ colors[n] = b | (g << 8) | (r << 16) | (a << 24);
+ }
+}
+
+// Initialize sub from sub->avsub.
+static void read_sub_bitmaps(struct sd *sd, struct sub *sub)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_lavc_priv *priv = sd->priv;
+ AVSubtitle *avsub = &sub->avsub;
+
+ MP_TARRAY_GROW(priv, sub->inbitmaps, avsub->num_rects);
+
+ packer_set_size(priv->packer, avsub->num_rects);
+
+ // If we blur, we want a transparent region around the bitmap data to
+ // avoid "cut off" artifacts on the borders.
+ bool apply_blur = opts->sub_gauss != 0.0f;
+ int extend = apply_blur ? 5 : 0;
+ // Assume consumers may use bilinear scaling on it (2x2 filter)
+ int padding = 1 + extend;
+
+ priv->packer->padding = padding;
+
+ // For the sake of libswscale, which in some cases takes sub-rects as
+ // source images, and wants 16 byte start pointer and stride alignment.
+ int align = 4;
+
+ for (int i = 0; i < avsub->num_rects; i++) {
+ struct AVSubtitleRect *r = avsub->rects[i];
+ struct sub_bitmap *b = &sub->inbitmaps[sub->count];
+
+ if (r->type != SUBTITLE_BITMAP) {
+ MP_ERR(sd, "unsupported subtitle type from libavcodec\n");
+ continue;
+ }
+ if (!(r->flags & AV_SUBTITLE_FLAG_FORCED) && opts->sub_forced_events_only)
+ continue;
+ if (r->w <= 0 || r->h <= 0)
+ continue;
+
+ b->bitmap = r; // save for later (dumb hack to avoid more complexity)
+
+ priv->packer->in[sub->count] = (struct pos){r->w + (align - 1), r->h};
+ sub->count++;
+ }
+
+ priv->packer->count = sub->count;
+
+ if (packer_pack(priv->packer) < 0) {
+ MP_ERR(sd, "Unable to pack subtitle bitmaps.\n");
+ sub->count = 0;
+ }
+
+ if (!sub->count)
+ return;
+
+ struct pos bb[2];
+ packer_get_bb(priv->packer, bb);
+
+ sub->bound_w = bb[1].x;
+ sub->bound_h = bb[1].y;
+
+ if (!sub->data || sub->data->w < sub->bound_w || sub->data->h < sub->bound_h) {
+ talloc_free(sub->data);
+ sub->data = mp_image_alloc(IMGFMT_BGRA, priv->packer->w, priv->packer->h);
+ if (!sub->data) {
+ sub->count = 0;
+ return;
+ }
+ talloc_steal(priv, sub->data);
+ }
+
+ if (!mp_image_make_writeable(sub->data)) {
+ sub->count = 0;
+ return;
+ }
+
+ for (int i = 0; i < sub->count; i++) {
+ struct sub_bitmap *b = &sub->inbitmaps[i];
+ struct pos pos = priv->packer->result[i];
+ struct AVSubtitleRect *r = b->bitmap;
+ uint8_t **data = r->data;
+ int *linesize = r->linesize;
+ b->w = r->w;
+ b->h = r->h;
+ b->x = r->x;
+ b->y = r->y;
+
+ // Choose such that the extended start position is aligned.
+ pos.x = MP_ALIGN_UP(pos.x - extend, align) + extend;
+
+ b->src_x = pos.x;
+ b->src_y = pos.y;
+ b->stride = sub->data->stride[0];
+ b->bitmap = sub->data->planes[0] + pos.y * b->stride + pos.x * 4;
+
+ sub->src_w = MPMAX(sub->src_w, b->x + b->w);
+ sub->src_h = MPMAX(sub->src_h, b->y + b->h);
+
+ assert(r->nb_colors > 0);
+ assert(r->nb_colors <= 256);
+ uint32_t pal[256] = {0};
+ memcpy(pal, data[1], r->nb_colors * 4);
+ convert_pal(pal, 256, opts->sub_gray);
+
+ for (int y = -padding; y < b->h + padding; y++) {
+ uint32_t *out = (uint32_t*)((char*)b->bitmap + y * b->stride);
+ int start = 0;
+ for (int x = -padding; x < 0; x++)
+ out[x] = 0;
+ if (y >= 0 && y < b->h) {
+ uint8_t *in = data[0] + y * linesize[0];
+ for (int x = 0; x < b->w; x++)
+ *out++ = pal[*in++];
+ start = b->w;
+ }
+ for (int x = start; x < b->w + padding; x++)
+ *out++ = 0;
+ }
+
+ b->bitmap = (char*)b->bitmap - extend * b->stride - extend * 4;
+ b->src_x -= extend;
+ b->src_y -= extend;
+ b->x -= extend;
+ b->y -= extend;
+ b->w += extend * 2;
+ b->h += extend * 2;
+
+ if (apply_blur)
+ mp_blur_rgba_sub_bitmap(b, opts->sub_gauss);
+ }
+}
+
+static void decode(struct sd *sd, struct demux_packet *packet)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_lavc_priv *priv = sd->priv;
+ AVCodecContext *ctx = priv->avctx;
+ double pts = packet->pts;
+ double endpts = MP_NOPTS_VALUE;
+ AVSubtitle sub;
+
+ if (pts == MP_NOPTS_VALUE)
+ MP_WARN(sd, "Subtitle with unknown start time.\n");
+
+ mp_set_av_packet(priv->avpkt, packet, &priv->pkt_timebase);
+
+ if (ctx->codec_id == AV_CODEC_ID_DVB_TELETEXT) {
+ char page[4];
+ snprintf(page, sizeof(page), "%d", opts->teletext_page);
+ av_opt_set(ctx, "txt_page", page, AV_OPT_SEARCH_CHILDREN);
+ }
+
+ int got_sub;
+ int res = avcodec_decode_subtitle2(ctx, &sub, &got_sub, priv->avpkt);
+ if (res < 0 || !got_sub)
+ return;
+
+ if (sub.pts != AV_NOPTS_VALUE)
+ pts = sub.pts / (double)AV_TIME_BASE;
+
+ if (pts != MP_NOPTS_VALUE) {
+ if (sub.end_display_time > sub.start_display_time &&
+ sub.end_display_time != UINT32_MAX)
+ {
+ endpts = pts + sub.end_display_time / 1000.0;
+ }
+ pts += sub.start_display_time / 1000.0;
+
+ // set end time of previous sub
+ struct sub *prev = &priv->subs[0];
+ if (prev->valid) {
+ if (prev->endpts == MP_NOPTS_VALUE || prev->endpts > pts)
+ prev->endpts = pts;
+
+ if (opts->sub_fix_timing && pts - prev->endpts <= SUB_GAP_THRESHOLD)
+ prev->endpts = pts;
+
+ for (int n = 0; n < priv->num_seekpoints; n++) {
+ if (priv->seekpoints[n].pts == prev->pts) {
+ priv->seekpoints[n].endpts = prev->endpts;
+ break;
+ }
+ }
+ }
+
+ // This subtitle packet only signals the end of subtitle display.
+ if (!sub.num_rects) {
+ avsubtitle_free(&sub);
+ return;
+ }
+ }
+
+ alloc_sub(priv);
+ struct sub *current = &priv->subs[0];
+
+ current->valid = true;
+ current->pts = pts;
+ current->endpts = endpts;
+ current->avsub = sub;
+
+ read_sub_bitmaps(sd, current);
+
+ if (pts != MP_NOPTS_VALUE) {
+ for (int n = 0; n < priv->num_seekpoints; n++) {
+ if (priv->seekpoints[n].pts == pts)
+ goto skip;
+ }
+ // Set arbitrary limit as safe-guard against insane files.
+ if (priv->num_seekpoints >= 10000)
+ MP_TARRAY_REMOVE_AT(priv->seekpoints, priv->num_seekpoints, 0);
+ MP_TARRAY_APPEND(priv, priv->seekpoints, priv->num_seekpoints,
+ (struct seekpoint){.pts = pts, .endpts = endpts});
+ skip: ;
+ }
+}
+
+static struct sub *get_current(struct sd_lavc_priv *priv, double pts)
+{
+ struct sub *current = NULL;
+ for (int n = 0; n < MAX_QUEUE; n++) {
+ struct sub *sub = &priv->subs[n];
+ if (!sub->valid)
+ continue;
+ if (pts == MP_NOPTS_VALUE ||
+ ((sub->pts == MP_NOPTS_VALUE || pts + 1e-6 >= sub->pts) &&
+ (sub->endpts == MP_NOPTS_VALUE || pts + 1e-6 < sub->endpts)))
+ {
+ // Ignore "trailing" subtitles with unknown length after 1 minute.
+ if (sub->endpts == MP_NOPTS_VALUE && pts >= sub->pts + 60)
+ break;
+ current = sub;
+ break;
+ }
+ }
+ return current;
+}
+
+static struct sub_bitmaps *get_bitmaps(struct sd *sd, struct mp_osd_res d,
+ int format, double pts)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+
+ priv->current_pts = pts;
+
+ struct sub *current = get_current(priv, pts);
+
+ if (!current)
+ return NULL;
+
+ MP_TARRAY_GROW(priv, priv->outbitmaps, current->count);
+ for (int n = 0; n < current->count; n++)
+ priv->outbitmaps[n] = current->inbitmaps[n];
+
+ struct sub_bitmaps *res = &(struct sub_bitmaps){0};
+ res->parts = priv->outbitmaps;
+ res->num_parts = current->count;
+ if (priv->displayed_id != current->id)
+ res->change_id++;
+ priv->displayed_id = current->id;
+ res->packed = current->data;
+ res->packed_w = current->bound_w;
+ res->packed_h = current->bound_h;
+ res->format = SUBBITMAP_BGRA;
+
+ double video_par = 0;
+ if (priv->avctx->codec_id == AV_CODEC_ID_DVD_SUBTITLE &&
+ opts->stretch_dvd_subs)
+ {
+ // For DVD subs, try to keep the subtitle PAR at display PAR.
+ double par = priv->video_params.p_w / (double)priv->video_params.p_h;
+ if (isnormal(par))
+ video_par = par;
+ }
+ if (priv->avctx->codec_id == AV_CODEC_ID_HDMV_PGS_SUBTITLE)
+ video_par = -1;
+ if (opts->stretch_image_subs)
+ d.ml = d.mr = d.mt = d.mb = 0;
+ int w = priv->avctx->width;
+ int h = priv->avctx->height;
+ if (w <= 0 || h <= 0 || opts->image_subs_video_res) {
+ w = priv->video_params.w;
+ h = priv->video_params.h;
+ }
+ if (current->src_w > w || current->src_h > h) {
+ w = MPMAX(priv->video_params.w, current->src_w);
+ h = MPMAX(priv->video_params.h, current->src_h);
+ }
+
+ if (opts->sub_pos != 100.0f && opts->ass_style_override) {
+ float offset = (100.0f - opts->sub_pos) / 100.0f * h;
+
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *sub = &res->parts[n];
+
+ // Decide by heuristic whether this is a sub-title or something
+ // else (top-title, covering whole screen).
+ if (sub->y < h / 2)
+ continue;
+
+ // Allow moving up the subtitle, but only until it clips.
+ sub->y = MPMAX(sub->y - offset, 0);
+ sub->y = MPMIN(sub->y + sub->h, h) - sub->h;
+ }
+ }
+
+ osd_rescale_bitmaps(res, w, h, d, video_par);
+
+ if (opts->sub_scale != 1.0 && opts->ass_style_override) {
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *sub = &res->parts[n];
+
+ float shit = (opts->sub_scale - 1.0f) / 2;
+
+ // Fortunately VO isn't supposed to give a FUCKING FUCK about
+ // whether the sub might e.g. go outside of the screen.
+ sub->x -= sub->dw * shit;
+ sub->y -= sub->dh * shit;
+ sub->dw += sub->dw * shit * 2;
+ sub->dh += sub->dh * shit * 2;
+ }
+ }
+
+ if (priv->prevret_num != res->num_parts)
+ res->change_id++;
+
+ if (!res->change_id) {
+ assert(priv->prevret_num == res->num_parts);
+ for (int n = 0; n < priv->prevret_num; n++) {
+ struct sub_bitmap *a = &res->parts[n];
+ struct sub_bitmap *b = &priv->prevret[n];
+
+ if (a->x != b->x || a->y != b->y ||
+ a->dw != b->dw || a->dh != b->dh)
+ {
+ res->change_id++;
+ break;
+ }
+ }
+ }
+
+ priv->prevret_num = res->num_parts;
+ MP_TARRAY_GROW(priv, priv->prevret, priv->prevret_num);
+ memcpy(priv->prevret, res->parts, res->num_parts * sizeof(priv->prevret[0]));
+
+ return sub_bitmaps_copy(NULL, res);
+}
+
+static struct sd_times get_times(struct sd *sd, double pts)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+
+ if (pts == MP_NOPTS_VALUE)
+ return res;
+
+ struct sub *current = get_current(priv, pts);
+
+ if (!current)
+ return res;
+
+ res.start = current->pts;
+ res.end = current->endpts;
+
+ return res;
+}
+
+static bool accepts_packet(struct sd *sd, double min_pts)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+
+ double pts = priv->current_pts;
+ if (min_pts != MP_NOPTS_VALUE) {
+ // guard against bogus rendering PTS in the future.
+ if (pts == MP_NOPTS_VALUE || min_pts < pts)
+ pts = min_pts;
+ // Heuristic: we assume rendering cannot lag behind more than 1 second
+ // behind decoding.
+ if (pts + 1 < min_pts)
+ pts = min_pts;
+ }
+
+ int last_needed = -1;
+ for (int n = 0; n < MAX_QUEUE; n++) {
+ struct sub *sub = &priv->subs[n];
+ if (!sub->valid)
+ continue;
+ if (pts == MP_NOPTS_VALUE ||
+ ((sub->pts == MP_NOPTS_VALUE || sub->pts >= pts) ||
+ (sub->endpts == MP_NOPTS_VALUE || pts < sub->endpts)))
+ {
+ last_needed = n;
+ }
+ }
+ // We can accept a packet if it wouldn't overflow the fixed subtitle queue.
+ // We assume that get_bitmaps() never decreases the PTS.
+ return last_needed + 1 < MAX_QUEUE;
+}
+
+static void reset(struct sd *sd)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+
+ for (int n = 0; n < MAX_QUEUE; n++)
+ clear_sub(&priv->subs[n]);
+ // lavc might not do this right for all codecs; may need close+reopen
+ avcodec_flush_buffers(priv->avctx);
+
+ priv->current_pts = MP_NOPTS_VALUE;
+}
+
+static void uninit(struct sd *sd)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+
+ for (int n = 0; n < MAX_QUEUE; n++)
+ clear_sub(&priv->subs[n]);
+ avcodec_free_context(&priv->avctx);
+ mp_free_av_packet(&priv->avpkt);
+ talloc_free(priv);
+}
+
+static int compare_seekpoint(const void *pa, const void *pb)
+{
+ const struct seekpoint *a = pa, *b = pb;
+ return a->pts == b->pts ? 0 : (a->pts < b->pts ? -1 : +1);
+}
+
+// taken from ass_step_sub(), libass (ISC)
+static double step_sub(struct sd *sd, double now, int movement)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ int best = -1;
+ double target = now;
+ int direction = (movement > 0 ? 1 : -1) * !!movement;
+
+ if (priv->num_seekpoints == 0)
+ return MP_NOPTS_VALUE;
+
+ qsort(priv->seekpoints, priv->num_seekpoints, sizeof(priv->seekpoints[0]),
+ compare_seekpoint);
+
+ do {
+ int closest = -1;
+ double closest_time = 0;
+ for (int i = 0; i < priv->num_seekpoints; i++) {
+ struct seekpoint *p = &priv->seekpoints[i];
+ double start = p->pts;
+ if (direction < 0) {
+ double end = p->endpts == MP_NOPTS_VALUE ? INFINITY : p->endpts;
+ if (end < target) {
+ if (closest < 0 || end > closest_time) {
+ closest = i;
+ closest_time = end;
+ }
+ }
+ } else if (direction > 0) {
+ if (start > target) {
+ if (closest < 0 || start < closest_time) {
+ closest = i;
+ closest_time = start;
+ }
+ }
+ } else {
+ if (start < target) {
+ if (closest < 0 || start >= closest_time) {
+ closest = i;
+ closest_time = start;
+ }
+ }
+ }
+ }
+ if (closest < 0)
+ break;
+ target = closest_time + direction;
+ best = closest;
+ movement -= direction;
+ } while (movement);
+
+ return best < 0 ? now : priv->seekpoints[best].pts;
+}
+
+static int control(struct sd *sd, enum sd_ctrl cmd, void *arg)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ switch (cmd) {
+ case SD_CTRL_SUB_STEP: {
+ double *a = arg;
+ double res = step_sub(sd, a[0], a[1]);
+ if (res == MP_NOPTS_VALUE)
+ return false;
+ a[0] = res;
+ return true;
+ }
+ case SD_CTRL_SET_VIDEO_PARAMS:
+ priv->video_params = *(struct mp_image_params *)arg;
+ return CONTROL_OK;
+ default:
+ return CONTROL_UNKNOWN;
+ }
+}
+
+const struct sd_functions sd_lavc = {
+ .name = "lavc",
+ .init = init,
+ .decode = decode,
+ .get_bitmaps = get_bitmaps,
+ .get_times = get_times,
+ .accepts_packet = accepts_packet,
+ .control = control,
+ .reset = reset,
+ .uninit = uninit,
+};
diff --git a/ta/README b/ta/README
new file mode 100644
index 0000000..4e2fd16
--- /dev/null
+++ b/ta/README
@@ -0,0 +1,39 @@
+TA ("Tree Allocator") is a wrapper around malloc() and related functions,
+adding features like automatically freeing sub-trees of memory allocations if
+a parent allocation is freed.
+
+Generally, the idea is that every TA allocation can have a parent (indicated
+by the ta_parent argument in allocation function calls). If a parent is freed,
+its child allocations are automatically freed as well. It is also allowed to
+free a child before the parent, or to move a child to another parent with
+ta_set_parent().
+
+It also provides a bunch of convenience macros and debugging facilities.
+
+The TA functions are documented in the implementation files (ta.c, ta_utils.c).
+
+TA is intended to be useable as library independent from mpv. It doesn't
+depend on anything mpv specific.
+
+Note:
+-----
+
+mpv doesn't use the TA API yet for two reasons: first, the TA API is not
+necessarily finalized yet. Second, it should be easily possible to revert
+the commit adding TA, and changing all the code would not allow this.
+
+Especially the naming schema for some TA functions is still somewhat
+undecided. (The talloc naming is a bit verbose at times.)
+
+For now, mpv goes through a talloc wrapper, which maps the talloc API to TA.
+New code should still use talloc as well. At one point, all talloc calls
+will be replaced with TA calls, and the talloc wrapper will be removed.
+
+Documentation for the talloc API is here:
+
+ https://talloc.samba.org/talloc/doc/html/group__talloc.html
+
+There are some minor differences with mpv's talloc bridge. mpv calls abort()
+on allocation failures, and the talloc_set_destructor() signature is slightly
+different. libtalloc also has a weird 256MB limit per allocation. The talloc
+wrapper supports only a strict subset of libtalloc functionality used by mpv.
diff --git a/ta/ta.c b/ta/ta.c
new file mode 100644
index 0000000..2f68400
--- /dev/null
+++ b/ta/ta.c
@@ -0,0 +1,404 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define TA_NO_WRAPPERS
+#include "ta.h"
+
+#if !defined(TA_MEMORY_DEBUGGING)
+ #if !defined(NDEBUG)
+ #define TA_MEMORY_DEBUGGING 1
+ #else
+ #define TA_MEMORY_DEBUGGING 0
+ #endif
+#endif
+
+struct ta_header {
+ size_t size; // size of the user allocation
+ // Invariant: parent!=NULL => prev==NULL
+ struct ta_header *prev; // siblings list (by destructor order)
+ struct ta_header *next;
+ // Invariant: parent==NULL || parent->child==this
+ struct ta_header *child; // points to first child
+ struct ta_header *parent; // set for _first_ child only, NULL otherwise
+ void (*destructor)(void *);
+#if TA_MEMORY_DEBUGGING
+ unsigned int canary;
+ struct ta_header *leak_next;
+ struct ta_header *leak_prev;
+ const char *name;
+#endif
+};
+
+#define CANARY 0xD3ADB3EF
+
+#define MIN_ALIGN _Alignof(max_align_t)
+union aligned_header {
+ struct ta_header ta;
+ // Make sure to satisfy typical alignment requirements
+ void *align_ptr;
+ int align_int;
+ double align_d;
+ long long align_ll;
+ char align_min[(sizeof(struct ta_header) + MIN_ALIGN - 1) & ~(MIN_ALIGN - 1)];
+};
+
+#define PTR_TO_HEADER(ptr) (&((union aligned_header *)(ptr) - 1)->ta)
+#define PTR_FROM_HEADER(h) ((void *)((union aligned_header *)(h) + 1))
+
+#define MAX_ALLOC (((size_t)-1) - sizeof(union aligned_header))
+
+static void ta_dbg_add(struct ta_header *h);
+static void ta_dbg_check_header(struct ta_header *h);
+static void ta_dbg_remove(struct ta_header *h);
+
+static struct ta_header *get_header(void *ptr)
+{
+ struct ta_header *h = ptr ? PTR_TO_HEADER(ptr) : NULL;
+ ta_dbg_check_header(h);
+ return h;
+}
+
+/* Set the parent allocation of ptr. If parent==NULL, remove the parent.
+ * Setting parent==NULL (with ptr!=NULL) unsets the parent of ptr.
+ * With ptr==NULL, the function does nothing.
+ *
+ * Warning: if ta_parent is a direct or indirect child of ptr, things will go
+ * wrong. The function will apparently succeed, but creates circular
+ * parent links, which are not allowed.
+ */
+void ta_set_parent(void *ptr, void *ta_parent)
+{
+ struct ta_header *ch = get_header(ptr);
+ if (!ch)
+ return;
+ struct ta_header *new_parent = get_header(ta_parent);
+ // Unlink from previous parent
+ if (ch->prev)
+ ch->prev->next = ch->next;
+ if (ch->next)
+ ch->next->prev = ch->prev;
+ // If ch was the first child, change child link of old parent
+ if (ch->parent) {
+ assert(ch->parent->child == ch);
+ ch->parent->child = ch->next;
+ if (ch->parent->child) {
+ assert(!ch->parent->child->parent);
+ ch->parent->child->parent = ch->parent;
+ }
+ }
+ ch->next = ch->prev = ch->parent = NULL;
+ // Link to new parent - insert at start of list (LIFO destructor order)
+ if (new_parent) {
+ ch->next = new_parent->child;
+ if (ch->next) {
+ ch->next->prev = ch;
+ ch->next->parent = NULL;
+ }
+ new_parent->child = ch;
+ ch->parent = new_parent;
+ }
+}
+
+/* Return the parent allocation, or NULL if none or if ptr==NULL.
+ *
+ * Warning: do not use this for program logic, or I'll be sad.
+ */
+void *ta_get_parent(void *ptr)
+{
+ struct ta_header *ch = get_header(ptr);
+ return ch ? ch->parent : NULL;
+}
+
+/* Allocate size bytes of memory. If ta_parent is not NULL, this is used as
+ * parent allocation (if ta_parent is freed, this allocation is automatically
+ * freed as well). size==0 allocates a block of size 0 (i.e. returns non-NULL).
+ * Returns NULL on OOM.
+ */
+void *ta_alloc_size(void *ta_parent, size_t size)
+{
+ if (size >= MAX_ALLOC)
+ return NULL;
+ struct ta_header *h = malloc(sizeof(union aligned_header) + size);
+ if (!h)
+ return NULL;
+ *h = (struct ta_header) {.size = size};
+ ta_dbg_add(h);
+ void *ptr = PTR_FROM_HEADER(h);
+ ta_set_parent(ptr, ta_parent);
+ return ptr;
+}
+
+/* Exactly the same as ta_alloc_size(), but the returned memory block is
+ * initialized to 0.
+ */
+void *ta_zalloc_size(void *ta_parent, size_t size)
+{
+ if (size >= MAX_ALLOC)
+ return NULL;
+ struct ta_header *h = calloc(1, sizeof(union aligned_header) + size);
+ if (!h)
+ return NULL;
+ *h = (struct ta_header) {.size = size};
+ ta_dbg_add(h);
+ void *ptr = PTR_FROM_HEADER(h);
+ ta_set_parent(ptr, ta_parent);
+ return ptr;
+}
+
+/* Reallocate the allocation given by ptr and return a new pointer. Much like
+ * realloc(), the returned pointer can be different, and on OOM, NULL is
+ * returned.
+ *
+ * size==0 is equivalent to ta_free(ptr).
+ * ptr==NULL is equivalent to ta_alloc_size(ta_parent, size).
+ *
+ * ta_parent is used only in the ptr==NULL case.
+ *
+ * Returns NULL if the operation failed.
+ * NULL is also returned if size==0.
+ */
+void *ta_realloc_size(void *ta_parent, void *ptr, size_t size)
+{
+ if (size >= MAX_ALLOC)
+ return NULL;
+ if (!size) {
+ ta_free(ptr);
+ return NULL;
+ }
+ if (!ptr)
+ return ta_alloc_size(ta_parent, size);
+ struct ta_header *h = get_header(ptr);
+ struct ta_header *old_h = h;
+ if (h->size == size)
+ return ptr;
+ ta_dbg_remove(h);
+ h = realloc(h, sizeof(union aligned_header) + size);
+ ta_dbg_add(h ? h : old_h);
+ if (!h)
+ return NULL;
+ h->size = size;
+ if (h != old_h) {
+ // Relink parent
+ if (h->parent)
+ h->parent->child = h;
+ // Relink siblings
+ if (h->next)
+ h->next->prev = h;
+ if (h->prev)
+ h->prev->next = h;
+ // Relink children
+ if (h->child)
+ h->child->parent = h;
+ }
+ return PTR_FROM_HEADER(h);
+}
+
+/* Return the allocated size of ptr. This returns the size parameter of the
+ * most recent ta_alloc.../ta_realloc... call.
+ * If ptr==NULL, return 0.
+ */
+size_t ta_get_size(void *ptr)
+{
+ struct ta_header *h = get_header(ptr);
+ return h ? h->size : 0;
+}
+
+/* Free all allocations that (recursively) have ptr as parent allocation, but
+ * do not free ptr itself.
+ */
+void ta_free_children(void *ptr)
+{
+ struct ta_header *h = get_header(ptr);
+ while (h && h->child)
+ ta_free(PTR_FROM_HEADER(h->child));
+}
+
+/* Free the given allocation, and all of its direct and indirect children.
+ */
+void ta_free(void *ptr)
+{
+ struct ta_header *h = get_header(ptr);
+ if (!h)
+ return;
+ if (h->destructor)
+ h->destructor(ptr);
+ ta_free_children(ptr);
+ ta_set_parent(ptr, NULL);
+ ta_dbg_remove(h);
+ free(h);
+}
+
+/* Set a destructor that is to be called when the given allocation is freed.
+ * (Whether the allocation is directly freed with ta_free() or indirectly by
+ * freeing its parent does not matter.) There is only one destructor. If an
+ * destructor was already set, it's overwritten.
+ *
+ * The destructor will be called with ptr as argument. The destructor can do
+ * almost anything, but it must not attempt to free or realloc ptr. The
+ * destructor is run before the allocation's children are freed (also, before
+ * their destructors are run).
+ */
+void ta_set_destructor(void *ptr, void (*destructor)(void *))
+{
+ struct ta_header *h = get_header(ptr);
+ if (h)
+ h->destructor = destructor;
+}
+
+#if TA_MEMORY_DEBUGGING
+
+#include "osdep/threads.h"
+
+static mp_static_mutex ta_dbg_mutex = MP_STATIC_MUTEX_INITIALIZER;
+static bool enable_leak_check; // pretty much constant
+static struct ta_header leak_node;
+static char allocation_is_string;
+
+static void ta_dbg_add(struct ta_header *h)
+{
+ h->canary = CANARY;
+ if (enable_leak_check) {
+ mp_mutex_lock(&ta_dbg_mutex);
+ h->leak_next = &leak_node;
+ h->leak_prev = leak_node.leak_prev;
+ leak_node.leak_prev->leak_next = h;
+ leak_node.leak_prev = h;
+ mp_mutex_unlock(&ta_dbg_mutex);
+ }
+}
+
+static void ta_dbg_check_header(struct ta_header *h)
+{
+ if (h) {
+ assert(h->canary == CANARY);
+ if (h->parent) {
+ assert(!h->prev);
+ assert(h->parent->child == h);
+ }
+ }
+}
+
+static void ta_dbg_remove(struct ta_header *h)
+{
+ ta_dbg_check_header(h);
+ if (h->leak_next) { // assume checking for !=NULL invariant ok without lock
+ mp_mutex_lock(&ta_dbg_mutex);
+ h->leak_next->leak_prev = h->leak_prev;
+ h->leak_prev->leak_next = h->leak_next;
+ mp_mutex_unlock(&ta_dbg_mutex);
+ h->leak_next = h->leak_prev = NULL;
+ }
+ h->canary = 0;
+}
+
+static size_t get_children_size(struct ta_header *h)
+{
+ size_t size = 0;
+ for (struct ta_header *s = h->child; s; s = s->next)
+ size += s->size + get_children_size(s);
+ return size;
+}
+
+static void print_leak_report(void)
+{
+ mp_mutex_lock(&ta_dbg_mutex);
+ if (leak_node.leak_next && leak_node.leak_next != &leak_node) {
+ size_t size = 0;
+ size_t num_blocks = 0;
+ fprintf(stderr, "Blocks not freed:\n");
+ fprintf(stderr, " %-20s %10s %10s %s\n",
+ "Ptr", "Bytes", "C. Bytes", "Name");
+ while (leak_node.leak_next != &leak_node) {
+ struct ta_header *cur = leak_node.leak_next;
+ // Don't list those with parent; logically, only parents are listed
+ if (!cur->next) {
+ size_t c_size = get_children_size(cur);
+ char name[50] = {0};
+ if (cur->name)
+ snprintf(name, sizeof(name), "%s", cur->name);
+ if (cur->name == &allocation_is_string) {
+ snprintf(name, sizeof(name), "'%.*s'",
+ (int)cur->size, (char *)PTR_FROM_HEADER(cur));
+ }
+ for (int n = 0; n < sizeof(name); n++) {
+ if (name[n] && name[n] < 0x20)
+ name[n] = '.';
+ }
+ fprintf(stderr, " %-20p %10zu %10zu %s\n",
+ cur, cur->size, c_size, name);
+ }
+ size += cur->size;
+ num_blocks += 1;
+ // Unlink, and don't confuse valgrind by leaving live pointers.
+ cur->leak_next->leak_prev = cur->leak_prev;
+ cur->leak_prev->leak_next = cur->leak_next;
+ cur->leak_next = cur->leak_prev = NULL;
+ }
+ fprintf(stderr, "%zu bytes in %zu blocks.\n", size, num_blocks);
+ }
+ mp_mutex_unlock(&ta_dbg_mutex);
+}
+
+void ta_enable_leak_report(void)
+{
+ mp_mutex_lock(&ta_dbg_mutex);
+ enable_leak_check = true;
+ if (!leak_node.leak_prev && !leak_node.leak_next) {
+ leak_node.leak_prev = &leak_node;
+ leak_node.leak_next = &leak_node;
+ atexit(print_leak_report);
+ }
+ mp_mutex_unlock(&ta_dbg_mutex);
+}
+
+/* Set a (static) string that will be printed if the memory allocation in ptr
+ * shows up on the leak report. The string must stay valid until ptr is freed.
+ * Calling it on ptr==NULL does nothing.
+ * Typically used to set location info.
+ * Always returns ptr (useful for chaining function calls).
+ */
+void *ta_dbg_set_loc(void *ptr, const char *loc)
+{
+ struct ta_header *h = get_header(ptr);
+ if (h)
+ h->name = loc;
+ return ptr;
+}
+
+/* Mark the allocation as string. The leak report will print it literally.
+ */
+void *ta_dbg_mark_as_string(void *ptr)
+{
+ // Specially handled by leak report code.
+ return ta_dbg_set_loc(ptr, &allocation_is_string);
+}
+
+#else
+
+static void ta_dbg_add(struct ta_header *h){}
+static void ta_dbg_check_header(struct ta_header *h){}
+static void ta_dbg_remove(struct ta_header *h){}
+
+void ta_enable_leak_report(void){}
+void *ta_dbg_set_loc(void *ptr, const char *loc){return ptr;}
+void *ta_dbg_mark_as_string(void *ptr){return ptr;}
+
+#endif
diff --git a/ta/ta.h b/ta/ta.h
new file mode 100644
index 0000000..4baac8f
--- /dev/null
+++ b/ta/ta.h
@@ -0,0 +1,157 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef TA_H_
+#define TA_H_
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#ifdef __GNUC__
+#define TA_PRF(a1, a2) __attribute__ ((format(printf, a1, a2)))
+#define TA_TYPEOF(t) __typeof__(t)
+#else
+#define TA_PRF(a1, a2)
+#define TA_TYPEOF(t) void *
+#endif
+
+// Broken crap with __USE_MINGW_ANSI_STDIO
+#if defined(__MINGW32__) && defined(__GNUC__) && !defined(__clang__)
+#undef TA_PRF
+#define TA_PRF(a1, a2) __attribute__ ((format (gnu_printf, a1, a2)))
+#endif
+
+#define TA_STRINGIFY_(x) # x
+#define TA_STRINGIFY(x) TA_STRINGIFY_(x)
+
+#ifdef NDEBUG
+#define TA_LOC ""
+#else
+#define TA_LOC __FILE__ ":" TA_STRINGIFY(__LINE__)
+#endif
+
+// Core functions
+void *ta_alloc_size(void *ta_parent, size_t size);
+void *ta_zalloc_size(void *ta_parent, size_t size);
+void *ta_realloc_size(void *ta_parent, void *ptr, size_t size);
+size_t ta_get_size(void *ptr);
+void ta_free(void *ptr);
+void ta_free_children(void *ptr);
+void ta_set_destructor(void *ptr, void (*destructor)(void *));
+void ta_set_parent(void *ptr, void *ta_parent);
+void *ta_get_parent(void *ptr);
+
+// Utility functions
+size_t ta_calc_array_size(size_t element_size, size_t count);
+size_t ta_calc_prealloc_elems(size_t nextidx);
+void *ta_new_context(void *ta_parent);
+void *ta_steal_(void *ta_parent, void *ptr);
+void *ta_memdup(void *ta_parent, void *ptr, size_t size);
+char *ta_strdup(void *ta_parent, const char *str);
+bool ta_strdup_append(char **str, const char *a);
+bool ta_strdup_append_buffer(char **str, const char *a);
+char *ta_strndup(void *ta_parent, const char *str, size_t n);
+bool ta_strndup_append(char **str, const char *a, size_t n);
+bool ta_strndup_append_buffer(char **str, const char *a, size_t n);
+char *ta_asprintf(void *ta_parent, const char *fmt, ...) TA_PRF(2, 3);
+char *ta_vasprintf(void *ta_parent, const char *fmt, va_list ap) TA_PRF(2, 0);
+bool ta_asprintf_append(char **str, const char *fmt, ...) TA_PRF(2, 3);
+bool ta_vasprintf_append(char **str, const char *fmt, va_list ap) TA_PRF(2, 0);
+bool ta_asprintf_append_buffer(char **str, const char *fmt, ...) TA_PRF(2, 3);
+bool ta_vasprintf_append_buffer(char **str, const char *fmt, va_list ap) TA_PRF(2, 0);
+
+#define ta_new(ta_parent, type) (type *)ta_alloc_size(ta_parent, sizeof(type))
+#define ta_znew(ta_parent, type) (type *)ta_zalloc_size(ta_parent, sizeof(type))
+
+#define ta_new_array(ta_parent, type, count) \
+ (type *)ta_alloc_size(ta_parent, ta_calc_array_size(sizeof(type), count))
+
+#define ta_znew_array(ta_parent, type, count) \
+ (type *)ta_zalloc_size(ta_parent, ta_calc_array_size(sizeof(type), count))
+
+#define ta_new_array_size(ta_parent, element_size, count) \
+ ta_alloc_size(ta_parent, ta_calc_array_size(element_size, count))
+
+#define ta_realloc(ta_parent, ptr, type, count) \
+ (type *)ta_realloc_size(ta_parent, ptr, ta_calc_array_size(sizeof(type), count))
+
+#define ta_new_ptrtype(ta_parent, ptr) \
+ (TA_TYPEOF(ptr))ta_alloc_size(ta_parent, sizeof(*ptr))
+
+#define ta_new_array_ptrtype(ta_parent, ptr, count) \
+ (TA_TYPEOF(ptr))ta_new_array_size(ta_parent, sizeof(*(ptr)), count)
+
+#define ta_steal(ta_parent, ptr) (TA_TYPEOF(ptr))ta_steal_(ta_parent, ptr)
+
+#define ta_dup(ta_parent, ptr) \
+ (TA_TYPEOF(ptr))ta_memdup(ta_parent, ptr, sizeof(*(ptr)))
+
+// Ugly macros that crash on OOM.
+// All of these mirror real functions (with a 'x' added after the 'ta_'
+// prefix), and the only difference is that they will call abort() on allocation
+// failures (such as out of memory conditions), instead of returning an error
+// code.
+#define ta_xalloc_size(...) ta_oom_p(ta_alloc_size(__VA_ARGS__))
+#define ta_xzalloc_size(...) ta_oom_p(ta_zalloc_size(__VA_ARGS__))
+#define ta_xnew_context(...) ta_oom_p(ta_new_context(__VA_ARGS__))
+#define ta_xstrdup_append(...) ta_oom_b(ta_strdup_append(__VA_ARGS__))
+#define ta_xstrdup_append_buffer(...) ta_oom_b(ta_strdup_append_buffer(__VA_ARGS__))
+#define ta_xstrndup_append(...) ta_oom_b(ta_strndup_append(__VA_ARGS__))
+#define ta_xstrndup_append_buffer(...) ta_oom_b(ta_strndup_append_buffer(__VA_ARGS__))
+#define ta_xasprintf(...) ta_oom_s(ta_asprintf(__VA_ARGS__))
+#define ta_xvasprintf(...) ta_oom_s(ta_vasprintf(__VA_ARGS__))
+#define ta_xasprintf_append(...) ta_oom_b(ta_asprintf_append(__VA_ARGS__))
+#define ta_xvasprintf_append(...) ta_oom_b(ta_vasprintf_append(__VA_ARGS__))
+#define ta_xasprintf_append_buffer(...) ta_oom_b(ta_asprintf_append_buffer(__VA_ARGS__))
+#define ta_xvasprintf_append_buffer(...) ta_oom_b(ta_vasprintf_append_buffer(__VA_ARGS__))
+#define ta_xnew(...) ta_oom_g(ta_new(__VA_ARGS__))
+#define ta_xznew(...) ta_oom_g(ta_znew(__VA_ARGS__))
+#define ta_xnew_array(...) ta_oom_g(ta_new_array(__VA_ARGS__))
+#define ta_xznew_array(...) ta_oom_g(ta_znew_array(__VA_ARGS__))
+#define ta_xnew_array_size(...) ta_oom_p(ta_new_array_size(__VA_ARGS__))
+#define ta_xnew_ptrtype(...) ta_oom_g(ta_new_ptrtype(__VA_ARGS__))
+#define ta_xnew_array_ptrtype(...) ta_oom_g(ta_new_array_ptrtype(__VA_ARGS__))
+#define ta_xdup(...) ta_oom_g(ta_dup(__VA_ARGS__))
+
+#define ta_xrealloc(ta_parent, ptr, type, count) \
+ (type *)ta_xrealloc_size(ta_parent, ptr, ta_calc_array_size(sizeof(type), count))
+
+// Can't be macros, because the OOM logic is slightly less trivial.
+char *ta_xstrdup(void *ta_parent, const char *str);
+char *ta_xstrndup(void *ta_parent, const char *str, size_t n);
+void *ta_xmemdup(void *ta_parent, void *ptr, size_t size);
+void *ta_xrealloc_size(void *ta_parent, void *ptr, size_t size);
+
+#ifndef TA_NO_WRAPPERS
+#define ta_alloc_size(...) ta_dbg_set_loc(ta_alloc_size(__VA_ARGS__), TA_LOC)
+#define ta_zalloc_size(...) ta_dbg_set_loc(ta_zalloc_size(__VA_ARGS__), TA_LOC)
+#define ta_realloc_size(...) ta_dbg_set_loc(ta_realloc_size(__VA_ARGS__), TA_LOC)
+#define ta_memdup(...) ta_dbg_set_loc(ta_memdup(__VA_ARGS__), TA_LOC)
+#define ta_xmemdup(...) ta_dbg_set_loc(ta_xmemdup(__VA_ARGS__), TA_LOC)
+#define ta_xrealloc_size(...) ta_dbg_set_loc(ta_xrealloc_size(__VA_ARGS__), TA_LOC)
+#endif
+
+void ta_oom_b(bool b);
+char *ta_oom_s(char *s);
+void *ta_oom_p(void *p);
+// Generic pointer
+#define ta_oom_g(ptr) (TA_TYPEOF(ptr))ta_oom_p(ptr)
+
+void ta_enable_leak_report(void);
+void *ta_dbg_set_loc(void *ptr, const char *name);
+void *ta_dbg_mark_as_string(void *ptr);
+
+#endif
diff --git a/ta/ta_talloc.c b/ta/ta_talloc.c
new file mode 100644
index 0000000..27dca22
--- /dev/null
+++ b/ta/ta_talloc.c
@@ -0,0 +1,77 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <assert.h>
+
+#include "ta_talloc.h"
+
+char *ta_talloc_strdup_append(char *s, const char *a)
+{
+ ta_xstrdup_append(&s, a);
+ return s;
+}
+
+char *ta_talloc_strdup_append_buffer(char *s, const char *a)
+{
+ ta_xstrdup_append_buffer(&s, a);
+ return s;
+}
+
+char *ta_talloc_strndup_append(char *s, const char *a, size_t n)
+{
+ ta_xstrndup_append(&s, a, n);
+ return s;
+}
+
+char *ta_talloc_strndup_append_buffer(char *s, const char *a, size_t n)
+{
+ ta_xstrndup_append_buffer(&s, a, n);
+ return s;
+}
+
+char *ta_talloc_vasprintf_append(char *s, const char *fmt, va_list ap)
+{
+ ta_xvasprintf_append(&s, fmt, ap);
+ return s;
+}
+
+char *ta_talloc_vasprintf_append_buffer(char *s, const char *fmt, va_list ap)
+{
+ ta_xvasprintf_append_buffer(&s, fmt, ap);
+ return s;
+}
+
+char *ta_talloc_asprintf_append(char *s, const char *fmt, ...)
+{
+ char *res;
+ va_list ap;
+ va_start(ap, fmt);
+ res = talloc_vasprintf_append(s, fmt, ap);
+ va_end(ap);
+ return res;
+}
+
+char *ta_talloc_asprintf_append_buffer(char *s, const char *fmt, ...)
+{
+ char *res;
+ va_list ap;
+ va_start(ap, fmt);
+ res = talloc_vasprintf_append_buffer(s, fmt, ap);
+ va_end(ap);
+ return res;
+}
diff --git a/ta/ta_talloc.h b/ta/ta_talloc.h
new file mode 100644
index 0000000..cacc72e
--- /dev/null
+++ b/ta/ta_talloc.h
@@ -0,0 +1,157 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef TA_TALLOC_H_
+#define TA_TALLOC_H_
+
+#include <string.h>
+
+#include "ta.h"
+
+// Note: all talloc wrappers are wired to the "x" functions, which abort on OOM.
+// libtalloc doesn't do that, but the mplayer2 internal copy of it did.
+
+#define talloc ta_xnew
+#define talloc_zero ta_xznew
+
+#define talloc_array ta_xnew_array
+#define talloc_zero_array ta_xznew_array
+
+#define talloc_array_size ta_xnew_array_size
+#define talloc_realloc ta_xrealloc
+#define talloc_ptrtype ta_xnew_ptrtype
+#define talloc_array_ptrtype ta_xnew_array_ptrtype
+
+#define talloc_steal ta_steal
+#define talloc_realloc_size ta_xrealloc_size
+#define talloc_new ta_xnew_context
+#define talloc_set_destructor ta_set_destructor
+#define talloc_enable_leak_report ta_enable_leak_report
+#define talloc_size ta_xalloc_size
+#define talloc_zero_size ta_xzalloc_size
+#define talloc_get_size ta_get_size
+#define talloc_free_children ta_free_children
+#define talloc_free ta_free
+#define talloc_dup ta_xdup
+#define talloc_memdup ta_xmemdup
+#define talloc_strdup ta_xstrdup
+#define talloc_strndup ta_xstrndup
+#define talloc_asprintf ta_xasprintf
+#define talloc_vasprintf ta_xvasprintf
+
+// Don't define linker-level symbols, as that would clash with real libtalloc.
+#define talloc_strdup_append ta_talloc_strdup_append
+#define talloc_strdup_append_buffer ta_talloc_strdup_append_buffer
+#define talloc_strndup_append ta_talloc_strndup_append
+#define talloc_strndup_append_buffer ta_talloc_strndup_append_buffer
+#define talloc_vasprintf_append ta_talloc_vasprintf_append
+#define talloc_vasprintf_append_buffer ta_talloc_vasprintf_append_buffer
+#define talloc_asprintf_append ta_talloc_asprintf_append
+#define talloc_asprintf_append_buffer ta_talloc_asprintf_append_buffer
+
+char *ta_talloc_strdup(void *t, const char *p);
+char *ta_talloc_strdup_append(char *s, const char *a);
+char *ta_talloc_strdup_append_buffer(char *s, const char *a);
+
+char *ta_talloc_strndup(void *t, const char *p, size_t n);
+char *ta_talloc_strndup_append(char *s, const char *a, size_t n);
+char *ta_talloc_strndup_append_buffer(char *s, const char *a, size_t n);
+
+char *ta_talloc_vasprintf_append(char *s, const char *fmt, va_list ap) TA_PRF(2, 0);
+char *ta_talloc_vasprintf_append_buffer(char *s, const char *fmt, va_list ap) TA_PRF(2, 0);
+
+char *ta_talloc_asprintf_append(char *s, const char *fmt, ...) TA_PRF(2, 3);
+char *ta_talloc_asprintf_append_buffer(char *s, const char *fmt, ...) TA_PRF(2, 3);
+
+// mpv specific stuff - should be made part of proper TA API
+
+#define TA_FREEP(pctx) do {talloc_free(*(pctx)); *(pctx) = NULL;} while(0)
+
+// Return number of allocated entries in typed array p[].
+#define MP_TALLOC_AVAIL(p) (talloc_get_size(p) / sizeof((p)[0]))
+
+// Resize array p so that p[count-1] is the last valid entry. ctx as ta parent.
+#define MP_RESIZE_ARRAY(ctx, p, count) \
+ do { \
+ (p) = ta_xrealloc_size(ctx, p, \
+ ta_calc_array_size(sizeof((p)[0]), count)); \
+ } while (0)
+
+// Resize array p so that p[nextidx] is accessible. Preallocate additional
+// space to make appending more efficient, never shrink. ctx as ta parent.
+#define MP_TARRAY_GROW(ctx, p, nextidx) \
+ do { \
+ size_t nextidx_ = (nextidx); \
+ if (nextidx_ >= MP_TALLOC_AVAIL(p)) \
+ MP_RESIZE_ARRAY(ctx, p, ta_calc_prealloc_elems(nextidx_)); \
+ } while (0)
+
+// Append the last argument to array p (with count idxvar), basically:
+// p[idxvar++] = ...; ctx as ta parent.
+#define MP_TARRAY_APPEND(ctx, p, idxvar, ...) \
+ do { \
+ MP_TARRAY_GROW(ctx, p, idxvar); \
+ (p)[(idxvar)] = (__VA_ARGS__); \
+ (idxvar)++; \
+ } while (0)
+
+// Insert the last argument at p[at] (array p with count idxvar), basically:
+// for(idxvar-1 down to at) p[n+1] = p[n]; p[at] = ...; idxvar++;
+// ctx as ta parent. Required: at >= 0 && at <= idxvar.
+#define MP_TARRAY_INSERT_AT(ctx, p, idxvar, at, ...)\
+ do { \
+ size_t at_ = (at); \
+ assert(at_ <= (idxvar)); \
+ MP_TARRAY_GROW(ctx, p, idxvar); \
+ memmove((p) + at_ + 1, (p) + at_, \
+ ((idxvar) - at_) * sizeof((p)[0])); \
+ (idxvar)++; \
+ (p)[at_] = (__VA_ARGS__); \
+ } while (0)
+
+// Given an array p with count idxvar, insert c elements at p[at], so that
+// p[at] to p[at+c-1] can be accessed. The elements at p[at] and following
+// are shifted up by c before insertion. The new entries are uninitialized.
+// ctx as ta parent. Required: at >= 0 && at <= idxvar.
+#define MP_TARRAY_INSERT_N_AT(ctx, p, idxvar, at, c)\
+ do { \
+ size_t at_ = (at); \
+ assert(at_ <= (idxvar)); \
+ size_t c_ = (c); \
+ MP_TARRAY_GROW(ctx, p, (idxvar) + c_); \
+ memmove((p) + at_ + c_, (p) + at_, \
+ ((idxvar) - at_) * sizeof((p)[0])); \
+ (idxvar) += c_; \
+ } while (0)
+
+// Remove p[at] from array p with count idxvar (inverse of MP_TARRAY_INSERT_AT()).
+// Doesn't actually free any memory, or do any other talloc calls.
+#define MP_TARRAY_REMOVE_AT(p, idxvar, at) \
+ do { \
+ size_t at_ = (at); \
+ assert(at_ <= (idxvar)); \
+ memmove((p) + at_, (p) + at_ + 1, \
+ ((idxvar) - at_ - 1) * sizeof((p)[0])); \
+ (idxvar)--; \
+ } while (0)
+
+// Returns whether or not there was any element to pop.
+#define MP_TARRAY_POP(p, idxvar, out) \
+ ((idxvar) > 0 \
+ ? (*(out) = (p)[--(idxvar)], true) \
+ : false \
+ )
+
+#endif
diff --git a/ta/ta_utils.c b/ta/ta_utils.c
new file mode 100644
index 0000000..6246968
--- /dev/null
+++ b/ta/ta_utils.c
@@ -0,0 +1,315 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <assert.h>
+#include "osdep/strnlen.h"
+
+#define TA_NO_WRAPPERS
+#include "ta.h"
+
+// Return element_size * count. If it overflows, return (size_t)-1 (SIZE_MAX).
+// I.e. this returns the equivalent of: MIN(element_size * count, SIZE_MAX).
+// The idea is that every real memory allocator will reject (size_t)-1, thus
+// this is a valid way to handle too large array allocation requests.
+size_t ta_calc_array_size(size_t element_size, size_t count)
+{
+ if (count > (((size_t)-1) / element_size))
+ return (size_t)-1;
+ return element_size * count;
+}
+
+// This is used when an array has to be enlarged for appending new elements.
+// Return a "good" size for the new array (in number of elements). This returns
+// a value > nextidx, unless the calculation overflows, in which case SIZE_MAX
+// is returned.
+size_t ta_calc_prealloc_elems(size_t nextidx)
+{
+ if (nextidx >= ((size_t)-1) / 2 - 1)
+ return (size_t)-1;
+ return (nextidx + 1) * 2;
+}
+
+/* Create an empty (size 0) TA allocation.
+ */
+void *ta_new_context(void *ta_parent)
+{
+ return ta_alloc_size(ta_parent, 0);
+}
+
+/* Set parent of ptr to ta_parent, return the ptr.
+ * Note that ta_parent==NULL will simply unset the current parent of ptr.
+ */
+void *ta_steal_(void *ta_parent, void *ptr)
+{
+ ta_set_parent(ptr, ta_parent);
+ return ptr;
+}
+
+/* Duplicate the memory at ptr with the given size.
+ */
+void *ta_memdup(void *ta_parent, void *ptr, size_t size)
+{
+ if (!ptr) {
+ assert(!size);
+ return NULL;
+ }
+ void *res = ta_alloc_size(ta_parent, size);
+ if (!res)
+ return NULL;
+ memcpy(res, ptr, size);
+ return res;
+}
+
+// *str = *str[0..at] + append[0..append_len]
+// (append_len being a maximum length; shorter if embedded \0s are encountered)
+static bool strndup_append_at(char **str, size_t at, const char *append,
+ size_t append_len)
+{
+ assert(ta_get_size(*str) >= at);
+
+ if (!*str && !append)
+ return true; // stays NULL, but not an OOM condition
+
+ size_t real_len = append ? strnlen(append, append_len) : 0;
+ if (append_len > real_len)
+ append_len = real_len;
+
+ if (ta_get_size(*str) < at + append_len + 1) {
+ char *t = ta_realloc_size(NULL, *str, at + append_len + 1);
+ if (!t)
+ return false;
+ *str = t;
+ }
+
+ if (append_len)
+ memcpy(*str + at, append, append_len);
+
+ (*str)[at + append_len] = '\0';
+
+ ta_dbg_mark_as_string(*str);
+
+ return true;
+}
+
+/* Return a copy of str.
+ * Returns NULL on OOM.
+ */
+char *ta_strdup(void *ta_parent, const char *str)
+{
+ return ta_strndup(ta_parent, str, str ? strlen(str) : 0);
+}
+
+/* Return a copy of str. If the string is longer than n, copy only n characters
+ * (the returned allocation will be n+1 bytes and contain a terminating '\0').
+ * The returned string will have the length MIN(strlen(str), n)
+ * If str==NULL, return NULL. Returns NULL on OOM as well.
+ */
+char *ta_strndup(void *ta_parent, const char *str, size_t n)
+{
+ if (!str)
+ return NULL;
+ char *new = NULL;
+ strndup_append_at(&new, 0, str, n);
+ ta_set_parent(new, ta_parent);
+ return new;
+}
+
+/* Append a to *str. If *str is NULL, the string is newly allocated, otherwise
+ * ta_realloc() is used on *str as needed.
+ * Return success or failure (it can fail due to OOM only).
+ */
+bool ta_strdup_append(char **str, const char *a)
+{
+ return strndup_append_at(str, *str ? strlen(*str) : 0, a, (size_t)-1);
+}
+
+/* Like ta_strdup_append(), but use ta_get_size(*str)-1 instead of strlen(*str).
+ * (See also: ta_asprintf_append_buffer())
+ */
+bool ta_strdup_append_buffer(char **str, const char *a)
+{
+ size_t size = ta_get_size(*str);
+ if (size > 0)
+ size -= 1;
+ return strndup_append_at(str, size, a, (size_t)-1);
+}
+
+/* Like ta_strdup_append(), but limit the length of a with n.
+ * (See also: ta_strndup())
+ */
+bool ta_strndup_append(char **str, const char *a, size_t n)
+{
+ return strndup_append_at(str, *str ? strlen(*str) : 0, a, n);
+}
+
+/* Like ta_strdup_append_buffer(), but limit the length of a with n.
+ * (See also: ta_strndup())
+ */
+bool ta_strndup_append_buffer(char **str, const char *a, size_t n)
+{
+ size_t size = ta_get_size(*str);
+ if (size > 0)
+ size -= 1;
+ return strndup_append_at(str, size, a, n);
+}
+
+static bool ta_vasprintf_append_at(char **str, size_t at, const char *fmt,
+ va_list ap)
+{
+ assert(ta_get_size(*str) >= at);
+
+ int size;
+ va_list copy;
+ va_copy(copy, ap);
+ char c;
+ size = vsnprintf(&c, 1, fmt, copy);
+ va_end(copy);
+
+ if (size < 0)
+ return false;
+
+ if (ta_get_size(*str) < at + size + 1) {
+ char *t = ta_realloc_size(NULL, *str, at + size + 1);
+ if (!t)
+ return false;
+ *str = t;
+ }
+ vsnprintf(*str + at, size + 1, fmt, ap);
+
+ ta_dbg_mark_as_string(*str);
+
+ return true;
+}
+
+/* Like snprintf(); returns the formatted string as allocation (or NULL on OOM
+ * or snprintf() errors).
+ */
+char *ta_asprintf(void *ta_parent, const char *fmt, ...)
+{
+ char *res;
+ va_list ap;
+ va_start(ap, fmt);
+ res = ta_vasprintf(ta_parent, fmt, ap);
+ va_end(ap);
+ return res;
+}
+
+char *ta_vasprintf(void *ta_parent, const char *fmt, va_list ap)
+{
+ char *res = NULL;
+ ta_vasprintf_append_at(&res, 0, fmt, ap);
+ ta_set_parent(res, ta_parent);
+ if (!res) {
+ ta_free(res);
+ return NULL;
+ }
+ return res;
+}
+
+/* Append the formatted string to *str (after strlen(*str)). The allocation is
+ * ta_realloced if needed.
+ * Returns false on OOM or snprintf() errors, with *str left untouched.
+ */
+bool ta_asprintf_append(char **str, const char *fmt, ...)
+{
+ bool res;
+ va_list ap;
+ va_start(ap, fmt);
+ res = ta_vasprintf_append(str, fmt, ap);
+ va_end(ap);
+ return res;
+}
+
+bool ta_vasprintf_append(char **str, const char *fmt, va_list ap)
+{
+ return ta_vasprintf_append_at(str, *str ? strlen(*str) : 0, fmt, ap);
+}
+
+/* Append the formatted string at the end of the allocation of *str. It
+ * overwrites the last byte of the allocation too (which is assumed to be the
+ * '\0' terminating the string). Compared to ta_asprintf_append(), this is
+ * useful if you know that the string ends with the allocation, so that the
+ * extra strlen() can be avoided for better performance.
+ * Returns false on OOM or snprintf() errors, with *str left untouched.
+ */
+bool ta_asprintf_append_buffer(char **str, const char *fmt, ...)
+{
+ bool res;
+ va_list ap;
+ va_start(ap, fmt);
+ res = ta_vasprintf_append_buffer(str, fmt, ap);
+ va_end(ap);
+ return res;
+}
+
+bool ta_vasprintf_append_buffer(char **str, const char *fmt, va_list ap)
+{
+ size_t size = ta_get_size(*str);
+ if (size > 0)
+ size -= 1;
+ return ta_vasprintf_append_at(str, size, fmt, ap);
+}
+
+
+void *ta_oom_p(void *p)
+{
+ if (!p)
+ abort();
+ return p;
+}
+
+void ta_oom_b(bool b)
+{
+ if (!b)
+ abort();
+}
+
+char *ta_oom_s(char *s)
+{
+ if (!s)
+ abort();
+ return s;
+}
+
+void *ta_xmemdup(void *ta_parent, void *ptr, size_t size)
+{
+ void *new = ta_memdup(ta_parent, ptr, size);
+ ta_oom_b(new || !ptr);
+ return new;
+}
+
+void *ta_xrealloc_size(void *ta_parent, void *ptr, size_t size)
+{
+ ptr = ta_realloc_size(ta_parent, ptr, size);
+ ta_oom_b(ptr || !size);
+ return ptr;
+}
+
+char *ta_xstrdup(void *ta_parent, const char *str)
+{
+ char *res = ta_strdup(ta_parent, str);
+ ta_oom_b(res || !str);
+ return res;
+}
+
+char *ta_xstrndup(void *ta_parent, const char *str, size_t n)
+{
+ char *res = ta_strndup(ta_parent, str, n);
+ ta_oom_b(res || !str);
+ return res;
+}
diff --git a/test/chmap.c b/test/chmap.c
new file mode 100644
index 0000000..48af822
--- /dev/null
+++ b/test/chmap.c
@@ -0,0 +1,218 @@
+#include "audio/chmap.h"
+#include "audio/chmap_sel.h"
+#include "config.h"
+#include "test_utils.h"
+
+#if HAVE_AV_CHANNEL_LAYOUT
+#include "audio/chmap_avchannel.h"
+#endif
+
+#define LAYOUTS(...) (char*[]){__VA_ARGS__, NULL}
+
+static void test_sel(const char *input, const char *expected_selection,
+ char **layouts)
+{
+ struct mp_chmap_sel s = {0};
+ struct mp_chmap input_map;
+ struct mp_chmap expected_map;
+
+ assert_true(mp_chmap_from_str(&input_map, bstr0(input)));
+ assert_true(mp_chmap_from_str(&expected_map, bstr0(expected_selection)));
+
+ for (int n = 0; layouts[n]; n++) {
+ struct mp_chmap tmp;
+ assert_true(mp_chmap_from_str(&tmp, bstr0(layouts[n])));
+ int count = s.num_chmaps;
+ mp_chmap_sel_add_map(&s, &tmp);
+ assert_true(s.num_chmaps > count); // assure validity and max. count
+ }
+
+ assert_true(mp_chmap_sel_fallback(&s, &input_map));
+ // We convert expected_map to a chmap and then back to a string to avoid
+ // problems with ambiguous layouts.
+ assert_string_equal(mp_chmap_to_str(&input_map),
+ mp_chmap_to_str(&expected_map));
+}
+
+#if HAVE_AV_CHANNEL_LAYOUT
+static bool layout_matches(const AVChannelLayout *av_layout,
+ const struct mp_chmap *mp_layout,
+ bool require_default_unspec)
+{
+ if (!mp_chmap_is_valid(mp_layout) ||
+ !av_channel_layout_check(av_layout) ||
+ av_layout->nb_channels != mp_layout->num ||
+ mp_layout->num > MP_NUM_CHANNELS)
+ return false;
+
+ switch (av_layout->order) {
+ case AV_CHANNEL_ORDER_UNSPEC:
+ {
+ if (!require_default_unspec)
+ return true;
+
+ // mp_chmap essentially does not have a concept of "unspecified"
+ // so we check if the mp layout matches the default layout for such
+ // channel count.
+ struct mp_chmap default_layout = { 0 };
+ mp_chmap_from_channels(&default_layout, mp_layout->num);
+ return mp_chmap_equals(mp_layout, &default_layout);
+ }
+ case AV_CHANNEL_ORDER_NATIVE:
+ return av_layout->u.mask == mp_chmap_to_lavc(mp_layout);
+ default:
+ // TODO: handle custom layouts
+ return false;
+ }
+
+ return true;
+}
+
+static void test_mp_chmap_to_av_channel_layout(void)
+{
+ mp_ch_layout_tuple *mapping_array = NULL;
+ void *iter = NULL;
+ bool anything_failed = false;
+
+ printf("Testing mp_chmap -> AVChannelLayout conversions\n");
+
+ while ((mapping_array = mp_iterate_builtin_layouts(&iter))) {
+ const char *mapping_name = (*mapping_array)[0];
+ const char *mapping_str = (*mapping_array)[1];
+ struct mp_chmap mp_layout = { 0 };
+ AVChannelLayout av_layout = { 0 };
+ char layout_desc[128] = {0};
+
+ assert_true(mp_chmap_from_str(&mp_layout, bstr0(mapping_str)));
+
+ mp_chmap_to_av_layout(&av_layout, &mp_layout);
+
+ assert_false(av_channel_layout_describe(&av_layout,
+ layout_desc, 128) < 0);
+
+ bool success =
+ (strcmp(layout_desc, mp_chmap_to_str(&mp_layout)) == 0 ||
+ layout_matches(&av_layout, &mp_layout, false));
+ if (!success)
+ anything_failed = true;
+
+ printf("%s: %s (%s) -> %s\n",
+ success ? "Success" : "Failure",
+ mapping_str, mapping_name, layout_desc);
+
+ av_channel_layout_uninit(&av_layout);
+ }
+
+ assert_false(anything_failed);
+}
+
+static void test_av_channel_layout_to_mp_chmap(void)
+{
+ const AVChannelLayout *av_layout = NULL;
+ void *iter = NULL;
+ bool anything_failed = false;
+
+ printf("Testing AVChannelLayout -> mp_chmap conversions\n");
+
+ while ((av_layout = av_channel_layout_standard(&iter))) {
+ struct mp_chmap mp_layout = { 0 };
+ char layout_desc[128] = {0};
+
+ assert_false(av_channel_layout_describe(av_layout,
+ layout_desc, 128) < 0);
+
+ bool ret = mp_chmap_from_av_layout(&mp_layout, av_layout);
+ if (!ret) {
+ bool too_many_channels =
+ av_layout->nb_channels > MP_NUM_CHANNELS;
+ printf("Conversion from '%s' to mp_chmap failed (%s)!\n",
+ layout_desc,
+ too_many_channels ?
+ "channel count was over max, ignoring" :
+ "unexpected, failing");
+
+ // we should for now only fail with things such as 22.2
+ // due to mp_chmap being currently limited to 16 channels
+ assert_true(too_many_channels);
+
+ continue;
+ }
+
+ bool success =
+ (strcmp(layout_desc, mp_chmap_to_str(&mp_layout)) == 0 ||
+ layout_matches(av_layout, &mp_layout, true));
+ if (!success)
+ anything_failed = true;
+
+ printf("%s: %s -> %s\n",
+ success ? "Success" : "Failure",
+ layout_desc, mp_chmap_to_str(&mp_layout));
+ }
+
+ assert_false(anything_failed);
+}
+#endif
+
+
+int main(void)
+{
+ struct mp_chmap a;
+ struct mp_chmap b;
+ struct mp_chmap_sel s = {0};
+
+ test_sel("5.1", "7.1", LAYOUTS("7.1"));
+ test_sel("7.1", "5.1", LAYOUTS("5.1"));
+ test_sel("7.1(wide-side)", "7.1", LAYOUTS("7.1"));
+ test_sel("7.1(wide-side)", "5.1(side)", LAYOUTS("7.1", "5.1(side)"));
+ test_sel("3.1", "5.1", LAYOUTS("7.1", "5.1", "2.1", "stereo", "mono"));
+ test_sel("5.1", "7.1(rear)", LAYOUTS("7.1(rear)"));
+ test_sel("5.1(side)", "5.1", LAYOUTS("5.1", "7.1"));
+ test_sel("5.1", "7.1(alsa)", LAYOUTS("7.1(alsa)"));
+ test_sel("mono", "stereo", LAYOUTS("stereo", "5.1"));
+ test_sel("stereo", "stereo", LAYOUTS("stereo", "5.1"));
+ test_sel("5.1(side)", "7.1(rear)", LAYOUTS("stereo", "7.1(rear)"));
+ test_sel("7.1", "fl-fr-lfe-fc-bl-br-flc-frc",
+ LAYOUTS("fl-fr-lfe-fc-bl-br-flc-frc", "3.0(back)"));
+
+ mp_chmap_set_unknown(&a, 2);
+
+ mp_chmap_from_str(&b, bstr0("5.1"));
+
+ mp_chmap_sel_add_map(&s, &a);
+ assert_false(mp_chmap_sel_fallback(&s, &b));
+ assert_string_equal(mp_chmap_to_str(&b), "5.1");
+
+ test_sel("quad", "quad(side)", LAYOUTS("quad(side)", "stereo"));
+ test_sel("quad", "quad(side)", LAYOUTS("quad(side)", "7.0"));
+ test_sel("quad", "quad(side)", LAYOUTS("7.0", "quad(side)"));
+ test_sel("quad", "7.1(wide-side)", LAYOUTS("7.1(wide-side)", "stereo"));
+ test_sel("quad", "7.1(wide-side)", LAYOUTS("stereo", "7.1(wide-side)"));
+ test_sel("quad", "fl-fr-sl-sr",
+ LAYOUTS("fl-fr-fc-bl-br", "fl-fr-sl-sr"));
+ test_sel("quad", "fl-fr-bl-br-na-na-na-na",
+ LAYOUTS("fl-fr-bl-br-na-na-na-na", "quad(side)", "stereo"));
+ test_sel("quad", "fl-fr-bl-br-na-na-na-na",
+ LAYOUTS("stereo", "quad(side)", "fl-fr-bl-br-na-na-na-na"));
+ test_sel("fl-fr-fc-lfe-sl-sr", "fl-fr-lfe-fc-bl-br-na-na",
+ LAYOUTS("fl-fr-lfe-fc-bl-br-na-na", "fl-fr-lfe-fc-bl-br-sdl-sdr"));
+ test_sel("fl-fr-fc-lfe-sl-sr", "fl-fr-lfe-fc-bl-br-na-na",
+ LAYOUTS("fl-fr-lfe-fc-bl-br-sdl-sdr", "fl-fr-lfe-fc-bl-br-na-na"));
+
+ test_sel("na-fl-fr", "na-fl-fr", LAYOUTS("na-fl-fr-na", "fl-na-fr", "na-fl-fr",
+ "fl-fr-na-na", "na-na-fl-fr"));
+
+ mp_chmap_from_str(&a, bstr0("3.1"));
+ mp_chmap_from_str(&b, bstr0("2.1"));
+
+ assert_int_equal(mp_chmap_diffn(&a, &b), 1);
+
+ mp_chmap_from_str(&b, bstr0("6.1(back)"));
+ assert_int_equal(mp_chmap_diffn(&a, &b), 0);
+ assert_int_equal(mp_chmap_diffn(&b, &a), 3);
+
+#if HAVE_AV_CHANNEL_LAYOUT
+ test_av_channel_layout_to_mp_chmap();
+ test_mp_chmap_to_av_channel_layout();
+#endif
+ return 0;
+}
diff --git a/test/gl_video.c b/test/gl_video.c
new file mode 100644
index 0000000..a2bdda4
--- /dev/null
+++ b/test/gl_video.c
@@ -0,0 +1,25 @@
+#include "test_utils.h"
+#include "video/out/gpu/utils.h"
+
+int main(void)
+{
+ float x;
+
+ x = gl_video_scale_ambient_lux(16.0, 64.0, 2.40, 1.961, 16.0);
+ assert_float_equal(x, 2.40f, FLT_EPSILON);
+
+ x = gl_video_scale_ambient_lux(16.0, 64.0, 2.40, 1.961, 64.0);
+ assert_float_equal(x, 1.961f, FLT_EPSILON);
+
+ x = gl_video_scale_ambient_lux(16.0, 64.0, 1.961, 2.40, 64.0);
+ assert_float_equal(x, 2.40f, FLT_EPSILON);
+
+ x = gl_video_scale_ambient_lux(16.0, 64.0, 2.40, 1.961, 0.0);
+ assert_float_equal(x, 2.40f, FLT_EPSILON);
+
+ // 32 corresponds to the midpoint after converting lux to the log10 scale
+ x = gl_video_scale_ambient_lux(16.0, 64.0, 2.40, 1.961, 32.0);
+ float mid_gamma = (2.40 - 1.961) / 2 + 1.961;
+ assert_float_equal(x, mid_gamma, FLT_EPSILON);
+ return 0;
+}
diff --git a/test/img_format.c b/test/img_format.c
new file mode 100644
index 0000000..3cc8ff5
--- /dev/null
+++ b/test/img_format.c
@@ -0,0 +1,217 @@
+#include <libavutil/frame.h>
+#include <libavutil/pixdesc.h>
+
+#include "img_utils.h"
+#include "options/path.h"
+#include "test_utils.h"
+#include "video/fmt-conversion.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+
+static enum AVPixelFormat pixfmt_unsup[100];
+static int num_pixfmt_unsup;
+
+static const char *comp_type(enum mp_component_type type)
+{
+ switch (type) {
+ case MP_COMPONENT_TYPE_UINT: return "uint";
+ case MP_COMPONENT_TYPE_FLOAT: return "float";
+ default: return "unknown";
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ init_imgfmts_list();
+ const char *refdir = argv[1];
+ const char *outdir = argv[2];
+
+ FILE *f = test_open_out(outdir, "img_formats.txt");
+
+ for (int z = 0; z < num_imgfmts; z++) {
+ int mpfmt = imgfmts[z];
+ enum AVPixelFormat pixfmt = imgfmt2pixfmt(mpfmt);
+ const AVPixFmtDescriptor *avd = av_pix_fmt_desc_get(pixfmt);
+
+ fprintf(f, "%s: ", mp_imgfmt_to_name(mpfmt));
+ if (mpfmt >= IMGFMT_AVPIXFMT_START && mpfmt < IMGFMT_AVPIXFMT_END)
+ fprintf(f, "[GENERIC] ");
+
+ int fcsp = mp_imgfmt_get_forced_csp(mpfmt);
+ if (fcsp)
+ fprintf(f, "fcsp=%s ", m_opt_choice_str(mp_csp_names, fcsp));
+ fprintf(f, "ctype=%s\n", comp_type(mp_imgfmt_get_component_type(mpfmt)));
+
+ struct mp_imgfmt_desc d = mp_imgfmt_get_desc(mpfmt);
+ if (d.id) {
+ fprintf(f, " Basic desc: ");
+ #define FLAG(t, c) if (d.flags & (t)) fprintf(f, "[%s]", c);
+ FLAG(MP_IMGFLAG_BYTE_ALIGNED, "ba")
+ FLAG(MP_IMGFLAG_BYTES, "bb")
+ FLAG(MP_IMGFLAG_ALPHA, "a")
+ FLAG(MP_IMGFLAG_YUV_P, "yuvp")
+ FLAG(MP_IMGFLAG_YUV_NV, "nv")
+ FLAG(MP_IMGFLAG_COLOR_YUV, "yuv")
+ FLAG(MP_IMGFLAG_COLOR_RGB, "rgb")
+ FLAG(MP_IMGFLAG_COLOR_XYZ, "xyz")
+ FLAG(MP_IMGFLAG_GRAY, "gray")
+ FLAG(MP_IMGFLAG_LE, "le")
+ FLAG(MP_IMGFLAG_BE, "be")
+ FLAG(MP_IMGFLAG_TYPE_PAL8, "pal")
+ FLAG(MP_IMGFLAG_TYPE_HW, "hw")
+ FLAG(MP_IMGFLAG_TYPE_FLOAT, "float")
+ FLAG(MP_IMGFLAG_TYPE_UINT, "uint")
+ fprintf(f, "\n");
+ fprintf(f, " planes=%d, chroma=%d:%d align=%d:%d\n",
+ d.num_planes, d.chroma_xs, d.chroma_ys, d.align_x, d.align_y);
+ fprintf(f, " {");
+ for (int n = 0; n < MP_MAX_PLANES; n++) {
+ if (n >= d.num_planes) {
+ assert(d.bpp[n] == 0 && d.xs[n] == 0 && d.ys[n] == 0);
+ continue;
+ }
+ fprintf(f, "%d/[%d:%d] ", d.bpp[n], d.xs[n], d.ys[n]);
+ }
+ fprintf(f, "}\n");
+ } else {
+ fprintf(f, " [NODESC]\n");
+ }
+
+ for (int n = 0; n < d.num_planes; n++) {
+ fprintf(f, " %d: %dbits", n, d.bpp[n]);
+ if (d.endian_shift)
+ fprintf(f, " endian_bytes=%d", 1 << d.endian_shift);
+ for (int x = 0; x < MP_NUM_COMPONENTS; x++) {
+ struct mp_imgfmt_comp_desc cm = d.comps[x];
+ fprintf(f, " {");
+ if (cm.plane == n) {
+ if (cm.size) {
+ fprintf(f, "%d:%d", cm.offset, cm.size);
+ if (cm.pad)
+ fprintf(f, "/%d", cm.pad);
+ } else {
+ assert(cm.offset == 0);
+ assert(cm.pad == 0);
+ }
+ }
+ fprintf(f, "}");
+ if (!(d.flags & (MP_IMGFLAG_PACKED_SS_YUV | MP_IMGFLAG_HAS_COMPS)))
+ {
+ assert(cm.size == 0);
+ assert(cm.offset == 0);
+ assert(cm.pad == 0);
+ }
+ }
+ fprintf(f, "\n");
+ if (d.flags & MP_IMGFLAG_PACKED_SS_YUV) {
+ assert(!(d.flags & MP_IMGFLAG_HAS_COMPS));
+ uint8_t offsets[10];
+ bool r = mp_imgfmt_get_packed_yuv_locations(mpfmt, offsets);
+ assert(r);
+ fprintf(f, " luma_offsets=[");
+ for (int x = 0; x < d.align_x; x++)
+ fprintf(f, " %d", offsets[x]);
+ fprintf(f, "]\n");
+ }
+ }
+
+ if (!(d.flags & MP_IMGFLAG_HWACCEL) && pixfmt != AV_PIX_FMT_NONE) {
+ AVFrame *fr = av_frame_alloc();
+ fr->format = pixfmt;
+ fr->width = 128;
+ fr->height = 128;
+ int err = av_frame_get_buffer(fr, MP_IMAGE_BYTE_ALIGN);
+ assert(err >= 0);
+ struct mp_image *mpi = mp_image_alloc(mpfmt, fr->width, fr->height);
+ if (mpi) {
+ // A rather fuzzy test, which might fail even if there's no bug.
+ for (int n = 0; n < 4; n++) {
+ if (!!mpi->planes[n] != !!fr->data[n]) {
+ #ifdef AV_PIX_FMT_FLAG_PSEUDOPAL
+ if (n == 1 && (avd->flags & AV_PIX_FMT_FLAG_PSEUDOPAL))
+ continue;
+ #endif
+ fprintf(f, " Warning: p%d: %p %p\n", n,
+ mpi->planes[n], fr->data[n]);
+ }
+ if (mpi->stride[n] != fr->linesize[n]) {
+ fprintf(f, " Warning: p%d: %d %d\n", n,
+ mpi->stride[n], fr->linesize[n]);
+ }
+ }
+ } else {
+ fprintf(f, " [NOALLOC]\n");
+ }
+ talloc_free(mpi);
+ av_frame_free(&fr);
+ }
+
+ struct mp_regular_imgfmt reg;
+ if (mp_get_regular_imgfmt(&reg, mpfmt)) {
+ fprintf(f, " Regular: planes=%d compbytes=%d bitpad=%d "
+ "chroma=%dx%d ctype=%s\n",
+ reg.num_planes, reg.component_size, reg.component_pad,
+ 1 << reg.chroma_xs, 1 << reg.chroma_ys,
+ comp_type(reg.component_type));
+ for (int n = 0; n < reg.num_planes; n++) {
+ struct mp_regular_imgfmt_plane *plane = &reg.planes[n];
+ fprintf(f, " %d: {", n);
+ for (int i = 0; i < plane->num_components; i++) {
+ if (i > 0)
+ fprintf(f, ", ");
+ fprintf(f, "%d", plane->components[i]);
+ }
+ fprintf(f, "}\n");
+ }
+ }
+
+ // This isn't ours, but changes likely affect us.
+ if (avd) {
+ fprintf(f, " AVD: name=%s chroma=%d:%d flags=0x%"PRIx64, avd->name,
+ avd->log2_chroma_w, avd->log2_chroma_h, avd->flags);
+ #define FLAGAV(t, c) if (avd->flags & (t)) \
+ {fprintf(f, "%s[%s]", pre, c); pre = ""; }
+ char *pre = " ";
+ FLAGAV(AV_PIX_FMT_FLAG_BE, "be")
+ FLAGAV(AV_PIX_FMT_FLAG_PAL, "pal")
+ FLAGAV(AV_PIX_FMT_FLAG_BITSTREAM, "bs")
+ FLAGAV(AV_PIX_FMT_FLAG_HWACCEL, "hw")
+ FLAGAV(AV_PIX_FMT_FLAG_PLANAR, "planar")
+ FLAGAV(AV_PIX_FMT_FLAG_RGB, "rgb")
+ FLAGAV(AV_PIX_FMT_FLAG_ALPHA, "alpha")
+ FLAGAV(AV_PIX_FMT_FLAG_BAYER, "bayer")
+ FLAGAV(AV_PIX_FMT_FLAG_FLOAT, "float")
+ fprintf(f, "\n");
+ for (int n = 0; n < avd->nb_components; n++) {
+ const AVComponentDescriptor *cd = &avd->comp[n];
+ fprintf(f, " %d: p=%-2d st=%-2d o=%-2d sh=%-2d d=%d\n",
+ n, cd->plane, cd->step, cd->offset, cd->shift, cd->depth);
+ }
+ for (int n = avd->nb_components; n < 4; n++) {
+ const AVComponentDescriptor *cd = &avd->comp[n];
+ assert(!cd->plane && !cd->step && !cd->offset && !cd->shift &&
+ !cd->depth);
+ }
+ }
+
+ const AVPixFmtDescriptor *avd2 = av_pix_fmt_desc_next(NULL);
+ for (; avd2; avd2 = av_pix_fmt_desc_next(avd2)) {
+ enum AVPixelFormat pixfmt2 = av_pix_fmt_desc_get_id(avd2);
+ int mpfmt2 = pixfmt2imgfmt(pixfmt2);
+ if (mpfmt2 == mpfmt && pixfmt2 != pixfmt)
+ fprintf(f, " Ambiguous alias: %s\n", avd2->name);
+ }
+ }
+
+ for (int z = 0; z < num_pixfmt_unsup; z++) {
+ const AVPixFmtDescriptor *avd = av_pix_fmt_desc_get(pixfmt_unsup[z]);
+ fprintf(f, "Unsupported: %s\n", avd->name);
+ }
+
+ fclose(f);
+
+ assert_text_files_equal(refdir, outdir, "img_formats.txt",
+ "This can fail if FFmpeg adds new formats or flags.");
+ return 0;
+}
diff --git a/test/img_utils.c b/test/img_utils.c
new file mode 100644
index 0000000..71764f3
--- /dev/null
+++ b/test/img_utils.c
@@ -0,0 +1,63 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <libavutil/frame.h>
+#include <libavutil/pixdesc.h>
+
+#include "common/common.h"
+#include "img_utils.h"
+#include "video/img_format.h"
+#include "video/fmt-conversion.h"
+
+int imgfmts[IMGFMT_AVPIXFMT_END - IMGFMT_AVPIXFMT_START + 100];
+int num_imgfmts;
+
+static enum AVPixelFormat pixfmt_unsup[100];
+static int num_pixfmt_unsup;
+
+static int cmp_imgfmt_name(const void *a, const void *b)
+{
+ char *name_a = mp_imgfmt_to_name(*(int *)a);
+ char *name_b = mp_imgfmt_to_name(*(int *)b);
+
+ return strcmp(name_a, name_b);
+}
+
+void init_imgfmts_list(void)
+{
+ const AVPixFmtDescriptor *avd = av_pix_fmt_desc_next(NULL);
+ for (; avd; avd = av_pix_fmt_desc_next(avd)) {
+ enum AVPixelFormat fmt = av_pix_fmt_desc_get_id(avd);
+ int mpfmt = pixfmt2imgfmt(fmt);
+ if (!mpfmt) {
+ assert(num_pixfmt_unsup < MP_ARRAY_SIZE(pixfmt_unsup));
+ pixfmt_unsup[num_pixfmt_unsup++] = fmt;
+ }
+ }
+
+ for (int fmt = IMGFMT_START; fmt <= IMGFMT_END; fmt++) {
+ struct mp_imgfmt_desc d = mp_imgfmt_get_desc(fmt);
+ enum AVPixelFormat pixfmt = imgfmt2pixfmt(fmt);
+ if (d.id || pixfmt != AV_PIX_FMT_NONE) {
+ assert(num_imgfmts < MP_ARRAY_SIZE(imgfmts)); // enlarge that array
+ imgfmts[num_imgfmts++] = fmt;
+ }
+ }
+
+ qsort(imgfmts, num_imgfmts, sizeof(imgfmts[0]), cmp_imgfmt_name);
+}
diff --git a/test/img_utils.h b/test/img_utils.h
new file mode 100644
index 0000000..2c21bcd
--- /dev/null
+++ b/test/img_utils.h
@@ -0,0 +1,24 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// Sorted list of valid imgfmts. Call init_imgfmts_list() before use.
+extern int imgfmts[];
+extern int num_imgfmts;
+
+void init_imgfmts_list(void);
diff --git a/test/json.c b/test/json.c
new file mode 100644
index 0000000..8aeae23
--- /dev/null
+++ b/test/json.c
@@ -0,0 +1,87 @@
+#include "misc/json.h"
+#include "misc/node.h"
+#include "test_utils.h"
+
+struct entry {
+ const char *src;
+ const char *out_txt;
+ struct mpv_node out_data;
+ bool expect_fail;
+};
+
+#define TEXT(...) #__VA_ARGS__
+
+#define VAL_LIST(...) (struct mpv_node[]){__VA_ARGS__}
+
+#define L(...) __VA_ARGS__
+
+#define NODE_INT64(v) {.format = MPV_FORMAT_INT64, .u = { .int64 = (v) }}
+#define NODE_STR(v) {.format = MPV_FORMAT_STRING, .u = { .string = (v) }}
+#define NODE_BOOL(v) {.format = MPV_FORMAT_FLAG, .u = { .flag = (bool)(v) }}
+#define NODE_FLOAT(v) {.format = MPV_FORMAT_DOUBLE, .u = { .double_ = (v) }}
+#define NODE_NONE() {.format = MPV_FORMAT_NONE }
+#define NODE_ARRAY(...) {.format = MPV_FORMAT_NODE_ARRAY, .u = { .list = \
+ &(struct mpv_node_list) { \
+ .num = sizeof(VAL_LIST(__VA_ARGS__)) / sizeof(struct mpv_node), \
+ .values = VAL_LIST(__VA_ARGS__)}}}
+#define NODE_MAP(k, v) {.format = MPV_FORMAT_NODE_MAP, .u = { .list = \
+ &(struct mpv_node_list) { \
+ .num = sizeof(VAL_LIST(v)) / sizeof(struct mpv_node), \
+ .values = VAL_LIST(v), \
+ .keys = (char**)(const char *[]){k}}}}
+
+static const struct entry entries[] = {
+ { "null", "null", NODE_NONE()},
+ { "true", "true", NODE_BOOL(true)},
+ { "false", "false", NODE_BOOL(false)},
+ { "", .expect_fail = true},
+ { "abc", .expect_fail = true},
+ { " 123 ", "123", NODE_INT64(123)},
+ { "123.25", "123.250000", NODE_FLOAT(123.25)},
+ { TEXT("a\n\\\/\\\""), TEXT("a\n\\/\\\""), NODE_STR("a\n\\/\\\"")},
+ { TEXT("a\u2c29"), TEXT("aⰩ"), NODE_STR("a\342\260\251")},
+ { "[1,2,3]", "[1,2,3]",
+ NODE_ARRAY(NODE_INT64(1), NODE_INT64(2), NODE_INT64(3))},
+ { "[ ]", "[]", NODE_ARRAY()},
+ { "[1,,2]", .expect_fail = true},
+ { "[,]", .expect_fail = true},
+ { TEXT({"a":1, "b":2}), TEXT({"a":1,"b":2}),
+ NODE_MAP(L("a", "b"), L(NODE_INT64(1), NODE_INT64(2)))},
+ { "{ }", "{}", NODE_MAP(L(), L())},
+ { TEXT({"a":b}), .expect_fail = true},
+ { TEXT({1a:"b"}), .expect_fail = true},
+
+ // non-standard extensions
+ { "[1,2,]", "[1,2]", NODE_ARRAY(NODE_INT64(1), NODE_INT64(2))},
+ { TEXT({a:"b"}), TEXT({"a":"b"}),
+ NODE_MAP(L("a"), L(NODE_STR("b")))},
+ { TEXT({a="b"}), TEXT({"a":"b"}),
+ NODE_MAP(L("a"), L(NODE_STR("b")))},
+ { TEXT({a ="b"}), TEXT({"a":"b"}),
+ NODE_MAP(L("a"), L(NODE_STR("b")))},
+ { TEXT({_a12="b"}), TEXT({"_a12":"b"}),
+ NODE_MAP(L("_a12"), L(NODE_STR("b")))},
+};
+
+int main(void)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(entries); n++) {
+ const struct entry *e = &entries[n];
+ void *tmp = talloc_new(NULL);
+ char *s = talloc_strdup(tmp, e->src);
+ json_skip_whitespace(&s);
+ struct mpv_node res;
+ bool ok = json_parse(tmp, &res, &s, MAX_JSON_DEPTH) >= 0;
+ assert_true(ok != e->expect_fail);
+ if (!ok) {
+ talloc_free(tmp);
+ continue;
+ }
+ char *d = talloc_strdup(tmp, "");
+ assert_true(json_write(&d, &res) >= 0);
+ assert_string_equal(e->out_txt, d);
+ assert_true(equal_mpv_node(&e->out_data, &res));
+ talloc_free(tmp);
+ }
+ return 0;
+}
diff --git a/test/libmpv_test.c b/test/libmpv_test.c
new file mode 100644
index 0000000..fafef6a
--- /dev/null
+++ b/test/libmpv_test.c
@@ -0,0 +1,271 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <libmpv/client.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// Stolen from osdep/compiler.h
+#ifdef __GNUC__
+#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format(printf, a1, a2)))
+#define MP_NORETURN __attribute__((noreturn))
+#else
+#define PRINTF_ATTRIBUTE(a1, a2)
+#define MP_NORETURN
+#endif
+
+// Broken crap with __USE_MINGW_ANSI_STDIO
+#if defined(__MINGW32__) && defined(__GNUC__) && !defined(__clang__)
+#undef PRINTF_ATTRIBUTE
+#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format (gnu_printf, a1, a2)))
+#endif
+
+// Dummy values for test_options_and_properties
+static const char *str = "string";
+static int flag = 1;
+static int64_t int_ = 20;
+static double double_ = 1.5;
+
+// Global handle.
+static mpv_handle *ctx;
+
+
+MP_NORETURN PRINTF_ATTRIBUTE(1, 2)
+static void fail(const char *fmt, ...)
+{
+ if (fmt) {
+ va_list va;
+ va_start(va, fmt);
+ vfprintf(stderr, fmt, va);
+ va_end(va);
+ }
+ mpv_destroy(ctx);
+ exit(1);
+}
+
+static void check_api_error(int status)
+{
+ if (status < 0)
+ fail("mpv API error: %s\n", mpv_error_string(status));
+}
+
+static void check_double(const char *property, double expect)
+{
+ double result_double;
+ check_api_error(mpv_get_property(ctx, property, MPV_FORMAT_DOUBLE, &result_double));
+ if (expect != result_double)
+ fail("Double: expected '%f' but got '%f'!\n", expect, result_double);
+}
+
+static void check_flag(const char *property, int expect)
+{
+ int result_flag;
+ check_api_error(mpv_get_property(ctx, property, MPV_FORMAT_FLAG, &result_flag));
+ if (expect != result_flag)
+ fail("Flag: expected '%d' but got '%d'!\n", expect, result_flag);
+}
+
+static void check_int(const char *property, int64_t expect)
+{
+ int64_t result_int;
+ check_api_error(mpv_get_property(ctx, property, MPV_FORMAT_INT64, &result_int));
+ if (expect != result_int)
+ fail("Int: expected '%" PRId64 "' but got '%" PRId64 "'!\n", expect, result_int);
+}
+
+static void check_string(const char *property, const char *expect)
+{
+ char *result_string;
+ check_api_error(mpv_get_property(ctx, property, MPV_FORMAT_STRING, &result_string));
+ if (strcmp(expect, result_string) != 0)
+ fail("String: expected '%s' but got '%s'!\n", expect, result_string);
+ mpv_free(result_string);
+}
+
+static void check_results(const char *properties[], enum mpv_format formats[])
+{
+ for (int i = 0; properties[i]; i++) {
+ switch (formats[i]) {
+ case MPV_FORMAT_STRING:
+ check_string(properties[i], str);
+ break;
+ case MPV_FORMAT_FLAG:
+ check_flag(properties[i], flag);
+ break;
+ case MPV_FORMAT_INT64:
+ check_int(properties[i], int_);
+ break;
+ case MPV_FORMAT_DOUBLE:
+ check_double(properties[i], double_);
+ break;
+ }
+ }
+}
+
+static void set_options_and_properties(const char *options[], const char *properties[],
+ enum mpv_format formats[])
+{
+ for (int i = 0; options[i]; i++) {
+ switch (formats[i]) {
+ case MPV_FORMAT_STRING:
+ check_api_error(mpv_set_option(ctx, options[i], formats[i], &str));
+ check_api_error(mpv_set_property(ctx, properties[i], formats[i], &str));
+ break;
+ case MPV_FORMAT_FLAG:
+ check_api_error(mpv_set_option(ctx, options[i], formats[i], &flag));
+ check_api_error(mpv_set_property(ctx, properties[i], formats[i], &flag));
+ break;
+ case MPV_FORMAT_INT64:
+ check_api_error(mpv_set_option(ctx, options[i], formats[i], &int_));
+ check_api_error(mpv_set_property(ctx, properties[i], formats[i], &int_));
+ break;
+ case MPV_FORMAT_DOUBLE:
+ check_api_error(mpv_set_option(ctx, options[i], formats[i], &double_));
+ check_api_error(mpv_set_property(ctx, properties[i], formats[i], &double_));
+ break;
+ }
+ }
+}
+
+static void test_file_loading(char *file)
+{
+ const char *cmd[] = {"loadfile", file, NULL};
+ check_api_error(mpv_command(ctx, cmd));
+ int loaded = 0;
+ int finished = 0;
+ while (!finished) {
+ mpv_event *event = mpv_wait_event(ctx, 0);
+ switch (event->event_id) {
+ case MPV_EVENT_FILE_LOADED:
+ // make sure it loads before exiting
+ loaded = 1;
+ break;
+ case MPV_EVENT_END_FILE:
+ if (loaded)
+ finished = 1;
+ break;
+ }
+ }
+ if (!finished)
+ fail("Unable to load test file!\n");
+}
+
+static void test_lavfi_complex(char *file)
+{
+ const char *cmd[] = {"loadfile", file, NULL};
+ check_api_error(mpv_command(ctx, cmd));
+ int finished = 0;
+ int loaded = 0;
+ while (!finished) {
+ mpv_event *event = mpv_wait_event(ctx, 0);
+ switch (event->event_id) {
+ case MPV_EVENT_FILE_LOADED:
+ // Add file as external and toggle lavfi-complex on.
+ if (!loaded) {
+ check_api_error(mpv_set_property_string(ctx, "external-files", file));
+ const char *add_cmd[] = {"video-add", file, "auto", NULL};
+ check_api_error(mpv_command(ctx, add_cmd));
+ check_api_error(mpv_set_property_string(ctx, "lavfi-complex", "[vid1] [vid2] vstack [vo]"));
+ }
+ loaded = 1;
+ break;
+ case MPV_EVENT_END_FILE:
+ if (loaded)
+ finished = 1;
+ break;
+ }
+ }
+ if (!finished)
+ fail("Lavfi complex failed!\n");
+}
+
+// Ensure that setting options/properties work correctly and
+// have the expected values.
+static void test_options_and_properties(void)
+{
+ // Order matters. string -> flag -> int -> double (repeat)
+ // One for set_option the other for set_property
+ const char *options[] = {
+ "screen-name",
+ "save-position-on-quit",
+ "cursor-autohide",
+ "speed",
+ NULL
+ };
+
+ const char *properties[] = {
+ "fs-screen-name",
+ "shuffle",
+ "sub-pos",
+ "window-scale",
+ NULL
+ };
+
+ // Must match above ordering.
+ enum mpv_format formats[] = {
+ MPV_FORMAT_STRING,
+ MPV_FORMAT_FLAG,
+ MPV_FORMAT_INT64,
+ MPV_FORMAT_DOUBLE,
+ };
+
+ set_options_and_properties(options, properties, formats);
+
+ check_api_error(mpv_initialize(ctx));
+
+ check_results(options, formats);
+ check_results(properties, formats);
+
+ // Ensure the format is still MPV_FORMAT_FLAG for these property types.
+ mpv_node result_node;
+ check_api_error(mpv_get_property(ctx, "idle-active", MPV_FORMAT_NODE, &result_node));
+ if (result_node.format != MPV_FORMAT_FLAG)
+ fail("Node: expected mpv format '%d' but got '%d'!\n", MPV_FORMAT_FLAG, result_node.format);
+
+ // Always should be true.
+ if (result_node.u.flag != 1)
+ fail("Node: expected 1 but got %d'!\n", result_node.u.flag);
+}
+
+int main(int argc, char *argv[])
+{
+ if (argc != 2)
+ return 1;
+
+ ctx = mpv_create();
+ if (!ctx)
+ return 1;
+
+ check_api_error(mpv_set_option_string(ctx, "vo", "null"));
+ check_api_error(mpv_set_option_string(ctx, "terminal", "yes"));
+ check_api_error(mpv_set_option_string(ctx, "msg-level", "all=debug"));
+
+ const char *fmt = "================ TEST: %s ================\n";
+
+ printf(fmt, "test_options_and_properties");
+ test_options_and_properties();
+ printf(fmt, "test_file_loading");
+ test_file_loading(argv[1]);
+ printf(fmt, "test_lavfi_complex");
+ test_lavfi_complex(argv[1]);
+
+ mpv_destroy(ctx);
+ return 0;
+}
diff --git a/test/linked_list.c b/test/linked_list.c
new file mode 100644
index 0000000..691de35
--- /dev/null
+++ b/test/linked_list.c
@@ -0,0 +1,160 @@
+#include "common/common.h"
+#include "misc/linked_list.h"
+#include "test_utils.h"
+
+struct list_item {
+ int v;
+ struct {
+ struct list_item *prev, *next;
+ } list_node;
+};
+
+struct the_list {
+ struct list_item *head, *tail;
+};
+
+// This serves to remove some -Waddress "always true" warnings.
+static struct list_item *STUPID_SHIT(struct list_item *item)
+{
+ return item;
+}
+
+static bool do_check_list(struct the_list *lst, int *c, int num_c)
+{
+ if (!lst->head)
+ assert_true(!lst->tail);
+ if (!lst->tail)
+ assert_true(!lst->head);
+
+ for (struct list_item *cur = lst->head; cur; cur = cur->list_node.next) {
+ if (cur->list_node.prev) {
+ assert_true(cur->list_node.prev->list_node.next == cur);
+ assert_true(lst->head != cur);
+ } else {
+ assert_true(lst->head == cur);
+ }
+ if (cur->list_node.next) {
+ assert_true(cur->list_node.next->list_node.prev == cur);
+ assert_true(lst->tail != cur);
+ } else {
+ assert_true(lst->tail == cur);
+ }
+
+ if (num_c < 1)
+ return false;
+ if (c[0] != cur->v)
+ return false;
+
+ num_c--;
+ c++;
+ }
+
+ if (num_c)
+ return false;
+
+ return true;
+}
+
+int main(void)
+{
+ struct the_list lst = {0};
+ struct list_item e1 = {1};
+ struct list_item e2 = {2};
+ struct list_item e3 = {3};
+ struct list_item e4 = {4};
+ struct list_item e5 = {5};
+ struct list_item e6 = {6};
+
+#define check_list(...) \
+ assert_true(do_check_list(&lst, (int[]){__VA_ARGS__}, \
+ sizeof((int[]){__VA_ARGS__}) / sizeof(int)));
+#define check_list_empty() \
+ assert_true(do_check_list(&lst, NULL, 0));
+
+ check_list_empty();
+ LL_APPEND(list_node, &lst, &e1);
+
+ check_list(1);
+ LL_APPEND(list_node, &lst, &e2);
+
+ check_list(1, 2);
+ LL_APPEND(list_node, &lst, &e4);
+
+ check_list(1, 2, 4);
+ LL_CLEAR(list_node, &lst);
+
+ check_list_empty();
+ LL_PREPEND(list_node, &lst, &e4);
+
+ check_list(4);
+ LL_PREPEND(list_node, &lst, &e2);
+
+ check_list(2, 4);
+ LL_PREPEND(list_node, &lst, &e1);
+
+ check_list(1, 2, 4);
+ LL_CLEAR(list_node, &lst);
+
+ check_list_empty();
+ LL_INSERT_BEFORE(list_node, &lst, (struct list_item *)NULL, &e6);
+
+ check_list(6);
+ LL_INSERT_BEFORE(list_node, &lst, (struct list_item *)NULL, &e1);
+
+ check_list(6, 1);
+ LL_INSERT_BEFORE(list_node, &lst, (struct list_item *)NULL, &e2);
+
+ check_list(6, 1, 2);
+ LL_INSERT_BEFORE(list_node, &lst, STUPID_SHIT(&e6), &e3);
+
+ check_list(3, 6, 1, 2);
+ LL_INSERT_BEFORE(list_node, &lst, STUPID_SHIT(&e6), &e5);
+
+ check_list(3, 5, 6, 1, 2);
+ LL_INSERT_BEFORE(list_node, &lst, STUPID_SHIT(&e2), &e4);
+
+ check_list(3, 5, 6, 1, 4, 2);
+ LL_REMOVE(list_node, &lst, &e6);
+
+ check_list(3, 5, 1, 4, 2);
+ LL_REMOVE(list_node, &lst, &e3);
+
+ check_list(5, 1, 4, 2);
+ LL_REMOVE(list_node, &lst, &e2);
+
+ check_list(5, 1, 4);
+ LL_REMOVE(list_node, &lst, &e4);
+
+ check_list(5, 1);
+ LL_REMOVE(list_node, &lst, &e5);
+
+ check_list(1);
+ LL_REMOVE(list_node, &lst, &e1);
+
+ check_list_empty();
+ LL_APPEND(list_node, &lst, &e2);
+
+ check_list(2);
+ LL_REMOVE(list_node, &lst, &e2);
+
+ check_list_empty();
+ LL_INSERT_AFTER(list_node, &lst, (struct list_item *)NULL, &e1);
+
+ check_list(1);
+ LL_INSERT_AFTER(list_node, &lst, (struct list_item *)NULL, &e2);
+
+ check_list(2, 1);
+ LL_INSERT_AFTER(list_node, &lst, (struct list_item *)NULL, &e3);
+
+ check_list(3, 2, 1);
+ LL_INSERT_AFTER(list_node, &lst, STUPID_SHIT(&e3), &e4);
+
+ check_list(3, 4, 2, 1);
+ LL_INSERT_AFTER(list_node, &lst, STUPID_SHIT(&e4), &e5);
+
+ check_list(3, 4, 5, 2, 1);
+ LL_INSERT_AFTER(list_node, &lst, STUPID_SHIT(&e1), &e6);
+
+ check_list(3, 4, 5, 2, 1, 6);
+ return 0;
+}
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..ebd4395
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,148 @@
+# So we don't have to reorganize the entire directory tree.
+incdir = include_directories('../')
+outdir = join_paths(build_root, 'test', 'out')
+refdir = join_paths(source_root, 'test', 'ref')
+
+# Convenient testing libraries. An adhoc collection of
+# mpv objects that test_utils.c needs. Paths and subprocesses
+# are required in order to run a diff command when comparing
+# different files. Stuff will probably break if core things are
+# carelessly moved around.
+test_utils_args = []
+test_utils_files = [
+ 'audio/chmap.c',
+ 'audio/format.c',
+ 'common/common.c',
+ 'misc/bstr.c',
+ 'misc/dispatch.c',
+ 'misc/json.c',
+ 'misc/node.c',
+ 'misc/random.c',
+ 'misc/thread_tools.c',
+ 'options/m_config_core.c',
+ 'options/m_config_frontend.c',
+ 'options/m_option.c',
+ 'options/path.c',
+ 'osdep/io.c',
+ 'osdep/subprocess.c',
+ 'osdep/timer.c',
+ timer_source,
+ path_source,
+ subprocess_source,
+ 'ta/ta.c',
+ 'ta/ta_talloc.c',
+ 'ta/ta_utils.c'
+]
+
+test_utils_deps = [libavutil, libm]
+
+if win32
+ test_utils_files += 'osdep/windows_utils.c'
+endif
+
+if features['pthread-debug']
+ test_utils_files += 'osdep/threads-posix.c'
+endif
+
+# The zimg code requires using threads.
+if not features['win32-threads']
+ test_utils_deps += pthreads
+endif
+
+if features['win32-desktop']
+ test_utils_deps += cc.find_library('winmm')
+endif
+test_utils_objects = libmpv.extract_objects(test_utils_files)
+test_utils = static_library('test-utils', 'test_utils.c', include_directories: incdir,
+ c_args: test_utils_args, objects: test_utils_objects,
+ dependencies: test_utils_deps)
+
+# For getting imgfmts and stuff.
+img_utils_files = [
+ 'misc/thread_pool.c',
+ 'video/csputils.c',
+ 'video/fmt-conversion.c',
+ 'video/img_format.c',
+ 'video/mp_image.c',
+ 'video/out/placebo/utils.c',
+ 'video/sws_utils.c'
+]
+if features['zimg']
+ img_utils_files += ['video/repack.c', 'video/zimg.c']
+endif
+
+img_utils_objects = libmpv.extract_objects(img_utils_files)
+img_utils = static_library('img-utils', 'img_utils.c', include_directories: incdir,
+ dependencies: [libavcodec], objects: img_utils_objects)
+
+# The actual tests.
+chmap_files = [
+ 'audio/chmap_sel.c'
+]
+if features['av-channel-layout']
+ chmap_files += 'audio/chmap_avchannel.c'
+endif
+chmap_objects = libmpv.extract_objects(chmap_files)
+chmap = executable('chmap', 'chmap.c', include_directories: incdir,
+ dependencies: [libavutil], objects: chmap_objects,
+ link_with: test_utils)
+test('chmap', chmap)
+
+gl_video_objects = libmpv.extract_objects('video/out/gpu/ra.c',
+ 'video/out/gpu/utils.c')
+gl_video = executable('gl-video', 'gl_video.c', objects: gl_video_objects,
+ dependencies: [libavutil], include_directories: incdir,
+ link_with: [img_utils, test_utils])
+test('gl-video', gl_video)
+
+json = executable('json', 'json.c', include_directories: incdir, link_with: test_utils)
+test('json', json)
+
+linked_list = executable('linked-list', files('linked_list.c'), include_directories: incdir)
+test('linked-list', linked_list)
+
+timer = executable('timer', files('timer.c'), include_directories: incdir, link_with: test_utils)
+test('timer', timer)
+
+paths_objects = libmpv.extract_objects('options/path.c', path_source)
+paths = executable('paths', 'paths.c', include_directories: incdir,
+ objects: paths_objects, link_with: test_utils)
+test('paths', paths)
+
+if get_option('libmpv')
+ libmpv_test = executable('libmpv-test', 'libmpv_test.c',
+ include_directories: incdir, link_with: libmpv)
+ file = join_paths(source_root, 'etc', 'mpv-icon-8bit-16x16.png')
+ test('libmpv', libmpv_test, args: file, timeout: 60)
+endif
+
+# Minimum required libavutil version that works with these tests.
+# Will need to be manually updated when ffmpeg adds/removes more formats in the future.
+if libavutil.version().version_compare('>= 58.27.100')
+
+# The CI can randomly fail if libavutil isn't explicitly linked again here.
+ img_format = executable('img-format', 'img_format.c', include_directories: incdir,
+ dependencies: [libavutil, libplacebo], link_with: [img_utils, test_utils])
+ test('img-format', img_format, args: [refdir, outdir], suite: 'ffmpeg')
+
+
+ scale_sws_objects = libmpv.extract_objects('video/image_writer.c',
+ 'video/repack.c')
+ scale_sws = executable('scale-sws', ['scale_sws.c', 'scale_test.c'], include_directories: incdir,
+ objects: scale_sws_objects, dependencies: [libavutil, libavformat, libswscale, jpeg, zimg, libplacebo],
+ link_with: [img_utils, test_utils])
+ test('scale-sws', scale_sws, args: [refdir, outdir], suite: 'ffmpeg')
+
+ if features['zimg']
+ repack_objects = libmpv.extract_objects('sub/draw_bmp.c')
+ repack = executable('repack', 'repack.c', include_directories: incdir, objects: repack_objects,
+ dependencies: [libavutil, libswscale, zimg, libplacebo], link_with: [img_utils, test_utils])
+ test('repack', repack, args: [refdir, outdir], suite: 'ffmpeg')
+
+ scale_zimg_objects = libmpv.extract_objects('video/image_writer.c')
+ scale_zimg = executable('scale-zimg', ['scale_test.c', 'scale_zimg.c'], include_directories: incdir,
+ objects: scale_zimg_objects, dependencies:[libavutil, libavformat, libswscale, jpeg, zimg, libplacebo],
+ link_with: [img_utils, test_utils])
+ test('scale-zimg', scale_zimg, args: [refdir, outdir], suite: 'ffmpeg')
+ endif
+endif
diff --git a/test/paths.c b/test/paths.c
new file mode 100644
index 0000000..aa610db
--- /dev/null
+++ b/test/paths.c
@@ -0,0 +1,65 @@
+#include "common/common.h"
+#include "common/msg.h"
+#include "config.h"
+#include "options/path.h"
+#include "test_utils.h"
+
+static void test_join(char *file, int line, char *a, char *b, char *c)
+{
+ char *res = mp_path_join(NULL, a, b);
+ if (strcmp(res, c) != 0) {
+ printf("%s:%d: '%s' + '%s' = '%s', expected '%s'\n", file, line,
+ a, b, res, c);
+ abort();
+ }
+ talloc_free(res);
+}
+
+static void test_abs(char *file, int line, bool abs, char *a)
+{
+ if (mp_path_is_absolute(bstr0(a)) != abs) {
+ printf("%s:%d: mp_path_is_absolute('%s') => %d, expected %d\n",
+ file, line, a, !abs, abs);
+ abort();
+ }
+}
+
+#define TEST_JOIN(a, b, c) \
+ test_join(__FILE__, __LINE__, a, b, c);
+
+#define TEST_ABS(abs, a) \
+ test_abs(__FILE__, __LINE__, abs, a)
+
+int main(void)
+{
+ TEST_ABS(true, "/ab");
+ TEST_ABS(false, "ab");
+ TEST_JOIN("", "", "");
+ TEST_JOIN("a", "", "a");
+ TEST_JOIN("/a", "", "/a");
+ TEST_JOIN("", "b", "b");
+ TEST_JOIN("", "/b", "/b");
+ TEST_JOIN("ab", "cd", "ab/cd");
+ TEST_JOIN("ab/", "cd", "ab/cd");
+ TEST_JOIN("ab/", "/cd", "/cd");
+ // Note: we prefer "/" on win32, but tolerate "\".
+#if HAVE_DOS_PATHS
+ TEST_ABS(true, "\\ab");
+ TEST_ABS(true, "c:\\");
+ TEST_ABS(true, "c:/");
+ TEST_ABS(false, "c:");
+ TEST_ABS(false, "c:a");
+ TEST_ABS(false, "c:a\\");
+ TEST_JOIN("ab\\", "cd", "ab\\cd");
+ TEST_JOIN("ab\\", "\\cd", "\\cd");
+ TEST_JOIN("c:/", "de", "c:/de");
+ TEST_JOIN("c:/a", "de", "c:/a/de");
+ TEST_JOIN("c:\\a", "c:\\b", "c:\\b");
+ TEST_JOIN("c:/a", "c:/b", "c:/b");
+ // Note: drive-relative paths are not always supported "properly"
+ TEST_JOIN("c:/a", "d:b", "c:/a/d:b");
+ TEST_JOIN("c:a", "b", "c:a/b");
+ TEST_JOIN("c:", "b", "c:b");
+#endif
+ return 0;
+}
diff --git a/test/ref/draw_bmp.txt b/test/ref/draw_bmp.txt
new file mode 100644
index 0000000..66de4de
--- /dev/null
+++ b/test/ref/draw_bmp.txt
@@ -0,0 +1,249 @@
+0bgr = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+0rgb = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+abgr = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrap, a=unknown, ca=unknown, ca_f=unknown
+argb = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrap, a=unknown, ca=unknown, ca_f=unknown
+ayuv64 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+ayuv64be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+bayer_bggr16= no
+bayer_bggr16be= no
+bayer_bggr8 = no
+bayer_gbrg16= no
+bayer_gbrg16be= no
+bayer_gbrg8 = no
+bayer_grbg16= no
+bayer_grbg16be= no
+bayer_grbg8 = no
+bayer_rggb16= no
+bayer_rggb16be= no
+bayer_rggb8 = no
+bgr0 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr24 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr4 = no
+bgr444 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr444be = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr48 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+bgr48be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+bgr4_byte = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr555 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr555be = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr565 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr565be = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgr8 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+bgra = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrap, a=unknown, ca=unknown, ca_f=unknown
+bgra64 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+bgra64be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+cuda = no
+d3d11 = no
+d3d11va_vld = no
+drm_prime = no
+dxva2_vld = no
+gbrap = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrap, a=unknown, ca=unknown, ca_f=unknown
+gbrap10 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap10be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap12 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap12be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap14 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap14be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap16 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrap16be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrapf32 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrapf32be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+gbrp1 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp10 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp10be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp12 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp12be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp14 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp14be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp16 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp16be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp2 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp3 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp4 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp5 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp6 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp9 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrp9be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrpf32 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gbrpf32be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+gray = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray10 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray10be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray12 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray12be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray14 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray14be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray16 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray16be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray9 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+gray9be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+grayaf32 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayaf32, a=unknown, ca=unknown, ca_f=unknown
+grayf32 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+grayf32be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+mediacodec = no
+mmal = no
+monob = align=8:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+monow = align=8:1 ov=yap8 , ov_f=grayaf32, v_f=grayf32, a=unknown, ca=unknown, ca_f=unknown
+nv12 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+nv16 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+nv20 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+nv20be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+nv21 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+nv24 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+nv42 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+opencl = no
+p010 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+p010be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+p012 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+p012be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+p016 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+p016be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+p210 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+p210be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+p212 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+p212be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+p216 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+p216be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+p410 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+p410be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+p412 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+p412be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+p416 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+p416be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+pal8 = no
+qsv = no
+rgb0 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb24 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb30 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+rgb4 = no
+rgb444 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb444be = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb48 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+rgb48be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+rgb4_byte = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb555 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb555be = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb565 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb565be = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgb8 = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrp, a=unknown, ca=unknown, ca_f=unknown
+rgba = align=1:1 ov=unknown, ov_f=gbrap, v_f=gbrap, a=unknown, ca=unknown, ca_f=unknown
+rgba64 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+rgba64be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrapf32, a=unknown, ca=unknown, ca_f=unknown
+rgbaf16 = no
+rgbaf16be = no
+rgbaf32 = no
+rgbaf32be = no
+rgbf32 = no
+rgbf32be = no
+uyvy422 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+uyyvyy411 = no
+vaapi = no
+vdpau = no
+vdpau_output= no
+videotoolbox= no
+vulkan = no
+vuya = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+vuyx = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+x2bgr10 = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+x2bgr10be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+x2rgb10be = align=1:1 ov=unknown, ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+xv30 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+xv30be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+xv36 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+xv36be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+xvmc = no
+xyz12 = align=1:1 ov=gbrap , ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+xyz12be = align=1:1 ov=gbrap , ov_f=gbrapf32, v_f=gbrpf32, a=unknown, ca=unknown, ca_f=unknown
+y1 = no
+y210 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+y210be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+y212 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+y212be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+ya16 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayaf32, a=unknown, ca=unknown, ca_f=unknown
+ya16be = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayaf32, a=unknown, ca=unknown, ca_f=unknown
+ya8 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayaf32, a=unknown, ca=unknown, ca_f=unknown
+yap16 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayaf32, a=unknown, ca=unknown, ca_f=unknown
+yap8 = align=1:1 ov=yap8 , ov_f=grayaf32, v_f=grayaf32, a=unknown, ca=unknown, ca_f=unknown
+yuv410p = no
+yuv410pf = no
+yuv411p = no
+yuv411pf = no
+yuv420p = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p10 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p10be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p12 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p12be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p14 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p14be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p16 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p16be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p9 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420p9be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv420pf = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuv420pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p10 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p10be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p12 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p12be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p14 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p14be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p16 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p16be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p9 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422p9be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv422pf = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuv440p = no
+yuv440p10 = no
+yuv440p10be = no
+yuv440p12 = no
+yuv440p12be = no
+yuv440pf = no
+yuv444p = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p10 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p10be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p12 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p12be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p14 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p14be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p16 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p16be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p9 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444p9be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuv444pf = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuv444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva410pf = no
+yuva411pf = no
+yuva420p = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420p10 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420p10be= align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420p16 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420p16be= align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420p9 = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420p9be = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva420pf = align=2:2 ov=yuva420p, ov_f=yuva420pf, v_f=yuva420pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p10 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p10be= align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p12 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p12be= align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p16 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p16be= align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p9 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422p9be = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva422pf = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuva422pf, a=gray, ca=gray, ca_f=grayf32
+yuva440pf = no
+yuva444p = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p10 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p10be= align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p12 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p12be= align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p16 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p16be= align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p9 = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444p9be = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuva444pf = align=1:1 ov=yuva444p, ov_f=yuva444pf, v_f=yuva444pf, a=unknown, ca=unknown, ca_f=unknown
+yuvj411p = no
+yuvj422p = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yuvj440p = no
+yuyv422 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
+yvyu422 = align=2:1 ov=yuva422p, ov_f=yuva422pf, v_f=yuv422pf, a=gray, ca=gray, ca_f=grayf32
diff --git a/test/ref/img_formats.txt b/test/ref/img_formats.txt
new file mode 100644
index 0000000..9a3826b
--- /dev/null
+++ b/test/ref/img_formats.txt
@@ -0,0 +1,2834 @@
+0bgr: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {24:8} {16:8} {8:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {0, 3, 2, 1}
+ AVD: name=0bgr chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=4 o=3 sh=0 d=8
+ 1: p=0 st=4 o=2 sh=0 d=8
+ 2: p=0 st=4 o=1 sh=0 d=8
+0rgb: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {8:8} {16:8} {24:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {0, 1, 2, 3}
+ AVD: name=0rgb chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=4 o=1 sh=0 d=8
+ 1: p=0 st=4 o=2 sh=0 d=8
+ 2: p=0 st=4 o=3 sh=0 d=8
+abgr: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {24:8} {16:8} {8:8} {0:8}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {4, 3, 2, 1}
+ AVD: name=abgr chroma=0:0 flags=0xa0 [rgb][alpha]
+ 0: p=0 st=4 o=3 sh=0 d=8
+ 1: p=0 st=4 o=2 sh=0 d=8
+ 2: p=0 st=4 o=1 sh=0 d=8
+ 3: p=0 st=4 o=0 sh=0 d=8
+argb: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {8:8} {16:8} {24:8} {0:8}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {4, 1, 2, 3}
+ AVD: name=argb chroma=0:0 flags=0xa0 [rgb][alpha]
+ 0: p=0 st=4 o=1 sh=0 d=8
+ 1: p=0 st=4 o=2 sh=0 d=8
+ 2: p=0 st=4 o=3 sh=0 d=8
+ 3: p=0 st=4 o=0 sh=0 d=8
+ayuv64: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuv][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits {16:16} {32:16} {48:16} {0:16}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {4, 1, 2, 3}
+ AVD: name=ayuv64le chroma=0:0 flags=0x80 [alpha]
+ 0: p=0 st=8 o=2 sh=0 d=16
+ 1: p=0 st=8 o=4 sh=0 d=16
+ 2: p=0 st=8 o=6 sh=0 d=16
+ 3: p=0 st=8 o=0 sh=0 d=16
+ayuv64be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuv][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits endian_bytes=2 {16:16} {32:16} {48:16} {0:16}
+ AVD: name=ayuv64be chroma=0:0 flags=0x81 [be][alpha]
+ 0: p=0 st=8 o=2 sh=0 d=16
+ 1: p=0 st=8 o=4 sh=0 d=16
+ 2: p=0 st=8 o=6 sh=0 d=16
+ 3: p=0 st=8 o=0 sh=0 d=16
+bayer_bggr16: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {} {} {} {}
+ AVD: name=bayer_bggr16le chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_bggr16be: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {} {} {}
+ AVD: name=bayer_bggr16be chroma=0:0 flags=0x121 [be][rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_bggr8: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {} {} {} {}
+ AVD: name=bayer_bggr8 chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=1 o=0 sh=0 d=2
+ 1: p=0 st=1 o=0 sh=0 d=4
+ 2: p=0 st=1 o=0 sh=0 d=2
+bayer_gbrg16: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {} {} {} {}
+ AVD: name=bayer_gbrg16le chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_gbrg16be: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {} {} {}
+ AVD: name=bayer_gbrg16be chroma=0:0 flags=0x121 [be][rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_gbrg8: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {} {} {} {}
+ AVD: name=bayer_gbrg8 chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=1 o=0 sh=0 d=2
+ 1: p=0 st=1 o=0 sh=0 d=4
+ 2: p=0 st=1 o=0 sh=0 d=2
+bayer_grbg16: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {} {} {} {}
+ AVD: name=bayer_grbg16le chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_grbg16be: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {} {} {}
+ AVD: name=bayer_grbg16be chroma=0:0 flags=0x121 [be][rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_grbg8: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {} {} {} {}
+ AVD: name=bayer_grbg8 chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=1 o=0 sh=0 d=2
+ 1: p=0 st=1 o=0 sh=0 d=4
+ 2: p=0 st=1 o=0 sh=0 d=2
+bayer_rggb16: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {} {} {} {}
+ AVD: name=bayer_rggb16le chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_rggb16be: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {} {} {}
+ AVD: name=bayer_rggb16be chroma=0:0 flags=0x121 [be][rgb][bayer]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=0 d=8
+ 2: p=0 st=2 o=0 sh=0 d=4
+bayer_rggb8: [GENERIC] fcsp=rgb ctype=unknown
+ Basic desc: [ba][rgb][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {} {} {} {}
+ AVD: name=bayer_rggb8 chroma=0:0 flags=0x120 [rgb][bayer]
+ 0: p=0 st=1 o=0 sh=0 d=2
+ 1: p=0 st=1 o=0 sh=0 d=4
+ 2: p=0 st=1 o=0 sh=0 d=2
+bgr0: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {16:8} {8:8} {0:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1, 0}
+ AVD: name=bgr0 chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=4 o=2 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=0 sh=0 d=8
+bgr24: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {24/[0:0] }
+ 0: 24bits {16:8} {8:8} {0:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1}
+ AVD: name=bgr24 chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=3 o=2 sh=0 d=8
+ 1: p=0 st=3 o=1 sh=0 d=8
+ 2: p=0 st=3 o=0 sh=0 d=8
+bgr4: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [rgb][le][be][uint]
+ planes=1, chroma=0:0 align=2:1
+ {4/[0:0] }
+ 0: 4bits {3:1} {1:2} {0:1} {}
+ AVD: name=bgr4 chroma=0:0 flags=0x24 [bs][rgb]
+ 0: p=0 st=4 o=3 sh=0 d=1
+ 1: p=0 st=4 o=1 sh=0 d=2
+ 2: p=0 st=4 o=0 sh=0 d=1
+bgr444: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:4} {4:4} {8:4} {}
+ AVD: name=bgr444le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=4 d=4
+ 2: p=0 st=2 o=1 sh=0 d=4
+bgr444be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:4} {4:4} {8:4} {}
+ AVD: name=bgr444be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=2 o=0 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=4 d=4
+ 2: p=0 st=2 o=-1 sh=0 d=4
+bgr48: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {48/[0:0] }
+ 0: 48bits {32:16} {16:16} {0:16} {}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1}
+ AVD: name=bgr48le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=6 o=4 sh=0 d=16
+ 1: p=0 st=6 o=2 sh=0 d=16
+ 2: p=0 st=6 o=0 sh=0 d=16
+bgr48be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {48/[0:0] }
+ 0: 48bits endian_bytes=2 {32:16} {16:16} {0:16} {}
+ AVD: name=bgr48be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=6 o=4 sh=0 d=16
+ 1: p=0 st=6 o=2 sh=0 d=16
+ 2: p=0 st=6 o=0 sh=0 d=16
+bgr4_byte: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {0:1} {1:2} {3:1} {}
+ AVD: name=bgr4_byte chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=1 o=0 sh=0 d=1
+ 1: p=0 st=1 o=0 sh=1 d=2
+ 2: p=0 st=1 o=0 sh=3 d=1
+bgr555: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:5} {5:5} {10:5} {}
+ AVD: name=bgr555le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=2 o=0 sh=0 d=5
+ 1: p=0 st=2 o=0 sh=5 d=5
+ 2: p=0 st=2 o=1 sh=2 d=5
+bgr555be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:5} {5:5} {10:5} {}
+ AVD: name=bgr555be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=2 o=0 sh=0 d=5
+ 1: p=0 st=2 o=0 sh=5 d=5
+ 2: p=0 st=2 o=-1 sh=2 d=5
+bgr565: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:5} {5:6} {11:5} {}
+ AVD: name=bgr565le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=2 o=0 sh=0 d=5
+ 1: p=0 st=2 o=0 sh=5 d=6
+ 2: p=0 st=2 o=1 sh=3 d=5
+bgr565be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:5} {5:6} {11:5} {}
+ AVD: name=bgr565be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=2 o=0 sh=0 d=5
+ 1: p=0 st=2 o=0 sh=5 d=6
+ 2: p=0 st=2 o=-1 sh=3 d=5
+bgr8: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {0:3} {3:3} {6:2} {}
+ AVD: name=bgr8 chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=1 o=0 sh=0 d=3
+ 1: p=0 st=1 o=0 sh=3 d=3
+ 2: p=0 st=1 o=0 sh=6 d=2
+bgra: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {16:8} {8:8} {0:8} {24:8}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1, 4}
+ AVD: name=bgra chroma=0:0 flags=0xa0 [rgb][alpha]
+ 0: p=0 st=4 o=2 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=0 sh=0 d=8
+ 3: p=0 st=4 o=3 sh=0 d=8
+bgra64: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits {32:16} {16:16} {0:16} {48:16}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1, 4}
+ AVD: name=bgra64le chroma=0:0 flags=0xa0 [rgb][alpha]
+ 0: p=0 st=8 o=4 sh=0 d=16
+ 1: p=0 st=8 o=2 sh=0 d=16
+ 2: p=0 st=8 o=0 sh=0 d=16
+ 3: p=0 st=8 o=6 sh=0 d=16
+bgra64be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits endian_bytes=2 {32:16} {16:16} {0:16} {48:16}
+ AVD: name=bgra64be chroma=0:0 flags=0xa1 [be][rgb][alpha]
+ 0: p=0 st=8 o=4 sh=0 d=16
+ 1: p=0 st=8 o=2 sh=0 d=16
+ 2: p=0 st=8 o=0 sh=0 d=16
+ 3: p=0 st=8 o=6 sh=0 d=16
+cuda: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=cuda chroma=0:0 flags=0x8 [hw]
+d3d11: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=d3d11 chroma=0:0 flags=0x8 [hw]
+d3d11va_vld: [GENERIC] ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=1:1 align=2:2
+ {}
+ AVD: name=d3d11va_vld chroma=1:1 flags=0x8 [hw]
+drm_prime: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=drm_prime chroma=0:0 flags=0x8 [hw]
+dxva2_vld: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=1:1 align=2:2
+ {}
+ AVD: name=dxva2_vld chroma=1:1 flags=0x8 [hw]
+gbrap: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8} {} {}
+ 1: 8bits {} {} {0:8} {}
+ 2: 8bits {0:8} {} {} {}
+ 3: 8bits {} {} {} {0:8}
+ Regular: planes=4 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ 3: {4}
+ AVD: name=gbrap chroma=0:0 flags=0xb0 [planar][rgb][alpha]
+ 0: p=2 st=1 o=0 sh=0 d=8
+ 1: p=0 st=1 o=0 sh=0 d=8
+ 2: p=1 st=1 o=0 sh=0 d=8
+ 3: p=3 st=1 o=0 sh=0 d=8
+gbrap10: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-6} {} {}
+ 1: 16bits {} {} {0:16/-6} {}
+ 2: 16bits {0:16/-6} {} {} {}
+ 3: 16bits {} {} {} {0:16/-6}
+ Regular: planes=4 compbytes=2 bitpad=-6 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ 3: {4}
+ AVD: name=gbrap10le chroma=0:0 flags=0xb0 [planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=10
+ 1: p=0 st=2 o=0 sh=0 d=10
+ 2: p=1 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+gbrap10be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ 2: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-6}
+ AVD: name=gbrap10be chroma=0:0 flags=0xb1 [be][planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=10
+ 1: p=0 st=2 o=0 sh=0 d=10
+ 2: p=1 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+gbrap12: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-4} {} {}
+ 1: 16bits {} {} {0:16/-4} {}
+ 2: 16bits {0:16/-4} {} {} {}
+ 3: 16bits {} {} {} {0:16/-4}
+ Regular: planes=4 compbytes=2 bitpad=-4 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ 3: {4}
+ AVD: name=gbrap12le chroma=0:0 flags=0xb0 [planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=12
+ 1: p=0 st=2 o=0 sh=0 d=12
+ 2: p=1 st=2 o=0 sh=0 d=12
+ 3: p=3 st=2 o=0 sh=0 d=12
+gbrap12be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ 2: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-4}
+ AVD: name=gbrap12be chroma=0:0 flags=0xb1 [be][planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=12
+ 1: p=0 st=2 o=0 sh=0 d=12
+ 2: p=1 st=2 o=0 sh=0 d=12
+ 3: p=3 st=2 o=0 sh=0 d=12
+gbrap14: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-2} {} {}
+ 1: 16bits {} {} {0:16/-2} {}
+ 2: 16bits {0:16/-2} {} {} {}
+ 3: 16bits {} {} {} {0:16/-2}
+ Regular: planes=4 compbytes=2 bitpad=-2 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ 3: {4}
+ AVD: name=gbrap14le chroma=0:0 flags=0xb0 [planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=14
+ 1: p=0 st=2 o=0 sh=0 d=14
+ 2: p=1 st=2 o=0 sh=0 d=14
+ 3: p=3 st=2 o=0 sh=0 d=14
+gbrap14be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-2} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-2} {}
+ 2: 16bits endian_bytes=2 {0:16/-2} {} {} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-2}
+ AVD: name=gbrap14be chroma=0:0 flags=0xb1 [be][planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=14
+ 1: p=0 st=2 o=0 sh=0 d=14
+ 2: p=1 st=2 o=0 sh=0 d=14
+ 3: p=3 st=2 o=0 sh=0 d=14
+gbrap16: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16} {} {}
+ 1: 16bits {} {} {0:16} {}
+ 2: 16bits {0:16} {} {} {}
+ 3: 16bits {} {} {} {0:16}
+ Regular: planes=4 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ 3: {4}
+ AVD: name=gbrap16le chroma=0:0 flags=0xb0 [planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=16
+ 1: p=0 st=2 o=0 sh=0 d=16
+ 2: p=1 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+gbrap16be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16} {}
+ 2: 16bits endian_bytes=2 {0:16} {} {} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16}
+ AVD: name=gbrap16be chroma=0:0 flags=0xb1 [be][planar][rgb][alpha]
+ 0: p=2 st=2 o=0 sh=0 d=16
+ 1: p=0 st=2 o=0 sh=0 d=16
+ 2: p=1 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+gbrapf32: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][bb][a][rgb][le][float]
+ planes=4, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] 32/[0:0] 32/[0:0] }
+ 0: 32bits {} {0:32} {} {}
+ 1: 32bits {} {} {0:32} {}
+ 2: 32bits {0:32} {} {} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=1x1 ctype=float
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ 3: {4}
+ AVD: name=gbrapf32le chroma=0:0 flags=0x2b0 [planar][rgb][alpha][float]
+ 0: p=2 st=4 o=0 sh=0 d=32
+ 1: p=0 st=4 o=0 sh=0 d=32
+ 2: p=1 st=4 o=0 sh=0 d=32
+ 3: p=3 st=4 o=0 sh=0 d=32
+gbrapf32be: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][bb][a][rgb][be][float]
+ planes=4, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] 32/[0:0] 32/[0:0] }
+ 0: 32bits endian_bytes=4 {} {0:32} {} {}
+ 1: 32bits endian_bytes=4 {} {} {0:32} {}
+ 2: 32bits endian_bytes=4 {0:32} {} {} {}
+ 3: 32bits endian_bytes=4 {} {} {} {0:32}
+ AVD: name=gbrapf32be chroma=0:0 flags=0x2b1 [be][planar][rgb][alpha][float]
+ 0: p=2 st=4 o=0 sh=0 d=32
+ 1: p=0 st=4 o=0 sh=0 d=32
+ 2: p=1 st=4 o=0 sh=0 d=32
+ 3: p=3 st=4 o=0 sh=0 d=32
+gbrp: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8} {} {}
+ 1: 8bits {} {} {0:8} {}
+ 2: 8bits {0:8} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrp chroma=0:0 flags=0x30 [planar][rgb]
+ 0: p=2 st=1 o=0 sh=0 d=8
+ 1: p=0 st=1 o=0 sh=0 d=8
+ 2: p=1 st=1 o=0 sh=0 d=8
+gbrp1: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8/-7} {} {}
+ 1: 8bits {} {} {0:8/-7} {}
+ 2: 8bits {0:8/-7} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=-7 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+gbrp10: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-6} {} {}
+ 1: 16bits {} {} {0:16/-6} {}
+ 2: 16bits {0:16/-6} {} {} {}
+ Regular: planes=3 compbytes=2 bitpad=-6 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrp10le chroma=0:0 flags=0x30 [planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=10
+ 1: p=0 st=2 o=0 sh=0 d=10
+ 2: p=1 st=2 o=0 sh=0 d=10
+gbrp10be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ 2: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ AVD: name=gbrp10be chroma=0:0 flags=0x31 [be][planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=10
+ 1: p=0 st=2 o=0 sh=0 d=10
+ 2: p=1 st=2 o=0 sh=0 d=10
+gbrp12: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-4} {} {}
+ 1: 16bits {} {} {0:16/-4} {}
+ 2: 16bits {0:16/-4} {} {} {}
+ Regular: planes=3 compbytes=2 bitpad=-4 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrp12le chroma=0:0 flags=0x30 [planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=12
+ 1: p=0 st=2 o=0 sh=0 d=12
+ 2: p=1 st=2 o=0 sh=0 d=12
+gbrp12be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ 2: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ AVD: name=gbrp12be chroma=0:0 flags=0x31 [be][planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=12
+ 1: p=0 st=2 o=0 sh=0 d=12
+ 2: p=1 st=2 o=0 sh=0 d=12
+gbrp14: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-2} {} {}
+ 1: 16bits {} {} {0:16/-2} {}
+ 2: 16bits {0:16/-2} {} {} {}
+ Regular: planes=3 compbytes=2 bitpad=-2 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrp14le chroma=0:0 flags=0x30 [planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=14
+ 1: p=0 st=2 o=0 sh=0 d=14
+ 2: p=1 st=2 o=0 sh=0 d=14
+gbrp14be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-2} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-2} {}
+ 2: 16bits endian_bytes=2 {0:16/-2} {} {} {}
+ AVD: name=gbrp14be chroma=0:0 flags=0x31 [be][planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=14
+ 1: p=0 st=2 o=0 sh=0 d=14
+ 2: p=1 st=2 o=0 sh=0 d=14
+gbrp16: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16} {} {}
+ 1: 16bits {} {} {0:16} {}
+ 2: 16bits {0:16} {} {} {}
+ Regular: planes=3 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrp16le chroma=0:0 flags=0x30 [planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=16
+ 1: p=0 st=2 o=0 sh=0 d=16
+ 2: p=1 st=2 o=0 sh=0 d=16
+gbrp16be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16} {}
+ 2: 16bits endian_bytes=2 {0:16} {} {} {}
+ AVD: name=gbrp16be chroma=0:0 flags=0x31 [be][planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=16
+ 1: p=0 st=2 o=0 sh=0 d=16
+ 2: p=1 st=2 o=0 sh=0 d=16
+gbrp2: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8/-6} {} {}
+ 1: 8bits {} {} {0:8/-6} {}
+ 2: 8bits {0:8/-6} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=-6 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+gbrp3: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8/-5} {} {}
+ 1: 8bits {} {} {0:8/-5} {}
+ 2: 8bits {0:8/-5} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=-5 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+gbrp4: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8/-4} {} {}
+ 1: 8bits {} {} {0:8/-4} {}
+ 2: 8bits {0:8/-4} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=-4 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+gbrp5: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8/-3} {} {}
+ 1: 8bits {} {} {0:8/-3} {}
+ 2: 8bits {0:8/-3} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=-3 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+gbrp6: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {} {0:8/-2} {} {}
+ 1: 8bits {} {} {0:8/-2} {}
+ 2: 8bits {0:8/-2} {} {} {}
+ Regular: planes=3 compbytes=1 bitpad=-2 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+gbrp9: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {} {0:16/-7} {} {}
+ 1: 16bits {} {} {0:16/-7} {}
+ 2: 16bits {0:16/-7} {} {} {}
+ Regular: planes=3 compbytes=2 bitpad=-7 chroma=1x1 ctype=uint
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrp9le chroma=0:0 flags=0x30 [planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=9
+ 1: p=0 st=2 o=0 sh=0 d=9
+ 2: p=1 st=2 o=0 sh=0 d=9
+gbrp9be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 1: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ 2: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ AVD: name=gbrp9be chroma=0:0 flags=0x31 [be][planar][rgb]
+ 0: p=2 st=2 o=0 sh=0 d=9
+ 1: p=0 st=2 o=0 sh=0 d=9
+ 2: p=1 st=2 o=0 sh=0 d=9
+gbrpf32: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][bb][rgb][le][float]
+ planes=3, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] 32/[0:0] }
+ 0: 32bits {} {0:32} {} {}
+ 1: 32bits {} {} {0:32} {}
+ 2: 32bits {0:32} {} {} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=1x1 ctype=float
+ 0: {2}
+ 1: {3}
+ 2: {1}
+ AVD: name=gbrpf32le chroma=0:0 flags=0x230 [planar][rgb][float]
+ 0: p=2 st=4 o=0 sh=0 d=32
+ 1: p=0 st=4 o=0 sh=0 d=32
+ 2: p=1 st=4 o=0 sh=0 d=32
+gbrpf32be: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][bb][rgb][be][float]
+ planes=3, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] 32/[0:0] }
+ 0: 32bits endian_bytes=4 {} {0:32} {} {}
+ 1: 32bits endian_bytes=4 {} {} {0:32} {}
+ 2: 32bits endian_bytes=4 {0:32} {} {} {}
+ AVD: name=gbrpf32be chroma=0:0 flags=0x231 [be][planar][rgb][float]
+ 0: p=2 st=4 o=0 sh=0 d=32
+ 1: p=0 st=4 o=0 sh=0 d=32
+ 2: p=1 st=4 o=0 sh=0 d=32
+gray: ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ AVD: name=gray chroma=0:0 flags=0x0
+ 0: p=0 st=1 o=0 sh=0 d=8
+gray10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ Regular: planes=1 compbytes=2 bitpad=-6 chroma=1x1 ctype=uint
+ 0: {1}
+ AVD: name=gray10le chroma=0:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=10
+gray10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ AVD: name=gray10be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=2 o=0 sh=0 d=10
+gray12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:16/-4} {} {} {}
+ Regular: planes=1 compbytes=2 bitpad=-4 chroma=1x1 ctype=uint
+ 0: {1}
+ AVD: name=gray12le chroma=0:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=12
+gray12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ AVD: name=gray12be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=2 o=0 sh=0 d=12
+gray14: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:16/-2} {} {} {}
+ Regular: planes=1 compbytes=2 bitpad=-2 chroma=1x1 ctype=uint
+ 0: {1}
+ AVD: name=gray14le chroma=0:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=14
+gray14be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-2} {} {} {}
+ AVD: name=gray14be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=2 o=0 sh=0 d=14
+gray16: ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ AVD: name=gray16le chroma=0:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=16
+gray16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ AVD: name=gray16be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=2 o=0 sh=0 d=16
+gray9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:16/-7} {} {} {}
+ Regular: planes=1 compbytes=2 bitpad=-7 chroma=1x1 ctype=uint
+ 0: {1}
+ AVD: name=gray9le chroma=0:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=9
+gray9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][gray][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ AVD: name=gray9be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=2 o=0 sh=0 d=9
+grayaf32: ctype=float
+ Basic desc: [ba][bb][a][yuv][gray][le][float]
+ planes=2, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {} {} {0:32}
+ Regular: planes=2 compbytes=4 bitpad=0 chroma=1x1 ctype=float
+ 0: {1}
+ 1: {4}
+grayf32: [GENERIC] ctype=float
+ Basic desc: [ba][bb][yuv][gray][le][float]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ Regular: planes=1 compbytes=4 bitpad=0 chroma=1x1 ctype=float
+ 0: {1}
+ AVD: name=grayf32le chroma=0:0 flags=0x200 [float]
+ 0: p=0 st=4 o=0 sh=0 d=32
+grayf32be: [GENERIC] ctype=float
+ Basic desc: [ba][bb][yuv][gray][be][float]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=4 {0:32} {} {} {}
+ AVD: name=grayf32be chroma=0:0 flags=0x201 [be][float]
+ 0: p=0 st=4 o=0 sh=0 d=32
+mediacodec: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=mediacodec chroma=0:0 flags=0x8 [hw]
+mmal: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=mmal chroma=0:0 flags=0x8 [hw]
+monob: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [rgb][gray][le][be][uint]
+ planes=1, chroma=0:0 align=8:1
+ {1/[0:0] }
+ 0: 1bits {0:1} {} {} {}
+ AVD: name=monob chroma=0:0 flags=0x4 [bs]
+ 0: p=0 st=1 o=0 sh=7 d=1
+monow: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [rgb][gray][le][be][uint]
+ planes=1, chroma=0:0 align=8:1
+ {1/[0:0] }
+ 0: 1bits {0:1} {} {} {}
+ AVD: name=monow chroma=0:0 flags=0x4 [bs]
+ 0: p=0 st=1 o=0 sh=0 d=1
+nv12: ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][be][uint]
+ planes=2, chroma=1:1 align=2:2
+ {8/[0:0] 16/[1:1] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 16bits {} {0:8} {8:8} {}
+ Regular: planes=2 compbytes=1 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=nv12 chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=2 o=0 sh=0 d=8
+ 2: p=1 st=2 o=1 sh=0 d=8
+nv16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][be][uint]
+ planes=2, chroma=1:0 align=2:1
+ {8/[0:0] 16/[1:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 16bits {} {0:8} {8:8} {}
+ Regular: planes=2 compbytes=1 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=nv16 chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=2 o=0 sh=0 d=8
+ 2: p=1 st=2 o=1 sh=0 d=8
+nv20: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 32bits {} {0:16/-6} {16:16/-6} {}
+ Regular: planes=2 compbytes=2 bitpad=-6 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=nv20le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=4 o=0 sh=0 d=10
+ 2: p=1 st=4 o=2 sh=0 d=10
+nv20be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/-6} {16:16/-6} {}
+ AVD: name=nv20be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=4 o=0 sh=0 d=10
+ 2: p=1 st=4 o=2 sh=0 d=10
+nv21: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][be][uint]
+ planes=2, chroma=1:1 align=2:2
+ {8/[0:0] 16/[1:1] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 16bits {} {8:8} {0:8} {}
+ Regular: planes=2 compbytes=1 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {3, 2}
+ AVD: name=nv21 chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=2 o=1 sh=0 d=8
+ 2: p=1 st=2 o=0 sh=0 d=8
+nv24: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][be][uint]
+ planes=2, chroma=0:0 align=1:1
+ {8/[0:0] 16/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 16bits {} {0:8} {8:8} {}
+ Regular: planes=2 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=nv24 chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=2 o=0 sh=0 d=8
+ 2: p=1 st=2 o=1 sh=0 d=8
+nv42: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][be][uint]
+ planes=2, chroma=0:0 align=1:1
+ {8/[0:0] 16/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 16bits {} {8:8} {0:8} {}
+ Regular: planes=2 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {3, 2}
+ AVD: name=nv42 chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=2 o=1 sh=0 d=8
+ 2: p=1 st=2 o=0 sh=0 d=8
+opencl: [GENERIC] ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=opencl chroma=0:0 flags=0x8 [hw]
+p010: ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:1 align=2:2
+ {16/[0:0] 32/[1:1] }
+ 0: 16bits {0:16/6} {} {} {}
+ 1: 32bits {} {0:16/6} {16:16/6} {}
+ Regular: planes=2 compbytes=2 bitpad=6 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p010le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=6 d=10
+ 1: p=1 st=4 o=0 sh=6 d=10
+ 2: p=1 st=4 o=2 sh=6 d=10
+p010be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:1 align=2:2
+ {16/[0:0] 32/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16/6} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/6} {16:16/6} {}
+ AVD: name=p010be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=6 d=10
+ 1: p=1 st=4 o=0 sh=6 d=10
+ 2: p=1 st=4 o=2 sh=6 d=10
+p012: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:1 align=2:2
+ {16/[0:0] 32/[1:1] }
+ 0: 16bits {0:16/4} {} {} {}
+ 1: 32bits {} {0:16/4} {16:16/4} {}
+ Regular: planes=2 compbytes=2 bitpad=4 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p012le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=4 d=12
+ 1: p=1 st=4 o=0 sh=4 d=12
+ 2: p=1 st=4 o=2 sh=4 d=12
+p012be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:1 align=2:2
+ {16/[0:0] 32/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16/4} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/4} {16:16/4} {}
+ AVD: name=p012be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=4 d=12
+ 1: p=1 st=4 o=0 sh=4 d=12
+ 2: p=1 st=4 o=2 sh=4 d=12
+p016: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:1 align=2:2
+ {16/[0:0] 32/[1:1] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 32bits {} {0:16} {16:16} {}
+ Regular: planes=2 compbytes=2 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p016le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=4 o=0 sh=0 d=16
+ 2: p=1 st=4 o=2 sh=0 d=16
+p016be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:1 align=2:2
+ {16/[0:0] 32/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16} {16:16} {}
+ AVD: name=p016be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=4 o=0 sh=0 d=16
+ 2: p=1 st=4 o=2 sh=0 d=16
+p210: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits {0:16/6} {} {} {}
+ 1: 32bits {} {0:16/6} {16:16/6} {}
+ Regular: planes=2 compbytes=2 bitpad=6 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p210le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=6 d=10
+ 1: p=1 st=4 o=0 sh=6 d=10
+ 2: p=1 st=4 o=2 sh=6 d=10
+p210be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/6} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/6} {16:16/6} {}
+ AVD: name=p210be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=6 d=10
+ 1: p=1 st=4 o=0 sh=6 d=10
+ 2: p=1 st=4 o=2 sh=6 d=10
+p212: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits {0:16/4} {} {} {}
+ 1: 32bits {} {0:16/4} {16:16/4} {}
+ Regular: planes=2 compbytes=2 bitpad=4 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p212le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=4 d=12
+ 1: p=1 st=4 o=0 sh=4 d=12
+ 2: p=1 st=4 o=2 sh=4 d=12
+p212be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/4} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/4} {16:16/4} {}
+ AVD: name=p212be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=4 d=12
+ 1: p=1 st=4 o=0 sh=4 d=12
+ 2: p=1 st=4 o=2 sh=4 d=12
+p216: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 32bits {} {0:16} {16:16} {}
+ Regular: planes=2 compbytes=2 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p216le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=4 o=0 sh=0 d=16
+ 2: p=1 st=4 o=2 sh=0 d=16
+p216be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=1:0 align=2:1
+ {16/[0:0] 32/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16} {16:16} {}
+ AVD: name=p216be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=4 o=0 sh=0 d=16
+ 2: p=1 st=4 o=2 sh=0 d=16
+p410: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 32/[0:0] }
+ 0: 16bits {0:16/6} {} {} {}
+ 1: 32bits {} {0:16/6} {16:16/6} {}
+ Regular: planes=2 compbytes=2 bitpad=6 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p410le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=6 d=10
+ 1: p=1 st=4 o=0 sh=6 d=10
+ 2: p=1 st=4 o=2 sh=6 d=10
+p410be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 32/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/6} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/6} {16:16/6} {}
+ AVD: name=p410be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=6 d=10
+ 1: p=1 st=4 o=0 sh=6 d=10
+ 2: p=1 st=4 o=2 sh=6 d=10
+p412: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 32/[0:0] }
+ 0: 16bits {0:16/4} {} {} {}
+ 1: 32bits {} {0:16/4} {16:16/4} {}
+ Regular: planes=2 compbytes=2 bitpad=4 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p412le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=4 d=12
+ 1: p=1 st=4 o=0 sh=4 d=12
+ 2: p=1 st=4 o=2 sh=4 d=12
+p412be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 32/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/4} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16/4} {16:16/4} {}
+ AVD: name=p412be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=4 d=12
+ 1: p=1 st=4 o=0 sh=4 d=12
+ 2: p=1 st=4 o=2 sh=4 d=12
+p416: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][le][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 32/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 32bits {} {0:16} {16:16} {}
+ Regular: planes=2 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2, 3}
+ AVD: name=p416le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=4 o=0 sh=0 d=16
+ 2: p=1 st=4 o=2 sh=0 d=16
+p416be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][nv][yuv][be][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 32/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 32bits endian_bytes=2 {} {0:16} {16:16} {}
+ AVD: name=p416be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=4 o=0 sh=0 d=16
+ 2: p=1 st=4 o=2 sh=0 d=16
+pal8: fcsp=rgb ctype=unknown
+ Basic desc: [ba][a][rgb][le][be][pal]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {} {} {} {}
+ AVD: name=pal8 chroma=0:0 flags=0x82 [pal][alpha]
+ 0: p=0 st=1 o=0 sh=0 d=8
+qsv: [GENERIC] ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=qsv chroma=0:0 flags=0x8 [hw]
+rgb0: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {0:8} {8:8} {16:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 2, 3, 0}
+ AVD: name=rgb0 chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=4 o=0 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=2 sh=0 d=8
+rgb24: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {24/[0:0] }
+ 0: 24bits {0:8} {8:8} {16:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 2, 3}
+ AVD: name=rgb24 chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=3 o=0 sh=0 d=8
+ 1: p=0 st=3 o=1 sh=0 d=8
+ 2: p=0 st=3 o=2 sh=0 d=8
+rgb30: fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {20:10} {10:10} {0:10} {}
+ AVD: name=x2rgb10le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=4 o=2 sh=4 d=10
+ 1: p=0 st=4 o=1 sh=2 d=10
+ 2: p=0 st=4 o=0 sh=0 d=10
+rgb4: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [rgb][le][be][uint]
+ planes=1, chroma=0:0 align=2:1
+ {4/[0:0] }
+ 0: 4bits {0:1} {1:2} {3:1} {}
+ AVD: name=rgb4 chroma=0:0 flags=0x24 [bs][rgb]
+ 0: p=0 st=4 o=0 sh=0 d=1
+ 1: p=0 st=4 o=1 sh=0 d=2
+ 2: p=0 st=4 o=3 sh=0 d=1
+rgb444: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {8:4} {4:4} {0:4} {}
+ AVD: name=rgb444le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=2 o=1 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=4 d=4
+ 2: p=0 st=2 o=0 sh=0 d=4
+rgb444be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {8:4} {4:4} {0:4} {}
+ AVD: name=rgb444be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=2 o=-1 sh=0 d=4
+ 1: p=0 st=2 o=0 sh=4 d=4
+ 2: p=0 st=2 o=0 sh=0 d=4
+rgb48: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {48/[0:0] }
+ 0: 48bits {0:16} {16:16} {32:16} {}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 2, 3}
+ AVD: name=rgb48le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=6 o=0 sh=0 d=16
+ 1: p=0 st=6 o=2 sh=0 d=16
+ 2: p=0 st=6 o=4 sh=0 d=16
+rgb48be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {48/[0:0] }
+ 0: 48bits endian_bytes=2 {0:16} {16:16} {32:16} {}
+ AVD: name=rgb48be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=6 o=0 sh=0 d=16
+ 1: p=0 st=6 o=2 sh=0 d=16
+ 2: p=0 st=6 o=4 sh=0 d=16
+rgb4_byte: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {3:1} {1:2} {0:1} {}
+ AVD: name=rgb4_byte chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=1 o=0 sh=3 d=1
+ 1: p=0 st=1 o=0 sh=1 d=2
+ 2: p=0 st=1 o=0 sh=0 d=1
+rgb555: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {10:5} {5:5} {0:5} {}
+ AVD: name=rgb555le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=2 o=1 sh=2 d=5
+ 1: p=0 st=2 o=0 sh=5 d=5
+ 2: p=0 st=2 o=0 sh=0 d=5
+rgb555be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {10:5} {5:5} {0:5} {}
+ AVD: name=rgb555be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=2 o=-1 sh=2 d=5
+ 1: p=0 st=2 o=0 sh=5 d=5
+ 2: p=0 st=2 o=0 sh=0 d=5
+rgb565: fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {11:5} {5:6} {0:5} {}
+ AVD: name=rgb565le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=2 o=1 sh=3 d=5
+ 1: p=0 st=2 o=0 sh=5 d=6
+ 2: p=0 st=2 o=0 sh=0 d=5
+rgb565be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits endian_bytes=2 {11:5} {5:6} {0:5} {}
+ AVD: name=rgb565be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=2 o=-1 sh=3 d=5
+ 1: p=0 st=2 o=0 sh=5 d=6
+ 2: p=0 st=2 o=0 sh=0 d=5
+rgb8: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {5:3} {2:3} {0:2} {}
+ AVD: name=rgb8 chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=1 o=0 sh=6 d=2
+ 1: p=0 st=1 o=0 sh=3 d=3
+ 2: p=0 st=1 o=0 sh=0 d=3
+rgba: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {0:8} {8:8} {16:8} {24:8}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 2, 3, 4}
+ AVD: name=rgba chroma=0:0 flags=0xa0 [rgb][alpha]
+ 0: p=0 st=4 o=0 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=2 sh=0 d=8
+ 3: p=0 st=4 o=3 sh=0 d=8
+rgba64: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits {0:16} {16:16} {32:16} {48:16}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 2, 3, 4}
+ AVD: name=rgba64le chroma=0:0 flags=0xa0 [rgb][alpha]
+ 0: p=0 st=8 o=0 sh=0 d=16
+ 1: p=0 st=8 o=2 sh=0 d=16
+ 2: p=0 st=8 o=4 sh=0 d=16
+ 3: p=0 st=8 o=6 sh=0 d=16
+rgba64be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][a][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits endian_bytes=2 {0:16} {16:16} {32:16} {48:16}
+ AVD: name=rgba64be chroma=0:0 flags=0xa1 [be][rgb][alpha]
+ 0: p=0 st=8 o=0 sh=0 d=16
+ 1: p=0 st=8 o=2 sh=0 d=16
+ 2: p=0 st=8 o=4 sh=0 d=16
+ 3: p=0 st=8 o=6 sh=0 d=16
+rgbaf16: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][bb][a][rgb][le][float]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits {0:16} {16:16} {32:16} {48:16}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=float
+ 0: {1, 2, 3, 4}
+ AVD: name=rgbaf16le chroma=0:0 flags=0x2a0 [rgb][alpha][float]
+ 0: p=0 st=8 o=0 sh=0 d=16
+ 1: p=0 st=8 o=2 sh=0 d=16
+ 2: p=0 st=8 o=4 sh=0 d=16
+ 3: p=0 st=8 o=6 sh=0 d=16
+rgbaf16be: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][bb][a][rgb][be][float]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits endian_bytes=2 {0:16} {16:16} {32:16} {48:16}
+ AVD: name=rgbaf16be chroma=0:0 flags=0x2a1 [be][rgb][alpha][float]
+ 0: p=0 st=8 o=0 sh=0 d=16
+ 1: p=0 st=8 o=2 sh=0 d=16
+ 2: p=0 st=8 o=4 sh=0 d=16
+ 3: p=0 st=8 o=6 sh=0 d=16
+rgbaf32: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][a][rgb][le][be][float]
+ planes=1, chroma=0:0 align=1:1
+ {-128/[0:0] }
+ 0: -128bits {} {} {} {}
+ [NOALLOC]
+ AVD: name=rgbaf32le chroma=0:0 flags=0x2a0 [rgb][alpha][float]
+ 0: p=0 st=16 o=0 sh=0 d=32
+ 1: p=0 st=16 o=4 sh=0 d=32
+ 2: p=0 st=16 o=8 sh=0 d=32
+ 3: p=0 st=16 o=12 sh=0 d=32
+rgbaf32be: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][a][rgb][le][be][float]
+ planes=1, chroma=0:0 align=1:1
+ {-128/[0:0] }
+ 0: -128bits endian_bytes=4 {} {} {} {}
+ [NOALLOC]
+ AVD: name=rgbaf32be chroma=0:0 flags=0x2a1 [be][rgb][alpha][float]
+ 0: p=0 st=16 o=0 sh=0 d=32
+ 1: p=0 st=16 o=4 sh=0 d=32
+ 2: p=0 st=16 o=8 sh=0 d=32
+ 3: p=0 st=16 o=12 sh=0 d=32
+rgbf32: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][rgb][le][be][float]
+ planes=1, chroma=0:0 align=1:1
+ {96/[0:0] }
+ 0: 96bits {} {} {} {}
+ AVD: name=rgbf32le chroma=0:0 flags=0x220 [rgb][float]
+ 0: p=0 st=12 o=0 sh=0 d=32
+ 1: p=0 st=12 o=4 sh=0 d=32
+ 2: p=0 st=12 o=8 sh=0 d=32
+rgbf32be: [GENERIC] fcsp=rgb ctype=float
+ Basic desc: [ba][rgb][le][be][float]
+ planes=1, chroma=0:0 align=1:1
+ {96/[0:0] }
+ 0: 96bits endian_bytes=4 {} {} {} {}
+ AVD: name=rgbf32be chroma=0:0 flags=0x221 [be][rgb][float]
+ 0: p=0 st=12 o=0 sh=0 d=32
+ 1: p=0 st=12 o=4 sh=0 d=32
+ 2: p=0 st=12 o=8 sh=0 d=32
+uyvy422: ctype=uint
+ Basic desc: [ba][yuv][le][be][uint]
+ planes=1, chroma=1:0 align=2:1
+ {16/[0:0] }
+ 0: 16bits {8:8} {0:8} {16:8} {}
+ luma_offsets=[ 8 24]
+ AVD: name=uyvy422 chroma=1:0 flags=0x0
+ 0: p=0 st=2 o=1 sh=0 d=8
+ 1: p=0 st=4 o=0 sh=0 d=8
+ 2: p=0 st=4 o=2 sh=0 d=8
+uyyvyy411: [GENERIC] ctype=uint
+ Basic desc: [yuv][le][be][uint]
+ planes=1, chroma=2:0 align=4:1
+ {12/[0:0] }
+ 0: 12bits {8:8} {0:8} {24:8} {}
+ luma_offsets=[ 8 16 32 40]
+ AVD: name=uyyvyy411 chroma=2:0 flags=0x0
+ 0: p=0 st=4 o=1 sh=0 d=8
+ 1: p=0 st=6 o=0 sh=0 d=8
+ 2: p=0 st=6 o=3 sh=0 d=8
+vaapi: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=1:1 align=2:2
+ {}
+ AVD: name=vaapi chroma=1:1 flags=0x8 [hw]
+vdpau: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=1:1 align=2:2
+ {}
+ AVD: name=vdpau chroma=1:1 flags=0x8 [hw]
+vdpau_output: fcsp=rgb ctype=unknown
+ Basic desc: [rgb][le][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+videotoolbox: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=videotoolbox_vld chroma=0:0 flags=0x8 [hw]
+vulkan: ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=vulkan chroma=0:0 flags=0x8 [hw]
+vuya: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuv][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {16:8} {8:8} {0:8} {24:8}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1, 4}
+ AVD: name=vuya chroma=0:0 flags=0x80 [alpha]
+ 0: p=0 st=4 o=2 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=0 sh=0 d=8
+ 3: p=0 st=4 o=3 sh=0 d=8
+vuyx: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuv][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {16:8} {8:8} {0:8} {}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {3, 2, 1, 0}
+ AVD: name=vuyx chroma=0:0 flags=0x0
+ 0: p=0 st=4 o=2 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=0 sh=0 d=8
+x2bgr10: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {0:10} {10:10} {20:10} {}
+ AVD: name=x2bgr10le chroma=0:0 flags=0x20 [rgb]
+ 0: p=0 st=4 o=0 sh=0 d=10
+ 1: p=0 st=4 o=1 sh=2 d=10
+ 2: p=0 st=4 o=2 sh=4 d=10
+x2bgr10be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=4 {0:10} {10:10} {20:10} {}
+ AVD: name=x2bgr10be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=4 o=2 sh=0 d=10
+ 1: p=0 st=4 o=1 sh=2 d=10
+ 2: p=0 st=4 o=0 sh=4 d=10
+x2rgb10be: [GENERIC] fcsp=rgb ctype=uint
+ Basic desc: [ba][rgb][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=4 {20:10} {10:10} {0:10} {}
+ AVD: name=x2rgb10be chroma=0:0 flags=0x21 [be][rgb]
+ 0: p=0 st=4 o=0 sh=4 d=10
+ 1: p=0 st=4 o=1 sh=2 d=10
+ 2: p=0 st=4 o=2 sh=0 d=10
+xv30: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {10:10} {0:10} {20:10} {}
+ AVD: name=xv30le chroma=0:0 flags=0x0
+ 0: p=0 st=4 o=1 sh=2 d=10
+ 1: p=0 st=4 o=0 sh=0 d=10
+ 2: p=0 st=4 o=2 sh=4 d=10
+xv30be: [GENERIC] ctype=unknown
+ Basic desc: [ba][yuv][le][be]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=4 {} {} {} {}
+ AVD: name=xv30be chroma=0:0 flags=0x5 [be][bs]
+ 0: p=0 st=32 o=10 sh=0 d=10
+ 1: p=0 st=32 o=0 sh=0 d=10
+ 2: p=0 st=32 o=20 sh=0 d=10
+xv36: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuv][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits {16:16/4} {0:16/4} {32:16/4} {}
+ Regular: planes=1 compbytes=2 bitpad=4 chroma=1x1 ctype=uint
+ 0: {2, 1, 3, 0}
+ AVD: name=xv36le chroma=0:0 flags=0x0
+ 0: p=0 st=8 o=2 sh=4 d=12
+ 1: p=0 st=8 o=0 sh=4 d=12
+ 2: p=0 st=8 o=4 sh=4 d=12
+xv36be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuv][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {64/[0:0] }
+ 0: 64bits endian_bytes=2 {16:16/4} {0:16/4} {32:16/4} {}
+ AVD: name=xv36be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=8 o=2 sh=4 d=12
+ 1: p=0 st=8 o=0 sh=4 d=12
+ 2: p=0 st=8 o=4 sh=4 d=12
+xvmc: [GENERIC] ctype=unknown
+ Basic desc: [le][be][hw]
+ planes=0, chroma=0:0 align=1:1
+ {}
+ AVD: name=xvmc chroma=0:0 flags=0x8 [hw]
+xyz12: [GENERIC] fcsp=xyz ctype=uint
+ Basic desc: [ba][bb][xyz][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {48/[0:0] }
+ 0: 48bits {0:16/4} {16:16/4} {32:16/4} {}
+ Regular: planes=1 compbytes=2 bitpad=4 chroma=1x1 ctype=uint
+ 0: {1, 2, 3}
+ AVD: name=xyz12le chroma=0:0 flags=0x0
+ 0: p=0 st=6 o=0 sh=4 d=12
+ 1: p=0 st=6 o=2 sh=4 d=12
+ 2: p=0 st=6 o=4 sh=4 d=12
+xyz12be: [GENERIC] fcsp=xyz ctype=uint
+ Basic desc: [ba][bb][xyz][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {48/[0:0] }
+ 0: 48bits endian_bytes=2 {0:16/4} {16:16/4} {32:16/4} {}
+ AVD: name=xyz12be chroma=0:0 flags=0x1 [be]
+ 0: p=0 st=6 o=0 sh=4 d=12
+ 1: p=0 st=6 o=2 sh=4 d=12
+ 2: p=0 st=6 o=4 sh=4 d=12
+y1: fcsp=rgb ctype=uint
+ Basic desc: [ba][bb][rgb][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {8/[0:0] }
+ 0: 8bits {0:8/-7} {} {} {}
+ Regular: planes=1 compbytes=1 bitpad=-7 chroma=1x1 ctype=uint
+ 0: {1}
+y210: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][le][uint]
+ planes=1, chroma=1:0 align=2:1
+ {32/[0:0] }
+ 0: 32bits {0:16/6} {16:16/6} {48:16/6} {}
+ luma_offsets=[ 0 32]
+ AVD: name=y210le chroma=1:0 flags=0x0
+ 0: p=0 st=4 o=0 sh=6 d=10
+ 1: p=0 st=8 o=2 sh=6 d=10
+ 2: p=0 st=8 o=6 sh=6 d=10
+y210be: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][be][uint]
+ planes=1, chroma=1:0 align=2:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=2 {0:16/6} {16:16/6} {48:16/6} {}
+ luma_offsets=[ 0 32]
+ AVD: name=y210be chroma=1:0 flags=0x1 [be]
+ 0: p=0 st=4 o=0 sh=6 d=10
+ 1: p=0 st=8 o=2 sh=6 d=10
+ 2: p=0 st=8 o=6 sh=6 d=10
+y212: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][le][uint]
+ planes=1, chroma=1:0 align=2:1
+ {32/[0:0] }
+ 0: 32bits {0:16/4} {16:16/4} {48:16/4} {}
+ luma_offsets=[ 0 32]
+ AVD: name=y212le chroma=1:0 flags=0x0
+ 0: p=0 st=4 o=0 sh=4 d=12
+ 1: p=0 st=8 o=2 sh=4 d=12
+ 2: p=0 st=8 o=6 sh=4 d=12
+y212be: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][be][uint]
+ planes=1, chroma=1:0 align=2:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=2 {0:16/4} {16:16/4} {48:16/4} {}
+ luma_offsets=[ 0 32]
+ AVD: name=y212be chroma=1:0 flags=0x1 [be]
+ 0: p=0 st=4 o=0 sh=4 d=12
+ 1: p=0 st=8 o=2 sh=4 d=12
+ 2: p=0 st=8 o=6 sh=4 d=12
+ya16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuv][gray][le][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits {0:16} {} {} {16:16}
+ Regular: planes=1 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 4}
+ AVD: name=ya16le chroma=0:0 flags=0x80 [alpha]
+ 0: p=0 st=4 o=0 sh=0 d=16
+ 1: p=0 st=4 o=2 sh=0 d=16
+ya16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuv][gray][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {32/[0:0] }
+ 0: 32bits endian_bytes=2 {0:16} {} {} {16:16}
+ AVD: name=ya16be chroma=0:0 flags=0x81 [be][alpha]
+ 0: p=0 st=4 o=0 sh=0 d=16
+ 1: p=0 st=4 o=2 sh=0 d=16
+ya8: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuv][gray][le][be][uint]
+ planes=1, chroma=0:0 align=1:1
+ {16/[0:0] }
+ 0: 16bits {0:8} {} {} {8:8}
+ Regular: planes=1 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1, 4}
+ AVD: name=ya8 chroma=0:0 flags=0x80 [alpha]
+ 0: p=0 st=2 o=0 sh=0 d=8
+ 1: p=0 st=2 o=1 sh=0 d=8
+yap16: ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][gray][le][uint]
+ planes=2, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {} {} {0:16}
+ Regular: planes=2 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {4}
+yap8: ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][gray][le][uint]
+ planes=2, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {} {} {0:8}
+ Regular: planes=2 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {4}
+yuv410p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=2:2 align=4:4
+ {8/[0:0] 8/[2:2] 8/[2:2] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=4x4 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv410p chroma=2:2 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuv410pf: ctype=float
+ Basic desc: [ba][bb][yuv][le][float]
+ planes=3, chroma=2:2 align=4:4
+ {32/[0:0] 32/[2:2] 32/[2:2] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=4x4 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+yuv411p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=2:0 align=4:1
+ {8/[0:0] 8/[2:0] 8/[2:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=4x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv411p chroma=2:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuv411pf: ctype=float
+ Basic desc: [ba][bb][yuv][le][float]
+ planes=3, chroma=2:0 align=4:1
+ {32/[0:0] 32/[2:0] 32/[2:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=4x1 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+yuv420p: ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=1:1 align=2:2
+ {8/[0:0] 8/[1:1] 8/[1:1] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv420p chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+ Ambiguous alias: yuvj420p
+yuv420p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ Regular: planes=3 compbytes=2 bitpad=-6 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv420p10le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv420p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ AVD: name=yuv420p10be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv420p12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits {0:16/-4} {} {} {}
+ 1: 16bits {} {0:16/-4} {} {}
+ 2: 16bits {} {} {0:16/-4} {}
+ Regular: planes=3 compbytes=2 bitpad=-4 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv420p12le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv420p12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ AVD: name=yuv420p12be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv420p14: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits {0:16/-2} {} {} {}
+ 1: 16bits {} {0:16/-2} {} {}
+ 2: 16bits {} {} {0:16/-2} {}
+ Regular: planes=3 compbytes=2 bitpad=-2 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv420p14le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=14
+ 1: p=1 st=2 o=0 sh=0 d=14
+ 2: p=2 st=2 o=0 sh=0 d=14
+yuv420p14be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16/-2} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-2} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-2} {}
+ AVD: name=yuv420p14be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=14
+ 1: p=1 st=2 o=0 sh=0 d=14
+ 2: p=2 st=2 o=0 sh=0 d=14
+yuv420p16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {0:16} {} {}
+ 2: 16bits {} {} {0:16} {}
+ Regular: planes=3 compbytes=2 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv420p16le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+yuv420p16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16} {}
+ AVD: name=yuv420p16be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+yuv420p9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits {0:16/-7} {} {} {}
+ 1: 16bits {} {0:16/-7} {} {}
+ 2: 16bits {} {} {0:16/-7} {}
+ Regular: planes=3 compbytes=2 bitpad=-7 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv420p9le chroma=1:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+yuv420p9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ AVD: name=yuv420p9be chroma=1:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+yuv420pf: ctype=float
+ Basic desc: [ba][bb][yuv][le][float]
+ planes=3, chroma=1:1 align=2:2
+ {32/[0:0] 32/[1:1] 32/[1:1] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=2x2 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+yuv422p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {8/[0:0] 8/[1:0] 8/[1:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv422p chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuv422p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ Regular: planes=3 compbytes=2 bitpad=-6 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv422p10le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv422p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ AVD: name=yuv422p10be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv422p12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits {0:16/-4} {} {} {}
+ 1: 16bits {} {0:16/-4} {} {}
+ 2: 16bits {} {} {0:16/-4} {}
+ Regular: planes=3 compbytes=2 bitpad=-4 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv422p12le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv422p12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ AVD: name=yuv422p12be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv422p14: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits {0:16/-2} {} {} {}
+ 1: 16bits {} {0:16/-2} {} {}
+ 2: 16bits {} {} {0:16/-2} {}
+ Regular: planes=3 compbytes=2 bitpad=-2 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv422p14le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=14
+ 1: p=1 st=2 o=0 sh=0 d=14
+ 2: p=2 st=2 o=0 sh=0 d=14
+yuv422p14be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/-2} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-2} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-2} {}
+ AVD: name=yuv422p14be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=14
+ 1: p=1 st=2 o=0 sh=0 d=14
+ 2: p=2 st=2 o=0 sh=0 d=14
+yuv422p16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {0:16} {} {}
+ 2: 16bits {} {} {0:16} {}
+ Regular: planes=3 compbytes=2 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv422p16le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+yuv422p16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16} {}
+ AVD: name=yuv422p16be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+yuv422p9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits {0:16/-7} {} {} {}
+ 1: 16bits {} {0:16/-7} {} {}
+ 2: 16bits {} {} {0:16/-7} {}
+ Regular: planes=3 compbytes=2 bitpad=-7 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv422p9le chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+yuv422p9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ AVD: name=yuv422p9be chroma=1:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+yuv422pf: ctype=float
+ Basic desc: [ba][bb][yuv][le][float]
+ planes=3, chroma=1:0 align=2:1
+ {32/[0:0] 32/[1:0] 32/[1:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=2x1 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+yuv440p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=0:1 align=1:2
+ {8/[0:0] 8/[0:1] 8/[0:1] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=1x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv440p chroma=0:1 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuv440p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:1 align=1:2
+ {16/[0:0] 16/[0:1] 16/[0:1] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ Regular: planes=3 compbytes=2 bitpad=-6 chroma=1x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv440p10le chroma=0:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv440p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:1 align=1:2
+ {16/[0:0] 16/[0:1] 16/[0:1] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ AVD: name=yuv440p10be chroma=0:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv440p12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:1 align=1:2
+ {16/[0:0] 16/[0:1] 16/[0:1] }
+ 0: 16bits {0:16/-4} {} {} {}
+ 1: 16bits {} {0:16/-4} {} {}
+ 2: 16bits {} {} {0:16/-4} {}
+ Regular: planes=3 compbytes=2 bitpad=-4 chroma=1x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv440p12le chroma=0:1 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv440p12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:1 align=1:2
+ {16/[0:0] 16/[0:1] 16/[0:1] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ AVD: name=yuv440p12be chroma=0:1 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv440pf: ctype=float
+ Basic desc: [ba][bb][yuv][le][float]
+ planes=3, chroma=0:1 align=1:2
+ {32/[0:0] 32/[0:1] 32/[0:1] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=1x2 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+yuv444p: ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv444p chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+ Ambiguous alias: yuvj444p
+yuv444p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ Regular: planes=3 compbytes=2 bitpad=-6 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv444p10le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv444p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ AVD: name=yuv444p10be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+yuv444p12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-4} {} {} {}
+ 1: 16bits {} {0:16/-4} {} {}
+ 2: 16bits {} {} {0:16/-4} {}
+ Regular: planes=3 compbytes=2 bitpad=-4 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv444p12le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv444p12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ AVD: name=yuv444p12be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+yuv444p14: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-2} {} {} {}
+ 1: 16bits {} {0:16/-2} {} {}
+ 2: 16bits {} {} {0:16/-2} {}
+ Regular: planes=3 compbytes=2 bitpad=-2 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv444p14le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=14
+ 1: p=1 st=2 o=0 sh=0 d=14
+ 2: p=2 st=2 o=0 sh=0 d=14
+yuv444p14be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-2} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-2} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-2} {}
+ AVD: name=yuv444p14be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=14
+ 1: p=1 st=2 o=0 sh=0 d=14
+ 2: p=2 st=2 o=0 sh=0 d=14
+yuv444p16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {0:16} {} {}
+ 2: 16bits {} {} {0:16} {}
+ Regular: planes=3 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv444p16le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+yuv444p16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16} {}
+ AVD: name=yuv444p16be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+yuv444p9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-7} {} {} {}
+ 1: 16bits {} {0:16/-7} {} {}
+ 2: 16bits {} {} {0:16/-7} {}
+ Regular: planes=3 compbytes=2 bitpad=-7 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuv444p9le chroma=0:0 flags=0x10 [planar]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+yuv444p9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][be][uint]
+ planes=3, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ AVD: name=yuv444p9be chroma=0:0 flags=0x11 [be][planar]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+yuv444pf: ctype=float
+ Basic desc: [ba][bb][yuv][le][float]
+ planes=3, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ Regular: planes=3 compbytes=4 bitpad=0 chroma=1x1 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+yuva410pf: ctype=float
+ Basic desc: [ba][bb][a][yuv][le][float]
+ planes=4, chroma=2:2 align=4:4
+ {32/[0:0] 32/[2:2] 32/[2:2] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=4x4 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+yuva411pf: ctype=float
+ Basic desc: [ba][bb][a][yuv][le][float]
+ planes=4, chroma=2:0 align=4:1
+ {32/[0:0] 32/[2:0] 32/[2:0] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=4x1 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+yuva420p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][be][uint]
+ planes=4, chroma=1:1 align=2:2
+ {8/[0:0] 8/[1:1] 8/[1:1] 8/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ 3: 8bits {} {} {} {0:8}
+ Regular: planes=4 compbytes=1 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva420p chroma=1:1 flags=0x90 [planar][alpha]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+ 3: p=3 st=1 o=0 sh=0 d=8
+yuva420p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] 16/[0:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ 3: 16bits {} {} {} {0:16/-6}
+ Regular: planes=4 compbytes=2 bitpad=-6 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva420p10le chroma=1:1 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+yuva420p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-6}
+ AVD: name=yuva420p10be chroma=1:1 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+yuva420p16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] 16/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {0:16} {} {}
+ 2: 16bits {} {} {0:16} {}
+ 3: 16bits {} {} {} {0:16}
+ Regular: planes=4 compbytes=2 bitpad=0 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva420p16le chroma=1:1 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+yuva420p16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16}
+ AVD: name=yuva420p16be chroma=1:1 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+yuva420p9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] 16/[0:0] }
+ 0: 16bits {0:16/-7} {} {} {}
+ 1: 16bits {} {0:16/-7} {} {}
+ 2: 16bits {} {} {0:16/-7} {}
+ 3: 16bits {} {} {} {0:16/-7}
+ Regular: planes=4 compbytes=2 bitpad=-7 chroma=2x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva420p9le chroma=1:1 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+ 3: p=3 st=2 o=0 sh=0 d=9
+yuva420p9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:1 align=2:2
+ {16/[0:0] 16/[1:1] 16/[1:1] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-7}
+ AVD: name=yuva420p9be chroma=1:1 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+ 3: p=3 st=2 o=0 sh=0 d=9
+yuva420pf: ctype=float
+ Basic desc: [ba][bb][a][yuv][le][float]
+ planes=4, chroma=1:1 align=2:2
+ {32/[0:0] 32/[1:1] 32/[1:1] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=2x2 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+yuva422p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][be][uint]
+ planes=4, chroma=1:0 align=2:1
+ {8/[0:0] 8/[1:0] 8/[1:0] 8/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ 3: 8bits {} {} {} {0:8}
+ Regular: planes=4 compbytes=1 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva422p chroma=1:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+ 3: p=3 st=1 o=0 sh=0 d=8
+yuva422p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ 3: 16bits {} {} {} {0:16/-6}
+ Regular: planes=4 compbytes=2 bitpad=-6 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva422p10le chroma=1:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+yuva422p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-6}
+ AVD: name=yuva422p10be chroma=1:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+yuva422p12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits {0:16/-4} {} {} {}
+ 1: 16bits {} {0:16/-4} {} {}
+ 2: 16bits {} {} {0:16/-4} {}
+ 3: 16bits {} {} {} {0:16/-4}
+ Regular: planes=4 compbytes=2 bitpad=-4 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva422p12le chroma=1:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+ 3: p=3 st=2 o=0 sh=0 d=12
+yuva422p12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-4}
+ AVD: name=yuva422p12be chroma=1:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+ 3: p=3 st=2 o=0 sh=0 d=12
+yuva422p16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {0:16} {} {}
+ 2: 16bits {} {} {0:16} {}
+ 3: 16bits {} {} {} {0:16}
+ Regular: planes=4 compbytes=2 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva422p16le chroma=1:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+yuva422p16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16}
+ AVD: name=yuva422p16be chroma=1:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+yuva422p9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits {0:16/-7} {} {} {}
+ 1: 16bits {} {0:16/-7} {} {}
+ 2: 16bits {} {} {0:16/-7} {}
+ 3: 16bits {} {} {} {0:16/-7}
+ Regular: planes=4 compbytes=2 bitpad=-7 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva422p9le chroma=1:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+ 3: p=3 st=2 o=0 sh=0 d=9
+yuva422p9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=1:0 align=2:1
+ {16/[0:0] 16/[1:0] 16/[1:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-7}
+ AVD: name=yuva422p9be chroma=1:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+ 3: p=3 st=2 o=0 sh=0 d=9
+yuva422pf: ctype=float
+ Basic desc: [ba][bb][a][yuv][le][float]
+ planes=4, chroma=1:0 align=2:1
+ {32/[0:0] 32/[1:0] 32/[1:0] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=2x1 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+yuva440pf: ctype=float
+ Basic desc: [ba][bb][a][yuv][le][float]
+ planes=4, chroma=0:1 align=1:2
+ {32/[0:0] 32/[0:1] 32/[0:1] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=1x2 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+yuva444p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {8/[0:0] 8/[0:0] 8/[0:0] 8/[0:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ 3: 8bits {} {} {} {0:8}
+ Regular: planes=4 compbytes=1 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva444p chroma=0:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+ 3: p=3 st=1 o=0 sh=0 d=8
+yuva444p10: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-6} {} {} {}
+ 1: 16bits {} {0:16/-6} {} {}
+ 2: 16bits {} {} {0:16/-6} {}
+ 3: 16bits {} {} {} {0:16/-6}
+ Regular: planes=4 compbytes=2 bitpad=-6 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva444p10le chroma=0:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+yuva444p10be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-6} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-6} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-6} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-6}
+ AVD: name=yuva444p10be chroma=0:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=10
+ 1: p=1 st=2 o=0 sh=0 d=10
+ 2: p=2 st=2 o=0 sh=0 d=10
+ 3: p=3 st=2 o=0 sh=0 d=10
+yuva444p12: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-4} {} {} {}
+ 1: 16bits {} {0:16/-4} {} {}
+ 2: 16bits {} {} {0:16/-4} {}
+ 3: 16bits {} {} {} {0:16/-4}
+ Regular: planes=4 compbytes=2 bitpad=-4 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva444p12le chroma=0:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+ 3: p=3 st=2 o=0 sh=0 d=12
+yuva444p12be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-4} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-4} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-4} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-4}
+ AVD: name=yuva444p12be chroma=0:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=12
+ 1: p=1 st=2 o=0 sh=0 d=12
+ 2: p=2 st=2 o=0 sh=0 d=12
+ 3: p=3 st=2 o=0 sh=0 d=12
+yuva444p16: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16} {} {} {}
+ 1: 16bits {} {0:16} {} {}
+ 2: 16bits {} {} {0:16} {}
+ 3: 16bits {} {} {} {0:16}
+ Regular: planes=4 compbytes=2 bitpad=0 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva444p16le chroma=0:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+yuva444p16be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16}
+ AVD: name=yuva444p16be chroma=0:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=16
+ 1: p=1 st=2 o=0 sh=0 d=16
+ 2: p=2 st=2 o=0 sh=0 d=16
+ 3: p=3 st=2 o=0 sh=0 d=16
+yuva444p9: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][le][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits {0:16/-7} {} {} {}
+ 1: 16bits {} {0:16/-7} {} {}
+ 2: 16bits {} {} {0:16/-7} {}
+ 3: 16bits {} {} {} {0:16/-7}
+ Regular: planes=4 compbytes=2 bitpad=-7 chroma=1x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+ AVD: name=yuva444p9le chroma=0:0 flags=0x90 [planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+ 3: p=3 st=2 o=0 sh=0 d=9
+yuva444p9be: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][a][yuvp][yuv][be][uint]
+ planes=4, chroma=0:0 align=1:1
+ {16/[0:0] 16/[0:0] 16/[0:0] 16/[0:0] }
+ 0: 16bits endian_bytes=2 {0:16/-7} {} {} {}
+ 1: 16bits endian_bytes=2 {} {0:16/-7} {} {}
+ 2: 16bits endian_bytes=2 {} {} {0:16/-7} {}
+ 3: 16bits endian_bytes=2 {} {} {} {0:16/-7}
+ AVD: name=yuva444p9be chroma=0:0 flags=0x91 [be][planar][alpha]
+ 0: p=0 st=2 o=0 sh=0 d=9
+ 1: p=1 st=2 o=0 sh=0 d=9
+ 2: p=2 st=2 o=0 sh=0 d=9
+ 3: p=3 st=2 o=0 sh=0 d=9
+yuva444pf: ctype=float
+ Basic desc: [ba][bb][a][yuv][le][float]
+ planes=4, chroma=0:0 align=1:1
+ {32/[0:0] 32/[0:0] 32/[0:0] 32/[0:0] }
+ 0: 32bits {0:32} {} {} {}
+ 1: 32bits {} {0:32} {} {}
+ 2: 32bits {} {} {0:32} {}
+ 3: 32bits {} {} {} {0:32}
+ Regular: planes=4 compbytes=4 bitpad=0 chroma=1x1 ctype=float
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ 3: {4}
+yuvj411p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=2:0 align=4:1
+ {8/[0:0] 8/[2:0] 8/[2:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=4x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuvj411p chroma=2:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuvj422p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=1:0 align=2:1
+ {8/[0:0] 8/[1:0] 8/[1:0] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=2x1 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuvj422p chroma=1:0 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuvj440p: [GENERIC] ctype=uint
+ Basic desc: [ba][bb][yuvp][yuv][le][be][uint]
+ planes=3, chroma=0:1 align=1:2
+ {8/[0:0] 8/[0:1] 8/[0:1] }
+ 0: 8bits {0:8} {} {} {}
+ 1: 8bits {} {0:8} {} {}
+ 2: 8bits {} {} {0:8} {}
+ Regular: planes=3 compbytes=1 bitpad=0 chroma=1x2 ctype=uint
+ 0: {1}
+ 1: {2}
+ 2: {3}
+ AVD: name=yuvj440p chroma=0:1 flags=0x10 [planar]
+ 0: p=0 st=1 o=0 sh=0 d=8
+ 1: p=1 st=1 o=0 sh=0 d=8
+ 2: p=2 st=1 o=0 sh=0 d=8
+yuyv422: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][le][be][uint]
+ planes=1, chroma=1:0 align=2:1
+ {16/[0:0] }
+ 0: 16bits {0:8} {8:8} {24:8} {}
+ luma_offsets=[ 0 16]
+ AVD: name=yuyv422 chroma=1:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=8
+ 1: p=0 st=4 o=1 sh=0 d=8
+ 2: p=0 st=4 o=3 sh=0 d=8
+yvyu422: [GENERIC] ctype=uint
+ Basic desc: [ba][yuv][le][be][uint]
+ planes=1, chroma=1:0 align=2:1
+ {16/[0:0] }
+ 0: 16bits {0:8} {24:8} {8:8} {}
+ luma_offsets=[ 0 16]
+ AVD: name=yvyu422 chroma=1:0 flags=0x0
+ 0: p=0 st=2 o=0 sh=0 d=8
+ 1: p=0 st=4 o=3 sh=0 d=8
+ 2: p=0 st=4 o=1 sh=0 d=8
diff --git a/test/ref/repack.txt b/test/ref/repack.txt
new file mode 100644
index 0000000..89b29be
--- /dev/null
+++ b/test/ref/repack.txt
@@ -0,0 +1,385 @@
+0bgr => [pa] [un] gbrp | a=1:1 [tu] [tp]
+0bgr => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+0rgb => [pa] [un] gbrp | a=1:1 [tu] [tp]
+0rgb => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+abgr => [pa] [un] gbrap | a=1:1 [tu] [tp]
+abgr => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+argb => [pa] [un] gbrap | a=1:1 [tu] [tp]
+argb => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+ayuv64 => [pa] [un] yuva444p16 | a=1:1 [tu] [tp]
+ayuv64 => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+ayuv64be => [pa] [un] yuva444p16 | a=1:1 [tu] [tp]
+ayuv64be => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+bayer_bggr16 => no
+bayer_bggr16be => no
+bayer_bggr8 => no
+bayer_gbrg16 => no
+bayer_gbrg16be => no
+bayer_gbrg8 => no
+bayer_grbg16 => no
+bayer_grbg16be => no
+bayer_grbg8 => no
+bayer_rggb16 => no
+bayer_rggb16be => no
+bayer_rggb8 => no
+bgr0 => [pa] [un] gbrp | a=1:1 [tu] [tp]
+bgr0 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr24 => [pa] [un] gbrp | a=1:1 [tu] [tp]
+bgr24 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr4 => no
+bgr444 => [pa] [un] gbrp4 | a=1:1
+bgr444 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr444 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr444be => [pa] [un] gbrp4 | a=1:1
+bgr444be => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr444be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr48 => [pa] [un] gbrp16 | a=1:1 [tu] [tp]
+bgr48 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr48be => [pa] [un] gbrp16 | a=1:1 [tu] [tp]
+bgr48be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr4_byte => [pa] [un] gbrp2 | a=1:1
+bgr4_byte => [pa] [un] gbrp1 | a=1:1 [round-down]
+bgr4_byte => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr4_byte => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr555 => [pa] [un] gbrp5 | a=1:1
+bgr555 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr555 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr555be => [pa] [un] gbrp5 | a=1:1
+bgr555be => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr555be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr565 => [pa] [un] gbrp6 | a=1:1
+bgr565 => [pa] [un] gbrp5 | a=1:1 [round-down]
+bgr565 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr565 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr565be => [pa] [un] gbrp6 | a=1:1
+bgr565be => [pa] [un] gbrp5 | a=1:1 [round-down]
+bgr565be => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr565be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgr8 => [pa] [un] gbrp3 | a=1:1
+bgr8 => [pa] [un] gbrp2 | a=1:1 [round-down]
+bgr8 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+bgr8 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+bgra => [pa] [un] gbrap | a=1:1 [tu] [tp]
+bgra => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+bgra64 => [pa] [un] gbrap16 | a=1:1 [tu] [tp]
+bgra64 => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+bgra64be => [pa] [un] gbrap16 | a=1:1 [tu] [tp]
+bgra64be => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+cuda => no
+d3d11 => no
+d3d11va_vld => no
+drm_prime => no
+dxva2_vld => no
+gbrap => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap10 => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap10be => [pa] [un] gbrap10 | a=1:1
+gbrap10be => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap12 => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap12be => [pa] [un] gbrap12 | a=1:1
+gbrap12be => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap14 => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap14be => [pa] [un] gbrap14 | a=1:1
+gbrap14be => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap16 => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrap16be => [pa] [un] gbrap16 | a=1:1
+gbrap16be => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+gbrapf32be => [pa] [un] gbrapf32 | a=1:1
+gbrp => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp1 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp10 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp10be => [pa] [un] gbrp10 | a=1:1
+gbrp10be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp12 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp12be => [pa] [un] gbrp12 | a=1:1
+gbrp12be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp14 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp14be => [pa] [un] gbrp14 | a=1:1
+gbrp14be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp16 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp16be => [pa] [un] gbrp16 | a=1:1
+gbrp16be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp2 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp3 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp4 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp5 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp6 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp9 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrp9be => [pa] [un] gbrp9 | a=1:1
+gbrp9be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+gbrpf32be => [pa] [un] gbrpf32 | a=1:1
+gray => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray10 => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray10be => [pa] [un] gray10 | a=1:1
+gray10be => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray12 => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray12be => [pa] [un] gray12 | a=1:1
+gray12be => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray14 => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray14be => [pa] [un] gray14 | a=1:1
+gray14be => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray16 => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray16be => [pa] [un] gray16 | a=1:1
+gray16be => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray9 => [pa] [un] grayf32 | a=1:1 [planar-f32]
+gray9be => [pa] [un] gray9 | a=1:1
+gray9be => [pa] [un] grayf32 | a=1:1 [planar-f32]
+grayf32be => [pa] [un] grayf32 | a=1:1
+mediacodec => no
+mmal => no
+monob => [pa] [un] y1 | a=8:1 [tu] [tp]
+monob => [pa] [un] gray | a=8:1 [expand-8bit]
+monow => [pa] [un] y1 | a=8:1 [tu] [tp]
+monow => [pa] [un] gray | a=8:1 [expand-8bit]
+nv12 => [pa] [un] yuv420p | a=2:2 [tu] [tp]
+nv12 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+nv16 => [pa] [un] yuv422p | a=2:1
+nv16 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+nv20 => [pa] [un] yuv422p10 | a=2:1
+nv20 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+nv20be => [pa] [un] yuv422p10 | a=2:1
+nv20be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+nv21 => [pa] [un] yuv420p | a=2:2 [tu] [tp]
+nv21 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+nv24 => [pa] [un] yuv444p | a=1:1
+nv24 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+nv42 => [pa] [un] yuv444p | a=1:1
+nv42 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+opencl => no
+p010 => [pa] [un] yuv420p16 | a=2:2
+p010 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+p010be => [pa] [un] yuv420p16 | a=2:2
+p010be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+p012 => [pa] [un] yuv420p16 | a=2:2
+p012 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+p012be => [pa] [un] yuv420p16 | a=2:2
+p012be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+p016 => [pa] [un] yuv420p16 | a=2:2
+p016 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+p016be => [pa] [un] yuv420p16 | a=2:2
+p016be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+p210 => [pa] [un] yuv422p16 | a=2:1
+p210 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+p210be => [pa] [un] yuv422p16 | a=2:1
+p210be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+p212 => [pa] [un] yuv422p16 | a=2:1
+p212 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+p212be => [pa] [un] yuv422p16 | a=2:1
+p212be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+p216 => [pa] [un] yuv422p16 | a=2:1
+p216 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+p216be => [pa] [un] yuv422p16 | a=2:1
+p216be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+p410 => [pa] [un] yuv444p16 | a=1:1
+p410 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+p410be => [pa] [un] yuv444p16 | a=1:1
+p410be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+p412 => [pa] [un] yuv444p16 | a=1:1
+p412 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+p412be => [pa] [un] yuv444p16 | a=1:1
+p412be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+p416 => [pa] [un] yuv444p16 | a=1:1
+p416 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+p416be => [pa] [un] yuv444p16 | a=1:1
+p416be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+pal8 => [un] gbrap | a=1:1
+pal8 => [un] gbrapf32 | a=1:1 [planar-f32]
+qsv => no
+rgb0 => [pa] [un] gbrp | a=1:1 [tu] [tp]
+rgb0 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb24 => [pa] [un] gbrp | a=1:1 [tu] [tp]
+rgb24 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb30 => [pa] [un] gbrp10 | a=1:1 [tu] [tp]
+rgb30 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb4 => no
+rgb444 => [pa] [un] gbrp4 | a=1:1
+rgb444 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb444 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb444be => [pa] [un] gbrp4 | a=1:1
+rgb444be => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb444be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb48 => [pa] [un] gbrp16 | a=1:1 [tu] [tp]
+rgb48 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb48be => [pa] [un] gbrp16 | a=1:1 [tu] [tp]
+rgb48be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb4_byte => [pa] [un] gbrp2 | a=1:1
+rgb4_byte => [pa] [un] gbrp1 | a=1:1 [round-down]
+rgb4_byte => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb4_byte => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb555 => [pa] [un] gbrp5 | a=1:1
+rgb555 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb555 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb555be => [pa] [un] gbrp5 | a=1:1
+rgb555be => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb555be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb565 => [pa] [un] gbrp6 | a=1:1
+rgb565 => [pa] [un] gbrp5 | a=1:1 [round-down]
+rgb565 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb565 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb565be => [pa] [un] gbrp6 | a=1:1
+rgb565be => [pa] [un] gbrp5 | a=1:1 [round-down]
+rgb565be => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb565be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgb8 => [pa] [un] gbrp3 | a=1:1
+rgb8 => [pa] [un] gbrp2 | a=1:1 [round-down]
+rgb8 => [pa] [un] gbrp | a=1:1 [expand-8bit] [tu] [tp]
+rgb8 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+rgba => [pa] [un] gbrap | a=1:1 [tu] [tp]
+rgba => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+rgba64 => [pa] [un] gbrap16 | a=1:1 [tu] [tp]
+rgba64 => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+rgba64be => [pa] [un] gbrap16 | a=1:1 [tu] [tp]
+rgba64be => [pa] [un] gbrapf32 | a=1:1 [planar-f32]
+rgbaf16 => no
+rgbaf16be => no
+rgbaf32 => no
+rgbaf32be => no
+rgbf32 => no
+rgbf32be => no
+uyvy422 => [pa] [un] yuv422p | a=2:1 [tu] [tp]
+uyvy422 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+uyyvyy411 => [pa] [un] yuv411p | a=4:1 [tu] [tp]
+uyyvyy411 => [pa] [un] yuv411pf | a=4:1 [planar-f32]
+vaapi => no
+vdpau => no
+vdpau_output => no
+videotoolbox => no
+vulkan => no
+vuya => [pa] [un] yuva444p | a=1:1
+vuya => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+vuyx => [pa] [un] yuv444p | a=1:1
+vuyx => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+x2bgr10 => [pa] [un] gbrp10 | a=1:1
+x2bgr10 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+x2bgr10be => [pa] [un] gbrp10 | a=1:1
+x2bgr10be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+x2rgb10be => [pa] [un] gbrp10 | a=1:1 [tu] [tp]
+x2rgb10be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+xv30 => [pa] [un] yuv444p10 | a=1:1
+xv30 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+xv30be => [pa] [un] yuv444p10 | a=1:1
+xv30be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+xv36 => [pa] [un] yuv444p16 | a=1:1
+xv36 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+xv36be => [pa] [un] yuv444p16 | a=1:1
+xv36be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+xvmc => no
+xyz12 => [pa] [un] gbrp16 | a=1:1 [tu] [tp]
+xyz12 => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+xyz12be => [pa] [un] gbrp16 | a=1:1 [tu] [tp]
+xyz12be => [pa] [un] gbrpf32 | a=1:1 [planar-f32]
+y210 => [pa] [un] yuv422p16 | a=2:1 [tu] [tp]
+y210 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+y210be => [pa] [un] yuv422p16 | a=2:1 [tu] [tp]
+y210be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+y212 => [pa] [un] yuv422p16 | a=2:1
+y212 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+y212be => [pa] [un] yuv422p16 | a=2:1
+y212be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+ya16 => [pa] [un] yap16 | a=1:1 [tu] [tp]
+ya16 => [pa] [un] grayaf32 | a=1:1 [planar-f32]
+ya16be => [pa] [un] yap16 | a=1:1
+ya16be => [pa] [un] grayaf32 | a=1:1 [planar-f32]
+ya8 => [pa] [un] yap8 | a=1:1 [tu] [tp]
+ya8 => [pa] [un] grayaf32 | a=1:1 [planar-f32]
+yap16 => [pa] [un] grayaf32 | a=1:1 [planar-f32]
+yap8 => [pa] [un] grayaf32 | a=1:1 [planar-f32]
+yuv410p => [pa] [un] yuv410pf | a=4:4 [planar-f32]
+yuv411p => [pa] [un] yuv411pf | a=4:1 [planar-f32]
+yuv420p => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p10 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p10be => [pa] [un] yuv420p10 | a=2:2
+yuv420p10be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p12 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p12be => [pa] [un] yuv420p12 | a=2:2
+yuv420p12be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p14 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p14be => [pa] [un] yuv420p14 | a=2:2
+yuv420p14be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p16 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p16be => [pa] [un] yuv420p16 | a=2:2
+yuv420p16be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p9 => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv420p9be => [pa] [un] yuv420p9 | a=2:2
+yuv420p9be => [pa] [un] yuv420pf | a=2:2 [planar-f32]
+yuv422p => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p10 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p10be => [pa] [un] yuv422p10 | a=2:1
+yuv422p10be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p12 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p12be => [pa] [un] yuv422p12 | a=2:1
+yuv422p12be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p14 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p14be => [pa] [un] yuv422p14 | a=2:1
+yuv422p14be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p16 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p16be => [pa] [un] yuv422p16 | a=2:1 [tu] [tp]
+yuv422p16be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p9 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv422p9be => [pa] [un] yuv422p9 | a=2:1
+yuv422p9be => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuv440p => [pa] [un] yuv440pf | a=1:2 [planar-f32]
+yuv440p10 => [pa] [un] yuv440pf | a=1:2 [planar-f32]
+yuv440p10be => [pa] [un] yuv440p10 | a=1:2
+yuv440p10be => [pa] [un] yuv440pf | a=1:2 [planar-f32]
+yuv440p12 => [pa] [un] yuv440pf | a=1:2 [planar-f32]
+yuv440p12be => [pa] [un] yuv440p12 | a=1:2
+yuv440p12be => [pa] [un] yuv440pf | a=1:2 [planar-f32]
+yuv444p => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p10 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p10be => [pa] [un] yuv444p10 | a=1:1
+yuv444p10be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p12 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p12be => [pa] [un] yuv444p12 | a=1:1
+yuv444p12be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p14 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p14be => [pa] [un] yuv444p14 | a=1:1
+yuv444p14be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p16 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p16be => [pa] [un] yuv444p16 | a=1:1
+yuv444p16be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p9 => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuv444p9be => [pa] [un] yuv444p9 | a=1:1
+yuv444p9be => [pa] [un] yuv444pf | a=1:1 [planar-f32]
+yuva420p => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva420p10 => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva420p10be => [pa] [un] yuva420p10 | a=2:2
+yuva420p10be => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva420p16 => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva420p16be => [pa] [un] yuva420p16 | a=2:2
+yuva420p16be => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva420p9 => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva420p9be => [pa] [un] yuva420p9 | a=2:2
+yuva420p9be => [pa] [un] yuva420pf | a=2:2 [planar-f32]
+yuva422p => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p10 => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p10be => [pa] [un] yuva422p10 | a=2:1
+yuva422p10be => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p12 => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p12be => [pa] [un] yuva422p12 | a=2:1
+yuva422p12be => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p16 => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p16be => [pa] [un] yuva422p16 | a=2:1
+yuva422p16be => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p9 => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva422p9be => [pa] [un] yuva422p9 | a=2:1
+yuva422p9be => [pa] [un] yuva422pf | a=2:1 [planar-f32]
+yuva444p => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p10 => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p10be => [pa] [un] yuva444p10 | a=1:1
+yuva444p10be => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p12 => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p12be => [pa] [un] yuva444p12 | a=1:1
+yuva444p12be => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p16 => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p16be => [pa] [un] yuva444p16 | a=1:1
+yuva444p16be => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p9 => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuva444p9be => [pa] [un] yuva444p9 | a=1:1
+yuva444p9be => [pa] [un] yuva444pf | a=1:1 [planar-f32]
+yuvj411p => [pa] [un] yuv411pf | a=4:1 [planar-f32]
+yuvj422p => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yuvj440p => [pa] [un] yuv440pf | a=1:2 [planar-f32]
+yuyv422 => [pa] [un] yuv422p | a=2:1 [tu] [tp]
+yuyv422 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
+yvyu422 => [pa] [un] yuv422p | a=2:1 [tu] [tp]
+yvyu422 => [pa] [un] yuv422pf | a=2:1 [planar-f32]
diff --git a/test/ref/repack_sws.log b/test/ref/repack_sws.log
new file mode 100644
index 0000000..2f5a7e9
--- /dev/null
+++ b/test/ref/repack_sws.log
@@ -0,0 +1,18 @@
+0bgr using gbrp
+0rgb using gbrp
+abgr using gbrap
+argb using gbrap
+bgr0 using gbrp
+bgr24 using gbrp
+bgr48 using gbrp16
+bgr48be using gbrp16
+bgra using gbrap
+bgra64 using gbrap16
+bgra64be using gbrap16
+rgb0 using gbrp
+rgb24 using gbrp
+rgb48 using gbrp16
+rgb48be using gbrp16
+rgba using gbrap
+rgba64 using gbrap16
+rgba64be using gbrap16
diff --git a/test/ref/repack_zimg.log b/test/ref/repack_zimg.log
new file mode 100644
index 0000000..2f5a7e9
--- /dev/null
+++ b/test/ref/repack_zimg.log
@@ -0,0 +1,18 @@
+0bgr using gbrp
+0rgb using gbrp
+abgr using gbrap
+argb using gbrap
+bgr0 using gbrp
+bgr24 using gbrp
+bgr48 using gbrp16
+bgr48be using gbrp16
+bgra using gbrap
+bgra64 using gbrap16
+bgra64be using gbrap16
+rgb0 using gbrp
+rgb24 using gbrp
+rgb48 using gbrp16
+rgb48be using gbrp16
+rgba using gbrap
+rgba64 using gbrap16
+rgba64be using gbrap16
diff --git a/test/ref/zimg_formats.txt b/test/ref/zimg_formats.txt
new file mode 100644
index 0000000..6c199b1
--- /dev/null
+++ b/test/ref/zimg_formats.txt
@@ -0,0 +1,249 @@
+ 0bgr Zin Zout SWSin SWSout |
+ 0rgb Zin Zout SWSin SWSout |
+ abgr Zin Zout SWSin SWSout |
+ argb Zin Zout SWSin SWSout |
+ ayuv64 Zin Zout SWSin SWSout |
+ ayuv64be Zin Zout |
+ bayer_bggr16 SWSin |
+ bayer_bggr16be SWSin |
+ bayer_bggr8 SWSin |
+ bayer_gbrg16 SWSin |
+ bayer_gbrg16be SWSin |
+ bayer_gbrg8 SWSin |
+ bayer_grbg16 SWSin |
+ bayer_grbg16be SWSin |
+ bayer_grbg8 SWSin |
+ bayer_rggb16 SWSin |
+ bayer_rggb16be SWSin |
+ bayer_rggb8 SWSin |
+ bgr0 Zin Zout SWSin SWSout |
+ bgr24 Zin Zout SWSin SWSout |
+ bgr4 SWSout |
+ bgr444 Zin Zout SWSin SWSout |
+ bgr444be Zin Zout SWSin SWSout |
+ bgr48 Zin Zout SWSin SWSout |
+ bgr48be Zin Zout SWSin SWSout |
+ bgr4_byte Zin Zout SWSin SWSout |
+ bgr555 Zin Zout SWSin SWSout |
+ bgr555be Zin Zout SWSin SWSout |
+ bgr565 Zin Zout SWSin SWSout |
+ bgr565be Zin Zout SWSin SWSout |
+ bgr8 Zin Zout SWSin SWSout |
+ bgra Zin Zout SWSin SWSout |
+ bgra64 Zin Zout SWSin SWSout |
+ bgra64be Zin Zout SWSin SWSout |
+ cuda |
+ d3d11 |
+ d3d11va_vld |
+ drm_prime |
+ dxva2_vld |
+ gbrap Zin Zout SWSin SWSout |
+ gbrap10 Zin Zout SWSin SWSout |
+ gbrap10be Zin Zout SWSin SWSout |
+ gbrap12 Zin Zout SWSin SWSout |
+ gbrap12be Zin Zout SWSin SWSout |
+ gbrap14 Zin Zout SWSin SWSout |
+ gbrap14be Zin Zout SWSin SWSout |
+ gbrap16 Zin Zout SWSin SWSout |
+ gbrap16be Zin Zout SWSin SWSout |
+ gbrapf32 Zin Zout SWSin SWSout |
+ gbrapf32be Zin Zout SWSin SWSout |
+ gbrp Zin Zout SWSin SWSout |
+ gbrp1 Zin Zout |
+ gbrp10 Zin Zout SWSin SWSout |
+ gbrp10be Zin Zout SWSin SWSout |
+ gbrp12 Zin Zout SWSin SWSout |
+ gbrp12be Zin Zout SWSin SWSout |
+ gbrp14 Zin Zout SWSin SWSout |
+ gbrp14be Zin Zout SWSin SWSout |
+ gbrp16 Zin Zout SWSin SWSout |
+ gbrp16be Zin Zout SWSin SWSout |
+ gbrp2 Zin Zout |
+ gbrp3 Zin Zout |
+ gbrp4 Zin Zout |
+ gbrp5 Zin Zout |
+ gbrp6 Zin Zout |
+ gbrp9 Zin Zout SWSin SWSout |
+ gbrp9be Zin Zout SWSin SWSout |
+ gbrpf32 Zin Zout SWSin SWSout |
+ gbrpf32be Zin Zout SWSin SWSout |
+ gray Zin Zout SWSin SWSout |
+ gray10 Zin Zout SWSin SWSout |
+ gray10be Zin Zout SWSin SWSout |
+ gray12 Zin Zout SWSin SWSout |
+ gray12be Zin Zout SWSin SWSout |
+ gray14 Zin Zout SWSin SWSout |
+ gray14be Zin Zout SWSin SWSout |
+ gray16 Zin Zout SWSin SWSout |
+ gray16be Zin Zout SWSin SWSout |
+ gray9 Zin Zout SWSin SWSout |
+ gray9be Zin Zout SWSin SWSout |
+ grayaf32 Zin Zout |
+ grayf32 Zin Zout SWSin SWSout |
+ grayf32be Zin Zout SWSin SWSout |
+ mediacodec |
+ mmal |
+ monob Zin Zout SWSin SWSout |
+ monow Zin Zout SWSin SWSout |
+ nv12 Zin Zout SWSin SWSout |
+ nv16 Zin Zout SWSin SWSout |
+ nv20 Zin Zout |
+ nv20be Zin Zout |
+ nv21 Zin Zout SWSin SWSout |
+ nv24 Zin Zout SWSin SWSout |
+ nv42 Zin Zout SWSin SWSout |
+ opencl |
+ p010 Zin Zout SWSin SWSout |
+ p010be Zin Zout SWSin SWSout |
+ p012 Zin Zout SWSin SWSout |
+ p012be Zin Zout SWSin SWSout |
+ p016 Zin Zout SWSin SWSout |
+ p016be Zin Zout SWSin SWSout |
+ p210 Zin Zout SWSin SWSout |
+ p210be Zin Zout SWSin SWSout |
+ p212 Zin Zout SWSin SWSout |
+ p212be Zin Zout SWSin SWSout |
+ p216 Zin Zout SWSin SWSout |
+ p216be Zin Zout SWSin SWSout |
+ p410 Zin Zout SWSin SWSout |
+ p410be Zin Zout SWSin SWSout |
+ p412 Zin Zout SWSin SWSout |
+ p412be Zin Zout SWSin SWSout |
+ p416 Zin Zout SWSin SWSout |
+ p416be Zin Zout SWSin SWSout |
+ pal8 Zin SWSin |
+ qsv |
+ rgb0 Zin Zout SWSin SWSout |
+ rgb24 Zin Zout SWSin SWSout |
+ rgb30 Zin Zout SWSin SWSout |
+ rgb4 SWSout |
+ rgb444 Zin Zout SWSin SWSout |
+ rgb444be Zin Zout SWSin SWSout |
+ rgb48 Zin Zout SWSin SWSout |
+ rgb48be Zin Zout SWSin SWSout |
+ rgb4_byte Zin Zout SWSin SWSout |
+ rgb555 Zin Zout SWSin SWSout |
+ rgb555be Zin Zout SWSin SWSout |
+ rgb565 Zin Zout SWSin SWSout |
+ rgb565be Zin Zout SWSin SWSout |
+ rgb8 Zin Zout SWSin SWSout |
+ rgba Zin Zout SWSin SWSout |
+ rgba64 Zin Zout SWSin SWSout |
+ rgba64be Zin Zout SWSin SWSout |
+ rgbaf16 SWSin |
+ rgbaf16be SWSin |
+ rgbaf32 |
+ rgbaf32be |
+ rgbf32 |
+ rgbf32be |
+ uyvy422 Zin Zout SWSin SWSout |
+ uyyvyy411 Zin Zout |
+ vaapi |
+ vdpau |
+ vdpau_output |
+ videotoolbox |
+ vulkan |
+ vuya Zin Zout SWSin SWSout |
+ vuyx Zin Zout SWSin SWSout |
+ x2bgr10 Zin Zout SWSin SWSout |
+ x2bgr10be Zin Zout |
+ x2rgb10be Zin Zout |
+ xv30 Zin Zout SWSin SWSout |
+ xv30be Zin Zout |
+ xv36 Zin Zout SWSin SWSout |
+ xv36be Zin Zout |
+ xvmc |
+ xyz12 Zin Zout SWSin SWSout |
+ xyz12be Zin Zout SWSin SWSout |
+ y1 Zin Zout |
+ y210 Zin Zout SWSin SWSout |
+ y210be Zin Zout |
+ y212 Zin Zout SWSin SWSout |
+ y212be Zin Zout |
+ ya16 Zin Zout SWSin SWSout |
+ ya16be Zin Zout SWSin SWSout |
+ ya8 Zin Zout SWSin SWSout |
+ yap16 Zin Zout |
+ yap8 Zin Zout |
+ yuv410p Zin Zout SWSin SWSout |
+ yuv410pf Zin Zout |
+ yuv411p Zin Zout SWSin SWSout |
+ yuv411pf Zin Zout |
+ yuv420p Zin Zout SWSin SWSout |
+ yuv420p10 Zin Zout SWSin SWSout |
+ yuv420p10be Zin Zout SWSin SWSout |
+ yuv420p12 Zin Zout SWSin SWSout |
+ yuv420p12be Zin Zout SWSin SWSout |
+ yuv420p14 Zin Zout SWSin SWSout |
+ yuv420p14be Zin Zout SWSin SWSout |
+ yuv420p16 Zin Zout SWSin SWSout |
+ yuv420p16be Zin Zout SWSin SWSout |
+ yuv420p9 Zin Zout SWSin SWSout |
+ yuv420p9be Zin Zout SWSin SWSout |
+ yuv420pf Zin Zout |
+ yuv422p Zin Zout SWSin SWSout |
+ yuv422p10 Zin Zout SWSin SWSout |
+ yuv422p10be Zin Zout SWSin SWSout |
+ yuv422p12 Zin Zout SWSin SWSout |
+ yuv422p12be Zin Zout SWSin SWSout |
+ yuv422p14 Zin Zout SWSin SWSout |
+ yuv422p14be Zin Zout SWSin SWSout |
+ yuv422p16 Zin Zout SWSin SWSout |
+ yuv422p16be Zin Zout SWSin SWSout |
+ yuv422p9 Zin Zout SWSin SWSout |
+ yuv422p9be Zin Zout SWSin SWSout |
+ yuv422pf Zin Zout |
+ yuv440p Zin Zout SWSin SWSout |
+ yuv440p10 Zin Zout SWSin SWSout |
+ yuv440p10be Zin Zout SWSin SWSout |
+ yuv440p12 Zin Zout SWSin SWSout |
+ yuv440p12be Zin Zout SWSin SWSout |
+ yuv440pf Zin Zout |
+ yuv444p Zin Zout SWSin SWSout |
+ yuv444p10 Zin Zout SWSin SWSout |
+ yuv444p10be Zin Zout SWSin SWSout |
+ yuv444p12 Zin Zout SWSin SWSout |
+ yuv444p12be Zin Zout SWSin SWSout |
+ yuv444p14 Zin Zout SWSin SWSout |
+ yuv444p14be Zin Zout SWSin SWSout |
+ yuv444p16 Zin Zout SWSin SWSout |
+ yuv444p16be Zin Zout SWSin SWSout |
+ yuv444p9 Zin Zout SWSin SWSout |
+ yuv444p9be Zin Zout SWSin SWSout |
+ yuv444pf Zin Zout |
+ yuva410pf Zin Zout |
+ yuva411pf Zin Zout |
+ yuva420p Zin Zout SWSin SWSout |
+ yuva420p10 Zin Zout SWSin SWSout |
+ yuva420p10be Zin Zout SWSin SWSout |
+ yuva420p16 Zin Zout SWSin SWSout |
+ yuva420p16be Zin Zout SWSin SWSout |
+ yuva420p9 Zin Zout SWSin SWSout |
+ yuva420p9be Zin Zout SWSin SWSout |
+ yuva420pf Zin Zout |
+ yuva422p Zin Zout SWSin SWSout |
+ yuva422p10 Zin Zout SWSin SWSout |
+ yuva422p10be Zin Zout SWSin SWSout |
+ yuva422p12 Zin Zout SWSin SWSout |
+ yuva422p12be Zin Zout SWSin SWSout |
+ yuva422p16 Zin Zout SWSin SWSout |
+ yuva422p16be Zin Zout SWSin SWSout |
+ yuva422p9 Zin Zout SWSin SWSout |
+ yuva422p9be Zin Zout SWSin SWSout |
+ yuva422pf Zin Zout |
+ yuva440pf Zin Zout |
+ yuva444p Zin Zout SWSin SWSout |
+ yuva444p10 Zin Zout SWSin SWSout |
+ yuva444p10be Zin Zout SWSin SWSout |
+ yuva444p12 Zin Zout SWSin SWSout |
+ yuva444p12be Zin Zout SWSin SWSout |
+ yuva444p16 Zin Zout SWSin SWSout |
+ yuva444p16be Zin Zout SWSin SWSout |
+ yuva444p9 Zin Zout SWSin SWSout |
+ yuva444p9be Zin Zout SWSin SWSout |
+ yuva444pf Zin Zout |
+ yuvj411p Zin Zout SWSin SWSout |
+ yuvj422p Zin Zout SWSin SWSout |
+ yuvj440p Zin Zout SWSin SWSout |
+ yuyv422 Zin Zout SWSin SWSout |
+ yvyu422 Zin Zout SWSin SWSout |
diff --git a/test/repack.c b/test/repack.c
new file mode 100644
index 0000000..a37559b
--- /dev/null
+++ b/test/repack.c
@@ -0,0 +1,532 @@
+#include <limits.h>
+
+#include <libavutil/pixfmt.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "img_utils.h"
+#include "sub/draw_bmp.h"
+#include "sub/osd.h"
+#include "test_utils.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "video/img_format.h"
+#include "video/repack.h"
+#include "video/sws_utils.h"
+#include "video/zimg.h"
+
+// Excuse the utter stupidity.
+#define UNFUCK(v) ((v) > 0 ? (v) : pixfmt2imgfmt(-(v)))
+static_assert(IMGFMT_START > 0, "");
+#define IMGFMT_GBRP (-AV_PIX_FMT_GBRP)
+#define IMGFMT_GBRAP (-AV_PIX_FMT_GBRAP)
+
+struct entry {
+ int w, h;
+ int fmt_a;
+ const void *const a[4];
+ int fmt_b;
+ const void *const b[4];
+ int flags;
+};
+
+#define P8(...) (const uint8_t[]){__VA_ARGS__}
+#define P16(...) (const uint16_t[]){__VA_ARGS__}
+#define P32(...) (const uint32_t[]){__VA_ARGS__}
+#define SW16(v) ((((v) & 0xFF) << 8) | ((v) >> 8))
+#define SW32(v) ((SW16((v) & 0xFFFFu) << 16) | (SW16(((v) | 0u) >> 16)))
+
+#define ZIMG_IMAGE_DIMENSION_MAX ((size_t)(1) << (CHAR_BIT * sizeof(size_t) / 2 - 2))
+
+// Warning: only entries that match existing conversions are tested.
+static const struct entry repack_tests[] = {
+ // Note: the '0' tests rely on 0 being written, although by definition the
+ // contents of this padding is undefined. The repacker always writes
+ // it this way, though.
+ {1, 1, IMGFMT_RGB0, {P8(1, 2, 3, 0)},
+ IMGFMT_GBRP, {P8(2), P8(3), P8(1)}},
+ {1, 1, IMGFMT_BGR0, {P8(1, 2, 3, 0)},
+ IMGFMT_GBRP, {P8(2), P8(1), P8(3)}},
+ {1, 1, IMGFMT_0RGB, {P8(0, 1, 2, 3)},
+ IMGFMT_GBRP, {P8(2), P8(3), P8(1)}},
+ {1, 1, IMGFMT_0BGR, {P8(0, 1, 2, 3)},
+ IMGFMT_GBRP, {P8(2), P8(1), P8(3)}},
+ {1, 1, IMGFMT_RGBA, {P8(1, 2, 3, 4)},
+ IMGFMT_GBRAP, {P8(2), P8(3), P8(1), P8(4)}},
+ {1, 1, IMGFMT_BGRA, {P8(1, 2, 3, 4)},
+ IMGFMT_GBRAP, {P8(2), P8(1), P8(3), P8(4)}},
+ {1, 1, IMGFMT_ARGB, {P8(4, 1, 2, 3)},
+ IMGFMT_GBRAP, {P8(2), P8(3), P8(1), P8(4)}},
+ {1, 1, IMGFMT_ABGR, {P8(4, 1, 2, 3)},
+ IMGFMT_GBRAP, {P8(2), P8(1), P8(3), P8(4)}},
+ {1, 1, IMGFMT_BGR24, {P8(1, 2, 3)},
+ IMGFMT_GBRP, {P8(2), P8(1), P8(3)}},
+ {1, 1, IMGFMT_RGB24, {P8(1, 2, 3)},
+ IMGFMT_GBRP, {P8(2), P8(3), P8(1)}},
+ {1, 1, IMGFMT_RGBA64, {P16(0x1a1b, 0x2a2b, 0x3a3b, 0x4a4b)},
+ -AV_PIX_FMT_GBRAP16, {P16(0x2a2b), P16(0x3a3b),
+ P16(0x1a1b), P16(0x4a4b)}},
+ {1, 1, -AV_PIX_FMT_BGRA64LE, {P16(0x1a1b, 0x2a2b, 0x3a3b, 0x4a4b)},
+ -AV_PIX_FMT_GBRAP16, {P16(0x2a2b), P16(0x1a1b),
+ P16(0x3a3b), P16(0x4a4b)}},
+ {1, 1, -AV_PIX_FMT_RGBA64BE, {P16(0x1b1a, 0x2b2a, 0x3b3a, 0x4b4a)},
+ -AV_PIX_FMT_GBRAP16, {P16(0x2a2b), P16(0x3a3b),
+ P16(0x1a1b), P16(0x4a4b)}},
+ {1, 1, -AV_PIX_FMT_BGRA64BE, {P16(0x1b1a, 0x2b2a, 0x3b3a, 0x4b4a)},
+ -AV_PIX_FMT_GBRAP16, {P16(0x2a2b), P16(0x1a1b),
+ P16(0x3a3b), P16(0x4a4b)}},
+ {1, 1, -AV_PIX_FMT_RGB48BE, {P16(0x1a1b, 0x2a2b, 0x3a3b)},
+ -AV_PIX_FMT_GBRP16, {P16(0x2b2a), P16(0x3b3a), P16(0x1b1a)}},
+ {1, 1, -AV_PIX_FMT_RGB48LE, {P16(0x1a1b, 0x2a2b, 0x3a3b)},
+ -AV_PIX_FMT_GBRP16, {P16(0x2a2b), P16(0x3a3b), P16(0x1a1b)}},
+ {1, 1, -AV_PIX_FMT_BGR48BE, {P16(0x1a1b, 0x2a2b, 0x3a3b)},
+ -AV_PIX_FMT_GBRP16, {P16(0x2b2a), P16(0x1b1a), P16(0x3b3a)}},
+ {1, 1, -AV_PIX_FMT_BGR48LE, {P16(0x1a1b, 0x2a2b, 0x3a3b)},
+ -AV_PIX_FMT_GBRP16, {P16(0x2a2b), P16(0x1a1b), P16(0x3a3b)}},
+ {1, 1, -AV_PIX_FMT_XYZ12LE, {P16(0x1a1b, 0x2a2b, 0x3a3b)},
+ -AV_PIX_FMT_GBRP16, {P16(0x2a2b), P16(0x3a3b), P16(0x1a1b)}},
+ {1, 1, -AV_PIX_FMT_XYZ12BE, {P16(0x1b1a, 0x2b2a, 0x3b3a)},
+ -AV_PIX_FMT_GBRP16, {P16(0x2a2b), P16(0x3a3b), P16(0x1a1b)}},
+ {3, 1, -AV_PIX_FMT_BGR8, {P8(7, (7 << 3), (3 << 6))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB8, {P8(3, (7 << 2), (7 << 5))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR4_BYTE, {P8(1, (3 << 1), (1 << 3))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB4_BYTE, {P8(1, (3 << 1), (1 << 3))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB565LE, {P16((31), (63 << 5), (31 << 11))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB565BE, {P16(SW16(31), SW16(63 << 5), SW16(31 << 11))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR565LE, {P16((31), (63 << 5), (31 << 11))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR565BE, {P16(SW16(31), SW16(63 << 5), SW16(31 << 11))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB555LE, {P16((31), (31 << 5), (31 << 10))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB555BE, {P16(SW16(31), SW16(31 << 5), SW16(31 << 10))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR555LE, {P16((31), (31 << 5), (31 << 10))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR555BE, {P16(SW16(31), SW16(31 << 5), SW16(31 << 10))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB444LE, {P16((15), (15 << 4), (15 << 8))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_RGB444BE, {P16(SW16(15), SW16(15 << 4), SW16(15 << 8))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0xFF,0,0), P8(0,0,0xFF)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR444LE, {P16((15), (15 << 4), (15 << 8))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {3, 1, -AV_PIX_FMT_BGR444BE, {P16(SW16(15), SW16(15 << 4), SW16(15 << 8))},
+ IMGFMT_GBRP, {P8(0,0xFF,0), P8(0,0,0xFF), P8(0xFF,0,0)},
+ .flags = REPACK_CREATE_EXPAND_8BIT},
+ {1, 1, IMGFMT_RGB30, {P32((3 << 20) | (2 << 10) | 1)},
+ -AV_PIX_FMT_GBRP10, {P16(2), P16(1), P16(3)}},
+ {1, 1, -AV_PIX_FMT_X2RGB10BE, {P32(SW32((3 << 20) | (2 << 10) | 1))},
+ -AV_PIX_FMT_GBRP10, {P16(2), P16(1), P16(3)}},
+ {8, 1, -AV_PIX_FMT_MONOWHITE, {P8(0xAA)},
+ IMGFMT_Y1, {P8(0, 1, 0, 1, 0, 1, 0, 1)}},
+ {8, 1, -AV_PIX_FMT_MONOBLACK, {P8(0xAA)},
+ IMGFMT_Y1, {P8(1, 0, 1, 0, 1, 0, 1, 0)}},
+ {2, 2, IMGFMT_NV12, {P8(1, 2, 3, 4), P8(5, 6)},
+ IMGFMT_420P, {P8(1, 2, 3, 4), P8(5), P8(6)}},
+ {2, 2, -AV_PIX_FMT_NV21, {P8(1, 2, 3, 4), P8(5, 6)},
+ IMGFMT_420P, {P8(1, 2, 3, 4), P8(6), P8(5)}},
+ {1, 1, -AV_PIX_FMT_AYUV64LE, {P16(1, 2, 3, 4)},
+ -AV_PIX_FMT_YUVA444P16, {P16(2), P16(3), P16(4), P16(1)}},
+ {1, 1, -AV_PIX_FMT_AYUV64BE, {P16(0x0100, 0x0200, 0x0300, 0x0400)},
+ -AV_PIX_FMT_YUVA444P16, {P16(2), P16(3), P16(4), P16(1)}},
+ {4, 1, -AV_PIX_FMT_YUYV422, {P8(1, 2, 3, 4, 5, 6, 7, 8)},
+ -AV_PIX_FMT_YUV422P, {P8(1, 3, 5, 7), P8(2, 6), P8(4, 8)}},
+ {2, 1, -AV_PIX_FMT_YVYU422, {P8(1, 2, 3, 4)},
+ -AV_PIX_FMT_YUV422P, {P8(1, 3), P8(4), P8(2)}},
+ {2, 1, -AV_PIX_FMT_UYVY422, {P8(1, 2, 3, 4)},
+ -AV_PIX_FMT_YUV422P, {P8(2, 4), P8(1), P8(3)}},
+ {2, 1, -AV_PIX_FMT_Y210LE, {P16(0x1a1b, 0x2a2b, 0x3a3b, 0x4a4b)},
+ -AV_PIX_FMT_YUV422P16, {P16(0x1a1b, 0x3a3b), P16(0x2a2b), P16(0x4a4b)}},
+ {2, 1, -AV_PIX_FMT_Y210BE, {P16(0x1b1a, 0x2b2a, 0x3b3a, 0x4b4a)},
+ -AV_PIX_FMT_YUV422P16, {P16(0x1a1b, 0x3a3b), P16(0x2a2b), P16(0x4a4b)}},
+ {1, 1, -AV_PIX_FMT_YA8, {P8(1, 2)},
+ IMGFMT_YAP8, {P8(1), P8(2)}},
+ {1, 1, -AV_PIX_FMT_YA16, {P16(0x1a1b, 0x2a2b)},
+ IMGFMT_YAP16, {P16(0x1a1b), P16(0x2a2b)}},
+ {2, 1, -AV_PIX_FMT_YUV422P16BE, {P16(0x1a1b, 0x2a2b), P16(0x3a3b),
+ P16(0x4a4b)},
+ -AV_PIX_FMT_YUV422P16, {P16(0x1b1a, 0x2b2a), P16(0x3b3a),
+ P16(0x4b4a)}},
+ {8, 1, -AV_PIX_FMT_UYYVYY411, {P8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)},
+ -AV_PIX_FMT_YUV411P, {P8(2, 3, 5, 6, 8, 9, 11, 12),
+ P8(1, 7), P8(4, 10)}},
+};
+
+static bool is_true_planar(int imgfmt)
+{
+ struct mp_regular_imgfmt desc;
+ if (!mp_get_regular_imgfmt(&desc, imgfmt))
+ return false;
+
+ for (int n = 0; n < desc.num_planes; n++) {
+ if (desc.planes[n].num_components != 1)
+ return false;
+ }
+
+ return true;
+}
+
+static int try_repack(FILE *f, int imgfmt, int flags, int not_if_fmt)
+{
+ char *head = mp_tprintf(80, "%-15s =>", mp_imgfmt_to_name(imgfmt));
+ struct mp_repack *un = mp_repack_create_planar(imgfmt, false, flags);
+ struct mp_repack *pa = mp_repack_create_planar(imgfmt, true, flags);
+
+ // If both exists, they must be always symmetric.
+ if (un && pa) {
+ assert(mp_repack_get_format_src(pa) == mp_repack_get_format_dst(un));
+ assert(mp_repack_get_format_src(un) == mp_repack_get_format_dst(pa));
+ assert(mp_repack_get_align_x(pa) == mp_repack_get_align_x(un));
+ assert(mp_repack_get_align_y(pa) == mp_repack_get_align_y(un));
+ }
+
+ int a = 0;
+ int b = 0;
+ if (un) {
+ a = mp_repack_get_format_src(un);
+ b = mp_repack_get_format_dst(un);
+ } else if (pa) {
+ a = mp_repack_get_format_dst(pa);
+ b = mp_repack_get_format_src(pa);
+ }
+
+ // Skip the identity ones because they're uninteresting, and add too much
+ // noise. But still make sure they behave as expected.
+ if (a == imgfmt && b == imgfmt) {
+ assert(is_true_planar(imgfmt));
+ // (note that we require alpha-enabled zimg)
+ assert(mp_zimg_supports_in_format(imgfmt));
+ assert(un && pa);
+ talloc_free(pa);
+ talloc_free(un);
+ return b;
+ }
+
+ struct mp_repack *rp = pa ? pa : un;
+ if (!rp) {
+ if (!flags)
+ fprintf(f, "%s no\n", head);
+ return 0;
+ }
+
+ assert(a == imgfmt);
+ if (b && b == not_if_fmt) {
+ talloc_free(pa);
+ talloc_free(un);
+ return 0;
+ }
+
+ fprintf(f, "%s %4s %4s %-15s |", head, pa ? "[pa]" : "", un ? "[un]" : "",
+ mp_imgfmt_to_name(b));
+
+ fprintf(f, " a=%d:%d", mp_repack_get_align_x(rp), mp_repack_get_align_y(rp));
+
+ if (flags & REPACK_CREATE_PLANAR_F32)
+ fprintf(f, " [planar-f32]");
+ if (flags & REPACK_CREATE_ROUND_DOWN)
+ fprintf(f, " [round-down]");
+ if (flags & REPACK_CREATE_EXPAND_8BIT)
+ fprintf(f, " [expand-8bit]");
+
+ // LCM of alignment of all packers.
+ int ax = mp_repack_get_align_x(rp);
+ int ay = mp_repack_get_align_y(rp);
+ if (pa && un) {
+ ax = MPMAX(mp_repack_get_align_x(pa), mp_repack_get_align_x(un));
+ ay = MPMAX(mp_repack_get_align_y(pa), mp_repack_get_align_y(un));
+ }
+
+ for (int n = 0; n < MP_ARRAY_SIZE(repack_tests); n++) {
+ const struct entry *e = &repack_tests[n];
+ int fmt_a = UNFUCK(e->fmt_a);
+ int fmt_b = UNFUCK(e->fmt_b);
+ if (!(fmt_a == a && fmt_b == b && e->flags == flags))
+ continue;
+
+ // We convert a "random" macro pixel to catch potential addressing bugs
+ // that might be ignored with (0, 0) origins.
+ struct mp_image *ia = mp_image_alloc(fmt_a, e->w * 5 * ax, e->h * 5 * ay);
+ struct mp_image *ib = mp_image_alloc(fmt_b, e->w * 7 * ax, e->h * 6 * ay);
+ int sx = 4 * ax, sy = 3 * ay, dx = 3 * ax, dy = 2 * ay;
+
+ assert(ia && ib);
+
+ mp_image_params_guess_csp(&ia->params);
+ mp_image_params_guess_csp(&ib->params);
+
+ for (int pack = 0; pack < 2; pack++) {
+ struct mp_repack *repacker = pack ? pa : un;
+ if (!repacker)
+ continue;
+
+ mp_image_clear(ia, 0, 0, ia->w, ia->h);
+ mp_image_clear(ib, 0, 0, ib->w, ib->h);
+
+ const void *const *dstd = pack ? e->a : e->b;
+ const void *const *srcd = pack ? e->b : e->a;
+ struct mp_image *dsti = pack ? ia : ib;
+ struct mp_image *srci = pack ? ib : ia;
+
+ bool r = repack_config_buffers(repacker, 0, dsti, 0, srci, NULL);
+ assert(r);
+
+ for (int p = 0; p < srci->num_planes; p++) {
+ uint8_t *ptr = mp_image_pixel_ptr(srci, p, sx, sy);
+ for (int y = 0; y < e->h >> srci->fmt.ys[p]; y++) {
+ int wb = mp_image_plane_bytes(srci, p, 0, e->w);
+ const void *cptr = (uint8_t *)srcd[p] + wb * y;
+ memcpy(ptr + srci->stride[p] * y, cptr, wb);
+ }
+ }
+
+ repack_line(repacker, dx, dy, sx, sy, e->w);
+
+ for (int p = 0; p < dsti->num_planes; p++) {
+ uint8_t *ptr = mp_image_pixel_ptr(dsti, p, dx, dy);
+ for (int y = 0; y < e->h >> dsti->fmt.ys[p]; y++) {
+ int wb = mp_image_plane_bytes(dsti, p, 0, e->w);
+ const void *cptr = (uint8_t *)dstd[p] + wb * y;
+ assert_memcmp(ptr + dsti->stride[p] * y, cptr, wb);
+ }
+ }
+
+ fprintf(f, " [t%s]", pack ? "p" : "u");
+ }
+
+ talloc_free(ia);
+ talloc_free(ib);
+ }
+
+ fprintf(f, "\n");
+
+ talloc_free(pa);
+ talloc_free(un);
+ return b;
+}
+
+static void check_float_repack(int imgfmt, enum mp_csp csp,
+ enum mp_csp_levels levels)
+{
+ imgfmt = UNFUCK(imgfmt);
+
+ struct mp_regular_imgfmt desc = {0};
+ mp_get_regular_imgfmt(&desc, imgfmt);
+ int bpp = desc.component_size;
+ int comp_bits = desc.component_size * 8 + MPMIN(desc.component_pad, 0);
+
+ assert(bpp == 1 || bpp == 2);
+
+ int w = 1 << (bpp * 8);
+
+ if (w > ZIMG_IMAGE_DIMENSION_MAX) {
+ printf("Image dimension (%d) exceeded maximum allowed by zimg (%zu)."
+ " Skipping test...\n", w, ZIMG_IMAGE_DIMENSION_MAX);
+ return;
+ }
+
+ struct mp_image *src = mp_image_alloc(imgfmt, w, 1);
+ assert(src);
+
+ src->params.color.space = csp;
+ src->params.color.levels = levels;
+ mp_image_params_guess_csp(&src->params);
+ // mpv may not allow all combinations
+ assert(src->params.color.space == csp);
+ assert(src->params.color.levels == levels);
+
+ for (int p = 0; p < src->num_planes; p++) {
+ int val = 0;
+ for (int x = 0; x < w >> src->fmt.xs[p]; x++) {
+ val = MPMIN(val, (1 << comp_bits) - 1);
+ void *pixel = mp_image_pixel_ptr(src, p, x, 0);
+ if (bpp == 1) {
+ *(uint8_t *)pixel = val;
+ } else {
+ *(uint16_t *)pixel = val;
+ }
+ val++;
+ }
+ }
+
+ struct mp_repack *to_f =
+ mp_repack_create_planar(src->imgfmt, false, REPACK_CREATE_PLANAR_F32);
+ struct mp_repack *from_f =
+ mp_repack_create_planar(src->imgfmt, true, REPACK_CREATE_PLANAR_F32);
+ assert(to_f && from_f);
+
+ struct mp_image *z_f = mp_image_alloc(mp_repack_get_format_dst(to_f), w, 1);
+ struct mp_image *r_f = mp_image_alloc(z_f->imgfmt, w, 1);
+ struct mp_image *z_i = mp_image_alloc(src->imgfmt, w, 1);
+ struct mp_image *r_i = mp_image_alloc(src->imgfmt, w, 1);
+ assert(z_f && r_f && z_i && r_i);
+
+ z_f->params.color = r_f->params.color = z_i->params.color =
+ r_i->params.color = src->params.color;
+
+ // The idea is to use zimg to cross-check conversion.
+ struct mp_sws_context *s = mp_sws_alloc(NULL);
+ s->force_scaler = MP_SWS_ZIMG;
+ struct zimg_opts opts = zimg_opts_defaults;
+ opts.dither = ZIMG_DITHER_NONE;
+ s->zimg_opts = &opts;
+ int ret = mp_sws_scale(s, z_f, src);
+ assert_true(ret >= 0);
+ ret = mp_sws_scale(s, z_i, z_f);
+ assert_true(ret >= 0);
+ talloc_free(s);
+
+ repack_config_buffers(to_f, 0, r_f, 0, src, NULL);
+ repack_line(to_f, 0, 0, 0, 0, w);
+ repack_config_buffers(from_f, 0, r_i, 0, r_f, NULL);
+ repack_line(from_f, 0, 0, 0, 0, w);
+
+ for (int p = 0; p < src->num_planes; p++) {
+ for (int x = 0; x < w >> src->fmt.xs[p]; x++) {
+ uint32_t src_val, z_i_val, r_i_val;
+ if (bpp == 1) {
+ src_val = *(uint8_t *)mp_image_pixel_ptr(src, p, x, 0);
+ z_i_val = *(uint8_t *)mp_image_pixel_ptr(z_i, p, x, 0);
+ r_i_val = *(uint8_t *)mp_image_pixel_ptr(r_i, p, x, 0);
+ } else {
+ src_val = *(uint16_t *)mp_image_pixel_ptr(src, p, x, 0);
+ z_i_val = *(uint16_t *)mp_image_pixel_ptr(z_i, p, x, 0);
+ r_i_val = *(uint16_t *)mp_image_pixel_ptr(r_i, p, x, 0);
+ }
+ float z_f_val = *(float *)mp_image_pixel_ptr(z_f, p, x, 0);
+ float r_f_val = *(float *)mp_image_pixel_ptr(r_f, p, x, 0);
+
+ assert_int_equal(src_val, z_i_val);
+ assert_int_equal(src_val, r_i_val);
+ double tolerance = 1.0 / (1 << (bpp * 8)) / 4;
+ assert_float_equal(r_f_val, z_f_val, tolerance);
+ }
+ }
+
+ talloc_free(src);
+ talloc_free(z_i);
+ talloc_free(z_f);
+ talloc_free(r_i);
+ talloc_free(r_f);
+ talloc_free(to_f);
+ talloc_free(from_f);
+}
+
+static bool try_draw_bmp(FILE *f, int imgfmt)
+{
+ bool ok = false;
+
+ struct mp_image *dst = mp_image_alloc(imgfmt, 64, 64);
+ if (!dst)
+ goto done;
+
+ struct sub_bitmap sb = {
+ .bitmap = &(uint8_t[]){123},
+ .stride = 1,
+ .x = 1,
+ .y = 1,
+ .w = 1, .dw = 1,
+ .h = 1, .dh = 1,
+
+ .libass = { .color = 0xDEDEDEDE },
+ };
+ struct sub_bitmaps sbs = {
+ .format = SUBBITMAP_LIBASS,
+ .parts = &sb,
+ .num_parts = 1,
+ .change_id = 1,
+ };
+ struct sub_bitmap_list sbs_list = {
+ .change_id = 1,
+ .w = dst->w,
+ .h = dst->h,
+ .items = (struct sub_bitmaps *[]){&sbs},
+ .num_items = 1,
+ };
+
+ struct mp_draw_sub_cache *c = mp_draw_sub_alloc_test(dst);
+ if (mp_draw_sub_bitmaps(c, dst, &sbs_list)) {
+ char *info = mp_draw_sub_get_dbg_info(c);
+ fprintf(f, "%s\n", info);
+ talloc_free(info);
+ ok = true;
+ }
+
+ talloc_free(c);
+ talloc_free(dst);
+
+done:
+ if (!ok)
+ fprintf(f, "no\n");
+ return ok;
+}
+
+int main(int argc, char *argv[])
+{
+ const char *refdir = argv[1];
+ const char *outdir = argv[2];
+ FILE *f = test_open_out(outdir, "repack.txt");
+
+ init_imgfmts_list();
+ for (int n = 0; n < num_imgfmts; n++) {
+ int imgfmt = imgfmts[n];
+
+ int other = try_repack(f, imgfmt, 0, 0);
+ try_repack(f, imgfmt, REPACK_CREATE_ROUND_DOWN, other);
+ try_repack(f, imgfmt, REPACK_CREATE_EXPAND_8BIT, other);
+ try_repack(f, imgfmt, REPACK_CREATE_PLANAR_F32, other);
+ }
+
+ fclose(f);
+
+ assert_text_files_equal(refdir, outdir, "repack.txt",
+ "This can fail if FFmpeg/libswscale adds or removes pixfmts.");
+
+ check_float_repack(-AV_PIX_FMT_GBRAP, MP_CSP_RGB, MP_CSP_LEVELS_PC);
+ check_float_repack(-AV_PIX_FMT_GBRAP10, MP_CSP_RGB, MP_CSP_LEVELS_PC);
+ check_float_repack(-AV_PIX_FMT_GBRAP16, MP_CSP_RGB, MP_CSP_LEVELS_PC);
+ check_float_repack(-AV_PIX_FMT_YUVA444P, MP_CSP_BT_709, MP_CSP_LEVELS_PC);
+ check_float_repack(-AV_PIX_FMT_YUVA444P, MP_CSP_BT_709, MP_CSP_LEVELS_TV);
+ check_float_repack(-AV_PIX_FMT_YUVA444P10, MP_CSP_BT_709, MP_CSP_LEVELS_PC);
+ check_float_repack(-AV_PIX_FMT_YUVA444P10, MP_CSP_BT_709, MP_CSP_LEVELS_TV);
+ check_float_repack(-AV_PIX_FMT_YUVA444P16, MP_CSP_BT_709, MP_CSP_LEVELS_PC);
+ check_float_repack(-AV_PIX_FMT_YUVA444P16, MP_CSP_BT_709, MP_CSP_LEVELS_TV);
+
+ // Determine the list of possible draw_bmp input formats. Do this here
+ // because it mostly depends on repack and imgformat stuff.
+ f = test_open_out(outdir, "draw_bmp.txt");
+
+ for (int n = 0; n < num_imgfmts; n++) {
+ int imgfmt = imgfmts[n];
+
+ fprintf(f, "%-12s= ", mp_imgfmt_to_name(imgfmt));
+ try_draw_bmp(f, imgfmt);
+ }
+
+ fclose(f);
+
+ assert_text_files_equal(refdir, outdir, "draw_bmp.txt",
+ "This can fail if FFmpeg/libswscale adds or removes pixfmts.");
+ return 0;
+}
diff --git a/test/scale_sws.c b/test/scale_sws.c
new file mode 100644
index 0000000..c9f5e31
--- /dev/null
+++ b/test/scale_sws.c
@@ -0,0 +1,42 @@
+// Test scaling using libswscale.
+// Note: libswscale is already tested in FFmpeg. This code serves mostly to test
+// the functionality scale_test.h using the already tested libswscale as
+// reference.
+
+#include "scale_test.h"
+#include "video/sws_utils.h"
+
+static bool scale(void *pctx, struct mp_image *dst, struct mp_image *src)
+{
+ struct mp_sws_context *ctx = pctx;
+ return mp_sws_scale(ctx, dst, src) >= 0;
+}
+
+static bool supports_fmts(void *pctx, int imgfmt_dst, int imgfmt_src)
+{
+ struct mp_sws_context *ctx = pctx;
+ return mp_sws_supports_formats(ctx, imgfmt_dst, imgfmt_src);
+}
+
+static const struct scale_test_fns fns = {
+ .scale = scale,
+ .supports_fmts = supports_fmts,
+};
+
+int main(int argc, char *argv[])
+{
+ struct mp_sws_context *sws = mp_sws_alloc(NULL);
+
+ struct scale_test *stest = talloc_zero(NULL, struct scale_test);
+ stest->fns = &fns;
+ stest->fns_priv = sws;
+ stest->test_name = "repack_sws";
+ stest->refdir = talloc_strdup(stest, argv[1]);
+ stest->outdir = talloc_strdup(stest, argv[2]);
+
+ repack_test_run(stest);
+
+ talloc_free(stest);
+ talloc_free(sws);
+ return 0;
+}
diff --git a/test/scale_test.c b/test/scale_test.c
new file mode 100644
index 0000000..f919dca
--- /dev/null
+++ b/test/scale_test.c
@@ -0,0 +1,192 @@
+#include <libavcodec/avcodec.h>
+
+#include "scale_test.h"
+#include "video/image_writer.h"
+#include "video/sws_utils.h"
+
+static struct mp_image *gen_repack_test_img(int w, int h, int bytes, bool rgb,
+ bool alpha)
+{
+ struct mp_regular_imgfmt planar_desc = {
+ .component_type = MP_COMPONENT_TYPE_UINT,
+ .component_size = bytes,
+ .forced_csp = rgb ? MP_CSP_RGB : 0,
+ .num_planes = alpha ? 4 : 3,
+ .planes = {
+ {1, {rgb ? 2 : 1}},
+ {1, {rgb ? 3 : 2}},
+ {1, {rgb ? 1 : 3}},
+ {1, {4}},
+ },
+ };
+ int mpfmt = mp_find_regular_imgfmt(&planar_desc);
+ assert(mpfmt);
+ struct mp_image *mpi = mp_image_alloc(mpfmt, w, h);
+ assert(mpi);
+
+ // Well, I have no idea what makes a good test image. So here's some crap.
+ // This contains bars/tiles of solid colors. For each of R/G/B, it toggles
+ // though 0/100% range, so 2*2*2 = 8 combinations (16 with alpha).
+ int b_h = 16, b_w = 16;
+
+ for (int y = 0; y < h; y++) {
+ for (int p = 0; p < mpi->num_planes; p++) {
+ void *line = mpi->planes[p] + mpi->stride[p] * (ptrdiff_t)y;
+
+ for (int x = 0; x < w; x += b_w) {
+ unsigned i = x / b_w + y / b_h * 2;
+ int c = ((i >> p) & 1);
+ if (bytes == 1) {
+ c *= (1 << 8) - 1;
+ for (int xs = x; xs < x + b_w; xs++)
+ ((uint8_t *)line)[xs] = c;
+ } else if (bytes == 2) {
+ c *= (1 << 16) - 1;
+ for (int xs = x; xs < x + b_w; xs++)
+ ((uint16_t *)line)[xs] = c;
+ }
+ }
+ }
+ }
+
+ return mpi;
+}
+
+static void dump_image(struct scale_test *stest, const char *name,
+ struct mp_image *img)
+{
+ char *path = mp_tprintf(4096, "%s/%s.png", stest->outdir, name);
+
+ struct image_writer_opts opts = image_writer_opts_defaults;
+ opts.format = AV_CODEC_ID_PNG;
+
+ if (!write_image(img, &opts, path, NULL, NULL)) {
+ printf("Failed to write '%s'.\n", path);
+ abort();
+ }
+}
+
+// Compare 2 images (same format and size) for exact pixel data match.
+// Does generally not work with formats that include undefined padding.
+// Does not work with non-byte aligned formats.
+static void assert_imgs_equal(struct scale_test *stest, FILE *f,
+ struct mp_image *ref, struct mp_image *new)
+{
+ assert(ref->imgfmt == new->imgfmt);
+ assert(ref->w == new->w);
+ assert(ref->h == new->h);
+
+ assert(ref->fmt.flags & MP_IMGFLAG_BYTE_ALIGNED);
+ assert(ref->fmt.bpp[0]);
+
+ for (int p = 0; p < ref->num_planes; p++) {
+ for (int y = 0; y < ref->h; y++) {
+ void *line_r = ref->planes[p] + ref->stride[p] * (ptrdiff_t)y;
+ void *line_o = new->planes[p] + new->stride[p] * (ptrdiff_t)y;
+ size_t size = mp_image_plane_bytes(ref, p, 0, new->w);
+
+ bool ok = memcmp(line_r, line_o, size) == 0;
+ if (!ok) {
+ stest->fail += 1;
+ char *fn_a = mp_tprintf(80, "img%d_ref", stest->fail);
+ char *fn_b = mp_tprintf(80, "img%d_new", stest->fail);
+ fprintf(f, "Images mismatching, dumping to %s/%s\n", fn_a, fn_b);
+ dump_image(stest, fn_a, ref);
+ dump_image(stest, fn_b, new);
+ return;
+ }
+ }
+ }
+}
+
+void repack_test_run(struct scale_test *stest)
+{
+ char *logname = mp_tprintf(80, "%s.log", stest->test_name);
+ FILE *f = test_open_out(stest->outdir, logname);
+
+ if (!stest->sws) {
+ init_imgfmts_list();
+
+ stest->sws = mp_sws_alloc(stest);
+
+ stest->img_repack_rgb8 = gen_repack_test_img(256, 128, 1, true, false);
+ stest->img_repack_rgba8 = gen_repack_test_img(256, 128, 1, true, true);
+ stest->img_repack_rgb16 = gen_repack_test_img(256, 128, 2, true, false);
+ stest->img_repack_rgba16 = gen_repack_test_img(256, 128, 2, true, true);
+
+ talloc_steal(stest, stest->img_repack_rgb8);
+ talloc_steal(stest, stest->img_repack_rgba8);
+ talloc_steal(stest, stest->img_repack_rgb16);
+ talloc_steal(stest, stest->img_repack_rgba16);
+ }
+
+ for (int a = 0; a < num_imgfmts; a++) {
+ int mpfmt = imgfmts[a];
+ struct mp_imgfmt_desc fmtdesc = mp_imgfmt_get_desc(mpfmt);
+ struct mp_regular_imgfmt rdesc;
+ if (!mp_get_regular_imgfmt(&rdesc, mpfmt)) {
+ int ofmt = mp_find_other_endian(mpfmt);
+ if (!mp_get_regular_imgfmt(&rdesc, ofmt))
+ continue;
+ }
+ if (rdesc.num_planes > 1 || rdesc.forced_csp != MP_CSP_RGB)
+ continue;
+
+ struct mp_image *test_img = NULL;
+ bool alpha = fmtdesc.flags & MP_IMGFLAG_ALPHA;
+ bool hidepth = rdesc.component_size > 1;
+ if (alpha) {
+ test_img = hidepth ? stest->img_repack_rgba16 : stest->img_repack_rgba8;
+ } else {
+ test_img = hidepth ? stest->img_repack_rgb16 : stest->img_repack_rgb8;
+ }
+
+ if (test_img->imgfmt == mpfmt)
+ continue;
+
+ if (!stest->fns->supports_fmts(stest->fns_priv, mpfmt, test_img->imgfmt))
+ continue;
+
+ if (!mp_sws_supports_formats(stest->sws, mpfmt, test_img->imgfmt))
+ continue;
+
+ fprintf(f, "%s using %s\n", mp_imgfmt_to_name(mpfmt),
+ mp_imgfmt_to_name(test_img->imgfmt));
+
+ struct mp_image *dst = mp_image_alloc(mpfmt, test_img->w, test_img->h);
+ assert(dst);
+
+ // This tests packing.
+ bool ok = stest->fns->scale(stest->fns_priv, dst, test_img);
+ assert(ok);
+
+ // Cross-check with swscale in the other direction.
+ // (Mostly so we don't have to worry about padding.)
+ struct mp_image *src2 =
+ mp_image_alloc(test_img->imgfmt, test_img->w, test_img->h);
+ assert(src2);
+ ok = mp_sws_scale(stest->sws, src2, dst) >= 0;
+ assert_imgs_equal(stest, f, test_img, src2);
+
+ // Assume the other conversion direction also works.
+ assert(stest->fns->supports_fmts(stest->fns_priv, test_img->imgfmt, mpfmt));
+
+ struct mp_image *back = mp_image_alloc(test_img->imgfmt, dst->w, dst->h);
+ assert(back);
+
+ // This tests unpacking.
+ ok = stest->fns->scale(stest->fns_priv, back, dst);
+ assert(ok);
+
+ assert_imgs_equal(stest, f, test_img, back);
+
+ talloc_free(back);
+ talloc_free(src2);
+ talloc_free(dst);
+ }
+
+ fclose(f);
+
+ assert_text_files_equal(stest->refdir, stest->outdir, logname,
+ "This can fail if FFmpeg adds or removes pixfmts.");
+}
diff --git a/test/scale_test.h b/test/scale_test.h
new file mode 100644
index 0000000..5c83786
--- /dev/null
+++ b/test/scale_test.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "img_utils.h"
+#include "test_utils.h"
+#include "video/mp_image.h"
+
+struct scale_test_fns {
+ bool (*scale)(void *ctx, struct mp_image *dst, struct mp_image *src);
+ bool (*supports_fmts)(void *ctx, int imgfmt_dst, int imgfmt_src);
+};
+
+struct scale_test {
+ // To be filled in by user.
+ const struct scale_test_fns *fns;
+ void *fns_priv;
+ const char *test_name;
+ const char *refdir;
+ const char *outdir;
+
+ // Private.
+ struct mp_image *img_repack_rgb8;
+ struct mp_image *img_repack_rgba8;
+ struct mp_image *img_repack_rgb16;
+ struct mp_image *img_repack_rgba16;
+ struct mp_sws_context *sws;
+ int fail;
+};
+
+// Test color repacking between packed formats (typically RGB).
+void repack_test_run(struct scale_test *stest);
diff --git a/test/scale_zimg.c b/test/scale_zimg.c
new file mode 100644
index 0000000..57894be
--- /dev/null
+++ b/test/scale_zimg.c
@@ -0,0 +1,56 @@
+#include <libswscale/swscale.h>
+
+#include "scale_test.h"
+#include "video/fmt-conversion.h"
+#include "video/zimg.h"
+
+static bool scale(void *pctx, struct mp_image *dst, struct mp_image *src)
+{
+ struct mp_zimg_context *ctx = pctx;
+ return mp_zimg_convert(ctx, dst, src);
+}
+
+static bool supports_fmts(void *pctx, int imgfmt_dst, int imgfmt_src)
+{
+ return mp_zimg_supports_in_format(imgfmt_src) &&
+ mp_zimg_supports_out_format(imgfmt_dst);
+}
+
+static const struct scale_test_fns fns = {
+ .scale = scale,
+ .supports_fmts = supports_fmts,
+};
+
+int main(int argc, char *argv[])
+{
+ struct mp_zimg_context *zimg = mp_zimg_alloc();
+ zimg->opts.threads = 1;
+
+ struct scale_test *stest = talloc_zero(NULL, struct scale_test);
+ stest->fns = &fns;
+ stest->fns_priv = zimg;
+ stest->test_name = "repack_zimg";
+ stest->refdir = talloc_strdup(stest, argv[1]);
+ stest->outdir = talloc_strdup(stest, argv[2]);
+
+ repack_test_run(stest);
+
+ FILE *f = test_open_out(stest->outdir, "zimg_formats.txt");
+ for (int n = 0; n < num_imgfmts; n++) {
+ int imgfmt = imgfmts[n];
+ fprintf(f, "%15s%7s%7s%7s%8s |\n", mp_imgfmt_to_name(imgfmt),
+ mp_zimg_supports_in_format(imgfmt) ? " Zin" : "",
+ mp_zimg_supports_out_format(imgfmt) ? " Zout" : "",
+ sws_isSupportedInput(imgfmt2pixfmt(imgfmt)) ? " SWSin" : "",
+ sws_isSupportedOutput(imgfmt2pixfmt(imgfmt)) ? " SWSout" : "");
+
+ }
+ fclose(f);
+
+ assert_text_files_equal(stest->refdir, stest->outdir, "zimg_formats.txt",
+ "This can fail if FFmpeg/libswscale adds or removes pixfmts.");
+
+ talloc_free(stest);
+ talloc_free(zimg);
+ return 0;
+}
diff --git a/test/test_utils.c b/test/test_utils.c
new file mode 100644
index 0000000..b80caf8
--- /dev/null
+++ b/test/test_utils.c
@@ -0,0 +1,111 @@
+#include <libavutil/common.h>
+
+#include "common/msg.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "osdep/subprocess.h"
+#include "test_utils.h"
+
+#ifdef NDEBUG
+static_assert(false, "don't define NDEBUG for tests");
+#endif
+
+void assert_int_equal_impl(const char *file, int line, int64_t a, int64_t b)
+{
+ if (a != b) {
+ printf("%s:%d: %"PRId64" != %"PRId64"\n", file, line, a, b);
+ abort();
+ }
+}
+
+void assert_string_equal_impl(const char *file, int line,
+ const char *a, const char *b)
+{
+ if (strcmp(a, b) != 0) {
+ printf("%s:%d: '%s' != '%s'\n", file, line, a, b);
+ abort();
+ }
+}
+
+void assert_float_equal_impl(const char *file, int line,
+ double a, double b, double tolerance)
+{
+ if (fabs(a - b) > tolerance) {
+ printf("%s:%d: %f != %f\n", file, line, a, b);
+ abort();
+ }
+}
+
+FILE *test_open_out(const char *outdir, const char *name)
+{
+ mp_mkdirp(outdir);
+ assert(mp_path_isdir(outdir));
+ char *path = mp_tprintf(4096, "%s/%s", outdir, name);
+ FILE *f = fopen(path, "wb");
+ if (!f) {
+ printf("Could not open '%s' for writing: %s\n", path,
+ mp_strerror(errno));
+ abort();
+ }
+ return f;
+}
+
+void assert_text_files_equal_impl(const char *file, int line,
+ const char *refdir, const char *outdir,
+ const char *ref, const char *new,
+ const char *err)
+{
+ char *path_ref = mp_tprintf(4096, "%s/%s", refdir, ref);
+ char *path_new = mp_tprintf(4096, "%s/%s", outdir, new);
+
+ struct mp_subprocess_opts opts = {
+ .exe = "diff",
+ .args = (char*[]){"diff", "-u", "--", path_ref, path_new, 0},
+ .fds = { {0, .src_fd = 0}, {1, .src_fd = 1}, {2, .src_fd = 2} },
+ .num_fds = 3,
+ };
+
+ struct mp_subprocess_result res;
+ mp_subprocess2(&opts, &res);
+
+ if (res.error || res.exit_status) {
+ if (res.error)
+ printf("Note: %s\n", mp_subprocess_err_str(res.error));
+ printf("Giving up.\n");
+ abort();
+ }
+}
+
+static void hexdump(const uint8_t *d, size_t size)
+{
+ printf("|");
+ while (size--) {
+ printf(" %02x", d[0]);
+ d++;
+ }
+ printf(" |\n");
+}
+
+void assert_memcmp_impl(const char *file, int line,
+ const void *a, const void *b, size_t size)
+{
+ if (memcmp(a, b, size) == 0)
+ return;
+
+ printf("%s:%d: mismatching data:\n", file, line);
+ hexdump(a, size);
+ hexdump(b, size);
+ abort();
+}
+
+/* Stubs: see test_utils.h */
+struct mp_log *const mp_null_log;
+const char *mp_help_text;
+
+void mp_msg(struct mp_log *log, int lev, const char *format, ...) {};
+int mp_msg_find_level(const char *s) {return 0;};
+int mp_msg_level(struct mp_log *log) {return 0;};
+void mp_write_console_ansi(void) {};
+void mp_set_avdict(AVDictionary **dict, char **kv) {};
+struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent,
+ const char *name) { return NULL; };
diff --git a/test/test_utils.h b/test/test_utils.h
new file mode 100644
index 0000000..66615d3
--- /dev/null
+++ b/test/test_utils.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <float.h>
+#include <inttypes.h>
+#include <math.h>
+
+#include "common/common.h"
+
+#define assert_true(x) assert(x)
+#define assert_false(x) assert(!(x))
+#define assert_int_equal(a, b) \
+ assert_int_equal_impl(__FILE__, __LINE__, (a), (b))
+#define assert_string_equal(a, b) \
+ assert_string_equal_impl(__FILE__, __LINE__, (a), (b))
+#define assert_float_equal(a, b, tolerance) \
+ assert_float_equal_impl(__FILE__, __LINE__, (a), (b), (tolerance))
+
+// Assert that memcmp(a,b,s)==0, or hexdump output on failure.
+#define assert_memcmp(a, b, s) \
+ assert_memcmp_impl(__FILE__, __LINE__, (a), (b), (s))
+
+// Require that the files "ref" and "new" are the same. The paths can be
+// relative to ref_path and out_path respectively. If they're not the same,
+// the output of "diff" is shown, the err message (if not NULL), and the test
+// fails.
+#define assert_text_files_equal(refdir, outdir, name, err) \
+ assert_text_files_equal_impl(__FILE__, __LINE__, (refdir), (outdir), (name), (name), (err))
+
+void assert_int_equal_impl(const char *file, int line, int64_t a, int64_t b);
+void assert_string_equal_impl(const char *file, int line,
+ const char *a, const char *b);
+void assert_float_equal_impl(const char *file, int line,
+ double a, double b, double tolerance);
+void assert_text_files_equal_impl(const char *file, int line,
+ const char *refdir, const char *outdir,
+ const char *ref, const char *new,
+ const char *err);
+void assert_memcmp_impl(const char *file, int line,
+ const void *a, const void *b, size_t size);
+
+// Open a new file in the build dir path. Always succeeds.
+FILE *test_open_out(const char *outdir, const char *name);
+
+/* Stubs */
+
+// Files commonly import common/msg.h which requires these to be
+// defined. We don't actually need mpv's logging system here so
+// just define these as stubs that do nothing.
+struct mp_log;
+void mp_msg(struct mp_log *log, int lev, const char *format, ...)
+ PRINTF_ATTRIBUTE(3, 4);
+int mp_msg_find_level(const char *s);
+int mp_msg_level(struct mp_log *log);
+void mp_write_console_ansi(void);
+typedef struct AVDictionary AVDictionary;
+void mp_set_avdict(AVDictionary **dict, char **kv);
diff --git a/test/timer.c b/test/timer.c
new file mode 100644
index 0000000..f85009c
--- /dev/null
+++ b/test/timer.c
@@ -0,0 +1,41 @@
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "test_utils.h"
+
+#include <time.h>
+#include <sys/time.h>
+#include <limits.h>
+
+int main(void)
+{
+ mp_time_init();
+
+ /* timekeeping */
+ {
+ int64_t now = mp_time_ns();
+ assert_true(now > 0);
+
+ mp_sleep_ns(MP_TIME_MS_TO_NS(10));
+
+ int64_t now2 = mp_time_ns();
+ assert_true(now2 > now);
+
+ mp_sleep_ns(MP_TIME_MS_TO_NS(10));
+
+ double now3 = mp_time_sec();
+ assert_true(now3 > MP_TIME_NS_TO_S(now2));
+ }
+
+ /* arithmetic */
+ {
+ const int64_t test = 123456;
+ assert_int_equal(mp_time_ns_add(test, 1.0), test + MP_TIME_S_TO_NS(1));
+ assert_int_equal(mp_time_ns_add(test, DBL_MAX), INT64_MAX);
+ assert_int_equal(mp_time_ns_add(test, -1e13), 1);
+
+ const int64_t test2 = INT64_MAX - MP_TIME_S_TO_NS(20);
+ assert_int_equal(mp_time_ns_add(test2, 20.44), INT64_MAX);
+ }
+
+ return 0;
+}
diff --git a/video/csputils.c b/video/csputils.c
new file mode 100644
index 0000000..59200c5
--- /dev/null
+++ b/video/csputils.c
@@ -0,0 +1,1020 @@
+/*
+ * Common code related to colorspaces and conversion
+ *
+ * Copyleft (C) 2009 Reimar Döffinger <Reimar.Doeffinger@gmx.de>
+ *
+ * mp_invert_cmat based on DarkPlaces engine (relicensed from GPL to LGPL)
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+#include <math.h>
+#include <assert.h>
+#include <libavutil/common.h>
+#include <libavcodec/avcodec.h>
+
+#include "mp_image.h"
+#include "csputils.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+
+const struct m_opt_choice_alternatives mp_csp_names[] = {
+ {"auto", MP_CSP_AUTO},
+ {"bt.601", MP_CSP_BT_601},
+ {"bt.709", MP_CSP_BT_709},
+ {"smpte-240m", MP_CSP_SMPTE_240M},
+ {"bt.2020-ncl", MP_CSP_BT_2020_NC},
+ {"bt.2020-cl", MP_CSP_BT_2020_C},
+ {"rgb", MP_CSP_RGB},
+ {"xyz", MP_CSP_XYZ},
+ {"ycgco", MP_CSP_YCGCO},
+ {0}
+};
+
+const struct m_opt_choice_alternatives mp_csp_levels_names[] = {
+ {"auto", MP_CSP_LEVELS_AUTO},
+ {"limited", MP_CSP_LEVELS_TV},
+ {"full", MP_CSP_LEVELS_PC},
+ {0}
+};
+
+const struct m_opt_choice_alternatives mp_csp_prim_names[] = {
+ {"auto", MP_CSP_PRIM_AUTO},
+ {"bt.601-525", MP_CSP_PRIM_BT_601_525},
+ {"bt.601-625", MP_CSP_PRIM_BT_601_625},
+ {"bt.709", MP_CSP_PRIM_BT_709},
+ {"bt.2020", MP_CSP_PRIM_BT_2020},
+ {"bt.470m", MP_CSP_PRIM_BT_470M},
+ {"apple", MP_CSP_PRIM_APPLE},
+ {"adobe", MP_CSP_PRIM_ADOBE},
+ {"prophoto", MP_CSP_PRIM_PRO_PHOTO},
+ {"cie1931", MP_CSP_PRIM_CIE_1931},
+ {"dci-p3", MP_CSP_PRIM_DCI_P3},
+ {"display-p3", MP_CSP_PRIM_DISPLAY_P3},
+ {"v-gamut", MP_CSP_PRIM_V_GAMUT},
+ {"s-gamut", MP_CSP_PRIM_S_GAMUT},
+ {"ebu3213", MP_CSP_PRIM_EBU_3213},
+ {"film-c", MP_CSP_PRIM_FILM_C},
+ {"aces-ap0", MP_CSP_PRIM_ACES_AP0},
+ {"aces-ap1", MP_CSP_PRIM_ACES_AP1},
+ {0}
+};
+
+const struct m_opt_choice_alternatives mp_csp_trc_names[] = {
+ {"auto", MP_CSP_TRC_AUTO},
+ {"bt.1886", MP_CSP_TRC_BT_1886},
+ {"srgb", MP_CSP_TRC_SRGB},
+ {"linear", MP_CSP_TRC_LINEAR},
+ {"gamma1.8", MP_CSP_TRC_GAMMA18},
+ {"gamma2.0", MP_CSP_TRC_GAMMA20},
+ {"gamma2.2", MP_CSP_TRC_GAMMA22},
+ {"gamma2.4", MP_CSP_TRC_GAMMA24},
+ {"gamma2.6", MP_CSP_TRC_GAMMA26},
+ {"gamma2.8", MP_CSP_TRC_GAMMA28},
+ {"prophoto", MP_CSP_TRC_PRO_PHOTO},
+ {"pq", MP_CSP_TRC_PQ},
+ {"hlg", MP_CSP_TRC_HLG},
+ {"v-log", MP_CSP_TRC_V_LOG},
+ {"s-log1", MP_CSP_TRC_S_LOG1},
+ {"s-log2", MP_CSP_TRC_S_LOG2},
+ {"st428", MP_CSP_TRC_ST428},
+ {0}
+};
+
+const struct m_opt_choice_alternatives mp_csp_light_names[] = {
+ {"auto", MP_CSP_LIGHT_AUTO},
+ {"display", MP_CSP_LIGHT_DISPLAY},
+ {"hlg", MP_CSP_LIGHT_SCENE_HLG},
+ {"709-1886", MP_CSP_LIGHT_SCENE_709_1886},
+ {"gamma1.2", MP_CSP_LIGHT_SCENE_1_2},
+ {0}
+};
+
+const struct m_opt_choice_alternatives mp_chroma_names[] = {
+ {"unknown", MP_CHROMA_AUTO},
+ {"uhd", MP_CHROMA_TOPLEFT},
+ {"mpeg2/4/h264",MP_CHROMA_LEFT},
+ {"mpeg1/jpeg", MP_CHROMA_CENTER},
+ {0}
+};
+
+const struct m_opt_choice_alternatives mp_alpha_names[] = {
+ {"auto", MP_ALPHA_AUTO},
+ {"straight", MP_ALPHA_STRAIGHT},
+ {"premul", MP_ALPHA_PREMUL},
+ {0}
+};
+
+void mp_colorspace_merge(struct mp_colorspace *orig, struct mp_colorspace *new)
+{
+ if (!orig->space)
+ orig->space = new->space;
+ if (!orig->levels)
+ orig->levels = new->levels;
+ if (!orig->primaries)
+ orig->primaries = new->primaries;
+ if (!orig->gamma)
+ orig->gamma = new->gamma;
+ if (!orig->light)
+ orig->light = new->light;
+ pl_hdr_metadata_merge(&orig->hdr, &new->hdr);
+}
+
+// The short name _must_ match with what vf_stereo3d accepts (if supported).
+// The long name in comments is closer to the Matroska spec (StereoMode element).
+// The numeric index matches the Matroska StereoMode value. If you add entries
+// that don't match Matroska, make sure demux_mkv.c rejects them properly.
+const struct m_opt_choice_alternatives mp_stereo3d_names[] = {
+ {"no", -1}, // disable/invalid
+ {"mono", 0},
+ {"sbs2l", 1}, // "side_by_side_left"
+ {"ab2r", 2}, // "top_bottom_right"
+ {"ab2l", 3}, // "top_bottom_left"
+ {"checkr", 4}, // "checkboard_right" (unsupported by vf_stereo3d)
+ {"checkl", 5}, // "checkboard_left" (unsupported by vf_stereo3d)
+ {"irr", 6}, // "row_interleaved_right"
+ {"irl", 7}, // "row_interleaved_left"
+ {"icr", 8}, // "column_interleaved_right" (unsupported by vf_stereo3d)
+ {"icl", 9}, // "column_interleaved_left" (unsupported by vf_stereo3d)
+ {"arcc", 10}, // "anaglyph_cyan_red" (Matroska: unclear which mode)
+ {"sbs2r", 11}, // "side_by_side_right"
+ {"agmc", 12}, // "anaglyph_green_magenta" (Matroska: unclear which mode)
+ {"al", 13}, // "alternating frames left first"
+ {"ar", 14}, // "alternating frames right first"
+ {0}
+};
+
+enum mp_csp avcol_spc_to_mp_csp(int avcolorspace)
+{
+ switch (avcolorspace) {
+ case AVCOL_SPC_BT709: return MP_CSP_BT_709;
+ case AVCOL_SPC_BT470BG: return MP_CSP_BT_601;
+ case AVCOL_SPC_BT2020_NCL: return MP_CSP_BT_2020_NC;
+ case AVCOL_SPC_BT2020_CL: return MP_CSP_BT_2020_C;
+ case AVCOL_SPC_SMPTE170M: return MP_CSP_BT_601;
+ case AVCOL_SPC_SMPTE240M: return MP_CSP_SMPTE_240M;
+ case AVCOL_SPC_RGB: return MP_CSP_RGB;
+ case AVCOL_SPC_YCOCG: return MP_CSP_YCGCO;
+ default: return MP_CSP_AUTO;
+ }
+}
+
+enum mp_csp_levels avcol_range_to_mp_csp_levels(int avrange)
+{
+ switch (avrange) {
+ case AVCOL_RANGE_MPEG: return MP_CSP_LEVELS_TV;
+ case AVCOL_RANGE_JPEG: return MP_CSP_LEVELS_PC;
+ default: return MP_CSP_LEVELS_AUTO;
+ }
+}
+
+enum mp_csp_prim avcol_pri_to_mp_csp_prim(int avpri)
+{
+ switch (avpri) {
+ case AVCOL_PRI_SMPTE240M: // Same as below
+ case AVCOL_PRI_SMPTE170M: return MP_CSP_PRIM_BT_601_525;
+ case AVCOL_PRI_BT470BG: return MP_CSP_PRIM_BT_601_625;
+ case AVCOL_PRI_BT709: return MP_CSP_PRIM_BT_709;
+ case AVCOL_PRI_BT2020: return MP_CSP_PRIM_BT_2020;
+ case AVCOL_PRI_BT470M: return MP_CSP_PRIM_BT_470M;
+ case AVCOL_PRI_SMPTE431: return MP_CSP_PRIM_DCI_P3;
+ case AVCOL_PRI_SMPTE432: return MP_CSP_PRIM_DISPLAY_P3;
+ default: return MP_CSP_PRIM_AUTO;
+ }
+}
+
+enum mp_csp_trc avcol_trc_to_mp_csp_trc(int avtrc)
+{
+ switch (avtrc) {
+ case AVCOL_TRC_BT709:
+ case AVCOL_TRC_SMPTE170M:
+ case AVCOL_TRC_SMPTE240M:
+ case AVCOL_TRC_BT1361_ECG:
+ case AVCOL_TRC_BT2020_10:
+ case AVCOL_TRC_BT2020_12: return MP_CSP_TRC_BT_1886;
+ case AVCOL_TRC_IEC61966_2_1: return MP_CSP_TRC_SRGB;
+ case AVCOL_TRC_LINEAR: return MP_CSP_TRC_LINEAR;
+ case AVCOL_TRC_GAMMA22: return MP_CSP_TRC_GAMMA22;
+ case AVCOL_TRC_GAMMA28: return MP_CSP_TRC_GAMMA28;
+ case AVCOL_TRC_SMPTEST2084: return MP_CSP_TRC_PQ;
+ case AVCOL_TRC_ARIB_STD_B67: return MP_CSP_TRC_HLG;
+ case AVCOL_TRC_SMPTE428: return MP_CSP_TRC_ST428;
+ default: return MP_CSP_TRC_AUTO;
+ }
+}
+
+int mp_csp_to_avcol_spc(enum mp_csp colorspace)
+{
+ switch (colorspace) {
+ case MP_CSP_BT_709: return AVCOL_SPC_BT709;
+ case MP_CSP_BT_601: return AVCOL_SPC_BT470BG;
+ case MP_CSP_BT_2020_NC: return AVCOL_SPC_BT2020_NCL;
+ case MP_CSP_BT_2020_C: return AVCOL_SPC_BT2020_CL;
+ case MP_CSP_SMPTE_240M: return AVCOL_SPC_SMPTE240M;
+ case MP_CSP_RGB: return AVCOL_SPC_RGB;
+ case MP_CSP_YCGCO: return AVCOL_SPC_YCOCG;
+ default: return AVCOL_SPC_UNSPECIFIED;
+ }
+}
+
+int mp_csp_levels_to_avcol_range(enum mp_csp_levels range)
+{
+ switch (range) {
+ case MP_CSP_LEVELS_TV: return AVCOL_RANGE_MPEG;
+ case MP_CSP_LEVELS_PC: return AVCOL_RANGE_JPEG;
+ default: return AVCOL_RANGE_UNSPECIFIED;
+ }
+}
+
+int mp_csp_prim_to_avcol_pri(enum mp_csp_prim prim)
+{
+ switch (prim) {
+ case MP_CSP_PRIM_BT_601_525: return AVCOL_PRI_SMPTE170M;
+ case MP_CSP_PRIM_BT_601_625: return AVCOL_PRI_BT470BG;
+ case MP_CSP_PRIM_BT_709: return AVCOL_PRI_BT709;
+ case MP_CSP_PRIM_BT_2020: return AVCOL_PRI_BT2020;
+ case MP_CSP_PRIM_BT_470M: return AVCOL_PRI_BT470M;
+ case MP_CSP_PRIM_DCI_P3: return AVCOL_PRI_SMPTE431;
+ case MP_CSP_PRIM_DISPLAY_P3: return AVCOL_PRI_SMPTE432;
+ default: return AVCOL_PRI_UNSPECIFIED;
+ }
+}
+
+int mp_csp_trc_to_avcol_trc(enum mp_csp_trc trc)
+{
+ switch (trc) {
+ // We just call it BT.1886 since we're decoding, but it's still BT.709
+ case MP_CSP_TRC_BT_1886: return AVCOL_TRC_BT709;
+ case MP_CSP_TRC_SRGB: return AVCOL_TRC_IEC61966_2_1;
+ case MP_CSP_TRC_LINEAR: return AVCOL_TRC_LINEAR;
+ case MP_CSP_TRC_GAMMA22: return AVCOL_TRC_GAMMA22;
+ case MP_CSP_TRC_GAMMA28: return AVCOL_TRC_GAMMA28;
+ case MP_CSP_TRC_PQ: return AVCOL_TRC_SMPTEST2084;
+ case MP_CSP_TRC_HLG: return AVCOL_TRC_ARIB_STD_B67;
+ case MP_CSP_TRC_ST428: return AVCOL_TRC_SMPTE428;
+ default: return AVCOL_TRC_UNSPECIFIED;
+ }
+}
+
+enum mp_csp mp_csp_guess_colorspace(int width, int height)
+{
+ return width >= 1280 || height > 576 ? MP_CSP_BT_709 : MP_CSP_BT_601;
+}
+
+enum mp_csp_prim mp_csp_guess_primaries(int width, int height)
+{
+ // HD content
+ if (width >= 1280 || height > 576)
+ return MP_CSP_PRIM_BT_709;
+
+ switch (height) {
+ case 576: // Typical PAL content, including anamorphic/squared
+ return MP_CSP_PRIM_BT_601_625;
+
+ case 480: // Typical NTSC content, including squared
+ case 486: // NTSC Pro or anamorphic NTSC
+ return MP_CSP_PRIM_BT_601_525;
+
+ default: // No good metric, just pick BT.709 to minimize damage
+ return MP_CSP_PRIM_BT_709;
+ }
+}
+
+enum mp_chroma_location avchroma_location_to_mp(int avloc)
+{
+ switch (avloc) {
+ case AVCHROMA_LOC_TOPLEFT: return MP_CHROMA_TOPLEFT;
+ case AVCHROMA_LOC_LEFT: return MP_CHROMA_LEFT;
+ case AVCHROMA_LOC_CENTER: return MP_CHROMA_CENTER;
+ default: return MP_CHROMA_AUTO;
+ }
+}
+
+int mp_chroma_location_to_av(enum mp_chroma_location mploc)
+{
+ switch (mploc) {
+ case MP_CHROMA_TOPLEFT: return AVCHROMA_LOC_TOPLEFT;
+ case MP_CHROMA_LEFT: return AVCHROMA_LOC_LEFT;
+ case MP_CHROMA_CENTER: return AVCHROMA_LOC_CENTER;
+ default: return AVCHROMA_LOC_UNSPECIFIED;
+ }
+}
+
+// Return location of chroma samples relative to luma samples. 0/0 means
+// centered. Other possible values are -1 (top/left) and +1 (right/bottom).
+void mp_get_chroma_location(enum mp_chroma_location loc, int *x, int *y)
+{
+ *x = 0;
+ *y = 0;
+ if (loc == MP_CHROMA_LEFT || loc == MP_CHROMA_TOPLEFT)
+ *x = -1;
+ if (loc == MP_CHROMA_TOPLEFT)
+ *y = -1;
+}
+
+void mp_invert_matrix3x3(float m[3][3])
+{
+ float m00 = m[0][0], m01 = m[0][1], m02 = m[0][2],
+ m10 = m[1][0], m11 = m[1][1], m12 = m[1][2],
+ m20 = m[2][0], m21 = m[2][1], m22 = m[2][2];
+
+ // calculate the adjoint
+ m[0][0] = (m11 * m22 - m21 * m12);
+ m[0][1] = -(m01 * m22 - m21 * m02);
+ m[0][2] = (m01 * m12 - m11 * m02);
+ m[1][0] = -(m10 * m22 - m20 * m12);
+ m[1][1] = (m00 * m22 - m20 * m02);
+ m[1][2] = -(m00 * m12 - m10 * m02);
+ m[2][0] = (m10 * m21 - m20 * m11);
+ m[2][1] = -(m00 * m21 - m20 * m01);
+ m[2][2] = (m00 * m11 - m10 * m01);
+
+ // calculate the determinant (as inverse == 1/det * adjoint,
+ // adjoint * m == identity * det, so this calculates the det)
+ float det = m00 * m[0][0] + m10 * m[0][1] + m20 * m[0][2];
+ det = 1.0f / det;
+
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++)
+ m[i][j] *= det;
+ }
+}
+
+// A := A * B
+static void mp_mul_matrix3x3(float a[3][3], float b[3][3])
+{
+ float a00 = a[0][0], a01 = a[0][1], a02 = a[0][2],
+ a10 = a[1][0], a11 = a[1][1], a12 = a[1][2],
+ a20 = a[2][0], a21 = a[2][1], a22 = a[2][2];
+
+ for (int i = 0; i < 3; i++) {
+ a[0][i] = a00 * b[0][i] + a01 * b[1][i] + a02 * b[2][i];
+ a[1][i] = a10 * b[0][i] + a11 * b[1][i] + a12 * b[2][i];
+ a[2][i] = a20 * b[0][i] + a21 * b[1][i] + a22 * b[2][i];
+ }
+}
+
+// return the primaries associated with a certain mp_csp_primaries val
+struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
+{
+ /*
+ Values from: ITU-R Recommendations BT.470-6, BT.601-7, BT.709-5, BT.2020-0
+
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.470-6-199811-S!!PDF-E.pdf
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-5-200204-I!!PDF-E.pdf
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-0-201208-I!!PDF-E.pdf
+
+ Other colorspaces from https://en.wikipedia.org/wiki/RGB_color_space#Specifications
+ */
+
+ // CIE standard illuminant series
+ static const struct mp_csp_col_xy
+ d50 = {0.34577, 0.35850},
+ d65 = {0.31271, 0.32902},
+ c = {0.31006, 0.31616},
+ dci = {0.31400, 0.35100},
+ e = {1.0/3.0, 1.0/3.0};
+
+ switch (spc) {
+ case MP_CSP_PRIM_BT_470M:
+ return (struct mp_csp_primaries) {
+ .red = {0.670, 0.330},
+ .green = {0.210, 0.710},
+ .blue = {0.140, 0.080},
+ .white = c
+ };
+ case MP_CSP_PRIM_BT_601_525:
+ return (struct mp_csp_primaries) {
+ .red = {0.630, 0.340},
+ .green = {0.310, 0.595},
+ .blue = {0.155, 0.070},
+ .white = d65
+ };
+ case MP_CSP_PRIM_BT_601_625:
+ return (struct mp_csp_primaries) {
+ .red = {0.640, 0.330},
+ .green = {0.290, 0.600},
+ .blue = {0.150, 0.060},
+ .white = d65
+ };
+ // This is the default assumption if no colorspace information could
+ // be determined, eg. for files which have no video channel.
+ case MP_CSP_PRIM_AUTO:
+ case MP_CSP_PRIM_BT_709:
+ return (struct mp_csp_primaries) {
+ .red = {0.640, 0.330},
+ .green = {0.300, 0.600},
+ .blue = {0.150, 0.060},
+ .white = d65
+ };
+ case MP_CSP_PRIM_BT_2020:
+ return (struct mp_csp_primaries) {
+ .red = {0.708, 0.292},
+ .green = {0.170, 0.797},
+ .blue = {0.131, 0.046},
+ .white = d65
+ };
+ case MP_CSP_PRIM_APPLE:
+ return (struct mp_csp_primaries) {
+ .red = {0.625, 0.340},
+ .green = {0.280, 0.595},
+ .blue = {0.115, 0.070},
+ .white = d65
+ };
+ case MP_CSP_PRIM_ADOBE:
+ return (struct mp_csp_primaries) {
+ .red = {0.640, 0.330},
+ .green = {0.210, 0.710},
+ .blue = {0.150, 0.060},
+ .white = d65
+ };
+ case MP_CSP_PRIM_PRO_PHOTO:
+ return (struct mp_csp_primaries) {
+ .red = {0.7347, 0.2653},
+ .green = {0.1596, 0.8404},
+ .blue = {0.0366, 0.0001},
+ .white = d50
+ };
+ case MP_CSP_PRIM_CIE_1931:
+ return (struct mp_csp_primaries) {
+ .red = {0.7347, 0.2653},
+ .green = {0.2738, 0.7174},
+ .blue = {0.1666, 0.0089},
+ .white = e
+ };
+ // From SMPTE RP 431-2 and 432-1
+ case MP_CSP_PRIM_DCI_P3:
+ case MP_CSP_PRIM_DISPLAY_P3:
+ return (struct mp_csp_primaries) {
+ .red = {0.680, 0.320},
+ .green = {0.265, 0.690},
+ .blue = {0.150, 0.060},
+ .white = spc == MP_CSP_PRIM_DCI_P3 ? dci : d65
+ };
+ // From Panasonic VARICAM reference manual
+ case MP_CSP_PRIM_V_GAMUT:
+ return (struct mp_csp_primaries) {
+ .red = {0.730, 0.280},
+ .green = {0.165, 0.840},
+ .blue = {0.100, -0.03},
+ .white = d65
+ };
+ // From Sony S-Log reference manual
+ case MP_CSP_PRIM_S_GAMUT:
+ return (struct mp_csp_primaries) {
+ .red = {0.730, 0.280},
+ .green = {0.140, 0.855},
+ .blue = {0.100, -0.05},
+ .white = d65
+ };
+ // from EBU Tech. 3213-E
+ case MP_CSP_PRIM_EBU_3213:
+ return (struct mp_csp_primaries) {
+ .red = {0.630, 0.340},
+ .green = {0.295, 0.605},
+ .blue = {0.155, 0.077},
+ .white = d65
+ };
+ // From H.273, traditional film with Illuminant C
+ case MP_CSP_PRIM_FILM_C:
+ return (struct mp_csp_primaries) {
+ .red = {0.681, 0.319},
+ .green = {0.243, 0.692},
+ .blue = {0.145, 0.049},
+ .white = c
+ };
+ // From libplacebo source code
+ case MP_CSP_PRIM_ACES_AP0:
+ return (struct mp_csp_primaries) {
+ .red = {0.7347, 0.2653},
+ .green = {0.0000, 1.0000},
+ .blue = {0.0001, -0.0770},
+ .white = {0.32168, 0.33767},
+ };
+ // From libplacebo source code
+ case MP_CSP_PRIM_ACES_AP1:
+ return (struct mp_csp_primaries) {
+ .red = {0.713, 0.293},
+ .green = {0.165, 0.830},
+ .blue = {0.128, 0.044},
+ .white = {0.32168, 0.33767},
+ };
+ default:
+ return (struct mp_csp_primaries) {{0}};
+ }
+}
+
+// Get the nominal peak for a given colorspace, relative to the reference white
+// level. In other words, this returns the brightest encodable value that can
+// be represented by a given transfer curve.
+float mp_trc_nom_peak(enum mp_csp_trc trc)
+{
+ switch (trc) {
+ case MP_CSP_TRC_PQ: return 10000.0 / MP_REF_WHITE;
+ case MP_CSP_TRC_HLG: return 12.0 / MP_REF_WHITE_HLG;
+ case MP_CSP_TRC_V_LOG: return 46.0855;
+ case MP_CSP_TRC_S_LOG1: return 6.52;
+ case MP_CSP_TRC_S_LOG2: return 9.212;
+ }
+
+ return 1.0;
+}
+
+bool mp_trc_is_hdr(enum mp_csp_trc trc)
+{
+ return mp_trc_nom_peak(trc) > 1.0;
+}
+
+// Compute the RGB/XYZ matrix as described here:
+// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3])
+{
+ float S[3], X[4], Z[4];
+
+ // Convert from CIE xyY to XYZ. Note that Y=1 holds true for all primaries
+ X[0] = space.red.x / space.red.y;
+ X[1] = space.green.x / space.green.y;
+ X[2] = space.blue.x / space.blue.y;
+ X[3] = space.white.x / space.white.y;
+
+ Z[0] = (1 - space.red.x - space.red.y) / space.red.y;
+ Z[1] = (1 - space.green.x - space.green.y) / space.green.y;
+ Z[2] = (1 - space.blue.x - space.blue.y) / space.blue.y;
+ Z[3] = (1 - space.white.x - space.white.y) / space.white.y;
+
+ // S = XYZ^-1 * W
+ for (int i = 0; i < 3; i++) {
+ m[0][i] = X[i];
+ m[1][i] = 1;
+ m[2][i] = Z[i];
+ }
+
+ mp_invert_matrix3x3(m);
+
+ for (int i = 0; i < 3; i++)
+ S[i] = m[i][0] * X[3] + m[i][1] * 1 + m[i][2] * Z[3];
+
+ // M = [Sc * XYZc]
+ for (int i = 0; i < 3; i++) {
+ m[0][i] = S[i] * X[i];
+ m[1][i] = S[i] * 1;
+ m[2][i] = S[i] * Z[i];
+ }
+}
+
+// M := M * XYZd<-XYZs
+static void mp_apply_chromatic_adaptation(struct mp_csp_col_xy src,
+ struct mp_csp_col_xy dest, float m[3][3])
+{
+ // If the white points are nearly identical, this is a wasteful identity
+ // operation.
+ if (fabs(src.x - dest.x) < 1e-6 && fabs(src.y - dest.y) < 1e-6)
+ return;
+
+ // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma
+ // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
+ float C[3][2], tmp[3][3] = {{0}};
+
+ // Ma = Bradford matrix, arguably most popular method in use today.
+ // This is derived experimentally and thus hard-coded.
+ float bradford[3][3] = {
+ { 0.8951, 0.2664, -0.1614 },
+ { -0.7502, 1.7135, 0.0367 },
+ { 0.0389, -0.0685, 1.0296 },
+ };
+
+ for (int i = 0; i < 3; i++) {
+ // source cone
+ C[i][0] = bradford[i][0] * mp_xy_X(src)
+ + bradford[i][1] * 1
+ + bradford[i][2] * mp_xy_Z(src);
+
+ // dest cone
+ C[i][1] = bradford[i][0] * mp_xy_X(dest)
+ + bradford[i][1] * 1
+ + bradford[i][2] * mp_xy_Z(dest);
+ }
+
+ // tmp := I * [Cd/Cs] * Ma
+ for (int i = 0; i < 3; i++)
+ tmp[i][i] = C[i][1] / C[i][0];
+
+ mp_mul_matrix3x3(tmp, bradford);
+
+ // M := M * Ma^-1 * tmp
+ mp_invert_matrix3x3(bradford);
+ mp_mul_matrix3x3(m, bradford);
+ mp_mul_matrix3x3(m, tmp);
+}
+
+// get the coefficients of the source -> dest cms matrix
+void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest,
+ enum mp_render_intent intent, float m[3][3])
+{
+ float tmp[3][3];
+
+ // In saturation mapping, we don't care about accuracy and just want
+ // primaries to map to primaries, making this an identity transformation.
+ if (intent == MP_INTENT_SATURATION) {
+ for (int i = 0; i < 3; i++)
+ m[i][i] = 1;
+ return;
+ }
+
+ // RGBd<-RGBs = RGBd<-XYZd * XYZd<-XYZs * XYZs<-RGBs
+ // Equations from: http://www.brucelindbloom.com/index.html?Math.html
+ // Note: Perceptual is treated like relative colorimetric. There's no
+ // definition for perceptual other than "make it look good".
+
+ // RGBd<-XYZd, inverted from XYZd<-RGBd
+ mp_get_rgb2xyz_matrix(dest, m);
+ mp_invert_matrix3x3(m);
+
+ // Chromatic adaptation, except in absolute colorimetric intent
+ if (intent != MP_INTENT_ABSOLUTE_COLORIMETRIC)
+ mp_apply_chromatic_adaptation(src.white, dest.white, m);
+
+ // XYZs<-RGBs
+ mp_get_rgb2xyz_matrix(src, tmp);
+ mp_mul_matrix3x3(m, tmp);
+}
+
+// get the coefficients of an ST 428-1 xyz -> rgb conversion matrix
+// intent = the rendering intent used to convert to the target primaries
+static void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params,
+ enum mp_render_intent intent, struct mp_cmat *m)
+{
+ // Convert to DCI-P3
+ struct mp_csp_primaries prim = mp_get_csp_primaries(MP_CSP_PRIM_DCI_P3);
+ float brightness = params->brightness;
+ mp_get_rgb2xyz_matrix(prim, m->m);
+ mp_invert_matrix3x3(m->m);
+
+ // All non-absolute mappings want to map source white to target white
+ if (intent != MP_INTENT_ABSOLUTE_COLORIMETRIC) {
+ // SMPTE EG 432-1 Annex H defines the white point as equal energy
+ static const struct mp_csp_col_xy smpte432 = {1.0/3.0, 1.0/3.0};
+ mp_apply_chromatic_adaptation(smpte432, prim.white, m->m);
+ }
+
+ // Since this outputs linear RGB rather than companded RGB, we
+ // want to linearize any brightness additions. 2 is a reasonable
+ // approximation for any sort of gamma function that could be in use.
+ // As this is an aesthetic setting only, any exact values do not matter.
+ brightness *= fabs(brightness);
+
+ for (int i = 0; i < 3; i++)
+ m->c[i] = brightness;
+}
+
+// Get multiplication factor required if image data is fit within the LSBs of a
+// higher smaller bit depth fixed-point texture data.
+// This is broken. Use mp_get_csp_uint_mul().
+double mp_get_csp_mul(enum mp_csp csp, int input_bits, int texture_bits)
+{
+ assert(texture_bits >= input_bits);
+
+ // Convenience for some irrelevant cases, e.g. rgb565 or disabling expansion.
+ if (!input_bits)
+ return 1;
+
+ // RGB always uses the full range available.
+ if (csp == MP_CSP_RGB)
+ return ((1LL << input_bits) - 1.) / ((1LL << texture_bits) - 1.);
+
+ if (csp == MP_CSP_XYZ)
+ return 1;
+
+ // High bit depth YUV uses a range shifted from 8 bit.
+ return (1LL << input_bits) / ((1LL << texture_bits) - 1.) * 255 / 256;
+}
+
+// Return information about color fixed point representation.his is needed for
+// converting color from integer formats to or from float. Use as follows:
+// float_val = uint_val * m + o
+// uint_val = clamp(round((float_val - o) / m))
+// See H.264/5 Annex E.
+// csp: colorspace
+// levels: full range flag
+// component: ID of the channel, as in mp_regular_imgfmt:
+// 1 is red/luminance/gray, 2 is green/Cb, 3 is blue/Cr, 4 is alpha.
+// bits: number of significant bits, e.g. 10 for yuv420p10, 16 for p010
+// out_m: returns factor to multiply the uint number with
+// out_o: returns offset to add after multiplication
+void mp_get_csp_uint_mul(enum mp_csp csp, enum mp_csp_levels levels,
+ int bits, int component, double *out_m, double *out_o)
+{
+ uint16_t i_min = 0;
+ uint16_t i_max = (1u << bits) - 1;
+ double f_min = 0; // min. float value
+
+ if (csp != MP_CSP_RGB && component != 4) {
+ if (component == 2 || component == 3) {
+ f_min = (1u << (bits - 1)) / -(double)i_max; // force center => 0
+
+ if (levels != MP_CSP_LEVELS_PC && bits >= 8) {
+ i_min = 16 << (bits - 8); // => -0.5
+ i_max = 240 << (bits - 8); // => 0.5
+ f_min = -0.5;
+ }
+ } else {
+ if (levels != MP_CSP_LEVELS_PC && bits >= 8) {
+ i_min = 16 << (bits - 8); // => 0
+ i_max = 235 << (bits - 8); // => 1
+ }
+ }
+ }
+
+ *out_m = 1.0 / (i_max - i_min);
+ *out_o = (1 + f_min) - i_max * *out_m;
+}
+
+/* Fill in the Y, U, V vectors of a yuv-to-rgb conversion matrix
+ * based on the given luma weights of the R, G and B components (lr, lg, lb).
+ * lr+lg+lb is assumed to equal 1.
+ * This function is meant for colorspaces satisfying the following
+ * conditions (which are true for common YUV colorspaces):
+ * - The mapping from input [Y, U, V] to output [R, G, B] is linear.
+ * - Y is the vector [1, 1, 1]. (meaning input Y component maps to 1R+1G+1B)
+ * - U maps to a value with zero R and positive B ([0, x, y], y > 0;
+ * i.e. blue and green only).
+ * - V maps to a value with zero B and positive R ([x, y, 0], x > 0;
+ * i.e. red and green only).
+ * - U and V are orthogonal to the luma vector [lr, lg, lb].
+ * - The magnitudes of the vectors U and V are the minimal ones for which
+ * the image of the set Y=[0...1],U=[-0.5...0.5],V=[-0.5...0.5] under the
+ * conversion function will cover the set R=[0...1],G=[0...1],B=[0...1]
+ * (the resulting matrix can be converted for other input/output ranges
+ * outside this function).
+ * Under these conditions the given parameters lr, lg, lb uniquely
+ * determine the mapping of Y, U, V to R, G, B.
+ */
+static void luma_coeffs(struct mp_cmat *mat, float lr, float lg, float lb)
+{
+ assert(fabs(lr+lg+lb - 1) < 1e-6);
+ *mat = (struct mp_cmat) {
+ { {1, 0, 2 * (1-lr) },
+ {1, -2 * (1-lb) * lb/lg, -2 * (1-lr) * lr/lg },
+ {1, 2 * (1-lb), 0 } },
+ // Constant coefficients (mat->c) not set here
+ };
+}
+
+// get the coefficients of the yuv -> rgb conversion matrix
+void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
+{
+ enum mp_csp colorspace = params->color.space;
+ if (colorspace <= MP_CSP_AUTO || colorspace >= MP_CSP_COUNT)
+ colorspace = MP_CSP_BT_601;
+ enum mp_csp_levels levels_in = params->color.levels;
+ if (levels_in <= MP_CSP_LEVELS_AUTO || levels_in >= MP_CSP_LEVELS_COUNT)
+ levels_in = MP_CSP_LEVELS_TV;
+
+ switch (colorspace) {
+ case MP_CSP_BT_601: luma_coeffs(m, 0.299, 0.587, 0.114 ); break;
+ case MP_CSP_BT_709: luma_coeffs(m, 0.2126, 0.7152, 0.0722); break;
+ case MP_CSP_SMPTE_240M: luma_coeffs(m, 0.2122, 0.7013, 0.0865); break;
+ case MP_CSP_BT_2020_NC: luma_coeffs(m, 0.2627, 0.6780, 0.0593); break;
+ case MP_CSP_BT_2020_C: {
+ // Note: This outputs into the [-0.5,0.5] range for chroma information.
+ // If this clips on any VO, a constant 0.5 coefficient can be added
+ // to the chroma channels to normalize them into [0,1]. This is not
+ // currently needed by anything, though.
+ *m = (struct mp_cmat){{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}};
+ break;
+ }
+ case MP_CSP_RGB: {
+ *m = (struct mp_cmat){{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
+ levels_in = -1;
+ break;
+ }
+ case MP_CSP_XYZ: {
+ // The vo should probably not be using a matrix generated by this
+ // function for XYZ sources, but if it does, let's just convert it to
+ // an equivalent RGB space based on the colorimetry metadata it
+ // provided in mp_csp_params. (At the risk of clipping, if the
+ // chosen primaries are too small to fit the actual data)
+ mp_get_xyz2rgb_coeffs(params, MP_INTENT_RELATIVE_COLORIMETRIC, m);
+ levels_in = -1;
+ break;
+ }
+ case MP_CSP_YCGCO: {
+ *m = (struct mp_cmat) {
+ {{1, -1, 1},
+ {1, 1, 0},
+ {1, -1, -1}},
+ };
+ break;
+ }
+ default:
+ MP_ASSERT_UNREACHABLE();
+ };
+
+ if (params->is_float)
+ levels_in = -1;
+
+ if ((colorspace == MP_CSP_BT_601 || colorspace == MP_CSP_BT_709 ||
+ colorspace == MP_CSP_SMPTE_240M || colorspace == MP_CSP_BT_2020_NC))
+ {
+ // Hue is equivalent to rotating input [U, V] subvector around the origin.
+ // Saturation scales [U, V].
+ float huecos = params->gray ? 0 : params->saturation * cos(params->hue);
+ float huesin = params->gray ? 0 : params->saturation * sin(params->hue);
+ for (int i = 0; i < 3; i++) {
+ float u = m->m[i][1], v = m->m[i][2];
+ m->m[i][1] = huecos * u - huesin * v;
+ m->m[i][2] = huesin * u + huecos * v;
+ }
+ }
+
+ // The values below are written in 0-255 scale - thus bring s into range.
+ double s =
+ mp_get_csp_mul(colorspace, params->input_bits, params->texture_bits) / 255;
+ // NOTE: The yuvfull ranges as presented here are arguably ambiguous,
+ // and conflict with at least the full-range YCbCr/ICtCp values as defined
+ // by ITU-R BT.2100. If somebody ever complains about full-range YUV looking
+ // different from their reference display, this comment is probably why.
+ struct yuvlevels { double ymin, ymax, cmax, cmid; }
+ yuvlim = { 16*s, 235*s, 240*s, 128*s },
+ yuvfull = { 0*s, 255*s, 255*s, 128*s },
+ anyfull = { 0*s, 255*s, 255*s/2, 0 }, // cmax picked to make cmul=ymul
+ yuvlev;
+ switch (levels_in) {
+ case MP_CSP_LEVELS_TV: yuvlev = yuvlim; break;
+ case MP_CSP_LEVELS_PC: yuvlev = yuvfull; break;
+ case -1: yuvlev = anyfull; break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ int levels_out = params->levels_out;
+ if (levels_out <= MP_CSP_LEVELS_AUTO || levels_out >= MP_CSP_LEVELS_COUNT)
+ levels_out = MP_CSP_LEVELS_PC;
+ struct rgblevels { double min, max; }
+ rgblim = { 16/255., 235/255. },
+ rgbfull = { 0, 1 },
+ rgblev;
+ switch (levels_out) {
+ case MP_CSP_LEVELS_TV: rgblev = rgblim; break;
+ case MP_CSP_LEVELS_PC: rgblev = rgbfull; break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ double ymul = (rgblev.max - rgblev.min) / (yuvlev.ymax - yuvlev.ymin);
+ double cmul = (rgblev.max - rgblev.min) / (yuvlev.cmax - yuvlev.cmid) / 2;
+
+ // Contrast scales the output value range (gain)
+ ymul *= params->contrast;
+ cmul *= params->contrast;
+
+ for (int i = 0; i < 3; i++) {
+ m->m[i][0] *= ymul;
+ m->m[i][1] *= cmul;
+ m->m[i][2] *= cmul;
+ // Set c so that Y=umin,UV=cmid maps to RGB=min (black to black),
+ // also add brightness offset (black lift)
+ m->c[i] = rgblev.min - m->m[i][0] * yuvlev.ymin
+ - (m->m[i][1] + m->m[i][2]) * yuvlev.cmid
+ + params->brightness;
+ }
+}
+
+// Set colorspace related fields in p from f. Don't touch other fields.
+void mp_csp_set_image_params(struct mp_csp_params *params,
+ const struct mp_image_params *imgparams)
+{
+ struct mp_image_params p = *imgparams;
+ mp_image_params_guess_csp(&p); // ensure consistency
+ params->color = p.color;
+}
+
+bool mp_colorspace_equal(struct mp_colorspace c1, struct mp_colorspace c2)
+{
+ return c1.space == c2.space &&
+ c1.levels == c2.levels &&
+ c1.primaries == c2.primaries &&
+ c1.gamma == c2.gamma &&
+ c1.light == c2.light &&
+ pl_hdr_metadata_equal(&c1.hdr, &c2.hdr);
+}
+
+enum mp_csp_equalizer_param {
+ MP_CSP_EQ_BRIGHTNESS,
+ MP_CSP_EQ_CONTRAST,
+ MP_CSP_EQ_HUE,
+ MP_CSP_EQ_SATURATION,
+ MP_CSP_EQ_GAMMA,
+ MP_CSP_EQ_COUNT,
+};
+
+// Default initialization with 0 is enough, except for the capabilities field
+struct mp_csp_equalizer_opts {
+ // Value for each property is in the range [-100.0, 100.0].
+ // 0.0 is default, meaning neutral or no change.
+ float values[MP_CSP_EQ_COUNT];
+ int output_levels;
+};
+
+#define OPT_BASE_STRUCT struct mp_csp_equalizer_opts
+
+const struct m_sub_options mp_csp_equalizer_conf = {
+ .opts = (const m_option_t[]) {
+ {"brightness", OPT_FLOAT(values[MP_CSP_EQ_BRIGHTNESS]),
+ M_RANGE(-100, 100)},
+ {"saturation", OPT_FLOAT(values[MP_CSP_EQ_SATURATION]),
+ M_RANGE(-100, 100)},
+ {"contrast", OPT_FLOAT(values[MP_CSP_EQ_CONTRAST]),
+ M_RANGE(-100, 100)},
+ {"hue", OPT_FLOAT(values[MP_CSP_EQ_HUE]),
+ M_RANGE(-100, 100)},
+ {"gamma", OPT_FLOAT(values[MP_CSP_EQ_GAMMA]),
+ M_RANGE(-100, 100)},
+ {"video-output-levels",
+ OPT_CHOICE_C(output_levels, mp_csp_levels_names)},
+ {0}
+ },
+ .size = sizeof(struct mp_csp_equalizer_opts),
+};
+
+// Copy settings from eq into params.
+static void mp_csp_copy_equalizer_values(struct mp_csp_params *params,
+ const struct mp_csp_equalizer_opts *eq)
+{
+ params->brightness = eq->values[MP_CSP_EQ_BRIGHTNESS] / 100.0;
+ params->contrast = (eq->values[MP_CSP_EQ_CONTRAST] + 100) / 100.0;
+ params->hue = eq->values[MP_CSP_EQ_HUE] / 100.0 * M_PI;
+ params->saturation = (eq->values[MP_CSP_EQ_SATURATION] + 100) / 100.0;
+ params->gamma = exp(log(8.0) * eq->values[MP_CSP_EQ_GAMMA] / 100.0);
+ params->levels_out = eq->output_levels;
+}
+
+struct mp_csp_equalizer_state *mp_csp_equalizer_create(void *ta_parent,
+ struct mpv_global *global)
+{
+ struct m_config_cache *c = m_config_cache_alloc(ta_parent, global,
+ &mp_csp_equalizer_conf);
+ // The terrible, terrible truth.
+ return (struct mp_csp_equalizer_state *)c;
+}
+
+bool mp_csp_equalizer_state_changed(struct mp_csp_equalizer_state *state)
+{
+ struct m_config_cache *c = (struct m_config_cache *)state;
+ return m_config_cache_update(c);
+}
+
+void mp_csp_equalizer_state_get(struct mp_csp_equalizer_state *state,
+ struct mp_csp_params *params)
+{
+ struct m_config_cache *c = (struct m_config_cache *)state;
+ m_config_cache_update(c);
+ struct mp_csp_equalizer_opts *opts = c->opts;
+ mp_csp_copy_equalizer_values(params, opts);
+}
+
+void mp_invert_cmat(struct mp_cmat *out, struct mp_cmat *in)
+{
+ *out = *in;
+ mp_invert_matrix3x3(out->m);
+
+ // fix the constant coefficient
+ // rgb = M * yuv + C
+ // M^-1 * rgb = yuv + M^-1 * C
+ // yuv = M^-1 * rgb - M^-1 * C
+ // ^^^^^^^^^^
+ out->c[0] = -(out->m[0][0] * in->c[0] + out->m[0][1] * in->c[1] + out->m[0][2] * in->c[2]);
+ out->c[1] = -(out->m[1][0] * in->c[0] + out->m[1][1] * in->c[1] + out->m[1][2] * in->c[2]);
+ out->c[2] = -(out->m[2][0] * in->c[0] + out->m[2][1] * in->c[1] + out->m[2][2] * in->c[2]);
+}
+
+// Multiply the color in c with the given matrix.
+// i/o is {R, G, B} or {Y, U, V} (depending on input/output and matrix), using
+// a fixed point representation with the given number of bits (so for bits==8,
+// [0,255] maps to [0,1]). The output is clipped to the range as needed.
+void mp_map_fixp_color(struct mp_cmat *matrix, int ibits, int in[3],
+ int obits, int out[3])
+{
+ for (int i = 0; i < 3; i++) {
+ double val = matrix->c[i];
+ for (int x = 0; x < 3; x++)
+ val += matrix->m[i][x] * in[x] / ((1 << ibits) - 1);
+ int ival = lrint(val * ((1 << obits) - 1));
+ out[i] = av_clip(ival, 0, (1 << obits) - 1);
+ }
+}
diff --git a/video/csputils.h b/video/csputils.h
new file mode 100644
index 0000000..3a904cb
--- /dev/null
+++ b/video/csputils.h
@@ -0,0 +1,290 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_CSPUTILS_H
+#define MPLAYER_CSPUTILS_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <libplacebo/colorspace.h>
+
+#include "options/m_option.h"
+
+/* NOTE: the csp and levels AUTO values are converted to specific ones
+ * above vf/vo level. At least vf_scale relies on all valid settings being
+ * nonzero at vf/vo level.
+ */
+
+enum mp_csp {
+ MP_CSP_AUTO,
+ MP_CSP_BT_601,
+ MP_CSP_BT_709,
+ MP_CSP_SMPTE_240M,
+ MP_CSP_BT_2020_NC,
+ MP_CSP_BT_2020_C,
+ MP_CSP_RGB,
+ MP_CSP_XYZ,
+ MP_CSP_YCGCO,
+ MP_CSP_COUNT
+};
+
+extern const struct m_opt_choice_alternatives mp_csp_names[];
+
+enum mp_csp_levels {
+ MP_CSP_LEVELS_AUTO,
+ MP_CSP_LEVELS_TV,
+ MP_CSP_LEVELS_PC,
+ MP_CSP_LEVELS_COUNT,
+};
+
+extern const struct m_opt_choice_alternatives mp_csp_levels_names[];
+
+enum mp_csp_prim {
+ MP_CSP_PRIM_AUTO,
+ MP_CSP_PRIM_BT_601_525,
+ MP_CSP_PRIM_BT_601_625,
+ MP_CSP_PRIM_BT_709,
+ MP_CSP_PRIM_BT_2020,
+ MP_CSP_PRIM_BT_470M,
+ MP_CSP_PRIM_APPLE,
+ MP_CSP_PRIM_ADOBE,
+ MP_CSP_PRIM_PRO_PHOTO,
+ MP_CSP_PRIM_CIE_1931,
+ MP_CSP_PRIM_DCI_P3,
+ MP_CSP_PRIM_DISPLAY_P3,
+ MP_CSP_PRIM_V_GAMUT,
+ MP_CSP_PRIM_S_GAMUT,
+ MP_CSP_PRIM_EBU_3213,
+ MP_CSP_PRIM_FILM_C,
+ MP_CSP_PRIM_ACES_AP0,
+ MP_CSP_PRIM_ACES_AP1,
+ MP_CSP_PRIM_COUNT
+};
+
+extern const struct m_opt_choice_alternatives mp_csp_prim_names[];
+
+enum mp_csp_trc {
+ MP_CSP_TRC_AUTO,
+ MP_CSP_TRC_BT_1886,
+ MP_CSP_TRC_SRGB,
+ MP_CSP_TRC_LINEAR,
+ MP_CSP_TRC_GAMMA18,
+ MP_CSP_TRC_GAMMA20,
+ MP_CSP_TRC_GAMMA22,
+ MP_CSP_TRC_GAMMA24,
+ MP_CSP_TRC_GAMMA26,
+ MP_CSP_TRC_GAMMA28,
+ MP_CSP_TRC_PRO_PHOTO,
+ MP_CSP_TRC_PQ,
+ MP_CSP_TRC_HLG,
+ MP_CSP_TRC_V_LOG,
+ MP_CSP_TRC_S_LOG1,
+ MP_CSP_TRC_S_LOG2,
+ MP_CSP_TRC_ST428,
+ MP_CSP_TRC_COUNT
+};
+
+extern const struct m_opt_choice_alternatives mp_csp_trc_names[];
+
+enum mp_csp_light {
+ MP_CSP_LIGHT_AUTO,
+ MP_CSP_LIGHT_DISPLAY,
+ MP_CSP_LIGHT_SCENE_HLG,
+ MP_CSP_LIGHT_SCENE_709_1886,
+ MP_CSP_LIGHT_SCENE_1_2,
+ MP_CSP_LIGHT_COUNT
+};
+
+extern const struct m_opt_choice_alternatives mp_csp_light_names[];
+
+// These constants are based on the ICC specification (Table 23) and match
+// up with the API of LittleCMS, which treats them as integers.
+enum mp_render_intent {
+ MP_INTENT_PERCEPTUAL = 0,
+ MP_INTENT_RELATIVE_COLORIMETRIC = 1,
+ MP_INTENT_SATURATION = 2,
+ MP_INTENT_ABSOLUTE_COLORIMETRIC = 3
+};
+
+// The numeric values (except -1) match the Matroska StereoMode element value.
+enum mp_stereo3d_mode {
+ MP_STEREO3D_INVALID = -1,
+ /* only modes explicitly referenced in the code are listed */
+ MP_STEREO3D_MONO = 0,
+ MP_STEREO3D_SBS2L = 1,
+ MP_STEREO3D_AB2R = 2,
+ MP_STEREO3D_AB2L = 3,
+ MP_STEREO3D_SBS2R = 11,
+ /* no explicit enum entries for most valid values */
+ MP_STEREO3D_COUNT = 15, // 14 is last valid mode
+};
+
+extern const struct m_opt_choice_alternatives mp_stereo3d_names[];
+
+#define MP_STEREO3D_NAME(x) m_opt_choice_str(mp_stereo3d_names, x)
+
+#define MP_STEREO3D_NAME_DEF(x, def) \
+ (MP_STEREO3D_NAME(x) ? MP_STEREO3D_NAME(x) : (def))
+
+struct mp_colorspace {
+ enum mp_csp space;
+ enum mp_csp_levels levels;
+ enum mp_csp_prim primaries;
+ enum mp_csp_trc gamma;
+ enum mp_csp_light light;
+ struct pl_hdr_metadata hdr;
+};
+
+// For many colorspace conversions, in particular those involving HDR, an
+// implicit reference white level is needed. Since this magic constant shows up
+// a lot, give it an explicit name. The value of 203 cd/m² comes from ITU-R
+// Report BT.2408, and the value for HLG comes from the cited HLG 75% level
+// (transferred to scene space).
+#define MP_REF_WHITE 203.0
+#define MP_REF_WHITE_HLG 3.17955
+
+// Replaces unknown values in the first struct by those of the second struct
+void mp_colorspace_merge(struct mp_colorspace *orig, struct mp_colorspace *new);
+
+struct mp_csp_params {
+ struct mp_colorspace color; // input colorspace
+ enum mp_csp_levels levels_out; // output device
+ float brightness;
+ float contrast;
+ float hue;
+ float saturation;
+ float gamma;
+ // discard U/V components
+ bool gray;
+ // input is already centered and range-expanded
+ bool is_float;
+ // texture_bits/input_bits is for rescaling fixed point input to range [0,1]
+ int texture_bits;
+ int input_bits;
+};
+
+#define MP_CSP_PARAMS_DEFAULTS { \
+ .color = { .space = MP_CSP_BT_601, \
+ .levels = MP_CSP_LEVELS_TV }, \
+ .levels_out = MP_CSP_LEVELS_PC, \
+ .brightness = 0, .contrast = 1, .hue = 0, .saturation = 1, \
+ .gamma = 1, .texture_bits = 8, .input_bits = 8}
+
+struct mp_image_params;
+void mp_csp_set_image_params(struct mp_csp_params *params,
+ const struct mp_image_params *imgparams);
+
+bool mp_colorspace_equal(struct mp_colorspace c1, struct mp_colorspace c2);
+
+enum mp_chroma_location {
+ MP_CHROMA_AUTO,
+ MP_CHROMA_TOPLEFT, // uhd
+ MP_CHROMA_LEFT, // mpeg2/4, h264
+ MP_CHROMA_CENTER, // mpeg1, jpeg
+ MP_CHROMA_COUNT,
+};
+
+extern const struct m_opt_choice_alternatives mp_chroma_names[];
+
+enum mp_alpha_type {
+ MP_ALPHA_AUTO,
+ MP_ALPHA_STRAIGHT,
+ MP_ALPHA_PREMUL,
+};
+
+extern const struct m_opt_choice_alternatives mp_alpha_names[];
+
+extern const struct m_sub_options mp_csp_equalizer_conf;
+
+struct mpv_global;
+struct mp_csp_equalizer_state *mp_csp_equalizer_create(void *ta_parent,
+ struct mpv_global *global);
+bool mp_csp_equalizer_state_changed(struct mp_csp_equalizer_state *state);
+void mp_csp_equalizer_state_get(struct mp_csp_equalizer_state *state,
+ struct mp_csp_params *params);
+
+struct mp_csp_col_xy {
+ float x, y;
+};
+
+static inline float mp_xy_X(struct mp_csp_col_xy xy) {
+ return xy.x / xy.y;
+}
+
+static inline float mp_xy_Z(struct mp_csp_col_xy xy) {
+ return (1 - xy.x - xy.y) / xy.y;
+}
+
+struct mp_csp_primaries {
+ struct mp_csp_col_xy red, green, blue, white;
+};
+
+enum mp_csp avcol_spc_to_mp_csp(int avcolorspace);
+enum mp_csp_levels avcol_range_to_mp_csp_levels(int avrange);
+enum mp_csp_prim avcol_pri_to_mp_csp_prim(int avpri);
+enum mp_csp_trc avcol_trc_to_mp_csp_trc(int avtrc);
+
+int mp_csp_to_avcol_spc(enum mp_csp colorspace);
+int mp_csp_levels_to_avcol_range(enum mp_csp_levels range);
+int mp_csp_prim_to_avcol_pri(enum mp_csp_prim prim);
+int mp_csp_trc_to_avcol_trc(enum mp_csp_trc trc);
+
+enum mp_csp mp_csp_guess_colorspace(int width, int height);
+enum mp_csp_prim mp_csp_guess_primaries(int width, int height);
+
+enum mp_chroma_location avchroma_location_to_mp(int avloc);
+int mp_chroma_location_to_av(enum mp_chroma_location mploc);
+void mp_get_chroma_location(enum mp_chroma_location loc, int *x, int *y);
+
+struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim csp);
+float mp_trc_nom_peak(enum mp_csp_trc trc);
+bool mp_trc_is_hdr(enum mp_csp_trc trc);
+
+/* Color conversion matrix: RGB = m * YUV + c
+ * m is in row-major matrix, with m[row][col], e.g.:
+ * [ a11 a12 a13 ] float m[3][3] = { { a11, a12, a13 },
+ * [ a21 a22 a23 ] { a21, a22, a23 },
+ * [ a31 a32 a33 ] { a31, a32, a33 } };
+ * This is accessed as e.g.: m[2-1][1-1] = a21
+ * In particular, each row contains all the coefficients for one of R, G, B,
+ * while each column contains all the coefficients for one of Y, U, V:
+ * m[r,g,b][y,u,v] = ...
+ * The matrix could also be viewed as group of 3 vectors, e.g. the 1st column
+ * is the Y vector (1, 1, 1), the 2nd is the U vector, the 3rd the V vector.
+ * The matrix might also be used for other conversions and colorspaces.
+ */
+struct mp_cmat {
+ float m[3][3];
+ float c[3];
+};
+
+void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3]);
+void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest,
+ enum mp_render_intent intent, float cms_matrix[3][3]);
+
+double mp_get_csp_mul(enum mp_csp csp, int input_bits, int texture_bits);
+void mp_get_csp_uint_mul(enum mp_csp csp, enum mp_csp_levels levels,
+ int bits, int component, double *out_m, double *out_o);
+void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *out);
+
+void mp_invert_matrix3x3(float m[3][3]);
+void mp_invert_cmat(struct mp_cmat *out, struct mp_cmat *in);
+void mp_map_fixp_color(struct mp_cmat *matrix, int ibits, int in[3],
+ int obits, int out[3]);
+
+#endif /* MPLAYER_CSPUTILS_H */
diff --git a/video/cuda.c b/video/cuda.c
new file mode 100644
index 0000000..3b7a2d8
--- /dev/null
+++ b/video/cuda.c
@@ -0,0 +1,44 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "hwdec.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#include <libavutil/hwcontext.h>
+
+static struct AVBufferRef *cuda_create_standalone(struct mpv_global *global,
+ struct mp_log *log, struct hwcontext_create_dev_params *params)
+{
+ struct cuda_opts *opts = mp_get_config_group(NULL, global, &cuda_conf);
+
+ char *decode_dev = NULL;
+ if (opts->cuda_device != -1)
+ decode_dev = talloc_asprintf(NULL, "%d", opts->cuda_device);
+
+ AVBufferRef* ref = NULL;
+ av_hwdevice_ctx_create(&ref, AV_HWDEVICE_TYPE_CUDA, decode_dev, NULL, 0);
+
+ talloc_free(decode_dev);
+ talloc_free(opts);
+ return ref;
+}
+
+const struct hwcontext_fns hwcontext_fns_cuda = {
+ .av_hwdevice_type = AV_HWDEVICE_TYPE_CUDA,
+ .create_dev = cuda_create_standalone,
+};
diff --git a/video/d3d.c b/video/d3d.c
new file mode 100644
index 0000000..ceddcf3
--- /dev/null
+++ b/video/d3d.c
@@ -0,0 +1,273 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <libavcodec/avcodec.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_d3d11va.h>
+
+#if HAVE_D3D9_HWACCEL
+#include <libavutil/hwcontext_dxva2.h>
+#endif
+
+#include "common/av_common.h"
+#include "common/common.h"
+#include "osdep/threads.h"
+#include "osdep/windows_utils.h"
+#include "video/fmt-conversion.h"
+#include "video/hwdec.h"
+#include "video/mp_image_pool.h"
+#include "video/mp_image.h"
+
+#include "d3d.h"
+
+HMODULE d3d11_dll, d3d9_dll, dxva2_dll;
+PFN_D3D11_CREATE_DEVICE d3d11_D3D11CreateDevice;
+
+static mp_once d3d_load_once = MP_STATIC_ONCE_INITIALIZER;
+
+#if !HAVE_UWP
+static void d3d_do_load(void)
+{
+ d3d11_dll = LoadLibrary(L"d3d11.dll");
+ d3d9_dll = LoadLibrary(L"d3d9.dll");
+ dxva2_dll = LoadLibrary(L"dxva2.dll");
+
+ if (d3d11_dll) {
+ d3d11_D3D11CreateDevice =
+ (void *)GetProcAddress(d3d11_dll, "D3D11CreateDevice");
+ }
+}
+#else
+static void d3d_do_load(void)
+{
+
+ d3d11_D3D11CreateDevice = D3D11CreateDevice;
+}
+#endif
+
+void d3d_load_dlls(void)
+{
+ mp_exec_once(&d3d_load_once, d3d_do_load);
+}
+
+// Test if Direct3D11 can be used by us. Basically, this prevents trying to use
+// D3D11 on Win7, and then failing somewhere in the process.
+bool d3d11_check_decoding(ID3D11Device *dev)
+{
+ HRESULT hr;
+ // We assume that NV12 is always supported, if hw decoding is supported at
+ // all.
+ UINT supported = 0;
+ hr = ID3D11Device_CheckFormatSupport(dev, DXGI_FORMAT_NV12, &supported);
+ return !FAILED(hr) && (supported & D3D11_BIND_DECODER);
+}
+
+static void d3d11_refine_hwframes(AVBufferRef *hw_frames_ctx)
+{
+ AVHWFramesContext *fctx = (void *)hw_frames_ctx->data;
+
+ if (fctx->format == AV_PIX_FMT_D3D11) {
+ AVD3D11VAFramesContext *hwctx = fctx->hwctx;
+
+ // According to hwcontex_d3d11va.h, yuv420p means DXGI_FORMAT_420_OPAQUE,
+ // which has no shader support.
+ if (fctx->sw_format != AV_PIX_FMT_YUV420P)
+ hwctx->BindFlags |= D3D11_BIND_SHADER_RESOURCE;
+ }
+}
+
+AVBufferRef *d3d11_wrap_device_ref(ID3D11Device *device)
+{
+ AVBufferRef *device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA);
+ if (!device_ref)
+ return NULL;
+
+ AVHWDeviceContext *ctx = (void *)device_ref->data;
+ AVD3D11VADeviceContext *hwctx = ctx->hwctx;
+
+ ID3D11Device_AddRef(device);
+ hwctx->device = device;
+
+ if (av_hwdevice_ctx_init(device_ref) < 0)
+ av_buffer_unref(&device_ref);
+
+ return device_ref;
+}
+
+static struct AVBufferRef *d3d11_create_standalone(struct mpv_global *global,
+ struct mp_log *plog, struct hwcontext_create_dev_params *params)
+{
+ ID3D11Device *device = NULL;
+ HRESULT hr;
+
+ d3d_load_dlls();
+ if (!d3d11_D3D11CreateDevice) {
+ mp_err(plog, "Failed to load D3D11 library\n");
+ return NULL;
+ }
+
+ hr = d3d11_D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL,
+ D3D11_CREATE_DEVICE_VIDEO_SUPPORT, NULL, 0,
+ D3D11_SDK_VERSION, &device, NULL, NULL);
+ if (FAILED(hr)) {
+ mp_err(plog, "Failed to create D3D11 Device: %s\n",
+ mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+
+ AVBufferRef *avref = d3d11_wrap_device_ref(device);
+ ID3D11Device_Release(device);
+ if (!avref)
+ mp_err(plog, "Failed to allocate AVHWDeviceContext.\n");
+
+ return avref;
+}
+
+const struct hwcontext_fns hwcontext_fns_d3d11 = {
+ .av_hwdevice_type = AV_HWDEVICE_TYPE_D3D11VA,
+ .refine_hwframes = d3d11_refine_hwframes,
+ .create_dev = d3d11_create_standalone,
+};
+
+#if HAVE_D3D9_HWACCEL
+
+#define DXVA2API_USE_BITFIELDS
+#include <libavutil/common.h>
+
+#include <libavutil/hwcontext_dxva2.h>
+
+static void d3d9_free_av_device_ref(AVHWDeviceContext *ctx)
+{
+ AVDXVA2DeviceContext *hwctx = ctx->hwctx;
+
+ if (hwctx->devmgr)
+ IDirect3DDeviceManager9_Release(hwctx->devmgr);
+}
+
+AVBufferRef *d3d9_wrap_device_ref(IDirect3DDevice9 *device)
+{
+ HRESULT hr;
+
+ d3d_load_dlls();
+ if (!dxva2_dll)
+ return NULL;
+
+ HRESULT (WINAPI *DXVA2CreateDirect3DDeviceManager9)(UINT *, IDirect3DDeviceManager9 **) =
+ (void *)GetProcAddress(dxva2_dll, "DXVA2CreateDirect3DDeviceManager9");
+ if (!DXVA2CreateDirect3DDeviceManager9)
+ return NULL;
+
+ AVBufferRef *device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_DXVA2);
+ if (!device_ref)
+ return NULL;
+
+ AVHWDeviceContext *ctx = (void *)device_ref->data;
+ AVDXVA2DeviceContext *hwctx = ctx->hwctx;
+
+ UINT reset_token = 0;
+ hr = DXVA2CreateDirect3DDeviceManager9(&reset_token, &hwctx->devmgr);
+ if (FAILED(hr))
+ goto fail;
+
+ hr = IDirect3DDeviceManager9_ResetDevice(hwctx->devmgr, device, reset_token);
+ if (FAILED(hr))
+ goto fail;
+
+ ctx->free = d3d9_free_av_device_ref;
+
+ if (av_hwdevice_ctx_init(device_ref) < 0)
+ goto fail;
+
+ return device_ref;
+
+fail:
+ d3d9_free_av_device_ref(ctx);
+ av_buffer_unref(&device_ref);
+ return NULL;
+}
+
+static struct AVBufferRef *d3d9_create_standalone(struct mpv_global *global,
+ struct mp_log *plog, struct hwcontext_create_dev_params *params)
+{
+ d3d_load_dlls();
+ if (!d3d9_dll || !dxva2_dll) {
+ mp_err(plog, "Failed to load D3D9 library\n");
+ return NULL;
+ }
+
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT, IDirect3D9Ex **) =
+ (void *)GetProcAddress(d3d9_dll, "Direct3DCreate9Ex");
+ if (!Direct3DCreate9Ex) {
+ mp_err(plog, "Failed to locate Direct3DCreate9Ex\n");
+ return NULL;
+ }
+
+ IDirect3D9Ex *d3d9ex = NULL;
+ HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
+ if (FAILED(hr)) {
+ mp_err(plog, "Failed to create IDirect3D9Ex object\n");
+ return NULL;
+ }
+
+ UINT adapter = D3DADAPTER_DEFAULT;
+ D3DDISPLAYMODEEX modeex = {0};
+ IDirect3D9Ex_GetAdapterDisplayModeEx(d3d9ex, adapter, &modeex, NULL);
+
+ D3DPRESENT_PARAMETERS present_params = {
+ .Windowed = TRUE,
+ .BackBufferWidth = 640,
+ .BackBufferHeight = 480,
+ .BackBufferCount = 0,
+ .BackBufferFormat = modeex.Format,
+ .SwapEffect = D3DSWAPEFFECT_DISCARD,
+ .Flags = D3DPRESENTFLAG_VIDEO,
+ };
+
+ IDirect3DDevice9Ex *exdev = NULL;
+ hr = IDirect3D9Ex_CreateDeviceEx(d3d9ex, adapter,
+ D3DDEVTYPE_HAL,
+ GetShellWindow(),
+ D3DCREATE_SOFTWARE_VERTEXPROCESSING |
+ D3DCREATE_MULTITHREADED |
+ D3DCREATE_FPU_PRESERVE,
+ &present_params,
+ NULL,
+ &exdev);
+ IDirect3D9_Release(d3d9ex);
+ if (FAILED(hr)) {
+ mp_err(plog, "Failed to create Direct3D device: %s\n",
+ mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+
+ AVBufferRef *avref = d3d9_wrap_device_ref((IDirect3DDevice9 *)exdev);
+ IDirect3DDevice9Ex_Release(exdev);
+ if (!avref)
+ mp_err(plog, "Failed to allocate AVHWDeviceContext.\n");
+
+ return avref;
+}
+
+const struct hwcontext_fns hwcontext_fns_dxva2 = {
+ .av_hwdevice_type = AV_HWDEVICE_TYPE_DXVA2,
+ .create_dev = d3d9_create_standalone,
+};
+
+#endif /* HAVE_D3D9_HWACCEL */
diff --git a/video/d3d.h b/video/d3d.h
new file mode 100644
index 0000000..0058905
--- /dev/null
+++ b/video/d3d.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_DECODE_D3D_H
+#define MPV_DECODE_D3D_H
+
+#include <windows.h>
+#include <d3d11.h>
+
+#include <stdbool.h>
+#include <inttypes.h>
+
+// Must call d3d_load_dlls() before accessing. Once this is done, the DLLs
+// remain loaded forever.
+extern HMODULE d3d11_dll, d3d9_dll, dxva2_dll;
+extern PFN_D3D11_CREATE_DEVICE d3d11_D3D11CreateDevice;
+
+void d3d_load_dlls(void);
+
+bool d3d11_check_decoding(ID3D11Device *dev);
+
+struct AVBufferRef;
+struct IDirect3DDevice9;
+
+struct AVBufferRef *d3d11_wrap_device_ref(ID3D11Device *device);
+struct AVBufferRef *d3d9_wrap_device_ref(struct IDirect3DDevice9 *device);
+
+#endif
diff --git a/video/decode/vd_lavc.c b/video/decode/vd_lavc.c
new file mode 100644
index 0000000..b971d26
--- /dev/null
+++ b/video/decode/vd_lavc.c
@@ -0,0 +1,1457 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <stdbool.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavformat/version.h>
+#include <libavutil/common.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/opt.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/pixdesc.h>
+
+#include "mpv_talloc.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "osdep/threads.h"
+#include "misc/bstr.h"
+#include "common/av_common.h"
+#include "common/codecs.h"
+
+#include "video/fmt-conversion.h"
+
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+#include "video/hwdec.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+#include "demux/demux.h"
+#include "demux/stheader.h"
+#include "demux/packet.h"
+#include "video/csputils.h"
+#include "video/sws_utils.h"
+#include "video/out/vo.h"
+
+#include "options/m_option.h"
+
+static void init_avctx(struct mp_filter *vd);
+static void uninit_avctx(struct mp_filter *vd);
+
+static int get_buffer2_direct(AVCodecContext *avctx, AVFrame *pic, int flags);
+static enum AVPixelFormat get_format_hwdec(struct AVCodecContext *avctx,
+ const enum AVPixelFormat *pix_fmt);
+static int hwdec_opt_help(struct mp_log *log, const m_option_t *opt,
+ struct bstr name);
+
+#define HWDEC_DELAY_QUEUE_COUNT 2
+
+#define OPT_BASE_STRUCT struct vd_lavc_params
+
+struct vd_lavc_params {
+ bool fast;
+ int film_grain;
+ bool show_all;
+ int skip_loop_filter;
+ int skip_idct;
+ int skip_frame;
+ int framedrop;
+ int threads;
+ bool bitexact;
+ bool old_x264;
+ bool apply_cropping;
+ bool check_hw_profile;
+ int software_fallback;
+ char **avopts;
+ int dr;
+ char **hwdec_api;
+ char *hwdec_codecs;
+ int hwdec_image_format;
+ int hwdec_extra_frames;
+};
+
+static const struct m_opt_choice_alternatives discard_names[] = {
+ {"none", AVDISCARD_NONE},
+ {"default", AVDISCARD_DEFAULT},
+ {"nonref", AVDISCARD_NONREF},
+ {"bidir", AVDISCARD_BIDIR},
+ {"nonkey", AVDISCARD_NONKEY},
+ {"all", AVDISCARD_ALL},
+ {0}
+};
+#define OPT_DISCARD(field) OPT_CHOICE_C(field, discard_names)
+
+const struct m_sub_options vd_lavc_conf = {
+ .opts = (const m_option_t[]){
+ {"vd-lavc-fast", OPT_BOOL(fast)},
+ {"vd-lavc-film-grain", OPT_CHOICE(film_grain,
+ {"auto", -1}, {"cpu", 0}, {"gpu", 1})},
+ {"vd-lavc-show-all", OPT_BOOL(show_all)},
+ {"vd-lavc-skiploopfilter", OPT_DISCARD(skip_loop_filter)},
+ {"vd-lavc-skipidct", OPT_DISCARD(skip_idct)},
+ {"vd-lavc-skipframe", OPT_DISCARD(skip_frame)},
+ {"vd-lavc-framedrop", OPT_DISCARD(framedrop)},
+ {"vd-lavc-threads", OPT_INT(threads), M_RANGE(0, DBL_MAX)},
+ {"vd-lavc-bitexact", OPT_BOOL(bitexact)},
+ {"vd-lavc-assume-old-x264", OPT_BOOL(old_x264)},
+ {"vd-lavc-check-hw-profile", OPT_BOOL(check_hw_profile)},
+ {"vd-lavc-software-fallback", OPT_CHOICE(software_fallback,
+ {"no", INT_MAX}, {"yes", 1}), M_RANGE(1, INT_MAX)},
+ {"vd-lavc-o", OPT_KEYVALUELIST(avopts)},
+ {"vd-lavc-dr", OPT_CHOICE(dr,
+ {"auto", -1}, {"no", 0}, {"yes", 1})},
+ {"vd-apply-cropping", OPT_BOOL(apply_cropping)},
+ {"hwdec", OPT_STRINGLIST(hwdec_api),
+ .help = hwdec_opt_help,
+ .flags = M_OPT_OPTIONAL_PARAM | UPDATE_HWDEC},
+ {"hwdec-codecs", OPT_STRING(hwdec_codecs)},
+ {"hwdec-image-format", OPT_IMAGEFORMAT(hwdec_image_format)},
+ {"hwdec-extra-frames", OPT_INT(hwdec_extra_frames), M_RANGE(0, 256)},
+ {0}
+ },
+ .size = sizeof(struct vd_lavc_params),
+ .defaults = &(const struct vd_lavc_params){
+ .film_grain = -1 /*auto*/,
+ .check_hw_profile = true,
+ .software_fallback = 3,
+ .skip_loop_filter = AVDISCARD_DEFAULT,
+ .skip_idct = AVDISCARD_DEFAULT,
+ .skip_frame = AVDISCARD_DEFAULT,
+ .framedrop = AVDISCARD_NONREF,
+ .dr = -1,
+ .hwdec_api = (char *[]){"no", NULL,},
+ .hwdec_codecs = "h264,vc1,hevc,vp8,vp9,av1,prores",
+ // Maximum number of surfaces the player wants to buffer. This number
+ // might require adjustment depending on whatever the player does;
+ // for example, if vo_gpu increases the number of reference surfaces for
+ // interpolation, this value has to be increased too.
+ .hwdec_extra_frames = 6,
+ .apply_cropping = true,
+ },
+};
+
+struct hwdec_info {
+ char name[64];
+ char method_name[24]; // non-unique name describing the hwdec method
+ const AVCodec *codec; // implemented by this codec
+ enum AVHWDeviceType lavc_device; // if not NONE, get a hwdevice
+ bool copying; // if true, outputs sw frames, or copy to sw ourselves
+ enum AVPixelFormat pix_fmt; // if not NONE, select in get_format
+ bool use_hw_frames; // set AVCodecContext.hw_frames_ctx
+ bool use_hw_device; // set AVCodecContext.hw_device_ctx
+ unsigned int flags; // HWDEC_FLAG_*
+
+ // for internal sorting
+ int auto_pos;
+ int rank;
+};
+
+typedef struct lavc_ctx {
+ struct mp_log *log;
+ struct m_config_cache *opts_cache;
+ struct vd_lavc_params *opts;
+ struct mp_codec_params *codec;
+ AVCodecContext *avctx;
+ AVFrame *pic;
+ AVPacket *avpkt;
+ bool use_hwdec;
+ struct hwdec_info hwdec; // valid only if use_hwdec==true
+ bstr *attempted_hwdecs;
+ int num_attempted_hwdecs;
+ AVRational codec_timebase;
+ enum AVDiscard skip_frame;
+ bool flushing;
+ struct lavc_state state;
+ const char *decoder;
+ bool hwdec_failed;
+ bool hwdec_notified;
+ bool force_eof;
+
+ bool intra_only;
+ int framedrop_flags;
+
+ bool hw_probing;
+ struct demux_packet **sent_packets;
+ int num_sent_packets;
+
+ struct demux_packet **requeue_packets;
+ int num_requeue_packets;
+
+ struct mp_image **delay_queue;
+ int num_delay_queue;
+ int max_delay_queue;
+
+ // From VO
+ struct vo *vo;
+ struct mp_hwdec_devices *hwdec_devs;
+
+ // Wrapped AVHWDeviceContext* used for decoding.
+ AVBufferRef *hwdec_dev;
+
+ bool hwdec_request_reinit;
+ int hwdec_fail_count;
+
+ struct mp_image_pool *hwdec_swpool;
+
+ AVBufferRef *cached_hw_frames_ctx;
+
+ // --- The following fields are protected by dr_lock.
+ mp_mutex dr_lock;
+ bool dr_failed;
+ struct mp_image_pool *dr_pool;
+ int dr_imgfmt, dr_w, dr_h, dr_stride_align;
+
+ struct mp_decoder public;
+} vd_ffmpeg_ctx;
+
+enum {
+ HWDEC_FLAG_AUTO = (1 << 0), // prioritize in autoprobe order
+ HWDEC_FLAG_WHITELIST = (1 << 1), // whitelist for auto-safe
+};
+
+struct autoprobe_info {
+ const char *method_name;
+ unsigned int flags; // HWDEC_FLAG_*
+};
+
+// Things not included in this list will be tried last, in random order.
+const struct autoprobe_info hwdec_autoprobe_info[] = {
+ {"d3d11va", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"dxva2", HWDEC_FLAG_AUTO},
+ {"d3d11va-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"dxva2-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"nvdec", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"nvdec-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"vaapi", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"vaapi-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"vdpau", HWDEC_FLAG_AUTO},
+ {"vdpau-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"drm", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"drm-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"mmal", HWDEC_FLAG_AUTO},
+ {"mmal-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"mediacodec", HWDEC_FLAG_AUTO},
+ {"mediacodec-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"videotoolbox", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {"videotoolbox-copy", HWDEC_FLAG_AUTO | HWDEC_FLAG_WHITELIST},
+ {0}
+};
+
+static int hwdec_compare(const void *p1, const void *p2)
+{
+ struct hwdec_info *h1 = (void *)p1;
+ struct hwdec_info *h2 = (void *)p2;
+
+ if (h1 == h2)
+ return 0;
+
+ // Strictly put non-preferred hwdecs to the end of the list.
+ if ((h1->auto_pos == INT_MAX) != (h2->auto_pos == INT_MAX))
+ return h1->auto_pos == INT_MAX ? 1 : -1;
+ // List non-copying entries first, so --hwdec=auto takes them.
+ if (h1->copying != h2->copying)
+ return h1->copying ? 1 : -1;
+ // Order by autoprobe preference order.
+ if (h1->auto_pos != h2->auto_pos)
+ return h1->auto_pos > h2->auto_pos ? 1 : -1;
+ // Put hwdecs without hw_device_ctx last
+ if ((!!h1->lavc_device) != (!!h2->lavc_device))
+ return h1->lavc_device ? -1 : 1;
+ // Fallback sort order to make sorting stable.
+ return h1->rank > h2->rank ? 1 :-1;
+}
+
+// (This takes care of some bookkeeping too, like setting info.name)
+static void add_hwdec_item(struct hwdec_info **infos, int *num_infos,
+ struct hwdec_info info)
+{
+ if (info.copying)
+ mp_snprintf_cat(info.method_name, sizeof(info.method_name), "-copy");
+
+ // (Including the codec name in case this is a wrapper looks pretty dumb,
+ // but better not have them clash with hwaccels and others.)
+ snprintf(info.name, sizeof(info.name), "%s-%s",
+ info.codec->name, info.method_name);
+
+ info.rank = *num_infos;
+ info.auto_pos = INT_MAX;
+
+ for (int x = 0; hwdec_autoprobe_info[x].method_name; x++) {
+ const struct autoprobe_info *entry = &hwdec_autoprobe_info[x];
+ if (strcmp(entry->method_name, info.method_name) == 0) {
+ info.flags |= entry->flags;
+ if (info.flags & HWDEC_FLAG_AUTO)
+ info.auto_pos = x;
+ }
+ }
+
+ MP_TARRAY_APPEND(NULL, *infos, *num_infos, info);
+}
+
+static void add_all_hwdec_methods(struct hwdec_info **infos, int *num_infos)
+{
+ const AVCodec *codec = NULL;
+ void *iter = NULL;
+ while (1) {
+ codec = av_codec_iterate(&iter);
+ if (!codec)
+ break;
+ if (codec->type != AVMEDIA_TYPE_VIDEO || !av_codec_is_decoder(codec))
+ continue;
+
+ struct hwdec_info info_template = {
+ .pix_fmt = AV_PIX_FMT_NONE,
+ .codec = codec,
+ };
+
+ const char *wrapper = NULL;
+ if (codec->capabilities & (AV_CODEC_CAP_HARDWARE | AV_CODEC_CAP_HYBRID))
+ wrapper = codec->wrapper_name;
+
+ // A decoder can provide multiple methods. In particular, hwaccels
+ // provide various methods (e.g. native h264 with vaapi & d3d11), but
+ // even wrapper decoders could provide multiple methods.
+ bool found_any = false;
+ for (int n = 0; ; n++) {
+ const AVCodecHWConfig *cfg = avcodec_get_hw_config(codec, n);
+ if (!cfg)
+ break;
+
+ if ((cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX) ||
+ (cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX))
+ {
+ struct hwdec_info info = info_template;
+ info.lavc_device = cfg->device_type;
+ info.pix_fmt = cfg->pix_fmt;
+
+ const char *name = av_hwdevice_get_type_name(cfg->device_type);
+ assert(name); // API violation by libavcodec
+
+ // nvdec hwaccels and the cuvid full decoder clash with their
+ // naming, so fix it here; we also prefer nvdec for the hwaccel.
+ if (strcmp(name, "cuda") == 0 && !wrapper)
+ name = "nvdec";
+
+ snprintf(info.method_name, sizeof(info.method_name), "%s", name);
+
+ // Usually we want to prefer using hw_frames_ctx for true
+ // hwaccels only, but we actually don't have any way to detect
+ // those, so always use hw_frames_ctx if offered.
+ if (cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX) {
+ info.use_hw_frames = true;
+ } else {
+ info.use_hw_device = true;
+ }
+
+ // Direct variant.
+ add_hwdec_item(infos, num_infos, info);
+
+ // Copy variant.
+ info.copying = true;
+ if (cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) {
+ info.use_hw_frames = false;
+ info.use_hw_device = true;
+ }
+ add_hwdec_item(infos, num_infos, info);
+
+ found_any = true;
+ } else if (cfg->methods & AV_CODEC_HW_CONFIG_METHOD_INTERNAL) {
+ struct hwdec_info info = info_template;
+ info.pix_fmt = cfg->pix_fmt;
+
+ const char *name = wrapper;
+ if (!name)
+ name = av_get_pix_fmt_name(info.pix_fmt);
+ assert(name); // API violation by libavcodec
+
+ snprintf(info.method_name, sizeof(info.method_name), "%s", name);
+
+ // Direct variant.
+ add_hwdec_item(infos, num_infos, info);
+
+ // Copy variant.
+ info.copying = true;
+ info.pix_fmt = AV_PIX_FMT_NONE; // trust it can do sw output
+ add_hwdec_item(infos, num_infos, info);
+
+ found_any = true;
+ }
+ }
+
+ if (!found_any && wrapper) {
+ // We _know_ there's something supported here, usually outputting
+ // sw surfaces. E.g. mediacodec (before hw_device_ctx support).
+
+ struct hwdec_info info = info_template;
+ info.copying = true; // probably
+
+ snprintf(info.method_name, sizeof(info.method_name), "%s", wrapper);
+ add_hwdec_item(infos, num_infos, info);
+ }
+ }
+
+ qsort(*infos, *num_infos, sizeof(struct hwdec_info), hwdec_compare);
+}
+
+static bool hwdec_codec_allowed(struct mp_filter *vd, const char *codec)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ bstr s = bstr0(ctx->opts->hwdec_codecs);
+ while (s.len) {
+ bstr item;
+ bstr_split_tok(s, ",", &item, &s);
+ if (bstr_equals0(item, "all") || bstr_equals0(item, codec))
+ return true;
+ }
+ return false;
+}
+
+static AVBufferRef *hwdec_create_dev(struct mp_filter *vd,
+ struct hwdec_info *hwdec,
+ bool autoprobe)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ assert(hwdec->lavc_device);
+
+ if (hwdec->copying) {
+ const struct hwcontext_fns *fns =
+ hwdec_get_hwcontext_fns(hwdec->lavc_device);
+ if (fns && fns->create_dev) {
+ struct hwcontext_create_dev_params params = {
+ .probing = autoprobe,
+ };
+ return fns->create_dev(vd->global, vd->log, &params);
+ } else {
+ AVBufferRef* ref = NULL;
+ av_hwdevice_ctx_create(&ref, hwdec->lavc_device, NULL, NULL, 0);
+ return ref;
+ }
+ } else if (ctx->hwdec_devs) {
+ int imgfmt = pixfmt2imgfmt(hwdec->pix_fmt);
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = imgfmt,
+ .probing = autoprobe,
+ };
+ hwdec_devices_request_for_img_fmt(ctx->hwdec_devs, &params);
+
+ const struct mp_hwdec_ctx *hw_ctx =
+ hwdec_devices_get_by_imgfmt(ctx->hwdec_devs, imgfmt);
+
+ if (hw_ctx && hw_ctx->av_device_ref)
+ return av_buffer_ref(hw_ctx->av_device_ref);
+ }
+
+ return NULL;
+}
+
+// Select if and which hwdec to use. Also makes sure to get the decode device.
+static void select_and_set_hwdec(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ const char *codec = ctx->codec->codec;
+
+ m_config_cache_update(ctx->opts_cache);
+
+ struct hwdec_info *hwdecs = NULL;
+ int num_hwdecs = 0;
+ add_all_hwdec_methods(&hwdecs, &num_hwdecs);
+
+ char **hwdec_api = ctx->opts->hwdec_api;
+ for (int i = 0; hwdec_api[i]; i++) {
+ bstr opt = bstr0(hwdec_api[i]);
+
+ bool hwdec_requested = !bstr_equals0(opt, "no");
+ bool hwdec_auto_all = bstr_equals0(opt, "auto") ||
+ bstr_equals0(opt, "");
+ bool hwdec_auto_safe = bstr_equals0(opt, "auto-safe") ||
+ bstr_equals0(opt, "auto-copy-safe") ||
+ bstr_equals0(opt, "yes");
+ bool hwdec_auto_copy = bstr_equals0(opt, "auto-copy") ||
+ bstr_equals0(opt, "auto-copy-safe");
+ bool hwdec_auto = hwdec_auto_all || hwdec_auto_copy || hwdec_auto_safe;
+
+ if (!hwdec_requested) {
+ MP_VERBOSE(vd, "No hardware decoding requested.\n");
+ break;
+ } else if (!hwdec_codec_allowed(vd, codec)) {
+ MP_VERBOSE(vd, "Not trying to use hardware decoding: codec %s is not "
+ "on whitelist.\n", codec);
+ break;
+ } else {
+ bool hwdec_name_supported = false; // relevant only if !hwdec_auto
+ for (int n = 0; n < num_hwdecs; n++) {
+ struct hwdec_info *hwdec = &hwdecs[n];
+
+ if (!hwdec_auto && !(bstr_equals0(opt, hwdec->method_name) ||
+ bstr_equals0(opt, hwdec->name)))
+ continue;
+ hwdec_name_supported = true;
+
+ bool already_attempted = false;
+ for (int j = 0; j < ctx->num_attempted_hwdecs; j++) {
+ if (bstr_equals0(ctx->attempted_hwdecs[j], hwdec->name)) {
+ MP_DBG(vd, "Skipping previously attempted hwdec: %s\n",
+ hwdec->name);
+ already_attempted = true;
+ break;
+ }
+ }
+ if (already_attempted)
+ continue;
+
+ const char *hw_codec = mp_codec_from_av_codec_id(hwdec->codec->id);
+ if (!hw_codec || strcmp(hw_codec, codec) != 0)
+ continue;
+
+ if (hwdec_auto_safe && !(hwdec->flags & HWDEC_FLAG_WHITELIST))
+ continue;
+
+ MP_VERBOSE(vd, "Looking at hwdec %s...\n", hwdec->name);
+
+ /*
+ * Past this point, any kind of failure that results in us
+ * looking for a new hwdec should not lead to use trying this
+ * hwdec again - so add it to the list, regardless of whether
+ * initialisation will succeed or not.
+ */
+ MP_TARRAY_APPEND(ctx, ctx->attempted_hwdecs,
+ ctx->num_attempted_hwdecs,
+ bstrdup(ctx, bstr0(hwdec->name)));
+
+ if (hwdec_auto_copy && !hwdec->copying) {
+ MP_VERBOSE(vd, "Not using this for auto-copy.\n");
+ continue;
+ }
+
+ if (hwdec->lavc_device) {
+ ctx->hwdec_dev = hwdec_create_dev(vd, hwdec, hwdec_auto);
+ if (!ctx->hwdec_dev) {
+ MP_VERBOSE(vd, "Could not create device.\n");
+ continue;
+ }
+
+ const struct hwcontext_fns *fns =
+ hwdec_get_hwcontext_fns(hwdec->lavc_device);
+ if (fns && fns->is_emulated && fns->is_emulated(ctx->hwdec_dev)) {
+ if (hwdec_auto) {
+ MP_VERBOSE(vd, "Not using emulated API.\n");
+ av_buffer_unref(&ctx->hwdec_dev);
+ continue;
+ }
+ MP_WARN(vd, "Using emulated hardware decoding API.\n");
+ }
+ } else if (!hwdec->copying) {
+ // Most likely METHOD_INTERNAL, which often use delay-loaded
+ // VO support as well.
+ if (ctx->hwdec_devs) {
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = pixfmt2imgfmt(hwdec->pix_fmt),
+ .probing = hwdec_auto,
+ };
+ hwdec_devices_request_for_img_fmt(
+ ctx->hwdec_devs, &params);
+ }
+ }
+
+ ctx->use_hwdec = true;
+ ctx->hwdec = *hwdec;
+ break;
+ }
+ if (ctx->use_hwdec)
+ break;
+ else if (!hwdec_auto && !hwdec_name_supported)
+ MP_WARN(vd, "Unsupported hwdec: %.*s\n", BSTR_P(opt));
+ }
+ }
+ talloc_free(hwdecs);
+
+
+ if (ctx->use_hwdec) {
+ MP_VERBOSE(vd, "Trying hardware decoding via %s.\n", ctx->hwdec.name);
+ if (strcmp(ctx->decoder, ctx->hwdec.codec->name) != 0)
+ MP_VERBOSE(vd, "Using underlying hw-decoder '%s'\n",
+ ctx->hwdec.codec->name);
+ } else {
+ // If software fallback is disabled and we get here, all hwdec must
+ // have failed. Tell the ctx to always force an eof.
+ if (ctx->opts->software_fallback == INT_MAX) {
+ MP_WARN(ctx, "Software decoding fallback is disabled.\n");
+ ctx->force_eof = true;
+ } else {
+ MP_VERBOSE(vd, "Using software decoding.\n");
+ }
+ }
+}
+
+static int hwdec_opt_help(struct mp_log *log, const m_option_t *opt,
+ struct bstr name)
+{
+ struct hwdec_info *hwdecs = NULL;
+ int num_hwdecs = 0;
+ add_all_hwdec_methods(&hwdecs, &num_hwdecs);
+
+ mp_info(log, "Valid values (with alternative full names):\n");
+
+ for (int n = 0; n < num_hwdecs; n++) {
+ struct hwdec_info *hwdec = &hwdecs[n];
+
+ mp_info(log, " %s (%s)\n", hwdec->method_name, hwdec->name);
+ }
+
+ talloc_free(hwdecs);
+
+ mp_info(log, " auto (yes '')\n");
+ mp_info(log, " no\n");
+ mp_info(log, " auto-safe\n");
+ mp_info(log, " auto-copy\n");
+ mp_info(log, " auto-copy-safe\n");
+
+ return M_OPT_EXIT;
+}
+
+static void force_fallback(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ uninit_avctx(vd);
+ int lev = ctx->hwdec_notified ? MSGL_WARN : MSGL_V;
+ mp_msg(vd->log, lev, "Attempting next decoding method after failure of %.*s.\n",
+ BSTR_P(ctx->attempted_hwdecs[ctx->num_attempted_hwdecs - 1]));
+ select_and_set_hwdec(vd);
+ init_avctx(vd);
+}
+
+static void reinit(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ uninit_avctx(vd);
+
+ /*
+ * Reset attempted hwdecs so that if the hwdec list is reconfigured
+ * we attempt all of them from the beginning. The most practical
+ * reason for this is that ctrl+h toggles between `no` and
+ * `auto-safe`, and we want to reevaluate from a clean slate each time.
+ */
+ TA_FREEP(&ctx->attempted_hwdecs);
+ ctx->num_attempted_hwdecs = 0;
+ ctx->hwdec_notified = false;
+
+ select_and_set_hwdec(vd);
+
+ bool use_hwdec = ctx->use_hwdec;
+ init_avctx(vd);
+ if (!ctx->avctx && use_hwdec) {
+ do {
+ force_fallback(vd);
+ } while (!ctx->avctx);
+ }
+}
+
+static void init_avctx(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ struct vd_lavc_params *lavc_param = ctx->opts;
+ struct mp_codec_params *c = ctx->codec;
+
+ m_config_cache_update(ctx->opts_cache);
+
+ assert(!ctx->avctx);
+
+ const AVCodec *lavc_codec = NULL;
+
+ if (ctx->use_hwdec) {
+ lavc_codec = ctx->hwdec.codec;
+ } else {
+ lavc_codec = avcodec_find_decoder_by_name(ctx->decoder);
+ }
+ if (!lavc_codec)
+ return;
+
+ const AVCodecDescriptor *desc = avcodec_descriptor_get(lavc_codec->id);
+ ctx->intra_only = desc && (desc->props & AV_CODEC_PROP_INTRA_ONLY);
+
+ ctx->codec_timebase = mp_get_codec_timebase(ctx->codec);
+
+ // This decoder does not read pkt_timebase correctly yet.
+ if (strstr(lavc_codec->name, "_mmal"))
+ ctx->codec_timebase = (AVRational){1, 1000000};
+
+ ctx->hwdec_failed = false;
+ ctx->hwdec_request_reinit = false;
+ ctx->avctx = avcodec_alloc_context3(lavc_codec);
+ AVCodecContext *avctx = ctx->avctx;
+ if (!ctx->avctx)
+ goto error;
+ avctx->codec_type = AVMEDIA_TYPE_VIDEO;
+ avctx->codec_id = lavc_codec->id;
+ avctx->pkt_timebase = ctx->codec_timebase;
+
+ ctx->pic = av_frame_alloc();
+ if (!ctx->pic)
+ goto error;
+
+ ctx->avpkt = av_packet_alloc();
+ if (!ctx->avpkt)
+ goto error;
+
+ if (ctx->use_hwdec) {
+ avctx->opaque = vd;
+ avctx->thread_count = 1;
+ avctx->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
+ if (!lavc_param->check_hw_profile)
+ avctx->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
+
+#ifdef AV_HWACCEL_FLAG_UNSAFE_OUTPUT
+ /*
+ * This flag primarily exists for nvdec which has a very limited
+ * output frame pool, which can get exhausted if consumers don't
+ * release frames quickly. However, as an implementation
+ * requirement, we have to copy the frames anyway, so we don't
+ * need this extra implicit copy.
+ */
+ avctx->hwaccel_flags |= AV_HWACCEL_FLAG_UNSAFE_OUTPUT;
+#endif
+
+ if (ctx->hwdec.use_hw_device) {
+ if (ctx->hwdec_dev)
+ avctx->hw_device_ctx = av_buffer_ref(ctx->hwdec_dev);
+ if (!avctx->hw_device_ctx)
+ goto error;
+ }
+ if (ctx->hwdec.use_hw_frames) {
+ if (!ctx->hwdec_dev)
+ goto error;
+ }
+
+ if (ctx->hwdec.pix_fmt != AV_PIX_FMT_NONE)
+ avctx->get_format = get_format_hwdec;
+
+ // Some APIs benefit from this, for others it's additional bloat.
+ if (ctx->hwdec.copying)
+ ctx->max_delay_queue = HWDEC_DELAY_QUEUE_COUNT;
+ ctx->hw_probing = true;
+ } else {
+ mp_set_avcodec_threads(vd->log, avctx, lavc_param->threads);
+ }
+
+ if (!ctx->use_hwdec && ctx->vo && lavc_param->dr) {
+ avctx->opaque = vd;
+ avctx->get_buffer2 = get_buffer2_direct;
+#if LIBAVCODEC_VERSION_MAJOR < 60
+ AV_NOWARN_DEPRECATED({
+ avctx->thread_safe_callbacks = 1;
+ });
+#endif
+ }
+
+ avctx->flags |= lavc_param->bitexact ? AV_CODEC_FLAG_BITEXACT : 0;
+ avctx->flags2 |= lavc_param->fast ? AV_CODEC_FLAG2_FAST : 0;
+
+ if (lavc_param->show_all)
+ avctx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
+
+ avctx->skip_loop_filter = lavc_param->skip_loop_filter;
+ avctx->skip_idct = lavc_param->skip_idct;
+ avctx->skip_frame = lavc_param->skip_frame;
+ avctx->apply_cropping = lavc_param->apply_cropping;
+
+ if (lavc_codec->id == AV_CODEC_ID_H264 && lavc_param->old_x264)
+ av_opt_set(avctx, "x264_build", "150", AV_OPT_SEARCH_CHILDREN);
+
+#ifndef AV_CODEC_EXPORT_DATA_FILM_GRAIN
+ if (ctx->opts->film_grain == 1)
+ MP_WARN(vd, "GPU film grain requested, but FFmpeg too old to expose "
+ "film grain parameters. Please update to latest master, "
+ "or at least to release 4.4.\n");
+#else
+ switch(ctx->opts->film_grain) {
+ case 0: /*CPU*/
+ // default lavc flags handle film grain within the decoder.
+ break;
+ case 1: /*GPU*/
+ if (!ctx->vo ||
+ (ctx->vo && !(ctx->vo->driver->caps & VO_CAP_FILM_GRAIN))) {
+ MP_MSG(vd, ctx->vo ? MSGL_WARN : MSGL_V,
+ "GPU film grain requested, but VO %s, expect wrong output.\n",
+ ctx->vo ?
+ "does not support applying film grain" :
+ "is not available at decoder initialization to verify support");
+ }
+
+ avctx->export_side_data |= AV_CODEC_EXPORT_DATA_FILM_GRAIN;
+ break;
+ default:
+ if (ctx->vo && (ctx->vo->driver->caps & VO_CAP_FILM_GRAIN))
+ avctx->export_side_data |= AV_CODEC_EXPORT_DATA_FILM_GRAIN;
+
+ break;
+ }
+#endif
+
+ mp_set_avopts(vd->log, avctx, lavc_param->avopts);
+
+ // Do this after the above avopt handling in case it changes values
+ ctx->skip_frame = avctx->skip_frame;
+
+ if (mp_set_avctx_codec_headers(avctx, c) < 0) {
+ MP_ERR(vd, "Could not set codec parameters.\n");
+ goto error;
+ }
+
+ /* open it */
+ if (avcodec_open2(avctx, lavc_codec, NULL) < 0)
+ goto error;
+
+ // Sometimes, the first packet contains information required for correct
+ // decoding of the rest of the stream. The only currently known case is the
+ // x264 build number (encoded in a SEI element), needed to enable a
+ // workaround for broken 4:4:4 streams produced by older x264 versions.
+ if (lavc_codec->id == AV_CODEC_ID_H264 && c->first_packet) {
+ mp_set_av_packet(ctx->avpkt, c->first_packet, &ctx->codec_timebase);
+ avcodec_send_packet(avctx, ctx->avpkt);
+ avcodec_receive_frame(avctx, ctx->pic);
+ av_frame_unref(ctx->pic);
+ avcodec_flush_buffers(ctx->avctx);
+ }
+
+ return;
+
+error:
+ MP_ERR(vd, "Could not open codec.\n");
+ uninit_avctx(vd);
+}
+
+static void reset_avctx(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ if (ctx->avctx && avcodec_is_open(ctx->avctx))
+ avcodec_flush_buffers(ctx->avctx);
+ ctx->flushing = false;
+ ctx->hwdec_request_reinit = false;
+}
+
+static void flush_all(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ for (int n = 0; n < ctx->num_delay_queue; n++)
+ talloc_free(ctx->delay_queue[n]);
+ ctx->num_delay_queue = 0;
+
+ for (int n = 0; n < ctx->num_sent_packets; n++)
+ talloc_free(ctx->sent_packets[n]);
+ ctx->num_sent_packets = 0;
+
+ for (int n = 0; n < ctx->num_requeue_packets; n++)
+ talloc_free(ctx->requeue_packets[n]);
+ ctx->num_requeue_packets = 0;
+
+ reset_avctx(vd);
+}
+
+static void uninit_avctx(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ flush_all(vd);
+ av_frame_free(&ctx->pic);
+ mp_free_av_packet(&ctx->avpkt);
+ av_buffer_unref(&ctx->cached_hw_frames_ctx);
+
+ avcodec_free_context(&ctx->avctx);
+
+ av_buffer_unref(&ctx->hwdec_dev);
+
+ ctx->hwdec_failed = false;
+ ctx->hwdec_fail_count = 0;
+ ctx->max_delay_queue = 0;
+ ctx->hw_probing = false;
+ ctx->hwdec = (struct hwdec_info){0};
+ ctx->use_hwdec = false;
+}
+
+static int init_generic_hwaccel(struct mp_filter *vd, enum AVPixelFormat hw_fmt)
+{
+ struct lavc_ctx *ctx = vd->priv;
+ AVBufferRef *new_frames_ctx = NULL;
+
+ if (!ctx->hwdec.use_hw_frames)
+ return 0;
+
+ if (!ctx->hwdec_dev) {
+ MP_ERR(ctx, "Missing device context.\n");
+ goto error;
+ }
+
+ if (avcodec_get_hw_frames_parameters(ctx->avctx,
+ ctx->hwdec_dev, hw_fmt, &new_frames_ctx) < 0)
+ {
+ MP_VERBOSE(ctx, "Hardware decoding of this stream is unsupported?\n");
+ goto error;
+ }
+
+ AVHWFramesContext *new_fctx = (void *)new_frames_ctx->data;
+
+ if (ctx->opts->hwdec_image_format)
+ new_fctx->sw_format = imgfmt2pixfmt(ctx->opts->hwdec_image_format);
+
+ // 1 surface is already included by libavcodec. The field is 0 if the
+ // hwaccel supports dynamic surface allocation.
+ if (new_fctx->initial_pool_size)
+ new_fctx->initial_pool_size += ctx->opts->hwdec_extra_frames - 1;
+
+ const struct hwcontext_fns *fns =
+ hwdec_get_hwcontext_fns(new_fctx->device_ctx->type);
+
+ if (fns && fns->refine_hwframes)
+ fns->refine_hwframes(new_frames_ctx);
+
+ // We might be able to reuse a previously allocated frame pool.
+ if (ctx->cached_hw_frames_ctx) {
+ AVHWFramesContext *old_fctx = (void *)ctx->cached_hw_frames_ctx->data;
+
+ if (new_fctx->format != old_fctx->format ||
+ new_fctx->sw_format != old_fctx->sw_format ||
+ new_fctx->width != old_fctx->width ||
+ new_fctx->height != old_fctx->height ||
+ new_fctx->initial_pool_size != old_fctx->initial_pool_size)
+ av_buffer_unref(&ctx->cached_hw_frames_ctx);
+ }
+
+ if (!ctx->cached_hw_frames_ctx) {
+ if (av_hwframe_ctx_init(new_frames_ctx) < 0) {
+ MP_ERR(ctx, "Failed to allocate hw frames.\n");
+ goto error;
+ }
+
+ ctx->cached_hw_frames_ctx = new_frames_ctx;
+ new_frames_ctx = NULL;
+ }
+
+ ctx->avctx->hw_frames_ctx = av_buffer_ref(ctx->cached_hw_frames_ctx);
+ if (!ctx->avctx->hw_frames_ctx)
+ goto error;
+
+ av_buffer_unref(&new_frames_ctx);
+ return 0;
+
+error:
+ av_buffer_unref(&new_frames_ctx);
+ av_buffer_unref(&ctx->cached_hw_frames_ctx);
+ return -1;
+}
+
+static enum AVPixelFormat get_format_hwdec(struct AVCodecContext *avctx,
+ const enum AVPixelFormat *fmt)
+{
+ struct mp_filter *vd = avctx->opaque;
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ MP_VERBOSE(vd, "Pixel formats supported by decoder:");
+ for (int i = 0; fmt[i] != AV_PIX_FMT_NONE; i++)
+ MP_VERBOSE(vd, " %s", av_get_pix_fmt_name(fmt[i]));
+ MP_VERBOSE(vd, "\n");
+
+ const char *profile = avcodec_profile_name(avctx->codec_id, avctx->profile);
+ MP_VERBOSE(vd, "Codec profile: %s (0x%x)\n", profile ? profile : "unknown",
+ avctx->profile);
+
+ assert(ctx->use_hwdec);
+
+ ctx->hwdec_request_reinit |= ctx->hwdec_failed;
+ ctx->hwdec_failed = false;
+
+ enum AVPixelFormat select = AV_PIX_FMT_NONE;
+ for (int i = 0; fmt[i] != AV_PIX_FMT_NONE; i++) {
+ if (ctx->hwdec.pix_fmt == fmt[i]) {
+ if (init_generic_hwaccel(vd, fmt[i]) < 0)
+ break;
+ select = fmt[i];
+ break;
+ }
+ }
+
+ if (select == AV_PIX_FMT_NONE) {
+ ctx->hwdec_failed = true;
+ select = avcodec_default_get_format(avctx, fmt);
+ }
+
+ const char *name = av_get_pix_fmt_name(select);
+ MP_VERBOSE(vd, "Requesting pixfmt '%s' from decoder.\n", name ? name : "-");
+ return select;
+}
+
+static int get_buffer2_direct(AVCodecContext *avctx, AVFrame *pic, int flags)
+{
+ struct mp_filter *vd = avctx->opaque;
+ vd_ffmpeg_ctx *p = vd->priv;
+
+ mp_mutex_lock(&p->dr_lock);
+
+ int w = pic->width;
+ int h = pic->height;
+ int linesize_align[AV_NUM_DATA_POINTERS] = {0};
+ avcodec_align_dimensions2(avctx, &w, &h, linesize_align);
+
+ // We assume that different alignments are just different power-of-2s.
+ // Thus, a higher alignment always satisfies a lower alignment.
+ int stride_align = MP_IMAGE_BYTE_ALIGN;
+ for (int n = 0; n < AV_NUM_DATA_POINTERS; n++)
+ stride_align = MPMAX(stride_align, linesize_align[n]);
+
+ // Note: texel sizes may be NPOT, so use full lcm instead of max
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pic->format);
+ if (!(desc->flags & AV_PIX_FMT_FLAG_BITSTREAM)) {
+ for (int n = 0; n < desc->nb_components; n++)
+ stride_align = mp_lcm(stride_align, desc->comp[n].step);
+ }
+
+ int imgfmt = pixfmt2imgfmt(pic->format);
+ if (!imgfmt)
+ goto fallback;
+
+ if (p->dr_failed)
+ goto fallback;
+
+ // (For simplicity, we realloc on any parameter change, instead of trying
+ // to be clever.)
+ if (stride_align != p->dr_stride_align || w != p->dr_w || h != p->dr_h ||
+ imgfmt != p->dr_imgfmt)
+ {
+ mp_image_pool_clear(p->dr_pool);
+ p->dr_imgfmt = imgfmt;
+ p->dr_w = w;
+ p->dr_h = h;
+ p->dr_stride_align = stride_align;
+ MP_DBG(p, "DR parameter change to %dx%d %s align=%d\n", w, h,
+ mp_imgfmt_to_name(imgfmt), stride_align);
+ }
+
+ struct mp_image *img = mp_image_pool_get_no_alloc(p->dr_pool, imgfmt, w, h);
+ if (!img) {
+ bool host_cached = p->opts->dr == -1; // auto
+ int dr_flags = host_cached ? VO_DR_FLAG_HOST_CACHED : 0;
+ MP_DBG(p, "Allocating new%s DR image...\n", host_cached ? " (host-cached)" : "");
+ img = vo_get_image(p->vo, imgfmt, w, h, stride_align, dr_flags);
+ if (!img) {
+ MP_DBG(p, "...failed..\n");
+ goto fallback;
+ }
+
+ // Now make the mp_image part of the pool. This requires doing magic to
+ // the image, so just add it to the pool and get it back to avoid
+ // dealing with magic ourselves. (Normally this never fails.)
+ mp_image_pool_add(p->dr_pool, img);
+ img = mp_image_pool_get_no_alloc(p->dr_pool, imgfmt, w, h);
+ if (!img)
+ goto fallback;
+ }
+
+ // get_buffer2 callers seem very unappreciative of overwriting pic with a
+ // new reference. The AVCodecContext.get_buffer2 comments tell us exactly
+ // what we should do, so follow that.
+ for (int n = 0; n < 4; n++) {
+ pic->data[n] = img->planes[n];
+ pic->linesize[n] = img->stride[n];
+ pic->buf[n] = img->bufs[n];
+ img->bufs[n] = NULL;
+ }
+ talloc_free(img);
+
+ mp_mutex_unlock(&p->dr_lock);
+
+ return 0;
+
+fallback:
+ if (!p->dr_failed)
+ MP_VERBOSE(p, "DR failed - disabling.\n");
+ p->dr_failed = true;
+ mp_mutex_unlock(&p->dr_lock);
+
+ return avcodec_default_get_buffer2(avctx, pic, flags);
+}
+
+static void prepare_decoding(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ AVCodecContext *avctx = ctx->avctx;
+ struct vd_lavc_params *opts = ctx->opts;
+
+ if (!avctx)
+ return;
+
+ int drop = ctx->framedrop_flags;
+ if (drop == 1) {
+ avctx->skip_frame = opts->framedrop; // normal framedrop
+ } else if (drop == 2) {
+ avctx->skip_frame = AVDISCARD_NONREF; // hr-seek framedrop
+ // Can be much more aggressive for true intra codecs.
+ if (ctx->intra_only)
+ avctx->skip_frame = AVDISCARD_ALL;
+ } else {
+ avctx->skip_frame = ctx->skip_frame; // normal playback
+ }
+
+ if (ctx->hwdec_request_reinit)
+ reset_avctx(vd);
+}
+
+static void handle_err(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ struct vd_lavc_params *opts = ctx->opts;
+
+ MP_WARN(vd, "Error while decoding frame%s!\n",
+ ctx->use_hwdec ? " (hardware decoding)" : "");
+
+ if (ctx->use_hwdec) {
+ ctx->hwdec_fail_count += 1;
+ if (ctx->hwdec_fail_count >= opts->software_fallback)
+ ctx->hwdec_failed = true;
+ }
+}
+
+static int send_packet(struct mp_filter *vd, struct demux_packet *pkt)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ AVCodecContext *avctx = ctx->avctx;
+
+ if (ctx->num_requeue_packets && ctx->requeue_packets[0] != pkt)
+ return AVERROR(EAGAIN); // cannot consume the packet
+
+ if (ctx->hwdec_failed)
+ return AVERROR(EAGAIN);
+
+ if (!ctx->avctx)
+ return AVERROR_EOF;
+
+ prepare_decoding(vd);
+
+ if (avctx->skip_frame == AVDISCARD_ALL)
+ return 0;
+
+ mp_set_av_packet(ctx->avpkt, pkt, &ctx->codec_timebase);
+
+ int ret = avcodec_send_packet(avctx, pkt ? ctx->avpkt : NULL);
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
+ return ret;
+
+ if (ctx->hw_probing && ctx->num_sent_packets < 32 &&
+ ctx->opts->software_fallback <= 32)
+ {
+ pkt = pkt ? demux_copy_packet(pkt) : NULL;
+ MP_TARRAY_APPEND(ctx, ctx->sent_packets, ctx->num_sent_packets, pkt);
+ }
+
+ if (ret < 0)
+ handle_err(vd);
+ return ret;
+}
+
+static void send_queued_packet(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ assert(ctx->num_requeue_packets);
+
+ if (send_packet(vd, ctx->requeue_packets[0]) != AVERROR(EAGAIN)) {
+ talloc_free(ctx->requeue_packets[0]);
+ MP_TARRAY_REMOVE_AT(ctx->requeue_packets, ctx->num_requeue_packets, 0);
+ }
+}
+
+// Returns whether decoder is still active (!EOF state).
+static int decode_frame(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ AVCodecContext *avctx = ctx->avctx;
+
+ if (!avctx || ctx->force_eof)
+ return AVERROR_EOF;
+
+ prepare_decoding(vd);
+
+ // Re-send old packets (typically after a hwdec fallback during init).
+ if (ctx->num_requeue_packets)
+ send_queued_packet(vd);
+
+ int ret = avcodec_receive_frame(avctx, ctx->pic);
+ if (ret < 0) {
+ if (ret == AVERROR_EOF) {
+ // If flushing was initialized earlier and has ended now, make it
+ // start over in case we get new packets at some point in the future.
+ // This must take the delay queue into account, so avctx returns EOF
+ // until the delay queue has been drained.
+ if (!ctx->num_delay_queue)
+ reset_avctx(vd);
+ } else if (ret == AVERROR(EAGAIN)) {
+ // just retry after caller writes a packet
+ } else {
+ handle_err(vd);
+ }
+ return ret;
+ }
+
+ // If something was decoded successfully, it must return a frame with valid
+ // data.
+ assert(ctx->pic->buf[0]);
+
+ struct mp_image *mpi = mp_image_from_av_frame(ctx->pic);
+ if (!mpi) {
+ av_frame_unref(ctx->pic);
+ return ret;
+ }
+
+ if (mpi->imgfmt == IMGFMT_CUDA && !mpi->planes[0]) {
+ MP_ERR(vd, "CUDA frame without data. This is a FFmpeg bug.\n");
+ talloc_free(mpi);
+ handle_err(vd);
+ return AVERROR_BUG;
+ }
+
+ ctx->hwdec_fail_count = 0;
+
+ mpi->pts = mp_pts_from_av(ctx->pic->pts, &ctx->codec_timebase);
+ mpi->dts = mp_pts_from_av(ctx->pic->pkt_dts, &ctx->codec_timebase);
+
+ mpi->pkt_duration =
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 30, 100)
+ mp_pts_from_av(ctx->pic->duration, &ctx->codec_timebase);
+#else
+ mp_pts_from_av(ctx->pic->pkt_duration, &ctx->codec_timebase);
+#endif
+
+ av_frame_unref(ctx->pic);
+
+ MP_TARRAY_APPEND(ctx, ctx->delay_queue, ctx->num_delay_queue, mpi);
+ return ret;
+}
+
+static int receive_frame(struct mp_filter *vd, struct mp_frame *out_frame)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ int ret = decode_frame(vd);
+
+ if (ctx->hwdec_failed) {
+ // Failed hardware decoding? Try the next one, and eventually software.
+ struct demux_packet **pkts = ctx->sent_packets;
+ int num_pkts = ctx->num_sent_packets;
+ ctx->sent_packets = NULL;
+ ctx->num_sent_packets = 0;
+
+ /*
+ * We repeatedly force_fallback until we get an avctx, because there are
+ * certain hwdecs that are really full decoders, and so if these fail,
+ * they also fail to give us a valid avctx, and the early return path
+ * here will simply give up on decoding completely if there is no
+ * decoder. We should never hit an infinite loop as the hwdec list is
+ * finite and we will eventually exhaust it and fall back to software
+ * decoding (and in practice, most hwdecs are hwaccels and so the
+ * decoder will successfully init even if the hwaccel fails later.)
+ */
+ do {
+ force_fallback(vd);
+ } while (!ctx->avctx);
+
+ ctx->requeue_packets = pkts;
+ ctx->num_requeue_packets = num_pkts;
+
+ return 0; // force retry
+ }
+
+ if (ret == AVERROR(EAGAIN) && ctx->num_requeue_packets)
+ return 0; // force retry, so send_queued_packet() gets called
+
+ if (ctx->num_delay_queue <= ctx->max_delay_queue && ret != AVERROR_EOF)
+ return ret;
+
+ if (!ctx->num_delay_queue)
+ return ret;
+
+ struct mp_image *res = ctx->delay_queue[0];
+ MP_TARRAY_REMOVE_AT(ctx->delay_queue, ctx->num_delay_queue, 0);
+
+ res = res ? mp_img_swap_to_native(res) : NULL;
+ if (!res)
+ return AVERROR_UNKNOWN;
+
+ if (ctx->use_hwdec && ctx->hwdec.copying && res->hwctx) {
+ struct mp_image *sw = mp_image_hw_download(res, ctx->hwdec_swpool);
+ mp_image_unrefp(&res);
+ res = sw;
+ if (!res) {
+ MP_ERR(vd, "Could not copy back hardware decoded frame.\n");
+ ctx->hwdec_fail_count = INT_MAX - 1; // force fallback
+ handle_err(vd);
+ return AVERROR_UNKNOWN;
+ }
+ }
+
+ if (!ctx->hwdec_notified) {
+ if (ctx->use_hwdec) {
+ MP_INFO(vd, "Using hardware decoding (%s).\n",
+ ctx->hwdec.method_name);
+ } else {
+ MP_VERBOSE(vd, "Using software decoding.\n");
+ }
+ ctx->hwdec_notified = true;
+ }
+
+ if (ctx->hw_probing) {
+ for (int n = 0; n < ctx->num_sent_packets; n++)
+ talloc_free(ctx->sent_packets[n]);
+ ctx->num_sent_packets = 0;
+ ctx->hw_probing = false;
+ }
+
+ *out_frame = MAKE_FRAME(MP_FRAME_VIDEO, res);
+ return 0;
+}
+
+static int control(struct mp_filter *vd, enum dec_ctrl cmd, void *arg)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ switch (cmd) {
+ case VDCTRL_SET_FRAMEDROP:
+ ctx->framedrop_flags = *(int *)arg;
+ return CONTROL_TRUE;
+ case VDCTRL_CHECK_FORCED_EOF: {
+ *(bool *)arg = ctx->force_eof;
+ return CONTROL_TRUE;
+ }
+ case VDCTRL_GET_BFRAMES: {
+ AVCodecContext *avctx = ctx->avctx;
+ if (!avctx)
+ break;
+ if (ctx->use_hwdec && strcmp(ctx->hwdec.method_name, "mmal") == 0)
+ break; // MMAL has arbitrary buffering, thus unknown
+ *(int *)arg = avctx->has_b_frames;
+ return CONTROL_TRUE;
+ }
+ case VDCTRL_GET_HWDEC: {
+ *(char **)arg = ctx->use_hwdec ? ctx->hwdec.method_name : NULL;
+ return CONTROL_TRUE;
+ }
+ case VDCTRL_FORCE_HWDEC_FALLBACK:
+ if (ctx->use_hwdec) {
+ force_fallback(vd);
+ return ctx->avctx ? CONTROL_OK : CONTROL_ERROR;
+ }
+ return CONTROL_FALSE;
+ case VDCTRL_REINIT:
+ reinit(vd);
+ return CONTROL_TRUE;
+ }
+ return CONTROL_UNKNOWN;
+}
+
+static void process(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ lavc_process(vd, &ctx->state, send_packet, receive_frame);
+}
+
+static void reset(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ flush_all(vd);
+
+ ctx->state = (struct lavc_state){0};
+ ctx->framedrop_flags = 0;
+}
+
+static void destroy(struct mp_filter *vd)
+{
+ vd_ffmpeg_ctx *ctx = vd->priv;
+
+ uninit_avctx(vd);
+
+ mp_mutex_destroy(&ctx->dr_lock);
+}
+
+static const struct mp_filter_info vd_lavc_filter = {
+ .name = "vd_lavc",
+ .priv_size = sizeof(vd_ffmpeg_ctx),
+ .process = process,
+ .reset = reset,
+ .destroy = destroy,
+};
+
+static struct mp_decoder *create(struct mp_filter *parent,
+ struct mp_codec_params *codec,
+ const char *decoder)
+{
+ struct mp_filter *vd = mp_filter_create(parent, &vd_lavc_filter);
+ if (!vd)
+ return NULL;
+
+ mp_filter_add_pin(vd, MP_PIN_IN, "in");
+ mp_filter_add_pin(vd, MP_PIN_OUT, "out");
+
+ vd->log = mp_log_new(vd, parent->log, NULL);
+
+ vd_ffmpeg_ctx *ctx = vd->priv;
+ ctx->log = vd->log;
+ ctx->opts_cache = m_config_cache_alloc(ctx, vd->global, &vd_lavc_conf);
+ ctx->opts = ctx->opts_cache->opts;
+ ctx->codec = codec;
+ ctx->decoder = talloc_strdup(ctx, decoder);
+ ctx->hwdec_swpool = mp_image_pool_new(ctx);
+ ctx->dr_pool = mp_image_pool_new(ctx);
+
+ ctx->public.f = vd;
+ ctx->public.control = control;
+
+ mp_mutex_init(&ctx->dr_lock);
+
+ // hwdec/DR
+ struct mp_stream_info *info = mp_filter_find_stream_info(vd);
+ if (info) {
+ ctx->hwdec_devs = info->hwdec_devs;
+ ctx->vo = info->dr_vo;
+ }
+
+ reinit(vd);
+
+ if (!ctx->avctx) {
+ talloc_free(vd);
+ return NULL;
+ }
+ return &ctx->public;
+}
+
+static void add_decoders(struct mp_decoder_list *list)
+{
+ mp_add_lavc_decoders(list, AVMEDIA_TYPE_VIDEO);
+}
+
+const struct mp_decoder_fns vd_lavc = {
+ .create = create,
+ .add_decoders = add_decoders,
+};
diff --git a/video/drmprime.c b/video/drmprime.c
new file mode 100644
index 0000000..64d793f
--- /dev/null
+++ b/video/drmprime.c
@@ -0,0 +1,43 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/hwcontext.h>
+
+#include "hwdec.h"
+#include "options/m_config.h"
+#include "video/out/drm_common.h"
+
+extern const struct m_sub_options drm_conf;
+static struct AVBufferRef *drm_create_standalone(struct mpv_global *global,
+ struct mp_log *log, struct hwcontext_create_dev_params *params)
+{
+ void *tmp = talloc_new(NULL);
+ struct drm_opts *drm_opts = mp_get_config_group(tmp, global, &drm_conf);
+ const char *opt_path = drm_opts->device_path;
+
+ const char *device_path = opt_path ? opt_path : "/dev/dri/renderD128";
+ AVBufferRef* ref = NULL;
+ av_hwdevice_ctx_create(&ref, AV_HWDEVICE_TYPE_DRM, device_path, NULL, 0);
+
+ talloc_free(tmp);
+ return ref;
+}
+
+const struct hwcontext_fns hwcontext_fns_drmprime = {
+ .av_hwdevice_type = AV_HWDEVICE_TYPE_DRM,
+ .create_dev = drm_create_standalone,
+};
diff --git a/video/filter/refqueue.c b/video/filter/refqueue.c
new file mode 100644
index 0000000..d018e38
--- /dev/null
+++ b/video/filter/refqueue.c
@@ -0,0 +1,356 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <libavutil/buffer.h>
+
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter_internal.h"
+#include "video/mp_image.h"
+
+#include "refqueue.h"
+
+struct mp_refqueue {
+ struct mp_filter *filter;
+ struct mp_autoconvert *conv;
+ struct mp_pin *in, *out;
+
+ struct mp_image *in_format;
+
+ // Buffered frame in case of format changes.
+ struct mp_image *next;
+
+ int needed_past_frames;
+ int needed_future_frames;
+ int flags;
+
+ bool second_field; // current frame has to output a second field yet
+ bool eof;
+
+ // Queue of input frames, used to determine past/current/future frames.
+ // queue[0] is the newest frame, queue[num_queue - 1] the oldest.
+ struct mp_image **queue;
+ int num_queue;
+ // queue[pos] is the current frame, unless pos is an invalid index.
+ int pos;
+};
+
+static bool mp_refqueue_has_output(struct mp_refqueue *q);
+
+static void refqueue_dtor(void *p)
+{
+ struct mp_refqueue *q = p;
+ mp_refqueue_flush(q);
+ mp_image_unrefp(&q->in_format);
+ talloc_free(q->conv->f);
+}
+
+struct mp_refqueue *mp_refqueue_alloc(struct mp_filter *f)
+{
+ struct mp_refqueue *q = talloc_zero(f, struct mp_refqueue);
+ talloc_set_destructor(q, refqueue_dtor);
+ q->filter = f;
+
+ q->conv = mp_autoconvert_create(f);
+ MP_HANDLE_OOM(q->conv);
+
+ q->in = q->conv->f->pins[1];
+ mp_pin_connect(q->conv->f->pins[0], f->ppins[0]);
+ q->out = f->ppins[1];
+
+ mp_refqueue_flush(q);
+ return q;
+}
+
+void mp_refqueue_add_in_format(struct mp_refqueue *q, int fmt, int subfmt)
+{
+ mp_autoconvert_add_imgfmt(q->conv, fmt, subfmt);
+}
+
+// The minimum number of frames required before and after the current frame.
+void mp_refqueue_set_refs(struct mp_refqueue *q, int past, int future)
+{
+ assert(past >= 0 && future >= 0);
+ q->needed_past_frames = past;
+ q->needed_future_frames = MPMAX(future, 1); // at least 1 for determining PTS
+}
+
+// MP_MODE_* flags
+void mp_refqueue_set_mode(struct mp_refqueue *q, int flags)
+{
+ q->flags = flags;
+}
+
+// Whether the current frame should be deinterlaced.
+bool mp_refqueue_should_deint(struct mp_refqueue *q)
+{
+ if (!mp_refqueue_has_output(q) || !(q->flags & MP_MODE_DEINT))
+ return false;
+
+ return (q->queue[q->pos]->fields & MP_IMGFIELD_INTERLACED) ||
+ !(q->flags & MP_MODE_INTERLACED_ONLY);
+}
+
+// Whether the current output frame (field) is the top field, bottom field
+// otherwise. (Assumes the caller forces deinterlacing.)
+bool mp_refqueue_is_top_field(struct mp_refqueue *q)
+{
+ if (!mp_refqueue_has_output(q))
+ return false;
+
+ return !!(q->queue[q->pos]->fields & MP_IMGFIELD_TOP_FIRST) ^ q->second_field;
+}
+
+// Whether top-field-first mode is enabled.
+bool mp_refqueue_top_field_first(struct mp_refqueue *q)
+{
+ if (!mp_refqueue_has_output(q))
+ return false;
+
+ return q->queue[q->pos]->fields & MP_IMGFIELD_TOP_FIRST;
+}
+
+// Discard all state.
+void mp_refqueue_flush(struct mp_refqueue *q)
+{
+ for (int n = 0; n < q->num_queue; n++)
+ talloc_free(q->queue[n]);
+ q->num_queue = 0;
+ q->pos = -1;
+ q->second_field = false;
+ q->eof = false;
+ mp_image_unrefp(&q->next);
+}
+
+static void mp_refqueue_add_input(struct mp_refqueue *q, struct mp_image *img)
+{
+ assert(img);
+
+ MP_TARRAY_INSERT_AT(q, q->queue, q->num_queue, 0, img);
+ q->pos++;
+
+ assert(q->pos >= 0 && q->pos < q->num_queue);
+}
+
+static bool mp_refqueue_need_input(struct mp_refqueue *q)
+{
+ return q->pos < q->needed_future_frames && !q->eof;
+}
+
+static bool mp_refqueue_has_output(struct mp_refqueue *q)
+{
+ return q->pos >= 0 && !mp_refqueue_need_input(q);
+}
+
+static bool output_next_field(struct mp_refqueue *q)
+{
+ if (q->second_field)
+ return false;
+ if (!(q->flags & MP_MODE_OUTPUT_FIELDS))
+ return false;
+ if (!mp_refqueue_should_deint(q))
+ return false;
+
+ assert(q->pos >= 0);
+
+ // If there's no (reasonable) timestamp, also skip the field.
+ if (q->pos == 0)
+ return false;
+
+ double pts = q->queue[q->pos]->pts;
+ double next_pts = q->queue[q->pos - 1]->pts;
+ if (pts == MP_NOPTS_VALUE || next_pts == MP_NOPTS_VALUE)
+ return false;
+
+ double frametime = next_pts - pts;
+ if (frametime <= 0.0 || frametime >= 1.0)
+ return false;
+
+ q->queue[q->pos]->pts = pts + frametime / 2;
+ q->second_field = true;
+ return true;
+}
+
+// Advance to next input frame (skips fields even in field output mode).
+static void mp_refqueue_next(struct mp_refqueue *q)
+{
+ if (!mp_refqueue_has_output(q))
+ return;
+
+ q->pos--;
+ q->second_field = false;
+
+ assert(q->pos >= -1 && q->pos < q->num_queue);
+
+ // Discard unneeded past frames.
+ while (q->num_queue - (q->pos + 1) > q->needed_past_frames) {
+ assert(q->num_queue > 0);
+ talloc_free(q->queue[q->num_queue - 1]);
+ q->num_queue--;
+ }
+
+ assert(q->pos >= -1 && q->pos < q->num_queue);
+}
+
+// Advance current field, depending on interlace flags.
+static void mp_refqueue_next_field(struct mp_refqueue *q)
+{
+ if (!mp_refqueue_has_output(q))
+ return;
+
+ if (!output_next_field(q))
+ mp_refqueue_next(q);
+}
+
+// Return a frame by relative position:
+// -1: first past frame
+// 0: current frame
+// 1: first future frame
+// Caller doesn't get ownership. Return NULL if unavailable.
+struct mp_image *mp_refqueue_get(struct mp_refqueue *q, int pos)
+{
+ int i = q->pos - pos;
+ return i >= 0 && i < q->num_queue ? q->queue[i] : NULL;
+}
+
+// Same as mp_refqueue_get(), but return the frame which contains a field
+// relative to the current field's position.
+struct mp_image *mp_refqueue_get_field(struct mp_refqueue *q, int pos)
+{
+ // If the current field is the second field (conceptually), then pos=1
+ // needs to get the next frame. Similarly, pos=-1 needs to get the current
+ // frame, so round towards negative infinity.
+ int round = mp_refqueue_top_field_first(q) != mp_refqueue_is_top_field(q);
+ int frame = (pos < 0 ? pos - (1 - round) : pos + round) / 2;
+ return mp_refqueue_get(q, frame);
+}
+
+bool mp_refqueue_is_second_field(struct mp_refqueue *q)
+{
+ return mp_refqueue_has_output(q) && q->second_field;
+}
+
+// Return non-NULL if a format change happened. A format change is defined by
+// a change in image parameters, using broad enough checks that happen to be
+// sufficient for all users of refqueue.
+// On format change, the refqueue transparently drains remaining frames, and
+// once that is done, this function returns a mp_image reference of the new
+// frame. Reinit the low level video processor based on it, and then leave the
+// reference alone and continue normally.
+// All frames returned in the future will have a compatible format.
+struct mp_image *mp_refqueue_execute_reinit(struct mp_refqueue *q)
+{
+ if (mp_refqueue_has_output(q) || !q->next)
+ return NULL;
+
+ struct mp_image *cur = q->next;
+ q->next = NULL;
+
+ mp_image_unrefp(&q->in_format);
+ mp_refqueue_flush(q);
+
+ q->in_format = mp_image_new_ref(cur);
+ mp_image_unref_data(q->in_format);
+
+ mp_refqueue_add_input(q, cur);
+ return cur;
+}
+
+// Main processing function. Call this in the filter process function.
+// Returns if enough input frames are available for filtering, and output pin
+// needs data; in other words, if this returns true, you render a frame and
+// output it.
+// If this returns true, you must call mp_refqueue_write_out_pin() to make
+// progress.
+bool mp_refqueue_can_output(struct mp_refqueue *q)
+{
+ if (!mp_pin_in_needs_data(q->out))
+ return false;
+
+ // Strictly return any output first to reduce latency.
+ if (mp_refqueue_has_output(q))
+ return true;
+
+ if (q->next) {
+ // Make it call again for mp_refqueue_execute_reinit().
+ mp_filter_internal_mark_progress(q->filter);
+ return false;
+ }
+
+ struct mp_frame frame = mp_pin_out_read(q->in);
+ if (frame.type == MP_FRAME_NONE)
+ return false;
+
+ if (frame.type == MP_FRAME_EOF) {
+ q->eof = true;
+ if (mp_refqueue_has_output(q)) {
+ mp_pin_out_unread(q->in, frame);
+ return true;
+ }
+ mp_pin_in_write(q->out, frame);
+ mp_refqueue_flush(q);
+ return false;
+ }
+
+ if (frame.type != MP_FRAME_VIDEO) {
+ MP_ERR(q->filter, "unsupported frame type\n");
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(q->filter);
+ return false;
+ }
+
+ struct mp_image *img = frame.data;
+
+ if (!q->in_format || !!q->in_format->hwctx != !!img->hwctx ||
+ (img->hwctx && img->hwctx->data != q->in_format->hwctx->data) ||
+ !mp_image_params_equal(&q->in_format->params, &img->params))
+ {
+ q->next = img;
+ q->eof = true;
+ mp_filter_internal_mark_progress(q->filter);
+ return false;
+ }
+
+ mp_refqueue_add_input(q, img);
+
+ if (mp_refqueue_has_output(q))
+ return true;
+
+ mp_pin_out_request_data(q->in);
+ return false;
+}
+
+// (Accepts NULL for generic errors.)
+void mp_refqueue_write_out_pin(struct mp_refqueue *q, struct mp_image *mpi)
+{
+ if (mpi) {
+ mp_pin_in_write(q->out, MAKE_FRAME(MP_FRAME_VIDEO, mpi));
+ } else {
+ MP_WARN(q->filter, "failed to output frame\n");
+ mp_filter_internal_mark_failed(q->filter);
+ }
+ mp_refqueue_next_field(q);
+}
+
+// Return frame for current format (without data). Reference is owned by q,
+// might go away on further queue accesses. NULL if none yet.
+struct mp_image *mp_refqueue_get_format(struct mp_refqueue *q)
+{
+ return q->in_format;
+}
diff --git a/video/filter/refqueue.h b/video/filter/refqueue.h
new file mode 100644
index 0000000..0a8ace0
--- /dev/null
+++ b/video/filter/refqueue.h
@@ -0,0 +1,39 @@
+#ifndef MP_REFQUEUE_H_
+#define MP_REFQUEUE_H_
+
+#include <stdbool.h>
+
+#include "filters/filter.h"
+
+// A helper for deinterlacers which require past/future reference frames.
+
+struct mp_refqueue;
+
+struct mp_refqueue *mp_refqueue_alloc(struct mp_filter *f);
+
+void mp_refqueue_add_in_format(struct mp_refqueue *q, int fmt, int subfmt);
+
+void mp_refqueue_set_refs(struct mp_refqueue *q, int past, int future);
+void mp_refqueue_flush(struct mp_refqueue *q);
+struct mp_image *mp_refqueue_get(struct mp_refqueue *q, int pos);
+
+struct mp_image *mp_refqueue_execute_reinit(struct mp_refqueue *q);
+bool mp_refqueue_can_output(struct mp_refqueue *q);
+void mp_refqueue_write_out_pin(struct mp_refqueue *q, struct mp_image *mpi);
+
+struct mp_image *mp_refqueue_get_format(struct mp_refqueue *q);
+
+enum {
+ MP_MODE_DEINT = (1 << 0), // deinterlacing enabled
+ MP_MODE_OUTPUT_FIELDS = (1 << 1), // output fields separately
+ MP_MODE_INTERLACED_ONLY = (1 << 2), // only deinterlace marked frames
+};
+
+void mp_refqueue_set_mode(struct mp_refqueue *q, int flags);
+bool mp_refqueue_should_deint(struct mp_refqueue *q);
+bool mp_refqueue_is_top_field(struct mp_refqueue *q);
+bool mp_refqueue_top_field_first(struct mp_refqueue *q);
+bool mp_refqueue_is_second_field(struct mp_refqueue *q);
+struct mp_image *mp_refqueue_get_field(struct mp_refqueue *q, int pos);
+
+#endif
diff --git a/video/filter/vf_d3d11vpp.c b/video/filter/vf_d3d11vpp.c
new file mode 100644
index 0000000..3f00c5a
--- /dev/null
+++ b/video/filter/vf_d3d11vpp.c
@@ -0,0 +1,506 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <windows.h>
+#include <d3d11.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_d3d11va.h>
+
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "refqueue.h"
+#include "video/hwdec.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+
+// missing in MinGW
+#define D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_BLEND 0x1
+#define D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_BOB 0x2
+#define D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_ADAPTIVE 0x4
+#define D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_MOTION_COMPENSATION 0x8
+#define D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_INVERSE_TELECINE 0x10
+#define D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_FRAME_RATE_CONVERSION 0x20
+
+struct opts {
+ bool deint_enabled;
+ bool interlaced_only;
+ int mode;
+};
+
+struct priv {
+ struct opts *opts;
+
+ ID3D11Device *vo_dev;
+
+ ID3D11DeviceContext *device_ctx;
+ ID3D11VideoDevice *video_dev;
+ ID3D11VideoContext *video_ctx;
+
+ ID3D11VideoProcessor *video_proc;
+ ID3D11VideoProcessorEnumerator *vp_enum;
+ D3D11_VIDEO_FRAME_FORMAT d3d_frame_format;
+
+ DXGI_FORMAT out_format;
+
+ bool require_filtering;
+
+ struct mp_image_params params, out_params;
+ int c_w, c_h;
+
+ struct mp_image_pool *pool;
+
+ struct mp_refqueue *queue;
+};
+
+static void release_tex(void *arg)
+{
+ ID3D11Texture2D *texture = arg;
+
+ ID3D11Texture2D_Release(texture);
+}
+
+static struct mp_image *alloc_pool(void *pctx, int fmt, int w, int h)
+{
+ struct mp_filter *vf = pctx;
+ struct priv *p = vf->priv;
+ HRESULT hr;
+
+ ID3D11Texture2D *texture = NULL;
+ D3D11_TEXTURE2D_DESC texdesc = {
+ .Width = w,
+ .Height = h,
+ .Format = p->out_format,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc = { .Count = 1 },
+ .Usage = D3D11_USAGE_DEFAULT,
+ .BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->vo_dev, &texdesc, NULL, &texture);
+ if (FAILED(hr))
+ return NULL;
+
+ struct mp_image *mpi = mp_image_new_custom_ref(NULL, texture, release_tex);
+ MP_HANDLE_OOM(mpi);
+
+ mp_image_setfmt(mpi, IMGFMT_D3D11);
+ mp_image_set_size(mpi, w, h);
+ mpi->params.hw_subfmt = p->out_params.hw_subfmt;
+
+ mpi->planes[0] = (void *)texture;
+ mpi->planes[1] = (void *)(intptr_t)0;
+
+ return mpi;
+}
+
+static void flush_frames(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+ mp_refqueue_flush(p->queue);
+}
+
+static void destroy_video_proc(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+
+ if (p->video_proc)
+ ID3D11VideoProcessor_Release(p->video_proc);
+ p->video_proc = NULL;
+
+ if (p->vp_enum)
+ ID3D11VideoProcessorEnumerator_Release(p->vp_enum);
+ p->vp_enum = NULL;
+}
+
+static int recreate_video_proc(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+ HRESULT hr;
+
+ destroy_video_proc(vf);
+
+ D3D11_VIDEO_PROCESSOR_CONTENT_DESC vpdesc = {
+ .InputFrameFormat = p->d3d_frame_format,
+ .InputWidth = p->c_w,
+ .InputHeight = p->c_h,
+ .OutputWidth = p->params.w,
+ .OutputHeight = p->params.h,
+ };
+ hr = ID3D11VideoDevice_CreateVideoProcessorEnumerator(p->video_dev, &vpdesc,
+ &p->vp_enum);
+ if (FAILED(hr))
+ goto fail;
+
+ D3D11_VIDEO_PROCESSOR_CAPS caps;
+ hr = ID3D11VideoProcessorEnumerator_GetVideoProcessorCaps(p->vp_enum, &caps);
+ if (FAILED(hr))
+ goto fail;
+
+ MP_VERBOSE(vf, "Found %d rate conversion caps. Looking for caps=0x%x.\n",
+ (int)caps.RateConversionCapsCount, p->opts->mode);
+
+ int rindex = -1;
+ for (int n = 0; n < caps.RateConversionCapsCount; n++) {
+ D3D11_VIDEO_PROCESSOR_RATE_CONVERSION_CAPS rcaps;
+ hr = ID3D11VideoProcessorEnumerator_GetVideoProcessorRateConversionCaps
+ (p->vp_enum, n, &rcaps);
+ if (FAILED(hr))
+ goto fail;
+ MP_VERBOSE(vf, " - %d: 0x%08x\n", n, (unsigned)rcaps.ProcessorCaps);
+ if (rcaps.ProcessorCaps & p->opts->mode) {
+ MP_VERBOSE(vf, " (matching)\n");
+ if (rindex < 0)
+ rindex = n;
+ }
+ }
+
+ if (rindex < 0) {
+ MP_WARN(vf, "No fitting video processor found, picking #0.\n");
+ rindex = 0;
+ }
+
+ // TODO: so, how do we select which rate conversion mode the processor uses?
+
+ hr = ID3D11VideoDevice_CreateVideoProcessor(p->video_dev, p->vp_enum, rindex,
+ &p->video_proc);
+ if (FAILED(hr)) {
+ MP_ERR(vf, "Failed to create D3D11 video processor.\n");
+ goto fail;
+ }
+
+ // Note: libavcodec does not support cropping left/top with hwaccel.
+ RECT src_rc = {
+ .right = p->params.w,
+ .bottom = p->params.h,
+ };
+ ID3D11VideoContext_VideoProcessorSetStreamSourceRect(p->video_ctx,
+ p->video_proc,
+ 0, TRUE, &src_rc);
+
+ // This is supposed to stop drivers from fucking up the video quality.
+ ID3D11VideoContext_VideoProcessorSetStreamAutoProcessingMode(p->video_ctx,
+ p->video_proc,
+ 0, FALSE);
+
+ ID3D11VideoContext_VideoProcessorSetStreamOutputRate(p->video_ctx,
+ p->video_proc,
+ 0,
+ D3D11_VIDEO_PROCESSOR_OUTPUT_RATE_NORMAL,
+ FALSE, 0);
+
+ D3D11_VIDEO_PROCESSOR_COLOR_SPACE csp = {
+ .YCbCr_Matrix = p->params.color.space != MP_CSP_BT_601,
+ .Nominal_Range = p->params.color.levels == MP_CSP_LEVELS_TV ? 1 : 2,
+ };
+ ID3D11VideoContext_VideoProcessorSetStreamColorSpace(p->video_ctx,
+ p->video_proc,
+ 0, &csp);
+ ID3D11VideoContext_VideoProcessorSetOutputColorSpace(p->video_ctx,
+ p->video_proc,
+ &csp);
+
+ return 0;
+fail:
+ destroy_video_proc(vf);
+ return -1;
+}
+
+static struct mp_image *render(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+ int res = -1;
+ HRESULT hr;
+ ID3D11VideoProcessorInputView *in_view = NULL;
+ ID3D11VideoProcessorOutputView *out_view = NULL;
+ struct mp_image *in = NULL, *out = NULL;
+ out = mp_image_pool_get(p->pool, IMGFMT_D3D11, p->params.w, p->params.h);
+ if (!out) {
+ MP_WARN(vf, "failed to allocate frame\n");
+ goto cleanup;
+ }
+
+ ID3D11Texture2D *d3d_out_tex = (void *)out->planes[0];
+
+ in = mp_refqueue_get(p->queue, 0);
+ if (!in)
+ goto cleanup;
+ ID3D11Texture2D *d3d_tex = (void *)in->planes[0];
+ int d3d_subindex = (intptr_t)in->planes[1];
+
+ mp_image_copy_attributes(out, in);
+
+ D3D11_VIDEO_FRAME_FORMAT d3d_frame_format;
+ if (!mp_refqueue_should_deint(p->queue)) {
+ d3d_frame_format = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE;
+ } else if (mp_refqueue_top_field_first(p->queue)) {
+ d3d_frame_format = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST;
+ } else {
+ d3d_frame_format = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_BOTTOM_FIELD_FIRST;
+ }
+
+ D3D11_TEXTURE2D_DESC texdesc;
+ ID3D11Texture2D_GetDesc(d3d_tex, &texdesc);
+ if (!p->video_proc || p->c_w != texdesc.Width || p->c_h != texdesc.Height ||
+ p->d3d_frame_format != d3d_frame_format)
+ {
+ p->c_w = texdesc.Width;
+ p->c_h = texdesc.Height;
+ p->d3d_frame_format = d3d_frame_format;
+ if (recreate_video_proc(vf) < 0)
+ goto cleanup;
+ }
+
+ if (!mp_refqueue_should_deint(p->queue)) {
+ d3d_frame_format = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE;
+ } else if (mp_refqueue_is_top_field(p->queue)) {
+ d3d_frame_format = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST;
+ } else {
+ d3d_frame_format = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_BOTTOM_FIELD_FIRST;
+ }
+
+ ID3D11VideoContext_VideoProcessorSetStreamFrameFormat(p->video_ctx,
+ p->video_proc,
+ 0, d3d_frame_format);
+
+ D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC indesc = {
+ .ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D,
+ .Texture2D = {
+ .ArraySlice = d3d_subindex,
+ },
+ };
+ hr = ID3D11VideoDevice_CreateVideoProcessorInputView(p->video_dev,
+ (ID3D11Resource *)d3d_tex,
+ p->vp_enum, &indesc,
+ &in_view);
+ if (FAILED(hr)) {
+ MP_ERR(vf, "Could not create ID3D11VideoProcessorInputView\n");
+ goto cleanup;
+ }
+
+ D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC outdesc = {
+ .ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D,
+ };
+ hr = ID3D11VideoDevice_CreateVideoProcessorOutputView(p->video_dev,
+ (ID3D11Resource *)d3d_out_tex,
+ p->vp_enum, &outdesc,
+ &out_view);
+ if (FAILED(hr)) {
+ MP_ERR(vf, "Could not create ID3D11VideoProcessorOutputView\n");
+ goto cleanup;
+ }
+
+ D3D11_VIDEO_PROCESSOR_STREAM stream = {
+ .Enable = TRUE,
+ .pInputSurface = in_view,
+ };
+ int frame = mp_refqueue_is_second_field(p->queue);
+ hr = ID3D11VideoContext_VideoProcessorBlt(p->video_ctx, p->video_proc,
+ out_view, frame, 1, &stream);
+ if (FAILED(hr)) {
+ MP_ERR(vf, "VideoProcessorBlt failed.\n");
+ goto cleanup;
+ }
+
+ res = 0;
+cleanup:
+ if (in_view)
+ ID3D11VideoProcessorInputView_Release(in_view);
+ if (out_view)
+ ID3D11VideoProcessorOutputView_Release(out_view);
+ if (res < 0)
+ TA_FREEP(&out);
+ return out;
+}
+
+static void vf_d3d11vpp_process(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+
+ struct mp_image *in_fmt = mp_refqueue_execute_reinit(p->queue);
+ if (in_fmt) {
+ mp_image_pool_clear(p->pool);
+
+ destroy_video_proc(vf);
+
+ p->params = in_fmt->params;
+ p->out_params = p->params;
+
+ p->out_params.hw_subfmt = IMGFMT_NV12;
+ p->out_format = DXGI_FORMAT_NV12;
+
+ p->require_filtering = p->params.hw_subfmt != p->out_params.hw_subfmt;
+ }
+
+ if (!mp_refqueue_can_output(p->queue))
+ return;
+
+ if (!mp_refqueue_should_deint(p->queue) && !p->require_filtering) {
+ // no filtering
+ struct mp_image *in = mp_image_new_ref(mp_refqueue_get(p->queue, 0));
+ if (!in) {
+ mp_filter_internal_mark_failed(vf);
+ return;
+ }
+ mp_refqueue_write_out_pin(p->queue, in);
+ } else {
+ mp_refqueue_write_out_pin(p->queue, render(vf));
+ }
+}
+
+static void uninit(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+
+ destroy_video_proc(vf);
+
+ flush_frames(vf);
+ talloc_free(p->queue);
+ talloc_free(p->pool);
+
+ if (p->video_ctx)
+ ID3D11VideoContext_Release(p->video_ctx);
+
+ if (p->video_dev)
+ ID3D11VideoDevice_Release(p->video_dev);
+
+ if (p->device_ctx)
+ ID3D11DeviceContext_Release(p->device_ctx);
+
+ if (p->vo_dev)
+ ID3D11Device_Release(p->vo_dev);
+}
+
+static const struct mp_filter_info vf_d3d11vpp_filter = {
+ .name = "d3d11vpp",
+ .process = vf_d3d11vpp_process,
+ .reset = flush_frames,
+ .destroy = uninit,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *vf_d3d11vpp_create(struct mp_filter *parent,
+ void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &vf_d3d11vpp_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+
+ // Special path for vf_d3d11_create_outconv(): disable all processing except
+ // possibly surface format conversions.
+ if (!p->opts) {
+ static const struct opts opts = {0};
+ p->opts = (struct opts *)&opts;
+ }
+
+ p->queue = mp_refqueue_alloc(f);
+
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ if (!info || !info->hwdec_devs)
+ goto fail;
+
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = IMGFMT_D3D11,
+ .probing = false,
+ };
+ hwdec_devices_request_for_img_fmt(info->hwdec_devs, &params);
+
+ struct mp_hwdec_ctx *hwctx =
+ hwdec_devices_get_by_imgfmt(info->hwdec_devs, IMGFMT_D3D11);
+ if (!hwctx || !hwctx->av_device_ref)
+ goto fail;
+ AVHWDeviceContext *avhwctx = (void *)hwctx->av_device_ref->data;
+ AVD3D11VADeviceContext *d3dctx = avhwctx->hwctx;
+
+ p->vo_dev = d3dctx->device;
+ ID3D11Device_AddRef(p->vo_dev);
+
+ HRESULT hr;
+
+ hr = ID3D11Device_QueryInterface(p->vo_dev, &IID_ID3D11VideoDevice,
+ (void **)&p->video_dev);
+ if (FAILED(hr))
+ goto fail;
+
+ ID3D11Device_GetImmediateContext(p->vo_dev, &p->device_ctx);
+ if (!p->device_ctx)
+ goto fail;
+ hr = ID3D11DeviceContext_QueryInterface(p->device_ctx, &IID_ID3D11VideoContext,
+ (void **)&p->video_ctx);
+ if (FAILED(hr))
+ goto fail;
+
+ p->pool = mp_image_pool_new(f);
+ mp_image_pool_set_allocator(p->pool, alloc_pool, f);
+ mp_image_pool_set_lru(p->pool);
+
+ mp_refqueue_add_in_format(p->queue, IMGFMT_D3D11, 0);
+
+ mp_refqueue_set_refs(p->queue, 0, 0);
+ mp_refqueue_set_mode(p->queue,
+ (p->opts->deint_enabled ? MP_MODE_DEINT : 0) |
+ MP_MODE_OUTPUT_FIELDS |
+ (p->opts->interlaced_only ? MP_MODE_INTERLACED_ONLY : 0));
+
+ return f;
+
+fail:
+ talloc_free(f);
+ return NULL;
+}
+
+#define OPT_BASE_STRUCT struct opts
+static const m_option_t vf_opts_fields[] = {
+ {"deint", OPT_BOOL(deint_enabled)},
+ {"interlaced-only", OPT_BOOL(interlaced_only)},
+ {"mode", OPT_CHOICE(mode,
+ {"blend", D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_BLEND},
+ {"bob", D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_BOB},
+ {"adaptive", D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_ADAPTIVE},
+ {"mocomp", D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_MOTION_COMPENSATION},
+ {"ivctc", D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_INVERSE_TELECINE},
+ {"none", 0})},
+ {0}
+};
+
+const struct mp_user_filter_entry vf_d3d11vpp = {
+ .desc = {
+ .description = "D3D11 Video Post-Process Filter",
+ .name = "d3d11vpp",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT) {
+ .deint_enabled = true,
+ .mode = D3D11_VIDEO_PROCESSOR_PROCESSOR_CAPS_DEINTERLACE_BOB,
+ },
+ .options = vf_opts_fields,
+ },
+ .create = vf_d3d11vpp_create,
+};
diff --git a/video/filter/vf_fingerprint.c b/video/filter/vf_fingerprint.c
new file mode 100644
index 0000000..8714382
--- /dev/null
+++ b/video/filter/vf_fingerprint.c
@@ -0,0 +1,229 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include "common/common.h"
+#include "common/tags.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+#include "video/img_format.h"
+#include "video/sws_utils.h"
+#include "video/zimg.h"
+
+#include "osdep/timer.h"
+
+#define PRINT_ENTRY_NUM 10
+
+struct f_opts {
+ int type;
+ bool clear;
+ bool print;
+};
+
+const struct m_opt_choice_alternatives type_names[] = {
+ {"gray-hex-8x8", 8},
+ {"gray-hex-16x16", 16},
+ {0}
+};
+
+#define OPT_BASE_STRUCT struct f_opts
+static const struct m_option f_opts_list[] = {
+ {"type", OPT_CHOICE_C(type, type_names)},
+ {"clear-on-query", OPT_BOOL(clear)},
+ {"print", OPT_BOOL(print)},
+ {0}
+};
+
+static const struct f_opts f_opts_def = {
+ .type = 16,
+ .clear = true,
+};
+
+struct print_entry {
+ double pts;
+ char *print;
+};
+
+struct priv {
+ struct f_opts *opts;
+ struct mp_image *scaled;
+ struct mp_sws_context *sws;
+ struct mp_zimg_context *zimg;
+ struct print_entry entries[PRINT_ENTRY_NUM];
+ int num_entries;
+ bool fallback_warning;
+};
+
+// (Other code internal to this filter also calls this to reset the frame list.)
+static void f_reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ for (int n = 0; n < p->num_entries; n++)
+ talloc_free(p->entries[n].print);
+ p->num_entries = 0;
+}
+
+static void f_process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (mp_frame_is_signaling(frame)) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+
+ if (frame.type != MP_FRAME_VIDEO)
+ goto error;
+
+ struct mp_image *mpi = frame.data;
+
+ // Try to achieve minimum conversion, even if it makes the fingerprints less
+ // "portable" across source video.
+ p->scaled->params.color = mpi->params.color;
+ // Make output always full range; no reason to lose precision.
+ p->scaled->params.color.levels = MP_CSP_LEVELS_PC;
+
+ if (!mp_zimg_convert(p->zimg, p->scaled, mpi)) {
+ if (!p->fallback_warning) {
+ MP_WARN(f, "Falling back to libswscale.\n");
+ p->fallback_warning = true;
+ }
+ if (mp_sws_scale(p->sws, p->scaled, mpi) < 0)
+ goto error;
+ }
+
+ if (p->num_entries >= PRINT_ENTRY_NUM) {
+ talloc_free(p->entries[0].print);
+ MP_TARRAY_REMOVE_AT(p->entries, p->num_entries, 0);
+ }
+
+ int size = p->scaled->w;
+
+ struct print_entry *e = &p->entries[p->num_entries++];
+ e->pts = mpi->pts;
+ e->print = talloc_array(p, char, size * size * 2 + 1);
+
+ for (int y = 0; y < size; y++) {
+ for (int x = 0; x < size; x++) {
+ char *offs = &e->print[(y * size + x) * 2];
+ uint8_t v = p->scaled->planes[0][y * p->scaled->stride[0] + x];
+ snprintf(offs, 3, "%02x", v);
+ }
+ }
+
+ if (p->opts->print)
+ MP_INFO(f, "%f: %s\n", e->pts, e->print);
+
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+
+error:
+ MP_ERR(f, "unsupported video format\n");
+ mp_pin_in_write(f->ppins[1], frame);
+ mp_filter_internal_mark_failed(f);
+}
+
+static bool f_command(struct mp_filter *f, struct mp_filter_command *cmd)
+{
+ struct priv *p = f->priv;
+
+ switch (cmd->type) {
+ case MP_FILTER_COMMAND_GET_META: {
+ struct mp_tags *t = talloc_zero(NULL, struct mp_tags);
+
+ for (int n = 0; n < p->num_entries; n++) {
+ struct print_entry *e = &p->entries[n];
+
+ if (e->pts != MP_NOPTS_VALUE) {
+ mp_tags_set_str(t, mp_tprintf(80, "fp%d.pts", n),
+ mp_tprintf(80, "%f", e->pts));
+ }
+ mp_tags_set_str(t, mp_tprintf(80, "fp%d.hex", n), e->print);
+ }
+
+ mp_tags_set_str(t, "type", m_opt_choice_str(type_names, p->opts->type));
+
+ if (p->opts->clear)
+ f_reset(f);
+
+ *(struct mp_tags **)cmd->res = t;
+ return true;
+ }
+ default:
+ return false;
+ }
+}
+
+static const struct mp_filter_info filter = {
+ .name = "fingerprint",
+ .process = f_process,
+ .command = f_command,
+ .reset = f_reset,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *f_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+ int size = p->opts->type;
+ p->scaled = mp_image_alloc(IMGFMT_Y8, size, size);
+ MP_HANDLE_OOM(p->scaled);
+ talloc_steal(p, p->scaled);
+ p->sws = mp_sws_alloc(p);
+ MP_HANDLE_OOM(p->sws);
+ p->zimg = mp_zimg_alloc();
+ talloc_steal(p, p->zimg);
+ p->zimg->opts = (struct zimg_opts){
+ .scaler = ZIMG_RESIZE_BILINEAR,
+ .scaler_params = {NAN, NAN},
+ .scaler_chroma_params = {NAN, NAN},
+ .scaler_chroma = ZIMG_RESIZE_BILINEAR,
+ .dither = ZIMG_DITHER_NONE,
+ .fast = true,
+ };
+ return f;
+}
+
+const struct mp_user_filter_entry vf_fingerprint = {
+ .desc = {
+ .description = "Compute video frame fingerprints",
+ .name = "fingerprint",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &f_opts_def,
+ .options = f_opts_list,
+ },
+ .create = f_create,
+};
diff --git a/video/filter/vf_format.c b/video/filter/vf_format.c
new file mode 100644
index 0000000..4997d6f
--- /dev/null
+++ b/video/filter/vf_format.c
@@ -0,0 +1,245 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <math.h>
+
+#include <libavutil/rational.h>
+#include <libavutil/buffer.h>
+
+#include "common/msg.h"
+#include "common/common.h"
+#include "filters/f_autoconvert.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+
+#include "options/m_option.h"
+
+struct priv {
+ struct vf_format_opts *opts;
+ struct mp_autoconvert *conv;
+};
+
+struct vf_format_opts {
+ int fmt;
+ int colormatrix;
+ int colorlevels;
+ int primaries;
+ int gamma;
+ float sig_peak;
+ int light;
+ int chroma_location;
+ int stereo_in;
+ int rotate;
+ int alpha;
+ int w, h;
+ int dw, dh;
+ double dar;
+ bool convert;
+ int force_scaler;
+ bool dovi;
+ bool film_grain;
+};
+
+static void set_params(struct vf_format_opts *p, struct mp_image_params *out,
+ bool set_size)
+{
+ if (p->colormatrix)
+ out->color.space = p->colormatrix;
+ if (p->colorlevels)
+ out->color.levels = p->colorlevels;
+ if (p->primaries)
+ out->color.primaries = p->primaries;
+ if (p->gamma) {
+ enum mp_csp_trc in_gamma = p->gamma;
+ out->color.gamma = p->gamma;
+ if (in_gamma != out->color.gamma) {
+ // When changing the gamma function explicitly, also reset stuff
+ // related to the gamma function since that information will almost
+ // surely be false now and have to be re-inferred
+ out->color.hdr = (struct pl_hdr_metadata){0};
+ out->color.light = MP_CSP_LIGHT_AUTO;
+ }
+ }
+ if (p->sig_peak)
+ out->color.hdr = (struct pl_hdr_metadata){ .max_luma = p->sig_peak * MP_REF_WHITE };
+ if (p->light)
+ out->color.light = p->light;
+ if (p->chroma_location)
+ out->chroma_location = p->chroma_location;
+ if (p->stereo_in)
+ out->stereo3d = p->stereo_in;
+ if (p->rotate >= 0)
+ out->rotate = p->rotate;
+ if (p->alpha)
+ out->alpha = p->alpha;
+
+ if (p->w > 0 && set_size)
+ out->w = p->w;
+ if (p->h > 0 && set_size)
+ out->h = p->h;
+ AVRational dsize;
+ mp_image_params_get_dsize(out, &dsize.num, &dsize.den);
+ if (p->dw > 0)
+ dsize.num = p->dw;
+ if (p->dh > 0)
+ dsize.den = p->dh;
+ if (p->dar > 0)
+ dsize = av_d2q(p->dar, INT_MAX);
+ mp_image_params_set_dsize(out, dsize.num, dsize.den);
+}
+
+static void vf_format_process(struct mp_filter *f)
+{
+ struct priv *priv = f->priv;
+
+ if (mp_pin_can_transfer_data(priv->conv->f->pins[0], f->ppins[0])) {
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (priv->opts->convert && frame.type == MP_FRAME_VIDEO) {
+ struct mp_image *img = frame.data;
+ struct mp_image_params par = img->params;
+ int outfmt = priv->opts->fmt;
+
+ // If we convert from RGB to YUV, default to limited range.
+ if (mp_imgfmt_get_forced_csp(img->imgfmt) == MP_CSP_RGB &&
+ outfmt && mp_imgfmt_get_forced_csp(outfmt) == MP_CSP_AUTO)
+ {
+ par.color.levels = MP_CSP_LEVELS_TV;
+ }
+
+ set_params(priv->opts, &par, true);
+
+ if (outfmt && par.imgfmt != outfmt) {
+ par.imgfmt = outfmt;
+ par.hw_subfmt = 0;
+ }
+ mp_image_params_guess_csp(&par);
+
+ mp_autoconvert_set_target_image_params(priv->conv, &par);
+ }
+
+ mp_pin_in_write(priv->conv->f->pins[0], frame);
+ }
+
+ if (mp_pin_can_transfer_data(f->ppins[1], priv->conv->f->pins[1])) {
+ struct mp_frame frame = mp_pin_out_read(priv->conv->f->pins[1]);
+ struct mp_image *img = frame.data;
+
+ if (frame.type != MP_FRAME_VIDEO)
+ goto write_out;
+
+ if (!priv->opts->convert) {
+ set_params(priv->opts, &img->params, false);
+ mp_image_params_guess_csp(&img->params);
+ }
+
+ if (!priv->opts->dovi) {
+ av_buffer_unref(&img->dovi);
+ av_buffer_unref(&img->dovi_buf);
+ }
+
+ if (!priv->opts->film_grain)
+ av_buffer_unref(&img->film_grain);
+
+write_out:
+ mp_pin_in_write(f->ppins[1], frame);
+ }
+}
+
+static const struct mp_filter_info vf_format_filter = {
+ .name = "format",
+ .process = vf_format_process,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *vf_format_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &vf_format_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ struct priv *priv = f->priv;
+ priv->opts = talloc_steal(priv, options);
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ priv->conv = mp_autoconvert_create(f);
+ if (!priv->conv) {
+ talloc_free(f);
+ return NULL;
+ }
+
+ priv->conv->force_scaler = priv->opts->force_scaler;
+
+ if (priv->opts->fmt)
+ mp_autoconvert_add_imgfmt(priv->conv, priv->opts->fmt, 0);
+
+ return f;
+}
+
+#define OPT_BASE_STRUCT struct vf_format_opts
+static const m_option_t vf_opts_fields[] = {
+ {"fmt", OPT_IMAGEFORMAT(fmt)},
+ {"colormatrix", OPT_CHOICE_C(colormatrix, mp_csp_names)},
+ {"colorlevels", OPT_CHOICE_C(colorlevels, mp_csp_levels_names)},
+ {"primaries", OPT_CHOICE_C(primaries, mp_csp_prim_names)},
+ {"gamma", OPT_CHOICE_C(gamma, mp_csp_trc_names)},
+ {"sig-peak", OPT_FLOAT(sig_peak)},
+ {"light", OPT_CHOICE_C(light, mp_csp_light_names)},
+ {"chroma-location", OPT_CHOICE_C(chroma_location, mp_chroma_names)},
+ {"stereo-in", OPT_CHOICE_C(stereo_in, mp_stereo3d_names)},
+ {"rotate", OPT_INT(rotate), M_RANGE(-1, 359)},
+ {"alpha", OPT_CHOICE_C(alpha, mp_alpha_names)},
+ {"w", OPT_INT(w)},
+ {"h", OPT_INT(h)},
+ {"dw", OPT_INT(dw)},
+ {"dh", OPT_INT(dh)},
+ {"dar", OPT_DOUBLE(dar)},
+ {"convert", OPT_BOOL(convert)},
+ {"dolbyvision", OPT_BOOL(dovi)},
+ {"film-grain", OPT_BOOL(film_grain)},
+ {"force-scaler", OPT_CHOICE(force_scaler,
+ {"auto", MP_SWS_AUTO},
+ {"sws", MP_SWS_SWS},
+ {"zimg", MP_SWS_ZIMG})},
+ {0}
+};
+
+const struct mp_user_filter_entry vf_format = {
+ .desc = {
+ .description = "force output format",
+ .name = "format",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .rotate = -1,
+ .dovi = true,
+ .film_grain = true,
+ },
+ .options = vf_opts_fields,
+ },
+ .create = vf_format_create,
+};
diff --git a/video/filter/vf_gpu.c b/video/filter/vf_gpu.c
new file mode 100644
index 0000000..fb11941
--- /dev/null
+++ b/video/filter/vf_gpu.c
@@ -0,0 +1,373 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common/common.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/options.h"
+#include "video/out/aspect.h"
+#include "video/out/gpu/video.h"
+#include "video/out/opengl/egl_helpers.h"
+#include "video/out/opengl/ra_gl.h"
+
+struct offscreen_ctx {
+ struct mp_log *log;
+ struct ra *ra;
+ void *priv;
+
+ void (*set_context)(struct offscreen_ctx *ctx, bool enable);
+};
+
+struct gl_offscreen_ctx {
+ GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+};
+
+static void gl_ctx_destroy(void *p)
+{
+ struct offscreen_ctx *ctx = p;
+ struct gl_offscreen_ctx *gl = ctx->priv;
+
+ ra_free(&ctx->ra);
+
+ if (gl->egl_context)
+ eglDestroyContext(gl->egl_display, gl->egl_context);
+}
+
+static void gl_ctx_set_context(struct offscreen_ctx *ctx, bool enable)
+{
+ struct gl_offscreen_ctx *gl = ctx->priv;
+ EGLContext c = enable ? gl->egl_context : EGL_NO_CONTEXT;
+
+ if (!eglMakeCurrent(gl->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, c))
+ MP_ERR(ctx, "Could not make EGL context current.\n");
+}
+
+static struct offscreen_ctx *gl_offscreen_ctx_create(struct mpv_global *global,
+ struct mp_log *log)
+{
+ struct offscreen_ctx *ctx = talloc_zero(NULL, struct offscreen_ctx);
+ struct gl_offscreen_ctx *gl = talloc_zero(ctx, struct gl_offscreen_ctx);
+ talloc_set_destructor(ctx, gl_ctx_destroy);
+ *ctx = (struct offscreen_ctx){
+ .log = log,
+ .priv = gl,
+ .set_context = gl_ctx_set_context,
+ };
+
+ // This appears to work with Mesa. EGL 1.5 doesn't specify what a "default
+ // display" is at all.
+ gl->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(gl->egl_display, NULL, NULL)) {
+ MP_ERR(ctx, "Could not initialize EGL.\n");
+ goto error;
+ }
+
+ // Unfortunately, mpegl_create_context() is entangled with ra_ctx.
+ // Fortunately, it does not need much, and we can provide a stub.
+ struct ra_ctx ractx = {
+ .log = ctx->log,
+ .global = global,
+ };
+ EGLConfig config;
+ if (!mpegl_create_context(&ractx, gl->egl_display, &gl->egl_context, &config))
+ {
+ MP_ERR(ctx, "Could not create EGL context.\n");
+ goto error;
+ }
+
+ if (!eglMakeCurrent(gl->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ gl->egl_context))
+ {
+ MP_ERR(ctx, "Could not make EGL context current.\n");
+ goto error;
+ }
+
+ mpegl_load_functions(&gl->gl, ctx->log);
+ ctx->ra = ra_create_gl(&gl->gl, ctx->log);
+
+ if (!ctx->ra)
+ goto error;
+
+ gl_ctx_set_context(ctx, false);
+
+ return ctx;
+
+error:
+ talloc_free(ctx);
+ return NULL;
+}
+
+static void offscreen_ctx_set_current(struct offscreen_ctx *ctx, bool enable)
+{
+ if (ctx->set_context)
+ ctx->set_context(ctx, enable);
+}
+
+struct gpu_opts {
+ int w, h;
+};
+
+struct priv {
+ struct gpu_opts *opts;
+ struct m_config_cache *vo_opts_cache;
+ struct mp_vo_opts *vo_opts;
+
+ struct offscreen_ctx *ctx;
+ struct gl_video *renderer;
+ struct ra_tex *target;
+
+ struct mp_image_params img_params;
+ uint64_t next_frame_id;
+};
+
+static struct mp_image *gpu_render_frame(struct mp_filter *f, struct mp_image *in)
+{
+ struct priv *priv = f->priv;
+ bool ok = false;
+ struct mp_image *res = NULL;
+ struct ra *ra = priv->ctx->ra;
+
+ if (priv->opts->w <= 0)
+ priv->opts->w = in->w;
+ if (priv->opts->h <= 0)
+ priv->opts->h = in->h;
+
+ int w = priv->opts->w;
+ int h = priv->opts->h;
+
+ struct vo_frame frame = {
+ .pts = in->pts,
+ .duration = -1,
+ .num_vsyncs = 1,
+ .current = in,
+ .num_frames = 1,
+ .frames = {in},
+ .frame_id = ++(priv->next_frame_id),
+ };
+
+ bool need_reconfig = m_config_cache_update(priv->vo_opts_cache);
+
+ if (!mp_image_params_equal(&priv->img_params, &in->params)) {
+ priv->img_params = in->params;
+ gl_video_config(priv->renderer, &in->params);
+ need_reconfig = true;
+ }
+
+ if (need_reconfig) {
+ struct mp_rect src, dst;
+ struct mp_osd_res osd;
+
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ struct osd_state *osd_state = info ? info->osd : NULL;
+ if (osd_state) {
+ osd_set_render_subs_in_filter(osd_state, true);
+ // Assume the osd_state doesn't somehow disappear.
+ gl_video_set_osd_source(priv->renderer, osd_state);
+ }
+
+ mp_get_src_dst_rects(f->log, priv->vo_opts, VO_CAP_ROTATE90, &in->params,
+ w, h, 1, &src, &dst, &osd);
+
+ gl_video_resize(priv->renderer, &src, &dst, &osd);
+ }
+
+ if (!priv->target) {
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .downloadable = true,
+ .w = w,
+ .h = h,
+ .d = 1,
+ .render_dst = true,
+ };
+
+ params.format = ra_find_unorm_format(ra, 1, 4);
+
+ if (!params.format || !params.format->renderable)
+ goto done;
+
+ priv->target = ra_tex_create(ra, &params);
+ if (!priv->target)
+ goto done;
+ }
+
+ // (it doesn't have access to the OSD though)
+ int flags = RENDER_FRAME_SUBS | RENDER_FRAME_VF_SUBS;
+ gl_video_render_frame(priv->renderer, &frame, (struct ra_fbo){priv->target},
+ flags);
+
+ res = mp_image_alloc(IMGFMT_RGB0, w, h);
+ if (!res)
+ goto done;
+
+ struct ra_tex_download_params download_params = {
+ .tex = priv->target,
+ .dst = res->planes[0],
+ .stride = res->stride[0],
+ };
+ if (!ra->fns->tex_download(ra, &download_params))
+ goto done;
+
+ ok = true;
+done:
+ if (!ok)
+ TA_FREEP(&res);
+ return res;
+}
+
+static void gpu_process(struct mp_filter *f)
+{
+ struct priv *priv = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (mp_frame_is_signaling(frame)) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+
+ if (frame.type != MP_FRAME_VIDEO)
+ goto error;
+
+ offscreen_ctx_set_current(priv->ctx, true);
+
+ struct mp_image *mpi = frame.data;
+ struct mp_image *res = gpu_render_frame(f, mpi);
+ if (!res) {
+ MP_ERR(f, "Could not render or retrieve frame.\n");
+ goto error;
+ }
+
+ // It's not clear which parameters to copy.
+ res->pts = mpi->pts;
+ res->dts = mpi->dts;
+ res->nominal_fps = mpi->nominal_fps;
+
+ talloc_free(mpi);
+
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_VIDEO, res));
+ return;
+
+error:
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+ offscreen_ctx_set_current(priv->ctx, false);
+}
+
+static void gpu_reset(struct mp_filter *f)
+{
+ struct priv *priv = f->priv;
+
+ offscreen_ctx_set_current(priv->ctx, true);
+ gl_video_reset(priv->renderer);
+ offscreen_ctx_set_current(priv->ctx, false);
+}
+
+static void gpu_destroy(struct mp_filter *f)
+{
+ struct priv *priv = f->priv;
+
+ if (priv->ctx) {
+ offscreen_ctx_set_current(priv->ctx, true);
+
+ gl_video_uninit(priv->renderer);
+ ra_tex_free(priv->ctx->ra, &priv->target);
+
+ offscreen_ctx_set_current(priv->ctx, false);
+ }
+
+ talloc_free(priv->ctx);
+}
+
+static const struct mp_filter_info gpu_filter = {
+ .name = "gpu",
+ .process = gpu_process,
+ .reset = gpu_reset,
+ .destroy = gpu_destroy,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *gpu_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &gpu_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *priv = f->priv;
+ priv->opts = talloc_steal(priv, options);
+ priv->vo_opts_cache = m_config_cache_alloc(f, f->global, &vo_sub_opts);
+ priv->vo_opts = priv->vo_opts_cache->opts;
+
+ priv->ctx = gl_offscreen_ctx_create(f->global, f->log);
+ if (!priv->ctx) {
+ MP_FATAL(f, "Could not create offscreen ra context.\n");
+ goto error;
+ }
+
+ if (!priv->ctx->ra->fns->tex_download) {
+ MP_FATAL(f, "Offscreen ra context does not support image retrieval.\n");
+ goto error;
+ }
+
+ offscreen_ctx_set_current(priv->ctx, true);
+
+ priv->renderer = gl_video_init(priv->ctx->ra, f->log, f->global);
+ assert(priv->renderer); // can't fail (strangely)
+
+ offscreen_ctx_set_current(priv->ctx, false);
+
+ MP_WARN(f, "This is experimental. Keep in mind:\n");
+ MP_WARN(f, " - OSD rendering is done in software.\n");
+ MP_WARN(f, " - Encoding will convert the RGB output to yuv420p in software.\n");
+ MP_WARN(f, " - Using this with --vo=gpu will filter the video twice!\n");
+ MP_WARN(f, " (And you can't prevent this; they use the same options.)\n");
+ MP_WARN(f, " - Some features are simply not supported.\n");
+
+ return f;
+
+error:
+ talloc_free(f);
+ return NULL;
+}
+
+#define OPT_BASE_STRUCT struct gpu_opts
+const struct mp_user_filter_entry vf_gpu = {
+ .desc = {
+ .description = "vo_gpu as filter",
+ .name = "gpu",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = (const struct m_option[]){
+ {"w", OPT_INT(w)},
+ {"h", OPT_INT(h)},
+ {0}
+ },
+ },
+ .create = gpu_create,
+};
diff --git a/video/filter/vf_sub.c b/video/filter/vf_sub.c
new file mode 100644
index 0000000..de7f787
--- /dev/null
+++ b/video/filter/vf_sub.c
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <libavutil/common.h>
+
+#include "common/msg.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "options/options.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+#include "sub/osd.h"
+#include "sub/dec_sub.h"
+
+#include "video/sws_utils.h"
+
+#include "options/m_option.h"
+
+struct vf_sub_opts {
+ int top_margin, bottom_margin;
+};
+
+struct priv {
+ struct vf_sub_opts *opts;
+ struct mp_image_pool *pool;
+};
+
+static void vf_sub_process(struct mp_filter *f)
+{
+ struct priv *priv = f->priv;
+
+ if (!mp_pin_can_transfer_data(f->ppins[1], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+
+ if (mp_frame_is_signaling(frame)) {
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+ }
+
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ struct osd_state *osd = info ? info->osd : NULL;
+
+ if (!osd)
+ goto error;
+
+ osd_set_render_subs_in_filter(osd, true);
+
+ if (frame.type != MP_FRAME_VIDEO)
+ goto error;
+
+ struct mp_image *mpi = frame.data;
+
+ struct mp_osd_res dim = {
+ .w = mpi->w,
+ .h = mpi->h + priv->opts->top_margin + priv->opts->bottom_margin,
+ .mt = priv->opts->top_margin,
+ .mb = priv->opts->bottom_margin,
+ .display_par = mpi->params.p_w / (double)mpi->params.p_h,
+ };
+
+ if (dim.w != mpi->w || dim.h != mpi->h) {
+ struct mp_image *dmpi =
+ mp_image_pool_get(priv->pool, mpi->imgfmt, dim.w, dim.h);
+ if (!dmpi)
+ goto error;
+ mp_image_copy_attributes(dmpi, mpi);
+ int y1 = MP_ALIGN_DOWN(priv->opts->top_margin, mpi->fmt.align_y);
+ int y2 = MP_ALIGN_DOWN(y1 + mpi->h, mpi->fmt.align_y);
+ struct mp_image cropped = *dmpi;
+ mp_image_crop(&cropped, 0, y1, mpi->w, y1 + mpi->h);
+ mp_image_copy(&cropped, mpi);
+ mp_image_clear(dmpi, 0, 0, dmpi->w, y1);
+ mp_image_clear(dmpi, 0, y2, dmpi->w, dim.h);
+ mp_frame_unref(&frame);
+ mpi = dmpi;
+ frame = (struct mp_frame){MP_FRAME_VIDEO, mpi};
+ }
+
+ osd_draw_on_image_p(osd, dim, mpi->pts, OSD_DRAW_SUB_FILTER, priv->pool, mpi);
+
+ mp_pin_in_write(f->ppins[1], frame);
+ return;
+
+error:
+ MP_ERR(f, "unsupported format, missing OSD, or failed allocation\n");
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+}
+
+static void vf_sub_destroy(struct mp_filter *f)
+{
+ struct mp_stream_info *info = mp_filter_find_stream_info(f);
+ struct osd_state *osd = info ? info->osd : NULL;
+ if (osd)
+ osd_set_render_subs_in_filter(osd, false);
+}
+
+static const struct mp_filter_info vf_sub_filter = {
+ .name = "sub",
+ .process = vf_sub_process,
+ .destroy = vf_sub_destroy,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *vf_sub_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &vf_sub_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *priv = f->priv;
+ priv->opts = talloc_steal(priv, options);
+ priv->pool = mp_image_pool_new(priv);
+
+ return f;
+}
+
+#define OPT_BASE_STRUCT struct vf_sub_opts
+static const m_option_t vf_opts_fields[] = {
+ {"bottom-margin", OPT_INT(bottom_margin), M_RANGE(0, 2000)},
+ {"top-margin", OPT_INT(top_margin), M_RANGE(0, 2000)},
+ {0}
+};
+
+const struct mp_user_filter_entry vf_sub = {
+ .desc = {
+ .description = "Render subtitles",
+ .name = "sub",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = vf_opts_fields,
+ },
+ .create = vf_sub_create,
+};
diff --git a/video/filter/vf_vapoursynth.c b/video/filter/vf_vapoursynth.c
new file mode 100644
index 0000000..583a196
--- /dev/null
+++ b/video/filter/vf_vapoursynth.c
@@ -0,0 +1,892 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <assert.h>
+
+#include <VapourSynth.h>
+#include <VSHelper.h>
+
+#include <libavutil/rational.h>
+#include <libavutil/cpu.h>
+
+#include "common/msg.h"
+#include "filters/f_autoconvert.h"
+#include "filters/f_utils.h"
+#include "filters/filter_internal.h"
+#include "filters/filter.h"
+#include "filters/user_filters.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "osdep/threads.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+
+struct vapoursynth_opts {
+ char *file;
+ int maxbuffer;
+ int maxrequests;
+
+ const struct script_driver *drv;
+};
+
+struct priv {
+ struct mp_log *log;
+ struct vapoursynth_opts *opts;
+ char *script_path;
+
+ VSCore *vscore;
+ const VSAPI *vsapi;
+ VSNodeRef *out_node;
+ VSNodeRef *in_node;
+
+ const struct script_driver *drv;
+ // drv_vss
+ bool vs_initialized;
+ struct VSScript *se;
+
+ struct mp_filter *f;
+ struct mp_pin *in_pin;
+
+ // Format for which VS is currently configured.
+ struct mp_image_params fmt_in;
+
+ mp_mutex lock;
+ mp_cond wakeup;
+
+ // --- the following members are all protected by lock
+ struct mp_image **buffered; // oldest image first
+ int num_buffered;
+ int in_frameno; // frame number of buffered[0] (the oldest)
+ int requested_frameno; // last frame number for which we woke up core
+ int out_frameno; // frame number of first requested/ready frame
+ double out_pts; // pts corresponding to first requested/ready frame
+ struct mp_image **requested;// frame callback results (can point to dummy_img)
+ // requested[0] is the frame to return first
+ int max_requests; // upper bound for requested[] array
+ bool failed; // frame callback returned with an error
+ bool shutdown; // ask node to return
+ bool eof; // drain remaining data
+ int64_t frames_sent; // total nr. of frames ever added to input queue
+ bool initializing; // filters are being built
+ bool in_node_active; // node might still be called
+};
+
+// priv->requested[n] points to this if a request for frame n is in-progress
+static const struct mp_image dummy_img;
+// or if a request failed during EOF/reinit draining
+static const struct mp_image dummy_img_eof;
+
+static void destroy_vs(struct priv *p);
+static int reinit_vs(struct priv *p, struct mp_image *input);
+
+struct script_driver {
+ int (*init)(struct priv *p); // first time init
+ void (*uninit)(struct priv *p); // last time uninit
+ int (*load_core)(struct priv *p); // make vsapi/vscore available
+ int (*load)(struct priv *p, VSMap *vars); // also sets p->out_node
+ void (*unload)(struct priv *p); // unload script and maybe vs
+};
+
+struct mpvs_fmt {
+ VSPresetFormat vs;
+ int bits, xs, ys;
+};
+
+static const struct mpvs_fmt mpvs_fmt_table[] = {
+ {pfYUV420P8, 8, 1, 1},
+ {pfYUV420P9, 9, 1, 1},
+ {pfYUV420P10, 10, 1, 1},
+ {pfYUV420P16, 16, 1, 1},
+ {pfYUV422P8, 8, 1, 0},
+ {pfYUV422P9, 9, 1, 0},
+ {pfYUV422P10, 10, 1, 0},
+ {pfYUV422P16, 16, 1, 0},
+ {pfYUV410P8, 8, 2, 2},
+ {pfYUV411P8, 8, 2, 0},
+ {pfYUV440P8, 8, 0, 1},
+ {pfYUV444P8, 8, 0, 0},
+ {pfYUV444P9, 9, 0, 0},
+ {pfYUV444P10, 10, 0, 0},
+ {pfYUV444P16, 16, 0, 0},
+ {pfNone}
+};
+
+static bool compare_fmt(int imgfmt, const struct mpvs_fmt *vs)
+{
+ struct mp_regular_imgfmt rfmt;
+ if (!mp_get_regular_imgfmt(&rfmt, imgfmt))
+ return false;
+ if (rfmt.component_pad > 0)
+ return false;
+ if (rfmt.chroma_xs != vs->xs || rfmt.chroma_ys != vs->ys)
+ return false;
+ if (rfmt.component_size * 8 + rfmt.component_pad != vs->bits)
+ return false;
+ if (rfmt.num_planes != 3)
+ return false;
+ for (int n = 0; n < 3; n++) {
+ if (rfmt.planes[n].num_components != 1)
+ return false;
+ if (rfmt.planes[n].components[0] != n + 1)
+ return false;
+ }
+ return true;
+}
+
+static VSPresetFormat mp_to_vs(int imgfmt)
+{
+ for (int n = 0; mpvs_fmt_table[n].bits; n++) {
+ const struct mpvs_fmt *vsentry = &mpvs_fmt_table[n];
+ if (compare_fmt(imgfmt, vsentry))
+ return vsentry->vs;
+ }
+ return pfNone;
+}
+
+static int mp_from_vs(VSPresetFormat vs)
+{
+ for (int n = 0; mpvs_fmt_table[n].bits; n++) {
+ const struct mpvs_fmt *vsentry = &mpvs_fmt_table[n];
+ if (vsentry->vs == vs) {
+ for (int imgfmt = IMGFMT_START; imgfmt < IMGFMT_END; imgfmt++) {
+ if (compare_fmt(imgfmt, vsentry))
+ return imgfmt;
+ }
+ break;
+ }
+ }
+ return 0;
+}
+
+static void copy_mp_to_vs_frame_props_map(struct priv *p, VSMap *map,
+ struct mp_image *img)
+{
+ struct mp_image_params *params = &img->params;
+ p->vsapi->propSetInt(map, "_SARNum", params->p_w, 0);
+ p->vsapi->propSetInt(map, "_SARDen", params->p_h, 0);
+ if (params->color.levels) {
+ p->vsapi->propSetInt(map, "_ColorRange",
+ params->color.levels == MP_CSP_LEVELS_TV, 0);
+ }
+ // The docs explicitly say it uses libavcodec values.
+ p->vsapi->propSetInt(map, "_ColorSpace",
+ mp_csp_to_avcol_spc(params->color.space), 0);
+ if (params->chroma_location) {
+ p->vsapi->propSetInt(map, "_ChromaLocation",
+ params->chroma_location == MP_CHROMA_CENTER, 0);
+ }
+ char pict_type = 0;
+ switch (img->pict_type) {
+ case 1: pict_type = 'I'; break;
+ case 2: pict_type = 'P'; break;
+ case 3: pict_type = 'B'; break;
+ }
+ if (pict_type)
+ p->vsapi->propSetData(map, "_PictType", &pict_type, 1, 0);
+ int field = 0;
+ if (img->fields & MP_IMGFIELD_INTERLACED)
+ field = img->fields & MP_IMGFIELD_TOP_FIRST ? 2 : 1;
+ p->vsapi->propSetInt(map, "_FieldBased", field, 0);
+}
+
+static int set_vs_frame_props(struct priv *p, VSFrameRef *frame,
+ struct mp_image *img, int dur_num, int dur_den)
+{
+ VSMap *map = p->vsapi->getFramePropsRW(frame);
+ if (!map)
+ return -1;
+ p->vsapi->propSetInt(map, "_DurationNum", dur_num, 0);
+ p->vsapi->propSetInt(map, "_DurationDen", dur_den, 0);
+ copy_mp_to_vs_frame_props_map(p, map, img);
+ return 0;
+}
+
+static VSFrameRef *alloc_vs_frame(struct priv *p, struct mp_image_params *fmt)
+{
+ const VSFormat *vsfmt =
+ p->vsapi->getFormatPreset(mp_to_vs(fmt->imgfmt), p->vscore);
+ return p->vsapi->newVideoFrame(vsfmt, fmt->w, fmt->h, NULL, p->vscore);
+}
+
+static struct mp_image map_vs_frame(struct priv *p, const VSFrameRef *ref,
+ bool w)
+{
+ const VSFormat *fmt = p->vsapi->getFrameFormat(ref);
+
+ struct mp_image img = {0};
+ mp_image_setfmt(&img, mp_from_vs(fmt->id));
+ mp_image_set_size(&img, p->vsapi->getFrameWidth(ref, 0),
+ p->vsapi->getFrameHeight(ref, 0));
+
+ for (int n = 0; n < img.num_planes; n++) {
+ if (w) {
+ img.planes[n] = p->vsapi->getWritePtr((VSFrameRef *)ref, n);
+ } else {
+ img.planes[n] = (uint8_t *)p->vsapi->getReadPtr(ref, n);
+ }
+ img.stride[n] = p->vsapi->getStride(ref, n);
+ }
+
+ return img;
+}
+
+static void drain_oldest_buffered_frame(struct priv *p)
+{
+ if (!p->num_buffered)
+ return;
+ talloc_free(p->buffered[0]);
+ for (int n = 0; n < p->num_buffered - 1; n++)
+ p->buffered[n] = p->buffered[n + 1];
+ p->num_buffered--;
+ p->in_frameno++;
+}
+
+static void VS_CC vs_frame_done(void *userData, const VSFrameRef *f, int n,
+ VSNodeRef *node, const char *errorMsg)
+{
+ struct priv *p = userData;
+
+ struct mp_image *res = NULL;
+ if (f) {
+ struct mp_image img = map_vs_frame(p, f, false);
+ struct mp_image dummy = {.params = p->fmt_in};
+ if (p->fmt_in.w != img.w || p->fmt_in.h != img.h)
+ dummy.params.crop = (struct mp_rect){0, 0, img.w, img.h};
+ mp_image_copy_attributes(&img, &dummy);
+ img.pkt_duration = -1;
+ const VSMap *map = p->vsapi->getFramePropsRO(f);
+ if (map) {
+ int err1, err2;
+ int num = p->vsapi->propGetInt(map, "_DurationNum", 0, &err1);
+ int den = p->vsapi->propGetInt(map, "_DurationDen", 0, &err2);
+ if (!err1 && !err2)
+ img.pkt_duration = num / (double)den;
+ }
+ if (img.pkt_duration < 0) {
+ MP_ERR(p, "No PTS after filter at frame %d!\n", n);
+ } else {
+ img.nominal_fps = 1.0 / img.pkt_duration;
+ }
+ res = mp_image_new_copy(&img);
+ p->vsapi->freeFrame(f);
+ }
+
+ mp_mutex_lock(&p->lock);
+
+ // If these assertions fail, n is an unrequested frame (or filtered twice).
+ assert(n >= p->out_frameno && n < p->out_frameno + p->max_requests);
+ int index = n - p->out_frameno;
+ MP_TRACE(p, "filtered frame %d (%d)\n", n, index);
+ assert(p->requested[index] == &dummy_img);
+
+ if (!res && !p->shutdown) {
+ if (p->eof) {
+ res = (struct mp_image *)&dummy_img_eof;
+ } else {
+ p->failed = true;
+ MP_ERR(p, "Filter error at frame %d: %s\n", n, errorMsg);
+ }
+ }
+ p->requested[index] = res;
+ mp_cond_broadcast(&p->wakeup);
+ mp_mutex_unlock(&p->lock);
+ mp_filter_wakeup(p->f);
+}
+
+static void vf_vapoursynth_process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ mp_mutex_lock(&p->lock);
+
+ if (p->failed) {
+ // Not sure what we do on errors, but at least don't deadlock.
+ MP_ERR(f, "failed, no action taken\n");
+ mp_filter_internal_mark_failed(f);
+ goto done;
+ }
+
+ // Read input and pass it to the input queue VS reads.
+ while (p->num_buffered < MP_TALLOC_AVAIL(p->buffered) && !p->eof) {
+ // Note: this requests new input frames even if no output was ever
+ // requested. Normally this is not how mp_filter works, but since VS
+ // works asynchronously, it's probably ok.
+ struct mp_frame frame = mp_pin_out_read(p->in_pin);
+ if (frame.type == MP_FRAME_EOF) {
+ if (p->out_node && !p->eof) {
+ MP_VERBOSE(p, "initiate EOF\n");
+ p->eof = true;
+ mp_cond_broadcast(&p->wakeup);
+ }
+ if (!p->out_node && mp_pin_in_needs_data(f->ppins[1])) {
+ MP_VERBOSE(p, "return EOF\n");
+ mp_pin_in_write(f->ppins[1], frame);
+ } else {
+ // Keep it until we can propagate it.
+ mp_pin_out_unread(p->in_pin, frame);
+ break;
+ }
+ } else if (frame.type == MP_FRAME_VIDEO) {
+ struct mp_image *mpi = frame.data;
+ // Init VS script, or reinit it to change video format. (This
+ // includes derived parameters we pass manually to the script.)
+ if (!p->out_node || mpi->imgfmt != p->fmt_in.imgfmt ||
+ mpi->w != p->fmt_in.w || mpi->h != p->fmt_in.h ||
+ mpi->params.p_w != p->fmt_in.p_w ||
+ mpi->params.p_h != p->fmt_in.p_h)
+ {
+ if (p->out_node) {
+ // Drain still buffered frames.
+ MP_VERBOSE(p, "draining VS for format change\n");
+ mp_pin_out_unread(p->in_pin, frame);
+ p->eof = true;
+ mp_cond_broadcast(&p->wakeup);
+ mp_filter_internal_mark_progress(f);
+ goto done;
+ }
+ mp_mutex_unlock(&p->lock);
+ if (p->out_node)
+ destroy_vs(p);
+ p->fmt_in = mpi->params;
+ if (reinit_vs(p, mpi) < 0) {
+ MP_ERR(p, "could not init VS\n");
+ mp_frame_unref(&frame);
+ mp_filter_internal_mark_failed(f);
+ return;
+ }
+ mp_mutex_lock(&p->lock);
+ }
+ if (p->out_pts == MP_NOPTS_VALUE)
+ p->out_pts = mpi->pts;
+ p->frames_sent++;
+ p->buffered[p->num_buffered++] = mpi;
+ mp_cond_broadcast(&p->wakeup);
+ } else if (frame.type != MP_FRAME_NONE) {
+ MP_ERR(p, "discarding unknown frame type\n");
+ mp_frame_unref(&frame);
+ goto done;
+ } else {
+ break; // no new data available
+ }
+ }
+
+ // Read output and return them from the VS output queue.
+ if (mp_pin_in_needs_data(f->ppins[1]) && p->requested[0] &&
+ p->requested[0] != &dummy_img &&
+ p->requested[0] != &dummy_img_eof)
+ {
+ struct mp_image *out = p->requested[0];
+
+ out->pts = p->out_pts;
+ if (p->out_pts != MP_NOPTS_VALUE && out->pkt_duration >= 0)
+ p->out_pts += out->pkt_duration;
+
+ mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_VIDEO, out));
+
+ for (int n = 0; n < p->max_requests - 1; n++)
+ p->requested[n] = p->requested[n + 1];
+ p->requested[p->max_requests - 1] = NULL;
+ p->out_frameno++;
+ }
+
+ // This happens on EOF draining and format changes.
+ if (p->requested[0] == &dummy_img_eof) {
+ MP_VERBOSE(p, "finishing up\n");
+ assert(p->eof);
+ mp_mutex_unlock(&p->lock);
+ destroy_vs(p);
+ mp_filter_internal_mark_progress(f);
+ return;
+ }
+
+ // Don't request frames if we haven't sent any input yet.
+ if (p->frames_sent && p->out_node) {
+ // Request new future frames as far as possible.
+ for (int n = 0; n < p->max_requests; n++) {
+ if (!p->requested[n]) {
+ // Note: this assumes getFrameAsync() will never call
+ // infiltGetFrame (if it does, we would deadlock)
+ p->requested[n] = (struct mp_image *)&dummy_img;
+ p->failed = false;
+ MP_TRACE(p, "requesting frame %d (%d)\n", p->out_frameno + n, n);
+ p->vsapi->getFrameAsync(p->out_frameno + n, p->out_node,
+ vs_frame_done, p);
+ }
+ }
+ }
+
+done:
+ mp_mutex_unlock(&p->lock);
+}
+
+static void VS_CC infiltInit(VSMap *in, VSMap *out, void **instanceData,
+ VSNode *node, VSCore *core, const VSAPI *vsapi)
+{
+ struct priv *p = *instanceData;
+ // The number of frames of our input node is obviously unknown. The user
+ // could for example seek any time, randomly "ending" the clip.
+ // This specific value was suggested by the VapourSynth developer.
+ int enough_for_everyone = INT_MAX / 16;
+
+ // Note: this is called from createFilter, so no need for locking.
+
+ VSVideoInfo fmt = {
+ .format = p->vsapi->getFormatPreset(mp_to_vs(p->fmt_in.imgfmt), p->vscore),
+ .width = p->fmt_in.w,
+ .height = p->fmt_in.h,
+ .numFrames = enough_for_everyone,
+ };
+ if (!fmt.format) {
+ p->vsapi->setError(out, "Unsupported input format.\n");
+ return;
+ }
+
+ p->vsapi->setVideoInfo(&fmt, 1, node);
+ p->in_node_active = true;
+}
+
+static const VSFrameRef *VS_CC infiltGetFrame(int frameno, int activationReason,
+ void **instanceData, void **frameData,
+ VSFrameContext *frameCtx, VSCore *core,
+ const VSAPI *vsapi)
+{
+ struct priv *p = *instanceData;
+ VSFrameRef *ret = NULL;
+
+ mp_mutex_lock(&p->lock);
+ MP_TRACE(p, "VS asking for frame %d (at %d)\n", frameno, p->in_frameno);
+ while (1) {
+ if (p->shutdown) {
+ p->vsapi->setFilterError("EOF or filter reset/uninit", frameCtx);
+ MP_DBG(p, "returning error on reset/uninit\n");
+ break;
+ }
+ if (p->initializing) {
+ MP_WARN(p, "Frame requested during init! This is unsupported.\n"
+ "Returning black dummy frame with 0 duration.\n");
+ ret = alloc_vs_frame(p, &p->fmt_in);
+ if (!ret) {
+ p->vsapi->setFilterError("Could not allocate VS frame", frameCtx);
+ break;
+ }
+ struct mp_image vsframe = map_vs_frame(p, ret, true);
+ mp_image_clear(&vsframe, 0, 0, p->fmt_in.w, p->fmt_in.h);
+ struct mp_image dummy = {0};
+ mp_image_set_params(&dummy, &p->fmt_in);
+ set_vs_frame_props(p, ret, &dummy, 0, 1);
+ break;
+ }
+ if (frameno < p->in_frameno) {
+ char msg[180];
+ snprintf(msg, sizeof(msg),
+ "Frame %d requested, but only have frames starting from %d. "
+ "Try increasing the buffered-frames suboption.",
+ frameno, p->in_frameno);
+ MP_FATAL(p, "%s\n", msg);
+ p->vsapi->setFilterError(msg, frameCtx);
+ break;
+ }
+ if (frameno >= p->in_frameno + MP_TALLOC_AVAIL(p->buffered)) {
+ // Too far in the future. Remove frames, so that the main thread can
+ // queue new frames.
+ if (p->num_buffered) {
+ drain_oldest_buffered_frame(p);
+ mp_cond_broadcast(&p->wakeup);
+ mp_filter_wakeup(p->f);
+ continue;
+ }
+ }
+ if (frameno >= p->in_frameno + p->num_buffered) {
+ // If there won't be any new frames, abort the request.
+ if (p->eof) {
+ p->vsapi->setFilterError("EOF or filter EOF/reinit", frameCtx);
+ MP_DBG(p, "returning error on EOF/reinit\n");
+ break;
+ }
+ // Request more frames.
+ if (p->requested_frameno <= p->in_frameno + p->num_buffered) {
+ p->requested_frameno = p->in_frameno + p->num_buffered + 1;
+ mp_filter_wakeup(p->f);
+ }
+ } else {
+ struct mp_image *img = p->buffered[frameno - p->in_frameno];
+ ret = alloc_vs_frame(p, &img->params);
+ if (!ret) {
+ p->vsapi->setFilterError("Could not allocate VS frame", frameCtx);
+ break;
+ }
+
+ mp_mutex_unlock(&p->lock);
+ struct mp_image vsframe = map_vs_frame(p, ret, true);
+ mp_image_copy(&vsframe, img);
+ int res = 1e6;
+ int dur = img->pkt_duration * res + 0.5;
+ set_vs_frame_props(p, ret, img, dur, res);
+ mp_mutex_lock(&p->lock);
+ break;
+ }
+ mp_cond_wait(&p->wakeup, &p->lock);
+ }
+ mp_cond_broadcast(&p->wakeup);
+ mp_mutex_unlock(&p->lock);
+ return ret;
+}
+
+static void VS_CC infiltFree(void *instanceData, VSCore *core, const VSAPI *vsapi)
+{
+ struct priv *p = instanceData;
+
+ mp_mutex_lock(&p->lock);
+ p->in_node_active = false;
+ mp_cond_broadcast(&p->wakeup);
+ mp_mutex_unlock(&p->lock);
+}
+
+// number of getAsyncFrame calls in progress
+// must be called with p->lock held
+static int num_requested(struct priv *p)
+{
+ int r = 0;
+ for (int n = 0; n < p->max_requests; n++)
+ r += p->requested[n] == &dummy_img;
+ return r;
+}
+
+static void destroy_vs(struct priv *p)
+{
+ if (!p->out_node && !p->initializing)
+ return;
+
+ MP_DBG(p, "destroying VS filters\n");
+
+ // Wait until our frame callbacks return.
+ mp_mutex_lock(&p->lock);
+ p->initializing = false;
+ p->shutdown = true;
+ mp_cond_broadcast(&p->wakeup);
+ while (num_requested(p))
+ mp_cond_wait(&p->wakeup, &p->lock);
+ mp_mutex_unlock(&p->lock);
+
+ MP_DBG(p, "all requests terminated\n");
+
+ if (p->in_node)
+ p->vsapi->freeNode(p->in_node);
+ if (p->out_node)
+ p->vsapi->freeNode(p->out_node);
+ p->in_node = p->out_node = NULL;
+
+ p->drv->unload(p);
+
+ assert(!p->in_node_active);
+ assert(num_requested(p) == 0); // async callback didn't return?
+
+ p->shutdown = false;
+ p->eof = false;
+ p->frames_sent = 0;
+ // Kill filtered images that weren't returned yet
+ for (int n = 0; n < p->max_requests; n++) {
+ if (p->requested[n] != &dummy_img_eof)
+ mp_image_unrefp(&p->requested[n]);
+ p->requested[n] = NULL;
+ }
+ // Kill queued frames too
+ for (int n = 0; n < p->num_buffered; n++)
+ talloc_free(p->buffered[n]);
+ p->num_buffered = 0;
+ p->out_frameno = p->in_frameno = 0;
+ p->requested_frameno = 0;
+ p->failed = false;
+
+ MP_DBG(p, "uninitialized.\n");
+}
+
+static int reinit_vs(struct priv *p, struct mp_image *input)
+{
+ VSMap *vars = NULL, *in = NULL, *out = NULL;
+ int res = -1;
+
+ destroy_vs(p);
+
+ MP_DBG(p, "initializing...\n");
+
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p->fmt_in.imgfmt);
+ if (p->fmt_in.w % desc.align_x || p->fmt_in.h % desc.align_y) {
+ MP_FATAL(p, "VapourSynth does not allow unaligned/cropped video sizes.\n");
+ return -1;
+ }
+
+ p->initializing = true;
+ p->out_pts = MP_NOPTS_VALUE;
+
+ if (p->drv->load_core(p) < 0 || !p->vsapi || !p->vscore) {
+ MP_FATAL(p, "Could not get vapoursynth API handle.\n");
+ goto error;
+ }
+
+ in = p->vsapi->createMap();
+ out = p->vsapi->createMap();
+ vars = p->vsapi->createMap();
+ if (!in || !out || !vars)
+ goto error;
+
+ p->vsapi->createFilter(in, out, "Input", infiltInit, infiltGetFrame,
+ infiltFree, fmSerial, 0, p, p->vscore);
+ int vserr;
+ p->in_node = p->vsapi->propGetNode(out, "clip", 0, &vserr);
+ if (!p->in_node) {
+ MP_FATAL(p, "Could not get our own input node.\n");
+ goto error;
+ }
+
+ if (p->vsapi->propSetNode(vars, "video_in", p->in_node, 0))
+ goto error;
+
+ int d_w, d_h;
+ mp_image_params_get_dsize(&p->fmt_in, &d_w, &d_h);
+
+ p->vsapi->propSetInt(vars, "video_in_dw", d_w, 0);
+ p->vsapi->propSetInt(vars, "video_in_dh", d_h, 0);
+
+ struct mp_stream_info *info = mp_filter_find_stream_info(p->f);
+ double container_fps = input->nominal_fps;
+ double display_fps = 0;
+ int64_t display_res[2] = {0};
+ if (info) {
+ if (info->get_display_fps)
+ display_fps = info->get_display_fps(info);
+ if (info->get_display_res) {
+ int tmp[2] = {0};
+ info->get_display_res(info, tmp);
+ display_res[0] = tmp[0];
+ display_res[1] = tmp[1];
+ }
+ }
+ p->vsapi->propSetFloat(vars, "container_fps", container_fps, 0);
+ p->vsapi->propSetFloat(vars, "display_fps", display_fps, 0);
+ p->vsapi->propSetIntArray(vars, "display_res", display_res, 2);
+
+ if (p->drv->load(p, vars) < 0)
+ goto error;
+ if (!p->out_node) {
+ MP_FATAL(p, "Could not get script output node.\n");
+ goto error;
+ }
+
+ const VSVideoInfo *vi = p->vsapi->getVideoInfo(p->out_node);
+ if (!mp_from_vs(vi->format->id)) {
+ MP_FATAL(p, "Unsupported output format.\n");
+ goto error;
+ }
+
+ mp_mutex_lock(&p->lock);
+ p->initializing = false;
+ mp_mutex_unlock(&p->lock);
+ MP_DBG(p, "initialized.\n");
+ res = 0;
+error:
+ if (p->vsapi) {
+ p->vsapi->freeMap(in);
+ p->vsapi->freeMap(out);
+ p->vsapi->freeMap(vars);
+ }
+ if (res < 0)
+ destroy_vs(p);
+ return res;
+}
+
+static void vf_vapoursynth_reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ destroy_vs(p);
+}
+
+static void vf_vapoursynth_destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ destroy_vs(p);
+ p->drv->uninit(p);
+
+ mp_cond_destroy(&p->wakeup);
+ mp_mutex_destroy(&p->lock);
+
+ mp_filter_free_children(f);
+}
+
+static const struct mp_filter_info vf_vapoursynth_filter = {
+ .name = "vapoursynth",
+ .process = vf_vapoursynth_process,
+ .reset = vf_vapoursynth_reset,
+ .destroy = vf_vapoursynth_destroy,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *vf_vapoursynth_create(struct mp_filter *parent,
+ void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &vf_vapoursynth_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ // In theory, we could allow multiple inputs and outputs, but since this
+ // wrapper is for --vf only, we don't.
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+ p->log = f->log;
+ p->drv = p->opts->drv;
+ p->f = f;
+
+ mp_mutex_init(&p->lock);
+ mp_cond_init(&p->wakeup);
+
+ if (!p->opts->file || !p->opts->file[0]) {
+ MP_FATAL(p, "'file' parameter must be set.\n");
+ goto error;
+ }
+ p->script_path = mp_get_user_path(p, f->global, p->opts->file);
+
+ p->max_requests = p->opts->maxrequests;
+ if (p->max_requests < 0)
+ p->max_requests = av_cpu_count();
+ MP_VERBOSE(p, "using %d concurrent requests.\n", p->max_requests);
+ int maxbuffer = p->opts->maxbuffer * p->max_requests;
+ p->buffered = talloc_array(p, struct mp_image *, maxbuffer);
+ p->requested = talloc_zero_array(p, struct mp_image *, p->max_requests);
+
+ struct mp_autoconvert *conv = mp_autoconvert_create(f);
+ if (!conv)
+ goto error;
+
+ for (int n = 0; mpvs_fmt_table[n].bits; n++) {
+ int imgfmt = mp_from_vs(mpvs_fmt_table[n].vs);
+ if (imgfmt)
+ mp_autoconvert_add_imgfmt(conv, imgfmt, 0);
+ }
+
+ struct mp_filter *dur = mp_compute_frame_duration_create(f);
+ if (!dur)
+ goto error;
+
+ mp_pin_connect(conv->f->pins[0], f->ppins[0]);
+ mp_pin_connect(dur->pins[0], conv->f->pins[1]);
+ p->in_pin = dur->pins[1];
+
+ if (p->drv->init(p) < 0)
+ goto error;
+
+ return f;
+
+error:
+ talloc_free(f);
+ return NULL;
+}
+
+
+#define OPT_BASE_STRUCT struct vapoursynth_opts
+static const m_option_t vf_opts_fields[] = {
+ {"file", OPT_STRING(file), .flags = M_OPT_FILE},
+ {"buffered-frames", OPT_INT(maxbuffer), M_RANGE(1, 9999),
+ OPTDEF_INT(4)},
+ {"concurrent-frames", OPT_CHOICE(maxrequests, {"auto", -1}),
+ M_RANGE(1, 99), OPTDEF_INT(-1)},
+ {0}
+};
+
+#include <VSScript.h>
+
+static int drv_vss_init(struct priv *p)
+{
+ if (!vsscript_init()) {
+ MP_FATAL(p, "Could not initialize VapourSynth scripting.\n");
+ return -1;
+ }
+ p->vs_initialized = true;
+ return 0;
+}
+
+static void drv_vss_uninit(struct priv *p)
+{
+ if (p->vs_initialized)
+ vsscript_finalize();
+ p->vs_initialized = false;
+}
+
+static int drv_vss_load_core(struct priv *p)
+{
+ // First load an empty script to get a VSScript, so that we get the vsapi
+ // and vscore.
+ if (vsscript_createScript(&p->se))
+ return -1;
+ p->vsapi = vsscript_getVSApi();
+ p->vscore = vsscript_getCore(p->se);
+ return 0;
+}
+
+static int drv_vss_load(struct priv *p, VSMap *vars)
+{
+ vsscript_setVariable(p->se, vars);
+
+ if (vsscript_evaluateFile(&p->se, p->script_path, 0)) {
+ MP_FATAL(p, "Script evaluation failed:\n%s\n", vsscript_getError(p->se));
+ return -1;
+ }
+ p->out_node = vsscript_getOutput(p->se, 0);
+ return 0;
+}
+
+static void drv_vss_unload(struct priv *p)
+{
+ if (p->se)
+ vsscript_freeScript(p->se);
+ p->se = NULL;
+ p->vsapi = NULL;
+ p->vscore = NULL;
+}
+
+static const struct script_driver drv_vss = {
+ .init = drv_vss_init,
+ .uninit = drv_vss_uninit,
+ .load_core = drv_vss_load_core,
+ .load = drv_vss_load,
+ .unload = drv_vss_unload,
+};
+
+const struct mp_user_filter_entry vf_vapoursynth = {
+ .desc = {
+ .description = "VapourSynth bridge",
+ .name = "vapoursynth",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .drv = &drv_vss,
+ },
+ .options = vf_opts_fields,
+ },
+ .create = vf_vapoursynth_create,
+};
diff --git a/video/filter/vf_vavpp.c b/video/filter/vf_vavpp.c
new file mode 100644
index 0000000..52be148
--- /dev/null
+++ b/video/filter/vf_vavpp.c
@@ -0,0 +1,503 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <va/va.h>
+#include <va/va_vpp.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+
+#include "options/options.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "refqueue.h"
+
+#include "video/fmt-conversion.h"
+#include "video/vaapi.h"
+#include "video/hwdec.h"
+#include "video/mp_image_pool.h"
+
+struct surface_refs {
+ VASurfaceID *surfaces;
+ int num_surfaces;
+ int max_surfaces;
+};
+
+struct pipeline {
+ VABufferID *filters;
+ int num_filters;
+ VAProcColorStandardType input_colors[VAProcColorStandardCount];
+ VAProcColorStandardType output_colors[VAProcColorStandardCount];
+ int num_input_colors, num_output_colors;
+ struct surface_refs forward, backward;
+};
+
+struct opts {
+ int deint_type;
+ bool interlaced_only;
+ bool reversal_bug;
+};
+
+struct priv {
+ struct opts *opts;
+ bool do_deint;
+ VABufferID buffers[VAProcFilterCount];
+ int num_buffers;
+ VAConfigID config;
+ VAContextID context;
+ struct mp_image_params params;
+ VADisplay display;
+ AVBufferRef *av_device_ref;
+ struct pipeline pipe;
+ AVBufferRef *hw_pool;
+
+ struct mp_refqueue *queue;
+};
+
+static void add_surfaces(struct priv *p, struct surface_refs *refs, int dir)
+{
+ for (int n = 0; n < refs->max_surfaces; n++) {
+ struct mp_image *s = mp_refqueue_get(p->queue, (1 + n) * dir);
+ if (!s)
+ break;
+ VASurfaceID id = va_surface_id(s);
+ if (id == VA_INVALID_ID)
+ break;
+ MP_TARRAY_APPEND(p, refs->surfaces, refs->num_surfaces, id);
+ }
+}
+
+// The array items must match with the "deint" suboption values.
+// They're also sorted by quality.
+static const int deint_algorithm[] = {
+ [0] = VAProcDeinterlacingNone,
+ [1] = VAProcDeinterlacingBob, // first-field, special-cased
+ [2] = VAProcDeinterlacingBob,
+ [3] = VAProcDeinterlacingWeave,
+ [4] = VAProcDeinterlacingMotionAdaptive,
+ [5] = VAProcDeinterlacingMotionCompensated,
+};
+
+static void flush_frames(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ mp_refqueue_flush(p->queue);
+}
+
+static void update_pipeline(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+ VABufferID *filters = p->buffers;
+ int num_filters = p->num_buffers;
+ if (p->opts->deint_type && !p->do_deint) {
+ filters++;
+ num_filters--;
+ }
+ p->pipe.forward.num_surfaces = p->pipe.backward.num_surfaces = 0;
+ p->pipe.num_input_colors = p->pipe.num_output_colors = 0;
+ p->pipe.num_filters = 0;
+ p->pipe.filters = NULL;
+ if (!num_filters)
+ goto nodeint;
+ VAProcPipelineCaps caps = {
+ .input_color_standards = p->pipe.input_colors,
+ .output_color_standards = p->pipe.output_colors,
+ .num_input_color_standards = VAProcColorStandardCount,
+ .num_output_color_standards = VAProcColorStandardCount,
+ };
+ VAStatus status = vaQueryVideoProcPipelineCaps(p->display, p->context,
+ filters, num_filters, &caps);
+ if (!CHECK_VA_STATUS(vf, "vaQueryVideoProcPipelineCaps()"))
+ goto nodeint;
+ p->pipe.filters = filters;
+ p->pipe.num_filters = num_filters;
+ p->pipe.num_input_colors = caps.num_input_color_standards;
+ p->pipe.num_output_colors = caps.num_output_color_standards;
+ p->pipe.forward.max_surfaces = caps.num_forward_references;
+ p->pipe.backward.max_surfaces = caps.num_backward_references;
+ if (p->opts->reversal_bug) {
+ int max = MPMAX(caps.num_forward_references, caps.num_backward_references);
+ mp_refqueue_set_refs(p->queue, max, max);
+ } else {
+ mp_refqueue_set_refs(p->queue, p->pipe.backward.max_surfaces,
+ p->pipe.forward.max_surfaces);
+ }
+ mp_refqueue_set_mode(p->queue,
+ (p->do_deint ? MP_MODE_DEINT : 0) |
+ (p->opts->deint_type >= 2 ? MP_MODE_OUTPUT_FIELDS : 0) |
+ (p->opts->interlaced_only ? MP_MODE_INTERLACED_ONLY : 0));
+ return;
+
+nodeint:
+ mp_refqueue_set_refs(p->queue, 0, 0);
+ mp_refqueue_set_mode(p->queue, 0);
+}
+
+static struct mp_image *alloc_out(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+
+ struct mp_image *fmt = mp_refqueue_get_format(p->queue);
+ if (!fmt || !fmt->hwctx)
+ return NULL;
+
+ AVHWFramesContext *hw_frames = (void *)fmt->hwctx->data;
+ // VAAPI requires the full surface size to match for input and output.
+ int src_w = hw_frames->width;
+ int src_h = hw_frames->height;
+
+ if (!mp_update_av_hw_frames_pool(&p->hw_pool, p->av_device_ref,
+ IMGFMT_VAAPI, IMGFMT_NV12, src_w, src_h,
+ false))
+ {
+ MP_ERR(vf, "Failed to create hw pool.\n");
+ return NULL;
+ }
+
+ AVFrame *av_frame = av_frame_alloc();
+ MP_HANDLE_OOM(av_frame);
+ if (av_hwframe_get_buffer(p->hw_pool, av_frame, 0) < 0) {
+ MP_ERR(vf, "Failed to allocate frame from hw pool.\n");
+ av_frame_free(&av_frame);
+ return NULL;
+ }
+ struct mp_image *img = mp_image_from_av_frame(av_frame);
+ av_frame_free(&av_frame);
+ if (!img) {
+ MP_ERR(vf, "Unknown error.\n");
+ return NULL;
+ }
+ mp_image_set_size(img, fmt->w, fmt->h);
+ return img;
+}
+
+static struct mp_image *render(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+
+ struct mp_image *in = mp_refqueue_get(p->queue, 0);
+ struct mp_image *img = NULL;
+ bool need_end_picture = false;
+ bool success = false;
+ VABufferID buffer = VA_INVALID_ID;
+
+ VASurfaceID in_id = va_surface_id(in);
+ if (!p->pipe.filters || in_id == VA_INVALID_ID)
+ goto cleanup;
+
+ img = alloc_out(vf);
+ if (!img)
+ goto cleanup;
+
+ mp_image_copy_attributes(img, in);
+
+ unsigned int flags = va_get_colorspace_flag(p->params.color.space);
+ if (!mp_refqueue_should_deint(p->queue)) {
+ flags |= VA_FRAME_PICTURE;
+ } else if (mp_refqueue_is_top_field(p->queue)) {
+ flags |= VA_TOP_FIELD;
+ } else {
+ flags |= VA_BOTTOM_FIELD;
+ }
+
+ VASurfaceID id = va_surface_id(img);
+ if (id == VA_INVALID_ID)
+ goto cleanup;
+
+ VAStatus status = vaBeginPicture(p->display, p->context, id);
+ if (!CHECK_VA_STATUS(vf, "vaBeginPicture()"))
+ goto cleanup;
+
+ need_end_picture = true;
+
+ VAProcPipelineParameterBuffer *param = NULL;
+ status = vaCreateBuffer(p->display, p->context,
+ VAProcPipelineParameterBufferType,
+ sizeof(*param), 1, NULL, &buffer);
+ if (!CHECK_VA_STATUS(vf, "vaCreateBuffer()"))
+ goto cleanup;
+
+ VAProcFilterParameterBufferDeinterlacing *filter_params;
+ status = vaMapBuffer(p->display, *(p->pipe.filters), (void**)&filter_params);
+ if (!CHECK_VA_STATUS(vf, "vaMapBuffer()"))
+ goto cleanup;
+
+ filter_params->flags = flags & VA_TOP_FIELD ? 0 : VA_DEINTERLACING_BOTTOM_FIELD;
+ if (!mp_refqueue_top_field_first(p->queue))
+ filter_params->flags |= VA_DEINTERLACING_BOTTOM_FIELD_FIRST;
+
+ vaUnmapBuffer(p->display, *(p->pipe.filters));
+
+ status = vaMapBuffer(p->display, buffer, (void**)&param);
+ if (!CHECK_VA_STATUS(vf, "vaMapBuffer()"))
+ goto cleanup;
+
+ *param = (VAProcPipelineParameterBuffer){0};
+ param->surface = in_id;
+ param->surface_region = &(VARectangle){0, 0, in->w, in->h};
+ param->output_region = &(VARectangle){0, 0, img->w, img->h};
+ param->output_background_color = 0;
+ param->filter_flags = flags;
+ param->filters = p->pipe.filters;
+ param->num_filters = p->pipe.num_filters;
+
+ int dir = p->opts->reversal_bug ? -1 : 1;
+
+ add_surfaces(p, &p->pipe.forward, 1 * dir);
+ param->forward_references = p->pipe.forward.surfaces;
+ param->num_forward_references = p->pipe.forward.num_surfaces;
+
+ add_surfaces(p, &p->pipe.backward, -1 * dir);
+ param->backward_references = p->pipe.backward.surfaces;
+ param->num_backward_references = p->pipe.backward.num_surfaces;
+
+ MP_TRACE(vf, "in=0x%x\n", (unsigned)in_id);
+ for (int n = 0; n < param->num_backward_references; n++)
+ MP_TRACE(vf, " b%d=0x%x\n", n, (unsigned)param->backward_references[n]);
+ for (int n = 0; n < param->num_forward_references; n++)
+ MP_TRACE(vf, " f%d=0x%x\n", n, (unsigned)param->forward_references[n]);
+
+ vaUnmapBuffer(p->display, buffer);
+
+ status = vaRenderPicture(p->display, p->context, &buffer, 1);
+ if (!CHECK_VA_STATUS(vf, "vaRenderPicture()"))
+ goto cleanup;
+
+ success = true;
+
+cleanup:
+ if (need_end_picture)
+ vaEndPicture(p->display, p->context);
+ vaDestroyBuffer(p->display, buffer);
+ if (success)
+ return img;
+ talloc_free(img);
+ return NULL;
+}
+
+static void vf_vavpp_process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ update_pipeline(f);
+
+ mp_refqueue_execute_reinit(p->queue);
+
+ if (!mp_refqueue_can_output(p->queue))
+ return;
+
+ if (!p->pipe.num_filters || !mp_refqueue_should_deint(p->queue)) {
+ // no filtering
+ struct mp_image *in = mp_refqueue_get(p->queue, 0);
+ mp_refqueue_write_out_pin(p->queue, mp_image_new_ref(in));
+ } else {
+ mp_refqueue_write_out_pin(p->queue, render(f));
+ }
+}
+
+static void uninit(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+ for (int i = 0; i < p->num_buffers; i++)
+ vaDestroyBuffer(p->display, p->buffers[i]);
+ if (p->context != VA_INVALID_ID)
+ vaDestroyContext(p->display, p->context);
+ if (p->config != VA_INVALID_ID)
+ vaDestroyConfig(p->display, p->config);
+ av_buffer_unref(&p->hw_pool);
+ flush_frames(vf);
+ talloc_free(p->queue);
+ av_buffer_unref(&p->av_device_ref);
+}
+
+static int va_query_filter_caps(struct mp_filter *vf, VAProcFilterType type,
+ void *caps, unsigned int count)
+{
+ struct priv *p = vf->priv;
+ VAStatus status = vaQueryVideoProcFilterCaps(p->display, p->context, type,
+ caps, &count);
+ return CHECK_VA_STATUS(vf, "vaQueryVideoProcFilterCaps()") ? count : 0;
+}
+
+static VABufferID va_create_filter_buffer(struct mp_filter *vf, int bytes,
+ int num, void *data)
+{
+ struct priv *p = vf->priv;
+ VABufferID buffer;
+ VAStatus status = vaCreateBuffer(p->display, p->context,
+ VAProcFilterParameterBufferType,
+ bytes, num, data, &buffer);
+ return CHECK_VA_STATUS(vf, "vaCreateBuffer()") ? buffer : VA_INVALID_ID;
+}
+
+static bool initialize(struct mp_filter *vf)
+{
+ struct priv *p = vf->priv;
+ VAStatus status;
+
+ VAConfigID config;
+ status = vaCreateConfig(p->display, VAProfileNone, VAEntrypointVideoProc,
+ NULL, 0, &config);
+ if (!CHECK_VA_STATUS(vf, "vaCreateConfig()")) // no entrypoint for video porc
+ return false;
+ p->config = config;
+
+ VAContextID context;
+ status = vaCreateContext(p->display, p->config, 0, 0, 0, NULL, 0, &context);
+ if (!CHECK_VA_STATUS(vf, "vaCreateContext()"))
+ return false;
+ p->context = context;
+
+ VAProcFilterType filters[VAProcFilterCount];
+ int num_filters = VAProcFilterCount;
+ status = vaQueryVideoProcFilters(p->display, p->context, filters, &num_filters);
+ if (!CHECK_VA_STATUS(vf, "vaQueryVideoProcFilters()"))
+ return false;
+
+ VABufferID buffers[VAProcFilterCount];
+ for (int i = 0; i < VAProcFilterCount; i++)
+ buffers[i] = VA_INVALID_ID;
+ for (int i = 0; i < num_filters; i++) {
+ if (filters[i] == VAProcFilterDeinterlacing) {
+ VAProcFilterCapDeinterlacing caps[VAProcDeinterlacingCount];
+ int num = va_query_filter_caps(vf, VAProcFilterDeinterlacing, caps,
+ VAProcDeinterlacingCount);
+ if (!num)
+ continue;
+ if (p->opts->deint_type < 0) {
+ for (int n = MP_ARRAY_SIZE(deint_algorithm) - 1; n > 0; n--) {
+ for (int x = 0; x < num; x++) {
+ if (caps[x].type == deint_algorithm[n]) {
+ p->opts->deint_type = n;
+ MP_VERBOSE(vf, "Selected deinterlacing algorithm: "
+ "%d\n", deint_algorithm[n]);
+ goto found;
+ }
+ }
+ }
+ found: ;
+ }
+ if (p->opts->deint_type <= 0)
+ continue;
+ VAProcDeinterlacingType algorithm =
+ deint_algorithm[p->opts->deint_type];
+ for (int n=0; n < num; n++) { // find the algorithm
+ if (caps[n].type != algorithm)
+ continue;
+ VAProcFilterParameterBufferDeinterlacing param = {0};
+ param.type = VAProcFilterDeinterlacing;
+ param.algorithm = algorithm;
+ buffers[VAProcFilterDeinterlacing] =
+ va_create_filter_buffer(vf, sizeof(param), 1, &param);
+ }
+ if (buffers[VAProcFilterDeinterlacing] == VA_INVALID_ID)
+ MP_WARN(vf, "Selected deinterlacing algorithm not supported.\n");
+ } // check other filters
+ }
+ if (p->opts->deint_type < 0)
+ p->opts->deint_type = 0;
+ p->num_buffers = 0;
+ if (buffers[VAProcFilterDeinterlacing] != VA_INVALID_ID)
+ p->buffers[p->num_buffers++] = buffers[VAProcFilterDeinterlacing];
+ p->do_deint = !!p->opts->deint_type;
+ // next filters: p->buffers[p->num_buffers++] = buffers[next_filter];
+ return true;
+}
+
+static const struct mp_filter_info vf_vavpp_filter = {
+ .name = "vavpp",
+ .process = vf_vavpp_process,
+ .reset = flush_frames,
+ .destroy = uninit,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *vf_vavpp_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &vf_vavpp_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+ p->config = VA_INVALID_ID;
+ p->context = VA_INVALID_ID;
+
+ p->queue = mp_refqueue_alloc(f);
+
+ struct mp_hwdec_ctx *hwdec_ctx =
+ mp_filter_load_hwdec_device(f, IMGFMT_VAAPI);
+ if (!hwdec_ctx || !hwdec_ctx->av_device_ref)
+ goto error;
+ p->av_device_ref = av_buffer_ref(hwdec_ctx->av_device_ref);
+ if (!p->av_device_ref)
+ goto error;
+
+ AVHWDeviceContext *hwctx = (void *)p->av_device_ref->data;
+ AVVAAPIDeviceContext *vactx = hwctx->hwctx;
+
+ p->display = vactx->display;
+
+ mp_refqueue_add_in_format(p->queue, IMGFMT_VAAPI, 0);
+
+ if (!initialize(f))
+ goto error;
+
+ return f;
+
+error:
+ talloc_free(f);
+ return NULL;
+}
+
+#define OPT_BASE_STRUCT struct opts
+static const m_option_t vf_opts_fields[] = {
+ {"deint", OPT_CHOICE(deint_type,
+ // The values >=0 must match with deint_algorithm[].
+ {"auto", -1},
+ {"no", 0},
+ {"first-field", 1},
+ {"bob", 2},
+ {"weave", 3},
+ {"motion-adaptive", 4},
+ {"motion-compensated", 5})},
+ {"interlaced-only", OPT_BOOL(interlaced_only)},
+ {"reversal-bug", OPT_BOOL(reversal_bug)},
+ {0}
+};
+
+const struct mp_user_filter_entry vf_vavpp = {
+ .desc = {
+ .description = "VA-API Video Post-Process Filter",
+ .name = "vavpp",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .priv_defaults = &(const OPT_BASE_STRUCT){
+ .deint_type = -1,
+ .reversal_bug = true,
+ },
+ .options = vf_opts_fields,
+ },
+ .create = vf_vavpp_create,
+};
diff --git a/video/filter/vf_vdpaupp.c b/video/filter/vf_vdpaupp.c
new file mode 100644
index 0000000..0519f5a
--- /dev/null
+++ b/video/filter/vf_vdpaupp.c
@@ -0,0 +1,195 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_option.h"
+#include "filters/filter.h"
+#include "filters/filter_internal.h"
+#include "filters/user_filters.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/hwdec.h"
+#include "video/vdpau.h"
+#include "video/vdpau_mixer.h"
+#include "refqueue.h"
+
+// Note: this filter does no actual filtering; it merely sets appropriate
+// flags on vdpau images (mp_vdpau_mixer_frame) to do the appropriate
+// processing on the final rendering process in the VO.
+
+struct opts {
+ bool deint_enabled;
+ bool interlaced_only;
+ struct mp_vdpau_mixer_opts opts;
+};
+
+struct priv {
+ struct opts *opts;
+ struct mp_vdpau_ctx *ctx;
+ struct mp_refqueue *queue;
+ struct mp_pin *in_pin;
+};
+
+static VdpVideoSurface ref_field(struct priv *p,
+ struct mp_vdpau_mixer_frame *frame, int pos)
+{
+ struct mp_image *mpi = mp_image_new_ref(mp_refqueue_get_field(p->queue, pos));
+ if (!mpi)
+ return VDP_INVALID_HANDLE;
+ talloc_steal(frame, mpi);
+ return (uintptr_t)mpi->planes[3];
+}
+
+static void vf_vdpaupp_process(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+
+ mp_refqueue_execute_reinit(p->queue);
+
+ if (!mp_refqueue_can_output(p->queue))
+ return;
+
+ struct mp_image *mpi =
+ mp_vdpau_mixed_frame_create(mp_refqueue_get_field(p->queue, 0));
+ if (!mpi)
+ return; // OOM
+ struct mp_vdpau_mixer_frame *frame = mp_vdpau_mixed_frame_get(mpi);
+
+ if (!mp_refqueue_should_deint(p->queue)) {
+ frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;
+ } else if (mp_refqueue_is_top_field(p->queue)) {
+ frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_TOP_FIELD;
+ } else {
+ frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_BOTTOM_FIELD;
+ }
+
+ frame->future[0] = ref_field(p, frame, 1);
+ frame->current = ref_field(p, frame, 0);
+ frame->past[0] = ref_field(p, frame, -1);
+ frame->past[1] = ref_field(p, frame, -2);
+
+ frame->opts = p->opts->opts;
+
+ mpi->planes[3] = (void *)(uintptr_t)frame->current;
+
+ mpi->params.hw_subfmt = 0; // force mixer
+
+ mp_refqueue_write_out_pin(p->queue, mpi);
+}
+
+static void vf_vdpaupp_reset(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ mp_refqueue_flush(p->queue);
+}
+
+static void vf_vdpaupp_destroy(struct mp_filter *f)
+{
+ struct priv *p = f->priv;
+ talloc_free(p->queue);
+}
+
+static const struct mp_filter_info vf_vdpaupp_filter = {
+ .name = "vdpaupp",
+ .process = vf_vdpaupp_process,
+ .reset = vf_vdpaupp_reset,
+ .destroy = vf_vdpaupp_destroy,
+ .priv_size = sizeof(struct priv),
+};
+
+static struct mp_filter *vf_vdpaupp_create(struct mp_filter *parent, void *options)
+{
+ struct mp_filter *f = mp_filter_create(parent, &vf_vdpaupp_filter);
+ if (!f) {
+ talloc_free(options);
+ return NULL;
+ }
+
+ mp_filter_add_pin(f, MP_PIN_IN, "in");
+ mp_filter_add_pin(f, MP_PIN_OUT, "out");
+
+ struct priv *p = f->priv;
+ p->opts = talloc_steal(p, options);
+
+ p->queue = mp_refqueue_alloc(f);
+
+ struct mp_hwdec_ctx *hwdec_ctx =
+ mp_filter_load_hwdec_device(f, IMGFMT_VDPAU);
+ if (!hwdec_ctx || !hwdec_ctx->av_device_ref)
+ goto error;
+ p->ctx = mp_vdpau_get_ctx_from_av(hwdec_ctx->av_device_ref);
+ if (!p->ctx)
+ goto error;
+
+ if (!p->opts->deint_enabled)
+ p->opts->opts.deint = 0;
+
+ if (p->opts->opts.deint >= 2) {
+ mp_refqueue_set_refs(p->queue, 1, 1); // 2 past fields, 1 future field
+ } else {
+ mp_refqueue_set_refs(p->queue, 0, 0);
+ }
+ mp_refqueue_set_mode(p->queue,
+ (p->opts->deint_enabled ? MP_MODE_DEINT : 0) |
+ (p->opts->interlaced_only ? MP_MODE_INTERLACED_ONLY : 0) |
+ (p->opts->opts.deint >= 2 ? MP_MODE_OUTPUT_FIELDS : 0));
+
+ mp_refqueue_add_in_format(p->queue, IMGFMT_VDPAU, 0);
+
+ return f;
+
+error:
+ talloc_free(f);
+ return NULL;
+}
+
+#define OPT_BASE_STRUCT struct opts
+static const m_option_t vf_opts_fields[] = {
+ {"deint-mode", OPT_CHOICE(opts.deint,
+ {"first-field", 1},
+ {"bob", 2},
+ {"temporal", 3},
+ {"temporal-spatial", 4}),
+ OPTDEF_INT(3)},
+ {"deint", OPT_BOOL(deint_enabled)},
+ {"chroma-deint", OPT_BOOL(opts.chroma_deint), OPTDEF_INT(1)},
+ {"pullup", OPT_BOOL(opts.pullup)},
+ {"denoise", OPT_FLOAT(opts.denoise), M_RANGE(0, 1)},
+ {"sharpen", OPT_FLOAT(opts.sharpen), M_RANGE(-1, 1)},
+ {"hqscaling", OPT_INT(opts.hqscaling), M_RANGE(0, 9)},
+ {"interlaced-only", OPT_BOOL(interlaced_only)},
+ {0}
+};
+
+const struct mp_user_filter_entry vf_vdpaupp = {
+ .desc = {
+ .description = "vdpau postprocessing",
+ .name = "vdpaupp",
+ .priv_size = sizeof(OPT_BASE_STRUCT),
+ .options = vf_opts_fields,
+ },
+ .create = vf_vdpaupp_create,
+};
diff --git a/video/fmt-conversion.c b/video/fmt-conversion.c
new file mode 100644
index 0000000..aa7d857
--- /dev/null
+++ b/video/fmt-conversion.c
@@ -0,0 +1,112 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/pixdesc.h>
+#include <libavutil/avutil.h>
+
+#include "video/img_format.h"
+#include "fmt-conversion.h"
+
+static const struct {
+ int fmt;
+ enum AVPixelFormat pix_fmt;
+} conversion_map[] = {
+ {IMGFMT_ARGB, AV_PIX_FMT_ARGB},
+ {IMGFMT_BGRA, AV_PIX_FMT_BGRA},
+ {IMGFMT_BGR24, AV_PIX_FMT_BGR24},
+ {IMGFMT_RGB565, AV_PIX_FMT_RGB565},
+ {IMGFMT_ABGR, AV_PIX_FMT_ABGR},
+ {IMGFMT_RGBA, AV_PIX_FMT_RGBA},
+ {IMGFMT_RGB24, AV_PIX_FMT_RGB24},
+ {IMGFMT_PAL8, AV_PIX_FMT_PAL8},
+ {IMGFMT_UYVY, AV_PIX_FMT_UYVY422},
+ {IMGFMT_NV12, AV_PIX_FMT_NV12},
+ {IMGFMT_Y8, AV_PIX_FMT_GRAY8},
+ {IMGFMT_Y16, AV_PIX_FMT_GRAY16},
+ {IMGFMT_420P, AV_PIX_FMT_YUV420P},
+ {IMGFMT_444P, AV_PIX_FMT_YUV444P},
+
+ // YUVJ are YUV formats that use the full Y range. Decoder color range
+ // information is used instead. Deprecated in ffmpeg.
+ {IMGFMT_420P, AV_PIX_FMT_YUVJ420P},
+ {IMGFMT_444P, AV_PIX_FMT_YUVJ444P},
+
+ {IMGFMT_BGR0, AV_PIX_FMT_BGR0},
+ {IMGFMT_0RGB, AV_PIX_FMT_0RGB},
+ {IMGFMT_RGB0, AV_PIX_FMT_RGB0},
+ {IMGFMT_0BGR, AV_PIX_FMT_0BGR},
+
+ {IMGFMT_RGBA64, AV_PIX_FMT_RGBA64},
+
+#ifdef AV_PIX_FMT_X2RGB10
+ {IMGFMT_RGB30, AV_PIX_FMT_X2RGB10},
+#endif
+
+ {IMGFMT_VDPAU, AV_PIX_FMT_VDPAU},
+ {IMGFMT_VIDEOTOOLBOX, AV_PIX_FMT_VIDEOTOOLBOX},
+ {IMGFMT_MEDIACODEC, AV_PIX_FMT_MEDIACODEC},
+ {IMGFMT_VAAPI, AV_PIX_FMT_VAAPI},
+ {IMGFMT_DXVA2, AV_PIX_FMT_DXVA2_VLD},
+ {IMGFMT_D3D11, AV_PIX_FMT_D3D11},
+ {IMGFMT_MMAL, AV_PIX_FMT_MMAL},
+ {IMGFMT_CUDA, AV_PIX_FMT_CUDA},
+ {IMGFMT_P010, AV_PIX_FMT_P010},
+ {IMGFMT_DRMPRIME, AV_PIX_FMT_DRM_PRIME},
+#if HAVE_VULKAN_INTEROP
+ {IMGFMT_VULKAN, AV_PIX_FMT_VULKAN},
+#endif
+
+ {0, AV_PIX_FMT_NONE}
+};
+
+enum AVPixelFormat imgfmt2pixfmt(int fmt)
+{
+ if (fmt == IMGFMT_NONE)
+ return AV_PIX_FMT_NONE;
+
+ if (fmt >= IMGFMT_AVPIXFMT_START && fmt < IMGFMT_AVPIXFMT_END) {
+ enum AVPixelFormat pixfmt = fmt - IMGFMT_AVPIXFMT_START;
+ // Avoid duplicate format - each format must be unique.
+ int mpfmt = pixfmt2imgfmt(pixfmt);
+ if (mpfmt == fmt && av_pix_fmt_desc_get(pixfmt))
+ return pixfmt;
+ return AV_PIX_FMT_NONE;
+ }
+
+ for (int i = 0; conversion_map[i].fmt; i++) {
+ if (conversion_map[i].fmt == fmt)
+ return conversion_map[i].pix_fmt;
+ }
+ return AV_PIX_FMT_NONE;
+}
+
+int pixfmt2imgfmt(enum AVPixelFormat pix_fmt)
+{
+ if (pix_fmt == AV_PIX_FMT_NONE)
+ return IMGFMT_NONE;
+
+ for (int i = 0; conversion_map[i].pix_fmt != AV_PIX_FMT_NONE; i++) {
+ if (conversion_map[i].pix_fmt == pix_fmt)
+ return conversion_map[i].fmt;
+ }
+
+ int generic = IMGFMT_AVPIXFMT_START + pix_fmt;
+ if (generic < IMGFMT_AVPIXFMT_END && av_pix_fmt_desc_get(pix_fmt))
+ return generic;
+
+ return 0;
+}
diff --git a/video/fmt-conversion.h b/video/fmt-conversion.h
new file mode 100644
index 0000000..962e4b8
--- /dev/null
+++ b/video/fmt-conversion.h
@@ -0,0 +1,26 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_FMT_CONVERSION_H
+#define MPLAYER_FMT_CONVERSION_H
+
+#include <libavutil/pixfmt.h>
+
+enum AVPixelFormat imgfmt2pixfmt(int fmt);
+int pixfmt2imgfmt(enum AVPixelFormat pix_fmt);
+
+#endif /* MPLAYER_FMT_CONVERSION_H */
diff --git a/video/hwdec.c b/video/hwdec.c
new file mode 100644
index 0000000..f397f3b
--- /dev/null
+++ b/video/hwdec.c
@@ -0,0 +1,140 @@
+#include <assert.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "config.h"
+#include "hwdec.h"
+#include "osdep/threads.h"
+
+struct mp_hwdec_devices {
+ mp_mutex lock;
+
+ struct mp_hwdec_ctx **hwctxs;
+ int num_hwctxs;
+
+ void (*load_api)(void *ctx,
+ struct hwdec_imgfmt_request *params);
+ void *load_api_ctx;
+};
+
+struct mp_hwdec_devices *hwdec_devices_create(void)
+{
+ struct mp_hwdec_devices *devs = talloc_zero(NULL, struct mp_hwdec_devices);
+ mp_mutex_init(&devs->lock);
+ return devs;
+}
+
+void hwdec_devices_destroy(struct mp_hwdec_devices *devs)
+{
+ if (!devs)
+ return;
+ assert(!devs->num_hwctxs); // must have been hwdec_devices_remove()ed
+ assert(!devs->load_api); // must have been unset
+ mp_mutex_destroy(&devs->lock);
+ talloc_free(devs);
+}
+
+struct mp_hwdec_ctx *hwdec_devices_get_by_imgfmt(struct mp_hwdec_devices *devs,
+ int hw_imgfmt)
+{
+ struct mp_hwdec_ctx *res = NULL;
+ mp_mutex_lock(&devs->lock);
+ for (int n = 0; n < devs->num_hwctxs; n++) {
+ struct mp_hwdec_ctx *dev = devs->hwctxs[n];
+ if (dev->hw_imgfmt == hw_imgfmt) {
+ res = dev;
+ break;
+ }
+ }
+ mp_mutex_unlock(&devs->lock);
+ return res;
+}
+
+struct mp_hwdec_ctx *hwdec_devices_get_first(struct mp_hwdec_devices *devs)
+{
+ return hwdec_devices_get_n(devs, 0);
+}
+
+struct mp_hwdec_ctx *hwdec_devices_get_n(struct mp_hwdec_devices *devs, int n)
+{
+ mp_mutex_lock(&devs->lock);
+ struct mp_hwdec_ctx *res = n < devs->num_hwctxs ? devs->hwctxs[n] : NULL;
+ mp_mutex_unlock(&devs->lock);
+ return res;
+}
+
+void hwdec_devices_add(struct mp_hwdec_devices *devs, struct mp_hwdec_ctx *ctx)
+{
+ mp_mutex_lock(&devs->lock);
+ MP_TARRAY_APPEND(devs, devs->hwctxs, devs->num_hwctxs, ctx);
+ mp_mutex_unlock(&devs->lock);
+}
+
+void hwdec_devices_remove(struct mp_hwdec_devices *devs, struct mp_hwdec_ctx *ctx)
+{
+ mp_mutex_lock(&devs->lock);
+ for (int n = 0; n < devs->num_hwctxs; n++) {
+ if (devs->hwctxs[n] == ctx) {
+ MP_TARRAY_REMOVE_AT(devs->hwctxs, devs->num_hwctxs, n);
+ break;
+ }
+ }
+ mp_mutex_unlock(&devs->lock);
+}
+
+void hwdec_devices_set_loader(struct mp_hwdec_devices *devs,
+ void (*load_api)(void *ctx, struct hwdec_imgfmt_request *params),
+ void *load_api_ctx)
+{
+ devs->load_api = load_api;
+ devs->load_api_ctx = load_api_ctx;
+}
+
+void hwdec_devices_request_for_img_fmt(struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params)
+{
+ if (devs->load_api)
+ devs->load_api(devs->load_api_ctx, params);
+}
+
+char *hwdec_devices_get_names(struct mp_hwdec_devices *devs)
+{
+ char *res = NULL;
+ for (int n = 0; n < devs->num_hwctxs; n++) {
+ if (res)
+ ta_xstrdup_append(&res, ",");
+ ta_xstrdup_append(&res, devs->hwctxs[n]->driver_name);
+ }
+ return res;
+}
+
+static const struct hwcontext_fns *const hwcontext_fns[] = {
+#if HAVE_CUDA_HWACCEL
+ &hwcontext_fns_cuda,
+#endif
+#if HAVE_D3D_HWACCEL
+ &hwcontext_fns_d3d11,
+#endif
+#if HAVE_D3D9_HWACCEL
+ &hwcontext_fns_dxva2,
+#endif
+#if HAVE_DRM
+ &hwcontext_fns_drmprime,
+#endif
+#if HAVE_VAAPI
+ &hwcontext_fns_vaapi,
+#endif
+#if HAVE_VDPAU
+ &hwcontext_fns_vdpau,
+#endif
+ NULL,
+};
+
+const struct hwcontext_fns *hwdec_get_hwcontext_fns(int av_hwdevice_type)
+{
+ for (int n = 0; hwcontext_fns[n]; n++) {
+ if (hwcontext_fns[n]->av_hwdevice_type == av_hwdevice_type)
+ return hwcontext_fns[n];
+ }
+ return NULL;
+}
diff --git a/video/hwdec.h b/video/hwdec.h
new file mode 100644
index 0000000..723c60f
--- /dev/null
+++ b/video/hwdec.h
@@ -0,0 +1,108 @@
+#ifndef MP_HWDEC_H_
+#define MP_HWDEC_H_
+
+#include <libavutil/buffer.h>
+
+#include "options/m_option.h"
+
+struct mp_image_pool;
+
+struct mp_hwdec_ctx {
+ const char *driver_name; // NULL if unknown/not loaded
+
+ // libavutil-wrapped context, if available.
+ struct AVBufferRef *av_device_ref; // AVHWDeviceContext*
+
+ // List of allowed IMGFMT_s, terminated with 0.
+ // If NULL, all software formats are considered to be supported.
+ const int *supported_formats;
+ // HW format used by the hwdec
+ int hw_imgfmt;
+
+ // The name of this hwdec's matching conversion filter if available.
+ // This will be used for hardware conversion of frame formats.
+ // NULL otherwise.
+ const char *conversion_filter_name;
+
+ // The libavutil hwconfig to be used when querying constraints for the
+ // conversion filter. Can be NULL if no special config is required.
+ void *conversion_config;
+};
+
+// Used to communicate hardware decoder device handles from VO to video decoder.
+struct mp_hwdec_devices;
+
+struct mp_hwdec_devices *hwdec_devices_create(void);
+void hwdec_devices_destroy(struct mp_hwdec_devices *devs);
+
+struct mp_hwdec_ctx *hwdec_devices_get_by_imgfmt(struct mp_hwdec_devices *devs,
+ int hw_imgfmt);
+
+// For code which still strictly assumes there is 1 (or none) device.
+struct mp_hwdec_ctx *hwdec_devices_get_first(struct mp_hwdec_devices *devs);
+
+// Return the n-th device. NULL if none.
+struct mp_hwdec_ctx *hwdec_devices_get_n(struct mp_hwdec_devices *devs, int n);
+
+// Add this to the list of internal devices. Adding the same pointer twice must
+// be avoided.
+void hwdec_devices_add(struct mp_hwdec_devices *devs, struct mp_hwdec_ctx *ctx);
+
+// Remove this from the list of internal devices. Idempotent/ignores entries
+// not added yet. This is not thread-safe.
+void hwdec_devices_remove(struct mp_hwdec_devices *devs, struct mp_hwdec_ctx *ctx);
+
+struct hwdec_imgfmt_request {
+ int imgfmt;
+ bool probing;
+};
+
+// Can be used to enable lazy loading of an API with hwdec_devices_request().
+// If used at all, this must be set/unset during initialization/uninitialization,
+// as concurrent use with hwdec_devices_request() is a race condition.
+void hwdec_devices_set_loader(struct mp_hwdec_devices *devs,
+ void (*load_api)(void *ctx, struct hwdec_imgfmt_request *params),
+ void *load_api_ctx);
+
+// Cause VO to lazily load all devices for a specified img format, and will
+// block until this is done (even if not available). Pass IMGFMT_NONE to load
+// all available devices.
+void hwdec_devices_request_for_img_fmt(struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params);
+
+// Return "," concatenated list (for introspection/debugging). Use talloc_free().
+char *hwdec_devices_get_names(struct mp_hwdec_devices *devs);
+
+struct mp_image;
+struct mpv_global;
+
+struct hwcontext_create_dev_params {
+ bool probing; // if true, don't log errors if unavailable
+};
+
+// Per AV_HWDEVICE_TYPE_* functions, queryable via hwdec_get_hwcontext_fns().
+// All entries are strictly optional.
+struct hwcontext_fns {
+ int av_hwdevice_type;
+ // Fill in special format-specific requirements.
+ void (*refine_hwframes)(struct AVBufferRef *hw_frames_ctx);
+ // Returns a AVHWDeviceContext*. Used for copy hwdecs.
+ struct AVBufferRef *(*create_dev)(struct mpv_global *global,
+ struct mp_log *log,
+ struct hwcontext_create_dev_params *params);
+ // Return whether this is using some sort of sub-optimal emulation layer.
+ bool (*is_emulated)(struct AVBufferRef *hw_device_ctx);
+};
+
+// The parameter is of type enum AVHWDeviceType (as in int to avoid extensive
+// recursive includes). May return NULL for unknown device types.
+const struct hwcontext_fns *hwdec_get_hwcontext_fns(int av_hwdevice_type);
+
+extern const struct hwcontext_fns hwcontext_fns_cuda;
+extern const struct hwcontext_fns hwcontext_fns_d3d11;
+extern const struct hwcontext_fns hwcontext_fns_drmprime;
+extern const struct hwcontext_fns hwcontext_fns_dxva2;
+extern const struct hwcontext_fns hwcontext_fns_vaapi;
+extern const struct hwcontext_fns hwcontext_fns_vdpau;
+
+#endif
diff --git a/video/image_loader.c b/video/image_loader.c
new file mode 100644
index 0000000..ba4d62a
--- /dev/null
+++ b/video/image_loader.c
@@ -0,0 +1,48 @@
+#include <libavcodec/avcodec.h>
+
+#include "common/common.h"
+#include "mp_image.h"
+#include "player/screenshot.h"
+
+#include "image_loader.h"
+
+struct mp_image *load_image_png_buf(void *buffer, size_t buffer_size, int imgfmt)
+{
+ const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_PNG);
+ if (!codec)
+ return NULL;
+
+ AVCodecContext *avctx = avcodec_alloc_context3(codec);
+ if (!avctx)
+ return NULL;
+
+ if (avcodec_open2(avctx, codec, NULL) < 0) {
+ avcodec_free_context(&avctx);
+ return NULL;
+ }
+
+ AVPacket *pkt = av_packet_alloc();
+ if (pkt) {
+ if (av_new_packet(pkt, buffer_size) >= 0)
+ memcpy(pkt->data, buffer, buffer_size);
+ }
+
+ // (There is only 1 outcome: either it takes it and decodes it, or not.)
+ avcodec_send_packet(avctx, pkt);
+ avcodec_send_packet(avctx, NULL);
+
+ av_packet_free(&pkt);
+
+ struct mp_image *res = NULL;
+ AVFrame *frame = av_frame_alloc();
+ if (frame && avcodec_receive_frame(avctx, frame) >= 0) {
+ struct mp_image *r = mp_image_from_av_frame(frame);
+ if (r)
+ res = convert_image(r, imgfmt, NULL, mp_null_log);
+ talloc_free(r);
+ }
+ av_frame_free(&frame);
+
+ avcodec_free_context(&avctx);
+ return res;
+}
diff --git a/video/image_loader.h b/video/image_loader.h
new file mode 100644
index 0000000..f8b20c8
--- /dev/null
+++ b/video/image_loader.h
@@ -0,0 +1,9 @@
+#ifndef MP_IMAGE_LOADER_H_
+#define MP_IMAGE_LOADER_H_
+
+#include <stddef.h>
+
+struct mp_image;
+struct mp_image *load_image_png_buf(void *buffer, size_t buffer_size, int imgfmt);
+
+#endif
diff --git a/video/image_writer.c b/video/image_writer.c
new file mode 100644
index 0000000..288d809
--- /dev/null
+++ b/video/image_writer.c
@@ -0,0 +1,757 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/mem.h>
+#include <libavutil/opt.h>
+#include <libavutil/pixdesc.h>
+
+#include "common/msg.h"
+#include "config.h"
+
+#if HAVE_JPEG
+#include <setjmp.h>
+#include <jpeglib.h>
+#endif
+
+#include "osdep/io.h"
+
+#include "common/av_common.h"
+#include "common/msg.h"
+#include "image_writer.h"
+#include "mpv_talloc.h"
+#include "video/fmt-conversion.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+
+#include "options/m_option.h"
+
+const struct image_writer_opts image_writer_opts_defaults = {
+ .format = AV_CODEC_ID_MJPEG,
+ .high_bit_depth = true,
+ .png_compression = 7,
+ .png_filter = 5,
+ .jpeg_quality = 90,
+ .jpeg_source_chroma = true,
+ .webp_quality = 75,
+ .webp_compression = 4,
+ .jxl_distance = 1.0,
+ .jxl_effort = 4,
+ .avif_encoder = "libaom-av1",
+ .avif_pixfmt = "yuv420p",
+ .avif_opts = (char*[]){
+ "usage", "allintra",
+ "crf", "32",
+ "cpu-used", "8",
+ "tune", "ssim",
+ NULL
+ },
+ .tag_csp = true,
+};
+
+const struct m_opt_choice_alternatives mp_image_writer_formats[] = {
+ {"jpg", AV_CODEC_ID_MJPEG},
+ {"jpeg", AV_CODEC_ID_MJPEG},
+ {"png", AV_CODEC_ID_PNG},
+ {"webp", AV_CODEC_ID_WEBP},
+#if HAVE_JPEGXL
+ {"jxl", AV_CODEC_ID_JPEGXL},
+#endif
+#if HAVE_AVIF_MUXER
+ {"avif", AV_CODEC_ID_AV1},
+#endif
+ {0}
+};
+
+#define OPT_BASE_STRUCT struct image_writer_opts
+
+const struct m_option image_writer_opts[] = {
+ {"format", OPT_CHOICE_C(format, mp_image_writer_formats)},
+ {"jpeg-quality", OPT_INT(jpeg_quality), M_RANGE(0, 100)},
+ {"jpeg-source-chroma", OPT_BOOL(jpeg_source_chroma)},
+ {"png-compression", OPT_INT(png_compression), M_RANGE(0, 9)},
+ {"png-filter", OPT_INT(png_filter), M_RANGE(0, 5)},
+ {"webp-lossless", OPT_BOOL(webp_lossless)},
+ {"webp-quality", OPT_INT(webp_quality), M_RANGE(0, 100)},
+ {"webp-compression", OPT_INT(webp_compression), M_RANGE(0, 6)},
+#if HAVE_JPEGXL
+ {"jxl-distance", OPT_DOUBLE(jxl_distance), M_RANGE(0.0, 15.0)},
+ {"jxl-effort", OPT_INT(jxl_effort), M_RANGE(1, 9)},
+#endif
+#if HAVE_AVIF_MUXER
+ {"avif-encoder", OPT_STRING(avif_encoder)},
+ {"avif-opts", OPT_KEYVALUELIST(avif_opts)},
+ {"avif-pixfmt", OPT_STRING(avif_pixfmt)},
+#endif
+ {"high-bit-depth", OPT_BOOL(high_bit_depth)},
+ {"tag-colorspace", OPT_BOOL(tag_csp)},
+ {0},
+};
+
+struct image_writer_ctx {
+ struct mp_log *log;
+ const struct image_writer_opts *opts;
+ struct mp_imgfmt_desc original_format;
+};
+
+static enum AVPixelFormat replace_j_format(enum AVPixelFormat fmt)
+{
+ switch (fmt) {
+ case AV_PIX_FMT_YUV420P: return AV_PIX_FMT_YUVJ420P;
+ case AV_PIX_FMT_YUV422P: return AV_PIX_FMT_YUVJ422P;
+ case AV_PIX_FMT_YUV444P: return AV_PIX_FMT_YUVJ444P;
+ }
+ return fmt;
+}
+
+static void prepare_avframe(AVFrame *pic, AVCodecContext *avctx,
+ mp_image_t *image, bool tag_csp,
+ struct mp_log *log)
+{
+ for (int n = 0; n < 4; n++) {
+ pic->data[n] = image->planes[n];
+ pic->linesize[n] = image->stride[n];
+ }
+ pic->format = avctx->pix_fmt;
+ pic->width = avctx->width;
+ pic->height = avctx->height;
+ avctx->color_range = pic->color_range =
+ mp_csp_levels_to_avcol_range(image->params.color.levels);
+
+ if (!tag_csp)
+ return;
+ avctx->color_primaries = pic->color_primaries =
+ mp_csp_prim_to_avcol_pri(image->params.color.primaries);
+ avctx->color_trc = pic->color_trc =
+ mp_csp_trc_to_avcol_trc(image->params.color.gamma);
+ avctx->colorspace = pic->colorspace =
+ mp_csp_to_avcol_spc(image->params.color.space);
+ avctx->chroma_sample_location = pic->chroma_location =
+ mp_chroma_location_to_av(image->params.chroma_location);
+ mp_dbg(log, "mapped color params:\n"
+ " trc = %s\n"
+ " primaries = %s\n"
+ " range = %s\n"
+ " colorspace = %s\n"
+ " chroma_location = %s\n",
+ av_color_transfer_name(avctx->color_trc),
+ av_color_primaries_name(avctx->color_primaries),
+ av_color_range_name(avctx->color_range),
+ av_color_space_name(avctx->colorspace),
+ av_chroma_location_name(avctx->chroma_sample_location)
+ );
+}
+
+static bool write_lavc(struct image_writer_ctx *ctx, mp_image_t *image, const char *filename)
+{
+ FILE *fp = fopen(filename, "wb");
+ if (!fp) {
+ MP_ERR(ctx, "Error opening '%s' for writing!\n", filename);
+ return false;
+ }
+
+ bool success = false;
+ AVFrame *pic = NULL;
+ AVPacket *pkt = NULL;
+
+ const AVCodec *codec;
+ if (ctx->opts->format == AV_CODEC_ID_WEBP) {
+ codec = avcodec_find_encoder_by_name("libwebp"); // non-animated encoder
+ } else {
+ codec = avcodec_find_encoder(ctx->opts->format);
+ }
+
+ AVCodecContext *avctx = NULL;
+ if (!codec)
+ goto print_open_fail;
+ avctx = avcodec_alloc_context3(codec);
+ if (!avctx)
+ goto print_open_fail;
+
+ avctx->time_base = AV_TIME_BASE_Q;
+ avctx->width = image->w;
+ avctx->height = image->h;
+ avctx->pix_fmt = imgfmt2pixfmt(image->imgfmt);
+ if (codec->id == AV_CODEC_ID_MJPEG) {
+ // Annoying deprecated garbage for the jpg encoder.
+ if (image->params.color.levels == MP_CSP_LEVELS_PC)
+ avctx->pix_fmt = replace_j_format(avctx->pix_fmt);
+ }
+ if (avctx->pix_fmt == AV_PIX_FMT_NONE) {
+ MP_ERR(ctx, "Image format %s not supported by lavc.\n",
+ mp_imgfmt_to_name(image->imgfmt));
+ goto error_exit;
+ }
+
+ if (codec->id == AV_CODEC_ID_MJPEG) {
+ avctx->flags |= AV_CODEC_FLAG_QSCALE;
+ // jpeg_quality is set below
+ } else if (codec->id == AV_CODEC_ID_PNG) {
+ avctx->compression_level = ctx->opts->png_compression;
+ av_opt_set_int(avctx, "pred", ctx->opts->png_filter,
+ AV_OPT_SEARCH_CHILDREN);
+ } else if (codec->id == AV_CODEC_ID_WEBP) {
+ avctx->compression_level = ctx->opts->webp_compression;
+ av_opt_set_int(avctx, "lossless", ctx->opts->webp_lossless,
+ AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_int(avctx, "quality", ctx->opts->webp_quality,
+ AV_OPT_SEARCH_CHILDREN);
+#if HAVE_JPEGXL
+ } else if (codec->id == AV_CODEC_ID_JPEGXL) {
+ av_opt_set_double(avctx, "distance", ctx->opts->jxl_distance,
+ AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_int(avctx, "effort", ctx->opts->jxl_effort,
+ AV_OPT_SEARCH_CHILDREN);
+#endif
+ }
+
+ if (avcodec_open2(avctx, codec, NULL) < 0) {
+ print_open_fail:
+ MP_ERR(ctx, "Could not open libavcodec encoder for saving images\n");
+ goto error_exit;
+ }
+
+ pic = av_frame_alloc();
+ if (!pic)
+ goto error_exit;
+ prepare_avframe(pic, avctx, image, ctx->opts->tag_csp, ctx->log);
+ if (codec->id == AV_CODEC_ID_MJPEG) {
+ int qscale = 1 + (100 - ctx->opts->jpeg_quality) * 30 / 100;
+ pic->quality = qscale * FF_QP2LAMBDA;
+ }
+
+ int ret = avcodec_send_frame(avctx, pic);
+ if (ret < 0)
+ goto error_exit;
+ ret = avcodec_send_frame(avctx, NULL); // send EOF
+ if (ret < 0)
+ goto error_exit;
+ pkt = av_packet_alloc();
+ if (!pkt)
+ goto error_exit;
+ ret = avcodec_receive_packet(avctx, pkt);
+ if (ret < 0)
+ goto error_exit;
+
+ success = fwrite(pkt->data, pkt->size, 1, fp) == 1;
+
+error_exit:
+ avcodec_free_context(&avctx);
+ av_frame_free(&pic);
+ av_packet_free(&pkt);
+ return !fclose(fp) && success;
+}
+
+#if HAVE_JPEG
+
+static void write_jpeg_error_exit(j_common_ptr cinfo)
+{
+ // NOTE: do not write error message, too much effort to connect the libjpeg
+ // log callbacks with mplayer's log function mp_msp()
+
+ // Return control to the setjmp point
+ longjmp(*(jmp_buf*)cinfo->client_data, 1);
+}
+
+static bool write_jpeg(struct image_writer_ctx *ctx, mp_image_t *image,
+ const char *filename)
+{
+ FILE *fp = fopen(filename, "wb");
+ if (!fp) {
+ MP_ERR(ctx, "Error opening '%s' for writing!\n", filename);
+ return false;
+ }
+
+ struct jpeg_compress_struct cinfo;
+ struct jpeg_error_mgr jerr;
+
+ cinfo.err = jpeg_std_error(&jerr);
+ jerr.error_exit = write_jpeg_error_exit;
+
+ jmp_buf error_return_jmpbuf;
+ cinfo.client_data = &error_return_jmpbuf;
+ if (setjmp(cinfo.client_data)) {
+ jpeg_destroy_compress(&cinfo);
+ fclose(fp);
+ return false;
+ }
+
+ jpeg_create_compress(&cinfo);
+ jpeg_stdio_dest(&cinfo, fp);
+
+ cinfo.image_width = image->w;
+ cinfo.image_height = image->h;
+ cinfo.input_components = 3;
+ cinfo.in_color_space = JCS_RGB;
+
+ cinfo.write_JFIF_header = TRUE;
+ cinfo.JFIF_major_version = 1;
+ cinfo.JFIF_minor_version = 2;
+
+ jpeg_set_defaults(&cinfo);
+ jpeg_set_quality(&cinfo, ctx->opts->jpeg_quality, 0);
+
+ if (ctx->opts->jpeg_source_chroma) {
+ cinfo.comp_info[0].h_samp_factor = 1 << ctx->original_format.chroma_xs;
+ cinfo.comp_info[0].v_samp_factor = 1 << ctx->original_format.chroma_ys;
+ }
+
+ jpeg_start_compress(&cinfo, TRUE);
+
+ while (cinfo.next_scanline < cinfo.image_height) {
+ JSAMPROW row_pointer[1];
+ row_pointer[0] = image->planes[0] +
+ (ptrdiff_t)cinfo.next_scanline * image->stride[0];
+ jpeg_write_scanlines(&cinfo, row_pointer,1);
+ }
+
+ jpeg_finish_compress(&cinfo);
+
+ jpeg_destroy_compress(&cinfo);
+
+ return !fclose(fp);
+}
+
+#endif
+
+#if HAVE_AVIF_MUXER
+
+static void log_side_data(struct image_writer_ctx *ctx, AVPacketSideData *data,
+ size_t size)
+{
+ if (!mp_msg_test(ctx->log, MSGL_DEBUG))
+ return;
+ char dbgbuff[129];
+ if (size)
+ MP_DBG(ctx, "write_avif() packet side data:\n");
+ for (int i = 0; i < size; i++) {
+ AVPacketSideData *sd = &data[i];
+ for (int k = 0; k < MPMIN(sd->size, 64); k++)
+ snprintf(dbgbuff + k*2, 3, "%02x", (int)sd->data[k]);
+ MP_DBG(ctx, " [%d] = {[%s], '%s'}\n",
+ i, av_packet_side_data_name(sd->type), dbgbuff);
+ }
+}
+
+static bool write_avif(struct image_writer_ctx *ctx, mp_image_t *image,
+ const char *filename)
+{
+ const AVCodec *codec = NULL;
+ const AVOutputFormat *ofmt = NULL;
+ AVCodecContext *avctx = NULL;
+ AVIOContext *avioctx = NULL;
+ AVFormatContext *fmtctx = NULL;
+ AVStream *stream = NULL;
+ AVFrame *pic = NULL;
+ AVPacket *pkt = NULL;
+ int ret;
+ bool success = false;
+
+ codec = avcodec_find_encoder_by_name(ctx->opts->avif_encoder);
+ if (!codec) {
+ MP_ERR(ctx, "Could not find encoder '%s', for saving images\n",
+ ctx->opts->avif_encoder);
+ goto free_data;
+ }
+
+ ofmt = av_guess_format("avif", NULL, NULL);
+ if (!ofmt) {
+ MP_ERR(ctx, "Could not guess output format 'avif'\n");
+ goto free_data;
+ }
+
+ avctx = avcodec_alloc_context3(codec);
+ if (!avctx) {
+ MP_ERR(ctx, "Failed to allocate AVContext.\n");
+ goto free_data;
+ }
+
+ avctx->width = image->w;
+ avctx->height = image->h;
+ avctx->time_base = (AVRational){1, 30};
+ avctx->pkt_timebase = (AVRational){1, 30};
+ avctx->codec_type = AVMEDIA_TYPE_VIDEO;
+ avctx->pix_fmt = imgfmt2pixfmt(image->imgfmt);
+ if (avctx->pix_fmt == AV_PIX_FMT_NONE) {
+ MP_ERR(ctx, "Image format %s not supported by lavc.\n",
+ mp_imgfmt_to_name(image->imgfmt));
+ goto free_data;
+ }
+
+ av_opt_set_int(avctx, "still-picture", 1, AV_OPT_SEARCH_CHILDREN);
+
+ AVDictionary *avd = NULL;
+ mp_set_avdict(&avd, ctx->opts->avif_opts);
+ av_opt_set_dict2(avctx, &avd, AV_OPT_SEARCH_CHILDREN);
+ av_dict_free(&avd);
+
+ pic = av_frame_alloc();
+ if (!pic) {
+ MP_ERR(ctx, "Could not allocate AVFrame\n");
+ goto free_data;
+ }
+
+ prepare_avframe(pic, avctx, image, ctx->opts->tag_csp, ctx->log);
+ // Not setting this flag caused ffmpeg to output avif that was not passing
+ // standard checks but ffmpeg would still read and not complain...
+ avctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+
+ ret = avcodec_open2(avctx, codec, NULL);
+ if (ret < 0) {
+ MP_ERR(ctx, "Could not open libavcodec encoder for saving images\n");
+ goto free_data;
+ }
+
+ ret = avio_open(&avioctx, filename, AVIO_FLAG_WRITE);
+ if (ret < 0) {
+ MP_ERR(ctx, "Could not open file '%s' for saving images\n", filename);
+ goto free_data;
+ }
+
+ fmtctx = avformat_alloc_context();
+ if (!fmtctx) {
+ MP_ERR(ctx, "Could not allocate format context\n");
+ goto free_data;
+ }
+ fmtctx->pb = avioctx;
+ fmtctx->oformat = ofmt;
+
+ stream = avformat_new_stream(fmtctx, codec);
+ if (!stream) {
+ MP_ERR(ctx, "Could not allocate stream\n");
+ goto free_data;
+ }
+
+ ret = avcodec_parameters_from_context(stream->codecpar, avctx);
+ if (ret < 0) {
+ MP_ERR(ctx, "Could not copy parameters from context\n");
+ goto free_data;
+ }
+
+ ret = avformat_init_output(fmtctx, NULL);
+ if (ret < 0) {
+ MP_ERR(ctx, "Could not initialize output\n");
+ goto free_data;
+ }
+
+ ret = avformat_write_header(fmtctx, NULL);
+ if (ret < 0) {
+ MP_ERR(ctx, "Could not write format header\n");
+ goto free_data;
+ }
+
+ pkt = av_packet_alloc();
+ if (!pkt) {
+ MP_ERR(ctx, "Could not allocate packet\n");
+ goto free_data;
+ }
+
+ ret = avcodec_send_frame(avctx, pic);
+ if (ret < 0) {
+ MP_ERR(ctx, "Error sending frame\n");
+ goto free_data;
+ }
+ ret = avcodec_send_frame(avctx, NULL); // send EOF
+ if (ret < 0)
+ goto free_data;
+
+ int pts = 0;
+ log_side_data(ctx, avctx->coded_side_data, avctx->nb_coded_side_data);
+ while (ret >= 0) {
+ ret = avcodec_receive_packet(avctx, pkt);
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
+ break;
+ if (ret < 0) {
+ MP_ERR(ctx, "Error receiving packet\n");
+ goto free_data;
+ }
+ pkt->dts = pkt->pts = ++pts;
+ pkt->stream_index = stream->index;
+ log_side_data(ctx, pkt->side_data, pkt->side_data_elems);
+
+ ret = av_write_frame(fmtctx, pkt);
+ if (ret < 0) {
+ MP_ERR(ctx, "Error writing frame\n");
+ goto free_data;
+ }
+ av_packet_unref(pkt);
+ }
+
+ ret = av_write_trailer(fmtctx);
+ if (ret < 0) {
+ MP_ERR(ctx, "Could not write trailer\n");
+ goto free_data;
+ }
+ MP_DBG(ctx, "write_avif(): avio_size() = %"PRIi64"\n", avio_size(avioctx));
+
+ success = true;
+
+free_data:
+ success = !avio_closep(&avioctx) && success;
+ avformat_free_context(fmtctx);
+ avcodec_free_context(&avctx);
+ av_packet_free(&pkt);
+ av_frame_free(&pic);
+
+ return success;
+}
+
+#endif
+
+static int get_encoder_format(const AVCodec *codec, int srcfmt, bool highdepth)
+{
+ const enum AVPixelFormat *pix_fmts = codec->pix_fmts;
+ int current = 0;
+ for (int n = 0; pix_fmts && pix_fmts[n] != AV_PIX_FMT_NONE; n++) {
+ int fmt = pixfmt2imgfmt(pix_fmts[n]);
+ if (!fmt)
+ continue;
+ if (!highdepth) {
+ // Ignore formats larger than 8 bit per pixel. (Or which are unknown.)
+ struct mp_regular_imgfmt rdesc;
+ if (!mp_get_regular_imgfmt(&rdesc, fmt)) {
+ int ofmt = mp_find_other_endian(fmt);
+ if (!mp_get_regular_imgfmt(&rdesc, ofmt))
+ continue;
+ }
+ if (rdesc.component_size > 1)
+ continue;
+ }
+ current = current ? mp_imgfmt_select_best(current, fmt, srcfmt) : fmt;
+ }
+ return current;
+}
+
+static int get_target_format(struct image_writer_ctx *ctx)
+{
+ const AVCodec *codec = avcodec_find_encoder(ctx->opts->format);
+ if (!codec)
+ goto unknown;
+
+ int srcfmt = ctx->original_format.id;
+
+ int target = get_encoder_format(codec, srcfmt, ctx->opts->high_bit_depth);
+ if (!target) {
+ mp_dbg(ctx->log, "Falling back to high-depth format.\n");
+ target = get_encoder_format(codec, srcfmt, true);
+ }
+
+ if (!target)
+ goto unknown;
+
+ return target;
+
+unknown:
+ return IMGFMT_RGB0;
+}
+
+const char *image_writer_file_ext(const struct image_writer_opts *opts)
+{
+ struct image_writer_opts defs = image_writer_opts_defaults;
+
+ if (!opts)
+ opts = &defs;
+
+ return m_opt_choice_str(mp_image_writer_formats, opts->format);
+}
+
+bool image_writer_high_depth(const struct image_writer_opts *opts)
+{
+ return opts->format == AV_CODEC_ID_PNG
+#if HAVE_JPEGXL
+ || opts->format == AV_CODEC_ID_JPEGXL
+#endif
+#if HAVE_AVIF_MUXER
+ || opts->format == AV_CODEC_ID_AV1
+#endif
+ ;
+}
+
+bool image_writer_flexible_csp(const struct image_writer_opts *opts)
+{
+ if (!opts->tag_csp)
+ return false;
+ return false
+#if HAVE_JPEGXL
+ || opts->format == AV_CODEC_ID_JPEGXL
+#endif
+#if HAVE_AVIF_MUXER
+ || opts->format == AV_CODEC_ID_AV1
+#endif
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 58, 100)
+ // This version added support for cICP tag writing
+ || opts->format == AV_CODEC_ID_PNG
+#endif
+ ;
+}
+
+int image_writer_format_from_ext(const char *ext)
+{
+ for (int n = 0; mp_image_writer_formats[n].name; n++) {
+ if (ext && strcmp(mp_image_writer_formats[n].name, ext) == 0)
+ return mp_image_writer_formats[n].value;
+ }
+ return 0;
+}
+
+static struct mp_image *convert_image(struct mp_image *image, int destfmt,
+ enum mp_csp_levels yuv_levels,
+ const struct image_writer_opts *opts,
+ struct mpv_global *global,
+ struct mp_log *log)
+{
+ int d_w, d_h;
+ mp_image_params_get_dsize(&image->params, &d_w, &d_h);
+
+ struct mp_image_params p = {
+ .imgfmt = destfmt,
+ .w = d_w,
+ .h = d_h,
+ .p_w = 1,
+ .p_h = 1,
+ .color = image->params.color,
+ };
+ mp_image_params_guess_csp(&p);
+
+ if (!image_writer_flexible_csp(opts)) {
+ // If our format can't tag csps, set something sane
+ p.color.primaries = MP_CSP_PRIM_BT_709;
+ p.color.gamma = MP_CSP_TRC_AUTO;
+ p.color.light = MP_CSP_LIGHT_DISPLAY;
+ p.color.hdr = (struct pl_hdr_metadata){0};
+ if (p.color.space != MP_CSP_RGB) {
+ p.color.levels = yuv_levels;
+ p.color.space = MP_CSP_BT_601;
+ p.chroma_location = MP_CHROMA_CENTER;
+ }
+ mp_image_params_guess_csp(&p);
+ }
+
+ if (mp_image_params_equal(&p, &image->params))
+ return mp_image_new_ref(image);
+
+ mp_dbg(log, "will convert image to %s\n", mp_imgfmt_to_name(p.imgfmt));
+
+ struct mp_image *src = image;
+ if (mp_image_crop_valid(&src->params) &&
+ (mp_rect_w(src->params.crop) != src->w ||
+ mp_rect_h(src->params.crop) != src->h))
+ {
+ src = mp_image_new_ref(src);
+ if (!src) {
+ mp_err(log, "mp_image_new_ref failed!\n");
+ return NULL;
+ }
+ mp_image_crop_rc(src, src->params.crop);
+ }
+
+ struct mp_image *dst = mp_image_alloc(p.imgfmt, p.w, p.h);
+ if (!dst) {
+ mp_err(log, "Out of memory.\n");
+ return NULL;
+ }
+ mp_image_copy_attributes(dst, src);
+
+ dst->params = p;
+
+ struct mp_sws_context *sws = mp_sws_alloc(NULL);
+ sws->log = log;
+ if (global)
+ mp_sws_enable_cmdline_opts(sws, global);
+ bool ok = mp_sws_scale(sws, dst, src) >= 0;
+ talloc_free(sws);
+
+ if (src != image)
+ talloc_free(src);
+
+ if (!ok) {
+ mp_err(log, "Error when converting image.\n");
+ talloc_free(dst);
+ return NULL;
+ }
+
+ return dst;
+}
+
+bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
+ const char *filename, struct mpv_global *global,
+ struct mp_log *log)
+{
+ struct image_writer_opts defs = image_writer_opts_defaults;
+ if (!opts)
+ opts = &defs;
+
+ mp_dbg(log, "input: %s\n", mp_image_params_to_str(&image->params));
+
+ struct image_writer_ctx ctx = { log, opts, image->fmt };
+ bool (*write)(struct image_writer_ctx *, mp_image_t *, const char *) = write_lavc;
+ int destfmt = 0;
+
+#if HAVE_JPEG
+ if (opts->format == AV_CODEC_ID_MJPEG) {
+ write = write_jpeg;
+ destfmt = IMGFMT_RGB24;
+ }
+#endif
+#if HAVE_AVIF_MUXER
+ if (opts->format == AV_CODEC_ID_AV1) {
+ write = write_avif;
+ destfmt = mp_imgfmt_from_name(bstr0(opts->avif_pixfmt));
+ }
+#endif
+ if (opts->format == AV_CODEC_ID_WEBP && !opts->webp_lossless) {
+ // For lossy images, libwebp has its own RGB->YUV conversion.
+ // We don't want that, so force YUV/YUVA here.
+ int alpha = image->fmt.flags & MP_IMGFLAG_ALPHA;
+ destfmt = alpha ? pixfmt2imgfmt(AV_PIX_FMT_YUVA420P) : IMGFMT_420P;
+ }
+
+ if (!destfmt)
+ destfmt = get_target_format(&ctx);
+
+ enum mp_csp_levels levels; // Ignored if destfmt is a RGB format
+ if (opts->format == AV_CODEC_ID_WEBP) {
+ levels = MP_CSP_LEVELS_TV;
+ } else {
+ levels = MP_CSP_LEVELS_PC;
+ }
+
+ struct mp_image *dst = convert_image(image, destfmt, levels, opts, global, log);
+ if (!dst)
+ return false;
+
+ bool success = write(&ctx, dst, filename);
+ if (!success)
+ mp_err(log, "Error writing file '%s'!\n", filename);
+
+ talloc_free(dst);
+ return success;
+}
+
+void dump_png(struct mp_image *image, const char *filename, struct mp_log *log)
+{
+ struct image_writer_opts opts = image_writer_opts_defaults;
+ opts.format = AV_CODEC_ID_PNG;
+ write_image(image, &opts, filename, NULL, log);
+}
diff --git a/video/image_writer.h b/video/image_writer.h
new file mode 100644
index 0000000..72d1602
--- /dev/null
+++ b/video/image_writer.h
@@ -0,0 +1,74 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "options/m_option.h"
+
+struct mp_image;
+struct mp_log;
+
+struct image_writer_opts {
+ int format;
+ bool high_bit_depth;
+ int png_compression;
+ int png_filter;
+ int jpeg_quality;
+ bool jpeg_source_chroma;
+ bool webp_lossless;
+ int webp_quality;
+ int webp_compression;
+ double jxl_distance;
+ int jxl_effort;
+ char *avif_encoder;
+ char *avif_pixfmt;
+ char **avif_opts;
+ bool tag_csp;
+};
+
+extern const struct image_writer_opts image_writer_opts_defaults;
+
+extern const struct m_option image_writer_opts[];
+
+// Return the file extension that will be used, e.g. "png".
+const char *image_writer_file_ext(const struct image_writer_opts *opts);
+
+// Return whether the selected format likely supports >8 bit per component.
+bool image_writer_high_depth(const struct image_writer_opts *opts);
+
+// Return whether the selected format likely supports non-sRGB colorspaces
+bool image_writer_flexible_csp(const struct image_writer_opts *opts);
+
+// Map file extension to format ID - return 0 (which is invalid) if unknown.
+int image_writer_format_from_ext(const char *ext);
+
+/*
+ * Save the given image under the given filename. The parameters csp and opts
+ * are optional. All pixel formats supported by swscale are supported.
+ *
+ * File format and compression settings are controlled via the opts parameter.
+ *
+ * If global!=NULL, use command line scaler options etc.
+ *
+ * NOTE: The fields w/h/width/height of the passed mp_image must be all set
+ * accordingly. Setting w and width or h and height to different values
+ * can be used to store snapshots of anamorphic video.
+ */
+bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
+ const char *filename, struct mpv_global *global,
+ struct mp_log *log);
+
+// Debugging helper.
+void dump_png(struct mp_image *image, const char *filename, struct mp_log *log);
diff --git a/video/img_format.c b/video/img_format.c
new file mode 100644
index 0000000..6b7857f
--- /dev/null
+++ b/video/img_format.c
@@ -0,0 +1,824 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/imgutils.h>
+#include <libavutil/pixfmt.h>
+#include <libavutil/pixdesc.h>
+
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/fmt-conversion.h"
+
+struct mp_imgfmt_entry {
+ const char *name;
+ // Valid if flags!=0.
+ // This can be incomplete, and missing fields are filled in:
+ // - sets num_planes and bpp[], derived from comps[] (rounds to bytes)
+ // - sets MP_IMGFLAG_GRAY, derived from comps[]
+ // - sets MP_IMGFLAG_ALPHA, derived from comps[]
+ // - sets align_x/y if 0, derived from chroma shift
+ // - sets xs[]/ys[] always, derived from num_planes/chroma_shift
+ // - sets MP_IMGFLAG_HAS_COMPS|MP_IMGFLAG_NE if num_planes>0
+ // - sets MP_IMGFLAG_TYPE_UINT if no other type set
+ // - sets id to mp_imgfmt_list[] implied format
+ struct mp_imgfmt_desc desc;
+};
+
+#define FRINGE_GBRP(def, dname, b) \
+ [def - IMGFMT_CUST_BASE] = { \
+ .name = dname, \
+ .desc = { .flags = MP_IMGFLAG_COLOR_RGB, \
+ .comps = { {2, 0, 8, (b) - 8}, {0, 0, 8, (b) - 8}, \
+ {1, 0, 8, (b) - 8}, }, }}
+
+#define FLOAT_YUV(def, dname, xs, ys, a) \
+ [def - IMGFMT_CUST_BASE] = { \
+ .name = dname, \
+ .desc = { .flags = MP_IMGFLAG_COLOR_YUV | MP_IMGFLAG_TYPE_FLOAT, \
+ .chroma_xs = xs, .chroma_ys = ys, \
+ .comps = { {0, 0, 32}, {1, 0, 32}, {2, 0, 32}, \
+ {3 * (a), 0, 32 * (a)} }, }}
+
+static const struct mp_imgfmt_entry mp_imgfmt_list[] = {
+ // not in ffmpeg
+ [IMGFMT_VDPAU_OUTPUT - IMGFMT_CUST_BASE] = {
+ .name = "vdpau_output",
+ .desc = {
+ .flags = MP_IMGFLAG_NE | MP_IMGFLAG_RGB | MP_IMGFLAG_HWACCEL,
+ },
+ },
+ [IMGFMT_RGB30 - IMGFMT_CUST_BASE] = {
+ .name = "rgb30",
+ .desc = {
+ .flags = MP_IMGFLAG_RGB,
+ .comps = { {0, 20, 10}, {0, 10, 10}, {0, 0, 10} },
+ },
+ },
+ [IMGFMT_YAP8 - IMGFMT_CUST_BASE] = {
+ .name = "yap8",
+ .desc = {
+ .flags = MP_IMGFLAG_COLOR_YUV,
+ .comps = { {0, 0, 8}, {0}, {0}, {1, 0, 8} },
+ },
+ },
+ [IMGFMT_YAP16 - IMGFMT_CUST_BASE] = {
+ .name = "yap16",
+ .desc = {
+ .flags = MP_IMGFLAG_COLOR_YUV,
+ .comps = { {0, 0, 16}, {0}, {0}, {1, 0, 16} },
+ },
+ },
+ [IMGFMT_Y1 - IMGFMT_CUST_BASE] = {
+ .name = "y1",
+ .desc = {
+ .flags = MP_IMGFLAG_COLOR_RGB,
+ .comps = { {0, 0, 8, -7} },
+ },
+ },
+ [IMGFMT_YAPF - IMGFMT_CUST_BASE] = {
+ .name = "grayaf32", // try to mimic ffmpeg naming convention
+ .desc = {
+ .flags = MP_IMGFLAG_COLOR_YUV | MP_IMGFLAG_TYPE_FLOAT,
+ .comps = { {0, 0, 32}, {0}, {0}, {1, 0, 32} },
+ },
+ },
+ FLOAT_YUV(IMGFMT_444PF, "yuv444pf", 0, 0, 0),
+ FLOAT_YUV(IMGFMT_444APF, "yuva444pf", 0, 0, 1),
+ FLOAT_YUV(IMGFMT_420PF, "yuv420pf", 1, 1, 0),
+ FLOAT_YUV(IMGFMT_420APF, "yuva420pf", 1, 1, 1),
+ FLOAT_YUV(IMGFMT_422PF, "yuv422pf", 1, 0, 0),
+ FLOAT_YUV(IMGFMT_422APF, "yuva422pf", 1, 0, 1),
+ FLOAT_YUV(IMGFMT_440PF, "yuv440pf", 0, 1, 0),
+ FLOAT_YUV(IMGFMT_440APF, "yuva440pf", 0, 1, 1),
+ FLOAT_YUV(IMGFMT_410PF, "yuv410pf", 2, 2, 0),
+ FLOAT_YUV(IMGFMT_410APF, "yuva410pf", 2, 2, 1),
+ FLOAT_YUV(IMGFMT_411PF, "yuv411pf", 2, 0, 0),
+ FLOAT_YUV(IMGFMT_411APF, "yuva411pf", 2, 0, 1),
+ FRINGE_GBRP(IMGFMT_GBRP1, "gbrp1", 1),
+ FRINGE_GBRP(IMGFMT_GBRP2, "gbrp2", 2),
+ FRINGE_GBRP(IMGFMT_GBRP3, "gbrp3", 3),
+ FRINGE_GBRP(IMGFMT_GBRP4, "gbrp4", 4),
+ FRINGE_GBRP(IMGFMT_GBRP5, "gbrp5", 5),
+ FRINGE_GBRP(IMGFMT_GBRP6, "gbrp6", 6),
+ // in FFmpeg, but FFmpeg names have an annoying "_vld" suffix
+ [IMGFMT_VIDEOTOOLBOX - IMGFMT_CUST_BASE] = {
+ .name = "videotoolbox",
+ },
+ [IMGFMT_VAAPI - IMGFMT_CUST_BASE] = {
+ .name = "vaapi",
+ },
+};
+
+static const struct mp_imgfmt_entry *get_mp_desc(int imgfmt)
+{
+ if (imgfmt < IMGFMT_CUST_BASE)
+ return NULL;
+ int index = imgfmt - IMGFMT_CUST_BASE;
+ if (index >= MP_ARRAY_SIZE(mp_imgfmt_list))
+ return NULL;
+ const struct mp_imgfmt_entry *e = &mp_imgfmt_list[index];
+ return e->name ? e : NULL;
+}
+
+char **mp_imgfmt_name_list(void)
+{
+ int count = IMGFMT_END - IMGFMT_START;
+ char **list = talloc_zero_array(NULL, char *, count + 1);
+ int num = 0;
+ for (int n = IMGFMT_START; n < IMGFMT_END; n++) {
+ const char *name = mp_imgfmt_to_name(n);
+ if (strcmp(name, "unknown") != 0)
+ list[num++] = talloc_strdup(list, name);
+ }
+ return list;
+}
+
+int mp_imgfmt_from_name(bstr name)
+{
+ if (bstr_equals0(name, "none"))
+ return 0;
+ for (int n = 0; n < MP_ARRAY_SIZE(mp_imgfmt_list); n++) {
+ const struct mp_imgfmt_entry *p = &mp_imgfmt_list[n];
+ if (p->name && bstr_equals0(name, p->name))
+ return IMGFMT_CUST_BASE + n;
+ }
+ return pixfmt2imgfmt(av_get_pix_fmt(mp_tprintf(80, "%.*s", BSTR_P(name))));
+}
+
+char *mp_imgfmt_to_name_buf(char *buf, size_t buf_size, int fmt)
+{
+ const struct mp_imgfmt_entry *p = get_mp_desc(fmt);
+ const char *name = p ? p->name : NULL;
+ if (!name) {
+ const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(imgfmt2pixfmt(fmt));
+ if (pixdesc)
+ name = pixdesc->name;
+ }
+ if (!name)
+ name = "unknown";
+ snprintf(buf, buf_size, "%s", name);
+ int len = strlen(buf);
+ if (len > 2 && buf[len - 2] == MP_SELECT_LE_BE('l', 'b') && buf[len - 1] == 'e')
+ buf[len - 2] = '\0';
+ return buf;
+}
+
+static void fill_pixdesc_layout(struct mp_imgfmt_desc *desc,
+ enum AVPixelFormat fmt,
+ const AVPixFmtDescriptor *pd)
+{
+ if (pd->flags & AV_PIX_FMT_FLAG_PAL ||
+ pd->flags & AV_PIX_FMT_FLAG_HWACCEL)
+ goto fail;
+
+ bool has_alpha = pd->flags & AV_PIX_FMT_FLAG_ALPHA;
+ if (pd->nb_components != 1 + has_alpha &&
+ pd->nb_components != 3 + has_alpha)
+ goto fail;
+
+ // Very convenient: we assume we're always on little endian, and FFmpeg
+ // explicitly marks big endian formats => don't need to guess whether a
+ // format is little endian, or not affected by byte order.
+ bool is_be = pd->flags & AV_PIX_FMT_FLAG_BE;
+ bool is_ne = MP_SELECT_LE_BE(false, true) == is_be;
+
+ // Packed sub-sampled YUV is very... special.
+ bool is_packed_ss_yuv = pd->log2_chroma_w && !pd->log2_chroma_h &&
+ pd->comp[1].plane == 0 && pd->comp[2].plane == 0 &&
+ pd->nb_components == 3;
+
+ if (is_packed_ss_yuv)
+ desc->bpp[0] = pd->comp[1].step * 8;
+
+ // Determine if there are any byte overlaps => relevant for determining
+ // access unit for endian, since pixdesc does not expose this, and assumes
+ // a weird model where you do separate memory fetches for each component.
+ bool any_shared_bytes = !!(pd->flags & AV_PIX_FMT_FLAG_BITSTREAM);
+ for (int c = 0; c < pd->nb_components; c++) {
+ for (int i = 0; i < c; i++) {
+ const AVComponentDescriptor *d1 = &pd->comp[c];
+ const AVComponentDescriptor *d2 = &pd->comp[i];
+ if (d1->plane == d2->plane) {
+ if (d1->offset + (d1->depth + 7) / 8u > d2->offset &&
+ d2->offset + (d2->depth + 7) / 8u > d1->offset)
+ any_shared_bytes = true;
+ }
+ }
+ }
+
+ int el_bits = (pd->flags & AV_PIX_FMT_FLAG_BITSTREAM) ? 1 : 8;
+ for (int c = 0; c < pd->nb_components; c++) {
+ const AVComponentDescriptor *d = &pd->comp[c];
+ if (d->plane >= MP_MAX_PLANES)
+ goto fail;
+
+ desc->num_planes = MPMAX(desc->num_planes, d->plane + 1);
+
+ int plane_bits = desc->bpp[d->plane];
+ int c_bits = d->step * el_bits;
+
+ // The first component wins, because either all components result in
+ // the same value, or luma wins (luma always comes before chroma).
+ if (plane_bits) {
+ if (c_bits > plane_bits)
+ goto fail; // inconsistent
+ } else {
+ desc->bpp[d->plane] = plane_bits = c_bits;
+ }
+
+ int shift = d->shift;
+ // What the fuck: for some inexplicable reason, MONOB uses shift=7
+ // in pixdesc, which is basically out of bounds. Pixdesc bug?
+ // Make it behave like MONOW. (No, the bit-order is not different.)
+ if (fmt == AV_PIX_FMT_MONOBLACK)
+ shift = 0;
+
+ int offset = d->offset * el_bits;
+ // The pixdesc logic for reading and endian swapping is as follows
+ // (reverse engineered from av_read_image_line2()):
+ // - determine a word size that will include the component fully;
+ // this includes the "active" bits and the amount "shifted" away
+ // (for example shift=7/depth=18 => 32 bit word reading [31:0])
+ // - the same format can use different word sizes (e.g. bgr565: the R
+ // component at offset 0 is read as 8 bit; BG is read as 16 bits)
+ // - if BE flag is set, swap the word before proceeding
+ // - extract via shift and mask derived by depth
+ int word = mp_round_next_power_of_2(MPMAX(d->depth + shift, 8));
+ // The purpose of this is unknown. It's an absurdity fished out of
+ // av_read_image_line2()'s implementation. It seems technically
+ // unnecessary, and provides no information. On the other hand, it
+ // compensates for seemingly bogus packed integer pixdescs; this
+ // is "why" some formats use d->offset = -1.
+ if (is_be && el_bits == 8 && word == 8)
+ offset += 8;
+ // Pixdesc's model sometimes requires accesses with varying word-sizes,
+ // as seen in bgr565 and other formats. Also, it makes you read some
+ // formats with multiple endian-dependent accesses, where accessing a
+ // larger unit would make more sense. (Consider X2RGB10BE, for which
+ // pixdesc wants you to perform 3 * 2 byte accesses, and swap each of
+ // the read 16 bit words. What you really want is to swap the entire 4
+ // byte thing, and then extract the components with bit shifts).
+ // This is complete bullshit, so we transform it into word swaps before
+ // further processing. Care needs to be taken to not change formats like
+ // P010 or YA16 (prefer component accesses for them; P010 isn't even
+ // representable, because endian_shift is for all planes).
+ // As a heuristic, assume that if any components share a byte, the whole
+ // pixel is read as a single memory access and endian swapped at once.
+ int access_size = 8;
+ if (plane_bits > 8) {
+ if (any_shared_bytes) {
+ access_size = plane_bits;
+ if (is_be && word != access_size) {
+ // Before: offset = 8*byte_offset (with word bits of data)
+ // After: offset = bit_offset into swapped endian_size word
+ offset = access_size - word - offset;
+ }
+ } else {
+ access_size = word;
+ }
+ }
+ int endian_size = (access_size && !is_ne) ? access_size : 8;
+ int endian_shift = mp_log2(endian_size) - 3;
+ if (!MP_IS_POWER_OF_2(endian_size) || endian_shift < 0 || endian_shift > 3)
+ goto fail;
+ if (desc->endian_shift && desc->endian_shift != endian_shift)
+ goto fail;
+ desc->endian_shift = endian_shift;
+
+ // We always use bit offsets; this doesn't lose any information,
+ // and pixdesc is merely more redundant.
+ offset += shift;
+ if (offset < 0 || offset >= (1 << 6))
+ goto fail;
+ if (offset + d->depth > plane_bits)
+ goto fail;
+ if (d->depth < 0 || d->depth >= (1 << 6))
+ goto fail;
+ desc->comps[c] = (struct mp_imgfmt_comp_desc){
+ .plane = d->plane,
+ .offset = offset,
+ .size = d->depth,
+ };
+ }
+
+ for (int p = 0; p < desc->num_planes; p++) {
+ if (!desc->bpp[p])
+ goto fail; // plane doesn't exist
+ }
+
+ // What the fuck: this is probably a pixdesc bug, so fix it.
+ if (fmt == AV_PIX_FMT_RGB8) {
+ desc->comps[2] = (struct mp_imgfmt_comp_desc){0, 0, 2};
+ desc->comps[1] = (struct mp_imgfmt_comp_desc){0, 2, 3};
+ desc->comps[0] = (struct mp_imgfmt_comp_desc){0, 5, 3};
+ }
+
+ // Overlap test. If any shared bits are happening, this is not a format we
+ // can represent (or it's something like Bayer: components in the same bits,
+ // but different alternating lines).
+ bool any_shared_bits = false;
+ for (int c = 0; c < pd->nb_components; c++) {
+ for (int i = 0; i < c; i++) {
+ struct mp_imgfmt_comp_desc *c1 = &desc->comps[c];
+ struct mp_imgfmt_comp_desc *c2 = &desc->comps[i];
+ if (c1->plane == c2->plane) {
+ if (c1->offset + c1->size > c2->offset &&
+ c2->offset + c2->size > c1->offset)
+ any_shared_bits = true;
+ }
+ }
+ }
+
+ if (any_shared_bits) {
+ for (int c = 0; c < pd->nb_components; c++)
+ desc->comps[c] = (struct mp_imgfmt_comp_desc){0};
+ }
+
+ // Many important formats have padding within an access word. For example
+ // yuv420p10 has the upper 6 bit cleared to 0; P010 has the lower 6 bits
+ // cleared to 0. Pixdesc cannot represent that these bits are 0. There are
+ // other formats where padding is not guaranteed to be 0, but they are
+ // described in the same way.
+ // Apply a heuristic that is supposed to identify formats which use
+ // guaranteed 0 padding. This could fail, but nobody said this pixdesc crap
+ // is robust.
+ for (int c = 0; c < pd->nb_components; c++) {
+ struct mp_imgfmt_comp_desc *cd = &desc->comps[c];
+ // Note: rgb444 would defeat our heuristic if we checked only per comp.
+ // also, exclude "bitstream" formats due to monow/monob
+ int fsize = MP_ALIGN_UP(cd->size, 8);
+ if (!any_shared_bytes && el_bits == 8 && fsize != cd->size &&
+ fsize - cd->size <= (1 << 3))
+ {
+ if (!(cd->offset % 8u)) {
+ cd->pad = -(fsize - cd->size);
+ cd->size = fsize;
+ } else if (!((cd->offset + cd->size) % 8u)) {
+ cd->pad = fsize - cd->size;
+ cd->size = fsize;
+ cd->offset = MP_ALIGN_DOWN(cd->offset, 8);
+ }
+ }
+ }
+
+ // The alpha component always has ID 4 (index 3) in our representation, so
+ // move the alpha component to there.
+ if (has_alpha && pd->nb_components < 4) {
+ desc->comps[3] = desc->comps[pd->nb_components - 1];
+ desc->comps[pd->nb_components - 1] = (struct mp_imgfmt_comp_desc){0};
+ }
+
+ if (is_packed_ss_yuv) {
+ desc->flags |= MP_IMGFLAG_PACKED_SS_YUV;
+ desc->bpp[0] /= 1 << pd->log2_chroma_w;
+ } else if (!any_shared_bits) {
+ desc->flags |= MP_IMGFLAG_HAS_COMPS;
+ }
+
+ return;
+
+fail:
+ for (int n = 0; n < 4; n++)
+ desc->comps[n] = (struct mp_imgfmt_comp_desc){0};
+ // Average bit size fallback.
+ desc->num_planes = av_pix_fmt_count_planes(fmt);
+ for (int p = 0; p < desc->num_planes; p++) {
+ int ls = av_image_get_linesize(fmt, 256, p);
+ desc->bpp[p] = ls > 0 ? ls * 8 / 256 : 0;
+ }
+}
+
+static bool mp_imgfmt_get_desc_from_pixdesc(int mpfmt, struct mp_imgfmt_desc *out)
+{
+ enum AVPixelFormat fmt = imgfmt2pixfmt(mpfmt);
+ const AVPixFmtDescriptor *pd = av_pix_fmt_desc_get(fmt);
+ if (!pd || pd->nb_components > 4)
+ return false;
+
+ struct mp_imgfmt_desc desc = {
+ .id = mpfmt,
+ .chroma_xs = pd->log2_chroma_w,
+ .chroma_ys = pd->log2_chroma_h,
+ };
+
+ if (pd->flags & AV_PIX_FMT_FLAG_ALPHA)
+ desc.flags |= MP_IMGFLAG_ALPHA;
+
+ if (pd->flags & AV_PIX_FMT_FLAG_HWACCEL)
+ desc.flags |= MP_IMGFLAG_TYPE_HW;
+
+ // Pixdesc does not provide a flag for XYZ, so this is the best we can do.
+ if (strncmp(pd->name, "xyz", 3) == 0) {
+ desc.flags |= MP_IMGFLAG_COLOR_XYZ;
+ } else if (pd->flags & AV_PIX_FMT_FLAG_RGB) {
+ desc.flags |= MP_IMGFLAG_COLOR_RGB;
+ } else if (fmt == AV_PIX_FMT_MONOBLACK || fmt == AV_PIX_FMT_MONOWHITE) {
+ desc.flags |= MP_IMGFLAG_COLOR_RGB;
+ } else if (fmt == AV_PIX_FMT_PAL8) {
+ desc.flags |= MP_IMGFLAG_COLOR_RGB | MP_IMGFLAG_TYPE_PAL8;
+ }
+
+ if (pd->flags & AV_PIX_FMT_FLAG_FLOAT)
+ desc.flags |= MP_IMGFLAG_TYPE_FLOAT;
+
+ // Educated guess.
+ if (!(desc.flags & MP_IMGFLAG_COLOR_MASK) &&
+ !(desc.flags & MP_IMGFLAG_TYPE_HW))
+ desc.flags |= MP_IMGFLAG_COLOR_YUV;
+
+ desc.align_x = 1 << desc.chroma_xs;
+ desc.align_y = 1 << desc.chroma_ys;
+
+ fill_pixdesc_layout(&desc, fmt, pd);
+
+ if (desc.flags & (MP_IMGFLAG_HAS_COMPS | MP_IMGFLAG_PACKED_SS_YUV)) {
+ if (!(desc.flags & MP_IMGFLAG_TYPE_MASK))
+ desc.flags |= MP_IMGFLAG_TYPE_UINT;
+ }
+
+ if (desc.bpp[0] % 8u && (pd->flags & AV_PIX_FMT_FLAG_BITSTREAM))
+ desc.align_x = 8 / desc.bpp[0]; // expect power of 2
+
+ // Very heuristical.
+ bool is_ne = !desc.endian_shift;
+ bool need_endian = (desc.comps[0].size % 8u && desc.bpp[0] > 8) ||
+ desc.comps[0].size > 8;
+
+ if (need_endian) {
+ bool is_le = MP_SELECT_LE_BE(is_ne, !is_ne);
+ desc.flags |= is_le ? MP_IMGFLAG_LE : MP_IMGFLAG_BE;
+ } else {
+ desc.flags |= MP_IMGFLAG_LE | MP_IMGFLAG_BE;
+ }
+
+ *out = desc;
+ return true;
+}
+
+bool mp_imgfmt_get_packed_yuv_locations(int imgfmt, uint8_t *luma_offsets)
+{
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(imgfmt);
+ if (!(desc.flags & MP_IMGFLAG_PACKED_SS_YUV))
+ return false;
+
+ assert(desc.num_planes == 1);
+
+ // Guess at which positions the additional luma samples are. We iterate
+ // starting with the first byte, and then put a luma sample at places
+ // not covered by other luma/chroma.
+ // Pixdesc does not and can not provide this information. This heuristic
+ // may fail in certain cases. What a load of bullshit, right?
+ int lsize = desc.comps[0].size;
+ int cur_offset = 0;
+ for (int lsample = 1; lsample < (1 << desc.chroma_xs); lsample++) {
+ while (1) {
+ if (cur_offset + lsize > desc.bpp[0] * desc.align_x)
+ return false;
+ bool free = true;
+ for (int c = 0; c < 3; c++) {
+ struct mp_imgfmt_comp_desc *cd = &desc.comps[c];
+ if (!cd->size)
+ continue;
+ if (cd->offset + cd->size > cur_offset &&
+ cur_offset + lsize > cd->offset)
+ {
+ free = false;
+ break;
+ }
+ }
+ if (free)
+ break;
+ cur_offset += lsize;
+ }
+ luma_offsets[lsample] = cur_offset;
+ cur_offset += lsize;
+ }
+
+ luma_offsets[0] = desc.comps[0].offset;
+ return true;
+}
+
+static bool get_native_desc(int mpfmt, struct mp_imgfmt_desc *desc)
+{
+ const struct mp_imgfmt_entry *p = get_mp_desc(mpfmt);
+ if (!p || !p->desc.flags)
+ return false;
+
+ *desc = p->desc;
+
+ // Fill in some fields mp_imgfmt_entry.desc is not required to set.
+
+ desc->id = mpfmt;
+
+ for (int n = 0; n < MP_NUM_COMPONENTS; n++) {
+ struct mp_imgfmt_comp_desc *cd = &desc->comps[n];
+ if (cd->size)
+ desc->num_planes = MPMAX(desc->num_planes, cd->plane + 1);
+ desc->bpp[cd->plane] =
+ MPMAX(desc->bpp[cd->plane], MP_ALIGN_UP(cd->offset + cd->size, 8));
+ }
+
+ if (!desc->align_x && !desc->align_y) {
+ desc->align_x = 1 << desc->chroma_xs;
+ desc->align_y = 1 << desc->chroma_ys;
+ }
+
+ if (desc->num_planes)
+ desc->flags |= MP_IMGFLAG_HAS_COMPS | MP_IMGFLAG_NE;
+
+ if (!(desc->flags & MP_IMGFLAG_TYPE_MASK))
+ desc->flags |= MP_IMGFLAG_TYPE_UINT;
+
+ return true;
+}
+
+int mp_imgfmt_desc_get_num_comps(struct mp_imgfmt_desc *desc)
+{
+ int flags = desc->flags;
+ if (!(flags & MP_IMGFLAG_COLOR_MASK))
+ return 0;
+ return 3 + (flags & MP_IMGFLAG_GRAY ? -2 : 0) + !!(flags & MP_IMGFLAG_ALPHA);
+}
+
+struct mp_imgfmt_desc mp_imgfmt_get_desc(int mpfmt)
+{
+ struct mp_imgfmt_desc desc;
+
+ if (!get_native_desc(mpfmt, &desc) &&
+ !mp_imgfmt_get_desc_from_pixdesc(mpfmt, &desc))
+ return (struct mp_imgfmt_desc){0};
+
+ for (int p = 0; p < desc.num_planes; p++) {
+ desc.xs[p] = (p == 1 || p == 2) ? desc.chroma_xs : 0;
+ desc.ys[p] = (p == 1 || p == 2) ? desc.chroma_ys : 0;
+ }
+
+ bool is_ba = desc.num_planes > 0;
+ for (int p = 0; p < desc.num_planes; p++)
+ is_ba = !(desc.bpp[p] % 8u);
+
+ if (is_ba)
+ desc.flags |= MP_IMGFLAG_BYTE_ALIGNED;
+
+ if (desc.flags & MP_IMGFLAG_HAS_COMPS) {
+ if (desc.comps[3].size)
+ desc.flags |= MP_IMGFLAG_ALPHA;
+
+ // Assuming all colors are (CCC+[A]) or (C+[A]), the latter being gray.
+ if (!desc.comps[1].size)
+ desc.flags |= MP_IMGFLAG_GRAY;
+
+ bool bb = true;
+ for (int n = 0; n < MP_NUM_COMPONENTS; n++) {
+ if (desc.comps[n].offset % 8u || desc.comps[n].size % 8u)
+ bb = false;
+ }
+ if (bb)
+ desc.flags |= MP_IMGFLAG_BYTES;
+ }
+
+ if ((desc.flags & (MP_IMGFLAG_YUV | MP_IMGFLAG_RGB))
+ && (desc.flags & MP_IMGFLAG_HAS_COMPS)
+ && (desc.flags & MP_IMGFLAG_BYTES)
+ && ((desc.flags & MP_IMGFLAG_TYPE_MASK) == MP_IMGFLAG_TYPE_UINT))
+ {
+ int cnt = mp_imgfmt_desc_get_num_comps(&desc);
+ bool same_depth = true;
+ for (int p = 0; p < desc.num_planes; p++)
+ same_depth &= desc.bpp[p] == desc.bpp[0];
+ if (same_depth && cnt == desc.num_planes) {
+ if (desc.flags & MP_IMGFLAG_YUV) {
+ desc.flags |= MP_IMGFLAG_YUV_P;
+ } else {
+ desc.flags |= MP_IMGFLAG_RGB_P;
+ }
+ }
+ if (cnt == 3 && desc.num_planes == 2 &&
+ desc.bpp[1] == desc.bpp[0] * 2 &&
+ (desc.flags & MP_IMGFLAG_YUV))
+ {
+
+ desc.flags |= MP_IMGFLAG_YUV_NV;
+ }
+ }
+
+ return desc;
+}
+
+static bool validate_regular_imgfmt(const struct mp_regular_imgfmt *fmt)
+{
+ bool present[MP_NUM_COMPONENTS] = {0};
+ int n_comp = 0;
+
+ for (int n = 0; n < fmt->num_planes; n++) {
+ const struct mp_regular_imgfmt_plane *plane = &fmt->planes[n];
+ n_comp += plane->num_components;
+ if (n_comp > MP_NUM_COMPONENTS)
+ return false;
+ if (!plane->num_components)
+ return false; // no empty planes in between allowed
+
+ bool pad_only = true;
+ int chroma_luma = 0; // luma: 1, chroma: 2, both: 3
+ for (int i = 0; i < plane->num_components; i++) {
+ int comp = plane->components[i];
+ if (comp > MP_NUM_COMPONENTS)
+ return false;
+ if (comp == 0)
+ continue;
+ pad_only = false;
+ if (present[comp - 1])
+ return false; // no duplicates
+ present[comp - 1] = true;
+ chroma_luma |= (comp == 2 || comp == 3) ? 2 : 1;
+ }
+ if (pad_only)
+ return false; // no planes with only padding allowed
+ if ((fmt->chroma_xs > 0 || fmt->chroma_ys > 0) && chroma_luma == 3)
+ return false; // separate chroma/luma planes required
+ }
+
+ if (!(present[0] || present[3]) || // at least component 1 or alpha needed
+ (present[1] && !present[0]) || // component 2 requires component 1
+ (present[2] && !present[1])) // component 3 requires component 2
+ return false;
+
+ return true;
+}
+
+static enum mp_csp get_forced_csp_from_flags(int flags)
+{
+ if (flags & MP_IMGFLAG_COLOR_XYZ)
+ return MP_CSP_XYZ;
+
+ if (flags & MP_IMGFLAG_COLOR_RGB)
+ return MP_CSP_RGB;
+
+ return MP_CSP_AUTO;
+}
+
+enum mp_csp mp_imgfmt_get_forced_csp(int imgfmt)
+{
+ return get_forced_csp_from_flags(mp_imgfmt_get_desc(imgfmt).flags);
+}
+
+static enum mp_component_type get_component_type_from_flags(int flags)
+{
+ if (flags & MP_IMGFLAG_TYPE_UINT)
+ return MP_COMPONENT_TYPE_UINT;
+
+ if (flags & MP_IMGFLAG_TYPE_FLOAT)
+ return MP_COMPONENT_TYPE_FLOAT;
+
+ return MP_COMPONENT_TYPE_UNKNOWN;
+}
+
+enum mp_component_type mp_imgfmt_get_component_type(int imgfmt)
+{
+ return get_component_type_from_flags(mp_imgfmt_get_desc(imgfmt).flags);
+}
+
+int mp_find_other_endian(int imgfmt)
+{
+ return pixfmt2imgfmt(av_pix_fmt_swap_endianness(imgfmt2pixfmt(imgfmt)));
+}
+
+bool mp_get_regular_imgfmt(struct mp_regular_imgfmt *dst, int imgfmt)
+{
+ struct mp_regular_imgfmt res = {0};
+
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(imgfmt);
+ if (!desc.num_planes)
+ return false;
+ res.num_planes = desc.num_planes;
+
+ if (desc.endian_shift || !(desc.flags & MP_IMGFLAG_HAS_COMPS))
+ return false;
+
+ res.component_type = get_component_type_from_flags(desc.flags);
+ if (!res.component_type)
+ return false;
+
+ struct mp_imgfmt_comp_desc *comp0 = &desc.comps[0];
+ if (comp0->size < 1 || comp0->size > 64 || (comp0->size % 8u))
+ return false;
+
+ res.component_size = comp0->size / 8u;
+ res.component_pad = comp0->pad;
+
+ for (int n = 0; n < res.num_planes; n++) {
+ if (desc.bpp[n] % comp0->size)
+ return false;
+ res.planes[n].num_components = desc.bpp[n] / comp0->size;
+ }
+
+ for (int n = 0; n < MP_NUM_COMPONENTS; n++) {
+ struct mp_imgfmt_comp_desc *comp = &desc.comps[n];
+ if (!comp->size)
+ continue;
+
+ struct mp_regular_imgfmt_plane *plane = &res.planes[comp->plane];
+
+ res.num_planes = MPMAX(res.num_planes, comp->plane + 1);
+
+ // We support uniform depth only.
+ if (comp->size != comp0->size || comp->pad != comp0->pad)
+ return false;
+
+ // Size-aligned only.
+ int pos = comp->offset / comp->size;
+ if (comp->offset != pos * comp->size || pos >= MP_NUM_COMPONENTS)
+ return false;
+
+ if (plane->components[pos])
+ return false;
+ plane->components[pos] = n + 1;
+ }
+
+ res.chroma_xs = desc.chroma_xs;
+ res.chroma_ys = desc.chroma_ys;
+
+ res.forced_csp = get_forced_csp_from_flags(desc.flags);
+
+ if (!validate_regular_imgfmt(&res))
+ return false;
+
+ *dst = res;
+ return true;
+}
+
+static bool regular_imgfmt_equals(struct mp_regular_imgfmt *a,
+ struct mp_regular_imgfmt *b)
+{
+ if (a->component_type != b->component_type ||
+ a->component_size != b->component_size ||
+ a->num_planes != b->num_planes ||
+ a->component_pad != b->component_pad ||
+ a->forced_csp != b->forced_csp ||
+ a->chroma_xs != b->chroma_xs ||
+ a->chroma_ys != b->chroma_ys)
+ return false;
+
+ for (int n = 0; n < a->num_planes; n++) {
+ int num_comps = a->planes[n].num_components;
+ if (num_comps != b->planes[n].num_components)
+ return false;
+ for (int i = 0; i < num_comps; i++) {
+ if (a->planes[n].components[i] != b->planes[n].components[i])
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Find a format that matches this one exactly.
+int mp_find_regular_imgfmt(struct mp_regular_imgfmt *src)
+{
+ for (int n = IMGFMT_START + 1; n < IMGFMT_END; n++) {
+ struct mp_regular_imgfmt f;
+ if (mp_get_regular_imgfmt(&f, n) && regular_imgfmt_equals(src, &f))
+ return n;
+ }
+ return 0;
+}
+
+// Compare the dst image formats, and return the one which can carry more data
+// (e.g. higher depth, more color components, lower chroma subsampling, etc.),
+// with respect to what is required to keep most of the src format.
+// Returns the imgfmt, or 0 on error.
+int mp_imgfmt_select_best(int dst1, int dst2, int src)
+{
+ enum AVPixelFormat dst1pxf = imgfmt2pixfmt(dst1);
+ enum AVPixelFormat dst2pxf = imgfmt2pixfmt(dst2);
+ enum AVPixelFormat srcpxf = imgfmt2pixfmt(src);
+ enum AVPixelFormat dstlist[] = {dst1pxf, dst2pxf, AV_PIX_FMT_NONE};
+ return pixfmt2imgfmt(avcodec_find_best_pix_fmt_of_list(dstlist, srcpxf, 1, 0));
+}
+
+// Same as mp_imgfmt_select_best(), but with a list of dst formats.
+int mp_imgfmt_select_best_list(int *dst, int num_dst, int src)
+{
+ int best = 0;
+ for (int n = 0; n < num_dst; n++)
+ best = best ? mp_imgfmt_select_best(best, dst[n], src) : dst[n];
+ return best;
+}
diff --git a/video/img_format.h b/video/img_format.h
new file mode 100644
index 0000000..0753829
--- /dev/null
+++ b/video/img_format.h
@@ -0,0 +1,342 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_IMG_FORMAT_H
+#define MPLAYER_IMG_FORMAT_H
+
+#include <inttypes.h>
+
+#include "config.h"
+#include "osdep/endian.h"
+#include "misc/bstr.h"
+#include "video/csputils.h"
+
+#define MP_MAX_PLANES 4
+#define MP_NUM_COMPONENTS 4
+
+// mp_imgfmt_desc.comps[] is set to useful values. Some types of formats will
+// use comps[], but not set this flag, because it doesn't cover all requirements
+// (for example MP_IMGFLAG_PACKED_SS_YUV).
+#define MP_IMGFLAG_HAS_COMPS (1 << 0)
+
+// all components start on byte boundaries
+#define MP_IMGFLAG_BYTES (1 << 1)
+
+// all pixels start in byte boundaries
+#define MP_IMGFLAG_BYTE_ALIGNED (1 << 2)
+
+// set if in little endian, or endian independent
+#define MP_IMGFLAG_LE (1 << 3)
+
+// set if in big endian, or endian independent
+#define MP_IMGFLAG_BE (1 << 4)
+
+// set if in native (host) endian, or endian independent
+#define MP_IMGFLAG_NE MP_SELECT_LE_BE(MP_IMGFLAG_LE, MP_IMGFLAG_BE)
+
+// set if an alpha component is included
+#define MP_IMGFLAG_ALPHA (1 << 5)
+
+// color class flags - can use via bit tests, or use the mask and compare
+#define MP_IMGFLAG_COLOR_MASK (15 << 6)
+#define MP_IMGFLAG_COLOR_YUV (1 << 6)
+#define MP_IMGFLAG_COLOR_RGB (2 << 6)
+#define MP_IMGFLAG_COLOR_XYZ (4 << 6)
+
+// component type flags (same access conventions as MP_IMGFLAG_COLOR_*)
+#define MP_IMGFLAG_TYPE_MASK (15 << 10)
+#define MP_IMGFLAG_TYPE_UINT (1 << 10)
+#define MP_IMGFLAG_TYPE_FLOAT (2 << 10)
+#define MP_IMGFLAG_TYPE_PAL8 (4 << 10)
+#define MP_IMGFLAG_TYPE_HW (8 << 10)
+
+#define MP_IMGFLAG_YUV MP_IMGFLAG_COLOR_YUV
+#define MP_IMGFLAG_RGB MP_IMGFLAG_COLOR_RGB
+#define MP_IMGFLAG_PAL MP_IMGFLAG_TYPE_PAL8
+#define MP_IMGFLAG_HWACCEL MP_IMGFLAG_TYPE_HW
+
+// 1 component format (or 2 components if MP_IMGFLAG_ALPHA is set).
+// This should probably be a separate MP_IMGFLAG_COLOR_GRAY, but for now it
+// is too much of a mess.
+#define MP_IMGFLAG_GRAY (1 << 14)
+
+// Packed, sub-sampled YUV format. Does not apply to packed non-subsampled YUV.
+// These formats pack multiple pixels into one sample with strange organization.
+// In this specific case, mp_imgfmt_desc.align_x gives the size of a "full"
+// pixel, which has align_x luma samples, and 1 chroma sample of each Cb and Cr.
+// mp_imgfmt_desc.comps describes the chroma samples, and the first luma sample.
+// All luma samples have the same configuration as the first one, and you can
+// get their offsets with mp_imgfmt_get_packed_yuv_locations(). Note that the
+// component offsets can be >= bpp[0]; the actual range is bpp[0]*align_x.
+// These formats have no alpha.
+#define MP_IMGFLAG_PACKED_SS_YUV (1 << 15)
+
+// set if the format is in a standard YUV format:
+// - planar and yuv colorspace
+// - chroma shift 0-2
+// - 1-4 planes (1: gray, 2: gray/alpha, 3: yuv, 4: yuv/alpha)
+// - 8-16 bit per pixel/plane, all planes have same depth,
+// each plane has exactly one component
+#define MP_IMGFLAG_YUV_P (1 << 16)
+
+// Like MP_IMGFLAG_YUV_P, but RGB. This can be e.g. AV_PIX_FMT_GBRP. The planes
+// are always shuffled (G - B - R [- A]).
+#define MP_IMGFLAG_RGB_P (1 << 17)
+
+// Semi-planar YUV formats, like AV_PIX_FMT_NV12.
+#define MP_IMGFLAG_YUV_NV (1 << 18)
+
+struct mp_imgfmt_comp_desc {
+ // Plane on which this component is.
+ uint8_t plane;
+ // Bit offset of first sample, from start of the pixel group (little endian).
+ uint8_t offset : 6;
+ // Number of bits used by each sample.
+ uint8_t size : 6;
+ // Internal padding. See mp_regular_imgfmt.component_pad.
+ int8_t pad : 4;
+};
+
+struct mp_imgfmt_desc {
+ int id; // IMGFMT_*
+ int flags; // MP_IMGFLAG_* bitfield
+ int8_t num_planes;
+ int8_t chroma_xs, chroma_ys; // chroma shift (i.e. log2 of chroma pixel size)
+ int8_t align_x, align_y; // pixel count to get byte alignment and to get
+ // to a pixel pos where luma & chroma aligns
+ // always power of 2
+ int8_t bpp[MP_MAX_PLANES]; // bits per pixel (may be "average"; the real
+ // byte value is determined by align_x*bpp/8
+ // for align_x pixels)
+ // chroma shifts per plane (provided for convenience with planar formats)
+ // Packed YUV always uses xs[0]=ys[0]=0, because plane 0 contains luma in
+ // addition to chroma, and thus is not sub-sampled (uses align_x=2 instead).
+ int8_t xs[MP_MAX_PLANES];
+ int8_t ys[MP_MAX_PLANES];
+
+ // Description for each component. Generally valid only if flags has
+ // MP_IMGFLAG_HAS_COMPS set.
+ // This is indexed by component_type-1 (so 0=R, 1=G, etc.), see
+ // mp_regular_imgfmt_plane.components[x] for component_type. Components not
+ // present use size=0. Bits not covered by any component are random and not
+ // interpreted by any software.
+ // In particular, don't make the mistake to index this by plane.
+ struct mp_imgfmt_comp_desc comps[MP_NUM_COMPONENTS];
+
+ // log(2) of the word size in bytes for endian swapping that needs to be
+ // performed for converting to native endian. This is performed before any
+ // other unpacking steps, and for all data covered by bits.
+ // Always 0 if IMGFLAG_NE is set.
+ uint8_t endian_shift : 2;
+};
+
+struct mp_imgfmt_desc mp_imgfmt_get_desc(int imgfmt);
+
+// Return the number of component types, or 0 if unknown.
+int mp_imgfmt_desc_get_num_comps(struct mp_imgfmt_desc *desc);
+
+// For MP_IMGFLAG_PACKED_SS_YUV formats (packed sub-sampled YUV): positions of
+// further luma samples. luma_offsets must be an array of align_x size, and the
+// function will return the offset (like in mp_imgfmt_comp_desc.offset) of each
+// luma pixel. luma_offsets[0] == mp_imgfmt_desc.comps[0].offset.
+bool mp_imgfmt_get_packed_yuv_locations(int imgfmt, uint8_t *luma_offsets);
+
+// MP_CSP_AUTO for YUV, MP_CSP_RGB or MP_CSP_XYZ otherwise.
+// (Because IMGFMT/AV_PIX_FMT conflate format and csp for RGB and XYZ.)
+enum mp_csp mp_imgfmt_get_forced_csp(int imgfmt);
+
+enum mp_component_type {
+ MP_COMPONENT_TYPE_UNKNOWN = 0,
+ MP_COMPONENT_TYPE_UINT,
+ MP_COMPONENT_TYPE_FLOAT,
+};
+
+enum mp_component_type mp_imgfmt_get_component_type(int imgfmt);
+
+struct mp_regular_imgfmt_plane {
+ uint8_t num_components;
+ // 1 is red/luminance/gray, 2 is green/Cb, 3 is blue/Cr, 4 is alpha.
+ // 0 is used for padding (undefined contents).
+ // It is guaranteed that non-0 values occur only once in the whole format.
+ uint8_t components[MP_NUM_COMPONENTS];
+};
+
+// This describes pixel formats that are byte aligned, have byte aligned
+// components, native endian, etc.
+struct mp_regular_imgfmt {
+ // Type of each component.
+ enum mp_component_type component_type;
+
+ // See mp_imgfmt_get_forced_csp(). Normally code should use
+ // mp_image_params.colors. This field is only needed to map the format
+ // unambiguously to FFmpeg formats.
+ enum mp_csp forced_csp;
+
+ // Size of each component in bytes.
+ uint8_t component_size;
+
+ // If >0, LSB padding, if <0, MSB padding. The padding bits are always 0.
+ // This applies: bit_depth = component_size * 8 - abs(component_pad)
+ // bit_size = component_size * 8 + MPMIN(0, component_pad)
+ // E.g. P010: component_pad=6 (LSB always implied 0, all data in MSB)
+ // => has a "depth" of 10 bit, but usually treated as 16 bit value
+ // yuv420p10: component_pad=-6 (like a 10 bit value 0-extended to 16)
+ // => has depth of 10 bit, needs <<6 to get a 16 bit value
+ int8_t component_pad;
+
+ uint8_t num_planes;
+ struct mp_regular_imgfmt_plane planes[MP_MAX_PLANES];
+
+ // Chroma shifts for chroma planes. 0/0 is 4:4:4 YUV or RGB. If not 0/0,
+ // then this is always a yuv format, with components 2/3 on separate planes
+ // (reduced by the shift), and planes for components 1/4 are full sized.
+ uint8_t chroma_xs, chroma_ys;
+};
+
+bool mp_get_regular_imgfmt(struct mp_regular_imgfmt *dst, int imgfmt);
+int mp_find_regular_imgfmt(struct mp_regular_imgfmt *src);
+
+// If imgfmt is valid, and there exists a format that is exactly the same, but
+// has inverse endianness, return this other format. Otherwise return 0.
+int mp_find_other_endian(int imgfmt);
+
+enum mp_imgfmt {
+ IMGFMT_NONE = 0,
+
+ // Offset to make confusing with ffmpeg formats harder
+ IMGFMT_START = 1000,
+
+ // Planar YUV formats
+ IMGFMT_444P, // 1x1
+ IMGFMT_420P, // 2x2
+
+ // Gray
+ IMGFMT_Y8,
+ IMGFMT_Y16,
+
+ // Packed YUV formats (components are byte-accessed)
+ IMGFMT_UYVY, // U Y0 V Y1
+
+ // Y plane + packed plane for chroma
+ IMGFMT_NV12,
+
+ // Like IMGFMT_NV12, but with 10 bits per component (and 6 bits of padding)
+ IMGFMT_P010,
+
+ // RGB/BGR Formats
+
+ // Byte accessed (low address to high address)
+ IMGFMT_ARGB,
+ IMGFMT_BGRA,
+ IMGFMT_ABGR,
+ IMGFMT_RGBA,
+ IMGFMT_BGR24, // 3 bytes per pixel
+ IMGFMT_RGB24,
+
+ // Like e.g. IMGFMT_ARGB, but has a padding byte instead of alpha
+ IMGFMT_0RGB,
+ IMGFMT_BGR0,
+ IMGFMT_0BGR,
+ IMGFMT_RGB0,
+
+ // Like IMGFMT_RGBA, but 2 bytes per component.
+ IMGFMT_RGBA64,
+
+ // Accessed with bit-shifts after endian-swapping the uint16_t pixel
+ IMGFMT_RGB565, // 5r 6g 5b (MSB to LSB)
+
+ // AV_PIX_FMT_PAL8
+ IMGFMT_PAL8,
+
+ // Hardware accelerated formats. Plane data points to special data
+ // structures, instead of pixel data.
+ IMGFMT_VDPAU, // VdpVideoSurface
+ // plane 0: ID3D11Texture2D
+ // plane 1: slice index casted to pointer
+ IMGFMT_D3D11,
+ IMGFMT_DXVA2, // IDirect3DSurface9 (NV12/P010/P016)
+ IMGFMT_MMAL, // MMAL_BUFFER_HEADER_T
+ IMGFMT_MEDIACODEC, // AVMediaCodecBuffer
+ IMGFMT_CUDA, // CUDA Buffer
+
+ // Not an actual format; base for mpv-specific descriptor table.
+ // Some may still map to AV_PIX_FMT_*.
+ IMGFMT_CUST_BASE,
+
+ // Planar gray/alpha.
+ IMGFMT_YAP8,
+ IMGFMT_YAP16,
+
+ // Planar YUV/alpha formats. Sometimes useful for internal processing. There
+ // should be one for each subsampling factor, with and without alpha, gray.
+ IMGFMT_YAPF, // Note: non-alpha version exists in ffmpeg
+ IMGFMT_444PF,
+ IMGFMT_444APF,
+ IMGFMT_420PF,
+ IMGFMT_420APF,
+ IMGFMT_422PF,
+ IMGFMT_422APF,
+ IMGFMT_440PF,
+ IMGFMT_440APF,
+ IMGFMT_410PF,
+ IMGFMT_410APF,
+ IMGFMT_411PF,
+ IMGFMT_411APF,
+
+ // Accessed with bit-shifts, uint32_t units.
+ IMGFMT_RGB30, // 2pad 10r 10g 10b (MSB to LSB)
+
+ // Fringe formats for fringe RGB format repacking.
+ IMGFMT_Y1, // gray with 1 bit per pixel
+ IMGFMT_GBRP1, // planar RGB with N bits per color component
+ IMGFMT_GBRP2,
+ IMGFMT_GBRP3,
+ IMGFMT_GBRP4,
+ IMGFMT_GBRP5,
+ IMGFMT_GBRP6,
+
+ // Hardware accelerated formats (again).
+ IMGFMT_VDPAU_OUTPUT, // VdpOutputSurface
+ IMGFMT_VAAPI,
+ IMGFMT_VIDEOTOOLBOX, // CVPixelBufferRef
+#if HAVE_VULKAN_INTEROP
+ IMGFMT_VULKAN, // VKImage
+#endif
+ IMGFMT_DRMPRIME, // AVDRMFrameDescriptor
+
+ // Generic pass-through of AV_PIX_FMT_*. Used for formats which don't have
+ // a corresponding IMGFMT_ value.
+ IMGFMT_AVPIXFMT_START,
+ IMGFMT_AVPIXFMT_END = IMGFMT_AVPIXFMT_START + 500,
+
+ IMGFMT_END,
+};
+
+#define IMGFMT_IS_HWACCEL(fmt) (!!(mp_imgfmt_get_desc(fmt).flags & MP_IMGFLAG_HWACCEL))
+
+int mp_imgfmt_from_name(bstr name);
+char *mp_imgfmt_to_name_buf(char *buf, size_t buf_size, int fmt);
+#define mp_imgfmt_to_name(fmt) mp_imgfmt_to_name_buf((char[16]){0}, 16, (fmt))
+
+char **mp_imgfmt_name_list(void);
+
+#define vo_format_name mp_imgfmt_to_name
+
+int mp_imgfmt_select_best(int dst1, int dst2, int src);
+int mp_imgfmt_select_best_list(int *dst, int num_dst, int src);
+
+#endif /* MPLAYER_IMG_FORMAT_H */
diff --git a/video/mp_image.c b/video/mp_image.c
new file mode 100644
index 0000000..dff2051
--- /dev/null
+++ b/video/mp_image.c
@@ -0,0 +1,1289 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <limits.h>
+#include <assert.h>
+
+#include <libavutil/mem.h>
+#include <libavutil/common.h>
+#include <libavutil/display.h>
+#include <libavutil/bswap.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/rational.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/mastering_display_metadata.h>
+#include <libplacebo/utils/libav.h>
+
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 16, 100)
+# include <libavutil/dovi_meta.h>
+#endif
+
+#include "mpv_talloc.h"
+
+#include "common/av_common.h"
+#include "common/common.h"
+#include "fmt-conversion.h"
+#include "hwdec.h"
+#include "mp_image.h"
+#include "osdep/threads.h"
+#include "sws_utils.h"
+#include "out/placebo/utils.h"
+
+// Determine strides, plane sizes, and total required size for an image
+// allocation. Returns total size on success, <0 on error. Unused planes
+// have out_stride/out_plane_size to 0, and out_plane_offset set to -1 up
+// until MP_MAX_PLANES-1.
+static int mp_image_layout(int imgfmt, int w, int h, int stride_align,
+ int out_stride[MP_MAX_PLANES],
+ int out_plane_offset[MP_MAX_PLANES],
+ int out_plane_size[MP_MAX_PLANES])
+{
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(imgfmt);
+
+ w = MP_ALIGN_UP(w, desc.align_x);
+ h = MP_ALIGN_UP(h, desc.align_y);
+
+ struct mp_image_params params = {.imgfmt = imgfmt, .w = w, .h = h};
+
+ if (!mp_image_params_valid(&params) || desc.flags & MP_IMGFLAG_HWACCEL)
+ return -1;
+
+ // Note: for non-mod-2 4:2:0 YUV frames, we have to allocate an additional
+ // top/right border. This is needed for correct handling of such
+ // images in filter and VO code (e.g. vo_vdpau or vo_gpu).
+
+ for (int n = 0; n < MP_MAX_PLANES; n++) {
+ int alloc_w = mp_chroma_div_up(w, desc.xs[n]);
+ int alloc_h = MP_ALIGN_UP(h, 32) >> desc.ys[n];
+ int line_bytes = (alloc_w * desc.bpp[n] + 7) / 8;
+ out_stride[n] = MP_ALIGN_NPOT(line_bytes, stride_align);
+ out_plane_size[n] = out_stride[n] * alloc_h;
+ }
+ if (desc.flags & MP_IMGFLAG_PAL)
+ out_plane_size[1] = AVPALETTE_SIZE;
+
+ int sum = 0;
+ for (int n = 0; n < MP_MAX_PLANES; n++) {
+ out_plane_offset[n] = out_plane_size[n] ? sum : -1;
+ sum += out_plane_size[n];
+ }
+
+ return sum;
+}
+
+// Return the total size needed for an image allocation of the given
+// configuration (imgfmt, w, h must be set). Returns -1 on error.
+// Assumes the allocation is already aligned on stride_align (otherwise you
+// need to add padding yourself).
+int mp_image_get_alloc_size(int imgfmt, int w, int h, int stride_align)
+{
+ int stride[MP_MAX_PLANES];
+ int plane_offset[MP_MAX_PLANES];
+ int plane_size[MP_MAX_PLANES];
+ return mp_image_layout(imgfmt, w, h, stride_align, stride, plane_offset,
+ plane_size);
+}
+
+// Fill the mpi->planes and mpi->stride fields of the given mpi with data
+// from buffer according to the mpi's w/h/imgfmt fields. See mp_image_from_buffer
+// aboud remarks how to allocate/use buffer/buffer_size.
+// This does not free the data. You are expected to setup refcounting by
+// setting mp_image.bufs before or after this function is called.
+// Returns true on success, false on failure.
+static bool mp_image_fill_alloc(struct mp_image *mpi, int stride_align,
+ void *buffer, int buffer_size)
+{
+ int stride[MP_MAX_PLANES];
+ int plane_offset[MP_MAX_PLANES];
+ int plane_size[MP_MAX_PLANES];
+ int size = mp_image_layout(mpi->imgfmt, mpi->w, mpi->h, stride_align,
+ stride, plane_offset, plane_size);
+ if (size < 0 || size > buffer_size)
+ return false;
+
+ int align = MP_ALIGN_UP((uintptr_t)buffer, stride_align) - (uintptr_t)buffer;
+ if (buffer_size - size < align)
+ return false;
+ uint8_t *s = buffer;
+ s += align;
+
+ for (int n = 0; n < MP_MAX_PLANES; n++) {
+ mpi->planes[n] = plane_offset[n] >= 0 ? s + plane_offset[n] : NULL;
+ mpi->stride[n] = stride[n];
+ }
+
+ return true;
+}
+
+// Create a mp_image from the provided buffer. The mp_image is filled according
+// to the imgfmt/w/h parameters, and respecting the stride_align parameter to
+// align the plane start pointers and strides. Once the last reference to the
+// returned image is destroyed, free(free_opaque, buffer) is called. (Be aware
+// that this can happen from any thread.)
+// The allocated size of buffer must be given by buffer_size. buffer_size should
+// be at least the value returned by mp_image_get_alloc_size(). If buffer is not
+// already aligned to stride_align, the function will attempt to align the
+// pointer itself by incrementing the buffer pointer until their alignment is
+// achieved (if buffer_size is not large enough to allow aligning the buffer
+// safely, the function fails). To be safe, you may want to overallocate the
+// buffer by stride_align bytes, and include the overallocation in buffer_size.
+// Returns NULL on failure. On failure, the free() callback is not called.
+struct mp_image *mp_image_from_buffer(int imgfmt, int w, int h, int stride_align,
+ uint8_t *buffer, int buffer_size,
+ void *free_opaque,
+ void (*free)(void *opaque, uint8_t *data))
+{
+ struct mp_image *mpi = mp_image_new_dummy_ref(NULL);
+ mp_image_setfmt(mpi, imgfmt);
+ mp_image_set_size(mpi, w, h);
+
+ if (!mp_image_fill_alloc(mpi, stride_align, buffer, buffer_size))
+ goto fail;
+
+ mpi->bufs[0] = av_buffer_create(buffer, buffer_size, free, free_opaque, 0);
+ if (!mpi->bufs[0])
+ goto fail;
+
+ return mpi;
+
+fail:
+ talloc_free(mpi);
+ return NULL;
+}
+
+static bool mp_image_alloc_planes(struct mp_image *mpi)
+{
+ assert(!mpi->planes[0]);
+ assert(!mpi->bufs[0]);
+
+ int align = MP_IMAGE_BYTE_ALIGN;
+
+ int size = mp_image_get_alloc_size(mpi->imgfmt, mpi->w, mpi->h, align);
+ if (size < 0)
+ return false;
+
+ // Note: mp_image_pool assumes this creates only 1 AVBufferRef.
+ mpi->bufs[0] = av_buffer_alloc(size + align);
+ if (!mpi->bufs[0])
+ return false;
+
+ if (!mp_image_fill_alloc(mpi, align, mpi->bufs[0]->data, mpi->bufs[0]->size)) {
+ av_buffer_unref(&mpi->bufs[0]);
+ return false;
+ }
+
+ return true;
+}
+
+void mp_image_setfmt(struct mp_image *mpi, int out_fmt)
+{
+ struct mp_image_params params = mpi->params;
+ struct mp_imgfmt_desc fmt = mp_imgfmt_get_desc(out_fmt);
+ params.imgfmt = fmt.id;
+ mpi->fmt = fmt;
+ mpi->imgfmt = fmt.id;
+ mpi->num_planes = fmt.num_planes;
+ mpi->params = params;
+}
+
+static void mp_image_destructor(void *ptr)
+{
+ mp_image_t *mpi = ptr;
+ for (int p = 0; p < MP_MAX_PLANES; p++)
+ av_buffer_unref(&mpi->bufs[p]);
+ av_buffer_unref(&mpi->hwctx);
+ av_buffer_unref(&mpi->icc_profile);
+ av_buffer_unref(&mpi->a53_cc);
+ av_buffer_unref(&mpi->dovi);
+ av_buffer_unref(&mpi->film_grain);
+ av_buffer_unref(&mpi->dovi_buf);
+ for (int n = 0; n < mpi->num_ff_side_data; n++)
+ av_buffer_unref(&mpi->ff_side_data[n].buf);
+ talloc_free(mpi->ff_side_data);
+}
+
+int mp_chroma_div_up(int size, int shift)
+{
+ return (size + (1 << shift) - 1) >> shift;
+}
+
+// Return the storage width in pixels of the given plane.
+int mp_image_plane_w(struct mp_image *mpi, int plane)
+{
+ return mp_chroma_div_up(mpi->w, mpi->fmt.xs[plane]);
+}
+
+// Return the storage height in pixels of the given plane.
+int mp_image_plane_h(struct mp_image *mpi, int plane)
+{
+ return mp_chroma_div_up(mpi->h, mpi->fmt.ys[plane]);
+}
+
+// Caller has to make sure this doesn't exceed the allocated plane data/strides.
+void mp_image_set_size(struct mp_image *mpi, int w, int h)
+{
+ assert(w >= 0 && h >= 0);
+ mpi->w = mpi->params.w = w;
+ mpi->h = mpi->params.h = h;
+}
+
+void mp_image_set_params(struct mp_image *image,
+ const struct mp_image_params *params)
+{
+ // possibly initialize other stuff
+ mp_image_setfmt(image, params->imgfmt);
+ mp_image_set_size(image, params->w, params->h);
+ image->params = *params;
+}
+
+struct mp_image *mp_image_alloc(int imgfmt, int w, int h)
+{
+ struct mp_image *mpi = talloc_zero(NULL, struct mp_image);
+ talloc_set_destructor(mpi, mp_image_destructor);
+
+ mp_image_set_size(mpi, w, h);
+ mp_image_setfmt(mpi, imgfmt);
+ if (!mp_image_alloc_planes(mpi)) {
+ talloc_free(mpi);
+ return NULL;
+ }
+ return mpi;
+}
+
+int mp_image_approx_byte_size(struct mp_image *img)
+{
+ int total = sizeof(*img);
+
+ for (int n = 0; n < MP_MAX_PLANES; n++) {
+ struct AVBufferRef *buf = img->bufs[n];
+ if (buf)
+ total += buf->size;
+ }
+
+ return total;
+}
+
+struct mp_image *mp_image_new_copy(struct mp_image *img)
+{
+ struct mp_image *new = mp_image_alloc(img->imgfmt, img->w, img->h);
+ if (!new)
+ return NULL;
+ mp_image_copy(new, img);
+ mp_image_copy_attributes(new, img);
+ return new;
+}
+
+// Make dst take over the image data of src, and free src.
+// This is basically a safe version of *dst = *src; free(src);
+// Only works with ref-counted images, and can't change image size/format.
+void mp_image_steal_data(struct mp_image *dst, struct mp_image *src)
+{
+ assert(dst->imgfmt == src->imgfmt && dst->w == src->w && dst->h == src->h);
+ assert(dst->bufs[0] && src->bufs[0]);
+
+ mp_image_destructor(dst); // unref old
+ talloc_free_children(dst);
+
+ *dst = *src;
+
+ *src = (struct mp_image){0};
+ talloc_free(src);
+}
+
+// Unref most data buffer (and clear the data array), but leave other fields
+// allocated. In particular, mp_image.hwctx is preserved.
+void mp_image_unref_data(struct mp_image *img)
+{
+ for (int n = 0; n < MP_MAX_PLANES; n++) {
+ img->planes[n] = NULL;
+ img->stride[n] = 0;
+ av_buffer_unref(&img->bufs[n]);
+ }
+}
+
+static void ref_buffer(AVBufferRef **dst)
+{
+ if (*dst) {
+ *dst = av_buffer_ref(*dst);
+ MP_HANDLE_OOM(*dst);
+ }
+}
+
+// Return a new reference to img. The returned reference is owned by the caller,
+// while img is left untouched.
+struct mp_image *mp_image_new_ref(struct mp_image *img)
+{
+ if (!img)
+ return NULL;
+
+ if (!img->bufs[0])
+ return mp_image_new_copy(img);
+
+ struct mp_image *new = talloc_ptrtype(NULL, new);
+ talloc_set_destructor(new, mp_image_destructor);
+ *new = *img;
+
+ for (int p = 0; p < MP_MAX_PLANES; p++)
+ ref_buffer(&new->bufs[p]);
+
+ ref_buffer(&new->hwctx);
+ ref_buffer(&new->icc_profile);
+ ref_buffer(&new->a53_cc);
+ ref_buffer(&new->dovi);
+ ref_buffer(&new->film_grain);
+ ref_buffer(&new->dovi_buf);
+
+ new->ff_side_data = talloc_memdup(NULL, new->ff_side_data,
+ new->num_ff_side_data * sizeof(new->ff_side_data[0]));
+ for (int n = 0; n < new->num_ff_side_data; n++)
+ ref_buffer(&new->ff_side_data[n].buf);
+
+ return new;
+}
+
+struct free_args {
+ void *arg;
+ void (*free)(void *arg);
+};
+
+static void call_free(void *opaque, uint8_t *data)
+{
+ struct free_args *args = opaque;
+ args->free(args->arg);
+ talloc_free(args);
+}
+
+// Create a new mp_image based on img, but don't set any buffers.
+// Using this is only valid until the original img is unreferenced (including
+// implicit unreferencing of the data by mp_image_make_writeable()), unless
+// a new reference is set.
+struct mp_image *mp_image_new_dummy_ref(struct mp_image *img)
+{
+ struct mp_image *new = talloc_ptrtype(NULL, new);
+ talloc_set_destructor(new, mp_image_destructor);
+ *new = img ? *img : (struct mp_image){0};
+ for (int p = 0; p < MP_MAX_PLANES; p++)
+ new->bufs[p] = NULL;
+ new->hwctx = NULL;
+ new->icc_profile = NULL;
+ new->a53_cc = NULL;
+ new->dovi = NULL;
+ new->film_grain = NULL;
+ new->dovi_buf = NULL;
+ new->num_ff_side_data = 0;
+ new->ff_side_data = NULL;
+ return new;
+}
+
+// Return a reference counted reference to img. If the reference count reaches
+// 0, call free(free_arg). The data passed by img must not be free'd before
+// that. The new reference will be writeable.
+// On allocation failure, unref the frame and return NULL.
+// This is only used for hw decoding; this is important, because libav* expects
+// all plane data to be accounted for by AVBufferRefs.
+struct mp_image *mp_image_new_custom_ref(struct mp_image *img, void *free_arg,
+ void (*free)(void *arg))
+{
+ struct mp_image *new = mp_image_new_dummy_ref(img);
+
+ struct free_args *args = talloc_ptrtype(NULL, args);
+ *args = (struct free_args){free_arg, free};
+ new->bufs[0] = av_buffer_create(NULL, 0, call_free, args,
+ AV_BUFFER_FLAG_READONLY);
+ if (new->bufs[0])
+ return new;
+ talloc_free(new);
+ return NULL;
+}
+
+bool mp_image_is_writeable(struct mp_image *img)
+{
+ if (!img->bufs[0])
+ return true; // not ref-counted => always considered writeable
+ for (int p = 0; p < MP_MAX_PLANES; p++) {
+ if (!img->bufs[p])
+ break;
+ if (!av_buffer_is_writable(img->bufs[p]))
+ return false;
+ }
+ return true;
+}
+
+// Make the image data referenced by img writeable. This allocates new data
+// if the data wasn't already writeable, and img->planes[] and img->stride[]
+// will be set to the copy.
+// Returns success; if false is returned, the image could not be made writeable.
+bool mp_image_make_writeable(struct mp_image *img)
+{
+ if (mp_image_is_writeable(img))
+ return true;
+
+ struct mp_image *new = mp_image_new_copy(img);
+ if (!new)
+ return false;
+ mp_image_steal_data(img, new);
+ assert(mp_image_is_writeable(img));
+ return true;
+}
+
+// Helper function: unrefs *p_img, and sets *p_img to a new ref of new_value.
+// Only unrefs *p_img and sets it to NULL if out of memory.
+void mp_image_setrefp(struct mp_image **p_img, struct mp_image *new_value)
+{
+ if (*p_img != new_value) {
+ talloc_free(*p_img);
+ *p_img = new_value ? mp_image_new_ref(new_value) : NULL;
+ }
+}
+
+// Mere helper function (mp_image can be directly free'd with talloc_free)
+void mp_image_unrefp(struct mp_image **p_img)
+{
+ talloc_free(*p_img);
+ *p_img = NULL;
+}
+
+void memcpy_pic(void *dst, const void *src, int bytesPerLine, int height,
+ int dstStride, int srcStride)
+{
+ if (bytesPerLine == dstStride && dstStride == srcStride && height) {
+ if (srcStride < 0) {
+ src = (uint8_t*)src + (height - 1) * srcStride;
+ dst = (uint8_t*)dst + (height - 1) * dstStride;
+ srcStride = -srcStride;
+ }
+
+ memcpy(dst, src, srcStride * (height - 1) + bytesPerLine);
+ } else {
+ for (int i = 0; i < height; i++) {
+ memcpy(dst, src, bytesPerLine);
+ src = (uint8_t*)src + srcStride;
+ dst = (uint8_t*)dst + dstStride;
+ }
+ }
+}
+
+void mp_image_copy(struct mp_image *dst, struct mp_image *src)
+{
+ assert(dst->imgfmt == src->imgfmt);
+ assert(dst->w == src->w && dst->h == src->h);
+ assert(mp_image_is_writeable(dst));
+ for (int n = 0; n < dst->num_planes; n++) {
+ int line_bytes = (mp_image_plane_w(dst, n) * dst->fmt.bpp[n] + 7) / 8;
+ int plane_h = mp_image_plane_h(dst, n);
+ memcpy_pic(dst->planes[n], src->planes[n], line_bytes, plane_h,
+ dst->stride[n], src->stride[n]);
+ }
+ if (dst->fmt.flags & MP_IMGFLAG_PAL)
+ memcpy(dst->planes[1], src->planes[1], AVPALETTE_SIZE);
+}
+
+static enum mp_csp mp_image_params_get_forced_csp(struct mp_image_params *params)
+{
+ int imgfmt = params->hw_subfmt ? params->hw_subfmt : params->imgfmt;
+ return mp_imgfmt_get_forced_csp(imgfmt);
+}
+
+static void assign_bufref(AVBufferRef **dst, AVBufferRef *new)
+{
+ av_buffer_unref(dst);
+ if (new) {
+ *dst = av_buffer_ref(new);
+ MP_HANDLE_OOM(*dst);
+ }
+}
+
+void mp_image_copy_attributes(struct mp_image *dst, struct mp_image *src)
+{
+ assert(dst != src);
+
+ dst->pict_type = src->pict_type;
+ dst->fields = src->fields;
+ dst->pts = src->pts;
+ dst->dts = src->dts;
+ dst->pkt_duration = src->pkt_duration;
+ dst->params.rotate = src->params.rotate;
+ dst->params.stereo3d = src->params.stereo3d;
+ dst->params.p_w = src->params.p_w;
+ dst->params.p_h = src->params.p_h;
+ dst->params.color = src->params.color;
+ dst->params.chroma_location = src->params.chroma_location;
+ dst->params.alpha = src->params.alpha;
+ dst->params.crop = src->params.crop;
+ dst->nominal_fps = src->nominal_fps;
+
+ // ensure colorspace consistency
+ enum mp_csp dst_forced_csp = mp_image_params_get_forced_csp(&dst->params);
+ if (mp_image_params_get_forced_csp(&src->params) != dst_forced_csp) {
+ dst->params.color.space = dst_forced_csp != MP_CSP_AUTO ?
+ dst_forced_csp :
+ mp_csp_guess_colorspace(src->w, src->h);
+ }
+
+ if ((dst->fmt.flags & MP_IMGFLAG_PAL) && (src->fmt.flags & MP_IMGFLAG_PAL)) {
+ if (dst->planes[1] && src->planes[1]) {
+ if (mp_image_make_writeable(dst))
+ memcpy(dst->planes[1], src->planes[1], AVPALETTE_SIZE);
+ }
+ }
+ assign_bufref(&dst->icc_profile, src->icc_profile);
+ assign_bufref(&dst->dovi, src->dovi);
+ assign_bufref(&dst->dovi_buf, src->dovi_buf);
+ assign_bufref(&dst->film_grain, src->film_grain);
+ assign_bufref(&dst->a53_cc, src->a53_cc);
+
+ for (int n = 0; n < dst->num_ff_side_data; n++)
+ av_buffer_unref(&dst->ff_side_data[n].buf);
+
+ MP_RESIZE_ARRAY(NULL, dst->ff_side_data, src->num_ff_side_data);
+ dst->num_ff_side_data = src->num_ff_side_data;
+
+ for (int n = 0; n < dst->num_ff_side_data; n++) {
+ dst->ff_side_data[n].type = src->ff_side_data[n].type;
+ dst->ff_side_data[n].buf = av_buffer_ref(src->ff_side_data[n].buf);
+ MP_HANDLE_OOM(dst->ff_side_data[n].buf);
+ }
+}
+
+// Crop the given image to (x0, y0)-(x1, y1) (bottom/right border exclusive)
+// x0/y0 must be naturally aligned.
+void mp_image_crop(struct mp_image *img, int x0, int y0, int x1, int y1)
+{
+ assert(x0 >= 0 && y0 >= 0);
+ assert(x0 <= x1 && y0 <= y1);
+ assert(x1 <= img->w && y1 <= img->h);
+ assert(!(x0 & (img->fmt.align_x - 1)));
+ assert(!(y0 & (img->fmt.align_y - 1)));
+
+ for (int p = 0; p < img->num_planes; ++p) {
+ img->planes[p] += (y0 >> img->fmt.ys[p]) * img->stride[p] +
+ (x0 >> img->fmt.xs[p]) * img->fmt.bpp[p] / 8;
+ }
+ mp_image_set_size(img, x1 - x0, y1 - y0);
+}
+
+void mp_image_crop_rc(struct mp_image *img, struct mp_rect rc)
+{
+ mp_image_crop(img, rc.x0, rc.y0, rc.x1, rc.y1);
+}
+
+// Repeatedly write count patterns of src[0..src_size] to p.
+static void memset_pattern(void *p, size_t count, uint8_t *src, size_t src_size)
+{
+ assert(src_size >= 1);
+
+ if (src_size == 1) {
+ memset(p, src[0], count);
+ } else if (src_size == 2) { // >8 bit YUV => common, be slightly less naive
+ uint16_t val;
+ memcpy(&val, src, 2);
+ uint16_t *p16 = p;
+ while (count--)
+ *p16++ = val;
+ } else {
+ while (count--) {
+ memcpy(p, src, src_size);
+ p = (char *)p + src_size;
+ }
+ }
+}
+
+static bool endian_swap_bytes(void *d, size_t bytes, size_t word_size)
+{
+ if (word_size != 2 && word_size != 4)
+ return false;
+
+ size_t num_words = bytes / word_size;
+ uint8_t *ud = d;
+
+ switch (word_size) {
+ case 2:
+ for (size_t x = 0; x < num_words; x++)
+ AV_WL16(ud + x * 2, AV_RB16(ud + x * 2));
+ break;
+ case 4:
+ for (size_t x = 0; x < num_words; x++)
+ AV_WL32(ud + x * 2, AV_RB32(ud + x * 2));
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ return true;
+}
+
+// Bottom/right border is allowed not to be aligned, but it might implicitly
+// overwrite pixel data until the alignment (align_x/align_y) is reached.
+// Alpha is cleared to 0 (fully transparent).
+void mp_image_clear(struct mp_image *img, int x0, int y0, int x1, int y1)
+{
+ assert(x0 >= 0 && y0 >= 0);
+ assert(x0 <= x1 && y0 <= y1);
+ assert(x1 <= img->w && y1 <= img->h);
+ assert(!(x0 & (img->fmt.align_x - 1)));
+ assert(!(y0 & (img->fmt.align_y - 1)));
+
+ struct mp_image area = *img;
+ struct mp_imgfmt_desc *fmt = &area.fmt;
+ mp_image_crop(&area, x0, y0, x1, y1);
+
+ // "Black" color for each plane.
+ uint8_t plane_clear[MP_MAX_PLANES][8] = {0};
+ int plane_size[MP_MAX_PLANES] = {0};
+ int misery = 1; // pixel group width
+
+ // YUV integer chroma needs special consideration, and technically luma is
+ // usually not 0 either.
+ if ((fmt->flags & (MP_IMGFLAG_HAS_COMPS | MP_IMGFLAG_PACKED_SS_YUV)) &&
+ (fmt->flags & MP_IMGFLAG_TYPE_MASK) == MP_IMGFLAG_TYPE_UINT &&
+ (fmt->flags & MP_IMGFLAG_COLOR_MASK) == MP_IMGFLAG_COLOR_YUV)
+ {
+ uint64_t plane_clear_i[MP_MAX_PLANES] = {0};
+
+ // Need to handle "multiple" pixels with packed YUV.
+ uint8_t luma_offsets[4] = {0};
+ if (fmt->flags & MP_IMGFLAG_PACKED_SS_YUV) {
+ misery = fmt->align_x;
+ if (misery <= MP_ARRAY_SIZE(luma_offsets)) // ignore if out of bounds
+ mp_imgfmt_get_packed_yuv_locations(fmt->id, luma_offsets);
+ }
+
+ for (int c = 0; c < 4; c++) {
+ struct mp_imgfmt_comp_desc *cd = &fmt->comps[c];
+ int plane_bits = fmt->bpp[cd->plane] * misery;
+ if (plane_bits <= 64 && plane_bits % 8u == 0 && cd->size) {
+ plane_size[cd->plane] = plane_bits / 8u;
+ int depth = cd->size + MPMIN(cd->pad, 0);
+ double m, o;
+ mp_get_csp_uint_mul(area.params.color.space,
+ area.params.color.levels,
+ depth, c + 1, &m, &o);
+ uint64_t val = MPCLAMP(lrint((0 - o) / m), 0, 1ull << depth);
+ plane_clear_i[cd->plane] |= val << cd->offset;
+ for (int x = 1; x < (c ? 0 : misery); x++)
+ plane_clear_i[cd->plane] |= val << luma_offsets[x];
+ }
+ }
+
+ for (int p = 0; p < MP_MAX_PLANES; p++) {
+ if (!plane_clear_i[p])
+ plane_size[p] = 0;
+ memcpy(&plane_clear[p][0], &plane_clear_i[p], 8); // endian dependent
+
+ if (fmt->endian_shift) {
+ endian_swap_bytes(&plane_clear[p][0], plane_size[p],
+ 1 << fmt->endian_shift);
+ }
+ }
+ }
+
+ for (int p = 0; p < area.num_planes; p++) {
+ int p_h = mp_image_plane_h(&area, p);
+ int p_w = mp_image_plane_w(&area, p);
+ for (int y = 0; y < p_h; y++) {
+ void *ptr = area.planes[p] + (ptrdiff_t)area.stride[p] * y;
+ if (plane_size[p]) {
+ memset_pattern(ptr, p_w / misery, plane_clear[p], plane_size[p]);
+ } else {
+ memset(ptr, 0, mp_image_plane_bytes(&area, p, 0, area.w));
+ }
+ }
+ }
+}
+
+void mp_image_clear_rc(struct mp_image *mpi, struct mp_rect rc)
+{
+ mp_image_clear(mpi, rc.x0, rc.y0, rc.x1, rc.y1);
+}
+
+// Clear the are of the image _not_ covered by rc.
+void mp_image_clear_rc_inv(struct mp_image *mpi, struct mp_rect rc)
+{
+ struct mp_rect clr[4];
+ int cnt = mp_rect_subtract(&(struct mp_rect){0, 0, mpi->w, mpi->h}, &rc, clr);
+ for (int n = 0; n < cnt; n++)
+ mp_image_clear_rc(mpi, clr[n]);
+}
+
+void mp_image_vflip(struct mp_image *img)
+{
+ for (int p = 0; p < img->num_planes; p++) {
+ int plane_h = mp_image_plane_h(img, p);
+ img->planes[p] = img->planes[p] + img->stride[p] * (plane_h - 1);
+ img->stride[p] = -img->stride[p];
+ }
+}
+
+bool mp_image_crop_valid(const struct mp_image_params *p)
+{
+ return p->crop.x1 > p->crop.x0 && p->crop.y1 > p->crop.y0 &&
+ p->crop.x0 >= 0 && p->crop.y0 >= 0 &&
+ p->crop.x1 <= p->w && p->crop.y1 <= p->h;
+}
+
+// Display size derived from image size and pixel aspect ratio.
+void mp_image_params_get_dsize(const struct mp_image_params *p,
+ int *d_w, int *d_h)
+{
+ if (mp_image_crop_valid(p))
+ {
+ *d_w = mp_rect_w(p->crop);
+ *d_h = mp_rect_h(p->crop);
+ } else {
+ *d_w = p->w;
+ *d_h = p->h;
+ }
+
+ if (p->p_w > p->p_h && p->p_h >= 1)
+ *d_w = MPCLAMP(*d_w * (int64_t)p->p_w / p->p_h, 1, INT_MAX);
+ if (p->p_h > p->p_w && p->p_w >= 1)
+ *d_h = MPCLAMP(*d_h * (int64_t)p->p_h / p->p_w, 1, INT_MAX);
+}
+
+void mp_image_params_set_dsize(struct mp_image_params *p, int d_w, int d_h)
+{
+ AVRational ds = av_div_q((AVRational){d_w, d_h}, (AVRational){p->w, p->h});
+ p->p_w = ds.num;
+ p->p_h = ds.den;
+}
+
+char *mp_image_params_to_str_buf(char *b, size_t bs,
+ const struct mp_image_params *p)
+{
+ if (p && p->imgfmt) {
+ snprintf(b, bs, "%dx%d", p->w, p->h);
+ if (p->p_w != p->p_h || !p->p_w)
+ mp_snprintf_cat(b, bs, " [%d:%d]", p->p_w, p->p_h);
+ mp_snprintf_cat(b, bs, " %s", mp_imgfmt_to_name(p->imgfmt));
+ if (p->hw_subfmt)
+ mp_snprintf_cat(b, bs, "[%s]", mp_imgfmt_to_name(p->hw_subfmt));
+ mp_snprintf_cat(b, bs, " %s/%s/%s/%s/%s",
+ m_opt_choice_str(mp_csp_names, p->color.space),
+ m_opt_choice_str(mp_csp_prim_names, p->color.primaries),
+ m_opt_choice_str(mp_csp_trc_names, p->color.gamma),
+ m_opt_choice_str(mp_csp_levels_names, p->color.levels),
+ m_opt_choice_str(mp_csp_light_names, p->color.light));
+ mp_snprintf_cat(b, bs, " CL=%s",
+ m_opt_choice_str(mp_chroma_names, p->chroma_location));
+ if (mp_image_crop_valid(p)) {
+ mp_snprintf_cat(b, bs, " crop=%dx%d+%d+%d", mp_rect_w(p->crop),
+ mp_rect_h(p->crop), p->crop.x0, p->crop.y0);
+ }
+ if (p->rotate)
+ mp_snprintf_cat(b, bs, " rot=%d", p->rotate);
+ if (p->stereo3d > 0) {
+ mp_snprintf_cat(b, bs, " stereo=%s",
+ MP_STEREO3D_NAME_DEF(p->stereo3d, "?"));
+ }
+ if (p->alpha) {
+ mp_snprintf_cat(b, bs, " A=%s",
+ m_opt_choice_str(mp_alpha_names, p->alpha));
+ }
+ } else {
+ snprintf(b, bs, "???");
+ }
+ return b;
+}
+
+// Return whether the image parameters are valid.
+// Some non-essential fields are allowed to be unset (like colorspace flags).
+bool mp_image_params_valid(const struct mp_image_params *p)
+{
+ // av_image_check_size has similar checks and triggers around 16000*16000
+ // It's mostly needed to deal with the fact that offsets are sometimes
+ // ints. We also should (for now) do the same as FFmpeg, to be sure large
+ // images don't crash with libswscale or when wrapping with AVFrame and
+ // passing the result to filters.
+ if (p->w <= 0 || p->h <= 0 || (p->w + 128LL) * (p->h + 128LL) >= INT_MAX / 8)
+ return false;
+
+ if (p->p_w < 0 || p->p_h < 0)
+ return false;
+
+ if (p->rotate < 0 || p->rotate >= 360)
+ return false;
+
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p->imgfmt);
+ if (!desc.id)
+ return false;
+
+ if (p->hw_subfmt && !(desc.flags & MP_IMGFLAG_HWACCEL))
+ return false;
+
+ return true;
+}
+
+bool mp_image_params_equal(const struct mp_image_params *p1,
+ const struct mp_image_params *p2)
+{
+ return p1->imgfmt == p2->imgfmt &&
+ p1->hw_subfmt == p2->hw_subfmt &&
+ p1->w == p2->w && p1->h == p2->h &&
+ p1->p_w == p2->p_w && p1->p_h == p2->p_h &&
+ p1->force_window == p2->force_window &&
+ mp_colorspace_equal(p1->color, p2->color) &&
+ p1->chroma_location == p2->chroma_location &&
+ p1->rotate == p2->rotate &&
+ p1->stereo3d == p2->stereo3d &&
+ p1->alpha == p2->alpha &&
+ mp_rect_equals(&p1->crop, &p2->crop);
+}
+
+// Set most image parameters, but not image format or size.
+// Display size is used to set the PAR.
+void mp_image_set_attributes(struct mp_image *image,
+ const struct mp_image_params *params)
+{
+ struct mp_image_params nparams = *params;
+ nparams.imgfmt = image->imgfmt;
+ nparams.w = image->w;
+ nparams.h = image->h;
+ if (nparams.imgfmt != params->imgfmt)
+ nparams.color = (struct mp_colorspace){0};
+ mp_image_set_params(image, &nparams);
+}
+
+static enum mp_csp_levels infer_levels(enum mp_imgfmt imgfmt)
+{
+ switch (imgfmt2pixfmt(imgfmt)) {
+ case AV_PIX_FMT_YUVJ420P:
+ case AV_PIX_FMT_YUVJ411P:
+ case AV_PIX_FMT_YUVJ422P:
+ case AV_PIX_FMT_YUVJ444P:
+ case AV_PIX_FMT_YUVJ440P:
+ case AV_PIX_FMT_GRAY8:
+ case AV_PIX_FMT_YA8:
+ case AV_PIX_FMT_GRAY9LE:
+ case AV_PIX_FMT_GRAY9BE:
+ case AV_PIX_FMT_GRAY10LE:
+ case AV_PIX_FMT_GRAY10BE:
+ case AV_PIX_FMT_GRAY12LE:
+ case AV_PIX_FMT_GRAY12BE:
+ case AV_PIX_FMT_GRAY14LE:
+ case AV_PIX_FMT_GRAY14BE:
+ case AV_PIX_FMT_GRAY16LE:
+ case AV_PIX_FMT_GRAY16BE:
+ case AV_PIX_FMT_YA16BE:
+ case AV_PIX_FMT_YA16LE:
+ return MP_CSP_LEVELS_PC;
+ default:
+ return MP_CSP_LEVELS_TV;
+ }
+}
+
+// If details like params->colorspace/colorlevels are missing, guess them from
+// the other settings. Also, even if they are set, make them consistent with
+// the colorspace as implied by the pixel format.
+void mp_image_params_guess_csp(struct mp_image_params *params)
+{
+ enum mp_csp forced_csp = mp_image_params_get_forced_csp(params);
+ if (forced_csp == MP_CSP_AUTO) { // YUV/other
+ if (params->color.space != MP_CSP_BT_601 &&
+ params->color.space != MP_CSP_BT_709 &&
+ params->color.space != MP_CSP_BT_2020_NC &&
+ params->color.space != MP_CSP_BT_2020_C &&
+ params->color.space != MP_CSP_SMPTE_240M &&
+ params->color.space != MP_CSP_YCGCO)
+ {
+ // Makes no sense, so guess instead
+ // YCGCO should be separate, but libavcodec disagrees
+ params->color.space = MP_CSP_AUTO;
+ }
+ if (params->color.space == MP_CSP_AUTO)
+ params->color.space = mp_csp_guess_colorspace(params->w, params->h);
+ if (params->color.levels == MP_CSP_LEVELS_AUTO) {
+ if (params->color.gamma == MP_CSP_TRC_V_LOG) {
+ params->color.levels = MP_CSP_LEVELS_PC;
+ } else {
+ params->color.levels = infer_levels(params->imgfmt);
+ }
+ }
+ if (params->color.primaries == MP_CSP_PRIM_AUTO) {
+ // Guess based on the colormatrix as a first priority
+ if (params->color.space == MP_CSP_BT_2020_NC ||
+ params->color.space == MP_CSP_BT_2020_C) {
+ params->color.primaries = MP_CSP_PRIM_BT_2020;
+ } else if (params->color.space == MP_CSP_BT_709) {
+ params->color.primaries = MP_CSP_PRIM_BT_709;
+ } else {
+ // Ambiguous colormatrix for BT.601, guess based on res
+ params->color.primaries = mp_csp_guess_primaries(params->w, params->h);
+ }
+ }
+ if (params->color.gamma == MP_CSP_TRC_AUTO)
+ params->color.gamma = MP_CSP_TRC_BT_1886;
+ } else if (forced_csp == MP_CSP_RGB) {
+ params->color.space = MP_CSP_RGB;
+ params->color.levels = MP_CSP_LEVELS_PC;
+
+ // The majority of RGB content is either sRGB or (rarely) some other
+ // color space which we don't even handle, like AdobeRGB or
+ // ProPhotoRGB. The only reasonable thing we can do is assume it's
+ // sRGB and hope for the best, which should usually just work out fine.
+ // Note: sRGB primaries = BT.709 primaries
+ if (params->color.primaries == MP_CSP_PRIM_AUTO)
+ params->color.primaries = MP_CSP_PRIM_BT_709;
+ if (params->color.gamma == MP_CSP_TRC_AUTO)
+ params->color.gamma = MP_CSP_TRC_SRGB;
+ } else if (forced_csp == MP_CSP_XYZ) {
+ params->color.space = MP_CSP_XYZ;
+ params->color.levels = MP_CSP_LEVELS_PC;
+ // Force gamma to ST428 as this is the only correct for DCDM X'Y'Z'
+ params->color.gamma = MP_CSP_TRC_ST428;
+ // Don't care about primaries, they shouldn't be used, or if anything
+ // MP_CSP_PRIM_ST428 should be defined.
+ } else {
+ // We have no clue.
+ params->color.space = MP_CSP_AUTO;
+ params->color.levels = MP_CSP_LEVELS_AUTO;
+ params->color.primaries = MP_CSP_PRIM_AUTO;
+ params->color.gamma = MP_CSP_TRC_AUTO;
+ }
+
+ if (!params->color.hdr.max_luma) {
+ if (params->color.gamma == MP_CSP_TRC_HLG) {
+ params->color.hdr.max_luma = 1000; // reference display
+ } else {
+ // If the signal peak is unknown, we're forced to pick the TRC's
+ // nominal range as the signal peak to prevent clipping
+ params->color.hdr.max_luma = mp_trc_nom_peak(params->color.gamma) * MP_REF_WHITE;
+ }
+ }
+
+ if (!mp_trc_is_hdr(params->color.gamma)) {
+ // Some clips have leftover HDR metadata after conversion to SDR, so to
+ // avoid blowing up the tone mapping code, strip/sanitize it
+ params->color.hdr = pl_hdr_metadata_empty;
+ }
+
+ if (params->chroma_location == MP_CHROMA_AUTO) {
+ if (params->color.levels == MP_CSP_LEVELS_TV)
+ params->chroma_location = MP_CHROMA_LEFT;
+ if (params->color.levels == MP_CSP_LEVELS_PC)
+ params->chroma_location = MP_CHROMA_CENTER;
+ }
+
+ if (params->color.light == MP_CSP_LIGHT_AUTO) {
+ // HLG is always scene-referred (using its own OOTF), everything else
+ // we assume is display-referred by default.
+ if (params->color.gamma == MP_CSP_TRC_HLG) {
+ params->color.light = MP_CSP_LIGHT_SCENE_HLG;
+ } else {
+ params->color.light = MP_CSP_LIGHT_DISPLAY;
+ }
+ }
+}
+
+// Create a new mp_image reference to av_frame.
+struct mp_image *mp_image_from_av_frame(struct AVFrame *src)
+{
+ struct mp_image *dst = &(struct mp_image){0};
+ AVFrameSideData *sd;
+
+ for (int p = 0; p < MP_MAX_PLANES; p++)
+ dst->bufs[p] = src->buf[p];
+
+ dst->hwctx = src->hw_frames_ctx;
+
+ mp_image_setfmt(dst, pixfmt2imgfmt(src->format));
+ mp_image_set_size(dst, src->width, src->height);
+
+ dst->params.p_w = src->sample_aspect_ratio.num;
+ dst->params.p_h = src->sample_aspect_ratio.den;
+
+ for (int i = 0; i < 4; i++) {
+ dst->planes[i] = src->data[i];
+ dst->stride[i] = src->linesize[i];
+ }
+
+ dst->pict_type = src->pict_type;
+
+ dst->params.crop.x0 = src->crop_left;
+ dst->params.crop.y0 = src->crop_top;
+ dst->params.crop.x1 = src->width - src->crop_right;
+ dst->params.crop.y1 = src->height - src->crop_bottom;
+
+ dst->fields = 0;
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
+ if (src->flags & AV_FRAME_FLAG_INTERLACED)
+ dst->fields |= MP_IMGFIELD_INTERLACED;
+ if (src->flags & AV_FRAME_FLAG_TOP_FIELD_FIRST)
+ dst->fields |= MP_IMGFIELD_TOP_FIRST;
+#else
+ if (src->interlaced_frame)
+ dst->fields |= MP_IMGFIELD_INTERLACED;
+ if (src->top_field_first)
+ dst->fields |= MP_IMGFIELD_TOP_FIRST;
+#endif
+ if (src->repeat_pict == 1)
+ dst->fields |= MP_IMGFIELD_REPEAT_FIRST;
+
+ dst->params.color = (struct mp_colorspace){
+ .space = avcol_spc_to_mp_csp(src->colorspace),
+ .levels = avcol_range_to_mp_csp_levels(src->color_range),
+ .primaries = avcol_pri_to_mp_csp_prim(src->color_primaries),
+ .gamma = avcol_trc_to_mp_csp_trc(src->color_trc),
+ };
+
+ dst->params.chroma_location = avchroma_location_to_mp(src->chroma_location);
+
+ if (src->opaque_ref) {
+ struct mp_image_params *p = (void *)src->opaque_ref->data;
+ dst->params.stereo3d = p->stereo3d;
+ // Might be incorrect if colorspace changes.
+ dst->params.color.light = p->color.light;
+ dst->params.alpha = p->alpha;
+ }
+
+ sd = av_frame_get_side_data(src, AV_FRAME_DATA_DISPLAYMATRIX);
+ if (sd) {
+ double r = av_display_rotation_get((int32_t *)(sd->data));
+ if (!isnan(r))
+ dst->params.rotate = (((int)(-r) % 360) + 360) % 360;
+ }
+
+ sd = av_frame_get_side_data(src, AV_FRAME_DATA_ICC_PROFILE);
+ if (sd)
+ dst->icc_profile = sd->buf;
+
+ AVFrameSideData *mdm = av_frame_get_side_data(src, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);
+ AVFrameSideData *clm = av_frame_get_side_data(src, AV_FRAME_DATA_CONTENT_LIGHT_LEVEL);
+ AVFrameSideData *dhp = av_frame_get_side_data(src, AV_FRAME_DATA_DYNAMIC_HDR_PLUS);
+ pl_map_hdr_metadata(&dst->params.color.hdr, &(struct pl_av_hdr_metadata) {
+ .mdm = (void *)(mdm ? mdm->data : NULL),
+ .clm = (void *)(clm ? clm->data : NULL),
+ .dhp = (void *)(dhp ? dhp->data : NULL),
+ });
+
+ sd = av_frame_get_side_data(src, AV_FRAME_DATA_A53_CC);
+ if (sd)
+ dst->a53_cc = sd->buf;
+
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 16, 100)
+ sd = av_frame_get_side_data(src, AV_FRAME_DATA_DOVI_METADATA);
+ if (sd)
+ dst->dovi = sd->buf;
+
+ sd = av_frame_get_side_data(src, AV_FRAME_DATA_DOVI_RPU_BUFFER);
+ if (sd)
+ dst->dovi_buf = sd->buf;
+#endif
+
+ sd = av_frame_get_side_data(src, AV_FRAME_DATA_FILM_GRAIN_PARAMS);
+ if (sd)
+ dst->film_grain = sd->buf;
+
+ for (int n = 0; n < src->nb_side_data; n++) {
+ sd = src->side_data[n];
+ struct mp_ff_side_data mpsd = {
+ .type = sd->type,
+ .buf = sd->buf,
+ };
+ MP_TARRAY_APPEND(NULL, dst->ff_side_data, dst->num_ff_side_data, mpsd);
+ }
+
+ if (dst->hwctx) {
+ AVHWFramesContext *fctx = (void *)dst->hwctx->data;
+ dst->params.hw_subfmt = pixfmt2imgfmt(fctx->sw_format);
+ }
+
+ struct mp_image *res = mp_image_new_ref(dst);
+
+ // Allocated, but non-refcounted data.
+ talloc_free(dst->ff_side_data);
+
+ return res;
+}
+
+
+// Convert the mp_image reference to a AVFrame reference.
+struct AVFrame *mp_image_to_av_frame(struct mp_image *src)
+{
+ struct mp_image *new_ref = mp_image_new_ref(src);
+ AVFrame *dst = av_frame_alloc();
+ if (!dst || !new_ref) {
+ talloc_free(new_ref);
+ av_frame_free(&dst);
+ return NULL;
+ }
+
+ for (int p = 0; p < MP_MAX_PLANES; p++) {
+ dst->buf[p] = new_ref->bufs[p];
+ new_ref->bufs[p] = NULL;
+ }
+
+ dst->hw_frames_ctx = new_ref->hwctx;
+ new_ref->hwctx = NULL;
+
+ dst->format = imgfmt2pixfmt(src->imgfmt);
+ dst->width = src->w;
+ dst->height = src->h;
+
+ dst->crop_left = src->params.crop.x0;
+ dst->crop_top = src->params.crop.y0;
+ dst->crop_right = dst->width - src->params.crop.x1;
+ dst->crop_bottom = dst->height - src->params.crop.y1;
+
+ dst->sample_aspect_ratio.num = src->params.p_w;
+ dst->sample_aspect_ratio.den = src->params.p_h;
+
+ for (int i = 0; i < 4; i++) {
+ dst->data[i] = src->planes[i];
+ dst->linesize[i] = src->stride[i];
+ }
+ dst->extended_data = dst->data;
+
+ dst->pict_type = src->pict_type;
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
+ if (src->fields & MP_IMGFIELD_INTERLACED)
+ dst->flags |= AV_FRAME_FLAG_INTERLACED;
+ if (src->fields & MP_IMGFIELD_TOP_FIRST)
+ dst->flags |= AV_FRAME_FLAG_TOP_FIELD_FIRST;
+#else
+ if (src->fields & MP_IMGFIELD_INTERLACED)
+ dst->interlaced_frame = 1;
+ if (src->fields & MP_IMGFIELD_TOP_FIRST)
+ dst->top_field_first = 1;
+#endif
+ if (src->fields & MP_IMGFIELD_REPEAT_FIRST)
+ dst->repeat_pict = 1;
+
+ dst->colorspace = mp_csp_to_avcol_spc(src->params.color.space);
+ dst->color_range = mp_csp_levels_to_avcol_range(src->params.color.levels);
+ dst->color_primaries =
+ mp_csp_prim_to_avcol_pri(src->params.color.primaries);
+ dst->color_trc = mp_csp_trc_to_avcol_trc(src->params.color.gamma);
+
+ dst->chroma_location = mp_chroma_location_to_av(src->params.chroma_location);
+
+ dst->opaque_ref = av_buffer_alloc(sizeof(struct mp_image_params));
+ MP_HANDLE_OOM(dst->opaque_ref);
+ *(struct mp_image_params *)dst->opaque_ref->data = src->params;
+
+ if (src->icc_profile) {
+ AVFrameSideData *sd =
+ av_frame_new_side_data_from_buf(dst, AV_FRAME_DATA_ICC_PROFILE,
+ new_ref->icc_profile);
+ MP_HANDLE_OOM(sd);
+ new_ref->icc_profile = NULL;
+ }
+
+ pl_avframe_set_color(dst, (struct pl_color_space){
+ .primaries = mp_prim_to_pl(src->params.color.primaries),
+ .transfer = mp_trc_to_pl(src->params.color.gamma),
+ .hdr = src->params.color.hdr,
+ });
+
+ {
+ AVFrameSideData *sd = av_frame_new_side_data(dst,
+ AV_FRAME_DATA_DISPLAYMATRIX,
+ sizeof(int32_t) * 9);
+ MP_HANDLE_OOM(sd);
+ av_display_rotation_set((int32_t *)sd->data, src->params.rotate);
+ }
+
+ // Add back side data, but only for types which are not specially handled
+ // above. Keep in mind that the types above will be out of sync anyway.
+ for (int n = 0; n < new_ref->num_ff_side_data; n++) {
+ struct mp_ff_side_data *mpsd = &new_ref->ff_side_data[n];
+ if (!av_frame_get_side_data(dst, mpsd->type)) {
+ AVFrameSideData *sd = av_frame_new_side_data_from_buf(dst, mpsd->type,
+ mpsd->buf);
+ MP_HANDLE_OOM(sd);
+ mpsd->buf = NULL;
+ }
+ }
+
+ talloc_free(new_ref);
+
+ if (dst->format == AV_PIX_FMT_NONE)
+ av_frame_free(&dst);
+ return dst;
+}
+
+// Same as mp_image_to_av_frame(), but unref img. (It does so even on failure.)
+struct AVFrame *mp_image_to_av_frame_and_unref(struct mp_image *img)
+{
+ AVFrame *frame = mp_image_to_av_frame(img);
+ talloc_free(img);
+ return frame;
+}
+
+void memset_pic(void *dst, int fill, int bytesPerLine, int height, int stride)
+{
+ if (bytesPerLine == stride && height) {
+ memset(dst, fill, stride * (height - 1) + bytesPerLine);
+ } else {
+ for (int i = 0; i < height; i++) {
+ memset(dst, fill, bytesPerLine);
+ dst = (uint8_t *)dst + stride;
+ }
+ }
+}
+
+void memset16_pic(void *dst, int fill, int unitsPerLine, int height, int stride)
+{
+ if (fill == 0) {
+ memset_pic(dst, 0, unitsPerLine * 2, height, stride);
+ } else {
+ for (int i = 0; i < height; i++) {
+ uint16_t *line = dst;
+ uint16_t *end = line + unitsPerLine;
+ while (line < end)
+ *line++ = fill;
+ dst = (uint8_t *)dst + stride;
+ }
+ }
+}
+
+// Pixel at the given luma position on the given plane. x/y always refer to
+// non-subsampled coordinates (even if plane is chroma).
+// The coordinates must be aligned to mp_imgfmt_desc.align_x/y (these are byte
+// and chroma boundaries).
+// You cannot access e.g. individual luma pixels on the luma plane with yuv420p.
+void *mp_image_pixel_ptr(struct mp_image *img, int plane, int x, int y)
+{
+ assert(MP_IS_ALIGNED(x, img->fmt.align_x));
+ assert(MP_IS_ALIGNED(y, img->fmt.align_y));
+ return mp_image_pixel_ptr_ny(img, plane, x, y);
+}
+
+// Like mp_image_pixel_ptr(), but do not require alignment on Y coordinates if
+// the plane does not require it. Use with care.
+// Useful for addressing luma rows.
+void *mp_image_pixel_ptr_ny(struct mp_image *img, int plane, int x, int y)
+{
+ assert(MP_IS_ALIGNED(x, img->fmt.align_x));
+ assert(MP_IS_ALIGNED(y, 1 << img->fmt.ys[plane]));
+ return img->planes[plane] +
+ img->stride[plane] * (ptrdiff_t)(y >> img->fmt.ys[plane]) +
+ (x >> img->fmt.xs[plane]) * (size_t)img->fmt.bpp[plane] / 8;
+}
+
+// Return size of pixels [x0, x0+w-1] in bytes. The coordinates refer to non-
+// subsampled pixels (basically plane 0), and the size is rounded to chroma
+// and byte alignment boundaries for the entire image, even if plane!=0.
+// x0!=0 is useful for rounding (e.g. 8 bpp, x0=7, w=7 => 0..15 => 2 bytes).
+size_t mp_image_plane_bytes(struct mp_image *img, int plane, int x0, int w)
+{
+ int x1 = MP_ALIGN_UP(x0 + w, img->fmt.align_x);
+ x0 = MP_ALIGN_DOWN(x0, img->fmt.align_x);
+ size_t bpp = img->fmt.bpp[plane];
+ int xs = img->fmt.xs[plane];
+ return (x1 >> xs) * bpp / 8 - (x0 >> xs) * bpp / 8;
+}
diff --git a/video/mp_image.h b/video/mp_image.h
new file mode 100644
index 0000000..0408aab
--- /dev/null
+++ b/video/mp_image.h
@@ -0,0 +1,203 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_MP_IMAGE_H
+#define MPLAYER_MP_IMAGE_H
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include "common/common.h"
+#include "common/msg.h"
+#include "csputils.h"
+#include "video/img_format.h"
+
+// Assumed minimum align needed for image allocation. It's notable that FFmpeg's
+// libraries except libavcodec don't really know what alignment they want.
+// Things will randomly crash or get slower if the alignment is not satisfied.
+// Whatever. This value should be pretty safe with current CPU architectures.
+#define MP_IMAGE_BYTE_ALIGN 64
+
+#define MP_IMGFIELD_TOP_FIRST 0x02
+#define MP_IMGFIELD_REPEAT_FIRST 0x04
+#define MP_IMGFIELD_INTERLACED 0x20
+
+// Describes image parameters that usually stay constant.
+// New fields can be added in the future. Code changing the parameters should
+// usually copy the whole struct, so that fields added later will be preserved.
+struct mp_image_params {
+ enum mp_imgfmt imgfmt; // pixel format
+ enum mp_imgfmt hw_subfmt; // underlying format for some hwaccel pixfmts
+ int w, h; // image dimensions
+ int p_w, p_h; // define pixel aspect ratio (undefined: 0/0)
+ bool force_window; // fake image created by handle_force_window
+ struct mp_colorspace color;
+ enum mp_chroma_location chroma_location;
+ // The image should be rotated clockwise (0-359 degrees).
+ int rotate;
+ enum mp_stereo3d_mode stereo3d; // image is encoded with this mode
+ enum mp_alpha_type alpha; // usually auto; only set if explicitly known
+ struct mp_rect crop; // crop applied on image
+};
+
+/* Memory management:
+ * - mp_image is a light-weight reference to the actual image data (pixels).
+ * The actual image data is reference counted and can outlive mp_image
+ * allocations. mp_image references can be created with mp_image_new_ref()
+ * and free'd with talloc_free() (the helpers mp_image_setrefp() and
+ * mp_image_unrefp() can also be used). The actual image data is free'd when
+ * the last mp_image reference to it is free'd.
+ * - Each mp_image has a clear owner. The owner can do anything with it, such
+ * as changing mp_image fields. Instead of making ownership ambiguous by
+ * sharing a mp_image reference, new references should be created.
+ * - Write access to the actual image data is allowed only after calling
+ * mp_image_make_writeable(), or if mp_image_is_writeable() returns true.
+ * Conceptually, images can be changed by their owner only, and copy-on-write
+ * is used to ensure that other references do not see any changes to the
+ * image data. mp_image_make_writeable() will do that copy if required.
+ */
+typedef struct mp_image {
+ int w, h; // visible dimensions (redundant with params.w/h)
+
+ struct mp_image_params params;
+
+ // fields redundant to params.imgfmt, for convenience or compatibility
+ struct mp_imgfmt_desc fmt;
+ enum mp_imgfmt imgfmt;
+ int num_planes;
+
+ uint8_t *planes[MP_MAX_PLANES];
+ int stride[MP_MAX_PLANES];
+
+ int pict_type; // 0->unknown, 1->I, 2->P, 3->B
+ int fields;
+
+ /* only inside filter chain */
+ double pts;
+ /* only after decoder */
+ double dts, pkt_duration;
+ /* container reported FPS; can be incorrect, or 0 if unknown */
+ double nominal_fps;
+ /* for private use */
+ void* priv;
+
+ // Reference-counted data references.
+ // These do not necessarily map directly to planes[]. They can have
+ // different order or count. There shouldn't be more buffers than planes.
+ // If bufs[n] is NULL, bufs[n+1] must also be NULL.
+ // All mp_* functions manage this automatically; do not mess with it.
+ // (See also AVFrame.buf.)
+ struct AVBufferRef *bufs[MP_MAX_PLANES];
+ // Points to AVHWFramesContext* (same as AVFrame.hw_frames_ctx)
+ struct AVBufferRef *hwctx;
+ // Embedded ICC profile, if any
+ struct AVBufferRef *icc_profile;
+ // Closed captions packet, if any (only after decoder)
+ struct AVBufferRef *a53_cc;
+ // Dolby Vision metadata, if any
+ struct AVBufferRef *dovi;
+ // Film grain data, if any
+ struct AVBufferRef *film_grain;
+ // Dolby Vision RPU buffer, if any
+ struct AVBufferRef *dovi_buf;
+ // Other side data we don't care about.
+ struct mp_ff_side_data *ff_side_data;
+ int num_ff_side_data;
+} mp_image_t;
+
+struct mp_ff_side_data {
+ int type;
+ struct AVBufferRef *buf;
+};
+
+int mp_chroma_div_up(int size, int shift);
+
+int mp_image_get_alloc_size(int imgfmt, int w, int h, int stride_align);
+struct mp_image *mp_image_from_buffer(int imgfmt, int w, int h, int stride_align,
+ uint8_t *buffer, int buffer_size,
+ void *free_opaque,
+ void (*free)(void *opaque, uint8_t *data));
+
+struct mp_image *mp_image_alloc(int fmt, int w, int h);
+void mp_image_copy(struct mp_image *dmpi, struct mp_image *mpi);
+void mp_image_copy_attributes(struct mp_image *dmpi, struct mp_image *mpi);
+struct mp_image *mp_image_new_copy(struct mp_image *img);
+struct mp_image *mp_image_new_ref(struct mp_image *img);
+bool mp_image_is_writeable(struct mp_image *img);
+bool mp_image_make_writeable(struct mp_image *img);
+void mp_image_setrefp(struct mp_image **p_img, struct mp_image *new_value);
+void mp_image_unrefp(struct mp_image **p_img);
+
+void mp_image_clear(struct mp_image *mpi, int x0, int y0, int x1, int y1);
+void mp_image_clear_rc(struct mp_image *mpi, struct mp_rect rc);
+void mp_image_clear_rc_inv(struct mp_image *mpi, struct mp_rect rc);
+void mp_image_crop(struct mp_image *img, int x0, int y0, int x1, int y1);
+void mp_image_crop_rc(struct mp_image *img, struct mp_rect rc);
+void mp_image_vflip(struct mp_image *img);
+
+void mp_image_set_size(struct mp_image *mpi, int w, int h);
+int mp_image_plane_w(struct mp_image *mpi, int plane);
+int mp_image_plane_h(struct mp_image *mpi, int plane);
+
+void mp_image_setfmt(mp_image_t* mpi, int out_fmt);
+void mp_image_steal_data(struct mp_image *dst, struct mp_image *src);
+void mp_image_unref_data(struct mp_image *img);
+
+int mp_image_approx_byte_size(struct mp_image *img);
+
+struct mp_image *mp_image_new_dummy_ref(struct mp_image *img);
+struct mp_image *mp_image_new_custom_ref(struct mp_image *img, void *arg,
+ void (*free)(void *arg));
+
+void mp_image_params_guess_csp(struct mp_image_params *params);
+
+char *mp_image_params_to_str_buf(char *b, size_t bs,
+ const struct mp_image_params *p);
+#define mp_image_params_to_str(p) mp_image_params_to_str_buf((char[256]){0}, 256, p)
+
+bool mp_image_crop_valid(const struct mp_image_params *p);
+bool mp_image_params_valid(const struct mp_image_params *p);
+bool mp_image_params_equal(const struct mp_image_params *p1,
+ const struct mp_image_params *p2);
+
+void mp_image_params_get_dsize(const struct mp_image_params *p,
+ int *d_w, int *d_h);
+void mp_image_params_set_dsize(struct mp_image_params *p, int d_w, int d_h);
+
+void mp_image_set_params(struct mp_image *image,
+ const struct mp_image_params *params);
+
+void mp_image_set_attributes(struct mp_image *image,
+ const struct mp_image_params *params);
+
+struct AVFrame;
+struct mp_image *mp_image_from_av_frame(struct AVFrame *av_frame);
+struct AVFrame *mp_image_to_av_frame(struct mp_image *img);
+struct AVFrame *mp_image_to_av_frame_and_unref(struct mp_image *img);
+
+void memcpy_pic(void *dst, const void *src, int bytesPerLine, int height,
+ int dstStride, int srcStride);
+void memset_pic(void *dst, int fill, int bytesPerLine, int height, int stride);
+void memset16_pic(void *dst, int fill, int unitsPerLine, int height, int stride);
+
+void *mp_image_pixel_ptr(struct mp_image *img, int plane, int x, int y);
+void *mp_image_pixel_ptr_ny(struct mp_image *img, int plane, int x, int y);
+size_t mp_image_plane_bytes(struct mp_image *img, int plane, int x0, int w);
+
+#endif /* MPLAYER_MP_IMAGE_H */
diff --git a/video/mp_image_pool.c b/video/mp_image_pool.c
new file mode 100644
index 0000000..0b5e520
--- /dev/null
+++ b/video/mp_image_pool.c
@@ -0,0 +1,472 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <libavutil/buffer.h>
+#include <libavutil/hwcontext.h>
+#if HAVE_VULKAN_INTEROP
+#include <libavutil/hwcontext_vulkan.h>
+#endif
+#include <libavutil/mem.h>
+#include <libavutil/pixdesc.h>
+
+#include "mpv_talloc.h"
+
+#include "common/common.h"
+
+#include "fmt-conversion.h"
+#include "mp_image_pool.h"
+#include "mp_image.h"
+#include "osdep/threads.h"
+
+static mp_static_mutex pool_mutex = MP_STATIC_MUTEX_INITIALIZER;
+#define pool_lock() mp_mutex_lock(&pool_mutex)
+#define pool_unlock() mp_mutex_unlock(&pool_mutex)
+
+// Thread-safety: the pool itself is not thread-safe, but pool-allocated images
+// can be referenced and unreferenced from other threads. (As long as the image
+// destructors are thread-safe.)
+
+struct mp_image_pool {
+ struct mp_image **images;
+ int num_images;
+
+ int fmt, w, h;
+
+ mp_image_allocator allocator;
+ void *allocator_ctx;
+
+ bool use_lru;
+ unsigned int lru_counter;
+};
+
+// Used to gracefully handle the case when the pool is freed while image
+// references allocated from the image pool are still held by someone.
+struct image_flags {
+ // If both of these are false, the image must be freed.
+ bool referenced; // outside mp_image reference exists
+ bool pool_alive; // the mp_image_pool references this
+ unsigned int order; // for LRU allocation (basically a timestamp)
+};
+
+static void image_pool_destructor(void *ptr)
+{
+ struct mp_image_pool *pool = ptr;
+ mp_image_pool_clear(pool);
+}
+
+// If tparent!=NULL, set it as talloc parent for the pool.
+struct mp_image_pool *mp_image_pool_new(void *tparent)
+{
+ struct mp_image_pool *pool = talloc_ptrtype(tparent, pool);
+ talloc_set_destructor(pool, image_pool_destructor);
+ *pool = (struct mp_image_pool) {0};
+ return pool;
+}
+
+void mp_image_pool_clear(struct mp_image_pool *pool)
+{
+ for (int n = 0; n < pool->num_images; n++) {
+ struct mp_image *img = pool->images[n];
+ struct image_flags *it = img->priv;
+ bool referenced;
+ pool_lock();
+ assert(it->pool_alive);
+ it->pool_alive = false;
+ referenced = it->referenced;
+ pool_unlock();
+ if (!referenced)
+ talloc_free(img);
+ }
+ pool->num_images = 0;
+}
+
+// This is the only function that is allowed to run in a different thread.
+// (Consider passing an image to another thread, which frees it.)
+static void unref_image(void *opaque, uint8_t *data)
+{
+ struct mp_image *img = opaque;
+ struct image_flags *it = img->priv;
+ bool alive;
+ pool_lock();
+ assert(it->referenced);
+ it->referenced = false;
+ alive = it->pool_alive;
+ pool_unlock();
+ if (!alive)
+ talloc_free(img);
+}
+
+// Return a new image of given format/size. Unlike mp_image_pool_get(), this
+// returns NULL if there is no free image of this format/size.
+struct mp_image *mp_image_pool_get_no_alloc(struct mp_image_pool *pool, int fmt,
+ int w, int h)
+{
+ struct mp_image *new = NULL;
+ pool_lock();
+ for (int n = 0; n < pool->num_images; n++) {
+ struct mp_image *img = pool->images[n];
+ struct image_flags *img_it = img->priv;
+ assert(img_it->pool_alive);
+ if (!img_it->referenced) {
+ if (img->imgfmt == fmt && img->w == w && img->h == h) {
+ if (pool->use_lru) {
+ struct image_flags *new_it = new ? new->priv : NULL;
+ if (!new_it || new_it->order > img_it->order)
+ new = img;
+ } else {
+ new = img;
+ break;
+ }
+ }
+ }
+ }
+ pool_unlock();
+ if (!new)
+ return NULL;
+
+ // Reference the new image. Since mp_image_pool is not declared thread-safe,
+ // and unreffing images from other threads does not allocate new images,
+ // no synchronization is required here.
+ for (int p = 0; p < MP_MAX_PLANES; p++)
+ assert(!!new->bufs[p] == !p); // only 1 AVBufferRef
+
+ struct mp_image *ref = mp_image_new_dummy_ref(new);
+
+ // This assumes the buffer is at this point exclusively owned by us: we
+ // can't track whether the buffer is unique otherwise.
+ // (av_buffer_is_writable() checks the refcount of the new buffer only.)
+ int flags = av_buffer_is_writable(new->bufs[0]) ? 0 : AV_BUFFER_FLAG_READONLY;
+ ref->bufs[0] = av_buffer_create(new->bufs[0]->data, new->bufs[0]->size,
+ unref_image, new, flags);
+ if (!ref->bufs[0]) {
+ talloc_free(ref);
+ return NULL;
+ }
+
+ struct image_flags *it = new->priv;
+ assert(!it->referenced && it->pool_alive);
+ it->referenced = true;
+ it->order = ++pool->lru_counter;
+ return ref;
+}
+
+void mp_image_pool_add(struct mp_image_pool *pool, struct mp_image *new)
+{
+ struct image_flags *it = talloc_ptrtype(new, it);
+ *it = (struct image_flags) { .pool_alive = true };
+ new->priv = it;
+ MP_TARRAY_APPEND(pool, pool->images, pool->num_images, new);
+}
+
+// Return a new image of given format/size. The only difference to
+// mp_image_alloc() is that there is a transparent mechanism to recycle image
+// data allocations through this pool.
+// If pool==NULL, mp_image_alloc() is called (for convenience).
+// The image can be free'd with talloc_free().
+// Returns NULL on OOM.
+struct mp_image *mp_image_pool_get(struct mp_image_pool *pool, int fmt,
+ int w, int h)
+{
+ if (!pool)
+ return mp_image_alloc(fmt, w, h);
+ struct mp_image *new = mp_image_pool_get_no_alloc(pool, fmt, w, h);
+ if (!new) {
+ if (fmt != pool->fmt || w != pool->w || h != pool->h)
+ mp_image_pool_clear(pool);
+ pool->fmt = fmt;
+ pool->w = w;
+ pool->h = h;
+ if (pool->allocator) {
+ new = pool->allocator(pool->allocator_ctx, fmt, w, h);
+ } else {
+ new = mp_image_alloc(fmt, w, h);
+ }
+ if (!new)
+ return NULL;
+ mp_image_pool_add(pool, new);
+ new = mp_image_pool_get_no_alloc(pool, fmt, w, h);
+ }
+ return new;
+}
+
+// Like mp_image_new_copy(), but allocate the image out of the pool.
+// If pool==NULL, a plain copy is made (for convenience).
+// Returns NULL on OOM.
+struct mp_image *mp_image_pool_new_copy(struct mp_image_pool *pool,
+ struct mp_image *img)
+{
+ struct mp_image *new = mp_image_pool_get(pool, img->imgfmt, img->w, img->h);
+ if (new) {
+ mp_image_copy(new, img);
+ mp_image_copy_attributes(new, img);
+ }
+ return new;
+}
+
+// Like mp_image_make_writeable(), but if a copy has to be made, allocate it
+// out of the pool.
+// If pool==NULL, mp_image_make_writeable() is called (for convenience).
+// Returns false on failure (see mp_image_make_writeable()).
+bool mp_image_pool_make_writeable(struct mp_image_pool *pool,
+ struct mp_image *img)
+{
+ if (mp_image_is_writeable(img))
+ return true;
+ struct mp_image *new = mp_image_pool_new_copy(pool, img);
+ if (!new)
+ return false;
+ mp_image_steal_data(img, new);
+ assert(mp_image_is_writeable(img));
+ return true;
+}
+
+// Call cb(cb_data, fmt, w, h) to allocate an image. Note that the resulting
+// image must use only 1 AVBufferRef. The returned image must also be owned
+// exclusively by the image pool, otherwise mp_image_is_writeable() will not
+// work due to FFmpeg restrictions.
+void mp_image_pool_set_allocator(struct mp_image_pool *pool,
+ mp_image_allocator cb, void *cb_data)
+{
+ pool->allocator = cb;
+ pool->allocator_ctx = cb_data;
+}
+
+// Put into LRU mode. (Likely better for hwaccel surfaces, but worse for memory.)
+void mp_image_pool_set_lru(struct mp_image_pool *pool)
+{
+ pool->use_lru = true;
+}
+
+// Return the sw image format mp_image_hw_download() would use. This can be
+// different from src->params.hw_subfmt in obscure cases.
+int mp_image_hw_download_get_sw_format(struct mp_image *src)
+{
+ if (!src->hwctx)
+ return 0;
+
+ // Try to find the first format which we can apparently use.
+ int imgfmt = 0;
+ enum AVPixelFormat *fmts;
+ if (av_hwframe_transfer_get_formats(src->hwctx,
+ AV_HWFRAME_TRANSFER_DIRECTION_FROM, &fmts, 0) < 0)
+ return 0;
+ for (int n = 0; fmts[n] != AV_PIX_FMT_NONE; n++) {
+ imgfmt = pixfmt2imgfmt(fmts[n]);
+ if (imgfmt)
+ break;
+ }
+ av_free(fmts);
+
+ return imgfmt;
+}
+
+// Copies the contents of the HW surface src to system memory and returns it.
+// If swpool is not NULL, it's used to allocate the target image.
+// src must be a hw surface with a AVHWFramesContext attached.
+// The returned image is cropped as needed.
+// Returns NULL on failure.
+struct mp_image *mp_image_hw_download(struct mp_image *src,
+ struct mp_image_pool *swpool)
+{
+ int imgfmt = mp_image_hw_download_get_sw_format(src);
+ if (!imgfmt)
+ return NULL;
+
+ assert(src->hwctx);
+ AVHWFramesContext *fctx = (void *)src->hwctx->data;
+
+ struct mp_image *dst =
+ mp_image_pool_get(swpool, imgfmt, fctx->width, fctx->height);
+ if (!dst)
+ return NULL;
+
+ // Target image must be writable, so unref it.
+ AVFrame *dstav = mp_image_to_av_frame_and_unref(dst);
+ if (!dstav)
+ return NULL;
+
+ AVFrame *srcav = mp_image_to_av_frame(src);
+ if (!srcav) {
+ av_frame_unref(dstav);
+ return NULL;
+ }
+
+ int res = av_hwframe_transfer_data(dstav, srcav, 0);
+ av_frame_free(&srcav);
+ dst = mp_image_from_av_frame(dstav);
+ av_frame_free(&dstav);
+ if (res >= 0 && dst) {
+ mp_image_set_size(dst, src->w, src->h);
+ mp_image_copy_attributes(dst, src);
+ } else {
+ mp_image_unrefp(&dst);
+ }
+ return dst;
+}
+
+bool mp_image_hw_upload(struct mp_image *hw_img, struct mp_image *src)
+{
+ if (hw_img->w != src->w || hw_img->h != src->h)
+ return false;
+
+ if (!hw_img->hwctx)
+ return false;
+
+ bool ok = false;
+ AVFrame *dstav = NULL;
+ AVFrame *srcav = NULL;
+
+ // This means the destination image will not be "writable", which would be
+ // a pain if Libav enforced this - fortunately it doesn't care. We can
+ // transfer data to it even if there are multiple refs.
+ dstav = mp_image_to_av_frame(hw_img);
+ if (!dstav)
+ goto done;
+
+ srcav = mp_image_to_av_frame(src);
+ if (!srcav)
+ goto done;
+
+ ok = av_hwframe_transfer_data(dstav, srcav, 0) >= 0;
+
+done:
+ av_frame_free(&srcav);
+ av_frame_free(&dstav);
+
+ if (ok)
+ mp_image_copy_attributes(hw_img, src);
+ return ok;
+}
+
+bool mp_update_av_hw_frames_pool(struct AVBufferRef **hw_frames_ctx,
+ struct AVBufferRef *hw_device_ctx,
+ int imgfmt, int sw_imgfmt, int w, int h,
+ bool disable_multiplane)
+{
+ enum AVPixelFormat format = imgfmt2pixfmt(imgfmt);
+ enum AVPixelFormat sw_format = imgfmt2pixfmt(sw_imgfmt);
+
+ if (format == AV_PIX_FMT_NONE || sw_format == AV_PIX_FMT_NONE ||
+ !hw_device_ctx || w < 1 || h < 1)
+ {
+ av_buffer_unref(hw_frames_ctx);
+ return false;
+ }
+
+ if (*hw_frames_ctx) {
+ AVHWFramesContext *hw_frames = (void *)(*hw_frames_ctx)->data;
+
+ if (hw_frames->device_ref->data != hw_device_ctx->data ||
+ hw_frames->format != format || hw_frames->sw_format != sw_format ||
+ hw_frames->width != w || hw_frames->height != h)
+ av_buffer_unref(hw_frames_ctx);
+ }
+
+ if (!*hw_frames_ctx) {
+ *hw_frames_ctx = av_hwframe_ctx_alloc(hw_device_ctx);
+ if (!*hw_frames_ctx)
+ return false;
+
+ AVHWFramesContext *hw_frames = (void *)(*hw_frames_ctx)->data;
+ hw_frames->format = format;
+ hw_frames->sw_format = sw_format;
+ hw_frames->width = w;
+ hw_frames->height = h;
+
+#if HAVE_VULKAN_INTEROP
+ if (format == AV_PIX_FMT_VULKAN && disable_multiplane) {
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(sw_format);
+ if ((desc->flags & AV_PIX_FMT_FLAG_PLANAR) &&
+ !(desc->flags & AV_PIX_FMT_FLAG_RGB)) {
+ AVVulkanFramesContext *vk_frames = hw_frames->hwctx;
+ vk_frames->flags = AV_VK_FRAME_FLAG_DISABLE_MULTIPLANE;
+ }
+ }
+#endif
+
+ if (av_hwframe_ctx_init(*hw_frames_ctx) < 0) {
+ av_buffer_unref(hw_frames_ctx);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+struct mp_image *mp_av_pool_image_hw_upload(struct AVBufferRef *hw_frames_ctx,
+ struct mp_image *src)
+{
+ AVFrame *av_frame = av_frame_alloc();
+ if (!av_frame)
+ return NULL;
+ if (av_hwframe_get_buffer(hw_frames_ctx, av_frame, 0) < 0) {
+ av_frame_free(&av_frame);
+ return NULL;
+ }
+ struct mp_image *dst = mp_image_from_av_frame(av_frame);
+ av_frame_free(&av_frame);
+ if (!dst)
+ return NULL;
+
+ if (dst->w < src->w || dst->h < src->h) {
+ talloc_free(dst);
+ return NULL;
+ }
+
+ mp_image_set_size(dst, src->w, src->h);
+
+ if (!mp_image_hw_upload(dst, src)) {
+ talloc_free(dst);
+ return NULL;
+ }
+
+ mp_image_copy_attributes(dst, src);
+ return dst;
+}
+
+struct mp_image *mp_av_pool_image_hw_map(struct AVBufferRef *hw_frames_ctx,
+ struct mp_image *src)
+{
+ AVFrame *dst_frame = av_frame_alloc();
+ if (!dst_frame)
+ return NULL;
+
+ dst_frame->format = ((AVHWFramesContext*)hw_frames_ctx->data)->format;
+ dst_frame->hw_frames_ctx = av_buffer_ref(hw_frames_ctx);
+
+ AVFrame *src_frame = mp_image_to_av_frame(src);
+ if (av_hwframe_map(dst_frame, src_frame, 0) < 0) {
+ av_frame_free(&src_frame);
+ av_frame_free(&dst_frame);
+ return NULL;
+ }
+ av_frame_free(&src_frame);
+
+ struct mp_image *dst = mp_image_from_av_frame(dst_frame);
+ av_frame_free(&dst_frame);
+ if (!dst)
+ return NULL;
+
+ mp_image_copy_attributes(dst, src);
+ return dst;
+}
diff --git a/video/mp_image_pool.h b/video/mp_image_pool.h
new file mode 100644
index 0000000..8cb2a5f
--- /dev/null
+++ b/video/mp_image_pool.h
@@ -0,0 +1,47 @@
+#ifndef MPV_MP_IMAGE_POOL_H
+#define MPV_MP_IMAGE_POOL_H
+
+#include <stdbool.h>
+
+struct mp_image_pool;
+
+struct mp_image_pool *mp_image_pool_new(void *tparent);
+struct mp_image *mp_image_pool_get(struct mp_image_pool *pool, int fmt,
+ int w, int h);
+// the reference to "new" is transferred to the pool
+void mp_image_pool_add(struct mp_image_pool *pool, struct mp_image *new);
+void mp_image_pool_clear(struct mp_image_pool *pool);
+
+void mp_image_pool_set_lru(struct mp_image_pool *pool);
+
+struct mp_image *mp_image_pool_get_no_alloc(struct mp_image_pool *pool, int fmt,
+ int w, int h);
+
+typedef struct mp_image *(*mp_image_allocator)(void *data, int fmt, int w, int h);
+void mp_image_pool_set_allocator(struct mp_image_pool *pool,
+ mp_image_allocator cb, void *cb_data);
+
+struct mp_image *mp_image_pool_new_copy(struct mp_image_pool *pool,
+ struct mp_image *img);
+bool mp_image_pool_make_writeable(struct mp_image_pool *pool,
+ struct mp_image *img);
+
+struct mp_image *mp_image_hw_download(struct mp_image *img,
+ struct mp_image_pool *swpool);
+
+int mp_image_hw_download_get_sw_format(struct mp_image *img);
+
+bool mp_image_hw_upload(struct mp_image *hw_img, struct mp_image *src);
+
+struct AVBufferRef;
+bool mp_update_av_hw_frames_pool(struct AVBufferRef **hw_frames_ctx,
+ struct AVBufferRef *hw_device_ctx,
+ int imgfmt, int sw_imgfmt, int w, int h,
+ bool disable_multiplane);
+
+struct mp_image *mp_av_pool_image_hw_upload(struct AVBufferRef *hw_frames_ctx,
+ struct mp_image *src);
+
+struct mp_image *mp_av_pool_image_hw_map(struct AVBufferRef *hw_frames_ctx,
+ struct mp_image *src);
+#endif
diff --git a/video/out/android_common.c b/video/out/android_common.c
new file mode 100644
index 0000000..27e7b5b
--- /dev/null
+++ b/video/out/android_common.c
@@ -0,0 +1,99 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavcodec/jni.h>
+#include <android/native_window_jni.h>
+
+#include "android_common.h"
+#include "common/msg.h"
+#include "misc/jni.h"
+#include "options/m_config.h"
+#include "vo.h"
+
+struct vo_android_state {
+ struct mp_log *log;
+ ANativeWindow *native_window;
+};
+
+bool vo_android_init(struct vo *vo)
+{
+ vo->android = talloc_zero(vo, struct vo_android_state);
+ struct vo_android_state *ctx = vo->android;
+
+ *ctx = (struct vo_android_state){
+ .log = mp_log_new(ctx, vo->log, "android"),
+ };
+
+ JNIEnv *env = MP_JNI_GET_ENV(ctx);
+ if (!env) {
+ MP_FATAL(ctx, "Could not attach java VM.\n");
+ goto fail;
+ }
+
+ assert(vo->opts->WinID != 0 && vo->opts->WinID != -1);
+ jobject surface = (jobject)(intptr_t)vo->opts->WinID;
+ ctx->native_window = ANativeWindow_fromSurface(env, surface);
+ if (!ctx->native_window) {
+ MP_FATAL(ctx, "Failed to create ANativeWindow\n");
+ goto fail;
+ }
+
+ return true;
+fail:
+ talloc_free(ctx);
+ vo->android = NULL;
+ return false;
+}
+
+void vo_android_uninit(struct vo *vo)
+{
+ struct vo_android_state *ctx = vo->android;
+ if (!ctx)
+ return;
+
+ if (ctx->native_window)
+ ANativeWindow_release(ctx->native_window);
+
+ talloc_free(ctx);
+ vo->android = NULL;
+}
+
+ANativeWindow *vo_android_native_window(struct vo *vo)
+{
+ struct vo_android_state *ctx = vo->android;
+ return ctx->native_window;
+}
+
+bool vo_android_surface_size(struct vo *vo, int *out_w, int *out_h)
+{
+ struct vo_android_state *ctx = vo->android;
+
+ int w = vo->opts->android_surface_size.w,
+ h = vo->opts->android_surface_size.h;
+ if (!w)
+ w = ANativeWindow_getWidth(ctx->native_window);
+ if (!h)
+ h = ANativeWindow_getHeight(ctx->native_window);
+
+ if (w <= 0 || h <= 0) {
+ MP_ERR(ctx, "Failed to get height and width.\n");
+ return false;
+ }
+ *out_w = w;
+ *out_h = h;
+ return true;
+}
diff --git a/video/out/android_common.h b/video/out/android_common.h
new file mode 100644
index 0000000..7f075ea
--- /dev/null
+++ b/video/out/android_common.h
@@ -0,0 +1,29 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <android/native_window_jni.h>
+
+#include "common/common.h"
+
+struct vo;
+
+bool vo_android_init(struct vo *vo);
+void vo_android_uninit(struct vo *vo);
+ANativeWindow *vo_android_native_window(struct vo *vo);
+bool vo_android_surface_size(struct vo *vo, int *w, int *h);
diff --git a/video/out/aspect.c b/video/out/aspect.c
new file mode 100644
index 0000000..6e1cd63
--- /dev/null
+++ b/video/out/aspect.c
@@ -0,0 +1,216 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Stuff for correct aspect scaling. */
+#include "aspect.h"
+#include "math.h"
+#include "vo.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "video/mp_image.h"
+
+#include "vo.h"
+#include "sub/osd.h"
+
+static void aspect_calc_panscan(struct mp_vo_opts *opts,
+ int w, int h, int d_w, int d_h, int unscaled,
+ int window_w, int window_h, double monitor_par,
+ int *out_w, int *out_h)
+{
+ int fwidth = window_w;
+ int fheight = (float)window_w / d_w * d_h / monitor_par;
+ if (fheight > window_h || fheight < h) {
+ int tmpw = (float)window_h / d_h * d_w * monitor_par;
+ if (tmpw <= window_w) {
+ fheight = window_h;
+ fwidth = tmpw;
+ }
+ }
+
+ int vo_panscan_area = window_h - fheight;
+ double f_w = fwidth / (double)MPMAX(fheight, 1);
+ double f_h = 1;
+ if (vo_panscan_area == 0) {
+ vo_panscan_area = window_w - fwidth;
+ f_w = 1;
+ f_h = fheight / (double)MPMAX(fwidth, 1);
+ }
+
+ if (unscaled) {
+ vo_panscan_area = 0;
+ if (unscaled != 2 || (d_w <= window_w && d_h <= window_h)) {
+ fwidth = d_w * monitor_par;
+ fheight = d_h;
+ }
+ }
+
+ *out_w = fwidth + vo_panscan_area * opts->panscan * f_w;
+ *out_h = fheight + vo_panscan_area * opts->panscan * f_h;
+}
+
+// Clamp [start, end) to range [0, size) with various fallbacks.
+static void clamp_size(int size, int *start, int *end)
+{
+ *start = MPMAX(0, *start);
+ *end = MPMIN(size, *end);
+ if (*start >= *end) {
+ *start = 0;
+ *end = 1;
+ }
+}
+
+static void src_dst_split_scaling(int src_size, int dst_size,
+ int scaled_src_size,
+ float zoom, float align, float pan, float scale,
+ int *src_start, int *src_end,
+ int *dst_start, int *dst_end,
+ int *osd_margin_a, int *osd_margin_b)
+{
+ scaled_src_size *= powf(2, zoom) * scale;
+ scaled_src_size = MPMAX(scaled_src_size, 1);
+ align = (align + 1) / 2;
+
+ *dst_start = (dst_size - scaled_src_size) * align + pan * scaled_src_size;
+ *dst_end = *dst_start + scaled_src_size;
+
+ // Distance of screen frame to video
+ *osd_margin_a = *dst_start;
+ *osd_margin_b = dst_size - *dst_end;
+
+ // Clip to screen
+ int s_src = *src_end - *src_start;
+ int s_dst = *dst_end - *dst_start;
+ if (*dst_start < 0) {
+ int border = -(*dst_start) * s_src / s_dst;
+ *src_start += border;
+ *dst_start = 0;
+ }
+ if (*dst_end > dst_size) {
+ int border = (*dst_end - dst_size) * s_src / s_dst;
+ *src_end -= border;
+ *dst_end = dst_size;
+ }
+
+ // For sanity: avoid bothering VOs with corner cases
+ clamp_size(src_size, src_start, src_end);
+ clamp_size(dst_size, dst_start, dst_end);
+}
+
+static void calc_margin(float opts[2], int out[2], int size)
+{
+ out[0] = MPCLAMP((int)(opts[0] * size), 0, size);
+ out[1] = MPCLAMP((int)(opts[1] * size), 0, size);
+
+ if (out[0] + out[1] >= size) {
+ // This case is not really supported. Show an error by 1 pixel.
+ out[0] = 0;
+ out[1] = MPMAX(0, size - 1);
+ }
+}
+
+void mp_get_src_dst_rects(struct mp_log *log, struct mp_vo_opts *opts,
+ int vo_caps, struct mp_image_params *video,
+ int window_w, int window_h, double monitor_par,
+ struct mp_rect *out_src,
+ struct mp_rect *out_dst,
+ struct mp_osd_res *out_osd)
+{
+ int src_w = video->w;
+ int src_h = video->h;
+ int src_dw, src_dh;
+
+ mp_image_params_get_dsize(video, &src_dw, &src_dh);
+ window_w = MPMAX(1, window_w);
+ window_h = MPMAX(1, window_h);
+
+ int margin_x[2] = {0};
+ int margin_y[2] = {0};
+ if (opts->keepaspect) {
+ calc_margin(opts->margin_x, margin_x, window_w);
+ calc_margin(opts->margin_y, margin_y, window_h);
+ }
+
+ int vid_window_w = window_w - margin_x[0] - margin_x[1];
+ int vid_window_h = window_h - margin_y[0] - margin_y[1];
+
+ struct mp_rect dst = {0, 0, window_w, window_h};
+ struct mp_rect src = {0, 0, src_w, src_h};
+ if (mp_image_crop_valid(video))
+ src = video->crop;
+
+ if (vo_caps & VO_CAP_ROTATE90) {
+ if (video->rotate % 180 == 90) {
+ MPSWAP(int, src_w, src_h);
+ MPSWAP(int, src_dw, src_dh);
+ }
+ mp_rect_rotate(&src, src_w, src_h, video->rotate);
+ }
+
+ struct mp_osd_res osd = {
+ .w = window_w,
+ .h = window_h,
+ .display_par = monitor_par,
+ };
+
+ if (opts->keepaspect) {
+ int scaled_width, scaled_height;
+ aspect_calc_panscan(opts, src_w, src_h, src_dw, src_dh, opts->unscaled,
+ vid_window_w, vid_window_h, monitor_par,
+ &scaled_width, &scaled_height);
+ src_dst_split_scaling(src_w, vid_window_w, scaled_width,
+ opts->zoom, opts->align_x, opts->pan_x, opts->scale_x,
+ &src.x0, &src.x1, &dst.x0, &dst.x1,
+ &osd.ml, &osd.mr);
+ src_dst_split_scaling(src_h, vid_window_h, scaled_height,
+ opts->zoom, opts->align_y, opts->pan_y, opts->scale_y,
+ &src.y0, &src.y1, &dst.y0, &dst.y1,
+ &osd.mt, &osd.mb);
+ }
+
+ dst.x0 += margin_x[0];
+ dst.y0 += margin_y[0];
+ dst.x1 += margin_x[0];
+ dst.y1 += margin_y[0];
+
+ // OSD really uses the full window, but was computed on the margin-cut
+ // video sub-window. Correct it to the full window.
+ osd.ml += margin_x[0];
+ osd.mr += margin_x[1];
+ osd.mt += margin_y[0];
+ osd.mb += margin_y[1];
+
+ *out_src = src;
+ *out_dst = dst;
+ *out_osd = osd;
+
+ int sw = src.x1 - src.x0, sh = src.y1 - src.y0;
+ int dw = dst.x1 - dst.x0, dh = dst.y1 - dst.y0;
+
+ mp_verbose(log, "Window size: %dx%d (Borders: l=%d t=%d r=%d b=%d)\n",
+ window_w, window_h,
+ margin_x[0], margin_y[0], margin_x[1], margin_y[1]);
+ mp_verbose(log, "Video source: %dx%d (%d:%d)\n",
+ video->w, video->h, video->p_w, video->p_h);
+ mp_verbose(log, "Video display: (%d, %d) %dx%d -> (%d, %d) %dx%d\n",
+ src.x0, src.y0, sw, sh, dst.x0, dst.y0, dw, dh);
+ mp_verbose(log, "Video scale: %f/%f\n",
+ (double)dw / sw, (double)dh / sh);
+ mp_verbose(log, "OSD borders: l=%d t=%d r=%d b=%d\n",
+ osd.ml, osd.mt, osd.mr, osd.mb);
+ mp_verbose(log, "Video borders: l=%d t=%d r=%d b=%d\n",
+ dst.x0, dst.y0, window_w - dst.x1, window_h - dst.y1);
+}
diff --git a/video/out/aspect.h b/video/out/aspect.h
new file mode 100644
index 0000000..4123311
--- /dev/null
+++ b/video/out/aspect.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_ASPECT_H
+#define MPLAYER_ASPECT_H
+
+struct mp_log;
+struct mp_vo_opts;
+struct mp_image_params;
+struct mp_rect;
+struct mp_osd_res;
+void mp_get_src_dst_rects(struct mp_log *log, struct mp_vo_opts *opts,
+ int vo_caps, struct mp_image_params *video,
+ int window_w, int window_h, double monitor_par,
+ struct mp_rect *out_src,
+ struct mp_rect *out_dst,
+ struct mp_osd_res *out_osd);
+
+#endif /* MPLAYER_ASPECT_H */
diff --git a/video/out/bitmap_packer.c b/video/out/bitmap_packer.c
new file mode 100644
index 0000000..5ef090b
--- /dev/null
+++ b/video/out/bitmap_packer.c
@@ -0,0 +1,197 @@
+/*
+ * Calculate how to pack bitmap rectangles into a larger surface
+ *
+ * Copyright 2009, 2012 Uoti Urpala
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include "mpv_talloc.h"
+#include "bitmap_packer.h"
+#include "common/common.h"
+
+#define IS_POWER_OF_2(x) (((x) > 0) && !(((x) - 1) & (x)))
+
+void packer_reset(struct bitmap_packer *packer)
+{
+ struct bitmap_packer old = *packer;
+ *packer = (struct bitmap_packer) {
+ .w_max = old.w_max,
+ .h_max = old.h_max,
+ };
+ talloc_free_children(packer);
+}
+
+void packer_get_bb(struct bitmap_packer *packer, struct pos out_bb[2])
+{
+ out_bb[0] = (struct pos) {0};
+ out_bb[1] = (struct pos) {packer->used_width, packer->used_height};
+}
+
+#define HEIGHT_SORT_BITS 4
+static int size_index(int s)
+{
+ int n = mp_log2(s);
+ return (n << HEIGHT_SORT_BITS)
+ + ((- 1 - (s << HEIGHT_SORT_BITS >> n)) & ((1 << HEIGHT_SORT_BITS) - 1));
+}
+
+/* Pack the given rectangles into an area of size w * h.
+ * The size of each rectangle is read from in[i].x / in[i].y.
+ * The height of each rectangle must be less than 65536.
+ * 'scratch' must point to work memory for num_rects+16 ints.
+ * The packed position for rectangle number i is set in out[i].
+ * Return 0 on success, -1 if the rectangles did not fit in w*h.
+ *
+ * The rectangles are placed in rows in order approximately sorted by
+ * height (the approximate sorting is simpler than a full one would be,
+ * and allows the algorithm to work in linear time). Additionally, to
+ * reduce wasted space when there are a few tall rectangles, empty
+ * lower-right parts of rows are filled recursively when the size of
+ * rectangles in the row drops past a power-of-two threshold. So if a
+ * row starts with rectangles of size 3x50, 10x40 and 5x20 then the
+ * free rectangle with corners (13, 20)-(w, 50) is filled recursively.
+ */
+static int pack_rectangles(struct pos *in, struct pos *out, int num_rects,
+ int w, int h, int *scratch, int *used_width)
+{
+ int bins[16 << HEIGHT_SORT_BITS];
+ int sizes[16 << HEIGHT_SORT_BITS] = { 0 };
+ for (int i = 0; i < num_rects; i++)
+ sizes[size_index(in[i].y)]++;
+ int idx = 0;
+ for (int i = 0; i < 16 << HEIGHT_SORT_BITS; i += 1 << HEIGHT_SORT_BITS) {
+ for (int j = 0; j < 1 << HEIGHT_SORT_BITS; j++) {
+ bins[i + j] = idx;
+ idx += sizes[i + j];
+ }
+ scratch[idx++] = -1;
+ }
+ for (int i = 0; i < num_rects; i++)
+ scratch[bins[size_index(in[i].y)]++] = i;
+ for (int i = 0; i < 16; i++)
+ bins[i] = bins[i << HEIGHT_SORT_BITS] - sizes[i << HEIGHT_SORT_BITS];
+ struct {
+ int size, x, bottom;
+ } stack[16] = {{15, 0, h}}, s = {0};
+ int stackpos = 1;
+ int y;
+ while (stackpos) {
+ y = s.bottom;
+ s = stack[--stackpos];
+ s.size++;
+ while (s.size--) {
+ int maxy = -1;
+ int obj;
+ while ((obj = scratch[bins[s.size]]) >= 0) {
+ int bottom = y + in[obj].y;
+ if (bottom > s.bottom)
+ break;
+ int right = s.x + in[obj].x;
+ if (right > w)
+ break;
+ bins[s.size]++;
+ out[obj] = (struct pos){s.x, y};
+ num_rects--;
+ if (maxy < 0)
+ stack[stackpos++] = s;
+ s.x = right;
+ maxy = MPMAX(maxy, bottom);
+ }
+ *used_width = MPMAX(*used_width, s.x);
+ if (maxy > 0)
+ s.bottom = maxy;
+ }
+ }
+ return num_rects ? -1 : y;
+}
+
+int packer_pack(struct bitmap_packer *packer)
+{
+ if (packer->count == 0)
+ return 0;
+ int w_orig = packer->w, h_orig = packer->h;
+ struct pos *in = packer->in;
+ int xmax = 0, ymax = 0;
+ for (int i = 0; i < packer->count; i++) {
+ if (in[i].x <= 0 || in[i].y <= 0) {
+ in[i] = (struct pos){0, 0};
+ } else {
+ in[i].x += packer->padding * 2;
+ in[i].y += packer->padding * 2;
+ }
+ if (in[i].x < 0 || in [i].x > 65535 || in[i].y < 0 || in[i].y > 65535) {
+ fprintf(stderr, "Invalid OSD / subtitle bitmap size\n");
+ abort();
+ }
+ xmax = MPMAX(xmax, in[i].x);
+ ymax = MPMAX(ymax, in[i].y);
+ }
+ if (xmax > packer->w)
+ packer->w = 1 << (mp_log2(xmax - 1) + 1);
+ if (ymax > packer->h)
+ packer->h = 1 << (mp_log2(ymax - 1) + 1);
+ while (1) {
+ int used_width = 0;
+ int y = pack_rectangles(in, packer->result, packer->count,
+ packer->w, packer->h,
+ packer->scratch, &used_width);
+ if (y >= 0) {
+ packer->used_width = MPMIN(used_width, packer->w);
+ packer->used_height = MPMIN(y, packer->h);
+ assert(packer->w == 0 || IS_POWER_OF_2(packer->w));
+ assert(packer->h == 0 || IS_POWER_OF_2(packer->h));
+ if (packer->padding) {
+ for (int i = 0; i < packer->count; i++) {
+ packer->result[i].x += packer->padding;
+ packer->result[i].y += packer->padding;
+ }
+ }
+ return packer->w != w_orig || packer->h != h_orig;
+ }
+ int w_max = packer->w_max > 0 ? packer->w_max : INT_MAX;
+ int h_max = packer->h_max > 0 ? packer->h_max : INT_MAX;
+ if (packer->w <= packer->h && packer->w != w_max)
+ packer->w = MPMIN(packer->w * 2, w_max);
+ else if (packer->h != h_max)
+ packer->h = MPMIN(packer->h * 2, h_max);
+ else {
+ packer->w = w_orig;
+ packer->h = h_orig;
+ return -1;
+ }
+ }
+}
+
+void packer_set_size(struct bitmap_packer *packer, int size)
+{
+ packer->count = size;
+ if (size <= packer->asize)
+ return;
+ packer->asize = MPMAX(packer->asize * 2, size);
+ talloc_free(packer->result);
+ talloc_free(packer->scratch);
+ packer->in = talloc_realloc(packer, packer->in, struct pos, packer->asize);
+ packer->result = talloc_array_ptrtype(packer, packer->result,
+ packer->asize);
+ packer->scratch = talloc_array_ptrtype(packer, packer->scratch,
+ packer->asize + 16);
+}
diff --git a/video/out/bitmap_packer.h b/video/out/bitmap_packer.h
new file mode 100644
index 0000000..97bf88f
--- /dev/null
+++ b/video/out/bitmap_packer.h
@@ -0,0 +1,51 @@
+#ifndef MPLAYER_PACK_RECTANGLES_H
+#define MPLAYER_PACK_RECTANGLES_H
+
+struct pos {
+ int x;
+ int y;
+};
+
+struct bitmap_packer {
+ int w;
+ int h;
+ int w_max;
+ int h_max;
+ int padding;
+ int count;
+ struct pos *in;
+ struct pos *result;
+ int used_width;
+ int used_height;
+
+ // internal
+ int *scratch;
+ int asize;
+};
+
+struct sub_bitmaps;
+
+// Clear all internal state. Leave the following fields: w_max, h_max
+void packer_reset(struct bitmap_packer *packer);
+
+// Get the bounding box used for bitmap data (including padding).
+// The bounding box doesn't exceed (0,0)-(packer->w,packer->h).
+void packer_get_bb(struct bitmap_packer *packer, struct pos out_bb[2]);
+
+/* Reallocate packer->in for at least to desired number of items.
+ * Also sets packer->count to the same value.
+ */
+void packer_set_size(struct bitmap_packer *packer, int size);
+
+/* To use this, set packer->count to number of rectangles, w_max and h_max
+ * to maximum output rectangle size, and w and h to start size (may be 0).
+ * Write input sizes in packer->in.
+ * Resulting packing will be written in packer->result.
+ * w and h will be increased if necessary for successful packing.
+ * There is a strong guarantee that w and h will be powers of 2 (or set to 0).
+ * Return value is -1 if packing failed because w and h were set to max
+ * values but that wasn't enough, 1 if w or h was increased, and 0 otherwise.
+ */
+int packer_pack(struct bitmap_packer *packer);
+
+#endif
diff --git a/video/out/cocoa_cb_common.swift b/video/out/cocoa_cb_common.swift
new file mode 100644
index 0000000..9c0054a
--- /dev/null
+++ b/video/out/cocoa_cb_common.swift
@@ -0,0 +1,230 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class CocoaCB: Common {
+ var libmpv: LibmpvHelper
+ var layer: GLLayer?
+
+ @objc var isShuttingDown: Bool = false
+
+ enum State {
+ case uninitialized
+ case needsInit
+ case initialized
+ }
+ var backendState: State = .uninitialized
+
+
+ @objc init(_ mpvHandle: OpaquePointer) {
+ let newlog = mp_log_new(UnsafeMutablePointer<MPContext>(mpvHandle), mp_client_get_log(mpvHandle), "cocoacb")
+ libmpv = LibmpvHelper(mpvHandle, newlog)
+ super.init(newlog)
+ layer = GLLayer(cocoaCB: self)
+ }
+
+ func preinit(_ vo: UnsafeMutablePointer<vo>) {
+ mpv = MPVHelper(vo, log)
+
+ if backendState == .uninitialized {
+ backendState = .needsInit
+
+ guard let layer = self.layer else {
+ log.sendError("Something went wrong, no GLLayer was initialized")
+ exit(1)
+ }
+
+ initView(vo, layer)
+ initMisc(vo)
+ }
+ }
+
+ func uninit() {
+ window?.orderOut(nil)
+ window?.close()
+ mpv = nil
+ }
+
+ func reconfig(_ vo: UnsafeMutablePointer<vo>) {
+ mpv?.vo = vo
+ if backendState == .needsInit {
+ DispatchQueue.main.sync { self.initBackend(vo) }
+ } else {
+ DispatchQueue.main.async {
+ self.updateWindowSize(vo)
+ self.layer?.update(force: true)
+ }
+ }
+ }
+
+ func initBackend(_ vo: UnsafeMutablePointer<vo>) {
+ let previousActiveApp = getActiveApp()
+ initApp()
+ initWindow(vo, previousActiveApp)
+ updateICCProfile()
+ initWindowState()
+
+ backendState = .initialized
+ }
+
+ func updateWindowSize(_ vo: UnsafeMutablePointer<vo>) {
+ guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else
+ {
+ log.sendWarning("Couldn't update Window size, no Screen available")
+ return
+ }
+
+ let wr = getWindowGeometry(forScreen: targetScreen, videoOut: vo)
+ if !(window?.isVisible ?? false) &&
+ !(window?.isMiniaturized ?? false) &&
+ !NSApp.isHidden
+ {
+ window?.makeKeyAndOrderFront(nil)
+ }
+ layer?.atomicDrawingStart()
+ window?.updateSize(wr.size)
+ }
+
+ override func displayLinkCallback(_ displayLink: CVDisplayLink,
+ _ inNow: UnsafePointer<CVTimeStamp>,
+ _ inOutputTime: UnsafePointer<CVTimeStamp>,
+ _ flagsIn: CVOptionFlags,
+ _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
+ {
+ libmpv.reportRenderFlip()
+ return kCVReturnSuccess
+ }
+
+ override func lightSensorUpdate() {
+ libmpv.setRenderLux(lmuToLux(lastLmu))
+ }
+
+ override func updateICCProfile() {
+ guard let colorSpace = window?.screen?.colorSpace else {
+ log.sendWarning("Couldn't update ICC Profile, no color space available")
+ return
+ }
+
+ libmpv.setRenderICCProfile(colorSpace)
+ layer?.colorspace = colorSpace.cgColorSpace
+ }
+
+ override func windowDidEndAnimation() {
+ layer?.update()
+ checkShutdown()
+ }
+
+ override func windowSetToFullScreen() {
+ layer?.update(force: true)
+ }
+
+ override func windowSetToWindow() {
+ layer?.update(force: true)
+ }
+
+ override func windowDidUpdateFrame() {
+ layer?.update(force: true)
+ }
+
+ override func windowDidChangeScreen() {
+ layer?.update(force: true)
+ }
+
+ override func windowDidChangeScreenProfile() {
+ layer?.needsICCUpdate = true
+ }
+
+ override func windowDidChangeBackingProperties() {
+ layer?.contentsScale = window?.backingScaleFactor ?? 1
+ }
+
+ override func windowWillStartLiveResize() {
+ layer?.inLiveResize = true
+ }
+
+ override func windowDidEndLiveResize() {
+ layer?.inLiveResize = false
+ }
+
+ override func windowDidChangeOcclusionState() {
+ layer?.update(force: true)
+ }
+
+ var controlCallback: mp_render_cb_control_fn = { ( v, ctx, e, request, data ) -> Int32 in
+ let ccb = unsafeBitCast(ctx, to: CocoaCB.self)
+
+ guard let vo = v, let events = e else {
+ ccb.log.sendWarning("Unexpected nil value in Control Callback")
+ return VO_FALSE
+ }
+
+ return ccb.control(vo, events: events, request: request, data: data)
+ }
+
+ override func control(_ vo: UnsafeMutablePointer<vo>,
+ events: UnsafeMutablePointer<Int32>,
+ request: UInt32,
+ data: UnsafeMutableRawPointer?) -> Int32
+ {
+ switch mp_voctrl(request) {
+ case VOCTRL_PREINIT:
+ DispatchQueue.main.sync { self.preinit(vo) }
+ return VO_TRUE
+ case VOCTRL_UNINIT:
+ DispatchQueue.main.async { self.uninit() }
+ return VO_TRUE
+ case VOCTRL_RECONFIG:
+ reconfig(vo)
+ return VO_TRUE
+ default:
+ break
+ }
+
+ return super.control(vo, events: events, request: request, data: data)
+ }
+
+ func shutdown(_ destroy: Bool = false) {
+ isShuttingDown = window?.isAnimating ?? false ||
+ window?.isInFullscreen ?? false && mpv?.opts.native_fs ?? true
+ if window?.isInFullscreen ?? false && !(window?.isAnimating ?? false) {
+ window?.close()
+ }
+ if isShuttingDown { return }
+
+ uninit()
+ uninitCommon()
+
+ libmpv.deinitRender()
+ libmpv.deinitMPV(destroy)
+ }
+
+ func checkShutdown() {
+ if isShuttingDown {
+ shutdown(true)
+ }
+ }
+
+ @objc func processEvent(_ event: UnsafePointer<mpv_event>) {
+ switch event.pointee.event_id {
+ case MPV_EVENT_SHUTDOWN:
+ shutdown()
+ default:
+ break
+ }
+ }
+}
diff --git a/video/out/d3d11/context.c b/video/out/d3d11/context.c
new file mode 100644
index 0000000..05f04fd
--- /dev/null
+++ b/video/out/d3d11/context.c
@@ -0,0 +1,566 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+
+#include "video/out/gpu/context.h"
+#include "video/out/gpu/d3d11_helpers.h"
+#include "video/out/gpu/spirv.h"
+#include "video/out/w32_common.h"
+#include "context.h"
+#include "ra_d3d11.h"
+
+static int d3d11_validate_adapter(struct mp_log *log,
+ const struct m_option *opt,
+ struct bstr name, const char **value);
+
+struct d3d11_opts {
+ int feature_level;
+ int warp;
+ bool flip;
+ int sync_interval;
+ char *adapter_name;
+ int output_format;
+ int color_space;
+ bool exclusive_fs;
+};
+
+#define OPT_BASE_STRUCT struct d3d11_opts
+const struct m_sub_options d3d11_conf = {
+ .opts = (const struct m_option[]) {
+ {"d3d11-warp", OPT_CHOICE(warp,
+ {"auto", -1},
+ {"no", 0},
+ {"yes", 1})},
+ {"d3d11-feature-level", OPT_CHOICE(feature_level,
+ {"12_1", D3D_FEATURE_LEVEL_12_1},
+ {"12_0", D3D_FEATURE_LEVEL_12_0},
+ {"11_1", D3D_FEATURE_LEVEL_11_1},
+ {"11_0", D3D_FEATURE_LEVEL_11_0},
+ {"10_1", D3D_FEATURE_LEVEL_10_1},
+ {"10_0", D3D_FEATURE_LEVEL_10_0},
+ {"9_3", D3D_FEATURE_LEVEL_9_3},
+ {"9_2", D3D_FEATURE_LEVEL_9_2},
+ {"9_1", D3D_FEATURE_LEVEL_9_1})},
+ {"d3d11-flip", OPT_BOOL(flip)},
+ {"d3d11-sync-interval", OPT_INT(sync_interval), M_RANGE(0, 4)},
+ {"d3d11-adapter", OPT_STRING_VALIDATE(adapter_name,
+ d3d11_validate_adapter)},
+ {"d3d11-output-format", OPT_CHOICE(output_format,
+ {"auto", DXGI_FORMAT_UNKNOWN},
+ {"rgba8", DXGI_FORMAT_R8G8B8A8_UNORM},
+ {"bgra8", DXGI_FORMAT_B8G8R8A8_UNORM},
+ {"rgb10_a2", DXGI_FORMAT_R10G10B10A2_UNORM},
+ {"rgba16f", DXGI_FORMAT_R16G16B16A16_FLOAT})},
+ {"d3d11-output-csp", OPT_CHOICE(color_space,
+ {"auto", -1},
+ {"srgb", DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709},
+ {"linear", DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709},
+ {"pq", DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020},
+ {"bt.2020", DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020})},
+ {"d3d11-exclusive-fs", OPT_BOOL(exclusive_fs)},
+ {0}
+ },
+ .defaults = &(const struct d3d11_opts) {
+ .feature_level = D3D_FEATURE_LEVEL_12_1,
+ .warp = -1,
+ .flip = true,
+ .sync_interval = 1,
+ .adapter_name = NULL,
+ .output_format = DXGI_FORMAT_UNKNOWN,
+ .color_space = -1,
+ },
+ .size = sizeof(struct d3d11_opts)
+};
+
+struct priv {
+ struct d3d11_opts *opts;
+ struct m_config_cache *opts_cache;
+
+ struct mp_vo_opts *vo_opts;
+ struct m_config_cache *vo_opts_cache;
+
+ struct ra_tex *backbuffer;
+ ID3D11Device *device;
+ IDXGISwapChain *swapchain;
+ struct mp_colorspace swapchain_csp;
+
+ int64_t perf_freq;
+ unsigned sync_refresh_count;
+ int64_t sync_qpc_time;
+ int64_t vsync_duration_qpc;
+ int64_t last_submit_qpc;
+};
+
+static int d3d11_validate_adapter(struct mp_log *log,
+ const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ bool help = bstr_equals0(param, "help");
+ bool adapter_matched = false;
+ struct bstr listing = { 0 };
+
+ if (bstr_equals0(param, "")) {
+ return 0;
+ }
+
+ adapter_matched = mp_d3d11_list_or_verify_adapters(log,
+ help ? bstr0(NULL) : param,
+ help ? &listing : NULL);
+
+ if (help) {
+ mp_info(log, "Available D3D11 adapters:\n%.*s",
+ BSTR_P(listing));
+ talloc_free(listing.start);
+ return M_OPT_EXIT;
+ }
+
+ if (!adapter_matched) {
+ mp_err(log, "No adapter matching '%.*s'!\n", BSTR_P(param));
+ }
+
+ return adapter_matched ? 0 : M_OPT_INVALID;
+}
+
+static struct ra_tex *get_backbuffer(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ID3D11Texture2D *backbuffer = NULL;
+ struct ra_tex *tex = NULL;
+ HRESULT hr;
+
+ hr = IDXGISwapChain_GetBuffer(p->swapchain, 0, &IID_ID3D11Texture2D,
+ (void**)&backbuffer);
+ if (FAILED(hr)) {
+ MP_ERR(ctx, "Couldn't get swapchain image\n");
+ goto done;
+ }
+
+ tex = ra_d3d11_wrap_tex(ctx->ra, (ID3D11Resource *)backbuffer);
+done:
+ SAFE_RELEASE(backbuffer);
+ return tex;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HRESULT hr;
+
+ if (p->backbuffer) {
+ MP_ERR(ctx, "Attempt at resizing while a frame was in progress!\n");
+ return false;
+ }
+
+ hr = IDXGISwapChain_ResizeBuffers(p->swapchain, 0, ctx->vo->dwidth,
+ ctx->vo->dheight, DXGI_FORMAT_UNKNOWN, 0);
+ if (FAILED(hr)) {
+ MP_FATAL(ctx, "Couldn't resize swapchain: %s\n", mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ return true;
+}
+
+static bool d3d11_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ return resize(ctx);
+}
+
+static int d3d11_color_depth(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ DXGI_SWAP_CHAIN_DESC desc;
+
+ HRESULT hr = IDXGISwapChain_GetDesc(p->swapchain, &desc);
+ if (FAILED(hr)) {
+ MP_ERR(sw->ctx, "Failed to query swap chain description: %s!\n",
+ mp_HRESULT_to_str(hr));
+ return 0;
+ }
+
+ const struct ra_format *ra_fmt =
+ ra_d3d11_get_ra_format(sw->ctx->ra, desc.BufferDesc.Format);
+ if (!ra_fmt)
+ return 0;
+
+ return ra_fmt->component_depth[0];
+}
+
+static bool d3d11_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct priv *p = sw->priv;
+
+ if (!out_fbo)
+ return true;
+
+ assert(!p->backbuffer);
+
+ p->backbuffer = get_backbuffer(sw->ctx);
+ if (!p->backbuffer)
+ return false;
+
+ *out_fbo = (struct ra_fbo) {
+ .tex = p->backbuffer,
+ .flip = false,
+ .color_space = p->swapchain_csp
+ };
+ return true;
+}
+
+static bool d3d11_submit_frame(struct ra_swapchain *sw,
+ const struct vo_frame *frame)
+{
+ struct priv *p = sw->priv;
+
+ ra_d3d11_flush(sw->ctx->ra);
+ ra_tex_free(sw->ctx->ra, &p->backbuffer);
+ return true;
+}
+
+static int64_t qpc_to_ns(struct ra_swapchain *sw, int64_t qpc)
+{
+ struct priv *p = sw->priv;
+
+ // Convert QPC units (1/perf_freq seconds) to nanoseconds. This will work
+ // without overflow because the QPC value is guaranteed not to roll-over
+ // within 100 years, so perf_freq must be less than 2.9*10^9.
+ return qpc / p->perf_freq * INT64_C(1000000000) +
+ qpc % p->perf_freq * INT64_C(1000000000) / p->perf_freq;
+}
+
+static int64_t qpc_ns_now(struct ra_swapchain *sw)
+{
+ LARGE_INTEGER perf_count;
+ QueryPerformanceCounter(&perf_count);
+ return qpc_to_ns(sw, perf_count.QuadPart);
+}
+
+static void d3d11_swap_buffers(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+
+ m_config_cache_update(p->opts_cache);
+
+ LARGE_INTEGER perf_count;
+ QueryPerformanceCounter(&perf_count);
+ p->last_submit_qpc = perf_count.QuadPart;
+
+ IDXGISwapChain_Present(p->swapchain, p->opts->sync_interval, 0);
+}
+
+static void d3d11_get_vsync(struct ra_swapchain *sw, struct vo_vsync_info *info)
+{
+ struct priv *p = sw->priv;
+ HRESULT hr;
+
+ m_config_cache_update(p->opts_cache);
+
+ // The calculations below are only valid if mpv presents on every vsync
+ if (p->opts->sync_interval != 1)
+ return;
+
+ // They're also only valid for flip model swapchains
+ DXGI_SWAP_CHAIN_DESC desc;
+ hr = IDXGISwapChain_GetDesc(p->swapchain, &desc);
+ if (FAILED(hr) || (desc.SwapEffect != DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL &&
+ desc.SwapEffect != DXGI_SWAP_EFFECT_FLIP_DISCARD))
+ {
+ return;
+ }
+
+ // GetLastPresentCount returns a sequential ID for the frame submitted by
+ // the last call to IDXGISwapChain::Present()
+ UINT submit_count;
+ hr = IDXGISwapChain_GetLastPresentCount(p->swapchain, &submit_count);
+ if (FAILED(hr))
+ return;
+
+ // GetFrameStatistics returns two pairs. The first is (PresentCount,
+ // PresentRefreshCount) which relates a present ID (on the same timeline as
+ // GetLastPresentCount) to the physical vsync it was displayed on. The
+ // second is (SyncRefreshCount, SyncQPCTime), which relates a physical vsync
+ // to a timestamp on the same clock as QueryPerformanceCounter.
+ DXGI_FRAME_STATISTICS stats;
+ hr = IDXGISwapChain_GetFrameStatistics(p->swapchain, &stats);
+ if (hr == DXGI_ERROR_FRAME_STATISTICS_DISJOINT) {
+ p->sync_refresh_count = 0;
+ p->sync_qpc_time = 0;
+ }
+ if (FAILED(hr))
+ return;
+
+ info->last_queue_display_time = 0;
+ info->vsync_duration = 0;
+ // Detecting skipped vsyncs is possible but not supported yet
+ info->skipped_vsyncs = -1;
+
+ // Get the number of physical vsyncs that have passed since the start of the
+ // playback or disjoint event.
+ // Check for 0 here, since sometimes GetFrameStatistics returns S_OK but
+ // with 0s in some (all?) members of DXGI_FRAME_STATISTICS.
+ unsigned src_passed = 0;
+ if (stats.SyncRefreshCount && p->sync_refresh_count)
+ src_passed = stats.SyncRefreshCount - p->sync_refresh_count;
+ if (p->sync_refresh_count == 0)
+ p->sync_refresh_count = stats.SyncRefreshCount;
+
+ // Get the elapsed time passed between the above vsyncs
+ unsigned sqt_passed = 0;
+ if (stats.SyncQPCTime.QuadPart && p->sync_qpc_time)
+ sqt_passed = stats.SyncQPCTime.QuadPart - p->sync_qpc_time;
+ if (p->sync_qpc_time == 0)
+ p->sync_qpc_time = stats.SyncQPCTime.QuadPart;
+
+ // If any vsyncs have passed, estimate the physical frame rate
+ if (src_passed && sqt_passed)
+ p->vsync_duration_qpc = sqt_passed / src_passed;
+ if (p->vsync_duration_qpc)
+ info->vsync_duration = qpc_to_ns(sw, p->vsync_duration_qpc);
+
+ // If the physical frame rate is known and the other members of
+ // DXGI_FRAME_STATISTICS are non-0, estimate the timing of the next frame
+ if (p->vsync_duration_qpc && stats.PresentCount &&
+ stats.PresentRefreshCount && stats.SyncRefreshCount &&
+ stats.SyncQPCTime.QuadPart)
+ {
+ // It's not clear if PresentRefreshCount and SyncRefreshCount can refer
+ // to different frames, but in case they can, assuming mpv presents on
+ // every frame, guess the present count that relates to SyncRefreshCount.
+ unsigned expected_sync_pc = stats.PresentCount +
+ (stats.SyncRefreshCount - stats.PresentRefreshCount);
+
+ // Now guess the timestamp of the last submitted frame based on the
+ // timestamp of the frame at SyncRefreshCount and the frame rate
+ int queued_frames = submit_count - expected_sync_pc;
+ int64_t last_queue_display_time_qpc = stats.SyncQPCTime.QuadPart +
+ queued_frames * p->vsync_duration_qpc;
+
+ // Only set the estimated display time if it's after the last submission
+ // time. It could be before if mpv skips a lot of frames.
+ if (last_queue_display_time_qpc >= p->last_submit_qpc) {
+ info->last_queue_display_time = mp_time_ns() +
+ (qpc_to_ns(sw, last_queue_display_time_qpc) - qpc_ns_now(sw));
+ }
+ }
+}
+
+static bool d3d11_set_fullscreen(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HRESULT hr;
+
+ m_config_cache_update(p->opts_cache);
+
+ if (!p->swapchain) {
+ MP_ERR(ctx, "Full screen configuration was requested before D3D11 "
+ "swap chain was ready!");
+ return false;
+ }
+
+ // we only want exclusive FS if we are entering FS and
+ // exclusive FS is enabled. Otherwise disable exclusive FS.
+ bool enable_exclusive_fs = p->vo_opts->fullscreen &&
+ p->opts->exclusive_fs;
+
+ MP_VERBOSE(ctx, "%s full-screen exclusive mode while %s fullscreen\n",
+ enable_exclusive_fs ? "Enabling" : "Disabling",
+ ctx->vo->opts->fullscreen ? "entering" : "leaving");
+
+ hr = IDXGISwapChain_SetFullscreenState(p->swapchain,
+ enable_exclusive_fs, NULL);
+ if (FAILED(hr))
+ return false;
+
+ if (!resize(ctx))
+ return false;
+
+ return true;
+}
+
+static int d3d11_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ struct priv *p = ctx->priv;
+ int ret = -1;
+ bool fullscreen_switch_needed = false;
+
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *changed_option;
+
+ while (m_config_cache_get_next_changed(p->vo_opts_cache,
+ &changed_option))
+ {
+ struct mp_vo_opts *vo_opts = p->vo_opts_cache->opts;
+
+ if (changed_option == &vo_opts->fullscreen) {
+ fullscreen_switch_needed = true;
+ }
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ // if leaving full screen, handle d3d11 stuff first, then general
+ // windowing
+ if (fullscreen_switch_needed && !p->vo_opts->fullscreen) {
+ if (!d3d11_set_fullscreen(ctx))
+ return VO_FALSE;
+
+ fullscreen_switch_needed = false;
+ }
+
+ ret = vo_w32_control(ctx->vo, events, request, arg);
+
+ // if entering full screen, handle d3d11 after general windowing stuff
+ if (fullscreen_switch_needed && p->vo_opts->fullscreen) {
+ if (!d3d11_set_fullscreen(ctx))
+ return VO_FALSE;
+
+ fullscreen_switch_needed = false;
+ }
+
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+static void d3d11_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->swapchain)
+ IDXGISwapChain_SetFullscreenState(p->swapchain, FALSE, NULL);
+
+ if (ctx->ra)
+ ra_tex_free(ctx->ra, &p->backbuffer);
+ SAFE_RELEASE(p->swapchain);
+ vo_w32_uninit(ctx->vo);
+ SAFE_RELEASE(p->device);
+
+ // Destroy the RA last to prevent objects we hold from showing up in D3D's
+ // leak checker
+ if (ctx->ra)
+ ctx->ra->fns->destroy(ctx->ra);
+}
+
+static const struct ra_swapchain_fns d3d11_swapchain = {
+ .color_depth = d3d11_color_depth,
+ .start_frame = d3d11_start_frame,
+ .submit_frame = d3d11_submit_frame,
+ .swap_buffers = d3d11_swap_buffers,
+ .get_vsync = d3d11_get_vsync,
+};
+
+static bool d3d11_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ p->opts_cache = m_config_cache_alloc(ctx, ctx->global, &d3d11_conf);
+ p->opts = p->opts_cache->opts;
+
+ p->vo_opts_cache = m_config_cache_alloc(ctx, ctx->vo->global, &vo_sub_opts);
+ p->vo_opts = p->vo_opts_cache->opts;
+
+ LARGE_INTEGER perf_freq;
+ QueryPerformanceFrequency(&perf_freq);
+ p->perf_freq = perf_freq.QuadPart;
+
+ struct ra_swapchain *sw = ctx->swapchain = talloc_zero(ctx, struct ra_swapchain);
+ sw->priv = p;
+ sw->ctx = ctx;
+ sw->fns = &d3d11_swapchain;
+
+ struct d3d11_device_opts dopts = {
+ .debug = ctx->opts.debug,
+ .allow_warp = p->opts->warp != 0,
+ .force_warp = p->opts->warp == 1,
+ .max_feature_level = p->opts->feature_level,
+ .max_frame_latency = ctx->vo->opts->swapchain_depth,
+ .adapter_name = p->opts->adapter_name,
+ };
+ if (!mp_d3d11_create_present_device(ctx->log, &dopts, &p->device))
+ goto error;
+
+ if (!spirv_compiler_init(ctx))
+ goto error;
+ ctx->ra = ra_d3d11_create(p->device, ctx->log, ctx->spirv);
+ if (!ctx->ra)
+ goto error;
+
+ if (!vo_w32_init(ctx->vo))
+ goto error;
+
+ UINT usage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT;
+ if (ID3D11Device_GetFeatureLevel(p->device) >= D3D_FEATURE_LEVEL_11_0 &&
+ p->opts->output_format != DXGI_FORMAT_B8G8R8A8_UNORM)
+ {
+ usage |= DXGI_USAGE_UNORDERED_ACCESS;
+ }
+
+ struct d3d11_swapchain_opts scopts = {
+ .window = vo_w32_hwnd(ctx->vo),
+ .width = ctx->vo->dwidth,
+ .height = ctx->vo->dheight,
+ .format = p->opts->output_format,
+ .color_space = p->opts->color_space,
+ .configured_csp = &p->swapchain_csp,
+ .flip = p->opts->flip,
+ // Add one frame for the backbuffer and one frame of "slack" to reduce
+ // contention with the window manager when acquiring the backbuffer
+ .length = ctx->vo->opts->swapchain_depth + 2,
+ .usage = usage,
+ };
+ if (!mp_d3d11_create_swapchain(p->device, ctx->log, &scopts, &p->swapchain))
+ goto error;
+
+ return true;
+
+error:
+ d3d11_uninit(ctx);
+ return false;
+}
+
+IDXGISwapChain *ra_d3d11_ctx_get_swapchain(struct ra_ctx *ra)
+{
+ if (ra->swapchain->fns != &d3d11_swapchain)
+ return NULL;
+
+ struct priv *p = ra->priv;
+
+ IDXGISwapChain_AddRef(p->swapchain);
+
+ return p->swapchain;
+}
+
+const struct ra_ctx_fns ra_ctx_d3d11 = {
+ .type = "d3d11",
+ .name = "d3d11",
+ .reconfig = d3d11_reconfig,
+ .control = d3d11_control,
+ .init = d3d11_init,
+ .uninit = d3d11_uninit,
+};
diff --git a/video/out/d3d11/context.h b/video/out/d3d11/context.h
new file mode 100644
index 0000000..8a9ef4c
--- /dev/null
+++ b/video/out/d3d11/context.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <dxgi.h>
+
+#include "video/out/gpu/context.h"
+
+// Get the underlying D3D11 swap chain from an RA context. The returned swap chain is
+// refcounted and must be released by the caller.
+IDXGISwapChain *ra_d3d11_ctx_get_swapchain(struct ra_ctx *ra);
diff --git a/video/out/d3d11/hwdec_d3d11va.c b/video/out/d3d11/hwdec_d3d11va.c
new file mode 100644
index 0000000..6aaa12b
--- /dev/null
+++ b/video/out/d3d11/hwdec_d3d11va.c
@@ -0,0 +1,258 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <d3d11.h>
+#include <d3d11_1.h>
+
+#include "common/common.h"
+#include "options/m_config.h"
+#include "osdep/windows_utils.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+#include "video/out/d3d11/ra_d3d11.h"
+#include "video/out/gpu/hwdec.h"
+
+struct d3d11va_opts {
+ bool zero_copy;
+};
+
+#define OPT_BASE_STRUCT struct d3d11va_opts
+const struct m_sub_options d3d11va_conf = {
+ .opts = (const struct m_option[]) {
+ {"d3d11va-zero-copy", OPT_BOOL(zero_copy)},
+ {0}
+ },
+ .defaults = &(const struct d3d11va_opts) {0},
+ .size = sizeof(struct d3d11va_opts)
+};
+
+struct priv_owner {
+ struct d3d11va_opts *opts;
+
+ struct mp_hwdec_ctx hwctx;
+ ID3D11Device *device;
+ ID3D11Device1 *device1;
+};
+
+struct priv {
+ // 1-copy path
+ ID3D11DeviceContext1 *ctx;
+ ID3D11Texture2D *copy_tex;
+
+ // zero-copy path
+ int num_planes;
+ const struct ra_format *fmt[4];
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+ SAFE_RELEASE(p->device);
+ SAFE_RELEASE(p->device1);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ HRESULT hr;
+
+ if (!ra_is_d3d11(hw->ra_ctx->ra))
+ return -1;
+ p->device = ra_d3d11_get_device(hw->ra_ctx->ra);
+ if (!p->device)
+ return -1;
+
+ p->opts = mp_get_config_group(hw->priv, hw->global, &d3d11va_conf);
+
+ // D3D11VA requires Direct3D 11.1, so this should always succeed
+ hr = ID3D11Device_QueryInterface(p->device, &IID_ID3D11Device1,
+ (void**)&p->device1);
+ if (FAILED(hr)) {
+ MP_ERR(hw, "Failed to get D3D11.1 interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ ID3D10Multithread *multithread;
+ hr = ID3D11Device_QueryInterface(p->device, &IID_ID3D10Multithread,
+ (void **)&multithread);
+ if (FAILED(hr)) {
+ MP_ERR(hw, "Failed to get Multithread interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+ ID3D10Multithread_SetMultithreadProtected(multithread, TRUE);
+ ID3D10Multithread_Release(multithread);
+
+ static const int subfmts[] = {IMGFMT_NV12, IMGFMT_P010, 0};
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d11_wrap_device_ref(p->device),
+ .supported_formats = subfmts,
+ .hw_imgfmt = IMGFMT_D3D11,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ for (int i = 0; i < 4; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+ SAFE_RELEASE(p->copy_tex);
+ SAFE_RELEASE(p->ctx);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ struct ra_imgfmt_desc desc = {0};
+
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ if (o->opts->zero_copy) {
+ // In the zero-copy path, we create the ra_tex objects in the map
+ // operation, so we just need to store the format of each plane
+ p->num_planes = desc.num_planes;
+ for (int i = 0; i < desc.num_planes; i++)
+ p->fmt[i] = desc.planes[i];
+ } else {
+ struct mp_image layout = {0};
+ mp_image_set_params(&layout, &mapper->dst_params);
+
+ DXGI_FORMAT copy_fmt;
+ switch (mapper->dst_params.imgfmt) {
+ case IMGFMT_NV12: copy_fmt = DXGI_FORMAT_NV12; break;
+ case IMGFMT_P010: copy_fmt = DXGI_FORMAT_P010; break;
+ default: return -1;
+ }
+
+ D3D11_TEXTURE2D_DESC copy_desc = {
+ .Width = mapper->dst_params.w,
+ .Height = mapper->dst_params.h,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc.Count = 1,
+ .Format = copy_fmt,
+ .BindFlags = D3D11_BIND_SHADER_RESOURCE,
+ };
+ hr = ID3D11Device_CreateTexture2D(o->device, &copy_desc, NULL,
+ &p->copy_tex);
+ if (FAILED(hr)) {
+ MP_FATAL(mapper, "Could not create shader resource texture\n");
+ return -1;
+ }
+
+ for (int i = 0; i < desc.num_planes; i++) {
+ mapper->tex[i] = ra_d3d11_wrap_tex_video(mapper->ra, p->copy_tex,
+ mp_image_plane_w(&layout, i), mp_image_plane_h(&layout, i), 0,
+ desc.planes[i]);
+ if (!mapper->tex[i]) {
+ MP_FATAL(mapper, "Could not create RA texture view\n");
+ return -1;
+ }
+ }
+
+ // A ref to the immediate context is needed for CopySubresourceRegion
+ ID3D11Device1_GetImmediateContext1(o->device1, &p->ctx);
+ }
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ ID3D11Texture2D *tex = (void *)mapper->src->planes[0];
+ int subresource = (intptr_t)mapper->src->planes[1];
+
+ if (p->copy_tex) {
+ ID3D11DeviceContext1_CopySubresourceRegion1(p->ctx,
+ (ID3D11Resource *)p->copy_tex, 0, 0, 0, 0,
+ (ID3D11Resource *)tex, subresource, (&(D3D11_BOX) {
+ .left = 0,
+ .top = 0,
+ .front = 0,
+ .right = mapper->dst_params.w,
+ .bottom = mapper->dst_params.h,
+ .back = 1,
+ }), D3D11_COPY_DISCARD);
+
+ // We no longer need the original texture after copying it.
+ mp_image_unrefp(&mapper->src);
+ } else {
+ D3D11_TEXTURE2D_DESC desc2d;
+ ID3D11Texture2D_GetDesc(tex, &desc2d);
+
+ for (int i = 0; i < p->num_planes; i++) {
+ // The video decode texture may include padding, so the size of the
+ // ra_tex needs to be determined by the actual size of the Tex2D
+ bool chroma = i >= 1;
+ int w = desc2d.Width / (chroma ? 2 : 1);
+ int h = desc2d.Height / (chroma ? 2 : 1);
+
+ mapper->tex[i] = ra_d3d11_wrap_tex_video(mapper->ra, tex,
+ w, h, subresource, p->fmt[i]);
+ if (!mapper->tex[i])
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ if (p->copy_tex)
+ return;
+ for (int i = 0; i < 4; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+}
+
+const struct ra_hwdec_driver ra_hwdec_d3d11va = {
+ .name = "d3d11va",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_D3D11, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/d3d11/hwdec_dxva2dxgi.c b/video/out/d3d11/hwdec_dxva2dxgi.c
new file mode 100644
index 0000000..62158d4
--- /dev/null
+++ b/video/out/d3d11/hwdec_dxva2dxgi.c
@@ -0,0 +1,478 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <d3d9.h>
+#include <d3d11.h>
+#include <dxva2api.h>
+
+#include "common/common.h"
+#include "osdep/windows_utils.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+#include "video/out/d3d11/ra_d3d11.h"
+#include "video/out/gpu/hwdec.h"
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ ID3D11Device *dev11;
+ IDirect3DDevice9Ex *dev9;
+};
+
+struct queue_surf {
+ ID3D11Texture2D *tex11;
+ ID3D11Query *idle11;
+ ID3D11Texture2D *stage11;
+ IDirect3DTexture9 *tex9;
+ IDirect3DSurface9 *surf9;
+ IDirect3DSurface9 *stage9;
+ struct ra_tex *tex;
+
+ bool busy11; // The surface is currently being used by D3D11
+};
+
+struct priv {
+ ID3D11Device *dev11;
+ ID3D11DeviceContext *ctx11;
+ IDirect3DDevice9Ex *dev9;
+
+ // Surface queue stuff. Following Microsoft recommendations, a queue of
+ // surfaces is used to share images between D3D9 and D3D11. This allows
+ // multiple D3D11 frames to be in-flight at once.
+ struct queue_surf **queue;
+ int queue_len;
+ int queue_pos;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+ SAFE_RELEASE(p->dev11);
+ SAFE_RELEASE(p->dev9);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ IDirect3D9Ex *d3d9ex = NULL;
+ int ret = -1;
+ HRESULT hr;
+
+ if (!ra_is_d3d11(hw->ra_ctx->ra))
+ goto done;
+ p->dev11 = ra_d3d11_get_device(hw->ra_ctx->ra);
+ if (!p->dev11)
+ goto done;
+
+ d3d_load_dlls();
+ if (!d3d9_dll) {
+ MP_FATAL(hw, "Failed to load \"d3d9.dll\": %s\n", mp_LastError_to_str());
+ goto done;
+ }
+ if (!dxva2_dll) {
+ MP_FATAL(hw, "Failed to load \"dxva2.dll\": %s\n", mp_LastError_to_str());
+ goto done;
+ }
+
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT SDKVersion, IDirect3D9Ex **ppD3D);
+ Direct3DCreate9Ex = (void *)GetProcAddress(d3d9_dll, "Direct3DCreate9Ex");
+ if (!Direct3DCreate9Ex) {
+ MP_FATAL(hw, "Direct3D 9Ex not supported\n");
+ goto done;
+ }
+
+ hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Couldn't create Direct3D9Ex: %s\n", mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ D3DPRESENT_PARAMETERS pparams = {
+ .BackBufferWidth = 16,
+ .BackBufferHeight = 16,
+ .BackBufferCount = 1,
+ .SwapEffect = D3DSWAPEFFECT_DISCARD,
+ .hDeviceWindow = GetDesktopWindow(),
+ .Windowed = TRUE,
+ .Flags = D3DPRESENTFLAG_VIDEO,
+ };
+ hr = IDirect3D9Ex_CreateDeviceEx(d3d9ex, D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL, GetDesktopWindow(), D3DCREATE_NOWINDOWCHANGES |
+ D3DCREATE_FPU_PRESERVE | D3DCREATE_HARDWARE_VERTEXPROCESSING |
+ D3DCREATE_DISABLE_PSGP_THREADING | D3DCREATE_MULTITHREADED, &pparams,
+ NULL, &p->dev9);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Failed to create Direct3D9Ex device: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // Check if it's possible to StretchRect() from NV12 to XRGB surfaces
+ hr = IDirect3D9Ex_CheckDeviceFormatConversion(d3d9ex, D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL, MAKEFOURCC('N', 'V', '1', '2'), D3DFMT_X8R8G8B8);
+ if (hr != S_OK) {
+ MP_FATAL(hw, "Can't StretchRect from NV12 to XRGB surfaces\n");
+ goto done;
+ }
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d9_wrap_device_ref((IDirect3DDevice9 *)p->dev9),
+ .hw_imgfmt = IMGFMT_DXVA2,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ goto done;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ ret = 0;
+done:
+ SAFE_RELEASE(d3d9ex);
+ return ret;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ ID3D11Device_AddRef(o->dev11);
+ p->dev11 = o->dev11;
+ IDirect3DDevice9Ex_AddRef(o->dev9);
+ p->dev9 = o->dev9;
+ ID3D11Device_GetImmediateContext(o->dev11, &p->ctx11);
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+ return 0;
+}
+
+static void surf_destroy(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ if (!surf)
+ return;
+ SAFE_RELEASE(surf->tex11);
+ SAFE_RELEASE(surf->idle11);
+ SAFE_RELEASE(surf->stage11);
+ SAFE_RELEASE(surf->tex9);
+ SAFE_RELEASE(surf->surf9);
+ SAFE_RELEASE(surf->stage9);
+ ra_tex_free(mapper->ra, &surf->tex);
+ talloc_free(surf);
+}
+
+static struct queue_surf *surf_create(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ IDXGIResource *res11 = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ struct queue_surf *surf = talloc_ptrtype(p, surf);
+
+ D3D11_TEXTURE2D_DESC desc11 = {
+ .Width = mapper->src->w,
+ .Height = mapper->src->h,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .Format = DXGI_FORMAT_B8G8R8X8_UNORM,
+ .SampleDesc.Count = 1,
+ .Usage = D3D11_USAGE_DEFAULT,
+ .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET,
+ .MiscFlags = D3D11_RESOURCE_MISC_SHARED,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->dev11, &desc11, NULL, &surf->tex11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D11 texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // Try to use a 16x16 staging texture, unless the source surface is
+ // smaller. Ideally, a 1x1 texture would be sufficient, but Microsoft's
+ // D3D9ExDXGISharedSurf example uses 16x16 to avoid driver bugs.
+ D3D11_TEXTURE2D_DESC sdesc11 = {
+ .Width = MPMIN(16, desc11.Width),
+ .Height = MPMIN(16, desc11.Height),
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .Format = DXGI_FORMAT_B8G8R8X8_UNORM,
+ .SampleDesc.Count = 1,
+ .Usage = D3D11_USAGE_STAGING,
+ .CPUAccessFlags = D3D11_CPU_ACCESS_READ,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->dev11, &sdesc11, NULL, &surf->stage11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D11 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = ID3D11Texture2D_QueryInterface(surf->tex11, &IID_IDXGIResource,
+ (void**)&res11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get share handle: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ HANDLE share_handle;
+ hr = IDXGIResource_GetSharedHandle(res11, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get share handle: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = ID3D11Device_CreateQuery(p->dev11,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_EVENT }, &surf->idle11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D11 query: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // Share the D3D11 texture with D3D9Ex
+ hr = IDirect3DDevice9Ex_CreateTexture(p->dev9, desc11.Width, desc11.Height,
+ 1, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT,
+ &surf->tex9, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D9 texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDirect3DTexture9_GetSurfaceLevel(surf->tex9, 0, &surf->surf9);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get D3D9 surface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // As above, try to use a 16x16 staging texture to avoid driver bugs
+ hr = IDirect3DDevice9Ex_CreateRenderTarget(p->dev9,
+ MPMIN(16, desc11.Width), MPMIN(16, desc11.Height), D3DFMT_X8R8G8B8,
+ D3DMULTISAMPLE_NONE, 0, TRUE, &surf->stage9, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D9 staging surface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ surf->tex = ra_d3d11_wrap_tex(mapper->ra, (ID3D11Resource *)surf->tex11);
+ if (!surf->tex)
+ goto done;
+
+ success = true;
+done:
+ if (!success)
+ surf_destroy(mapper, surf);
+ SAFE_RELEASE(res11);
+ return success ? surf : NULL;
+}
+
+// true if the surface is currently in-use by the D3D11 graphics pipeline
+static bool surf_is_idle11(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+ BOOL idle;
+
+ if (!surf->busy11)
+ return true;
+
+ hr = ID3D11DeviceContext_GetData(p->ctx11,
+ (ID3D11Asynchronous *)surf->idle11, &idle, sizeof(idle),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE || !idle)
+ return false;
+
+ surf->busy11 = false;
+ return true;
+}
+
+// If the surface is currently in-use by the D3D11 graphics pipeline, wait for
+// it to become idle. Should only be called in the queue-underflow case.
+static bool surf_wait_idle11(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ ID3D11DeviceContext_CopySubresourceRegion(p->ctx11,
+ (ID3D11Resource *)surf->stage11, 0, 0, 0, 0,
+ (ID3D11Resource *)surf->tex11, 0, (&(D3D11_BOX){
+ .right = MPMIN(16, mapper->src->w),
+ .bottom = MPMIN(16, mapper->src->h),
+ .back = 1,
+ }));
+
+ // Block until the surface becomes idle (see surf_wait_idle9())
+ D3D11_MAPPED_SUBRESOURCE map = {0};
+ hr = ID3D11DeviceContext_Map(p->ctx11, (ID3D11Resource *)surf->stage11, 0,
+ D3D11_MAP_READ, 0, &map);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Couldn't map D3D11 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ ID3D11DeviceContext_Unmap(p->ctx11, (ID3D11Resource *)surf->stage11, 0);
+ surf->busy11 = false;
+ return true;
+}
+
+static bool surf_wait_idle9(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ // Rather than polling for the surface to become idle, copy part of the
+ // surface to a staging texture and map it. This should block until the
+ // surface becomes idle. Microsoft's ISurfaceQueue does this as well.
+ RECT rc = {0, 0, MPMIN(16, mapper->src->w), MPMIN(16, mapper->src->h)};
+ hr = IDirect3DDevice9Ex_StretchRect(p->dev9, surf->surf9, &rc, surf->stage9,
+ &rc, D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Couldn't copy to D3D9 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ D3DLOCKED_RECT lock;
+ hr = IDirect3DSurface9_LockRect(surf->stage9, &lock, NULL, D3DLOCK_READONLY);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Couldn't map D3D9 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ IDirect3DSurface9_UnlockRect(surf->stage9);
+ p->queue[p->queue_pos]->busy11 = true;
+ return true;
+}
+
+static struct queue_surf *surf_acquire(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ if (!p->queue_len || !surf_is_idle11(mapper, p->queue[p->queue_pos])) {
+ if (p->queue_len < 16) {
+ struct queue_surf *surf = surf_create(mapper);
+ if (!surf)
+ return NULL;
+
+ // The next surface is busy, so grow the queue
+ MP_TARRAY_INSERT_AT(p, p->queue, p->queue_len, p->queue_pos, surf);
+ MP_DBG(mapper, "Queue grew to %d surfaces\n", p->queue_len);
+ } else {
+ // For sanity, don't let the queue grow beyond 16 surfaces. It
+ // should never get this big. If it does, wait for the surface to
+ // become idle rather than polling it.
+ if (!surf_wait_idle11(mapper, p->queue[p->queue_pos]))
+ return NULL;
+ MP_WARN(mapper, "Queue underflow!\n");
+ }
+ }
+ return p->queue[p->queue_pos];
+}
+
+static void surf_release(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ ID3D11DeviceContext_End(p->ctx11,
+ (ID3D11Asynchronous *)p->queue[p->queue_pos]->idle11);
+
+ // The current surface is now in-flight, move to the next surface
+ p->queue_pos++;
+ if (p->queue_pos >= p->queue_len)
+ p->queue_pos = 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int i = 0; i < p->queue_len; i++)
+ surf_destroy(mapper, p->queue[i]);
+
+ SAFE_RELEASE(p->ctx11);
+ SAFE_RELEASE(p->dev9);
+ SAFE_RELEASE(p->dev11);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ struct queue_surf *surf = surf_acquire(mapper);
+ if (!surf)
+ return -1;
+
+ RECT rc = {0, 0, mapper->src->w, mapper->src->h};
+ IDirect3DSurface9* hw_surface = (IDirect3DSurface9 *)mapper->src->planes[3];
+
+ hr = IDirect3DDevice9Ex_StretchRect(p->dev9, hw_surface, &rc, surf->surf9,
+ &rc, D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "StretchRect() failed: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ if (!surf_wait_idle9(mapper, surf))
+ return -1;
+
+ mapper->tex[0] = surf->tex;
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ if (p->queue_pos < p->queue_len &&
+ p->queue[p->queue_pos]->tex == mapper->tex[0])
+ {
+ surf_release(mapper);
+ mapper->tex[0] = NULL;
+ }
+}
+
+const struct ra_hwdec_driver ra_hwdec_dxva2dxgi = {
+ .name = "dxva2-dxgi",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DXVA2, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/d3d11/ra_d3d11.c b/video/out/d3d11/ra_d3d11.c
new file mode 100644
index 0000000..84fd004
--- /dev/null
+++ b/video/out/d3d11/ra_d3d11.c
@@ -0,0 +1,2544 @@
+#include <windows.h>
+#include <versionhelpers.h>
+#include <d3d11_1.h>
+#include <d3d11sdklayers.h>
+#include <dxgi1_2.h>
+#include <d3dcompiler.h>
+#include <spirv_cross_c.h>
+
+#include "common/msg.h"
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/spirv.h"
+#include "video/out/gpu/utils.h"
+
+#include "ra_d3d11.h"
+
+#ifndef D3D11_1_UAV_SLOT_COUNT
+#define D3D11_1_UAV_SLOT_COUNT (64)
+#endif
+#define D3D11_FORMAT_SUPPORT2_UAV_TYPED_STORE (0x80)
+
+// D3D11.3 message IDs, not present in mingw-w64 v9
+#define D3D11_MESSAGE_ID_CREATE_FENCE ((D3D11_MESSAGE_ID)0x300209)
+#define D3D11_MESSAGE_ID_DESTROY_FENCE ((D3D11_MESSAGE_ID)0x30020b)
+
+struct dll_version {
+ uint16_t major;
+ uint16_t minor;
+ uint16_t build;
+ uint16_t revision;
+};
+
+struct ra_d3d11 {
+ struct spirv_compiler *spirv;
+
+ ID3D11Device *dev;
+ ID3D11Device1 *dev1;
+ ID3D11DeviceContext *ctx;
+ ID3D11DeviceContext1 *ctx1;
+ pD3DCompile D3DCompile;
+
+ struct dll_version d3d_compiler_ver;
+
+ // Debug interfaces (--gpu-debug)
+ ID3D11Debug *debug;
+ ID3D11InfoQueue *iqueue;
+
+ // Device capabilities
+ D3D_FEATURE_LEVEL fl;
+ bool has_clear_view;
+ bool has_timestamp_queries;
+ int max_uavs;
+
+ // Streaming dynamic vertex buffer, which is used for all renderpasses
+ ID3D11Buffer *vbuf;
+ size_t vbuf_size;
+ size_t vbuf_used;
+
+ // clear() renderpass resources (only used when has_clear_view is false)
+ ID3D11PixelShader *clear_ps;
+ ID3D11VertexShader *clear_vs;
+ ID3D11InputLayout *clear_layout;
+ ID3D11Buffer *clear_vbuf;
+ ID3D11Buffer *clear_cbuf;
+
+ // blit() renderpass resources
+ ID3D11PixelShader *blit_float_ps;
+ ID3D11VertexShader *blit_vs;
+ ID3D11InputLayout *blit_layout;
+ ID3D11Buffer *blit_vbuf;
+ ID3D11SamplerState *blit_sampler;
+};
+
+struct d3d_tex {
+ // res mirrors one of tex1d, tex2d or tex3d for convenience. It does not
+ // hold an additional reference to the texture object.
+ ID3D11Resource *res;
+
+ ID3D11Texture1D *tex1d;
+ ID3D11Texture2D *tex2d;
+ ID3D11Texture3D *tex3d;
+ int array_slice;
+
+ // Staging texture for tex_download(), 2D only
+ ID3D11Texture2D *staging;
+
+ ID3D11ShaderResourceView *srv;
+ ID3D11RenderTargetView *rtv;
+ ID3D11UnorderedAccessView *uav;
+ ID3D11SamplerState *sampler;
+};
+
+struct d3d_buf {
+ ID3D11Buffer *buf;
+ ID3D11UnorderedAccessView *uav;
+ void *data; // System-memory mirror of the data in buf
+ bool dirty; // Is buf out of date?
+};
+
+struct d3d_rpass {
+ ID3D11PixelShader *ps;
+ ID3D11VertexShader *vs;
+ ID3D11ComputeShader *cs;
+ ID3D11InputLayout *layout;
+ ID3D11BlendState *bstate;
+};
+
+struct d3d_timer {
+ ID3D11Query *ts_start;
+ ID3D11Query *ts_end;
+ ID3D11Query *disjoint;
+ uint64_t result; // Latches the result from the previous use of the timer
+};
+
+struct d3d_fmt {
+ const char *name;
+ int components;
+ int bytes;
+ int bits[4];
+ DXGI_FORMAT fmt;
+ enum ra_ctype ctype;
+ bool unordered;
+};
+
+static const char clear_vs[] = "\
+float4 main(float2 pos : POSITION) : SV_Position\n\
+{\n\
+ return float4(pos, 0.0, 1.0);\n\
+}\n\
+";
+
+static const char clear_ps[] = "\
+cbuffer ps_cbuf : register(b0) {\n\
+ float4 color : packoffset(c0);\n\
+}\n\
+\n\
+float4 main(float4 pos : SV_Position) : SV_Target\n\
+{\n\
+ return color;\n\
+}\n\
+";
+
+struct blit_vert {
+ float x, y, u, v;
+};
+
+static const char blit_vs[] = "\
+void main(float2 pos : POSITION, float2 coord : TEXCOORD0,\n\
+ out float4 out_pos : SV_Position, out float2 out_coord : TEXCOORD0)\n\
+{\n\
+ out_pos = float4(pos, 0.0, 1.0);\n\
+ out_coord = coord;\n\
+}\n\
+";
+
+static const char blit_float_ps[] = "\
+Texture2D<float4> tex : register(t0);\n\
+SamplerState samp : register(s0);\n\
+\n\
+float4 main(float4 pos : SV_Position, float2 coord : TEXCOORD0) : SV_Target\n\
+{\n\
+ return tex.Sample(samp, coord);\n\
+}\n\
+";
+
+#define DXFMT(f, t) .fmt = DXGI_FORMAT_##f##_##t, .ctype = RA_CTYPE_##t
+static struct d3d_fmt formats[] = {
+ { "r8", 1, 1, { 8}, DXFMT(R8, UNORM) },
+ { "rg8", 2, 2, { 8, 8}, DXFMT(R8G8, UNORM) },
+ { "rgba8", 4, 4, { 8, 8, 8, 8}, DXFMT(R8G8B8A8, UNORM) },
+ { "r16", 1, 2, {16}, DXFMT(R16, UNORM) },
+ { "rg16", 2, 4, {16, 16}, DXFMT(R16G16, UNORM) },
+ { "rgba16", 4, 8, {16, 16, 16, 16}, DXFMT(R16G16B16A16, UNORM) },
+
+ { "r32ui", 1, 4, {32}, DXFMT(R32, UINT) },
+ { "rg32ui", 2, 8, {32, 32}, DXFMT(R32G32, UINT) },
+ { "rgb32ui", 3, 12, {32, 32, 32}, DXFMT(R32G32B32, UINT) },
+ { "rgba32ui", 4, 16, {32, 32, 32, 32}, DXFMT(R32G32B32A32, UINT) },
+
+ { "r16hf", 1, 2, {16}, DXFMT(R16, FLOAT) },
+ { "rg16hf", 2, 4, {16, 16}, DXFMT(R16G16, FLOAT) },
+ { "rgba16hf", 4, 8, {16, 16, 16, 16}, DXFMT(R16G16B16A16, FLOAT) },
+ { "r32f", 1, 4, {32}, DXFMT(R32, FLOAT) },
+ { "rg32f", 2, 8, {32, 32}, DXFMT(R32G32, FLOAT) },
+ { "rgb32f", 3, 12, {32, 32, 32}, DXFMT(R32G32B32, FLOAT) },
+ { "rgba32f", 4, 16, {32, 32, 32, 32}, DXFMT(R32G32B32A32, FLOAT) },
+
+ { "rgb10_a2", 4, 4, {10, 10, 10, 2}, DXFMT(R10G10B10A2, UNORM) },
+ { "bgra8", 4, 4, { 8, 8, 8, 8}, DXFMT(B8G8R8A8, UNORM), .unordered = true },
+ { "bgrx8", 3, 4, { 8, 8, 8}, DXFMT(B8G8R8X8, UNORM), .unordered = true },
+};
+
+static bool dll_version_equal(struct dll_version a, struct dll_version b)
+{
+ return a.major == b.major &&
+ a.minor == b.minor &&
+ a.build == b.build &&
+ a.revision == b.revision;
+}
+
+DXGI_FORMAT ra_d3d11_get_format(const struct ra_format *fmt)
+{
+ struct d3d_fmt *d3d = fmt->priv;
+ return d3d->fmt;
+}
+
+const struct ra_format *ra_d3d11_get_ra_format(struct ra *ra, DXGI_FORMAT fmt)
+{
+ for (int i = 0; i < ra->num_formats; i++) {
+ struct ra_format *ra_fmt = ra->formats[i];
+
+ if (ra_d3d11_get_format(ra_fmt) == fmt)
+ return ra_fmt;
+ }
+
+ return NULL;
+}
+
+static void setup_formats(struct ra *ra)
+{
+ // All formats must be usable as a 2D texture
+ static const UINT sup_basic = D3D11_FORMAT_SUPPORT_TEXTURE2D;
+ // SHADER_SAMPLE indicates support for linear sampling, point always works
+ static const UINT sup_filter = D3D11_FORMAT_SUPPORT_SHADER_SAMPLE;
+ // RA requires renderable surfaces to be blendable as well
+ static const UINT sup_render = D3D11_FORMAT_SUPPORT_RENDER_TARGET |
+ D3D11_FORMAT_SUPPORT_BLENDABLE;
+ // Typed UAVs are equivalent to images. RA only cares if they're storable.
+ static const UINT sup_store = D3D11_FORMAT_SUPPORT_TYPED_UNORDERED_ACCESS_VIEW;
+ static const UINT sup2_store = D3D11_FORMAT_SUPPORT2_UAV_TYPED_STORE;
+
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(formats); i++) {
+ struct d3d_fmt *d3dfmt = &formats[i];
+ UINT support = 0;
+ hr = ID3D11Device_CheckFormatSupport(p->dev, d3dfmt->fmt, &support);
+ if (FAILED(hr))
+ continue;
+ if ((support & sup_basic) != sup_basic)
+ continue;
+
+ D3D11_FEATURE_DATA_FORMAT_SUPPORT2 sup2 = { .InFormat = d3dfmt->fmt };
+ ID3D11Device_CheckFeatureSupport(p->dev, D3D11_FEATURE_FORMAT_SUPPORT2,
+ &sup2, sizeof(sup2));
+ UINT support2 = sup2.OutFormatSupport2;
+
+ struct ra_format *fmt = talloc_zero(ra, struct ra_format);
+ *fmt = (struct ra_format) {
+ .name = d3dfmt->name,
+ .priv = d3dfmt,
+ .ctype = d3dfmt->ctype,
+ .ordered = !d3dfmt->unordered,
+ .num_components = d3dfmt->components,
+ .pixel_size = d3dfmt->bytes,
+ .linear_filter = (support & sup_filter) == sup_filter,
+ .renderable = (support & sup_render) == sup_render,
+ .storable = p->fl >= D3D_FEATURE_LEVEL_11_0 &&
+ (support & sup_store) == sup_store &&
+ (support2 & sup2_store) == sup2_store,
+ };
+
+ if (support & D3D11_FORMAT_SUPPORT_TEXTURE1D)
+ ra->caps |= RA_CAP_TEX_1D;
+
+ for (int j = 0; j < d3dfmt->components; j++)
+ fmt->component_size[j] = fmt->component_depth[j] = d3dfmt->bits[j];
+
+ fmt->glsl_format = ra_fmt_glsl_format(fmt);
+
+ MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, fmt);
+ }
+}
+
+static bool tex_init(struct ra *ra, struct ra_tex *tex)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *tex_p = tex->priv;
+ struct ra_tex_params *params = &tex->params;
+ HRESULT hr;
+
+ // A SRV is required for renderpasses and blitting, since blitting can use
+ // a renderpass internally
+ if (params->render_src || params->blit_src) {
+ // Always specify the SRV format for simplicity. This will match the
+ // texture format for textures created with tex_create, but it can be
+ // different for wrapped planar video textures.
+ D3D11_SHADER_RESOURCE_VIEW_DESC srvdesc = {
+ .Format = ra_d3d11_get_format(params->format),
+ };
+ switch (params->dimensions) {
+ case 1:
+ if (tex_p->array_slice >= 0) {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE1DARRAY;
+ srvdesc.Texture1DArray.MipLevels = 1;
+ srvdesc.Texture1DArray.FirstArraySlice = tex_p->array_slice;
+ srvdesc.Texture1DArray.ArraySize = 1;
+ } else {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE1D;
+ srvdesc.Texture1D.MipLevels = 1;
+ }
+ break;
+ case 2:
+ if (tex_p->array_slice >= 0) {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
+ srvdesc.Texture2DArray.MipLevels = 1;
+ srvdesc.Texture2DArray.FirstArraySlice = tex_p->array_slice;
+ srvdesc.Texture2DArray.ArraySize = 1;
+ } else {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
+ srvdesc.Texture2D.MipLevels = 1;
+ }
+ break;
+ case 3:
+ // D3D11 does not have Texture3D arrays
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE3D;
+ srvdesc.Texture3D.MipLevels = 1;
+ break;
+ }
+ hr = ID3D11Device_CreateShaderResourceView(p->dev, tex_p->res, &srvdesc,
+ &tex_p->srv);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create SRV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ // Samplers are required for renderpasses, but not blitting, since the blit
+ // code uses its own point sampler
+ if (params->render_src) {
+ D3D11_SAMPLER_DESC sdesc = {
+ .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .ComparisonFunc = D3D11_COMPARISON_NEVER,
+ .MinLOD = 0,
+ .MaxLOD = D3D11_FLOAT32_MAX,
+ .MaxAnisotropy = 1,
+ };
+ if (params->src_linear)
+ sdesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
+ if (params->src_repeat) {
+ sdesc.AddressU = sdesc.AddressV = sdesc.AddressW =
+ D3D11_TEXTURE_ADDRESS_WRAP;
+ }
+ // The runtime pools sampler state objects internally, so we don't have
+ // to worry about resource usage when creating one for every ra_tex
+ hr = ID3D11Device_CreateSamplerState(p->dev, &sdesc, &tex_p->sampler);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create sampler: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ // Like SRVs, an RTV is required for renderpass output and blitting
+ if (params->render_dst || params->blit_dst) {
+ hr = ID3D11Device_CreateRenderTargetView(p->dev, tex_p->res, NULL,
+ &tex_p->rtv);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create RTV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0 && params->storage_dst) {
+ hr = ID3D11Device_CreateUnorderedAccessView(p->dev, tex_p->res, NULL,
+ &tex_p->uav);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create UAV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ return true;
+error:
+ return false;
+}
+
+static void tex_destroy(struct ra *ra, struct ra_tex *tex)
+{
+ if (!tex)
+ return;
+ struct d3d_tex *tex_p = tex->priv;
+
+ SAFE_RELEASE(tex_p->srv);
+ SAFE_RELEASE(tex_p->rtv);
+ SAFE_RELEASE(tex_p->uav);
+ SAFE_RELEASE(tex_p->sampler);
+ SAFE_RELEASE(tex_p->res);
+ SAFE_RELEASE(tex_p->staging);
+ talloc_free(tex);
+}
+
+static struct ra_tex *tex_create(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ // Only 2D textures may be downloaded for now
+ if (params->downloadable && params->dimensions != 2)
+ return NULL;
+
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ tex->params = *params;
+ tex->params.initial_data = NULL;
+
+ struct d3d_tex *tex_p = tex->priv = talloc_zero(tex, struct d3d_tex);
+ DXGI_FORMAT fmt = ra_d3d11_get_format(params->format);
+
+ D3D11_SUBRESOURCE_DATA data;
+ D3D11_SUBRESOURCE_DATA *pdata = NULL;
+ if (params->initial_data) {
+ data = (D3D11_SUBRESOURCE_DATA) {
+ .pSysMem = params->initial_data,
+ .SysMemPitch = params->w * params->format->pixel_size,
+ };
+ if (params->dimensions >= 3)
+ data.SysMemSlicePitch = data.SysMemPitch * params->h;
+ pdata = &data;
+ }
+
+ D3D11_USAGE usage = D3D11_USAGE_DEFAULT;
+ D3D11_BIND_FLAG bind_flags = 0;
+
+ if (params->render_src || params->blit_src)
+ bind_flags |= D3D11_BIND_SHADER_RESOURCE;
+ if (params->render_dst || params->blit_dst)
+ bind_flags |= D3D11_BIND_RENDER_TARGET;
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0 && params->storage_dst)
+ bind_flags |= D3D11_BIND_UNORDERED_ACCESS;
+
+ // Apparently IMMUTABLE textures are efficient, so try to infer whether we
+ // can use one
+ if (params->initial_data && !params->render_dst && !params->storage_dst &&
+ !params->blit_dst && !params->host_mutable)
+ usage = D3D11_USAGE_IMMUTABLE;
+
+ switch (params->dimensions) {
+ case 1:;
+ D3D11_TEXTURE1D_DESC desc1d = {
+ .Width = params->w,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .Format = fmt,
+ .Usage = usage,
+ .BindFlags = bind_flags,
+ };
+ hr = ID3D11Device_CreateTexture1D(p->dev, &desc1d, pdata, &tex_p->tex1d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create Texture1D: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex1d;
+ break;
+ case 2:;
+ D3D11_TEXTURE2D_DESC desc2d = {
+ .Width = params->w,
+ .Height = params->h,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc.Count = 1,
+ .Format = fmt,
+ .Usage = usage,
+ .BindFlags = bind_flags,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->dev, &desc2d, pdata, &tex_p->tex2d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create Texture2D: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex2d;
+
+ // Create a staging texture with CPU access for tex_download()
+ if (params->downloadable) {
+ desc2d.BindFlags = 0;
+ desc2d.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc2d.Usage = D3D11_USAGE_STAGING;
+
+ hr = ID3D11Device_CreateTexture2D(p->dev, &desc2d, NULL,
+ &tex_p->staging);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+ break;
+ case 3:;
+ D3D11_TEXTURE3D_DESC desc3d = {
+ .Width = params->w,
+ .Height = params->h,
+ .Depth = params->d,
+ .MipLevels = 1,
+ .Format = fmt,
+ .Usage = usage,
+ .BindFlags = bind_flags,
+ };
+ hr = ID3D11Device_CreateTexture3D(p->dev, &desc3d, pdata, &tex_p->tex3d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create Texture3D: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex3d;
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ tex_p->array_slice = -1;
+
+ if (!tex_init(ra, tex))
+ goto error;
+
+ return tex;
+
+error:
+ tex_destroy(ra, tex);
+ return NULL;
+}
+
+struct ra_tex *ra_d3d11_wrap_tex(struct ra *ra, ID3D11Resource *res)
+{
+ HRESULT hr;
+
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ struct ra_tex_params *params = &tex->params;
+ struct d3d_tex *tex_p = tex->priv = talloc_zero(tex, struct d3d_tex);
+
+ DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
+ D3D11_USAGE usage = D3D11_USAGE_DEFAULT;
+ D3D11_BIND_FLAG bind_flags = 0;
+
+ D3D11_RESOURCE_DIMENSION type;
+ ID3D11Resource_GetType(res, &type);
+ switch (type) {
+ case D3D11_RESOURCE_DIMENSION_TEXTURE2D:
+ hr = ID3D11Resource_QueryInterface(res, &IID_ID3D11Texture2D,
+ (void**)&tex_p->tex2d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Resource is not a ID3D11Texture2D\n");
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex2d;
+
+ D3D11_TEXTURE2D_DESC desc2d;
+ ID3D11Texture2D_GetDesc(tex_p->tex2d, &desc2d);
+ if (desc2d.MipLevels != 1) {
+ MP_ERR(ra, "Mipmapped textures not supported for wrapping\n");
+ goto error;
+ }
+ if (desc2d.ArraySize != 1) {
+ MP_ERR(ra, "Texture arrays not supported for wrapping\n");
+ goto error;
+ }
+ if (desc2d.SampleDesc.Count != 1) {
+ MP_ERR(ra, "Multisampled textures not supported for wrapping\n");
+ goto error;
+ }
+
+ params->dimensions = 2;
+ params->w = desc2d.Width;
+ params->h = desc2d.Height;
+ params->d = 1;
+ usage = desc2d.Usage;
+ bind_flags = desc2d.BindFlags;
+ fmt = desc2d.Format;
+ break;
+ default:
+ // We could wrap Texture1D/3D as well, but keep it simple, since this
+ // function is only used for swapchain backbuffers at the moment
+ MP_ERR(ra, "Resource is not suitable to wrap\n");
+ goto error;
+ }
+
+ for (int i = 0; i < ra->num_formats; i++) {
+ DXGI_FORMAT target_fmt = ra_d3d11_get_format(ra->formats[i]);
+ if (fmt == target_fmt) {
+ params->format = ra->formats[i];
+ break;
+ }
+ }
+ if (!params->format) {
+ MP_ERR(ra, "Could not find a suitable RA format for wrapped resource\n");
+ goto error;
+ }
+
+ if (bind_flags & D3D11_BIND_SHADER_RESOURCE) {
+ params->render_src = params->blit_src = true;
+ params->src_linear = params->format->linear_filter;
+ }
+ if (bind_flags & D3D11_BIND_RENDER_TARGET)
+ params->render_dst = params->blit_dst = true;
+ if (bind_flags & D3D11_BIND_UNORDERED_ACCESS)
+ params->storage_dst = true;
+
+ if (usage != D3D11_USAGE_DEFAULT) {
+ MP_ERR(ra, "Resource is not D3D11_USAGE_DEFAULT\n");
+ goto error;
+ }
+
+ tex_p->array_slice = -1;
+
+ if (!tex_init(ra, tex))
+ goto error;
+
+ return tex;
+error:
+ tex_destroy(ra, tex);
+ return NULL;
+}
+
+struct ra_tex *ra_d3d11_wrap_tex_video(struct ra *ra, ID3D11Texture2D *res,
+ int w, int h, int array_slice,
+ const struct ra_format *fmt)
+{
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ struct ra_tex_params *params = &tex->params;
+ struct d3d_tex *tex_p = tex->priv = talloc_zero(tex, struct d3d_tex);
+
+ tex_p->tex2d = res;
+ tex_p->res = (ID3D11Resource *)tex_p->tex2d;
+ ID3D11Texture2D_AddRef(res);
+
+ D3D11_TEXTURE2D_DESC desc2d;
+ ID3D11Texture2D_GetDesc(tex_p->tex2d, &desc2d);
+ if (!(desc2d.BindFlags & D3D11_BIND_SHADER_RESOURCE)) {
+ MP_ERR(ra, "Video resource is not bindable\n");
+ goto error;
+ }
+
+ params->dimensions = 2;
+ params->w = w;
+ params->h = h;
+ params->d = 1;
+ params->render_src = true;
+ params->src_linear = true;
+ // fmt can be different to the texture format for planar video textures
+ params->format = fmt;
+
+ if (desc2d.ArraySize > 1) {
+ tex_p->array_slice = array_slice;
+ } else {
+ tex_p->array_slice = -1;
+ }
+
+ if (!tex_init(ra, tex))
+ goto error;
+
+ return tex;
+error:
+ tex_destroy(ra, tex);
+ return NULL;
+}
+
+ID3D11Resource *ra_d3d11_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ int *array_slice)
+{
+ struct d3d_tex *tex_p = tex->priv;
+
+ ID3D11Resource_AddRef(tex_p->res);
+ if (array_slice)
+ *array_slice = tex_p->array_slice;
+ return tex_p->res;
+}
+
+static bool tex_upload(struct ra *ra, const struct ra_tex_upload_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_tex *tex = params->tex;
+ struct d3d_tex *tex_p = tex->priv;
+
+ if (!params->src) {
+ MP_ERR(ra, "Pixel buffers are not supported\n");
+ return false;
+ }
+
+ const char *src = params->src;
+ ptrdiff_t stride = tex->params.dimensions >= 2 ? tex->params.w : 0;
+ ptrdiff_t pitch = tex->params.dimensions >= 3 ? stride * tex->params.h : 0;
+ bool invalidate = true;
+ D3D11_BOX rc;
+ D3D11_BOX *prc = NULL;
+
+ if (tex->params.dimensions == 2) {
+ stride = params->stride;
+
+ if (params->rc && (params->rc->x0 != 0 || params->rc->y0 != 0 ||
+ params->rc->x1 != tex->params.w || params->rc->y1 != tex->params.h))
+ {
+ rc = (D3D11_BOX) {
+ .left = params->rc->x0,
+ .top = params->rc->y0,
+ .front = 0,
+ .right = params->rc->x1,
+ .bottom = params->rc->y1,
+ .back = 1,
+ };
+ prc = &rc;
+ invalidate = params->invalidate;
+ }
+ }
+
+ int subresource = tex_p->array_slice >= 0 ? tex_p->array_slice : 0;
+ if (p->ctx1) {
+ ID3D11DeviceContext1_UpdateSubresource1(p->ctx1, tex_p->res,
+ subresource, prc, src, stride, pitch,
+ invalidate ? D3D11_COPY_DISCARD : 0);
+ } else {
+ ID3D11DeviceContext_UpdateSubresource(p->ctx, tex_p->res, subresource,
+ prc, src, stride, pitch);
+ }
+
+ return true;
+}
+
+static bool tex_download(struct ra *ra, struct ra_tex_download_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_tex *tex = params->tex;
+ struct d3d_tex *tex_p = tex->priv;
+ HRESULT hr;
+
+ if (!tex_p->staging)
+ return false;
+
+ ID3D11DeviceContext_CopyResource(p->ctx, (ID3D11Resource*)tex_p->staging,
+ tex_p->res);
+
+ D3D11_MAPPED_SUBRESOURCE lock;
+ hr = ID3D11DeviceContext_Map(p->ctx, (ID3D11Resource*)tex_p->staging, 0,
+ D3D11_MAP_READ, 0, &lock);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to map staging texture: %s\n", mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ char *cdst = params->dst;
+ char *csrc = lock.pData;
+ for (int y = 0; y < tex->params.h; y++) {
+ memcpy(cdst + y * params->stride, csrc + y * lock.RowPitch,
+ MPMIN(params->stride, lock.RowPitch));
+ }
+
+ ID3D11DeviceContext_Unmap(p->ctx, (ID3D11Resource*)tex_p->staging, 0);
+
+ return true;
+}
+
+static void buf_destroy(struct ra *ra, struct ra_buf *buf)
+{
+ if (!buf)
+ return;
+ struct d3d_buf *buf_p = buf->priv;
+ SAFE_RELEASE(buf_p->buf);
+ SAFE_RELEASE(buf_p->uav);
+ talloc_free(buf);
+}
+
+static struct ra_buf *buf_create(struct ra *ra,
+ const struct ra_buf_params *params)
+{
+ // D3D11 does not support permanent mapping or pixel buffers
+ if (params->host_mapped || params->type == RA_BUF_TYPE_TEX_UPLOAD)
+ return NULL;
+
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ struct ra_buf *buf = talloc_zero(NULL, struct ra_buf);
+ buf->params = *params;
+ buf->params.initial_data = NULL;
+
+ struct d3d_buf *buf_p = buf->priv = talloc_zero(buf, struct d3d_buf);
+
+ D3D11_SUBRESOURCE_DATA data;
+ D3D11_SUBRESOURCE_DATA *pdata = NULL;
+ if (params->initial_data) {
+ data = (D3D11_SUBRESOURCE_DATA) { .pSysMem = params->initial_data };
+ pdata = &data;
+ }
+
+ D3D11_BUFFER_DESC desc = { .ByteWidth = params->size };
+ switch (params->type) {
+ case RA_BUF_TYPE_SHADER_STORAGE:
+ desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
+ desc.ByteWidth = MP_ALIGN_UP(desc.ByteWidth, sizeof(float));
+ desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
+ break;
+ case RA_BUF_TYPE_UNIFORM:
+ desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
+ desc.ByteWidth = MP_ALIGN_UP(desc.ByteWidth, sizeof(float[4]));
+ break;
+ }
+
+ hr = ID3D11Device_CreateBuffer(p->dev, &desc, pdata, &buf_p->buf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create buffer: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // D3D11 doesn't allow constant buffer updates that aren't aligned to a
+ // full constant boundary (vec4,) and some drivers don't allow partial
+ // constant buffer updates at all. To support partial buffer updates, keep
+ // a mirror of the buffer data in system memory and upload the whole thing
+ // before the buffer is used.
+ if (params->host_mutable)
+ buf_p->data = talloc_zero_size(buf, desc.ByteWidth);
+
+ if (params->type == RA_BUF_TYPE_SHADER_STORAGE) {
+ D3D11_UNORDERED_ACCESS_VIEW_DESC udesc = {
+ .Format = DXGI_FORMAT_R32_TYPELESS,
+ .ViewDimension = D3D11_UAV_DIMENSION_BUFFER,
+ .Buffer = {
+ .NumElements = desc.ByteWidth / sizeof(float),
+ .Flags = D3D11_BUFFER_UAV_FLAG_RAW,
+ },
+ };
+ hr = ID3D11Device_CreateUnorderedAccessView(p->dev,
+ (ID3D11Resource *)buf_p->buf, &udesc, &buf_p->uav);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create UAV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ return buf;
+error:
+ buf_destroy(ra, buf);
+ return NULL;
+}
+
+static void buf_resolve(struct ra *ra, struct ra_buf *buf)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_buf *buf_p = buf->priv;
+
+ if (!buf->params.host_mutable || !buf_p->dirty)
+ return;
+
+ // Synchronize the GPU buffer with the system-memory copy
+ ID3D11DeviceContext_UpdateSubresource(p->ctx, (ID3D11Resource *)buf_p->buf,
+ 0, NULL, buf_p->data, 0, 0);
+ buf_p->dirty = false;
+}
+
+static void buf_update(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size)
+{
+ struct d3d_buf *buf_p = buf->priv;
+
+ char *cdata = buf_p->data;
+ memcpy(cdata + offset, data, size);
+ buf_p->dirty = true;
+}
+
+static const char *get_shader_target(struct ra *ra, enum glsl_shader type)
+{
+ struct ra_d3d11 *p = ra->priv;
+ switch (p->fl) {
+ default:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_5_0";
+ case GLSL_SHADER_FRAGMENT: return "ps_5_0";
+ case GLSL_SHADER_COMPUTE: return "cs_5_0";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_10_1:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_1";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_1";
+ case GLSL_SHADER_COMPUTE: return "cs_4_1";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_10_0:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_0";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_0";
+ case GLSL_SHADER_COMPUTE: return "cs_4_0";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_9_3:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_0_level_9_3";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_0_level_9_3";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_9_2:
+ case D3D_FEATURE_LEVEL_9_1:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_0_level_9_1";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_0_level_9_1";
+ }
+ break;
+ }
+ return NULL;
+}
+
+static const char *shader_type_name(enum glsl_shader type)
+{
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vertex";
+ case GLSL_SHADER_FRAGMENT: return "fragment";
+ case GLSL_SHADER_COMPUTE: return "compute";
+ default: return "unknown";
+ }
+}
+
+static bool setup_clear_rpass(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3DBlob *vs_blob = NULL;
+ ID3DBlob *ps_blob = NULL;
+ HRESULT hr;
+
+ hr = p->D3DCompile(clear_vs, sizeof(clear_vs), NULL, NULL, NULL, "main",
+ get_shader_target(ra, GLSL_SHADER_VERTEX),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vs_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile clear() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreateVertexShader(p->dev,
+ ID3D10Blob_GetBufferPointer(vs_blob), ID3D10Blob_GetBufferSize(vs_blob),
+ NULL, &p->clear_vs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = p->D3DCompile(clear_ps, sizeof(clear_ps), NULL, NULL, NULL, "main",
+ get_shader_target(ra, GLSL_SHADER_FRAGMENT),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &ps_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile clear() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreatePixelShader(p->dev,
+ ID3D10Blob_GetBufferPointer(ps_blob), ID3D10Blob_GetBufferSize(ps_blob),
+ NULL, &p->clear_ps);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_INPUT_ELEMENT_DESC in_descs[] = {
+ { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0 },
+ };
+ hr = ID3D11Device_CreateInputLayout(p->dev, in_descs,
+ MP_ARRAY_SIZE(in_descs), ID3D10Blob_GetBufferPointer(vs_blob),
+ ID3D10Blob_GetBufferSize(vs_blob), &p->clear_layout);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() IA layout: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // clear() always draws to a quad covering the whole viewport
+ static const float verts[] = {
+ -1, -1,
+ 1, -1,
+ 1, 1,
+ -1, 1,
+ -1, -1,
+ 1, 1,
+ };
+ D3D11_BUFFER_DESC vdesc = {
+ .ByteWidth = sizeof(verts),
+ .Usage = D3D11_USAGE_IMMUTABLE,
+ .BindFlags = D3D11_BIND_VERTEX_BUFFER,
+ };
+ D3D11_SUBRESOURCE_DATA vdata = {
+ .pSysMem = verts,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &vdesc, &vdata, &p->clear_vbuf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() vertex buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_BUFFER_DESC cdesc = {
+ .ByteWidth = sizeof(float[4]),
+ .BindFlags = D3D11_BIND_CONSTANT_BUFFER,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &cdesc, NULL, &p->clear_cbuf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() constant buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return true;
+error:
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return false;
+}
+
+static void clear_rpass(struct ra *ra, struct ra_tex *tex, float color[4],
+ struct mp_rect *rc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *tex_p = tex->priv;
+ struct ra_tex_params *params = &tex->params;
+
+ ID3D11DeviceContext_UpdateSubresource(p->ctx,
+ (ID3D11Resource *)p->clear_cbuf, 0, NULL, color, 0, 0);
+
+ ID3D11DeviceContext_IASetInputLayout(p->ctx, p->clear_layout);
+ ID3D11DeviceContext_IASetVertexBuffers(p->ctx, 0, 1, &p->clear_vbuf,
+ &(UINT) { sizeof(float[2]) }, &(UINT) { 0 });
+ ID3D11DeviceContext_IASetPrimitiveTopology(p->ctx,
+ D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ ID3D11DeviceContext_VSSetShader(p->ctx, p->clear_vs, NULL, 0);
+
+ ID3D11DeviceContext_RSSetViewports(p->ctx, 1, (&(D3D11_VIEWPORT) {
+ .Width = params->w,
+ .Height = params->h,
+ .MinDepth = 0,
+ .MaxDepth = 1,
+ }));
+ ID3D11DeviceContext_RSSetScissorRects(p->ctx, 1, (&(D3D11_RECT) {
+ .left = rc->x0,
+ .top = rc->y0,
+ .right = rc->x1,
+ .bottom = rc->y1,
+ }));
+ ID3D11DeviceContext_PSSetShader(p->ctx, p->clear_ps, NULL, 0);
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, 1, &p->clear_cbuf);
+
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 1, &tex_p->rtv, NULL);
+ ID3D11DeviceContext_OMSetBlendState(p->ctx, NULL, NULL,
+ D3D11_DEFAULT_SAMPLE_MASK);
+
+ ID3D11DeviceContext_Draw(p->ctx, 6, 0);
+
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, 1,
+ &(ID3D11Buffer *){ NULL });
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 0, NULL, NULL);
+}
+
+static void clear(struct ra *ra, struct ra_tex *tex, float color[4],
+ struct mp_rect *rc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *tex_p = tex->priv;
+ struct ra_tex_params *params = &tex->params;
+
+ if (!tex_p->rtv)
+ return;
+
+ if (rc->x0 || rc->y0 || rc->x1 != params->w || rc->y1 != params->h) {
+ if (p->has_clear_view) {
+ ID3D11DeviceContext1_ClearView(p->ctx1, (ID3D11View *)tex_p->rtv,
+ color, (&(D3D11_RECT) {
+ .left = rc->x0,
+ .top = rc->y0,
+ .right = rc->x1,
+ .bottom = rc->y1,
+ }), 1);
+ } else {
+ clear_rpass(ra, tex, color, rc);
+ }
+ } else {
+ ID3D11DeviceContext_ClearRenderTargetView(p->ctx, tex_p->rtv, color);
+ }
+}
+
+static bool setup_blit_rpass(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3DBlob *vs_blob = NULL;
+ ID3DBlob *float_ps_blob = NULL;
+ HRESULT hr;
+
+ hr = p->D3DCompile(blit_vs, sizeof(blit_vs), NULL, NULL, NULL, "main",
+ get_shader_target(ra, GLSL_SHADER_VERTEX),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vs_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile blit() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreateVertexShader(p->dev,
+ ID3D10Blob_GetBufferPointer(vs_blob), ID3D10Blob_GetBufferSize(vs_blob),
+ NULL, &p->blit_vs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = p->D3DCompile(blit_float_ps, sizeof(blit_float_ps), NULL, NULL, NULL,
+ "main", get_shader_target(ra, GLSL_SHADER_FRAGMENT),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &float_ps_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile blit() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreatePixelShader(p->dev,
+ ID3D10Blob_GetBufferPointer(float_ps_blob),
+ ID3D10Blob_GetBufferSize(float_ps_blob),
+ NULL, &p->blit_float_ps);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_INPUT_ELEMENT_DESC in_descs[] = {
+ { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 8 },
+ };
+ hr = ID3D11Device_CreateInputLayout(p->dev, in_descs,
+ MP_ARRAY_SIZE(in_descs), ID3D10Blob_GetBufferPointer(vs_blob),
+ ID3D10Blob_GetBufferSize(vs_blob), &p->blit_layout);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() IA layout: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_BUFFER_DESC vdesc = {
+ .ByteWidth = sizeof(struct blit_vert[6]),
+ .Usage = D3D11_USAGE_DEFAULT,
+ .BindFlags = D3D11_BIND_VERTEX_BUFFER,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &vdesc, NULL, &p->blit_vbuf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() vertex buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // Blit always uses point sampling, regardless of the source texture
+ D3D11_SAMPLER_DESC sdesc = {
+ .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .ComparisonFunc = D3D11_COMPARISON_NEVER,
+ .MinLOD = 0,
+ .MaxLOD = D3D11_FLOAT32_MAX,
+ .MaxAnisotropy = 1,
+ };
+ hr = ID3D11Device_CreateSamplerState(p->dev, &sdesc, &p->blit_sampler);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() sampler: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(float_ps_blob);
+ return true;
+error:
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(float_ps_blob);
+ return false;
+}
+
+static void blit_rpass(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *dst_p = dst->priv;
+ struct d3d_tex *src_p = src->priv;
+
+ float u_min = (double)src_rc->x0 / src->params.w;
+ float u_max = (double)src_rc->x1 / src->params.w;
+ float v_min = (double)src_rc->y0 / src->params.h;
+ float v_max = (double)src_rc->y1 / src->params.h;
+
+ struct blit_vert verts[6] = {
+ { .x = -1, .y = -1, .u = u_min, .v = v_max },
+ { .x = 1, .y = -1, .u = u_max, .v = v_max },
+ { .x = 1, .y = 1, .u = u_max, .v = v_min },
+ { .x = -1, .y = 1, .u = u_min, .v = v_min },
+ };
+ verts[4] = verts[0];
+ verts[5] = verts[2];
+ ID3D11DeviceContext_UpdateSubresource(p->ctx,
+ (ID3D11Resource *)p->blit_vbuf, 0, NULL, verts, 0, 0);
+
+ ID3D11DeviceContext_IASetInputLayout(p->ctx, p->blit_layout);
+ ID3D11DeviceContext_IASetVertexBuffers(p->ctx, 0, 1, &p->blit_vbuf,
+ &(UINT) { sizeof(verts[0]) }, &(UINT) { 0 });
+ ID3D11DeviceContext_IASetPrimitiveTopology(p->ctx,
+ D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ ID3D11DeviceContext_VSSetShader(p->ctx, p->blit_vs, NULL, 0);
+
+ ID3D11DeviceContext_RSSetViewports(p->ctx, 1, (&(D3D11_VIEWPORT) {
+ .TopLeftX = dst_rc->x0,
+ .TopLeftY = dst_rc->y0,
+ .Width = mp_rect_w(*dst_rc),
+ .Height = mp_rect_h(*dst_rc),
+ .MinDepth = 0,
+ .MaxDepth = 1,
+ }));
+ ID3D11DeviceContext_RSSetScissorRects(p->ctx, 1, (&(D3D11_RECT) {
+ .left = dst_rc->x0,
+ .top = dst_rc->y0,
+ .right = dst_rc->x1,
+ .bottom = dst_rc->y1,
+ }));
+
+ ID3D11DeviceContext_PSSetShader(p->ctx, p->blit_float_ps, NULL, 0);
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, 1, &src_p->srv);
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, 1, &p->blit_sampler);
+
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 1, &dst_p->rtv, NULL);
+ ID3D11DeviceContext_OMSetBlendState(p->ctx, NULL, NULL,
+ D3D11_DEFAULT_SAMPLE_MASK);
+
+ ID3D11DeviceContext_Draw(p->ctx, 6, 0);
+
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, 1,
+ &(ID3D11ShaderResourceView *) { NULL });
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, 1,
+ &(ID3D11SamplerState *) { NULL });
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 0, NULL, NULL);
+}
+
+static void blit(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc_ptr, struct mp_rect *src_rc_ptr)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *dst_p = dst->priv;
+ struct d3d_tex *src_p = src->priv;
+ struct mp_rect dst_rc = *dst_rc_ptr;
+ struct mp_rect src_rc = *src_rc_ptr;
+
+ assert(dst->params.dimensions == 2);
+ assert(src->params.dimensions == 2);
+
+ // A zero-sized target rectangle is a no-op
+ if (!mp_rect_w(dst_rc) || !mp_rect_h(dst_rc))
+ return;
+
+ // ra.h seems to imply that both dst_rc and src_rc can be flipped, but it's
+ // easier for blit_rpass() if only src_rc can be flipped, so unflip dst_rc.
+ if (dst_rc.x0 > dst_rc.x1) {
+ MPSWAP(int, dst_rc.x0, dst_rc.x1);
+ MPSWAP(int, src_rc.x0, src_rc.x1);
+ }
+ if (dst_rc.y0 > dst_rc.y1) {
+ MPSWAP(int, dst_rc.y0, dst_rc.y1);
+ MPSWAP(int, src_rc.y0, src_rc.y1);
+ }
+
+ // If format conversion, stretching or flipping is required, a renderpass
+ // must be used
+ if (dst->params.format != src->params.format ||
+ mp_rect_w(dst_rc) != mp_rect_w(src_rc) ||
+ mp_rect_h(dst_rc) != mp_rect_h(src_rc))
+ {
+ blit_rpass(ra, dst, src, &dst_rc, &src_rc);
+ } else {
+ int dst_sr = dst_p->array_slice >= 0 ? dst_p->array_slice : 0;
+ int src_sr = src_p->array_slice >= 0 ? src_p->array_slice : 0;
+ ID3D11DeviceContext_CopySubresourceRegion(p->ctx, dst_p->res, dst_sr,
+ dst_rc.x0, dst_rc.y0, 0, src_p->res, src_sr, (&(D3D11_BOX) {
+ .left = src_rc.x0,
+ .top = src_rc.y0,
+ .front = 0,
+ .right = src_rc.x1,
+ .bottom = src_rc.y1,
+ .back = 1,
+ }));
+ }
+}
+
+static int desc_namespace(struct ra *ra, enum ra_vartype type)
+{
+ // Images and SSBOs both use UAV bindings
+ if (type == RA_VARTYPE_IMG_W)
+ type = RA_VARTYPE_BUF_RW;
+ return type;
+}
+
+static bool compile_glsl(struct ra *ra, enum glsl_shader type,
+ const char *glsl, ID3DBlob **out)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct spirv_compiler *spirv = p->spirv;
+ void *ta_ctx = talloc_new(NULL);
+ spvc_result sc_res = SPVC_SUCCESS;
+ spvc_context sc_ctx = NULL;
+ spvc_parsed_ir sc_ir = NULL;
+ spvc_compiler sc_compiler = NULL;
+ spvc_compiler_options sc_opts = NULL;
+ const char *hlsl = NULL;
+ ID3DBlob *errors = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ int sc_shader_model;
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0) {
+ sc_shader_model = 50;
+ } else if (p->fl >= D3D_FEATURE_LEVEL_10_1) {
+ sc_shader_model = 41;
+ } else {
+ sc_shader_model = 40;
+ }
+
+ int64_t start_ns = mp_time_ns();
+
+ bstr spv_module;
+ if (!spirv->fns->compile_glsl(spirv, ta_ctx, type, glsl, &spv_module))
+ goto done;
+
+ int64_t shaderc_ns = mp_time_ns();
+
+ sc_res = spvc_context_create(&sc_ctx);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_context_parse_spirv(sc_ctx, (SpvId *)spv_module.start,
+ spv_module.len / sizeof(SpvId), &sc_ir);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_context_create_compiler(sc_ctx, SPVC_BACKEND_HLSL, sc_ir,
+ SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
+ &sc_compiler);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_compiler_create_compiler_options(sc_compiler, &sc_opts);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+ sc_res = spvc_compiler_options_set_uint(sc_opts,
+ SPVC_COMPILER_OPTION_HLSL_SHADER_MODEL, sc_shader_model);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+ if (type == GLSL_SHADER_VERTEX) {
+ // FLIP_VERTEX_Y is only valid for vertex shaders
+ sc_res = spvc_compiler_options_set_bool(sc_opts,
+ SPVC_COMPILER_OPTION_FLIP_VERTEX_Y, SPVC_TRUE);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+ }
+ sc_res = spvc_compiler_install_compiler_options(sc_compiler, sc_opts);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_compiler_compile(sc_compiler, &hlsl);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ int64_t cross_ns = mp_time_ns();
+
+ hr = p->D3DCompile(hlsl, strlen(hlsl), NULL, NULL, NULL, "main",
+ get_shader_target(ra, type), D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, out,
+ &errors);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "D3DCompile failed: %s\n%.*s", mp_HRESULT_to_str(hr),
+ (int)ID3D10Blob_GetBufferSize(errors),
+ (char*)ID3D10Blob_GetBufferPointer(errors));
+ goto done;
+ }
+
+ int64_t d3dcompile_ns = mp_time_ns();
+
+ MP_VERBOSE(ra, "Compiled a %s shader in %lldns\n", shader_type_name(type),
+ d3dcompile_ns - start_ns);
+ MP_VERBOSE(ra, "shaderc: %lldns, SPIRV-Cross: %lldns, D3DCompile: %lldns\n",
+ shaderc_ns - start_ns,
+ cross_ns - shaderc_ns,
+ d3dcompile_ns - cross_ns);
+
+ success = true;
+done:
+ if (sc_res != SPVC_SUCCESS) {
+ MP_MSG(ra, MSGL_ERR, "SPIRV-Cross failed: %s\n",
+ spvc_context_get_last_error_string(sc_ctx));
+ }
+ int level = success ? MSGL_DEBUG : MSGL_ERR;
+ MP_MSG(ra, level, "GLSL source:\n");
+ mp_log_source(ra->log, level, glsl);
+ if (hlsl) {
+ MP_MSG(ra, level, "HLSL source:\n");
+ mp_log_source(ra->log, level, hlsl);
+ }
+ SAFE_RELEASE(errors);
+ if (sc_ctx)
+ spvc_context_destroy(sc_ctx);
+ talloc_free(ta_ctx);
+ return success;
+}
+
+static void renderpass_destroy(struct ra *ra, struct ra_renderpass *pass)
+{
+ if (!pass)
+ return;
+ struct d3d_rpass *pass_p = pass->priv;
+
+ SAFE_RELEASE(pass_p->vs);
+ SAFE_RELEASE(pass_p->ps);
+ SAFE_RELEASE(pass_p->cs);
+ SAFE_RELEASE(pass_p->layout);
+ SAFE_RELEASE(pass_p->bstate);
+ talloc_free(pass);
+}
+
+static D3D11_BLEND map_ra_blend(enum ra_blend blend)
+{
+ switch (blend) {
+ default:
+ case RA_BLEND_ZERO: return D3D11_BLEND_ZERO;
+ case RA_BLEND_ONE: return D3D11_BLEND_ONE;
+ case RA_BLEND_SRC_ALPHA: return D3D11_BLEND_SRC_ALPHA;
+ case RA_BLEND_ONE_MINUS_SRC_ALPHA: return D3D11_BLEND_INV_SRC_ALPHA;
+ };
+}
+
+static size_t vbuf_upload(struct ra *ra, void *data, size_t size)
+{
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ // Arbitrary size limit in case there is an insane number of vertices
+ if (size > 1e9) {
+ MP_ERR(ra, "Vertex buffer is too large\n");
+ return -1;
+ }
+
+ // If the vertex data doesn't fit, realloc the vertex buffer
+ if (size > p->vbuf_size) {
+ size_t new_size = p->vbuf_size;
+ // Arbitrary base size
+ if (!new_size)
+ new_size = 64 * 1024;
+ while (new_size < size)
+ new_size *= 2;
+
+ ID3D11Buffer *new_buf;
+ D3D11_BUFFER_DESC vbuf_desc = {
+ .ByteWidth = new_size,
+ .Usage = D3D11_USAGE_DYNAMIC,
+ .BindFlags = D3D11_BIND_VERTEX_BUFFER,
+ .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &vbuf_desc, NULL, &new_buf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create vertex buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ SAFE_RELEASE(p->vbuf);
+ p->vbuf = new_buf;
+ p->vbuf_size = new_size;
+ p->vbuf_used = 0;
+ }
+
+ bool discard = false;
+ size_t offset = p->vbuf_used;
+ if (offset + size > p->vbuf_size) {
+ // We reached the end of the buffer, so discard and wrap around
+ discard = true;
+ offset = 0;
+ }
+
+ D3D11_MAPPED_SUBRESOURCE map = { 0 };
+ hr = ID3D11DeviceContext_Map(p->ctx, (ID3D11Resource *)p->vbuf, 0,
+ discard ? D3D11_MAP_WRITE_DISCARD : D3D11_MAP_WRITE_NO_OVERWRITE,
+ 0, &map);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to map vertex buffer: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ char *cdata = map.pData;
+ memcpy(cdata + offset, data, size);
+
+ ID3D11DeviceContext_Unmap(p->ctx, (ID3D11Resource *)p->vbuf, 0);
+
+ p->vbuf_used = offset + size;
+ return offset;
+}
+
+static const char cache_magic[4] = "RD11";
+static const int cache_version = 3;
+
+struct cache_header {
+ char magic[sizeof(cache_magic)];
+ int cache_version;
+ char compiler[SPIRV_NAME_MAX_LEN];
+ int spv_compiler_version;
+ unsigned spvc_compiler_major;
+ unsigned spvc_compiler_minor;
+ unsigned spvc_compiler_patch;
+ struct dll_version d3d_compiler_version;
+ int feature_level;
+ size_t vert_bytecode_len;
+ size_t frag_bytecode_len;
+ size_t comp_bytecode_len;
+};
+
+static void load_cached_program(struct ra *ra,
+ const struct ra_renderpass_params *params,
+ bstr *vert_bc,
+ bstr *frag_bc,
+ bstr *comp_bc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct spirv_compiler *spirv = p->spirv;
+ bstr cache = params->cached_program;
+
+ if (cache.len < sizeof(struct cache_header))
+ return;
+
+ struct cache_header *header = (struct cache_header *)cache.start;
+ cache = bstr_cut(cache, sizeof(*header));
+
+ unsigned spvc_major, spvc_minor, spvc_patch;
+ spvc_get_version(&spvc_major, &spvc_minor, &spvc_patch);
+
+ if (strncmp(header->magic, cache_magic, sizeof(cache_magic)) != 0)
+ return;
+ if (header->cache_version != cache_version)
+ return;
+ if (strncmp(header->compiler, spirv->name, sizeof(header->compiler)) != 0)
+ return;
+ if (header->spv_compiler_version != spirv->compiler_version)
+ return;
+ if (header->spvc_compiler_major != spvc_major)
+ return;
+ if (header->spvc_compiler_minor != spvc_minor)
+ return;
+ if (header->spvc_compiler_patch != spvc_patch)
+ return;
+ if (!dll_version_equal(header->d3d_compiler_version, p->d3d_compiler_ver))
+ return;
+ if (header->feature_level != p->fl)
+ return;
+
+ if (header->vert_bytecode_len && vert_bc) {
+ *vert_bc = bstr_splice(cache, 0, header->vert_bytecode_len);
+ MP_VERBOSE(ra, "Using cached vertex shader\n");
+ }
+ cache = bstr_cut(cache, header->vert_bytecode_len);
+
+ if (header->frag_bytecode_len && frag_bc) {
+ *frag_bc = bstr_splice(cache, 0, header->frag_bytecode_len);
+ MP_VERBOSE(ra, "Using cached fragment shader\n");
+ }
+ cache = bstr_cut(cache, header->frag_bytecode_len);
+
+ if (header->comp_bytecode_len && comp_bc) {
+ *comp_bc = bstr_splice(cache, 0, header->comp_bytecode_len);
+ MP_VERBOSE(ra, "Using cached compute shader\n");
+ }
+ cache = bstr_cut(cache, header->comp_bytecode_len);
+}
+
+static void save_cached_program(struct ra *ra, struct ra_renderpass *pass,
+ bstr vert_bc,
+ bstr frag_bc,
+ bstr comp_bc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct spirv_compiler *spirv = p->spirv;
+
+ unsigned spvc_major, spvc_minor, spvc_patch;
+ spvc_get_version(&spvc_major, &spvc_minor, &spvc_patch);
+
+ struct cache_header header = {
+ .cache_version = cache_version,
+ .spv_compiler_version = p->spirv->compiler_version,
+ .spvc_compiler_major = spvc_major,
+ .spvc_compiler_minor = spvc_minor,
+ .spvc_compiler_patch = spvc_patch,
+ .d3d_compiler_version = p->d3d_compiler_ver,
+ .feature_level = p->fl,
+ .vert_bytecode_len = vert_bc.len,
+ .frag_bytecode_len = frag_bc.len,
+ .comp_bytecode_len = comp_bc.len,
+ };
+ memcpy(header.magic, cache_magic, sizeof(header.magic));
+ strncpy(header.compiler, spirv->name, sizeof(header.compiler));
+
+ struct bstr *prog = &pass->params.cached_program;
+ bstr_xappend(pass, prog, (bstr){ (char *) &header, sizeof(header) });
+ bstr_xappend(pass, prog, vert_bc);
+ bstr_xappend(pass, prog, frag_bc);
+ bstr_xappend(pass, prog, comp_bc);
+}
+
+static struct ra_renderpass *renderpass_create_raster(struct ra *ra,
+ struct ra_renderpass *pass, const struct ra_renderpass_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_rpass *pass_p = pass->priv;
+ ID3DBlob *vs_blob = NULL;
+ ID3DBlob *ps_blob = NULL;
+ HRESULT hr;
+
+ // load_cached_program will load compiled shader bytecode into vert_bc and
+ // frag_bc if the cache is valid. If not, vert_bc/frag_bc will remain NULL.
+ bstr vert_bc = {0};
+ bstr frag_bc = {0};
+ load_cached_program(ra, params, &vert_bc, &frag_bc, NULL);
+
+ if (!vert_bc.start) {
+ if (!compile_glsl(ra, GLSL_SHADER_VERTEX, params->vertex_shader,
+ &vs_blob))
+ goto error;
+ vert_bc = (bstr){
+ ID3D10Blob_GetBufferPointer(vs_blob),
+ ID3D10Blob_GetBufferSize(vs_blob),
+ };
+ }
+
+ hr = ID3D11Device_CreateVertexShader(p->dev, vert_bc.start, vert_bc.len,
+ NULL, &pass_p->vs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ if (!frag_bc.start) {
+ if (!compile_glsl(ra, GLSL_SHADER_FRAGMENT, params->frag_shader,
+ &ps_blob))
+ goto error;
+ frag_bc = (bstr){
+ ID3D10Blob_GetBufferPointer(ps_blob),
+ ID3D10Blob_GetBufferSize(ps_blob),
+ };
+ }
+
+ hr = ID3D11Device_CreatePixelShader(p->dev, frag_bc.start, frag_bc.len,
+ NULL, &pass_p->ps);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_INPUT_ELEMENT_DESC *in_descs = talloc_array(pass,
+ D3D11_INPUT_ELEMENT_DESC, params->num_vertex_attribs);
+ for (int i = 0; i < params->num_vertex_attribs; i++) {
+ struct ra_renderpass_input *inp = &params->vertex_attribs[i];
+
+ DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
+ switch (inp->type) {
+ case RA_VARTYPE_FLOAT:
+ switch (inp->dim_v) {
+ case 1: fmt = DXGI_FORMAT_R32_FLOAT; break;
+ case 2: fmt = DXGI_FORMAT_R32G32_FLOAT; break;
+ case 3: fmt = DXGI_FORMAT_R32G32B32_FLOAT; break;
+ case 4: fmt = DXGI_FORMAT_R32G32B32A32_FLOAT; break;
+ }
+ break;
+ case RA_VARTYPE_BYTE_UNORM:
+ switch (inp->dim_v) {
+ case 1: fmt = DXGI_FORMAT_R8_UNORM; break;
+ case 2: fmt = DXGI_FORMAT_R8G8_UNORM; break;
+ // There is no 3-component 8-bit DXGI format
+ case 4: fmt = DXGI_FORMAT_R8G8B8A8_UNORM; break;
+ }
+ break;
+ }
+ if (fmt == DXGI_FORMAT_UNKNOWN) {
+ MP_ERR(ra, "Could not find suitable vertex input format\n");
+ goto error;
+ }
+
+ in_descs[i] = (D3D11_INPUT_ELEMENT_DESC) {
+ // The semantic name doesn't mean much and is just used to verify
+ // the input description matches the shader. SPIRV-Cross always
+ // uses TEXCOORD, so we should too.
+ .SemanticName = "TEXCOORD",
+ .SemanticIndex = i,
+ .AlignedByteOffset = inp->offset,
+ .Format = fmt,
+ };
+ }
+
+ hr = ID3D11Device_CreateInputLayout(p->dev, in_descs,
+ params->num_vertex_attribs, vert_bc.start, vert_bc.len,
+ &pass_p->layout);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create IA layout: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ talloc_free(in_descs);
+ in_descs = NULL;
+
+ D3D11_BLEND_DESC bdesc = {
+ .RenderTarget[0] = {
+ .BlendEnable = params->enable_blend,
+ .SrcBlend = map_ra_blend(params->blend_src_rgb),
+ .DestBlend = map_ra_blend(params->blend_dst_rgb),
+ .BlendOp = D3D11_BLEND_OP_ADD,
+ .SrcBlendAlpha = map_ra_blend(params->blend_src_alpha),
+ .DestBlendAlpha = map_ra_blend(params->blend_dst_alpha),
+ .BlendOpAlpha = D3D11_BLEND_OP_ADD,
+ .RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL,
+ },
+ };
+ hr = ID3D11Device_CreateBlendState(p->dev, &bdesc, &pass_p->bstate);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blend state: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ save_cached_program(ra, pass, vert_bc, frag_bc, (bstr){0});
+
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return pass;
+
+error:
+ renderpass_destroy(ra, pass);
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return NULL;
+}
+
+static struct ra_renderpass *renderpass_create_compute(struct ra *ra,
+ struct ra_renderpass *pass, const struct ra_renderpass_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_rpass *pass_p = pass->priv;
+ ID3DBlob *cs_blob = NULL;
+ HRESULT hr;
+
+ bstr comp_bc = {0};
+ load_cached_program(ra, params, NULL, NULL, &comp_bc);
+
+ if (!comp_bc.start) {
+ if (!compile_glsl(ra, GLSL_SHADER_COMPUTE, params->compute_shader,
+ &cs_blob))
+ goto error;
+ comp_bc = (bstr){
+ ID3D10Blob_GetBufferPointer(cs_blob),
+ ID3D10Blob_GetBufferSize(cs_blob),
+ };
+ }
+ hr = ID3D11Device_CreateComputeShader(p->dev, comp_bc.start, comp_bc.len,
+ NULL, &pass_p->cs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create compute shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ save_cached_program(ra, pass, (bstr){0}, (bstr){0}, comp_bc);
+
+ SAFE_RELEASE(cs_blob);
+ return pass;
+error:
+ renderpass_destroy(ra, pass);
+ SAFE_RELEASE(cs_blob);
+ return NULL;
+}
+
+static struct ra_renderpass *renderpass_create(struct ra *ra,
+ const struct ra_renderpass_params *params)
+{
+ struct ra_renderpass *pass = talloc_zero(NULL, struct ra_renderpass);
+ pass->params = *ra_renderpass_params_copy(pass, params);
+ pass->params.cached_program = (bstr){0};
+ pass->priv = talloc_zero(pass, struct d3d_rpass);
+
+ if (params->type == RA_RENDERPASS_TYPE_COMPUTE) {
+ return renderpass_create_compute(ra, pass, params);
+ } else {
+ return renderpass_create_raster(ra, pass, params);
+ }
+}
+
+static void renderpass_run_raster(struct ra *ra,
+ const struct ra_renderpass_run_params *params,
+ ID3D11Buffer *ubos[], int ubos_len,
+ ID3D11SamplerState *samplers[],
+ ID3D11ShaderResourceView *srvs[],
+ int samplers_len,
+ ID3D11UnorderedAccessView *uavs[],
+ int uavs_len)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_renderpass *pass = params->pass;
+ struct d3d_rpass *pass_p = pass->priv;
+
+ UINT vbuf_offset = vbuf_upload(ra, params->vertex_data,
+ pass->params.vertex_stride * params->vertex_count);
+ if (vbuf_offset == (UINT)-1)
+ return;
+
+ ID3D11DeviceContext_IASetInputLayout(p->ctx, pass_p->layout);
+ ID3D11DeviceContext_IASetVertexBuffers(p->ctx, 0, 1, &p->vbuf,
+ &pass->params.vertex_stride, &vbuf_offset);
+ ID3D11DeviceContext_IASetPrimitiveTopology(p->ctx,
+ D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ ID3D11DeviceContext_VSSetShader(p->ctx, pass_p->vs, NULL, 0);
+
+ ID3D11DeviceContext_RSSetViewports(p->ctx, 1, (&(D3D11_VIEWPORT) {
+ .TopLeftX = params->viewport.x0,
+ .TopLeftY = params->viewport.y0,
+ .Width = mp_rect_w(params->viewport),
+ .Height = mp_rect_h(params->viewport),
+ .MinDepth = 0,
+ .MaxDepth = 1,
+ }));
+ ID3D11DeviceContext_RSSetScissorRects(p->ctx, 1, (&(D3D11_RECT) {
+ .left = params->scissors.x0,
+ .top = params->scissors.y0,
+ .right = params->scissors.x1,
+ .bottom = params->scissors.y1,
+ }));
+ ID3D11DeviceContext_PSSetShader(p->ctx, pass_p->ps, NULL, 0);
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, samplers_len, samplers);
+
+ struct ra_tex *target = params->target;
+ struct d3d_tex *target_p = target->priv;
+ ID3D11DeviceContext_OMSetRenderTargetsAndUnorderedAccessViews(p->ctx, 1,
+ &target_p->rtv, NULL, 1, uavs_len, uavs, NULL);
+ ID3D11DeviceContext_OMSetBlendState(p->ctx, pass_p->bstate, NULL,
+ D3D11_DEFAULT_SAMPLE_MASK);
+
+ ID3D11DeviceContext_Draw(p->ctx, params->vertex_count, 0);
+
+ // Unbind everything. It's easier to do this than to actually track state,
+ // and if we leave the RTV bound, it could trip up D3D's conflict checker.
+ for (int i = 0; i < ubos_len; i++)
+ ubos[i] = NULL;
+ for (int i = 0; i < samplers_len; i++) {
+ samplers[i] = NULL;
+ srvs[i] = NULL;
+ }
+ for (int i = 0; i < uavs_len; i++)
+ uavs[i] = NULL;
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, samplers_len, samplers);
+ ID3D11DeviceContext_OMSetRenderTargetsAndUnorderedAccessViews(p->ctx, 0,
+ NULL, NULL, 1, uavs_len, uavs, NULL);
+}
+
+static void renderpass_run_compute(struct ra *ra,
+ const struct ra_renderpass_run_params *params,
+ ID3D11Buffer *ubos[], int ubos_len,
+ ID3D11SamplerState *samplers[],
+ ID3D11ShaderResourceView *srvs[],
+ int samplers_len,
+ ID3D11UnorderedAccessView *uavs[],
+ int uavs_len)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_renderpass *pass = params->pass;
+ struct d3d_rpass *pass_p = pass->priv;
+
+ ID3D11DeviceContext_CSSetShader(p->ctx, pass_p->cs, NULL, 0);
+ ID3D11DeviceContext_CSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_CSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_CSSetSamplers(p->ctx, 0, samplers_len, samplers);
+ ID3D11DeviceContext_CSSetUnorderedAccessViews(p->ctx, 0, uavs_len, uavs,
+ NULL);
+
+ ID3D11DeviceContext_Dispatch(p->ctx, params->compute_groups[0],
+ params->compute_groups[1],
+ params->compute_groups[2]);
+
+ for (int i = 0; i < ubos_len; i++)
+ ubos[i] = NULL;
+ for (int i = 0; i < samplers_len; i++) {
+ samplers[i] = NULL;
+ srvs[i] = NULL;
+ }
+ for (int i = 0; i < uavs_len; i++)
+ uavs[i] = NULL;
+ ID3D11DeviceContext_CSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_CSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_CSSetSamplers(p->ctx, 0, samplers_len, samplers);
+ ID3D11DeviceContext_CSSetUnorderedAccessViews(p->ctx, 0, uavs_len, uavs,
+ NULL);
+}
+
+static void renderpass_run(struct ra *ra,
+ const struct ra_renderpass_run_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_renderpass *pass = params->pass;
+ enum ra_renderpass_type type = pass->params.type;
+
+ ID3D11Buffer *ubos[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT] = {0};
+ int ubos_len = 0;
+
+ ID3D11SamplerState *samplers[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {0};
+ ID3D11ShaderResourceView *srvs[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {0};
+ int samplers_len = 0;
+
+ ID3D11UnorderedAccessView *uavs[D3D11_1_UAV_SLOT_COUNT] = {0};
+ int uavs_len = 0;
+
+ // In a raster pass, one of the UAV slots is used by the runtime for the RTV
+ int uavs_max = type == RA_RENDERPASS_TYPE_COMPUTE ? p->max_uavs
+ : p->max_uavs - 1;
+
+ // Gather the input variables used in this pass. These will be mapped to
+ // HLSL registers.
+ for (int i = 0; i < params->num_values; i++) {
+ struct ra_renderpass_input_val *val = &params->values[i];
+ int binding = pass->params.inputs[val->index].binding;
+ switch (pass->params.inputs[val->index].type) {
+ case RA_VARTYPE_BUF_RO:
+ if (binding >= MP_ARRAY_SIZE(ubos)) {
+ MP_ERR(ra, "Too many constant buffers in pass\n");
+ return;
+ }
+ struct ra_buf *buf_ro = *(struct ra_buf **)val->data;
+ buf_resolve(ra, buf_ro);
+ struct d3d_buf *buf_ro_p = buf_ro->priv;
+ ubos[binding] = buf_ro_p->buf;
+ ubos_len = MPMAX(ubos_len, binding + 1);
+ break;
+ case RA_VARTYPE_BUF_RW:
+ if (binding > uavs_max) {
+ MP_ERR(ra, "Too many UAVs in pass\n");
+ return;
+ }
+ struct ra_buf *buf_rw = *(struct ra_buf **)val->data;
+ buf_resolve(ra, buf_rw);
+ struct d3d_buf *buf_rw_p = buf_rw->priv;
+ uavs[binding] = buf_rw_p->uav;
+ uavs_len = MPMAX(uavs_len, binding + 1);
+ break;
+ case RA_VARTYPE_TEX:
+ if (binding >= MP_ARRAY_SIZE(samplers)) {
+ MP_ERR(ra, "Too many textures in pass\n");
+ return;
+ }
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct d3d_tex *tex_p = tex->priv;
+ samplers[binding] = tex_p->sampler;
+ srvs[binding] = tex_p->srv;
+ samplers_len = MPMAX(samplers_len, binding + 1);
+ break;
+ case RA_VARTYPE_IMG_W:
+ if (binding > uavs_max) {
+ MP_ERR(ra, "Too many UAVs in pass\n");
+ return;
+ }
+ struct ra_tex *img = *(struct ra_tex **)val->data;
+ struct d3d_tex *img_p = img->priv;
+ uavs[binding] = img_p->uav;
+ uavs_len = MPMAX(uavs_len, binding + 1);
+ break;
+ }
+ }
+
+ if (type == RA_RENDERPASS_TYPE_COMPUTE) {
+ renderpass_run_compute(ra, params, ubos, ubos_len, samplers, srvs,
+ samplers_len, uavs, uavs_len);
+ } else {
+ renderpass_run_raster(ra, params, ubos, ubos_len, samplers, srvs,
+ samplers_len, uavs, uavs_len);
+ }
+}
+
+static void timer_destroy(struct ra *ra, ra_timer *ratimer)
+{
+ if (!ratimer)
+ return;
+ struct d3d_timer *timer = ratimer;
+
+ SAFE_RELEASE(timer->ts_start);
+ SAFE_RELEASE(timer->ts_end);
+ SAFE_RELEASE(timer->disjoint);
+ talloc_free(timer);
+}
+
+static ra_timer *timer_create(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ if (!p->has_timestamp_queries)
+ return NULL;
+
+ struct d3d_timer *timer = talloc_zero(NULL, struct d3d_timer);
+ HRESULT hr;
+
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP }, &timer->ts_start);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create start query: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP }, &timer->ts_end);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create end query: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // Measuring duration in D3D11 requires three queries: start and end
+ // timestamps, and a disjoint query containing a flag which says whether
+ // the timestamps are usable or if a discontinuity occurred between them,
+ // like a change in power state or clock speed. The disjoint query also
+ // contains the timer frequency, so the timestamps are useless without it.
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP_DISJOINT }, &timer->disjoint);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create timer query: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ return timer;
+error:
+ timer_destroy(ra, timer);
+ return NULL;
+}
+
+static uint64_t timestamp_to_ns(uint64_t timestamp, uint64_t freq)
+{
+ static const uint64_t ns_per_s = 1000000000llu;
+ return timestamp / freq * ns_per_s + timestamp % freq * ns_per_s / freq;
+}
+
+static uint64_t timer_get_result(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_timer *timer = ratimer;
+ HRESULT hr;
+
+ UINT64 start, end;
+ D3D11_QUERY_DATA_TIMESTAMP_DISJOINT dj;
+
+ hr = ID3D11DeviceContext_GetData(p->ctx,
+ (ID3D11Asynchronous *)timer->ts_end, &end, sizeof(end),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE)
+ return 0;
+ hr = ID3D11DeviceContext_GetData(p->ctx,
+ (ID3D11Asynchronous *)timer->ts_start, &start, sizeof(start),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE)
+ return 0;
+ hr = ID3D11DeviceContext_GetData(p->ctx,
+ (ID3D11Asynchronous *)timer->disjoint, &dj, sizeof(dj),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE || dj.Disjoint || !dj.Frequency)
+ return 0;
+
+ return timestamp_to_ns(end - start, dj.Frequency);
+}
+
+static void timer_start(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_timer *timer = ratimer;
+
+ // Latch the last result of this ra_timer (returned by timer_stop)
+ timer->result = timer_get_result(ra, ratimer);
+
+ ID3D11DeviceContext_Begin(p->ctx, (ID3D11Asynchronous *)timer->disjoint);
+ ID3D11DeviceContext_End(p->ctx, (ID3D11Asynchronous *)timer->ts_start);
+}
+
+static uint64_t timer_stop(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_timer *timer = ratimer;
+
+ ID3D11DeviceContext_End(p->ctx, (ID3D11Asynchronous *)timer->ts_end);
+ ID3D11DeviceContext_End(p->ctx, (ID3D11Asynchronous *)timer->disjoint);
+
+ return timer->result;
+}
+
+static int map_msg_severity(D3D11_MESSAGE_SEVERITY sev)
+{
+ switch (sev) {
+ case D3D11_MESSAGE_SEVERITY_CORRUPTION:
+ return MSGL_FATAL;
+ case D3D11_MESSAGE_SEVERITY_ERROR:
+ return MSGL_ERR;
+ case D3D11_MESSAGE_SEVERITY_WARNING:
+ return MSGL_WARN;
+ default:
+ case D3D11_MESSAGE_SEVERITY_INFO:
+ case D3D11_MESSAGE_SEVERITY_MESSAGE:
+ return MSGL_DEBUG;
+ }
+}
+
+static int map_msg_severity_by_id(D3D11_MESSAGE_ID id,
+ D3D11_MESSAGE_SEVERITY sev)
+{
+ switch (id) {
+ // These are normal. The RA timer queue habitually reuses timer objects
+ // without retrieving the results.
+ case D3D11_MESSAGE_ID_QUERY_BEGIN_ABANDONING_PREVIOUS_RESULTS:
+ case D3D11_MESSAGE_ID_QUERY_END_ABANDONING_PREVIOUS_RESULTS:
+ return MSGL_TRACE;
+
+ // D3D11 writes log messages every time an object is created or
+ // destroyed. That results in a lot of log spam, so force MSGL_TRACE.
+#define OBJ_LIFETIME_MESSAGES(obj) \
+ case D3D11_MESSAGE_ID_CREATE_ ## obj: \
+ case D3D11_MESSAGE_ID_DESTROY_ ## obj
+
+ OBJ_LIFETIME_MESSAGES(CONTEXT):
+ OBJ_LIFETIME_MESSAGES(BUFFER):
+ OBJ_LIFETIME_MESSAGES(TEXTURE1D):
+ OBJ_LIFETIME_MESSAGES(TEXTURE2D):
+ OBJ_LIFETIME_MESSAGES(TEXTURE3D):
+ OBJ_LIFETIME_MESSAGES(SHADERRESOURCEVIEW):
+ OBJ_LIFETIME_MESSAGES(RENDERTARGETVIEW):
+ OBJ_LIFETIME_MESSAGES(DEPTHSTENCILVIEW):
+ OBJ_LIFETIME_MESSAGES(VERTEXSHADER):
+ OBJ_LIFETIME_MESSAGES(HULLSHADER):
+ OBJ_LIFETIME_MESSAGES(DOMAINSHADER):
+ OBJ_LIFETIME_MESSAGES(GEOMETRYSHADER):
+ OBJ_LIFETIME_MESSAGES(PIXELSHADER):
+ OBJ_LIFETIME_MESSAGES(INPUTLAYOUT):
+ OBJ_LIFETIME_MESSAGES(SAMPLER):
+ OBJ_LIFETIME_MESSAGES(BLENDSTATE):
+ OBJ_LIFETIME_MESSAGES(DEPTHSTENCILSTATE):
+ OBJ_LIFETIME_MESSAGES(RASTERIZERSTATE):
+ OBJ_LIFETIME_MESSAGES(QUERY):
+ OBJ_LIFETIME_MESSAGES(PREDICATE):
+ OBJ_LIFETIME_MESSAGES(COUNTER):
+ OBJ_LIFETIME_MESSAGES(COMMANDLIST):
+ OBJ_LIFETIME_MESSAGES(CLASSINSTANCE):
+ OBJ_LIFETIME_MESSAGES(CLASSLINKAGE):
+ OBJ_LIFETIME_MESSAGES(COMPUTESHADER):
+ OBJ_LIFETIME_MESSAGES(UNORDEREDACCESSVIEW):
+ OBJ_LIFETIME_MESSAGES(VIDEODECODER):
+ OBJ_LIFETIME_MESSAGES(VIDEOPROCESSORENUM):
+ OBJ_LIFETIME_MESSAGES(VIDEOPROCESSOR):
+ OBJ_LIFETIME_MESSAGES(DECODEROUTPUTVIEW):
+ OBJ_LIFETIME_MESSAGES(PROCESSORINPUTVIEW):
+ OBJ_LIFETIME_MESSAGES(PROCESSOROUTPUTVIEW):
+ OBJ_LIFETIME_MESSAGES(DEVICECONTEXTSTATE):
+ OBJ_LIFETIME_MESSAGES(FENCE):
+ return MSGL_TRACE;
+
+#undef OBJ_LIFETIME_MESSAGES
+
+ default:
+ return map_msg_severity(sev);
+ }
+}
+
+static void debug_marker(struct ra *ra, const char *msg)
+{
+ struct ra_d3d11 *p = ra->priv;
+ void *talloc_ctx = talloc_new(NULL);
+ HRESULT hr;
+
+ if (!p->iqueue)
+ goto done;
+
+ // Copy debug-layer messages to mpv's log output
+ bool printed_header = false;
+ uint64_t messages = ID3D11InfoQueue_GetNumStoredMessages(p->iqueue);
+ for (uint64_t i = 0; i < messages; i++) {
+ SIZE_T len;
+ hr = ID3D11InfoQueue_GetMessage(p->iqueue, i, NULL, &len);
+ if (FAILED(hr) || !len)
+ goto done;
+
+ D3D11_MESSAGE *d3dmsg = talloc_size(talloc_ctx, len);
+ hr = ID3D11InfoQueue_GetMessage(p->iqueue, i, d3dmsg, &len);
+ if (FAILED(hr))
+ goto done;
+
+ int msgl = map_msg_severity_by_id(d3dmsg->ID, d3dmsg->Severity);
+ if (mp_msg_test(ra->log, msgl)) {
+ if (!printed_header)
+ MP_INFO(ra, "%s:\n", msg);
+ printed_header = true;
+
+ MP_MSG(ra, msgl, "%d: %.*s\n", (int)d3dmsg->ID,
+ (int)d3dmsg->DescriptionByteLength, d3dmsg->pDescription);
+ talloc_free(d3dmsg);
+ }
+ }
+
+ ID3D11InfoQueue_ClearStoredMessages(p->iqueue);
+done:
+ talloc_free(talloc_ctx);
+}
+
+static void destroy(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+
+ // Release everything except the interfaces needed to perform leak checking
+ SAFE_RELEASE(p->clear_ps);
+ SAFE_RELEASE(p->clear_vs);
+ SAFE_RELEASE(p->clear_layout);
+ SAFE_RELEASE(p->clear_vbuf);
+ SAFE_RELEASE(p->clear_cbuf);
+ SAFE_RELEASE(p->blit_float_ps);
+ SAFE_RELEASE(p->blit_vs);
+ SAFE_RELEASE(p->blit_layout);
+ SAFE_RELEASE(p->blit_vbuf);
+ SAFE_RELEASE(p->blit_sampler);
+ SAFE_RELEASE(p->vbuf);
+ SAFE_RELEASE(p->ctx1);
+ SAFE_RELEASE(p->dev1);
+ SAFE_RELEASE(p->dev);
+
+ if (p->ctx) {
+ // Destroy the device context synchronously so referenced objects don't
+ // show up in the leak check
+ ID3D11DeviceContext_ClearState(p->ctx);
+ ID3D11DeviceContext_Flush(p->ctx);
+ }
+ SAFE_RELEASE(p->ctx);
+
+ if (p->debug) {
+ // Report any leaked objects
+ debug_marker(ra, "after destroy");
+ ID3D11Debug_ReportLiveDeviceObjects(p->debug, D3D11_RLDO_DETAIL);
+ debug_marker(ra, "after leak check");
+ ID3D11Debug_ReportLiveDeviceObjects(p->debug, D3D11_RLDO_SUMMARY);
+ debug_marker(ra, "after leak summary");
+ }
+ SAFE_RELEASE(p->debug);
+ SAFE_RELEASE(p->iqueue);
+
+ talloc_free(ra);
+}
+
+static struct ra_fns ra_fns_d3d11 = {
+ .destroy = destroy,
+ .tex_create = tex_create,
+ .tex_destroy = tex_destroy,
+ .tex_upload = tex_upload,
+ .tex_download = tex_download,
+ .buf_create = buf_create,
+ .buf_destroy = buf_destroy,
+ .buf_update = buf_update,
+ .clear = clear,
+ .blit = blit,
+ .uniform_layout = std140_layout,
+ .desc_namespace = desc_namespace,
+ .renderpass_create = renderpass_create,
+ .renderpass_destroy = renderpass_destroy,
+ .renderpass_run = renderpass_run,
+ .timer_create = timer_create,
+ .timer_destroy = timer_destroy,
+ .timer_start = timer_start,
+ .timer_stop = timer_stop,
+ .debug_marker = debug_marker,
+};
+
+void ra_d3d11_flush(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3D11DeviceContext_Flush(p->ctx);
+}
+
+static void init_debug_layer(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ hr = ID3D11Device_QueryInterface(p->dev, &IID_ID3D11Debug,
+ (void**)&p->debug);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to get debug device: %s\n", mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ hr = ID3D11Device_QueryInterface(p->dev, &IID_ID3D11InfoQueue,
+ (void**)&p->iqueue);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to get info queue: %s\n", mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ // Store an unlimited amount of messages in the buffer. This is fine
+ // because we flush stored messages regularly (in debug_marker.)
+ ID3D11InfoQueue_SetMessageCountLimit(p->iqueue, -1);
+
+ // Push empty filter to get everything
+ D3D11_INFO_QUEUE_FILTER filter = {0};
+ ID3D11InfoQueue_PushStorageFilter(p->iqueue, &filter);
+}
+
+static struct dll_version get_dll_version(HMODULE dll)
+{
+ void *ctx = talloc_new(NULL);
+ struct dll_version ret = { 0 };
+
+ HRSRC rsrc = FindResourceW(dll, MAKEINTRESOURCEW(VS_VERSION_INFO),
+ VS_FILE_INFO);
+ if (!rsrc)
+ goto done;
+ DWORD size = SizeofResource(dll, rsrc);
+ HGLOBAL res = LoadResource(dll, rsrc);
+ if (!res)
+ goto done;
+ void *ptr = LockResource(res);
+ if (!ptr)
+ goto done;
+ void *copy = talloc_memdup(ctx, ptr, size);
+
+ VS_FIXEDFILEINFO *ffi;
+ UINT ffi_len;
+ if (!VerQueryValueW(copy, L"\\", (void**)&ffi, &ffi_len))
+ goto done;
+ if (ffi_len < sizeof(*ffi))
+ goto done;
+
+ ret.major = HIWORD(ffi->dwFileVersionMS);
+ ret.minor = LOWORD(ffi->dwFileVersionMS);
+ ret.build = HIWORD(ffi->dwFileVersionLS);
+ ret.revision = LOWORD(ffi->dwFileVersionLS);
+
+done:
+ talloc_free(ctx);
+ return ret;
+}
+
+static bool load_d3d_compiler(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ HMODULE d3dcompiler = NULL;
+
+ // Try the inbox D3DCompiler first (Windows 8.1 and up)
+ if (IsWindows8Point1OrGreater()) {
+ d3dcompiler = LoadLibraryExW(L"d3dcompiler_47.dll", NULL,
+ LOAD_LIBRARY_SEARCH_SYSTEM32);
+ }
+ // Check for a packaged version of d3dcompiler_47.dll
+ if (!d3dcompiler)
+ d3dcompiler = LoadLibraryW(L"d3dcompiler_47.dll");
+ // Try d3dcompiler_46.dll from the Windows 8 SDK
+ if (!d3dcompiler)
+ d3dcompiler = LoadLibraryW(L"d3dcompiler_46.dll");
+ // Try d3dcompiler_43.dll from the June 2010 DirectX SDK
+ if (!d3dcompiler)
+ d3dcompiler = LoadLibraryW(L"d3dcompiler_43.dll");
+ // Can't find any compiler DLL, so give up
+ if (!d3dcompiler)
+ return false;
+
+ p->d3d_compiler_ver = get_dll_version(d3dcompiler);
+
+ p->D3DCompile = (pD3DCompile)GetProcAddress(d3dcompiler, "D3DCompile");
+ if (!p->D3DCompile)
+ return false;
+ return true;
+}
+
+static void find_max_texture_dimension(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+
+ D3D11_TEXTURE2D_DESC desc = {
+ .Width = ra->max_texture_wh,
+ .Height = ra->max_texture_wh,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc.Count = 1,
+ .Format = DXGI_FORMAT_R8_UNORM,
+ .BindFlags = D3D11_BIND_SHADER_RESOURCE,
+ };
+ while (true) {
+ desc.Height = desc.Width *= 2;
+ if (desc.Width >= 0x8000000u)
+ return;
+ if (FAILED(ID3D11Device_CreateTexture2D(p->dev, &desc, NULL, NULL)))
+ return;
+ ra->max_texture_wh = desc.Width;
+ }
+}
+
+struct ra *ra_d3d11_create(ID3D11Device *dev, struct mp_log *log,
+ struct spirv_compiler *spirv)
+{
+ HRESULT hr;
+
+ struct ra *ra = talloc_zero(NULL, struct ra);
+ ra->log = log;
+ ra->fns = &ra_fns_d3d11;
+
+ // Even Direct3D 10level9 supports 3D textures
+ ra->caps = RA_CAP_TEX_3D | RA_CAP_DIRECT_UPLOAD | RA_CAP_BUF_RO |
+ RA_CAP_BLIT | spirv->ra_caps;
+
+ ra->glsl_version = spirv->glsl_version;
+ ra->glsl_vulkan = true;
+
+ struct ra_d3d11 *p = ra->priv = talloc_zero(ra, struct ra_d3d11);
+ p->spirv = spirv;
+
+ int minor = 0;
+ ID3D11Device_AddRef(dev);
+ p->dev = dev;
+ ID3D11Device_GetImmediateContext(p->dev, &p->ctx);
+ hr = ID3D11Device_QueryInterface(p->dev, &IID_ID3D11Device1,
+ (void**)&p->dev1);
+ if (SUCCEEDED(hr)) {
+ minor = 1;
+ ID3D11Device1_GetImmediateContext1(p->dev1, &p->ctx1);
+
+ D3D11_FEATURE_DATA_D3D11_OPTIONS fopts = { 0 };
+ hr = ID3D11Device_CheckFeatureSupport(p->dev,
+ D3D11_FEATURE_D3D11_OPTIONS, &fopts, sizeof(fopts));
+ if (SUCCEEDED(hr)) {
+ p->has_clear_view = fopts.ClearView;
+ }
+ }
+
+ MP_VERBOSE(ra, "Using Direct3D 11.%d runtime\n", minor);
+
+ p->fl = ID3D11Device_GetFeatureLevel(p->dev);
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0) {
+ ra->max_texture_wh = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ } else if (p->fl >= D3D_FEATURE_LEVEL_10_0) {
+ ra->max_texture_wh = D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ } else if (p->fl >= D3D_FEATURE_LEVEL_9_3) {
+ ra->max_texture_wh = D3D_FL9_3_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ } else {
+ ra->max_texture_wh = D3D_FL9_1_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ }
+
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0)
+ ra->caps |= RA_CAP_GATHER;
+ if (p->fl >= D3D_FEATURE_LEVEL_10_0)
+ ra->caps |= RA_CAP_FRAGCOORD;
+
+ // Some 10_0 hardware has compute shaders, but only 11_0 has image load/store
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0) {
+ ra->caps |= RA_CAP_COMPUTE | RA_CAP_BUF_RW;
+ ra->max_shmem = 32 * 1024;
+ ra->max_compute_group_threads =
+ D3D11_CS_THREAD_GROUP_MAX_THREADS_PER_GROUP;
+ }
+
+ if (p->fl >= D3D_FEATURE_LEVEL_11_1) {
+ p->max_uavs = D3D11_1_UAV_SLOT_COUNT;
+ } else {
+ p->max_uavs = D3D11_PS_CS_UAV_REGISTER_COUNT;
+ }
+
+ if (ID3D11Device_GetCreationFlags(p->dev) & D3D11_CREATE_DEVICE_DEBUG)
+ init_debug_layer(ra);
+
+ // Some level 9_x devices don't have timestamp queries
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP }, NULL);
+ p->has_timestamp_queries = SUCCEEDED(hr);
+
+ debug_marker(ra, "before maximum Texture2D size lookup");
+
+ // According to MSDN, the above texture sizes are just minimums and drivers
+ // may support larger textures. See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ff476874.aspx
+ find_max_texture_dimension(ra);
+
+ // Ignore any messages during find_max_texture_dimension
+ if (p->iqueue)
+ ID3D11InfoQueue_ClearStoredMessages(p->iqueue);
+
+ MP_VERBOSE(ra, "Maximum Texture2D size: %dx%d\n", ra->max_texture_wh,
+ ra->max_texture_wh);
+
+ if (!load_d3d_compiler(ra)) {
+ MP_FATAL(ra, "Could not find D3DCompiler DLL\n");
+ goto error;
+ }
+
+ MP_VERBOSE(ra, "D3DCompiler version: %u.%u.%u.%u\n",
+ p->d3d_compiler_ver.major, p->d3d_compiler_ver.minor,
+ p->d3d_compiler_ver.build, p->d3d_compiler_ver.revision);
+
+ setup_formats(ra);
+
+ // The rasterizer state never changes, so set it up here
+ ID3D11RasterizerState *rstate;
+ D3D11_RASTERIZER_DESC rdesc = {
+ .FillMode = D3D11_FILL_SOLID,
+ .CullMode = D3D11_CULL_NONE,
+ .FrontCounterClockwise = FALSE,
+ .DepthClipEnable = TRUE, // Required for 10level9
+ .ScissorEnable = TRUE,
+ };
+ hr = ID3D11Device_CreateRasterizerState(p->dev, &rdesc, &rstate);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create rasterizer state: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ ID3D11DeviceContext_RSSetState(p->ctx, rstate);
+ SAFE_RELEASE(rstate);
+
+ // If the device doesn't support ClearView, we have to set up a
+ // shader-based clear() implementation
+ if (!p->has_clear_view && !setup_clear_rpass(ra))
+ goto error;
+
+ if (!setup_blit_rpass(ra))
+ goto error;
+
+ return ra;
+
+error:
+ destroy(ra);
+ return NULL;
+}
+
+ID3D11Device *ra_d3d11_get_device(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3D11Device_AddRef(p->dev);
+ return p->dev;
+}
+
+bool ra_is_d3d11(struct ra *ra)
+{
+ return ra->fns == &ra_fns_d3d11;
+}
diff --git a/video/out/d3d11/ra_d3d11.h b/video/out/d3d11/ra_d3d11.h
new file mode 100644
index 0000000..6f62a7f
--- /dev/null
+++ b/video/out/d3d11/ra_d3d11.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <stdbool.h>
+#include <windows.h>
+#include <d3d11.h>
+#include <dxgi1_2.h>
+
+#include "video/out/gpu/ra.h"
+#include "video/out/gpu/spirv.h"
+
+// Get the underlying DXGI format from an RA format
+DXGI_FORMAT ra_d3d11_get_format(const struct ra_format *fmt);
+
+// Gets the matching ra_format for a given DXGI format.
+// Returns a nullptr in case of no known match.
+const struct ra_format *ra_d3d11_get_ra_format(struct ra *ra, DXGI_FORMAT fmt);
+
+// Create an RA instance from a D3D11 device. This takes a reference to the
+// device, which is released when the RA instance is destroyed.
+struct ra *ra_d3d11_create(ID3D11Device *device, struct mp_log *log,
+ struct spirv_compiler *spirv);
+
+// Flush the immediate context of the wrapped D3D11 device
+void ra_d3d11_flush(struct ra *ra);
+
+// Create an RA texture from a D3D11 resource. This takes a reference to the
+// texture, which is released when the RA texture is destroyed.
+struct ra_tex *ra_d3d11_wrap_tex(struct ra *ra, ID3D11Resource *res);
+
+// As above, but for a D3D11VA video resource. The fmt parameter selects which
+// plane of a planar format will be mapped when the RA texture is used.
+// array_slice should be set for texture arrays and is ignored for non-arrays.
+struct ra_tex *ra_d3d11_wrap_tex_video(struct ra *ra, ID3D11Texture2D *res,
+ int w, int h, int array_slice,
+ const struct ra_format *fmt);
+
+// Get the underlying D3D11 resource from an RA texture. The returned resource
+// is refcounted and must be released by the caller.
+ID3D11Resource *ra_d3d11_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ int *array_slice);
+
+// Get the underlying D3D11 device from an RA instance. The returned device is
+// refcounted and must be released by the caller.
+ID3D11Device *ra_d3d11_get_device(struct ra *ra);
+
+// True if the RA instance was created with ra_d3d11_create()
+bool ra_is_d3d11(struct ra *ra);
diff --git a/video/out/dither.c b/video/out/dither.c
new file mode 100644
index 0000000..44558ba
--- /dev/null
+++ b/video/out/dither.c
@@ -0,0 +1,175 @@
+/*
+ * Generate a dithering matrix for downsampling images.
+ *
+ * Copyright © 2013 Wessel Dankers <wsl@fruit.je>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <string.h>
+#include <assert.h>
+#include <math.h>
+
+#include <libavutil/lfg.h>
+
+#include "mpv_talloc.h"
+#include "dither.h"
+
+#define MAX_SIZEB 8
+#define MAX_SIZE (1 << MAX_SIZEB)
+#define MAX_SIZE2 (MAX_SIZE * MAX_SIZE)
+
+#define WRAP_SIZE2(k, x) ((unsigned int)((unsigned int)(x) & ((k)->size2 - 1)))
+#define XY(k, x, y) ((unsigned int)(((x) | ((y) << (k)->sizeb))))
+
+struct ctx {
+ unsigned int sizeb, size, size2;
+ unsigned int gauss_radius;
+ unsigned int gauss_middle;
+ uint64_t gauss[MAX_SIZE2];
+ unsigned int randomat[MAX_SIZE2];
+ bool calcmat[MAX_SIZE2];
+ uint64_t gaussmat[MAX_SIZE2];
+ unsigned int unimat[MAX_SIZE2];
+ AVLFG avlfg;
+};
+
+static void makegauss(struct ctx *k, unsigned int sizeb)
+{
+ assert(sizeb >= 1 && sizeb <= MAX_SIZEB);
+
+ av_lfg_init(&k->avlfg, 123);
+
+ k->sizeb = sizeb;
+ k->size = 1 << k->sizeb;
+ k->size2 = k->size * k->size;
+
+ k->gauss_radius = k->size / 2 - 1;
+ k->gauss_middle = XY(k, k->gauss_radius, k->gauss_radius);
+
+ unsigned int gauss_size = k->gauss_radius * 2 + 1;
+ unsigned int gauss_size2 = gauss_size * gauss_size;
+
+ for (unsigned int c = 0; c < k->size2; c++)
+ k->gauss[c] = 0;
+
+ double sigma = -log(1.5 / (double) UINT64_MAX * gauss_size2) / k->gauss_radius;
+
+ for (unsigned int gy = 0; gy <= k->gauss_radius; gy++) {
+ for (unsigned int gx = 0; gx <= gy; gx++) {
+ int cx = (int)gx - k->gauss_radius;
+ int cy = (int)gy - k->gauss_radius;
+ int sq = cx * cx + cy * cy;
+ double e = exp(-sqrt(sq) * sigma);
+ uint64_t v = e / gauss_size2 * (double) UINT64_MAX;
+ k->gauss[XY(k, gx, gy)] =
+ k->gauss[XY(k, gy, gx)] =
+ k->gauss[XY(k, gx, gauss_size - 1 - gy)] =
+ k->gauss[XY(k, gy, gauss_size - 1 - gx)] =
+ k->gauss[XY(k, gauss_size - 1 - gx, gy)] =
+ k->gauss[XY(k, gauss_size - 1 - gy, gx)] =
+ k->gauss[XY(k, gauss_size - 1 - gx, gauss_size - 1 - gy)] =
+ k->gauss[XY(k, gauss_size - 1 - gy, gauss_size - 1 - gx)] = v;
+ }
+ }
+ uint64_t total = 0;
+ for (unsigned int c = 0; c < k->size2; c++) {
+ uint64_t oldtotal = total;
+ total += k->gauss[c];
+ assert(total >= oldtotal);
+ }
+}
+
+static void setbit(struct ctx *k, unsigned int c)
+{
+ if (k->calcmat[c])
+ return;
+ k->calcmat[c] = true;
+ uint64_t *m = k->gaussmat;
+ uint64_t *me = k->gaussmat + k->size2;
+ uint64_t *g = k->gauss + WRAP_SIZE2(k, k->gauss_middle + k->size2 - c);
+ uint64_t *ge = k->gauss + k->size2;
+ while (g < ge)
+ *m++ += *g++;
+ g = k->gauss;
+ while (m < me)
+ *m++ += *g++;
+}
+
+static unsigned int getmin(struct ctx *k)
+{
+ uint64_t min = UINT64_MAX;
+ unsigned int resnum = 0;
+ unsigned int size2 = k->size2;
+ for (unsigned int c = 0; c < size2; c++) {
+ if (k->calcmat[c])
+ continue;
+ uint64_t total = k->gaussmat[c];
+ if (total <= min) {
+ if (total != min) {
+ min = total;
+ resnum = 0;
+ }
+ k->randomat[resnum++] = c;
+ }
+ }
+ if (resnum == 1)
+ return k->randomat[0];
+ if (resnum == size2)
+ return size2 / 2;
+ return k->randomat[av_lfg_get(&k->avlfg) % resnum];
+}
+
+static void makeuniform(struct ctx *k)
+{
+ unsigned int size2 = k->size2;
+ for (unsigned int c = 0; c < size2; c++) {
+ unsigned int r = getmin(k);
+ setbit(k, r);
+ k->unimat[r] = c;
+ }
+}
+
+// out_matrix is a reactangular tsize * tsize array, where tsize = (1 << size).
+void mp_make_fruit_dither_matrix(float *out_matrix, int size)
+{
+ struct ctx *k = talloc_zero(NULL, struct ctx);
+ makegauss(k, size);
+ makeuniform(k);
+ float invscale = k->size2;
+ for(unsigned int y = 0; y < k->size; y++) {
+ for(unsigned int x = 0; x < k->size; x++)
+ out_matrix[x + y * k->size] = k->unimat[XY(k, x, y)] / invscale;
+ }
+ talloc_free(k);
+}
+
+void mp_make_ordered_dither_matrix(unsigned char *m, int size)
+{
+ m[0] = 0;
+ for (int sz = 1; sz < size; sz *= 2) {
+ int offset[] = {sz*size, sz, sz * (size+1), 0};
+ for (int i = 0; i < 4; i++)
+ for (int y = 0; y < sz * size; y += size)
+ for (int x = 0; x < sz; x++)
+ m[x+y+offset[i]] = m[x+y] * 4 + (3-i) * 256/size/size;
+ }
+}
diff --git a/video/out/dither.h b/video/out/dither.h
new file mode 100644
index 0000000..ca804e3
--- /dev/null
+++ b/video/out/dither.h
@@ -0,0 +1,2 @@
+void mp_make_fruit_dither_matrix(float *out_matrix, int size);
+void mp_make_ordered_dither_matrix(unsigned char *m, int size);
diff --git a/video/out/dr_helper.c b/video/out/dr_helper.c
new file mode 100644
index 0000000..ac440a7
--- /dev/null
+++ b/video/out/dr_helper.c
@@ -0,0 +1,162 @@
+#include <assert.h>
+#include <stdatomic.h>
+#include <stdlib.h>
+
+#include <libavutil/buffer.h>
+
+#include "misc/dispatch.h"
+#include "mpv_talloc.h"
+#include "osdep/threads.h"
+#include "video/mp_image.h"
+
+#include "dr_helper.h"
+
+struct dr_helper {
+ mp_mutex thread_lock;
+ mp_thread_id thread_id;
+ bool thread_valid; // (POSIX defines no "unset" mp_thread value yet)
+
+ struct mp_dispatch_queue *dispatch;
+ atomic_ullong dr_in_flight;
+
+ struct mp_image *(*get_image)(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags);
+ void *get_image_ctx;
+};
+
+static void dr_helper_destroy(void *ptr)
+{
+ struct dr_helper *dr = ptr;
+
+ // All references must have been freed on destruction, or we'll have
+ // dangling pointers.
+ assert(atomic_load(&dr->dr_in_flight) == 0);
+
+ mp_mutex_destroy(&dr->thread_lock);
+}
+
+struct dr_helper *dr_helper_create(struct mp_dispatch_queue *dispatch,
+ struct mp_image *(*get_image)(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags),
+ void *get_image_ctx)
+{
+ struct dr_helper *dr = talloc_ptrtype(NULL, dr);
+ talloc_set_destructor(dr, dr_helper_destroy);
+ *dr = (struct dr_helper){
+ .dispatch = dispatch,
+ .dr_in_flight = 0,
+ .get_image = get_image,
+ .get_image_ctx = get_image_ctx,
+ };
+ mp_mutex_init(&dr->thread_lock);
+ return dr;
+}
+
+void dr_helper_acquire_thread(struct dr_helper *dr)
+{
+ mp_mutex_lock(&dr->thread_lock);
+ assert(!dr->thread_valid); // fails on API user errors
+ dr->thread_valid = true;
+ dr->thread_id = mp_thread_current_id();
+ mp_mutex_unlock(&dr->thread_lock);
+}
+
+void dr_helper_release_thread(struct dr_helper *dr)
+{
+ mp_mutex_lock(&dr->thread_lock);
+ // Fails on API user errors.
+ assert(dr->thread_valid);
+ assert(mp_thread_id_equal(dr->thread_id, mp_thread_current_id()));
+ dr->thread_valid = false;
+ mp_mutex_unlock(&dr->thread_lock);
+}
+
+struct free_dr_context {
+ struct dr_helper *dr;
+ AVBufferRef *ref;
+};
+
+static void dr_thread_free(void *ptr)
+{
+ struct free_dr_context *ctx = ptr;
+
+ unsigned long long v = atomic_fetch_add(&ctx->dr->dr_in_flight, -1);
+ assert(v); // value before sub is 0 - unexpected underflow.
+
+ av_buffer_unref(&ctx->ref);
+ talloc_free(ctx);
+}
+
+static void free_dr_buffer_on_dr_thread(void *opaque, uint8_t *data)
+{
+ struct free_dr_context *ctx = opaque;
+ struct dr_helper *dr = ctx->dr;
+
+ mp_mutex_lock(&dr->thread_lock);
+ bool on_this_thread =
+ dr->thread_valid && mp_thread_id_equal(ctx->dr->thread_id, mp_thread_current_id());
+ mp_mutex_unlock(&dr->thread_lock);
+
+ // The image could be unreffed even on the DR thread. In practice, this
+ // matters most on DR destruction.
+ if (on_this_thread) {
+ dr_thread_free(ctx);
+ } else {
+ mp_dispatch_enqueue(dr->dispatch, dr_thread_free, ctx);
+ }
+}
+
+struct get_image_cmd {
+ struct dr_helper *dr;
+ int imgfmt, w, h, stride_align, flags;
+ struct mp_image *res;
+};
+
+static void sync_get_image(void *ptr)
+{
+ struct get_image_cmd *cmd = ptr;
+ struct dr_helper *dr = cmd->dr;
+
+ cmd->res = dr->get_image(dr->get_image_ctx, cmd->imgfmt, cmd->w, cmd->h,
+ cmd->stride_align, cmd->flags);
+ if (!cmd->res)
+ return;
+
+ // We require exactly 1 AVBufferRef.
+ assert(cmd->res->bufs[0]);
+ assert(!cmd->res->bufs[1]);
+
+ // Apply some magic to get it free'd on the DR thread as well. For this to
+ // work, we create a dummy-ref that aliases the original ref, which is why
+ // the original ref must be writable in the first place. (A newly allocated
+ // image should be always writable of course.)
+ assert(mp_image_is_writeable(cmd->res));
+
+ struct free_dr_context *ctx = talloc_zero(NULL, struct free_dr_context);
+ *ctx = (struct free_dr_context){
+ .dr = dr,
+ .ref = cmd->res->bufs[0],
+ };
+
+ AVBufferRef *new_ref = av_buffer_create(ctx->ref->data, ctx->ref->size,
+ free_dr_buffer_on_dr_thread, ctx, 0);
+ MP_HANDLE_OOM(new_ref);
+
+ cmd->res->bufs[0] = new_ref;
+
+ atomic_fetch_add(&dr->dr_in_flight, 1);
+}
+
+struct mp_image *dr_helper_get_image(struct dr_helper *dr, int imgfmt,
+ int w, int h, int stride_align, int flags)
+{
+ struct get_image_cmd cmd = {
+ .dr = dr,
+ .imgfmt = imgfmt,
+ .w = w, .h = h,
+ .stride_align = stride_align,
+ .flags = flags,
+ };
+ mp_dispatch_run(dr->dispatch, sync_get_image, &cmd);
+ return cmd.res;
+}
diff --git a/video/out/dr_helper.h b/video/out/dr_helper.h
new file mode 100644
index 0000000..cf2ed14
--- /dev/null
+++ b/video/out/dr_helper.h
@@ -0,0 +1,37 @@
+#pragma once
+
+// This is a helper for implementing thread-safety for DR callbacks. These need
+// to allocate GPU buffers on the GPU thread (e.g. OpenGL with its forced TLS),
+// and the buffers also need to be freed on the GPU thread.
+// This is not a helpful "Dr.", rather it represents Satan in form of C code.
+struct dr_helper;
+
+struct mp_image;
+struct mp_dispatch_queue;
+
+// dr_helper_get_image() calls will use the dispatch queue to run get_image on
+// a target thread, which processes the dispatch queue.
+// Note: the dispatch queue must process outstanding async. work before the
+// dr_helper instance can be destroyed.
+struct dr_helper *dr_helper_create(struct mp_dispatch_queue *dispatch,
+ struct mp_image *(*get_image)(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags),
+ void *get_image_ctx);
+
+// Make DR release calls (freeing images) reentrant if they are called on current
+// thread. That means any free call will directly release the image as allocated
+// with get_image().
+// Only 1 thread can use this at a time. Note that it would make no sense to
+// call this on more than 1 thread, as get_image is assumed not thread-safe.
+void dr_helper_acquire_thread(struct dr_helper *dr);
+
+// This _must_ be called on the same thread as dr_helper_acquire_thread() was
+// called. Every release call must be paired with an acquire call.
+void dr_helper_release_thread(struct dr_helper *dr);
+
+// Allocate an image by running the get_image callback on the target thread.
+// Always blocks on dispatch queue processing. This implies there is no way to
+// allocate a DR'ed image on the render thread (at least not in a way which
+// actually works if you want foreign threads to be able to free them).
+struct mp_image *dr_helper_get_image(struct dr_helper *dr, int imgfmt,
+ int w, int h, int stride_align, int flags);
diff --git a/video/out/drm_atomic.c b/video/out/drm_atomic.c
new file mode 100644
index 0000000..5754504
--- /dev/null
+++ b/video/out/drm_atomic.c
@@ -0,0 +1,458 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "drm_atomic.h"
+
+int drm_object_create_properties(struct mp_log *log, int fd,
+ struct drm_object *object)
+{
+ object->props = drmModeObjectGetProperties(fd, object->id, object->type);
+ if (object->props) {
+ object->props_info = talloc_zero_size(NULL, object->props->count_props
+ * sizeof(object->props_info));
+ if (object->props_info) {
+ for (int i = 0; i < object->props->count_props; i++)
+ object->props_info[i] = drmModeGetProperty(fd, object->props->props[i]);
+ } else {
+ mp_err(log, "Out of memory\n");
+ goto fail;
+ }
+ } else {
+ mp_err(log, "Failed to retrieve properties for object id %d\n", object->id);
+ goto fail;
+ }
+
+ return 0;
+
+ fail:
+ drm_object_free_properties(object);
+ return -1;
+}
+
+void drm_object_free_properties(struct drm_object *object)
+{
+ if (object->props) {
+ for (int i = 0; i < object->props->count_props; i++) {
+ if (object->props_info[i]) {
+ drmModeFreeProperty(object->props_info[i]);
+ object->props_info[i] = NULL;
+ }
+ }
+
+ talloc_free(object->props_info);
+ object->props_info = NULL;
+
+ drmModeFreeObjectProperties(object->props);
+ object->props = NULL;
+ }
+}
+
+int drm_object_get_property(struct drm_object *object, char *name, uint64_t *value)
+{
+ for (int i = 0; i < object->props->count_props; i++) {
+ if (strcasecmp(name, object->props_info[i]->name) == 0) {
+ *value = object->props->prop_values[i];
+ return 0;
+ }
+ }
+
+ return -EINVAL;
+}
+
+drmModePropertyBlobPtr drm_object_get_property_blob(struct drm_object *object, char *name)
+{
+ uint64_t blob_id;
+
+ if (!drm_object_get_property(object, name, &blob_id)) {
+ return drmModeGetPropertyBlob(object->fd, blob_id);
+ }
+
+ return NULL;
+}
+
+int drm_object_set_property(drmModeAtomicReq *request, struct drm_object *object,
+ char *name, uint64_t value)
+{
+ for (int i = 0; i < object->props->count_props; i++) {
+ if (strcasecmp(name, object->props_info[i]->name) == 0) {
+ if (object->props_info[i]->flags & DRM_MODE_PROP_IMMUTABLE) {
+ /* Do not try to set immutable values, as this might cause the
+ * atomic commit operation to fail. */
+ return -EINVAL;
+ }
+ return drmModeAtomicAddProperty(request, object->id,
+ object->props_info[i]->prop_id, value);
+ }
+ }
+
+ return -EINVAL;
+}
+
+struct drm_object *drm_object_create(struct mp_log *log, int fd,
+ uint32_t object_id, uint32_t type)
+{
+ struct drm_object *obj = NULL;
+ obj = talloc_zero(NULL, struct drm_object);
+ obj->fd = fd;
+ obj->id = object_id;
+ obj->type = type;
+
+ if (drm_object_create_properties(log, fd, obj)) {
+ talloc_free(obj);
+ return NULL;
+ }
+
+ return obj;
+}
+
+void drm_object_free(struct drm_object *object)
+{
+ if (object) {
+ drm_object_free_properties(object);
+ talloc_free(object);
+ }
+}
+
+void drm_object_print_info(struct mp_log *log, struct drm_object *object)
+{
+ mp_err(log, "Object ID = %d (type = %x) has %d properties\n",
+ object->id, object->type, object->props->count_props);
+
+ for (int i = 0; i < object->props->count_props; i++)
+ mp_err(log, " Property '%s' = %lld\n", object->props_info[i]->name,
+ (long long)object->props->prop_values[i]);
+}
+
+struct drm_atomic_context *drm_atomic_create_context(struct mp_log *log, int fd, int crtc_id,
+ int connector_id,
+ int draw_plane_idx, int drmprime_video_plane_idx)
+{
+ drmModePlaneRes *plane_res = NULL;
+ drmModeRes *res = NULL;
+ struct drm_object *plane = NULL;
+ struct drm_atomic_context *ctx;
+ int crtc_index = -1;
+ int layercount = -1;
+ int primary_id = 0;
+ int overlay_id = 0;
+
+ uint64_t value;
+
+ res = drmModeGetResources(fd);
+ if (!res) {
+ mp_err(log, "Cannot retrieve DRM resources: %s\n", mp_strerror(errno));
+ goto fail;
+ }
+
+ plane_res = drmModeGetPlaneResources(fd);
+ if (!plane_res) {
+ mp_err(log, "Cannot retrieve plane resources: %s\n", mp_strerror(errno));
+ goto fail;
+ }
+
+ ctx = talloc_zero(NULL, struct drm_atomic_context);
+ if (!ctx) {
+ mp_err(log, "Out of memory\n");
+ goto fail;
+ }
+
+ ctx->fd = fd;
+ ctx->crtc = drm_object_create(log, ctx->fd, crtc_id, DRM_MODE_OBJECT_CRTC);
+ if (!ctx->crtc) {
+ mp_err(log, "Failed to create CRTC object\n");
+ goto fail;
+ }
+
+ for (int i = 0; i < res->count_crtcs; i++) {
+ if (res->crtcs[i] == crtc_id) {
+ crtc_index = i;
+ break;
+ }
+ }
+
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector = drmModeGetConnector(fd, res->connectors[i]);
+ if (connector) {
+ if (connector->connector_id == connector_id)
+ ctx->connector = drm_object_create(log, ctx->fd, connector->connector_id,
+ DRM_MODE_OBJECT_CONNECTOR);
+ drmModeFreeConnector(connector);
+ if (ctx->connector)
+ break;
+ }
+ }
+
+ for (unsigned int j = 0; j < plane_res->count_planes; j++) {
+
+ drmModePlane *drmplane = drmModeGetPlane(ctx->fd, plane_res->planes[j]);
+ const uint32_t possible_crtcs = drmplane->possible_crtcs;
+ const uint32_t plane_id = drmplane->plane_id;
+ drmModeFreePlane(drmplane);
+ drmplane = NULL;
+
+ if (possible_crtcs & (1 << crtc_index)) {
+ plane = drm_object_create(log, ctx->fd, plane_id, DRM_MODE_OBJECT_PLANE);
+
+ if (!plane) {
+ mp_err(log, "Failed to create Plane object from plane ID %d\n",
+ plane_id);
+ goto fail;
+ }
+
+ if (drm_object_get_property(plane, "TYPE", &value) == -EINVAL) {
+ mp_err(log, "Unable to retrieve type property from plane %d\n", j);
+ goto fail;
+ }
+
+ if (value != DRM_PLANE_TYPE_CURSOR) { // Skip cursor planes
+ layercount++;
+
+ if ((!primary_id) && (value == DRM_PLANE_TYPE_PRIMARY))
+ primary_id = plane_id;
+
+ if ((!overlay_id) && (value == DRM_PLANE_TYPE_OVERLAY))
+ overlay_id = plane_id;
+
+ if (layercount == draw_plane_idx) {
+ ctx->draw_plane = plane;
+ continue;
+ }
+
+ if (layercount == drmprime_video_plane_idx) {
+ ctx->drmprime_video_plane = plane;
+ continue;
+ }
+ }
+
+ drm_object_free(plane);
+ plane = NULL;
+ }
+ }
+
+ // draw plane was specified as either of the special options: any primary plane or any overlay plane
+ if (!ctx->draw_plane) {
+ const int draw_plane_id = (draw_plane_idx == DRM_OPTS_OVERLAY_PLANE) ? overlay_id : primary_id;
+ const char *plane_type = (draw_plane_idx == DRM_OPTS_OVERLAY_PLANE) ? "overlay" : "primary";
+ if (draw_plane_id) {
+ mp_verbose(log, "Using %s plane %d as draw plane\n", plane_type, draw_plane_id);
+ ctx->draw_plane = drm_object_create(log, ctx->fd, draw_plane_id, DRM_MODE_OBJECT_PLANE);
+ } else {
+ mp_err(log, "Failed to find draw plane with idx=%d\n", draw_plane_idx);
+ goto fail;
+ }
+ } else {
+ mp_verbose(log, "Found draw plane with ID %d\n", ctx->draw_plane->id);
+ }
+
+ // drmprime plane was specified as either of the special options: any primary plane or any overlay plane
+ if (!ctx->drmprime_video_plane) {
+ const int drmprime_video_plane_id = (drmprime_video_plane_idx == DRM_OPTS_PRIMARY_PLANE) ? primary_id : overlay_id;
+ const char *plane_type = (drmprime_video_plane_idx == DRM_OPTS_PRIMARY_PLANE) ? "primary" : "overlay";
+
+ if (drmprime_video_plane_id) {
+ mp_verbose(log, "Using %s plane %d as drmprime plane\n", plane_type, drmprime_video_plane_id);
+ ctx->drmprime_video_plane = drm_object_create(log, ctx->fd, drmprime_video_plane_id, DRM_MODE_OBJECT_PLANE);
+ } else {
+ mp_verbose(log, "Failed to find drmprime plane with idx=%d. drmprime-overlay hwdec interop will not work\n", drmprime_video_plane_idx);
+ }
+ } else {
+ mp_verbose(log, "Found drmprime plane with ID %d\n", ctx->drmprime_video_plane->id);
+ }
+
+ drmModeFreePlaneResources(plane_res);
+ drmModeFreeResources(res);
+ return ctx;
+
+fail:
+ if (res)
+ drmModeFreeResources(res);
+ if (plane_res)
+ drmModeFreePlaneResources(plane_res);
+ if (plane)
+ drm_object_free(plane);
+ return NULL;
+}
+
+void drm_atomic_destroy_context(struct drm_atomic_context *ctx)
+{
+ drm_mode_destroy_blob(ctx->fd, &ctx->old_state.crtc.mode);
+ drm_object_free(ctx->crtc);
+ drm_object_free(ctx->connector);
+ drm_object_free(ctx->draw_plane);
+ drm_object_free(ctx->drmprime_video_plane);
+ talloc_free(ctx);
+}
+
+static bool drm_atomic_save_plane_state(struct drm_object *plane,
+ struct drm_atomic_plane_state *plane_state)
+{
+ if (!plane)
+ return true;
+
+ bool ret = true;
+
+ if (0 > drm_object_get_property(plane, "FB_ID", &plane_state->fb_id))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_ID", &plane_state->crtc_id))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_X", &plane_state->src_x))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_Y", &plane_state->src_y))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_W", &plane_state->src_w))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_H", &plane_state->src_h))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_X", &plane_state->crtc_x))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_Y", &plane_state->crtc_y))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_W", &plane_state->crtc_w))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_H", &plane_state->crtc_h))
+ ret = false;
+ // ZPOS might not exist, so ignore whether or not this succeeds
+ drm_object_get_property(plane, "ZPOS", &plane_state->zpos);
+
+ return ret;
+}
+
+static bool drm_atomic_restore_plane_state(drmModeAtomicReq *request,
+ struct drm_object *plane,
+ const struct drm_atomic_plane_state *plane_state)
+{
+ if (!plane)
+ return true;
+
+ bool ret = true;
+
+ if (0 > drm_object_set_property(request, plane, "FB_ID", plane_state->fb_id))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_ID", plane_state->crtc_id))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_X", plane_state->src_x))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_Y", plane_state->src_y))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_W", plane_state->src_w))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_H", plane_state->src_h))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_X", plane_state->crtc_x))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_Y", plane_state->crtc_y))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_W", plane_state->crtc_w))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_H", plane_state->crtc_h))
+ ret = false;
+ // ZPOS might not exist, or be immutable, so ignore whether or not this succeeds
+ drm_object_set_property(request, plane, "ZPOS", plane_state->zpos);
+
+ return ret;
+}
+
+bool drm_atomic_save_old_state(struct drm_atomic_context *ctx)
+{
+ if (ctx->old_state.saved)
+ return false;
+
+ bool ret = true;
+
+ drmModeCrtc *crtc = drmModeGetCrtc(ctx->fd, ctx->crtc->id);
+ if (crtc == NULL)
+ return false;
+ ctx->old_state.crtc.mode.mode = crtc->mode;
+ drmModeFreeCrtc(crtc);
+
+ if (0 > drm_object_get_property(ctx->crtc, "ACTIVE", &ctx->old_state.crtc.active))
+ ret = false;
+
+ // This property was added in kernel 5.0. We will just ignore any errors.
+ drm_object_get_property(ctx->crtc, "VRR_ENABLED", &ctx->old_state.crtc.vrr_enabled);
+
+ if (0 > drm_object_get_property(ctx->connector, "CRTC_ID", &ctx->old_state.connector.crtc_id))
+ ret = false;
+
+ if (!drm_atomic_save_plane_state(ctx->draw_plane, &ctx->old_state.draw_plane))
+ ret = false;
+ if (!drm_atomic_save_plane_state(ctx->drmprime_video_plane, &ctx->old_state.drmprime_video_plane))
+ ret = false;
+
+ ctx->old_state.saved = true;
+
+ return ret;
+}
+
+bool drm_atomic_restore_old_state(drmModeAtomicReqPtr request, struct drm_atomic_context *ctx)
+{
+ if (!ctx->old_state.saved)
+ return false;
+
+ bool ret = true;
+
+ if (0 > drm_object_set_property(request, ctx->connector, "CRTC_ID", ctx->old_state.connector.crtc_id))
+ ret = false;
+
+ // This property was added in kernel 5.0. We will just ignore any errors.
+ drm_object_set_property(request, ctx->crtc, "VRR_ENABLED", ctx->old_state.crtc.vrr_enabled);
+
+ if (!drm_mode_ensure_blob(ctx->fd, &ctx->old_state.crtc.mode))
+ ret = false;
+ if (0 > drm_object_set_property(request, ctx->crtc, "MODE_ID", ctx->old_state.crtc.mode.blob_id))
+ ret = false;
+ if (0 > drm_object_set_property(request, ctx->crtc, "ACTIVE", ctx->old_state.crtc.active))
+ ret = false;
+
+ if (!drm_atomic_restore_plane_state(request, ctx->draw_plane, &ctx->old_state.draw_plane))
+ ret = false;
+ if (!drm_atomic_restore_plane_state(request, ctx->drmprime_video_plane, &ctx->old_state.drmprime_video_plane))
+ ret = false;
+
+ ctx->old_state.saved = false;
+
+ return ret;
+}
+
+bool drm_mode_ensure_blob(int fd, struct drm_mode *mode)
+{
+ int ret = 0;
+
+ if (!mode->blob_id) {
+ ret = drmModeCreatePropertyBlob(fd, &mode->mode, sizeof(drmModeModeInfo),
+ &mode->blob_id);
+ }
+
+ return (ret == 0);
+}
+
+bool drm_mode_destroy_blob(int fd, struct drm_mode *mode)
+{
+ int ret = 0;
+
+ if (mode->blob_id) {
+ ret = drmModeDestroyPropertyBlob(fd, mode->blob_id);
+ mode->blob_id = 0;
+ }
+
+ return (ret == 0);
+}
diff --git a/video/out/drm_atomic.h b/video/out/drm_atomic.h
new file mode 100644
index 0000000..499aa33
--- /dev/null
+++ b/video/out/drm_atomic.h
@@ -0,0 +1,100 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_DRMATOMIC_H
+#define MP_DRMATOMIC_H
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+#include "common/msg.h"
+#include "drm_common.h"
+
+#define DRM_OPTS_PRIMARY_PLANE -1
+#define DRM_OPTS_OVERLAY_PLANE -2
+
+struct drm_atomic_plane_state {
+ uint64_t fb_id;
+ uint64_t crtc_id;
+ uint64_t src_x;
+ uint64_t src_y;
+ uint64_t src_w;
+ uint64_t src_h;
+ uint64_t crtc_x;
+ uint64_t crtc_y;
+ uint64_t crtc_w;
+ uint64_t crtc_h;
+ uint64_t zpos;
+};
+
+// Used to store the restore state for VT switching and uninit
+struct drm_atomic_state {
+ bool saved;
+ struct {
+ uint64_t crtc_id;
+ } connector;
+ struct {
+ struct drm_mode mode;
+ uint64_t active;
+ uint64_t vrr_enabled;
+ } crtc;
+ struct drm_atomic_plane_state draw_plane;
+ struct drm_atomic_plane_state drmprime_video_plane;
+};
+
+struct drm_object {
+ int fd;
+ uint32_t id;
+ uint32_t type;
+ drmModeObjectProperties *props;
+ drmModePropertyRes **props_info;
+};
+
+struct drm_atomic_context {
+ int fd;
+
+ struct drm_object *crtc;
+ struct drm_object *connector;
+ struct drm_object *draw_plane;
+ struct drm_object *drmprime_video_plane;
+
+ drmModeAtomicReq *request;
+
+ struct drm_atomic_state old_state;
+};
+
+int drm_object_create_properties(struct mp_log *log, int fd, struct drm_object *object);
+void drm_object_free_properties(struct drm_object *object);
+int drm_object_get_property(struct drm_object *object, char *name, uint64_t *value);
+int drm_object_set_property(drmModeAtomicReq *request, struct drm_object *object, char *name, uint64_t value);
+drmModePropertyBlobPtr drm_object_get_property_blob(struct drm_object *object, char *name);
+struct drm_object *drm_object_create(struct mp_log *log, int fd, uint32_t object_id, uint32_t type);
+void drm_object_free(struct drm_object *object);
+void drm_object_print_info(struct mp_log *log, struct drm_object *object);
+struct drm_atomic_context *drm_atomic_create_context(struct mp_log *log, int fd, int crtc_id, int connector_id,
+ int draw_plane_idx, int drmprime_video_plane_idx);
+void drm_atomic_destroy_context(struct drm_atomic_context *ctx);
+
+bool drm_atomic_save_old_state(struct drm_atomic_context *ctx);
+bool drm_atomic_restore_old_state(drmModeAtomicReq *request, struct drm_atomic_context *ctx);
+
+bool drm_mode_ensure_blob(int fd, struct drm_mode *mode);
+bool drm_mode_destroy_blob(int fd, struct drm_mode *mode);
+
+#endif // MP_DRMATOMIC_H
diff --git a/video/out/drm_common.c b/video/out/drm_common.c
new file mode 100644
index 0000000..da45ca2
--- /dev/null
+++ b/video/out/drm_common.c
@@ -0,0 +1,1289 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <signal.h>
+#include <sys/ioctl.h>
+#include <poll.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <limits.h>
+#include <math.h>
+#include <time.h>
+#include <drm_fourcc.h>
+
+#include "config.h"
+
+#if HAVE_CONSIO_H
+#include <sys/consio.h>
+#else
+#include <sys/vt.h>
+#endif
+
+#include "drm_atomic.h"
+#include "drm_common.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "options/m_config.h"
+#include "osdep/io.h"
+#include "osdep/poll_wrapper.h"
+#include "osdep/timer.h"
+#include "present_sync.h"
+#include "video/out/vo.h"
+
+#define EVT_RELEASE 1
+#define EVT_ACQUIRE 2
+#define EVT_INTERRUPT 255
+#define HANDLER_ACQUIRE 0
+#define HANDLER_RELEASE 1
+#define RELEASE_SIGNAL SIGUSR1
+#define ACQUIRE_SIGNAL SIGUSR2
+#define MAX_CONNECTOR_NAME_LEN 20
+
+static int vt_switcher_pipe[2];
+
+static int drm_connector_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name);
+
+static int drm_mode_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name);
+
+static int drm_validate_mode_opt(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value);
+
+static void drm_show_available_modes(struct mp_log *log, const drmModeConnector *connector);
+
+static void drm_show_available_connectors(struct mp_log *log, int card_no,
+ const char *card_path);
+static double mode_get_Hz(const drmModeModeInfo *mode);
+
+#define OPT_BASE_STRUCT struct drm_opts
+const struct m_sub_options drm_conf = {
+ .opts = (const struct m_option[]) {
+ {"drm-device", OPT_STRING(device_path), .flags = M_OPT_FILE},
+ {"drm-connector", OPT_STRING(connector_spec),
+ .help = drm_connector_opt_help},
+ {"drm-mode", OPT_STRING_VALIDATE(mode_spec, drm_validate_mode_opt),
+ .help = drm_mode_opt_help},
+ {"drm-atomic", OPT_CHOICE(drm_atomic, {"no", 0}, {"auto", 1}),
+ .deprecation_message = "this option is deprecated: DRM Atomic is required"},
+ {"drm-draw-plane", OPT_CHOICE(draw_plane,
+ {"primary", DRM_OPTS_PRIMARY_PLANE},
+ {"overlay", DRM_OPTS_OVERLAY_PLANE}),
+ M_RANGE(0, INT_MAX)},
+ {"drm-drmprime-video-plane", OPT_CHOICE(drmprime_video_plane,
+ {"primary", DRM_OPTS_PRIMARY_PLANE},
+ {"overlay", DRM_OPTS_OVERLAY_PLANE}),
+ M_RANGE(0, INT_MAX)},
+ {"drm-format", OPT_CHOICE(drm_format,
+ {"xrgb8888", DRM_OPTS_FORMAT_XRGB8888},
+ {"xrgb2101010", DRM_OPTS_FORMAT_XRGB2101010},
+ {"xbgr8888", DRM_OPTS_FORMAT_XBGR8888},
+ {"xbgr2101010", DRM_OPTS_FORMAT_XBGR2101010})},
+ {"drm-draw-surface-size", OPT_SIZE_BOX(draw_surface_size)},
+ {"drm-vrr-enabled", OPT_CHOICE(vrr_enabled,
+ {"no", 0}, {"yes", 1}, {"auto", -1})},
+ {0},
+ },
+ .defaults = &(const struct drm_opts) {
+ .mode_spec = "preferred",
+ .drm_atomic = 1,
+ .draw_plane = DRM_OPTS_PRIMARY_PLANE,
+ .drmprime_video_plane = DRM_OPTS_OVERLAY_PLANE,
+ },
+ .size = sizeof(struct drm_opts),
+};
+
+static const char *connector_names[] = {
+ "Unknown", // DRM_MODE_CONNECTOR_Unknown
+ "VGA", // DRM_MODE_CONNECTOR_VGA
+ "DVI-I", // DRM_MODE_CONNECTOR_DVII
+ "DVI-D", // DRM_MODE_CONNECTOR_DVID
+ "DVI-A", // DRM_MODE_CONNECTOR_DVIA
+ "Composite", // DRM_MODE_CONNECTOR_Composite
+ "SVIDEO", // DRM_MODE_CONNECTOR_SVIDEO
+ "LVDS", // DRM_MODE_CONNECTOR_LVDS
+ "Component", // DRM_MODE_CONNECTOR_Component
+ "DIN", // DRM_MODE_CONNECTOR_9PinDIN
+ "DP", // DRM_MODE_CONNECTOR_DisplayPort
+ "HDMI-A", // DRM_MODE_CONNECTOR_HDMIA
+ "HDMI-B", // DRM_MODE_CONNECTOR_HDMIB
+ "TV", // DRM_MODE_CONNECTOR_TV
+ "eDP", // DRM_MODE_CONNECTOR_eDP
+ "Virtual", // DRM_MODE_CONNECTOR_VIRTUAL
+ "DSI", // DRM_MODE_CONNECTOR_DSI
+ "DPI", // DRM_MODE_CONNECTOR_DPI
+ "Writeback", // DRM_MODE_CONNECTOR_WRITEBACK
+ "SPI", // DRM_MODE_CONNECTOR_SPI
+ "USB", // DRM_MODE_CONNECTOR_USB
+};
+
+struct drm_mode_spec {
+ enum {
+ DRM_MODE_SPEC_BY_IDX, // Specified by idx
+ DRM_MODE_SPEC_BY_NUMBERS, // Specified by width, height and opt. refresh
+ DRM_MODE_SPEC_PREFERRED, // Select the preferred mode of the display
+ DRM_MODE_SPEC_HIGHEST, // Select the mode with the highest resolution
+ } type;
+ unsigned int idx;
+ unsigned int width;
+ unsigned int height;
+ double refresh;
+};
+
+/* VT Switcher */
+static void vt_switcher_sighandler(int sig)
+{
+ int saved_errno = errno;
+ unsigned char event = sig == RELEASE_SIGNAL ? EVT_RELEASE : EVT_ACQUIRE;
+ (void)write(vt_switcher_pipe[1], &event, sizeof(event));
+ errno = saved_errno;
+}
+
+static bool has_signal_installed(int signo)
+{
+ struct sigaction act = { 0 };
+ sigaction(signo, 0, &act);
+ return act.sa_handler != 0;
+}
+
+static int install_signal(int signo, void (*handler)(int))
+{
+ struct sigaction act = { 0 };
+ act.sa_handler = handler;
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = SA_RESTART;
+ return sigaction(signo, &act, NULL);
+}
+
+static void release_vt(void *data)
+{
+ struct vo_drm_state *drm = data;
+ MP_VERBOSE(drm, "Releasing VT\n");
+ vo_drm_release_crtc(drm);
+}
+
+static void acquire_vt(void *data)
+{
+ struct vo_drm_state *drm = data;
+ MP_VERBOSE(drm, "Acquiring VT\n");
+ vo_drm_acquire_crtc(drm);
+}
+
+static void vt_switcher_acquire(struct vt_switcher *s,
+ void (*handler)(void*), void *user_data)
+{
+ s->handlers[HANDLER_ACQUIRE] = handler;
+ s->handler_data[HANDLER_ACQUIRE] = user_data;
+}
+
+static void vt_switcher_release(struct vt_switcher *s,
+ void (*handler)(void*), void *user_data)
+{
+ s->handlers[HANDLER_RELEASE] = handler;
+ s->handler_data[HANDLER_RELEASE] = user_data;
+}
+
+static bool vt_switcher_init(struct vt_switcher *s, struct mp_log *log)
+{
+ s->tty_fd = -1;
+ s->log = log;
+ vt_switcher_pipe[0] = -1;
+ vt_switcher_pipe[1] = -1;
+
+ if (mp_make_cloexec_pipe(vt_switcher_pipe)) {
+ mp_err(log, "Creating pipe failed: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ s->tty_fd = open("/dev/tty", O_RDWR | O_CLOEXEC);
+ if (s->tty_fd < 0) {
+ mp_err(log, "Can't open TTY for VT control: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ if (has_signal_installed(RELEASE_SIGNAL)) {
+ mp_err(log, "Can't handle VT release - signal already used\n");
+ return false;
+ }
+ if (has_signal_installed(ACQUIRE_SIGNAL)) {
+ mp_err(log, "Can't handle VT acquire - signal already used\n");
+ return false;
+ }
+
+ if (install_signal(RELEASE_SIGNAL, vt_switcher_sighandler)) {
+ mp_err(log, "Failed to install release signal: %s\n", mp_strerror(errno));
+ return false;
+ }
+ if (install_signal(ACQUIRE_SIGNAL, vt_switcher_sighandler)) {
+ mp_err(log, "Failed to install acquire signal: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ struct vt_mode vt_mode = { 0 };
+ if (ioctl(s->tty_fd, VT_GETMODE, &vt_mode) < 0) {
+ mp_err(log, "VT_GETMODE failed: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ vt_mode.mode = VT_PROCESS;
+ vt_mode.relsig = RELEASE_SIGNAL;
+ vt_mode.acqsig = ACQUIRE_SIGNAL;
+ // frsig is a signal for forced release. Not implemented on Linux,
+ // Solaris, BSDs but must be set to a valid signal on some of those.
+ vt_mode.frsig = SIGIO; // unused
+ if (ioctl(s->tty_fd, VT_SETMODE, &vt_mode) < 0) {
+ mp_err(log, "VT_SETMODE failed: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ // Block the VT switching signals from interrupting the VO thread (they will
+ // still be picked up by other threads, which will fill vt_switcher_pipe for us)
+ sigset_t set;
+ sigemptyset(&set);
+ sigaddset(&set, RELEASE_SIGNAL);
+ sigaddset(&set, ACQUIRE_SIGNAL);
+ pthread_sigmask(SIG_BLOCK, &set, NULL);
+
+ return true;
+}
+
+static void vt_switcher_interrupt_poll(struct vt_switcher *s)
+{
+ unsigned char event = EVT_INTERRUPT;
+ (void)write(vt_switcher_pipe[1], &event, sizeof(event));
+}
+
+static void vt_switcher_destroy(struct vt_switcher *s)
+{
+ struct vt_mode vt_mode = {0};
+ vt_mode.mode = VT_AUTO;
+ if (ioctl(s->tty_fd, VT_SETMODE, &vt_mode) < 0) {
+ MP_ERR(s, "VT_SETMODE failed: %s\n", mp_strerror(errno));
+ return;
+ }
+
+ install_signal(RELEASE_SIGNAL, SIG_DFL);
+ install_signal(ACQUIRE_SIGNAL, SIG_DFL);
+ close(s->tty_fd);
+ close(vt_switcher_pipe[0]);
+ close(vt_switcher_pipe[1]);
+}
+
+static void vt_switcher_poll(struct vt_switcher *s, int timeout_ns)
+{
+ struct pollfd fds[1] = {
+ { .events = POLLIN, .fd = vt_switcher_pipe[0] },
+ };
+ mp_poll(fds, 1, timeout_ns);
+ if (!fds[0].revents)
+ return;
+
+ unsigned char event;
+ if (read(fds[0].fd, &event, sizeof(event)) != sizeof(event))
+ return;
+
+ switch (event) {
+ case EVT_RELEASE:
+ s->handlers[HANDLER_RELEASE](s->handler_data[HANDLER_RELEASE]);
+ if (ioctl(s->tty_fd, VT_RELDISP, 1) < 0) {
+ MP_ERR(s, "Failed to release virtual terminal\n");
+ }
+ break;
+ case EVT_ACQUIRE:
+ s->handlers[HANDLER_ACQUIRE](s->handler_data[HANDLER_ACQUIRE]);
+ if (ioctl(s->tty_fd, VT_RELDISP, VT_ACKACQ) < 0) {
+ MP_ERR(s, "Failed to acquire virtual terminal\n");
+ }
+ break;
+ case EVT_INTERRUPT:
+ break;
+ }
+}
+
+bool vo_drm_acquire_crtc(struct vo_drm_state *drm)
+{
+ if (drm->active)
+ return true;
+ drm->active = true;
+
+ if (drmSetMaster(drm->fd)) {
+ MP_WARN(drm, "Failed to acquire DRM master: %s\n",
+ mp_strerror(errno));
+ }
+
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+
+ if (!drm_atomic_save_old_state(atomic_ctx))
+ MP_WARN(drm, "Failed to save old DRM atomic state\n");
+
+ drmModeAtomicReqPtr request = drmModeAtomicAlloc();
+ if (!request) {
+ MP_ERR(drm, "Failed to allocate drm atomic request\n");
+ goto err;
+ }
+
+ if (drm_object_set_property(request, atomic_ctx->connector, "CRTC_ID", drm->crtc_id) < 0) {
+ MP_ERR(drm, "Could not set CRTC_ID on connector\n");
+ goto err;
+ }
+
+ if (!drm_mode_ensure_blob(drm->fd, &drm->mode)) {
+ MP_ERR(drm, "Failed to create DRM mode blob\n");
+ goto err;
+ }
+ if (drm_object_set_property(request, atomic_ctx->crtc, "MODE_ID", drm->mode.blob_id) < 0) {
+ MP_ERR(drm, "Could not set MODE_ID on crtc\n");
+ goto err;
+ }
+ if (drm_object_set_property(request, atomic_ctx->crtc, "ACTIVE", 1) < 0) {
+ MP_ERR(drm, "Could not set ACTIVE on crtc\n");
+ goto err;
+ }
+
+ /*
+ * VRR related properties were added in kernel 5.0. We will not fail if we
+ * cannot query or set the value, but we will log as appropriate.
+ */
+ uint64_t vrr_capable = 0;
+ drm_object_get_property(atomic_ctx->connector, "VRR_CAPABLE", &vrr_capable);
+ MP_VERBOSE(drm, "crtc is%s VRR capable\n", vrr_capable ? "" : " not");
+
+ uint64_t vrr_requested = drm->opts->vrr_enabled;
+ if (vrr_requested == 1 || (vrr_capable && vrr_requested == -1)) {
+ if (drm_object_set_property(request, atomic_ctx->crtc, "VRR_ENABLED", 1) < 0) {
+ MP_WARN(drm, "Could not enable VRR on crtc\n");
+ } else {
+ MP_VERBOSE(drm, "Enabled VRR on crtc\n");
+ }
+ }
+
+ drm_object_set_property(request, atomic_ctx->draw_plane, "FB_ID", drm->fb->id);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_ID", drm->crtc_id);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_X", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_Y", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_W", drm->width << 16);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_H", drm->height << 16);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_X", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_Y", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_W", drm->mode.mode.hdisplay);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_H", drm->mode.mode.vdisplay);
+
+ if (drmModeAtomicCommit(drm->fd, request, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL)) {
+ MP_ERR(drm, "Failed to commit ModeSetting atomic request: %s\n", strerror(errno));
+ goto err;
+ }
+
+ drmModeAtomicFree(request);
+ return true;
+
+err:
+ drmModeAtomicFree(request);
+ return false;
+}
+
+
+void vo_drm_release_crtc(struct vo_drm_state *drm)
+{
+ if (!drm->active)
+ return;
+ drm->active = false;
+
+ if (!drm->atomic_context->old_state.saved)
+ return;
+
+ bool success = true;
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+ drmModeAtomicReqPtr request = drmModeAtomicAlloc();
+ if (!request) {
+ MP_ERR(drm, "Failed to allocate drm atomic request\n");
+ success = false;
+ }
+
+ if (request && !drm_atomic_restore_old_state(request, atomic_ctx)) {
+ MP_WARN(drm, "Got error while restoring old state\n");
+ success = false;
+ }
+
+ if (request) {
+ if (drmModeAtomicCommit(drm->fd, request, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL)) {
+ MP_WARN(drm, "Failed to commit ModeSetting atomic request: %s\n",
+ mp_strerror(errno));
+ success = false;
+ }
+ }
+
+ if (request)
+ drmModeAtomicFree(request);
+
+ if (!success)
+ MP_ERR(drm, "Failed to restore previous mode\n");
+
+ if (drmDropMaster(drm->fd)) {
+ MP_WARN(drm, "Failed to drop DRM master: %s\n",
+ mp_strerror(errno));
+ }
+}
+
+/* libdrm */
+static void get_connector_name(const drmModeConnector *connector,
+ char ret[MAX_CONNECTOR_NAME_LEN])
+{
+ const char *type_name;
+
+ if (connector->connector_type < MP_ARRAY_SIZE(connector_names)) {
+ type_name = connector_names[connector->connector_type];
+ } else {
+ type_name = "UNKNOWN";
+ }
+
+ snprintf(ret, MAX_CONNECTOR_NAME_LEN, "%s-%d", type_name,
+ connector->connector_type_id);
+}
+
+// Gets the first connector whose name matches the input parameter.
+// The returned connector may be disconnected.
+// Result must be freed with drmModeFreeConnector.
+static drmModeConnector *get_connector_by_name(const drmModeRes *res,
+ const char *connector_name,
+ int fd)
+{
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector
+ = drmModeGetConnector(fd, res->connectors[i]);
+ if (!connector)
+ continue;
+ char other_connector_name[MAX_CONNECTOR_NAME_LEN];
+ get_connector_name(connector, other_connector_name);
+ if (!strcmp(connector_name, other_connector_name))
+ return connector;
+ drmModeFreeConnector(connector);
+ }
+ return NULL;
+}
+
+// Gets the first connected connector.
+// Result must be freed with drmModeFreeConnector.
+static drmModeConnector *get_first_connected_connector(const drmModeRes *res,
+ int fd)
+{
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector = drmModeGetConnector(fd, res->connectors[i]);
+ if (!connector)
+ continue;
+ if (connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0) {
+ return connector;
+ }
+ drmModeFreeConnector(connector);
+ }
+ return NULL;
+}
+
+static bool setup_connector(struct vo_drm_state *drm, const drmModeRes *res,
+ const char *connector_name)
+{
+ drmModeConnector *connector;
+
+ if (connector_name && strcmp(connector_name, "") && strcmp(connector_name, "auto")) {
+ connector = get_connector_by_name(res, connector_name, drm->fd);
+ if (!connector) {
+ MP_ERR(drm, "No connector with name %s found\n", connector_name);
+ drm_show_available_connectors(drm->log, drm->card_no, drm->card_path);
+ return false;
+ }
+ } else {
+ connector = get_first_connected_connector(res, drm->fd);
+ if (!connector) {
+ MP_ERR(drm, "No connected connectors found\n");
+ return false;
+ }
+ }
+
+ if (connector->connection != DRM_MODE_CONNECTED) {
+ drmModeFreeConnector(connector);
+ MP_ERR(drm, "Chosen connector is disconnected\n");
+ return false;
+ }
+
+ if (connector->count_modes == 0) {
+ drmModeFreeConnector(connector);
+ MP_ERR(drm, "Chosen connector has no valid modes\n");
+ return false;
+ }
+
+ drm->connector = connector;
+ return true;
+}
+
+static bool setup_crtc(struct vo_drm_state *drm, const drmModeRes *res)
+{
+ // First try to find currently connected encoder and its current CRTC
+ for (unsigned int i = 0; i < res->count_encoders; i++) {
+ drmModeEncoder *encoder = drmModeGetEncoder(drm->fd, res->encoders[i]);
+ if (!encoder) {
+ MP_WARN(drm, "Cannot retrieve encoder %u:%u: %s\n",
+ i, res->encoders[i], mp_strerror(errno));
+ continue;
+ }
+
+ if (encoder->encoder_id == drm->connector->encoder_id && encoder->crtc_id != 0) {
+ MP_VERBOSE(drm, "Connector %u currently connected to encoder %u\n",
+ drm->connector->connector_id, drm->connector->encoder_id);
+ drm->encoder = encoder;
+ drm->crtc_id = encoder->crtc_id;
+ goto success;
+ }
+
+ drmModeFreeEncoder(encoder);
+ }
+
+ // Otherwise pick first legal encoder and CRTC combo for the connector
+ for (unsigned int i = 0; i < drm->connector->count_encoders; ++i) {
+ drmModeEncoder *encoder
+ = drmModeGetEncoder(drm->fd, drm->connector->encoders[i]);
+ if (!encoder) {
+ MP_WARN(drm, "Cannot retrieve encoder %u:%u: %s\n",
+ i, drm->connector->encoders[i], mp_strerror(errno));
+ continue;
+ }
+
+ // iterate all global CRTCs
+ for (unsigned int j = 0; j < res->count_crtcs; ++j) {
+ // check whether this CRTC works with the encoder
+ if (!(encoder->possible_crtcs & (1 << j)))
+ continue;
+
+ drm->encoder = encoder;
+ drm->crtc_id = res->crtcs[j];
+ goto success;
+ }
+
+ drmModeFreeEncoder(encoder);
+ }
+
+ MP_ERR(drm, "Connector %u has no suitable CRTC\n",
+ drm->connector->connector_id);
+ return false;
+
+ success:
+ MP_VERBOSE(drm, "Selected Encoder %u with CRTC %u\n",
+ drm->encoder->encoder_id, drm->crtc_id);
+ return true;
+}
+
+static bool all_digits(const char *str)
+{
+ if (str == NULL || str[0] == '\0') {
+ return false;
+ }
+
+ for (const char *c = str; *c != '\0'; ++c) {
+ if (!mp_isdigit(*c))
+ return false;
+ }
+ return true;
+}
+
+static bool parse_mode_spec(const char *spec, struct drm_mode_spec *parse_result)
+{
+ if (spec == NULL || spec[0] == '\0' || strcmp(spec, "preferred") == 0) {
+ if (parse_result) {
+ *parse_result =
+ (struct drm_mode_spec) { .type = DRM_MODE_SPEC_PREFERRED };
+ }
+ return true;
+ }
+
+ if (strcmp(spec, "highest") == 0) {
+ if (parse_result) {
+ *parse_result =
+ (struct drm_mode_spec) { .type = DRM_MODE_SPEC_HIGHEST };
+ }
+ return true;
+ }
+
+ // If the string is made up of only digits, it means that it is an index number
+ if (all_digits(spec)) {
+ if (parse_result) {
+ *parse_result = (struct drm_mode_spec) {
+ .type = DRM_MODE_SPEC_BY_IDX,
+ .idx = strtoul(spec, NULL, 10),
+ };
+ }
+ return true;
+ }
+
+ if (!mp_isdigit(spec[0]))
+ return false;
+ char *height_part, *refresh_part;
+ const unsigned int width = strtoul(spec, &height_part, 10);
+ if (spec == height_part || height_part[0] == '\0' || height_part[0] != 'x')
+ return false;
+
+ height_part += 1;
+ if (!mp_isdigit(height_part[0]))
+ return false;
+ const unsigned int height = strtoul(height_part, &refresh_part, 10);
+ if (height_part == refresh_part)
+ return false;
+
+ char *rest = NULL;
+ double refresh;
+ switch (refresh_part[0]) {
+ case '\0':
+ refresh = nan("");
+ break;
+ case '@':
+ refresh_part += 1;
+ if (!(mp_isdigit(refresh_part[0]) || refresh_part[0] == '.'))
+ return false;
+ refresh = strtod(refresh_part, &rest);
+ if (refresh_part == rest || rest[0] != '\0' || refresh < 0.0)
+ return false;
+ break;
+ default:
+ return false;
+ }
+
+ if (parse_result) {
+ *parse_result = (struct drm_mode_spec) {
+ .type = DRM_MODE_SPEC_BY_NUMBERS,
+ .width = width,
+ .height = height,
+ .refresh = refresh,
+ };
+ }
+ return true;
+}
+
+static bool setup_mode_by_idx(struct vo_drm_state *drm, unsigned int mode_idx)
+{
+ if (mode_idx >= drm->connector->count_modes) {
+ MP_ERR(drm, "Bad mode index (max = %d).\n",
+ drm->connector->count_modes - 1);
+ return false;
+ }
+
+ drm->mode.mode = drm->connector->modes[mode_idx];
+ return true;
+}
+
+static bool mode_match(const drmModeModeInfo *mode,
+ unsigned int width,
+ unsigned int height,
+ double refresh)
+{
+ if (isnan(refresh)) {
+ return
+ (mode->hdisplay == width) &&
+ (mode->vdisplay == height);
+ } else {
+ const double mode_refresh = mode_get_Hz(mode);
+ return
+ (mode->hdisplay == width) &&
+ (mode->vdisplay == height) &&
+ ((int)round(refresh*100) == (int)round(mode_refresh*100));
+ }
+}
+
+static bool setup_mode_by_numbers(struct vo_drm_state *drm,
+ unsigned int width,
+ unsigned int height,
+ double refresh)
+{
+ for (unsigned int i = 0; i < drm->connector->count_modes; ++i) {
+ drmModeModeInfo *current_mode = &drm->connector->modes[i];
+ if (mode_match(current_mode, width, height, refresh)) {
+ drm->mode.mode = *current_mode;
+ return true;
+ }
+ }
+
+ MP_ERR(drm, "Could not find mode matching %s\n", drm->opts->mode_spec);
+ return false;
+}
+
+static bool setup_mode_preferred(struct vo_drm_state *drm)
+{
+ for (unsigned int i = 0; i < drm->connector->count_modes; ++i) {
+ drmModeModeInfo *current_mode = &drm->connector->modes[i];
+ if (current_mode->type & DRM_MODE_TYPE_PREFERRED) {
+ drm->mode.mode = *current_mode;
+ return true;
+ }
+ }
+
+ // Fall back to first mode
+ MP_WARN(drm, "Could not find any preferred mode. Picking the first mode.\n");
+ drm->mode.mode = drm->connector->modes[0];
+ return true;
+}
+
+static bool setup_mode_highest(struct vo_drm_state *drm)
+{
+ unsigned int area = 0;
+ drmModeModeInfo *highest_resolution_mode = &drm->connector->modes[0];
+ for (unsigned int i = 0; i < drm->connector->count_modes; ++i) {
+ drmModeModeInfo *current_mode = &drm->connector->modes[i];
+
+ const unsigned int current_area =
+ current_mode->hdisplay * current_mode->vdisplay;
+ if (current_area > area) {
+ highest_resolution_mode = current_mode;
+ area = current_area;
+ }
+ }
+
+ drm->mode.mode = *highest_resolution_mode;
+ return true;
+}
+
+static bool setup_mode(struct vo_drm_state *drm)
+{
+ if (drm->connector->count_modes <= 0) {
+ MP_ERR(drm, "No available modes\n");
+ return false;
+ }
+
+ struct drm_mode_spec parsed;
+ if (!parse_mode_spec(drm->opts->mode_spec, &parsed)) {
+ MP_ERR(drm, "Parse error\n");
+ goto err;
+ }
+
+ switch (parsed.type) {
+ case DRM_MODE_SPEC_BY_IDX:
+ if (!setup_mode_by_idx(drm, parsed.idx))
+ goto err;
+ break;
+ case DRM_MODE_SPEC_BY_NUMBERS:
+ if (!setup_mode_by_numbers(drm, parsed.width, parsed.height, parsed.refresh))
+ goto err;
+ break;
+ case DRM_MODE_SPEC_PREFERRED:
+ if (!setup_mode_preferred(drm))
+ goto err;
+ break;
+ case DRM_MODE_SPEC_HIGHEST:
+ if (!setup_mode_highest(drm))
+ goto err;
+ break;
+ default:
+ MP_ERR(drm, "setup_mode: Internal error\n");
+ goto err;
+ }
+
+ drmModeModeInfo *mode = &drm->mode.mode;
+ MP_VERBOSE(drm, "Selected mode: %s (%dx%d@%.2fHz)\n",
+ mode->name, mode->hdisplay, mode->vdisplay, mode_get_Hz(mode));
+
+ return true;
+
+err:
+ MP_INFO(drm, "Available modes:\n");
+ drm_show_available_modes(drm->log, drm->connector);
+ return false;
+}
+
+static int open_card_path(const char *path)
+{
+ return open(path, O_RDWR | O_CLOEXEC);
+}
+
+static bool card_supports_kms(const char *path)
+{
+ int fd = open_card_path(path);
+ bool ret = fd != -1 && drmIsKMS(fd);
+ if (fd != -1)
+ close(fd);
+ return ret;
+}
+
+static bool card_has_connection(const char *path)
+{
+ int fd = open_card_path(path);
+ bool ret = false;
+ if (fd != -1) {
+ drmModeRes *res = drmModeGetResources(fd);
+ if (res) {
+ drmModeConnector *connector = get_first_connected_connector(res, fd);
+ if (connector)
+ ret = true;
+ drmModeFreeConnector(connector);
+ drmModeFreeResources(res);
+ }
+ close(fd);
+ }
+ return ret;
+}
+
+static void get_primary_device_path(struct vo_drm_state *drm)
+{
+ if (drm->opts->device_path) {
+ drm->card_path = talloc_strdup(drm, drm->opts->device_path);
+ return;
+ }
+
+ drmDevice *devices[DRM_MAX_MINOR] = { 0 };
+ int card_count = drmGetDevices2(0, devices, MP_ARRAY_SIZE(devices));
+ bool card_no_given = drm->card_no >= 0;
+
+ if (card_count < 0) {
+ MP_ERR(drm, "Listing DRM devices with drmGetDevices failed! (%s)\n",
+ mp_strerror(errno));
+ goto err;
+ }
+
+ if (card_no_given && drm->card_no > (card_count - 1)) {
+ MP_ERR(drm, "Card number %d given too high! %d devices located.\n",
+ drm->card_no, card_count);
+ goto err;
+ }
+
+ for (int i = card_no_given ? drm->card_no : 0; i < card_count; i++) {
+ drmDevice *dev = devices[i];
+
+ if (!(dev->available_nodes & (1 << DRM_NODE_PRIMARY))) {
+ if (card_no_given) {
+ MP_ERR(drm, "DRM card number %d given, but it does not have "
+ "a primary node!\n", i);
+ break;
+ }
+
+ continue;
+ }
+
+ const char *card_path = dev->nodes[DRM_NODE_PRIMARY];
+
+ if (!card_supports_kms(card_path)) {
+ if (card_no_given) {
+ MP_ERR(drm,
+ "DRM card number %d given, but it does not support "
+ "KMS!\n", i);
+ break;
+ }
+
+ continue;
+ }
+
+ if (!card_has_connection(card_path)) {
+ if (card_no_given) {
+ MP_ERR(drm,
+ "DRM card number %d given, but it does not have any "
+ "connected outputs.\n", i);
+ break;
+ }
+
+ continue;
+ }
+
+ MP_VERBOSE(drm, "Picked DRM card %d, primary node %s%s.\n",
+ i, card_path,
+ card_no_given ? "" : " as the default");
+
+ drm->card_path = talloc_strdup(drm, card_path);
+ drm->card_no = i;
+ break;
+ }
+
+ if (!drm->card_path)
+ MP_ERR(drm, "No primary DRM device could be picked!\n");
+
+err:
+ drmFreeDevices(devices, card_count);
+}
+
+static void drm_pflip_cb(int fd, unsigned int msc, unsigned int sec,
+ unsigned int usec, void *data)
+{
+ struct vo_drm_state *drm = data;
+
+ int64_t ust = MP_TIME_S_TO_NS(sec) + MP_TIME_US_TO_NS(usec);
+ present_sync_update_values(drm->present, ust, msc);
+ present_sync_swap(drm->present);
+ drm->waiting_for_flip = false;
+}
+
+int vo_drm_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_drm_state *drm = vo->drm;
+ switch (request) {
+ case VOCTRL_GET_DISPLAY_FPS: {
+ double fps = vo_drm_get_display_fps(drm);
+ if (fps <= 0)
+ break;
+ *(double*)arg = fps;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_RES: {
+ ((int *)arg)[0] = drm->mode.mode.hdisplay;
+ ((int *)arg)[1] = drm->mode.mode.vdisplay;
+ return VO_TRUE;
+ }
+ case VOCTRL_PAUSE:
+ vo->want_redraw = true;
+ drm->paused = true;
+ return VO_TRUE;
+ case VOCTRL_RESUME:
+ drm->paused = false;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+bool vo_drm_init(struct vo *vo)
+{
+ vo->drm = talloc_zero(NULL, struct vo_drm_state);
+ struct vo_drm_state *drm = vo->drm;
+
+ *drm = (struct vo_drm_state) {
+ .vo = vo,
+ .log = mp_log_new(drm, vo->log, "drm"),
+ .mode = {{0}},
+ .crtc_id = -1,
+ .card_no = -1,
+ };
+
+ drm->vt_switcher_active = vt_switcher_init(&drm->vt_switcher, drm->log);
+ if (drm->vt_switcher_active) {
+ vt_switcher_acquire(&drm->vt_switcher, acquire_vt, drm);
+ vt_switcher_release(&drm->vt_switcher, release_vt, drm);
+ } else {
+ MP_WARN(drm, "Failed to set up VT switcher. Terminal switching will be unavailable.\n");
+ }
+
+ drm->opts = mp_get_config_group(drm, drm->vo->global, &drm_conf);
+
+ drmModeRes *res = NULL;
+ get_primary_device_path(drm);
+
+ if (!drm->card_path) {
+ MP_ERR(drm, "Failed to find a usable DRM primary node!\n");
+ goto err;
+ }
+
+ drm->fd = open_card_path(drm->card_path);
+ if (drm->fd < 0) {
+ MP_ERR(drm, "Cannot open card \"%d\": %s.\n", drm->card_no, mp_strerror(errno));
+ goto err;
+ }
+
+ drmVersionPtr ver = drmGetVersion(drm->fd);
+ if (ver) {
+ MP_VERBOSE(drm, "Driver: %s %d.%d.%d (%s)\n", ver->name, ver->version_major,
+ ver->version_minor, ver->version_patchlevel, ver->date);
+ drmFreeVersion(ver);
+ }
+
+ res = drmModeGetResources(drm->fd);
+ if (!res) {
+ MP_ERR(drm, "Cannot retrieve DRM resources: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ if (!setup_connector(drm, res, drm->opts->connector_spec))
+ goto err;
+ if (!setup_crtc(drm, res))
+ goto err;
+ if (!setup_mode(drm))
+ goto err;
+
+ // Universal planes allows accessing all the planes (including primary)
+ if (drmSetClientCap(drm->fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) {
+ MP_ERR(drm, "Failed to set Universal planes capability\n");
+ }
+
+ if (drmSetClientCap(drm->fd, DRM_CLIENT_CAP_ATOMIC, 1)) {
+ MP_ERR(drm, "Failed to create DRM atomic context, no DRM Atomic support\n");
+ goto err;
+ } else {
+ MP_VERBOSE(drm, "DRM Atomic support found\n");
+ drm->atomic_context = drm_atomic_create_context(drm->log, drm->fd, drm->crtc_id,
+ drm->connector->connector_id,
+ drm->opts->draw_plane,
+ drm->opts->drmprime_video_plane);
+ if (!drm->atomic_context) {
+ MP_ERR(drm, "Failed to create DRM atomic context\n");
+ goto err;
+ }
+ }
+
+ drmModeFreeResources(res);
+
+ drm->ev.version = DRM_EVENT_CONTEXT_VERSION;
+ drm->ev.page_flip_handler = &drm_pflip_cb;
+ drm->present = mp_present_initialize(drm, drm->vo->opts, VO_MAX_SWAPCHAIN_DEPTH);
+
+ return true;
+
+err:
+ if (res)
+ drmModeFreeResources(res);
+
+ vo_drm_uninit(vo);
+ return false;
+}
+
+void vo_drm_uninit(struct vo *vo)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (!drm)
+ return;
+
+ vo_drm_release_crtc(drm);
+ if (drm->vt_switcher_active)
+ vt_switcher_destroy(&drm->vt_switcher);
+
+ drm_mode_destroy_blob(drm->fd, &drm->mode);
+
+ if (drm->connector) {
+ drmModeFreeConnector(drm->connector);
+ drm->connector = NULL;
+ }
+ if (drm->encoder) {
+ drmModeFreeEncoder(drm->encoder);
+ drm->encoder = NULL;
+ }
+ if (drm->atomic_context) {
+ drm_atomic_destroy_context(drm->atomic_context);
+ }
+
+ close(drm->fd);
+ talloc_free(drm);
+ vo->drm = NULL;
+}
+
+static double mode_get_Hz(const drmModeModeInfo *mode)
+{
+ double rate = mode->clock * 1000.0 / mode->htotal / mode->vtotal;
+ if (mode->flags & DRM_MODE_FLAG_INTERLACE)
+ rate *= 2.0;
+ return rate;
+}
+
+static void drm_show_available_modes(struct mp_log *log,
+ const drmModeConnector *connector)
+{
+ for (unsigned int i = 0; i < connector->count_modes; i++) {
+ mp_info(log, " Mode %d: %s (%dx%d@%.2fHz)\n", i,
+ connector->modes[i].name,
+ connector->modes[i].hdisplay,
+ connector->modes[i].vdisplay,
+ mode_get_Hz(&connector->modes[i]));
+ }
+}
+
+static void drm_show_foreach_connector(struct mp_log *log, int card_no,
+ const char *card_path,
+ void (*show_fn)(struct mp_log*, int,
+ const drmModeConnector*))
+{
+ int fd = open_card_path(card_path);
+ if (fd < 0) {
+ mp_err(log, "Failed to open card %d (%s)\n", card_no, card_path);
+ return;
+ }
+
+ drmModeRes *res = drmModeGetResources(fd);
+ if (!res) {
+ mp_err(log, "Cannot retrieve DRM resources: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector = drmModeGetConnector(fd, res->connectors[i]);
+ if (!connector)
+ continue;
+ show_fn(log, card_no, connector);
+ drmModeFreeConnector(connector);
+ }
+
+err:
+ if (fd >= 0)
+ close(fd);
+ if (res)
+ drmModeFreeResources(res);
+}
+
+static void drm_show_connector_name_and_state_callback(struct mp_log *log, int card_no,
+ const drmModeConnector *connector)
+{
+ char other_connector_name[MAX_CONNECTOR_NAME_LEN];
+ get_connector_name(connector, other_connector_name);
+ const char *connection_str = (connector->connection == DRM_MODE_CONNECTED) ?
+ "connected" : "disconnected";
+ mp_info(log, " %s (%s)\n", other_connector_name, connection_str);
+}
+
+static void drm_show_available_connectors(struct mp_log *log, int card_no,
+ const char *card_path)
+{
+ mp_info(log, "Available connectors for card %d (%s):\n", card_no,
+ card_path);
+ drm_show_foreach_connector(log, card_no, card_path,
+ drm_show_connector_name_and_state_callback);
+ mp_info(log, "\n");
+}
+
+static void drm_show_connector_modes_callback(struct mp_log *log, int card_no,
+ const drmModeConnector *connector)
+{
+ if (connector->connection != DRM_MODE_CONNECTED)
+ return;
+
+ char other_connector_name[MAX_CONNECTOR_NAME_LEN];
+ get_connector_name(connector, other_connector_name);
+ mp_info(log, "Available modes for drm-connector=%d.%s\n",
+ card_no, other_connector_name);
+ drm_show_available_modes(log, connector);
+ mp_info(log, "\n");
+}
+
+static void drm_show_available_connectors_and_modes(struct mp_log *log,
+ int card_no,
+ const char *card_path)
+{
+ drm_show_foreach_connector(log, card_no, card_path,
+ drm_show_connector_modes_callback);
+}
+
+static void drm_show_foreach_card(struct mp_log *log,
+ void (*show_fn)(struct mp_log *, int,
+ const char *))
+{
+ drmDevice *devices[DRM_MAX_MINOR] = { 0 };
+ int card_count = drmGetDevices2(0, devices, MP_ARRAY_SIZE(devices));
+ if (card_count < 0) {
+ mp_err(log, "Listing DRM devices with drmGetDevices failed! (%s)\n",
+ mp_strerror(errno));
+ return;
+ }
+
+ for (int i = 0; i < card_count; i++) {
+ drmDevice *dev = devices[i];
+
+ if (!(dev->available_nodes & (1 << DRM_NODE_PRIMARY)))
+ continue;
+
+ const char *card_path = dev->nodes[DRM_NODE_PRIMARY];
+
+ int fd = open_card_path(card_path);
+ if (fd < 0) {
+ mp_err(log, "Failed to open primary DRM node path %s!\n",
+ card_path);
+ continue;
+ }
+
+ close(fd);
+ show_fn(log, i, card_path);
+ }
+
+ drmFreeDevices(devices, card_count);
+}
+
+static void drm_show_available_cards_and_connectors(struct mp_log *log)
+{
+ drm_show_foreach_card(log, drm_show_available_connectors);
+}
+
+static void drm_show_available_cards_connectors_and_modes(struct mp_log *log)
+{
+ drm_show_foreach_card(log, drm_show_available_connectors_and_modes);
+}
+
+static int drm_connector_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ drm_show_available_cards_and_connectors(log);
+ return M_OPT_EXIT;
+}
+
+static int drm_mode_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ drm_show_available_cards_connectors_and_modes(log);
+ return M_OPT_EXIT;
+}
+
+static int drm_validate_mode_opt(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ const char *param = *value;
+ if (!parse_mode_spec(param, NULL)) {
+ mp_fatal(log, "Invalid value for option drm-mode. Must be a positive number, a string of the format WxH[@R] or 'help'\n");
+ return M_OPT_INVALID;
+ }
+
+ return 1;
+}
+
+/* Helpers */
+double vo_drm_get_display_fps(struct vo_drm_state *drm)
+{
+ return mode_get_Hz(&drm->mode.mode);
+}
+
+void vo_drm_set_monitor_par(struct vo *vo)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (vo->opts->force_monitor_aspect != 0.0) {
+ vo->monitor_par = drm->fb->width / (double) drm->fb->height /
+ vo->opts->force_monitor_aspect;
+ } else {
+ vo->monitor_par = 1 / vo->opts->monitor_pixel_aspect;
+ }
+ MP_VERBOSE(drm, "Monitor pixel aspect: %g\n", vo->monitor_par);
+}
+
+void vo_drm_wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (drm->vt_switcher_active) {
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10));
+ vt_switcher_poll(&drm->vt_switcher, timeout_ns);
+ } else {
+ vo_wait_default(vo, until_time_ns);
+ }
+}
+
+void vo_drm_wait_on_flip(struct vo_drm_state *drm)
+{
+ // poll page flip finish event
+ while (drm->waiting_for_flip) {
+ const int timeout_ms = 3000;
+ struct pollfd fds[1] = { { .events = POLLIN, .fd = drm->fd } };
+ poll(fds, 1, timeout_ms);
+ if (fds[0].revents & POLLIN) {
+ const int ret = drmHandleEvent(drm->fd, &drm->ev);
+ if (ret != 0) {
+ MP_ERR(drm, "drmHandleEvent failed: %i\n", ret);
+ return;
+ }
+ }
+ }
+}
+
+void vo_drm_wakeup(struct vo *vo)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (drm->vt_switcher_active)
+ vt_switcher_interrupt_poll(&drm->vt_switcher);
+}
diff --git a/video/out/drm_common.h b/video/out/drm_common.h
new file mode 100644
index 0000000..581151f
--- /dev/null
+++ b/video/out/drm_common.h
@@ -0,0 +1,108 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_VT_SWITCHER_H
+#define MP_VT_SWITCHER_H
+
+#include <stdbool.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include "vo.h"
+
+#define DRM_OPTS_FORMAT_XRGB8888 0
+#define DRM_OPTS_FORMAT_XRGB2101010 1
+#define DRM_OPTS_FORMAT_XBGR8888 2
+#define DRM_OPTS_FORMAT_XBGR2101010 3
+
+struct framebuffer {
+ int fd;
+ uint32_t width;
+ uint32_t height;
+ uint32_t stride;
+ uint32_t size;
+ uint32_t handle;
+ uint8_t *map;
+ uint32_t id;
+};
+
+struct drm_mode {
+ drmModeModeInfo mode;
+ uint32_t blob_id;
+};
+
+struct drm_opts {
+ char *device_path;
+ char *connector_spec;
+ char *mode_spec;
+ int drm_atomic;
+ int draw_plane;
+ int drmprime_video_plane;
+ int drm_format;
+ struct m_geometry draw_surface_size;
+ int vrr_enabled;
+};
+
+struct vt_switcher {
+ int tty_fd;
+ struct mp_log *log;
+ void (*handlers[2])(void*);
+ void *handler_data[2];
+};
+
+struct vo_drm_state {
+ drmModeConnector *connector;
+ drmModeEncoder *encoder;
+ drmEventContext ev;
+
+ struct drm_atomic_context *atomic_context;
+ struct drm_mode mode;
+ struct drm_opts *opts;
+ struct framebuffer *fb;
+ struct mp_log *log;
+ struct mp_present *present;
+ struct vo *vo;
+ struct vt_switcher vt_switcher;
+
+ bool active;
+ bool paused;
+ bool still;
+ bool vt_switcher_active;
+ bool waiting_for_flip;
+
+ char *card_path;
+ int card_no;
+ int fd;
+
+ uint32_t crtc_id;
+ uint32_t height;
+ uint32_t width;
+};
+
+bool vo_drm_init(struct vo *vo);
+int vo_drm_control(struct vo *vo, int *events, int request, void *arg);
+
+double vo_drm_get_display_fps(struct vo_drm_state *drm);
+void vo_drm_set_monitor_par(struct vo *vo);
+void vo_drm_uninit(struct vo *vo);
+void vo_drm_wait_events(struct vo *vo, int64_t until_time_ns);
+void vo_drm_wait_on_flip(struct vo_drm_state *drm);
+void vo_drm_wakeup(struct vo *vo);
+
+bool vo_drm_acquire_crtc(struct vo_drm_state *drm);
+void vo_drm_release_crtc(struct vo_drm_state *drm);
+
+#endif
diff --git a/video/out/drm_prime.c b/video/out/drm_prime.c
new file mode 100644
index 0000000..9335fa8
--- /dev/null
+++ b/video/out/drm_prime.c
@@ -0,0 +1,160 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include <drm_mode.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "drm_common.h"
+#include "drm_prime.h"
+
+int drm_prime_create_framebuffer(struct mp_log *log, int fd,
+ AVDRMFrameDescriptor *descriptor, int width,
+ int height, struct drm_prime_framebuffer *framebuffer,
+ struct drm_prime_handle_refs *handle_refs)
+{
+ AVDRMLayerDescriptor *layer = NULL;
+ uint32_t pitches[4] = { 0 };
+ uint32_t offsets[4] = { 0 };
+ uint32_t handles[4] = { 0 };
+ uint64_t modifiers[4] = { 0 };
+ int ret, layer_fd;
+
+ if (descriptor && descriptor->nb_layers) {
+ *framebuffer = (struct drm_prime_framebuffer){0};
+
+ for (int object = 0; object < descriptor->nb_objects; object++) {
+ ret = drmPrimeFDToHandle(fd, descriptor->objects[object].fd,
+ &framebuffer->gem_handles[object]);
+ if (ret < 0) {
+ mp_err(log, "Failed to retrieve the Prime Handle from handle %d (%d).\n",
+ object, descriptor->objects[object].fd);
+ goto fail;
+ }
+ modifiers[object] = descriptor->objects[object].format_modifier;
+ }
+
+ layer = &descriptor->layers[0];
+
+ for (int plane = 0; plane < AV_DRM_MAX_PLANES; plane++) {
+ layer_fd = framebuffer->gem_handles[layer->planes[plane].object_index];
+ if (layer_fd && layer->planes[plane].pitch) {
+ pitches[plane] = layer->planes[plane].pitch;
+ offsets[plane] = layer->planes[plane].offset;
+ handles[plane] = layer_fd;
+ } else {
+ pitches[plane] = 0;
+ offsets[plane] = 0;
+ handles[plane] = 0;
+ modifiers[plane] = 0;
+ }
+ }
+
+ ret = drmModeAddFB2WithModifiers(fd, width, height, layer->format,
+ handles, pitches, offsets,
+ modifiers, &framebuffer->fb_id,
+ DRM_MODE_FB_MODIFIERS);
+ if (ret < 0) {
+ ret = drmModeAddFB2(fd, width, height, layer->format,
+ handles, pitches, offsets,
+ &framebuffer->fb_id, 0);
+ if (ret < 0) {
+ mp_err(log, "Failed to create framebuffer with drmModeAddFB2 on layer %d: %s\n",
+ 0, mp_strerror(errno));
+ goto fail;
+ }
+ }
+
+ for (int plane = 0; plane < AV_DRM_MAX_PLANES; plane++) {
+ drm_prime_add_handle_ref(handle_refs, framebuffer->gem_handles[plane]);
+ }
+ }
+
+ return 0;
+
+fail:
+ memset(framebuffer, 0, sizeof(*framebuffer));
+ return -1;
+}
+
+void drm_prime_destroy_framebuffer(struct mp_log *log, int fd,
+ struct drm_prime_framebuffer *framebuffer,
+ struct drm_prime_handle_refs *handle_refs)
+{
+ if (framebuffer->fb_id)
+ drmModeRmFB(fd, framebuffer->fb_id);
+
+ for (int i = 0; i < AV_DRM_MAX_PLANES; i++) {
+ if (framebuffer->gem_handles[i]) {
+ drm_prime_remove_handle_ref(handle_refs,
+ framebuffer->gem_handles[i]);
+ if (!drm_prime_get_handle_ref_count(handle_refs,
+ framebuffer->gem_handles[i])) {
+ drmIoctl(fd, DRM_IOCTL_GEM_CLOSE, &framebuffer->gem_handles[i]);
+ }
+ }
+ }
+
+ memset(framebuffer, 0, sizeof(*framebuffer));
+}
+
+void drm_prime_init_handle_ref_count(void *talloc_parent,
+ struct drm_prime_handle_refs *handle_refs)
+{
+ handle_refs->handle_ref_count = talloc_zero(talloc_parent, uint32_t);
+ handle_refs->size = 1;
+ handle_refs->ctx = talloc_parent;
+}
+
+void drm_prime_add_handle_ref(struct drm_prime_handle_refs *handle_refs,
+ uint32_t handle)
+{
+ if (handle) {
+ if (handle > handle_refs->size) {
+ handle_refs->size = handle;
+ MP_TARRAY_GROW(handle_refs->ctx, handle_refs->handle_ref_count,
+ handle_refs->size);
+ }
+ handle_refs->handle_ref_count[handle - 1]++;
+ }
+}
+
+void drm_prime_remove_handle_ref(struct drm_prime_handle_refs *handle_refs,
+ uint32_t handle)
+{
+ if (handle) {
+ if (handle <= handle_refs->size &&
+ handle_refs->handle_ref_count[handle - 1])
+ {
+ handle_refs->handle_ref_count[handle - 1]--;
+ }
+ }
+}
+
+uint32_t drm_prime_get_handle_ref_count(struct drm_prime_handle_refs *handle_refs,
+ uint32_t handle)
+{
+ if (handle) {
+ if (handle <= handle_refs->size)
+ return handle_refs->handle_ref_count[handle - 1];
+ }
+ return 0;
+}
diff --git a/video/out/drm_prime.h b/video/out/drm_prime.h
new file mode 100644
index 0000000..69acba6
--- /dev/null
+++ b/video/out/drm_prime.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DRM_PRIME_H
+#define DRM_PRIME_H
+
+#include <libavutil/hwcontext_drm.h>
+
+#include "common/msg.h"
+
+struct drm_prime_framebuffer {
+ uint32_t fb_id;
+ uint32_t gem_handles[AV_DRM_MAX_PLANES];
+};
+
+struct drm_prime_handle_refs {
+ uint32_t *handle_ref_count;
+ size_t size;
+ void *ctx;
+};
+
+int drm_prime_create_framebuffer(struct mp_log *log, int fd, AVDRMFrameDescriptor *descriptor, int width, int height,
+ struct drm_prime_framebuffer *framebuffers,
+ struct drm_prime_handle_refs *handle_refs);
+void drm_prime_destroy_framebuffer(struct mp_log *log, int fd, struct drm_prime_framebuffer *framebuffers,
+ struct drm_prime_handle_refs *handle_refs);
+void drm_prime_init_handle_ref_count(void *talloc_parent, struct drm_prime_handle_refs *handle_refs);
+void drm_prime_add_handle_ref(struct drm_prime_handle_refs *handle_refs, uint32_t handle);
+void drm_prime_remove_handle_ref(struct drm_prime_handle_refs *handle_refs, uint32_t handle);
+uint32_t drm_prime_get_handle_ref_count(struct drm_prime_handle_refs *handle_refs, uint32_t handle);
+#endif // DRM_PRIME_H
diff --git a/video/out/filter_kernels.c b/video/out/filter_kernels.c
new file mode 100644
index 0000000..95d99ff
--- /dev/null
+++ b/video/out/filter_kernels.c
@@ -0,0 +1,411 @@
+/*
+ * Some of the filter code was taken from Glumpy:
+ * # Copyright (c) 2009-2016 Nicolas P. Rougier. All rights reserved.
+ * # Distributed under the (new) BSD License.
+ * (https://github.com/glumpy/glumpy/blob/master/glumpy/library/build-spatial-filters.py)
+ *
+ * Also see:
+ * - http://vector-agg.cvs.sourceforge.net/viewvc/vector-agg/agg-2.5/include/agg_image_filters.h
+ * - Vapoursynth plugin fmtconv (WTFPL Licensed), which is based on
+ * dither plugin for avisynth from the same author:
+ * https://github.com/vapoursynth/fmtconv/tree/master/src/fmtc
+ * - Paul Heckbert's "zoom"
+ * - XBMC: ConvolutionKernels.cpp etc.
+ *
+ * This file is part of mpv.
+ *
+ * This file can be distributed under the 3-clause license ("New BSD License").
+ *
+ * You can alternatively redistribute the non-Glumpy parts of this file and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "filter_kernels.h"
+#include "common/common.h"
+
+// NOTE: all filters are designed for discrete convolution
+
+const struct filter_window *mp_find_filter_window(const char *name)
+{
+ if (!name)
+ return NULL;
+ for (const struct filter_window *w = mp_filter_windows; w->name; w++) {
+ if (strcmp(w->name, name) == 0)
+ return w;
+ }
+ return NULL;
+}
+
+const struct filter_kernel *mp_find_filter_kernel(const char *name)
+{
+ if (!name)
+ return NULL;
+ for (const struct filter_kernel *k = mp_filter_kernels; k->f.name; k++) {
+ if (strcmp(k->f.name, name) == 0)
+ return k;
+ }
+ return NULL;
+}
+
+// sizes = sorted list of available filter sizes, terminated with size 0
+// inv_scale = source_size / dest_size
+bool mp_init_filter(struct filter_kernel *filter, const int *sizes,
+ double inv_scale)
+{
+ assert(filter->f.radius > 0);
+ double blur = filter->f.blur > 0.0 ? filter->f.blur : 1.0;
+ filter->radius = blur * filter->f.radius;
+
+ // Only downscaling requires widening the filter
+ filter->filter_scale = MPMAX(1.0, inv_scale);
+ double src_radius = filter->radius * filter->filter_scale;
+ // Polar filters are dependent solely on the radius
+ if (filter->polar) {
+ filter->size = 1; // Not meaningful for EWA/polar scalers.
+ // Safety precaution to avoid generating a gigantic shader
+ if (src_radius > 16.0) {
+ src_radius = 16.0;
+ filter->filter_scale = src_radius / filter->radius;
+ return false;
+ }
+ return true;
+ }
+ int size = ceil(2.0 * src_radius);
+ // round up to smallest available size that's still large enough
+ if (size < sizes[0])
+ size = sizes[0];
+ const int *cursize = sizes;
+ while (size > *cursize && *cursize)
+ cursize++;
+ if (*cursize) {
+ filter->size = *cursize;
+ return true;
+ } else {
+ // The filter doesn't fit - instead of failing completely, use the
+ // largest filter available. This is incorrect, but better than refusing
+ // to do anything.
+ filter->size = cursize[-1];
+ filter->filter_scale = (filter->size/2.0) / filter->radius;
+ return false;
+ }
+}
+
+// Sample from a blurred and tapered window
+static double sample_window(struct filter_window *kernel, double x)
+{
+ if (!kernel->weight)
+ return 1.0;
+
+ // All windows are symmetric, this makes life easier
+ x = fabs(x);
+
+ // Stretch and taper the window size as needed
+ x = kernel->blur > 0.0 ? x / kernel->blur : x;
+ x = x <= kernel->taper ? 0.0 : (x - kernel->taper) / (1 - kernel->taper);
+
+ if (x < kernel->radius)
+ return kernel->weight(kernel, x);
+ return 0.0;
+}
+
+// Evaluate a filter's kernel and window at a given absolute position
+static double sample_filter(struct filter_kernel *filter, double x)
+{
+ // The window is always stretched to the entire kernel
+ double w = sample_window(&filter->w, x / filter->radius * filter->w.radius);
+ double k = w * sample_window(&filter->f, x);
+ return k < 0 ? (1 - filter->clamp) * k : k;
+}
+
+// Calculate the 1D filtering kernel for N sample points.
+// N = number of samples, which is filter->size
+// The weights will be stored in out_w[0] to out_w[N - 1]
+// f = x0 - abs(x0), subpixel position in the range [0,1) or [0,1].
+static void mp_compute_weights(struct filter_kernel *filter, double f,
+ float *out_w)
+{
+ assert(filter->size > 0);
+ double sum = 0;
+ for (int n = 0; n < filter->size; n++) {
+ double x = f - (n - filter->size / 2 + 1);
+ double w = sample_filter(filter, x / filter->filter_scale);
+ out_w[n] = w;
+ sum += w;
+ }
+ // Normalize to preserve energy
+ for (int n = 0; n < filter->size; n++)
+ out_w[n] /= sum;
+}
+
+// Fill the given array with weights for the range [0.0, 1.0]. The array is
+// interpreted as rectangular array of count * filter->size items, with a
+// stride of `stride` floats in between each array element. (For polar filters,
+// the `count` indicates the row size and filter->size/stride are ignored)
+//
+// There will be slight sampling error if these weights are used in a OpenGL
+// texture as LUT directly. The sampling point of a texel is located at its
+// center, so out_array[0] will end up at 0.5 / count instead of 0.0.
+// Correct lookup requires a linear coordinate mapping from [0.0, 1.0] to
+// [0.5 / count, 1.0 - 0.5 / count].
+void mp_compute_lut(struct filter_kernel *filter, int count, int stride,
+ float *out_array)
+{
+ if (filter->polar) {
+ filter->radius_cutoff = 0.0;
+ // Compute a 1D array indexed by radius
+ for (int x = 0; x < count; x++) {
+ double r = x * filter->radius / (count - 1);
+ out_array[x] = sample_filter(filter, r);
+
+ if (fabs(out_array[x]) > 1e-3f)
+ filter->radius_cutoff = r;
+ }
+ } else {
+ // Compute a 2D array indexed by subpixel position
+ for (int n = 0; n < count; n++) {
+ mp_compute_weights(filter, n / (double)(count - 1),
+ out_array + stride * n);
+ }
+ }
+}
+
+typedef struct filter_window params;
+
+static double box(params *p, double x)
+{
+ // This is mathematically 1.0 everywhere, the clipping is done implicitly
+ // based on the radius.
+ return 1.0;
+}
+
+static double triangle(params *p, double x)
+{
+ return fmax(0.0, 1.0 - fabs(x / p->radius));
+}
+
+static double cosine(params *p, double x)
+{
+ return cos(x);
+}
+
+static double hanning(params *p, double x)
+{
+ return 0.5 + 0.5 * cos(M_PI * x);
+}
+
+static double hamming(params *p, double x)
+{
+ return 0.54 + 0.46 * cos(M_PI * x);
+}
+
+static double quadric(params *p, double x)
+{
+ if (x < 0.5) {
+ return 0.75 - x * x;
+ } else if (x < 1.5) {
+ double t = x - 1.5;
+ return 0.5 * t * t;
+ }
+ return 0.0;
+}
+
+static double bessel_i0(double x)
+{
+ double s = 1.0;
+ double y = x * x / 4.0;
+ double t = y;
+ int i = 2;
+ while (t > 1e-12) {
+ s += t;
+ t *= y / (i * i);
+ i += 1;
+ }
+ return s;
+}
+
+static double kaiser(params *p, double x)
+{
+ if (x > 1)
+ return 0;
+ double i0a = 1.0 / bessel_i0(p->params[0]);
+ return bessel_i0(p->params[0] * sqrt(1.0 - x * x)) * i0a;
+}
+
+static double blackman(params *p, double x)
+{
+ double a = p->params[0];
+ double a0 = (1-a)/2.0, a1 = 1/2.0, a2 = a/2.0;
+ double pix = M_PI * x;
+ return a0 + a1*cos(pix) + a2*cos(2 * pix);
+}
+
+static double welch(params *p, double x)
+{
+ return 1.0 - x*x;
+}
+
+// Family of cubic B/C splines
+static double cubic_bc(params *p, double x)
+{
+ double b = p->params[0],
+ c = p->params[1];
+ double p0 = (6.0 - 2.0 * b) / 6.0,
+ p2 = (-18.0 + 12.0 * b + 6.0 * c) / 6.0,
+ p3 = (12.0 - 9.0 * b - 6.0 * c) / 6.0,
+ q0 = (8.0 * b + 24.0 * c) / 6.0,
+ q1 = (-12.0 * b - 48.0 * c) / 6.0,
+ q2 = (6.0 * b + 30.0 * c) / 6.0,
+ q3 = (-b - 6.0 * c) / 6.0;
+
+ if (x < 1.0) {
+ return p0 + x * x * (p2 + x * p3);
+ } else if (x < 2.0) {
+ return q0 + x * (q1 + x * (q2 + x * q3));
+ }
+ return 0.0;
+}
+
+static double spline16(params *p, double x)
+{
+ if (x < 1.0) {
+ return ((x - 9.0/5.0 ) * x - 1.0/5.0 ) * x + 1.0;
+ } else {
+ return ((-1.0/3.0 * (x-1) + 4.0/5.0) * (x-1) - 7.0/15.0 ) * (x-1);
+ }
+}
+
+static double spline36(params *p, double x)
+{
+ if (x < 1.0) {
+ return ((13.0/11.0 * x - 453.0/209.0) * x - 3.0/209.0) * x + 1.0;
+ } else if (x < 2.0) {
+ return ((-6.0/11.0 * (x-1) + 270.0/209.0) * (x-1) - 156.0/ 209.0) * (x-1);
+ } else {
+ return ((1.0/11.0 * (x-2) - 45.0/209.0) * (x-2) + 26.0/209.0) * (x-2);
+ }
+}
+
+static double spline64(params *p, double x)
+{
+ if (x < 1.0) {
+ return ((49.0/41.0 * x - 6387.0/2911.0) * x - 3.0/2911.0) * x + 1.0;
+ } else if (x < 2.0) {
+ return ((-24.0/41.0 * (x-1) + 4032.0/2911.0) * (x-1) - 2328.0/2911.0) * (x-1);
+ } else if (x < 3.0) {
+ return ((6.0/41.0 * (x-2) - 1008.0/2911.0) * (x-2) + 582.0/2911.0) * (x-2);
+ } else {
+ return ((-1.0/41.0 * (x-3) + 168.0/2911.0) * (x-3) - 97.0/2911.0) * (x-3);
+ }
+}
+
+static double gaussian(params *p, double x)
+{
+ return exp(-2.0 * x * x / p->params[0]);
+}
+
+static double sinc(params *p, double x)
+{
+ if (fabs(x) < 1e-8)
+ return 1.0;
+ x *= M_PI;
+ return sin(x) / x;
+}
+
+static double jinc(params *p, double x)
+{
+ if (fabs(x) < 1e-8)
+ return 1.0;
+ x *= M_PI;
+ return 2.0 * j1(x) / x;
+}
+
+static double sphinx(params *p, double x)
+{
+ if (fabs(x) < 1e-8)
+ return 1.0;
+ x *= M_PI;
+ return 3.0 * (sin(x) - x * cos(x)) / (x * x * x);
+}
+
+const struct filter_window mp_filter_windows[] = {
+ {"box", 1, box},
+ {"triangle", 1, triangle},
+ {"bartlett", 1, triangle},
+ {"cosine", M_PI_2, cosine},
+ {"hanning", 1, hanning},
+ {"tukey", 1, hanning, .taper = 0.5},
+ {"hamming", 1, hamming},
+ {"quadric", 1.5, quadric},
+ {"welch", 1, welch},
+ {"kaiser", 1, kaiser, .params = {6.33, NAN} },
+ {"blackman", 1, blackman, .params = {0.16, NAN} },
+ {"gaussian", 2, gaussian, .params = {1.00, NAN} },
+ {"sinc", 1, sinc},
+ {"jinc", 1.2196698912665045, jinc},
+ {"sphinx", 1.4302966531242027, sphinx},
+ {0}
+};
+
+#define JINC_R3 3.2383154841662362
+#define JINC_R4 4.2410628637960699
+
+const struct filter_kernel mp_filter_kernels[] = {
+ // Spline filters
+ {{"spline16", 2, spline16}},
+ {{"spline36", 3, spline36}},
+ {{"spline64", 4, spline64}},
+ // Sinc filters
+ {{"sinc", 2, sinc, .resizable = true}},
+ {{"lanczos", 3, sinc, .resizable = true}, .window = "sinc"},
+ {{"ginseng", 3, sinc, .resizable = true}, .window = "jinc"},
+ // Jinc filters
+ {{"jinc", JINC_R3, jinc, .resizable = true}, .polar = true},
+ {{"ewa_lanczos", JINC_R3, jinc, .resizable = true}, .polar = true, .window = "jinc"},
+ {{"ewa_hanning", JINC_R3, jinc, .resizable = true}, .polar = true, .window = "hanning" },
+ {{"ewa_ginseng", JINC_R3, jinc, .resizable = true}, .polar = true, .window = "sinc"},
+ // Slightly sharpened to minimize the 1D step response error (to better
+ // preserve horizontal/vertical lines)
+ {{"ewa_lanczossharp", JINC_R3, jinc, .blur = 0.9812505837223707, .resizable = true},
+ .polar = true, .window = "jinc"},
+ // Similar to the above, but sharpened substantially to the point of
+ // minimizing the total impulse response error on an integer grid. Tends
+ // to preserve hash patterns well. Very sharp but rings a lot.
+ {{"ewa_lanczos4sharpest", JINC_R4, jinc, .blur = 0.8845120932605005, .resizable = true},
+ .polar = true, .window = "jinc"},
+ // Similar to the above, but softened instead, to make even/odd integer
+ // contributions exactly symmetrical. Designed to smooth out hash patterns.
+ {{"ewa_lanczossoft", JINC_R3, jinc, .blur = 1.0164667662867047, .resizable = true},
+ .polar = true, .window = "jinc"},
+ // Very soft (blurred) hanning-windowed jinc; removes almost all aliasing.
+ // Blur parameter picked to match orthogonal and diagonal contributions
+ {{"haasnsoft", JINC_R3, jinc, .blur = 1.11, .resizable = true},
+ .polar = true, .window = "hanning"},
+ // Cubic filters
+ {{"bicubic", 2, cubic_bc, .params = {1.0, 0.0} }},
+ {{"hermite", 1, cubic_bc, .params = {0.0, 0.0} }},
+ {{"catmull_rom", 2, cubic_bc, .params = {0.0, 0.5} }},
+ {{"mitchell", 2, cubic_bc, .params = {1.0/3.0, 1.0/3.0} }},
+ {{"robidoux", 2, cubic_bc, .params = {12 / (19 + 9 * M_SQRT2),
+ 113 / (58 + 216 * M_SQRT2)} }},
+ {{"robidouxsharp", 2, cubic_bc, .params = {6 / (13 + 7 * M_SQRT2),
+ 7 / (2 + 12 * M_SQRT2)} }},
+ {{"ewa_robidoux", 2, cubic_bc, .params = {12 / (19 + 9 * M_SQRT2),
+ 113 / (58 + 216 * M_SQRT2)}},
+ .polar = true},
+ {{"ewa_robidouxsharp", 2,cubic_bc, .params = {6 / (13 + 7 * M_SQRT2),
+ 7 / (2 + 12 * M_SQRT2)}},
+ .polar = true},
+ // Miscellaneous filters
+ {{"box", 1, box, .resizable = true}},
+ {{"nearest", 0.5, box}},
+ {{"triangle", 1, triangle, .resizable = true}},
+ {{"gaussian", 2, gaussian, .params = {1.0, NAN}, .resizable = true}},
+ {{0}}
+};
diff --git a/video/out/filter_kernels.h b/video/out/filter_kernels.h
new file mode 100644
index 0000000..b8b2f67
--- /dev/null
+++ b/video/out/filter_kernels.h
@@ -0,0 +1,56 @@
+/*
+ * This file is part of mpv.
+ *
+ * This file can be distributed under the 3-clause license ("New BSD License").
+ *
+ * You can alternatively redistribute the non-Glumpy parts of this file and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ */
+
+#ifndef MPLAYER_FILTER_KERNELS_H
+#define MPLAYER_FILTER_KERNELS_H
+
+#include <stdbool.h>
+
+struct filter_window {
+ const char *name;
+ double radius; // Preferred radius, should only be changed if resizable
+ double (*weight)(struct filter_window *k, double x);
+ bool resizable; // Filter supports any given radius
+ double params[2]; // User-defined custom filter parameters. Not used by
+ // all filters
+ double blur; // Blur coefficient (sharpens or widens the filter)
+ double taper; // Taper coefficient (flattens the filter's center)
+};
+
+struct filter_kernel {
+ struct filter_window f; // the kernel itself
+ struct filter_window w; // window storage
+ double clamp; // clamping factor, affects negative weights
+ // Constant values
+ const char *window; // default window
+ bool polar; // whether or not the filter uses polar coordinates
+ // The following values are set by mp_init_filter() at runtime.
+ int size; // number of coefficients (may depend on radius)
+ double radius; // true filter radius, derived from f.radius and f.blur
+ double filter_scale; // Factor to convert the mathematical filter
+ // function radius to the possibly wider
+ // (in the case of downsampling) filter sample
+ // radius.
+ double radius_cutoff; // the radius at which we can cut off the filter
+};
+
+extern const struct filter_window mp_filter_windows[];
+extern const struct filter_kernel mp_filter_kernels[];
+
+const struct filter_window *mp_find_filter_window(const char *name);
+const struct filter_kernel *mp_find_filter_kernel(const char *name);
+
+bool mp_init_filter(struct filter_kernel *filter, const int *sizes,
+ double scale);
+void mp_compute_lut(struct filter_kernel *filter, int count, int stride,
+ float *out_array);
+
+#endif /* MPLAYER_FILTER_KERNELS_H */
diff --git a/video/out/gpu/context.c b/video/out/gpu/context.c
new file mode 100644
index 0000000..5ce18af
--- /dev/null
+++ b/video/out/gpu/context.c
@@ -0,0 +1,277 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+
+#include "config.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_option.h"
+#include "video/out/vo.h"
+
+#include "context.h"
+#include "spirv.h"
+
+/* OpenGL */
+extern const struct ra_ctx_fns ra_ctx_glx;
+extern const struct ra_ctx_fns ra_ctx_x11_egl;
+extern const struct ra_ctx_fns ra_ctx_drm_egl;
+extern const struct ra_ctx_fns ra_ctx_wayland_egl;
+extern const struct ra_ctx_fns ra_ctx_wgl;
+extern const struct ra_ctx_fns ra_ctx_angle;
+extern const struct ra_ctx_fns ra_ctx_dxgl;
+extern const struct ra_ctx_fns ra_ctx_rpi;
+extern const struct ra_ctx_fns ra_ctx_android;
+
+/* Vulkan */
+extern const struct ra_ctx_fns ra_ctx_vulkan_wayland;
+extern const struct ra_ctx_fns ra_ctx_vulkan_win;
+extern const struct ra_ctx_fns ra_ctx_vulkan_xlib;
+extern const struct ra_ctx_fns ra_ctx_vulkan_android;
+extern const struct ra_ctx_fns ra_ctx_vulkan_display;
+extern const struct ra_ctx_fns ra_ctx_vulkan_mac;
+
+/* Direct3D 11 */
+extern const struct ra_ctx_fns ra_ctx_d3d11;
+
+/* No API */
+extern const struct ra_ctx_fns ra_ctx_wldmabuf;
+
+static const struct ra_ctx_fns *contexts[] = {
+#if HAVE_D3D11
+ &ra_ctx_d3d11,
+#endif
+
+// OpenGL contexts:
+#if HAVE_EGL_ANDROID
+ &ra_ctx_android,
+#endif
+#if HAVE_RPI
+ &ra_ctx_rpi,
+#endif
+#if HAVE_EGL_ANGLE_WIN32
+ &ra_ctx_angle,
+#endif
+#if HAVE_GL_WIN32
+ &ra_ctx_wgl,
+#endif
+#if HAVE_GL_DXINTEROP
+ &ra_ctx_dxgl,
+#endif
+#if HAVE_EGL_WAYLAND
+ &ra_ctx_wayland_egl,
+#endif
+#if HAVE_EGL_X11
+ &ra_ctx_x11_egl,
+#endif
+#if HAVE_GL_X11
+ &ra_ctx_glx,
+#endif
+#if HAVE_EGL_DRM
+ &ra_ctx_drm_egl,
+#endif
+
+// Vulkan contexts:
+#if HAVE_VULKAN
+
+#if HAVE_ANDROID
+ &ra_ctx_vulkan_android,
+#endif
+#if HAVE_WIN32_DESKTOP
+ &ra_ctx_vulkan_win,
+#endif
+#if HAVE_WAYLAND
+ &ra_ctx_vulkan_wayland,
+#endif
+#if HAVE_X11
+ &ra_ctx_vulkan_xlib,
+#endif
+#if HAVE_VK_KHR_DISPLAY
+ &ra_ctx_vulkan_display,
+#endif
+#if HAVE_COCOA && HAVE_SWIFT
+ &ra_ctx_vulkan_mac,
+#endif
+#endif
+
+/* No API contexts: */
+#if HAVE_DMABUF_WAYLAND
+ &ra_ctx_wldmabuf,
+#endif
+};
+
+static int ra_ctx_api_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ mp_info(log, "GPU APIs (contexts):\n");
+ mp_info(log, " auto (autodetect)\n");
+ for (int n = 0; n < MP_ARRAY_SIZE(contexts); n++) {
+ if (!contexts[n]->hidden)
+ mp_info(log, " %s (%s)\n", contexts[n]->type, contexts[n]->name);
+ }
+ return M_OPT_EXIT;
+}
+
+static int ra_ctx_validate_api(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ if (bstr_equals0(param, "auto"))
+ return 1;
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (bstr_equals0(param, contexts[i]->type) && !contexts[i]->hidden)
+ return 1;
+ }
+ return M_OPT_INVALID;
+}
+
+static int ra_ctx_context_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ mp_info(log, "GPU contexts (APIs):\n");
+ mp_info(log, " auto (autodetect)\n");
+ for (int n = 0; n < MP_ARRAY_SIZE(contexts); n++) {
+ if (!contexts[n]->hidden)
+ mp_info(log, " %s (%s)\n", contexts[n]->name, contexts[n]->type);
+ }
+ return M_OPT_EXIT;
+}
+
+static int ra_ctx_validate_context(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ if (bstr_equals0(param, "auto"))
+ return 1;
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (bstr_equals0(param, contexts[i]->name) && !contexts[i]->hidden)
+ return 1;
+ }
+ return M_OPT_INVALID;
+}
+
+// Create a VO window and create a RA context on it.
+// vo_flags: passed to the backend's create window function
+struct ra_ctx *ra_ctx_create(struct vo *vo, struct ra_ctx_opts opts)
+{
+ bool api_auto = !opts.context_type || strcmp(opts.context_type, "auto") == 0;
+ bool ctx_auto = !opts.context_name || strcmp(opts.context_name, "auto") == 0;
+
+ if (ctx_auto) {
+ MP_VERBOSE(vo, "Probing for best GPU context.\n");
+ opts.probing = true;
+ }
+
+ // Hack to silence backend (X11/Wayland/etc.) errors. Kill it once backends
+ // are separate from `struct vo`
+ bool old_probing = vo->probing;
+ vo->probing = opts.probing;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (contexts[i]->hidden)
+ continue;
+ if (!opts.probing && strcmp(contexts[i]->name, opts.context_name) != 0)
+ continue;
+ if (!api_auto && strcmp(contexts[i]->type, opts.context_type) != 0)
+ continue;
+
+ struct ra_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct ra_ctx) {
+ .vo = vo,
+ .global = vo->global,
+ .log = mp_log_new(ctx, vo->log, contexts[i]->type),
+ .opts = opts,
+ .fns = contexts[i],
+ };
+
+ MP_VERBOSE(ctx, "Initializing GPU context '%s'\n", ctx->fns->name);
+ if (contexts[i]->init(ctx)) {
+ vo->probing = old_probing;
+ return ctx;
+ }
+
+ talloc_free(ctx);
+ }
+
+ vo->probing = old_probing;
+
+ // If we've reached this point, then none of the contexts matched the name
+ // requested, or the backend creation failed for all of them.
+ if (!vo->probing)
+ MP_ERR(vo, "Failed initializing any suitable GPU context!\n");
+ return NULL;
+}
+
+struct ra_ctx *ra_ctx_create_by_name(struct vo *vo, const char *name)
+{
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (strcmp(name, contexts[i]->name) != 0)
+ continue;
+
+ struct ra_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct ra_ctx) {
+ .vo = vo,
+ .global = vo->global,
+ .log = mp_log_new(ctx, vo->log, contexts[i]->type),
+ .fns = contexts[i],
+ };
+
+ MP_VERBOSE(ctx, "Initializing GPU context '%s'\n", ctx->fns->name);
+ if (contexts[i]->init(ctx))
+ return ctx;
+ talloc_free(ctx);
+ }
+ return NULL;
+}
+
+void ra_ctx_destroy(struct ra_ctx **ctx_ptr)
+{
+ struct ra_ctx *ctx = *ctx_ptr;
+ if (!ctx)
+ return;
+
+ if (ctx->spirv && ctx->spirv->fns->uninit)
+ ctx->spirv->fns->uninit(ctx);
+
+ ctx->fns->uninit(ctx);
+ talloc_free(ctx);
+
+ *ctx_ptr = NULL;
+}
+
+#define OPT_BASE_STRUCT struct ra_ctx_opts
+const struct m_sub_options ra_ctx_conf = {
+ .opts = (const m_option_t[]) {
+ {"gpu-context",
+ OPT_STRING_VALIDATE(context_name, ra_ctx_validate_context),
+ .help = ra_ctx_context_help},
+ {"gpu-api",
+ OPT_STRING_VALIDATE(context_type, ra_ctx_validate_api),
+ .help = ra_ctx_api_help},
+ {"gpu-debug", OPT_BOOL(debug)},
+ {"gpu-sw", OPT_BOOL(allow_sw)},
+ {0}
+ },
+ .size = sizeof(struct ra_ctx_opts),
+};
diff --git a/video/out/gpu/context.h b/video/out/gpu/context.h
new file mode 100644
index 0000000..6788e6f
--- /dev/null
+++ b/video/out/gpu/context.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include "video/out/vo.h"
+#include "video/csputils.h"
+
+#include "ra.h"
+
+struct ra_ctx_opts {
+ bool allow_sw; // allow software renderers
+ bool want_alpha; // create an alpha framebuffer if possible
+ bool debug; // enable debugging layers/callbacks etc.
+ bool probing; // the backend was auto-probed
+ char *context_name; // filter by `ra_ctx_fns.name`
+ char *context_type; // filter by `ra_ctx_fns.type`
+};
+
+extern const struct m_sub_options ra_ctx_conf;
+
+struct ra_ctx {
+ struct vo *vo;
+ struct ra *ra;
+ struct mpv_global *global;
+ struct mp_log *log;
+
+ struct ra_ctx_opts opts;
+ const struct ra_ctx_fns *fns;
+ struct ra_swapchain *swapchain;
+ struct spirv_compiler *spirv;
+
+ void *priv;
+};
+
+// The functions that make up a ra_ctx.
+struct ra_ctx_fns {
+ const char *type; // API type (for --gpu-api)
+ const char *name; // name (for --gpu-context)
+
+ bool hidden; // hide the ra_ctx from users
+
+ // Resize the window, or create a new window if there isn't one yet.
+ // Currently, there is an unfortunate interaction with ctx->vo, and
+ // display size etc. are determined by it.
+ bool (*reconfig)(struct ra_ctx *ctx);
+
+ // This behaves exactly like vo_driver.control().
+ int (*control)(struct ra_ctx *ctx, int *events, int request, void *arg);
+
+ // These behave exactly like vo_driver.wakeup/wait_events. They are
+ // optional.
+ void (*wakeup)(struct ra_ctx *ctx);
+ void (*wait_events)(struct ra_ctx *ctx, int64_t until_time_ns);
+ void (*update_render_opts)(struct ra_ctx *ctx);
+
+ // Initialize/destroy the 'struct ra' and possibly the underlying VO backend.
+ // Not normally called by the user of the ra_ctx.
+ bool (*init)(struct ra_ctx *ctx);
+ void (*uninit)(struct ra_ctx *ctx);
+};
+
+// Extra struct for the swapchain-related functions so they can be easily
+// inherited from helpers.
+struct ra_swapchain {
+ struct ra_ctx *ctx;
+ struct priv *priv;
+ const struct ra_swapchain_fns *fns;
+};
+
+// Represents a framebuffer / render target
+struct ra_fbo {
+ struct ra_tex *tex;
+ bool flip; // rendering needs to be inverted
+
+ // Host system's colorspace that it will be interpreting
+ // the frame buffer as.
+ struct mp_colorspace color_space;
+};
+
+struct ra_swapchain_fns {
+ // Gets the current framebuffer depth in bits (0 if unknown). Optional.
+ int (*color_depth)(struct ra_swapchain *sw);
+
+ // Called when rendering starts. Returns NULL on failure. This must be
+ // followed by submit_frame, to submit the rendered frame. This function
+ // can also fail sporadically, and such errors should be ignored unless
+ // they persist.
+ bool (*start_frame)(struct ra_swapchain *sw, struct ra_fbo *out_fbo);
+
+ // Present the frame. Issued in lockstep with start_frame, with rendering
+ // commands in between. The `frame` is just there for timing data, for
+ // swapchains smart enough to do something with it.
+ bool (*submit_frame)(struct ra_swapchain *sw, const struct vo_frame *frame);
+
+ // Performs a buffer swap. This blocks for as long as necessary to meet
+ // params.swapchain_depth, or until the next vblank (for vsynced contexts)
+ void (*swap_buffers)(struct ra_swapchain *sw);
+
+ // See vo. Usually called after swap_buffers().
+ void (*get_vsync)(struct ra_swapchain *sw, struct vo_vsync_info *info);
+};
+
+// Create and destroy a ra_ctx. This also takes care of creating and destroying
+// the underlying `struct ra`, and perhaps the underlying VO backend.
+struct ra_ctx *ra_ctx_create(struct vo *vo, struct ra_ctx_opts opts);
+void ra_ctx_destroy(struct ra_ctx **ctx);
+
+// Special case of creating a ra_ctx while specifying a specific context by name.
+struct ra_ctx *ra_ctx_create_by_name(struct vo *vo, const char *name);
diff --git a/video/out/gpu/d3d11_helpers.c b/video/out/gpu/d3d11_helpers.c
new file mode 100644
index 0000000..30d9eae
--- /dev/null
+++ b/video/out/gpu/d3d11_helpers.c
@@ -0,0 +1,966 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <d3d11.h>
+#include <dxgi1_6.h>
+#include <versionhelpers.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/windows_utils.h"
+
+#include "d3d11_helpers.h"
+
+// Windows 8 enum value, not present in mingw-w64 headers
+#define DXGI_ADAPTER_FLAG_SOFTWARE (2)
+typedef HRESULT(WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory);
+
+static mp_once d3d11_once = MP_STATIC_ONCE_INITIALIZER;
+static PFN_D3D11_CREATE_DEVICE pD3D11CreateDevice = NULL;
+static PFN_CREATE_DXGI_FACTORY pCreateDXGIFactory1 = NULL;
+static void d3d11_load(void)
+{
+ HMODULE d3d11 = LoadLibraryW(L"d3d11.dll");
+ HMODULE dxgilib = LoadLibraryW(L"dxgi.dll");
+ if (!d3d11 || !dxgilib)
+ return;
+
+ pD3D11CreateDevice = (PFN_D3D11_CREATE_DEVICE)
+ GetProcAddress(d3d11, "D3D11CreateDevice");
+ pCreateDXGIFactory1 = (PFN_CREATE_DXGI_FACTORY)
+ GetProcAddress(dxgilib, "CreateDXGIFactory1");
+}
+
+static bool load_d3d11_functions(struct mp_log *log)
+{
+ mp_exec_once(&d3d11_once, d3d11_load);
+ if (!pD3D11CreateDevice || !pCreateDXGIFactory1) {
+ mp_fatal(log, "Failed to load base d3d11 functionality: "
+ "CreateDevice: %s, CreateDXGIFactory1: %s\n",
+ pD3D11CreateDevice ? "success" : "failure",
+ pCreateDXGIFactory1 ? "success": "failure");
+ return false;
+ }
+
+ return true;
+}
+
+#define D3D11_DXGI_ENUM(prefix, define) { case prefix ## define: return #define; }
+
+static const char *d3d11_get_format_name(DXGI_FORMAT fmt)
+{
+ switch (fmt) {
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, UNKNOWN);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G8X24_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D32_FLOAT_S8X24_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_FLOAT_X8X24_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, X32_TYPELESS_G8X24_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10A2_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10A2_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10A2_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R11G11B10_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R24G8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D24_UNORM_S8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R24_UNORM_X8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, X24_TYPELESS_G8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, A8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R1_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R9G9B9E5_SHAREDEXP);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_B8G8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, G8R8_G8B8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC1_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC1_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC1_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC2_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC2_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC2_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC3_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC3_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC3_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC4_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC4_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC4_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC5_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC5_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC5_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B5G6R5_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B5G5R5A1_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8A8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8X8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10_XR_BIAS_A2_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8A8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8A8_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8X8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8X8_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC6H_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC6H_UF16);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC6H_SF16);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC7_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC7_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC7_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, AYUV);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y410);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y416);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, NV12);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P010);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P016);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, 420_OPAQUE);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, YUY2);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y210);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y216);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, NV11);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, AI44);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, IA44);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P8);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, A8P8);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B4G4R4A4_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P208);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, V208);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, V408);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, FORCE_UINT);
+ default:
+ return "<Unknown>";
+ }
+}
+
+static const char *d3d11_get_csp_name(DXGI_COLOR_SPACE_TYPE csp)
+{
+ switch (csp) {
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G22_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G10_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G22_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G22_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RESERVED);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_NONE_P709_X601);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_LEFT_P601);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_LEFT_P601);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_LEFT_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_LEFT_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G2084_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G2084_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G2084_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G2084_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G22_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_GHLG_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_GHLG_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G24_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G24_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G24_LEFT_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G24_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G24_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, CUSTOM);
+ default:
+ return "<Unknown>";
+ }
+}
+
+static bool d3d11_get_mp_csp(DXGI_COLOR_SPACE_TYPE csp,
+ struct mp_colorspace *mp_csp)
+{
+ if (!mp_csp)
+ return false;
+
+ // Colorspaces utilizing gamma 2.2 (G22) are set to
+ // AUTO as that keeps the current default flow regarding
+ // SDR transfer function handling.
+ // (no adjustment is done unless the user has a CMS LUT).
+ //
+ // Additionally, only set primary information with colorspaces
+ // utilizing non-709 primaries to keep the current behavior
+ // regarding not doing conversion from BT.601 to BT.709.
+ switch (csp) {
+ case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709:
+ *mp_csp = (struct mp_colorspace){
+ .gamma = MP_CSP_TRC_AUTO,
+ .primaries = MP_CSP_PRIM_AUTO,
+ };
+ break;
+ case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709:
+ *mp_csp = (struct mp_colorspace) {
+ .gamma = MP_CSP_TRC_LINEAR,
+ .primaries = MP_CSP_PRIM_AUTO,
+ };
+ break;
+ case DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020:
+ *mp_csp = (struct mp_colorspace) {
+ .gamma = MP_CSP_TRC_PQ,
+ .primaries = MP_CSP_PRIM_BT_2020,
+ };
+ break;
+ case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020:
+ *mp_csp = (struct mp_colorspace) {
+ .gamma = MP_CSP_TRC_AUTO,
+ .primaries = MP_CSP_PRIM_BT_2020,
+ };
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+static bool query_output_format_and_colorspace(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_FORMAT *out_fmt,
+ DXGI_COLOR_SPACE_TYPE *out_cspace)
+{
+ IDXGIOutput *output = NULL;
+ IDXGIOutput6 *output6 = NULL;
+ DXGI_OUTPUT_DESC1 desc = { 0 };
+ char *monitor_name = NULL;
+ bool success = false;
+
+ if (!out_fmt || !out_cspace)
+ return false;
+
+ HRESULT hr = IDXGISwapChain_GetContainingOutput(swapchain, &output);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to get swap chain's containing output: %s!\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDXGIOutput_QueryInterface(output, &IID_IDXGIOutput6,
+ (void**)&output6);
+ if (FAILED(hr)) {
+ // point where systems older than Windows 10 would fail,
+ // thus utilizing error log level only with windows 10+
+ mp_msg(log, IsWindows10OrGreater() ? MSGL_ERR : MSGL_V,
+ "Failed to create a DXGI 1.6 output interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDXGIOutput6_GetDesc1(output6, &desc);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to query swap chain's output information: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ monitor_name = mp_to_utf8(NULL, desc.DeviceName);
+
+ mp_verbose(log, "Queried output: %s, %ldx%ld @ %d bits, colorspace: %s (%d)\n",
+ monitor_name,
+ desc.DesktopCoordinates.right - desc.DesktopCoordinates.left,
+ desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top,
+ desc.BitsPerColor,
+ d3d11_get_csp_name(desc.ColorSpace),
+ desc.ColorSpace);
+
+ *out_cspace = desc.ColorSpace;
+
+ // limit ourselves to the 8bit and 10bit formats for now.
+ // while the 16bit float format would be preferable as something
+ // to default to, it seems to be hard-coded to linear transfer
+ // in windowed mode, and follows configured colorspace in full screen.
+ *out_fmt = desc.BitsPerColor > 8 ?
+ DXGI_FORMAT_R10G10B10A2_UNORM : DXGI_FORMAT_R8G8B8A8_UNORM;
+
+ success = true;
+
+done:
+ talloc_free(monitor_name);
+ SAFE_RELEASE(output6);
+ SAFE_RELEASE(output);
+ return success;
+}
+
+// Get a const array of D3D_FEATURE_LEVELs from max_fl to min_fl (inclusive)
+static int get_feature_levels(int max_fl, int min_fl,
+ const D3D_FEATURE_LEVEL **out)
+{
+ static const D3D_FEATURE_LEVEL levels[] = {
+ D3D_FEATURE_LEVEL_12_1,
+ D3D_FEATURE_LEVEL_12_0,
+ D3D_FEATURE_LEVEL_11_1,
+ D3D_FEATURE_LEVEL_11_0,
+ D3D_FEATURE_LEVEL_10_1,
+ D3D_FEATURE_LEVEL_10_0,
+ D3D_FEATURE_LEVEL_9_3,
+ D3D_FEATURE_LEVEL_9_2,
+ D3D_FEATURE_LEVEL_9_1,
+ };
+ static const int levels_len = MP_ARRAY_SIZE(levels);
+
+ int start = 0;
+ for (; start < levels_len; start++) {
+ if (levels[start] <= max_fl)
+ break;
+ }
+ int len = 0;
+ for (; start + len < levels_len; len++) {
+ if (levels[start + len] < min_fl)
+ break;
+ }
+ *out = &levels[start];
+ return len;
+}
+
+static IDXGIAdapter1 *get_d3d11_adapter(struct mp_log *log,
+ struct bstr requested_adapter_name,
+ struct bstr *listing)
+{
+ HRESULT hr = S_OK;
+ IDXGIFactory1 *factory;
+ IDXGIAdapter1 *picked_adapter = NULL;
+
+ hr = pCreateDXGIFactory1(&IID_IDXGIFactory1, (void **)&factory);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to create a DXGI factory: %s\n",
+ mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+
+ for (unsigned int adapter_num = 0; hr != DXGI_ERROR_NOT_FOUND; adapter_num++)
+ {
+ IDXGIAdapter1 *adapter = NULL;
+ DXGI_ADAPTER_DESC1 desc = { 0 };
+ char *adapter_description = NULL;
+
+ hr = IDXGIFactory1_EnumAdapters1(factory, adapter_num, &adapter);
+ if (FAILED(hr)) {
+ if (hr != DXGI_ERROR_NOT_FOUND) {
+ mp_fatal(log, "Failed to enumerate at adapter %u\n",
+ adapter_num);
+ }
+ continue;
+ }
+
+ if (FAILED(IDXGIAdapter1_GetDesc1(adapter, &desc))) {
+ mp_fatal(log, "Failed to get adapter description when listing at adapter %u\n",
+ adapter_num);
+ continue;
+ }
+
+ adapter_description = mp_to_utf8(NULL, desc.Description);
+
+ if (listing) {
+ bstr_xappend_asprintf(NULL, listing,
+ "Adapter %u: vendor: %u, description: %s\n",
+ adapter_num, desc.VendorId,
+ adapter_description);
+ }
+
+ if (requested_adapter_name.len &&
+ bstr_case_startswith(bstr0(adapter_description),
+ requested_adapter_name))
+ {
+ picked_adapter = adapter;
+ }
+
+ talloc_free(adapter_description);
+
+ if (picked_adapter) {
+ break;
+ }
+
+ SAFE_RELEASE(adapter);
+ }
+
+ SAFE_RELEASE(factory);
+
+ return picked_adapter;
+}
+
+static HRESULT create_device(struct mp_log *log, IDXGIAdapter1 *adapter,
+ bool warp, bool debug, int max_fl, int min_fl,
+ ID3D11Device **dev)
+{
+ const D3D_FEATURE_LEVEL *levels;
+ int levels_len = get_feature_levels(max_fl, min_fl, &levels);
+ if (!levels_len) {
+ mp_fatal(log, "No suitable Direct3D feature level found\n");
+ return E_FAIL;
+ }
+
+ D3D_DRIVER_TYPE type = warp ? D3D_DRIVER_TYPE_WARP
+ : D3D_DRIVER_TYPE_HARDWARE;
+ UINT flags = debug ? D3D11_CREATE_DEVICE_DEBUG : 0;
+ return pD3D11CreateDevice((IDXGIAdapter *)adapter, adapter ? D3D_DRIVER_TYPE_UNKNOWN : type,
+ NULL, flags, levels, levels_len, D3D11_SDK_VERSION, dev, NULL, NULL);
+}
+
+bool mp_d3d11_list_or_verify_adapters(struct mp_log *log,
+ bstr adapter_name,
+ bstr *listing)
+{
+ IDXGIAdapter1 *picked_adapter = NULL;
+
+ if (!load_d3d11_functions(log)) {
+ return false;
+ }
+
+ if ((picked_adapter = get_d3d11_adapter(log, adapter_name, listing))) {
+ SAFE_RELEASE(picked_adapter);
+ return true;
+ }
+
+ return false;
+}
+
+// Create a Direct3D 11 device for rendering and presentation. This is meant to
+// reduce boilerplate in backends that D3D11, while also making sure they share
+// the same device creation logic and log the same information.
+bool mp_d3d11_create_present_device(struct mp_log *log,
+ struct d3d11_device_opts *opts,
+ ID3D11Device **dev_out)
+{
+ bool debug = opts->debug;
+ bool warp = opts->force_warp;
+ int max_fl = opts->max_feature_level;
+ int min_fl = opts->min_feature_level;
+ // Normalize nullptr and an empty string to nullptr to simplify handling.
+ char *adapter_name = (opts->adapter_name && *(opts->adapter_name)) ?
+ opts->adapter_name : NULL;
+ ID3D11Device *dev = NULL;
+ IDXGIDevice1 *dxgi_dev = NULL;
+ IDXGIAdapter1 *adapter = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ if (!load_d3d11_functions(log)) {
+ goto done;
+ }
+
+ adapter = get_d3d11_adapter(log, bstr0(adapter_name), NULL);
+
+ if (adapter_name && !adapter) {
+ mp_warn(log, "Adapter matching '%s' was not found in the system! "
+ "Will fall back to the default adapter.\n",
+ adapter_name);
+ }
+
+ // Return here to retry creating the device
+ do {
+ // Use these default feature levels if they are not set
+ max_fl = max_fl ? max_fl : D3D_FEATURE_LEVEL_11_0;
+ min_fl = min_fl ? min_fl : D3D_FEATURE_LEVEL_9_1;
+
+ hr = create_device(log, adapter, warp, debug, max_fl, min_fl, &dev);
+
+ // Retry without debug, if SDK is not available
+ if (debug && hr == DXGI_ERROR_SDK_COMPONENT_MISSING) {
+ mp_warn(log, "gpu-debug disabled due to error: %s\n", mp_HRESULT_to_str(hr));
+ debug = false;
+ continue;
+ }
+
+ if (SUCCEEDED(hr))
+ break;
+
+ // Trying to create a D3D_FEATURE_LEVEL_12_0 device on Windows 8.1 or
+ // below will not succeed. Try an 11_1 device.
+ if (max_fl >= D3D_FEATURE_LEVEL_12_0 &&
+ min_fl <= D3D_FEATURE_LEVEL_11_1)
+ {
+ mp_dbg(log, "Failed to create 12_0+ device, trying 11_1\n");
+ max_fl = D3D_FEATURE_LEVEL_11_1;
+ continue;
+ }
+
+ // Trying to create a D3D_FEATURE_LEVEL_11_1 device on Windows 7
+ // without the platform update will not succeed. Try an 11_0 device.
+ if (max_fl >= D3D_FEATURE_LEVEL_11_1 &&
+ min_fl <= D3D_FEATURE_LEVEL_11_0)
+ {
+ mp_dbg(log, "Failed to create 11_1+ device, trying 11_0\n");
+ max_fl = D3D_FEATURE_LEVEL_11_0;
+ continue;
+ }
+
+ // Retry with WARP if allowed
+ if (!warp && opts->allow_warp) {
+ mp_dbg(log, "Failed to create hardware device, trying WARP\n");
+ warp = true;
+ max_fl = opts->max_feature_level;
+ min_fl = opts->min_feature_level;
+ continue;
+ }
+
+ mp_fatal(log, "Failed to create Direct3D 11 device: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ } while (true);
+
+ // if we picked an adapter, release it here - we're taking another
+ // from the device.
+ SAFE_RELEASE(adapter);
+
+ hr = ID3D11Device_QueryInterface(dev, &IID_IDXGIDevice1, (void**)&dxgi_dev);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI device\n");
+ goto done;
+ }
+ hr = IDXGIDevice1_GetParent(dxgi_dev, &IID_IDXGIAdapter1, (void**)&adapter);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI adapter\n");
+ goto done;
+ }
+
+ IDXGIDevice1_SetMaximumFrameLatency(dxgi_dev, opts->max_frame_latency);
+
+ DXGI_ADAPTER_DESC1 desc;
+ hr = IDXGIAdapter1_GetDesc1(adapter, &desc);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get adapter description\n");
+ goto done;
+ }
+
+ D3D_FEATURE_LEVEL selected_level = ID3D11Device_GetFeatureLevel(dev);
+ mp_verbose(log, "Using Direct3D 11 feature level %u_%u\n",
+ ((unsigned)selected_level) >> 12,
+ (((unsigned)selected_level) >> 8) & 0xf);
+
+ char *dev_name = mp_to_utf8(NULL, desc.Description);
+ mp_verbose(log, "Device Name: %s\n"
+ "Device ID: %04x:%04x (rev %02x)\n"
+ "Subsystem ID: %04x:%04x\n"
+ "LUID: %08lx%08lx\n",
+ dev_name,
+ desc.VendorId, desc.DeviceId, desc.Revision,
+ LOWORD(desc.SubSysId), HIWORD(desc.SubSysId),
+ desc.AdapterLuid.HighPart, desc.AdapterLuid.LowPart);
+ talloc_free(dev_name);
+
+ if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
+ warp = true;
+ // If the primary display adapter is a software adapter, the
+ // DXGI_ADAPTER_FLAG_SOFTWARE flag won't be set, but the device IDs should
+ // still match the Microsoft Basic Render Driver
+ if (desc.VendorId == 0x1414 && desc.DeviceId == 0x8c)
+ warp = true;
+ if (warp) {
+ mp_msg(log, opts->force_warp ? MSGL_V : MSGL_WARN,
+ "Using a software adapter\n");
+ }
+
+ *dev_out = dev;
+ dev = NULL;
+ success = true;
+
+done:
+ SAFE_RELEASE(adapter);
+ SAFE_RELEASE(dxgi_dev);
+ SAFE_RELEASE(dev);
+ return success;
+}
+
+static HRESULT create_swapchain_1_2(ID3D11Device *dev, IDXGIFactory2 *factory,
+ struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ bool flip, DXGI_FORMAT format,
+ IDXGISwapChain **swapchain_out)
+{
+ IDXGISwapChain *swapchain = NULL;
+ IDXGISwapChain1 *swapchain1 = NULL;
+ HRESULT hr;
+
+ DXGI_SWAP_CHAIN_DESC1 desc = {
+ .Width = opts->width ? opts->width : 1,
+ .Height = opts->height ? opts->height : 1,
+ .Format = format,
+ .SampleDesc = { .Count = 1 },
+ .BufferUsage = opts->usage,
+ };
+
+ if (flip) {
+ // UNORDERED_ACCESS with FLIP_SEQUENTIAL seems to be buggy with
+ // Windows 7 drivers
+ if ((desc.BufferUsage & DXGI_USAGE_UNORDERED_ACCESS) &&
+ !IsWindows8OrGreater())
+ {
+ mp_verbose(log, "Disabling UNORDERED_ACCESS for flip-model "
+ "swapchain backbuffers in Windows 7\n");
+ desc.BufferUsage &= ~DXGI_USAGE_UNORDERED_ACCESS;
+ }
+
+ if (IsWindows10OrGreater()) {
+ desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
+ } else {
+ desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
+ }
+ desc.BufferCount = opts->length;
+ } else {
+ desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
+ desc.BufferCount = 1;
+ }
+
+ hr = IDXGIFactory2_CreateSwapChainForHwnd(factory, (IUnknown*)dev,
+ opts->window, &desc, NULL, NULL, &swapchain1);
+ if (FAILED(hr))
+ goto done;
+ hr = IDXGISwapChain1_QueryInterface(swapchain1, &IID_IDXGISwapChain,
+ (void**)&swapchain);
+ if (FAILED(hr))
+ goto done;
+
+ *swapchain_out = swapchain;
+ swapchain = NULL;
+
+done:
+ SAFE_RELEASE(swapchain1);
+ SAFE_RELEASE(swapchain);
+ return hr;
+}
+
+static HRESULT create_swapchain_1_1(ID3D11Device *dev, IDXGIFactory1 *factory,
+ struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ DXGI_FORMAT format,
+ IDXGISwapChain **swapchain_out)
+{
+ DXGI_SWAP_CHAIN_DESC desc = {
+ .BufferDesc = {
+ .Width = opts->width ? opts->width : 1,
+ .Height = opts->height ? opts->height : 1,
+ .Format = format,
+ },
+ .SampleDesc = { .Count = 1 },
+ .BufferUsage = opts->usage,
+ .BufferCount = 1,
+ .OutputWindow = opts->window,
+ .Windowed = TRUE,
+ .SwapEffect = DXGI_SWAP_EFFECT_DISCARD,
+ };
+
+ return IDXGIFactory1_CreateSwapChain(factory, (IUnknown*)dev, &desc,
+ swapchain_out);
+}
+
+static bool update_swapchain_format(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_FORMAT format)
+{
+ DXGI_SWAP_CHAIN_DESC desc;
+
+ HRESULT hr = IDXGISwapChain_GetDesc(swapchain, &desc);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to query swap chain's current state: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ hr = IDXGISwapChain_ResizeBuffers(swapchain, 0, desc.BufferDesc.Width,
+ desc.BufferDesc.Height,
+ format, 0);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Couldn't update swapchain format: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ return true;
+}
+
+static bool update_swapchain_color_space(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_COLOR_SPACE_TYPE color_space)
+{
+ IDXGISwapChain4 *swapchain4 = NULL;
+ const char *csp_name = d3d11_get_csp_name(color_space);
+ bool success = false;
+ HRESULT hr = E_FAIL;
+ unsigned int csp_support_flags;
+
+ hr = IDXGISwapChain_QueryInterface(swapchain, &IID_IDXGISwapChain4,
+ (void *)&(swapchain4));
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to create v4 swapchain for color space "
+ "configuration (%s)!\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDXGISwapChain4_CheckColorSpaceSupport(swapchain4,
+ color_space,
+ &csp_support_flags);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to check color space support for color space "
+ "%s (%d): %s!\n",
+ csp_name, color_space, mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ mp_verbose(log,
+ "Swapchain capabilities for color space %s (%d): "
+ "normal: %s, overlay: %s\n",
+ csp_name, color_space,
+ (csp_support_flags & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) ?
+ "yes" : "no",
+ (csp_support_flags & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_OVERLAY_PRESENT) ?
+ "yes" : "no");
+
+ if (!(csp_support_flags & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT)) {
+ mp_err(log, "Color space %s (%d) is not supported by this swapchain!\n",
+ csp_name, color_space);
+ goto done;
+ }
+
+ hr = IDXGISwapChain4_SetColorSpace1(swapchain4, color_space);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to set color space %s (%d) for this swapchain "
+ "(%s)!\n",
+ csp_name, color_space, mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ mp_verbose(log, "Swapchain successfully configured to color space %s (%d)!\n",
+ csp_name, color_space);
+
+ success = true;
+
+done:
+ SAFE_RELEASE(swapchain4);
+ return success;
+}
+
+static bool configure_created_swapchain(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_FORMAT requested_format,
+ DXGI_COLOR_SPACE_TYPE requested_csp,
+ struct mp_colorspace *configured_csp)
+{
+ DXGI_FORMAT probed_format = DXGI_FORMAT_UNKNOWN;
+ DXGI_FORMAT selected_format = DXGI_FORMAT_UNKNOWN;
+ DXGI_COLOR_SPACE_TYPE probed_colorspace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
+ DXGI_COLOR_SPACE_TYPE selected_colorspace;
+ const char *format_name = NULL;
+ const char *csp_name = NULL;
+ struct mp_colorspace mp_csp = { 0 };
+ bool mp_csp_mapped = false;
+
+ query_output_format_and_colorspace(log, swapchain,
+ &probed_format,
+ &probed_colorspace);
+
+
+ selected_format = requested_format != DXGI_FORMAT_UNKNOWN ?
+ requested_format :
+ (probed_format != DXGI_FORMAT_UNKNOWN ?
+ probed_format : DXGI_FORMAT_R8G8B8A8_UNORM);
+ selected_colorspace = requested_csp != -1 ?
+ requested_csp : probed_colorspace;
+ format_name = d3d11_get_format_name(selected_format);
+ csp_name = d3d11_get_csp_name(selected_colorspace);
+ mp_csp_mapped = d3d11_get_mp_csp(selected_colorspace, &mp_csp);
+
+ mp_verbose(log, "Selected swapchain format %s (%d), attempting "
+ "to utilize it.\n",
+ format_name, selected_format);
+
+ if (!update_swapchain_format(log, swapchain, selected_format)) {
+ return false;
+ }
+
+ if (!IsWindows10OrGreater()) {
+ // On older than Windows 10, query_output_format_and_colorspace
+ // will not change probed_colorspace, and even if a user sets
+ // a colorspace it will not get applied. Thus warn user in case a
+ // value was specifically set and finish.
+ if (requested_csp != -1) {
+ mp_warn(log, "User selected a D3D11 color space %s (%d), "
+ "but configuration of color spaces is only supported"
+ "from Windows 10! The default configuration has been "
+ "left as-is.\n",
+ csp_name, selected_colorspace);
+ }
+
+ return true;
+ }
+
+ if (!mp_csp_mapped) {
+ mp_warn(log, "Color space %s (%d) does not have an mpv color space "
+ "mapping! Overriding to standard sRGB!\n",
+ csp_name, selected_colorspace);
+ selected_colorspace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
+ d3d11_get_mp_csp(selected_colorspace, &mp_csp);
+ }
+
+ mp_verbose(log, "Selected swapchain color space %s (%d), attempting to "
+ "utilize it.\n",
+ csp_name, selected_colorspace);
+
+ if (!update_swapchain_color_space(log, swapchain, selected_colorspace)) {
+ return false;
+ }
+
+ if (configured_csp) {
+ *configured_csp = mp_csp;
+ }
+
+ return true;
+}
+
+// Create a Direct3D 11 swapchain
+bool mp_d3d11_create_swapchain(ID3D11Device *dev, struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ IDXGISwapChain **swapchain_out)
+{
+ IDXGIDevice1 *dxgi_dev = NULL;
+ IDXGIAdapter1 *adapter = NULL;
+ IDXGIFactory1 *factory = NULL;
+ IDXGIFactory2 *factory2 = NULL;
+ IDXGISwapChain *swapchain = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ hr = ID3D11Device_QueryInterface(dev, &IID_IDXGIDevice1, (void**)&dxgi_dev);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI device\n");
+ goto done;
+ }
+ hr = IDXGIDevice1_GetParent(dxgi_dev, &IID_IDXGIAdapter1, (void**)&adapter);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI adapter\n");
+ goto done;
+ }
+ hr = IDXGIAdapter1_GetParent(adapter, &IID_IDXGIFactory1, (void**)&factory);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI factory\n");
+ goto done;
+ }
+ hr = IDXGIFactory1_QueryInterface(factory, &IID_IDXGIFactory2,
+ (void**)&factory2);
+ if (FAILED(hr))
+ factory2 = NULL;
+
+ bool flip = factory2 && opts->flip;
+
+ // Return here to retry creating the swapchain
+ do {
+ if (factory2) {
+ // Create a DXGI 1.2+ (Windows 8+) swap chain if possible
+ hr = create_swapchain_1_2(dev, factory2, log, opts, flip,
+ DXGI_FORMAT_R8G8B8A8_UNORM, &swapchain);
+ } else {
+ // Fall back to DXGI 1.1 (Windows 7)
+ hr = create_swapchain_1_1(dev, factory, log, opts,
+ DXGI_FORMAT_R8G8B8A8_UNORM, &swapchain);
+ }
+ if (SUCCEEDED(hr))
+ break;
+
+ if (flip) {
+ mp_dbg(log, "Failed to create flip-model swapchain, trying bitblt\n");
+ flip = false;
+ continue;
+ }
+
+ mp_fatal(log, "Failed to create swapchain: %s\n", mp_HRESULT_to_str(hr));
+ goto done;
+ } while (true);
+
+ // Prevent DXGI from making changes to the VO window, otherwise it will
+ // hook the Alt+Enter keystroke and make it trigger an ugly transition to
+ // exclusive fullscreen mode instead of running the user-set command.
+ IDXGIFactory_MakeWindowAssociation(factory, opts->window,
+ DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER |
+ DXGI_MWA_NO_PRINT_SCREEN);
+
+ if (factory2) {
+ mp_verbose(log, "Using DXGI 1.2+\n");
+ } else {
+ mp_verbose(log, "Using DXGI 1.1\n");
+ }
+
+ configure_created_swapchain(log, swapchain, opts->format,
+ opts->color_space,
+ opts->configured_csp);
+
+ DXGI_SWAP_CHAIN_DESC scd = {0};
+ IDXGISwapChain_GetDesc(swapchain, &scd);
+ if (scd.SwapEffect == DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL ||
+ scd.SwapEffect == DXGI_SWAP_EFFECT_FLIP_DISCARD)
+ {
+ mp_verbose(log, "Using flip-model presentation\n");
+ } else {
+ mp_verbose(log, "Using bitblt-model presentation\n");
+ }
+
+ *swapchain_out = swapchain;
+ swapchain = NULL;
+ success = true;
+
+done:
+ SAFE_RELEASE(swapchain);
+ SAFE_RELEASE(factory2);
+ SAFE_RELEASE(factory);
+ SAFE_RELEASE(adapter);
+ SAFE_RELEASE(dxgi_dev);
+ return success;
+}
diff --git a/video/out/gpu/d3d11_helpers.h b/video/out/gpu/d3d11_helpers.h
new file mode 100644
index 0000000..c115d33
--- /dev/null
+++ b/video/out/gpu/d3d11_helpers.h
@@ -0,0 +1,103 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_D3D11_HELPERS_H_
+#define MP_D3D11_HELPERS_H_
+
+#include <stdbool.h>
+#include <windows.h>
+#include <d3d11.h>
+#include <dxgi1_2.h>
+
+#include "video/mp_image.h"
+
+#define D3D_FEATURE_LEVEL_12_0 (0xc000)
+#define D3D_FEATURE_LEVEL_12_1 (0xc100)
+
+#define DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709 ((DXGI_COLOR_SPACE_TYPE)20)
+#define DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020 ((DXGI_COLOR_SPACE_TYPE)21)
+#define DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709 ((DXGI_COLOR_SPACE_TYPE)22)
+#define DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020 ((DXGI_COLOR_SPACE_TYPE)23)
+#define DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020 ((DXGI_COLOR_SPACE_TYPE)24)
+
+struct d3d11_device_opts {
+ // Enable the debug layer (D3D11_CREATE_DEVICE_DEBUG)
+ bool debug;
+
+ // Allow a software (WARP) adapter. Note, sometimes a software adapter will
+ // be used even when allow_warp is false. This is because, on Windows 8 and
+ // up, if there are no hardware adapters, Windows will pretend the WARP
+ // adapter is the primary hardware adapter.
+ bool allow_warp;
+
+ // Always use a WARP adapter. This is mainly for testing purposes.
+ bool force_warp;
+
+ // The maximum number of pending frames allowed to be queued to a swapchain
+ int max_frame_latency;
+
+ // The maximum Direct3D 11 feature level to attempt to create
+ // If unset, defaults to D3D_FEATURE_LEVEL_11_0
+ int max_feature_level;
+
+ // The minimum Direct3D 11 feature level to attempt to create. If this is
+ // not supported, device creation will fail.
+ // If unset, defaults to D3D_FEATURE_LEVEL_9_1
+ int min_feature_level;
+
+ // The adapter name to utilize if a specific adapter is required
+ // If unset, the default adapter will be utilized when creating
+ // a device.
+ char *adapter_name;
+};
+
+bool mp_d3d11_list_or_verify_adapters(struct mp_log *log,
+ bstr adapter_name,
+ bstr *listing);
+
+bool mp_d3d11_create_present_device(struct mp_log *log,
+ struct d3d11_device_opts *opts,
+ ID3D11Device **dev_out);
+
+struct d3d11_swapchain_opts {
+ HWND window;
+ int width;
+ int height;
+ DXGI_FORMAT format;
+ DXGI_COLOR_SPACE_TYPE color_space;
+
+ // mp_colorspace mapping of the configured swapchain colorspace
+ // shall be written into this memory location if configuration
+ // succeeds. Will be ignored if NULL.
+ struct mp_colorspace *configured_csp;
+
+ // Use DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL if possible
+ bool flip;
+
+ // Number of surfaces in the swapchain
+ int length;
+
+ // The BufferUsage value for swapchain surfaces. This should probably
+ // contain DXGI_USAGE_RENDER_TARGET_OUTPUT.
+ DXGI_USAGE usage;
+};
+
+bool mp_d3d11_create_swapchain(ID3D11Device *dev, struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ IDXGISwapChain **swapchain_out);
+
+#endif
diff --git a/video/out/gpu/error_diffusion.c b/video/out/gpu/error_diffusion.c
new file mode 100644
index 0000000..c1ea542
--- /dev/null
+++ b/video/out/gpu/error_diffusion.c
@@ -0,0 +1,316 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+
+#include "error_diffusion.h"
+
+#include "common/common.h"
+
+#define GLSL(...) gl_sc_addf(sc, __VA_ARGS__)
+#define GLSLH(...) gl_sc_haddf(sc, __VA_ARGS__)
+
+// After a (y, x) -> (y, x + y * shift) mapping, find the right most column that
+// will be affected by the current column.
+static int compute_rightmost_shifted_column(const struct error_diffusion_kernel *k)
+{
+ int ret = 0;
+ for (int y = 0; y <= EF_MAX_DELTA_Y; y++) {
+ for (int x = EF_MIN_DELTA_X; x <= EF_MAX_DELTA_X; x++) {
+ if (k->pattern[y][x - EF_MIN_DELTA_X] != 0) {
+ int shifted_x = x + y * k->shift;
+
+ // The shift mapping guarantees current column (or left of it)
+ // won't be affected by error diffusion.
+ assert(shifted_x > 0);
+
+ ret = MPMAX(ret, shifted_x);
+ }
+ }
+ }
+ return ret;
+}
+
+const struct error_diffusion_kernel *mp_find_error_diffusion_kernel(const char *name)
+{
+ if (!name)
+ return NULL;
+ for (const struct error_diffusion_kernel *k = mp_error_diffusion_kernels;
+ k->name;
+ k++) {
+ if (strcmp(k->name, name) == 0)
+ return k;
+ }
+ return NULL;
+}
+
+int mp_ef_compute_shared_memory_size(const struct error_diffusion_kernel *k,
+ int height)
+{
+ // We add EF_MAX_DELTA_Y empty lines on the bottom to handle errors
+ // propagated out from bottom side.
+ int rows = height + EF_MAX_DELTA_Y;
+ int shifted_columns = compute_rightmost_shifted_column(k) + 1;
+
+ // The shared memory is an array of size rows*shifted_columns. Each element
+ // is a single uint for three RGB component.
+ return rows * shifted_columns * 4;
+}
+
+void pass_error_diffusion(struct gl_shader_cache *sc,
+ const struct error_diffusion_kernel *k,
+ int tex, int width, int height, int depth, int block_size)
+{
+ assert(block_size <= height);
+
+ // The parallel error diffusion works by applying the shift mapping first.
+ // Taking the Floyd and Steinberg algorithm for example. After applying
+ // the (y, x) -> (y, x + y * shift) mapping (with shift=2), all errors are
+ // propagated into the next few columns, which makes parallel processing on
+ // the same column possible.
+ //
+ // X 7/16 X 7/16
+ // 3/16 5/16 1/16 ==> 0 0 3/16 5/16 1/16
+
+ // Figuring out the size of rectangle containing all shifted pixels.
+ // The rectangle height is not changed.
+ int shifted_width = width + (height - 1) * k->shift;
+
+ // We process all pixels from the shifted rectangles column by column, with
+ // a single global work group of size |block_size|.
+ // Figuring out how many block are required to process all pixels. We need
+ // this explicitly to make the number of barrier() calls match.
+ int blocks = (height * shifted_width + block_size - 1) / block_size;
+
+ // If we figure out how many of the next columns will be affected while the
+ // current columns is being processed. We can store errors of only a few
+ // columns in the shared memory. Using a ring buffer will further save the
+ // cost while iterating to next column.
+ int ring_buffer_rows = height + EF_MAX_DELTA_Y;
+ int ring_buffer_columns = compute_rightmost_shifted_column(k) + 1;
+ int ring_buffer_size = ring_buffer_rows * ring_buffer_columns;
+
+ // Defines the ring buffer in shared memory.
+ GLSLH("shared uint err_rgb8[%d];\n", ring_buffer_size);
+
+ // Initialize the ring buffer.
+ GLSL("for (int i = int(gl_LocalInvocationIndex); i < %d; i += %d) ",
+ ring_buffer_size, block_size);
+ GLSL("err_rgb8[i] = 0u;\n");
+
+ GLSL("for (int block_id = 0; block_id < %d; ++block_id) {\n", blocks);
+
+ // Add barrier here to have previous block all processed before starting
+ // the processing of the next.
+ GLSL("groupMemoryBarrier();\n");
+ GLSL("barrier();\n");
+
+ // Compute the coordinate of the pixel we are currently processing, both
+ // before and after the shift mapping.
+ GLSL("int id = int(gl_LocalInvocationIndex) + block_id * %d;\n", block_size);
+ GLSL("int y = id %% %d, x_shifted = id / %d;\n", height, height);
+ GLSL("int x = x_shifted - y * %d;\n", k->shift);
+
+ // Proceed only if we are processing a valid pixel.
+ GLSL("if (0 <= x && x < %d) {\n", width);
+
+ // The index that the current pixel have on the ring buffer.
+ GLSL("int idx = (x_shifted * %d + y) %% %d;\n", ring_buffer_rows, ring_buffer_size);
+
+ // Fetch the current pixel.
+ GLSL("vec3 pix = texelFetch(texture%d, ivec2(x, y), 0).rgb;\n", tex);
+
+ // The dithering will quantize pixel value into multiples of 1/dither_quant.
+ int dither_quant = (1 << depth) - 1;
+
+ // We encode errors in RGB components into a single 32-bit unsigned integer.
+ // The error we propagate from the current pixel is in range of
+ // [-0.5 / dither_quant, 0.5 / dither_quant]. While not quite obvious, the
+ // sum of all errors been propagated into a pixel is also in the same range.
+ // It's possible to map errors in this range into [-127, 127], and use an
+ // unsigned 8-bit integer to store it (using standard two's complement).
+ // The three 8-bit unsigned integers can then be encoded into a single
+ // 32-bit unsigned integer, with two 4-bit padding to prevent addition
+ // operation overflows affecting other component. There are at most 12
+ // addition operations on each pixel, so 4-bit padding should be enough.
+ // The overflow from R component will be discarded.
+ //
+ // The following figure is how the encoding looks like.
+ //
+ // +------------------------------------+
+ // |RRRRRRRR|0000|GGGGGGGG|0000|BBBBBBBB|
+ // +------------------------------------+
+ //
+
+ // The bitshift position for R and G component.
+ int bitshift_r = 24, bitshift_g = 12;
+ // The multiplier we use to map [-0.5, 0.5] to [-127, 127].
+ int uint8_mul = 127 * 2;
+
+ // Adding the error previously propagated into current pixel, and clear it
+ // in the buffer.
+ GLSL("uint err_u32 = err_rgb8[idx] + %uu;\n",
+ (128u << bitshift_r) | (128u << bitshift_g) | 128u);
+ GLSL("pix = pix * %d.0 + vec3("
+ "int((err_u32 >> %d) & 255u) - 128,"
+ "int((err_u32 >> %d) & 255u) - 128,"
+ "int( err_u32 & 255u) - 128"
+ ") / %d.0;\n", dither_quant, bitshift_r, bitshift_g, uint8_mul);
+ GLSL("err_rgb8[idx] = 0u;\n");
+
+ // Write the dithered pixel.
+ GLSL("vec3 dithered = round(pix);\n");
+ GLSL("imageStore(out_image, ivec2(x, y), vec4(dithered / %d.0, 0.0));\n",
+ dither_quant);
+
+ GLSL("vec3 err_divided = (pix - dithered) * %d.0 / %d.0;\n",
+ uint8_mul, k->divisor);
+ GLSL("ivec3 tmp;\n");
+
+ // Group error propagation with same weight factor together, in order to
+ // reduce the number of annoying error encoding.
+ for (int dividend = 1; dividend <= k->divisor; dividend++) {
+ bool err_assigned = false;
+
+ for (int y = 0; y <= EF_MAX_DELTA_Y; y++) {
+ for (int x = EF_MIN_DELTA_X; x <= EF_MAX_DELTA_X; x++) {
+ if (k->pattern[y][x - EF_MIN_DELTA_X] != dividend)
+ continue;
+
+ if (!err_assigned) {
+ err_assigned = true;
+
+ GLSL("tmp = ivec3(round(err_divided * %d.0));\n", dividend);
+
+ GLSL("err_u32 = "
+ "(uint(tmp.r & 255) << %d)|"
+ "(uint(tmp.g & 255) << %d)|"
+ " uint(tmp.b & 255);\n",
+ bitshift_r, bitshift_g);
+ }
+
+ int shifted_x = x + y * k->shift;
+
+ // Unlike the right border, errors propagated out from left
+ // border will remain in the ring buffer. This will produce
+ // visible artifacts near the left border, especially for
+ // shift=3 kernels.
+ if (x < 0)
+ GLSL("if (x >= %d) ", -x);
+
+ // Calculate the new position in the ring buffer to propagate
+ // the error into.
+ int ring_buffer_delta = shifted_x * ring_buffer_rows + y;
+ GLSL("atomicAdd(err_rgb8[(idx + %d) %% %d], err_u32);\n",
+ ring_buffer_delta, ring_buffer_size);
+ }
+ }
+ }
+
+ GLSL("}\n"); // if (0 <= x && x < width)
+
+ GLSL("}\n"); // block_id
+}
+
+// Different kernels for error diffusion.
+// Patterns are from http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
+const struct error_diffusion_kernel mp_error_diffusion_kernels[] = {
+ {
+ .name = "simple",
+ .shift = 1,
+ .pattern = {{0, 0, 0, 1, 0},
+ {0, 0, 1, 0, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 2
+ },
+ {
+ // The "false" Floyd-Steinberg kernel
+ .name = "false-fs",
+ .shift = 1,
+ .pattern = {{0, 0, 0, 3, 0},
+ {0, 0, 3, 2, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 8
+ },
+ {
+ .name = "sierra-lite",
+ .shift = 2,
+ .pattern = {{0, 0, 0, 2, 0},
+ {0, 1, 1, 0, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 4
+ },
+ {
+ .name = "floyd-steinberg",
+ .shift = 2,
+ .pattern = {{0, 0, 0, 7, 0},
+ {0, 3, 5, 1, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 16
+ },
+ {
+ .name = "atkinson",
+ .shift = 2,
+ .pattern = {{0, 0, 0, 1, 1},
+ {0, 1, 1, 1, 0},
+ {0, 0, 1, 0, 0}},
+ .divisor = 8
+ },
+ // All kernels below have shift value of 3, and probably are too heavy for
+ // low end GPU.
+ {
+ .name = "jarvis-judice-ninke",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 7, 5},
+ {3, 5, 7, 5, 3},
+ {1, 3, 5, 3, 1}},
+ .divisor = 48
+ },
+ {
+ .name = "stucki",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 8, 4},
+ {2, 4, 8, 4, 2},
+ {1, 2, 4, 2, 1}},
+ .divisor = 42
+ },
+ {
+ .name = "burkes",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 8, 4},
+ {2, 4, 8, 4, 2},
+ {0, 0, 0, 0, 0}},
+ .divisor = 32
+ },
+ {
+ .name = "sierra-3",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 5, 3},
+ {2, 4, 5, 4, 2},
+ {0, 2, 3, 2, 0}},
+ .divisor = 32
+ },
+ {
+ .name = "sierra-2",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 4, 3},
+ {1, 2, 3, 2, 1},
+ {0, 0, 0, 0, 0}},
+ .divisor = 16
+ },
+ {0}
+};
diff --git a/video/out/gpu/error_diffusion.h b/video/out/gpu/error_diffusion.h
new file mode 100644
index 0000000..6bdcea1
--- /dev/null
+++ b/video/out/gpu/error_diffusion.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_GL_ERROR_DIFFUSION
+#define MP_GL_ERROR_DIFFUSION
+
+#include "shader_cache.h"
+
+// defines the border of all error diffusion kernels
+#define EF_MIN_DELTA_X (-2)
+#define EF_MAX_DELTA_X (2)
+#define EF_MAX_DELTA_Y (2)
+
+struct error_diffusion_kernel {
+ const char *name;
+
+ // The minimum value such that a (y, x) -> (y, x + y * shift) mapping will
+ // make all error pushing operations affect next column (and after it) only.
+ int shift;
+
+ // The diffusion factor for (y, x) is pattern[y][x - EF_MIN_DELTA_X] / divisor.
+ int pattern[EF_MAX_DELTA_Y + 1][EF_MAX_DELTA_X - EF_MIN_DELTA_X + 1];
+ int divisor;
+};
+
+extern const struct error_diffusion_kernel mp_error_diffusion_kernels[];
+
+const struct error_diffusion_kernel *mp_find_error_diffusion_kernel(const char *name);
+int mp_ef_compute_shared_memory_size(const struct error_diffusion_kernel *k, int height);
+void pass_error_diffusion(struct gl_shader_cache *sc,
+ const struct error_diffusion_kernel *k,
+ int tex, int width, int height, int depth, int block_size);
+
+#endif /* MP_GL_ERROR_DIFFUSION */
diff --git a/video/out/gpu/hwdec.c b/video/out/gpu/hwdec.c
new file mode 100644
index 0000000..c8098f3
--- /dev/null
+++ b/video/out/gpu/hwdec.c
@@ -0,0 +1,358 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <string.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "hwdec.h"
+
+extern const struct ra_hwdec_driver ra_hwdec_vaapi;
+extern const struct ra_hwdec_driver ra_hwdec_videotoolbox;
+extern const struct ra_hwdec_driver ra_hwdec_vdpau;
+extern const struct ra_hwdec_driver ra_hwdec_dxva2egl;
+extern const struct ra_hwdec_driver ra_hwdec_d3d11egl;
+extern const struct ra_hwdec_driver ra_hwdec_dxva2gldx;
+extern const struct ra_hwdec_driver ra_hwdec_d3d11va;
+extern const struct ra_hwdec_driver ra_hwdec_dxva2dxgi;
+extern const struct ra_hwdec_driver ra_hwdec_cuda;
+extern const struct ra_hwdec_driver ra_hwdec_rpi_overlay;
+extern const struct ra_hwdec_driver ra_hwdec_drmprime;
+extern const struct ra_hwdec_driver ra_hwdec_drmprime_overlay;
+extern const struct ra_hwdec_driver ra_hwdec_aimagereader;
+extern const struct ra_hwdec_driver ra_hwdec_vulkan;
+
+const struct ra_hwdec_driver *const ra_hwdec_drivers[] = {
+#if HAVE_VAAPI
+ &ra_hwdec_vaapi,
+#endif
+#if HAVE_VIDEOTOOLBOX_GL || HAVE_IOS_GL || HAVE_VIDEOTOOLBOX_PL
+ &ra_hwdec_videotoolbox,
+#endif
+#if HAVE_D3D_HWACCEL
+ #if HAVE_EGL_ANGLE
+ &ra_hwdec_d3d11egl,
+ #if HAVE_D3D9_HWACCEL
+ &ra_hwdec_dxva2egl,
+ #endif
+ #endif
+ #if HAVE_D3D11
+ &ra_hwdec_d3d11va,
+ #if HAVE_D3D9_HWACCEL
+ &ra_hwdec_dxva2dxgi,
+ #endif
+ #endif
+#endif
+#if HAVE_GL_DXINTEROP_D3D9
+ &ra_hwdec_dxva2gldx,
+#endif
+#if HAVE_CUDA_INTEROP
+ &ra_hwdec_cuda,
+#endif
+#if HAVE_VDPAU_GL_X11
+ &ra_hwdec_vdpau,
+#endif
+#if HAVE_RPI_MMAL
+ &ra_hwdec_rpi_overlay,
+#endif
+#if HAVE_DRM
+ &ra_hwdec_drmprime,
+ &ra_hwdec_drmprime_overlay,
+#endif
+#if HAVE_ANDROID_MEDIA_NDK
+ &ra_hwdec_aimagereader,
+#endif
+#if HAVE_VULKAN_INTEROP
+ &ra_hwdec_vulkan,
+#endif
+
+ NULL
+};
+
+struct ra_hwdec *ra_hwdec_load_driver(struct ra_ctx *ra_ctx,
+ struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_hwdec_devices *devs,
+ const struct ra_hwdec_driver *drv,
+ bool is_auto)
+{
+ struct ra_hwdec *hwdec = talloc(NULL, struct ra_hwdec);
+ *hwdec = (struct ra_hwdec) {
+ .driver = drv,
+ .log = mp_log_new(hwdec, log, drv->name),
+ .global = global,
+ .ra_ctx = ra_ctx,
+ .devs = devs,
+ .probing = is_auto,
+ .priv = talloc_zero_size(hwdec, drv->priv_size),
+ };
+ mp_verbose(log, "Loading hwdec driver '%s'\n", drv->name);
+ if (hwdec->driver->init(hwdec) < 0) {
+ ra_hwdec_uninit(hwdec);
+ mp_verbose(log, "Loading failed.\n");
+ return NULL;
+ }
+ return hwdec;
+}
+
+void ra_hwdec_uninit(struct ra_hwdec *hwdec)
+{
+ if (hwdec)
+ hwdec->driver->uninit(hwdec);
+ talloc_free(hwdec);
+}
+
+bool ra_hwdec_test_format(struct ra_hwdec *hwdec, int imgfmt)
+{
+ for (int n = 0; hwdec->driver->imgfmts[n]; n++) {
+ if (hwdec->driver->imgfmts[n] == imgfmt)
+ return true;
+ }
+ return false;
+}
+
+struct ra_hwdec_mapper *ra_hwdec_mapper_create(struct ra_hwdec *hwdec,
+ const struct mp_image_params *params)
+{
+ assert(ra_hwdec_test_format(hwdec, params->imgfmt));
+
+ struct ra_hwdec_mapper *mapper = talloc_ptrtype(NULL, mapper);
+ *mapper = (struct ra_hwdec_mapper){
+ .owner = hwdec,
+ .driver = hwdec->driver->mapper,
+ .log = hwdec->log,
+ .ra = hwdec->ra_ctx->ra,
+ .priv = talloc_zero_size(mapper, hwdec->driver->mapper->priv_size),
+ .src_params = *params,
+ .dst_params = *params,
+ };
+ if (mapper->driver->init(mapper) < 0)
+ ra_hwdec_mapper_free(&mapper);
+ return mapper;
+}
+
+void ra_hwdec_mapper_free(struct ra_hwdec_mapper **mapper)
+{
+ struct ra_hwdec_mapper *p = *mapper;
+ if (p) {
+ ra_hwdec_mapper_unmap(p);
+ p->driver->uninit(p);
+ talloc_free(p);
+ }
+ *mapper = NULL;
+}
+
+void ra_hwdec_mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ if (mapper->driver->unmap)
+ mapper->driver->unmap(mapper);
+
+ // Clean up after the image if the mapper didn't already
+ mp_image_unrefp(&mapper->src);
+}
+
+int ra_hwdec_mapper_map(struct ra_hwdec_mapper *mapper, struct mp_image *img)
+{
+ ra_hwdec_mapper_unmap(mapper);
+ mp_image_setrefp(&mapper->src, img);
+ if (mapper->driver->map(mapper) < 0) {
+ ra_hwdec_mapper_unmap(mapper);
+ return -1;
+ }
+ return 0;
+}
+
+static int ra_hwdec_validate_opt_full(struct mp_log *log, bool include_modes,
+ const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ bool help = bstr_equals0(param, "help");
+ if (help)
+ mp_info(log, "Available hwdecs:\n");
+ for (int n = 0; ra_hwdec_drivers[n]; n++) {
+ const struct ra_hwdec_driver *drv = ra_hwdec_drivers[n];
+ if (help) {
+ mp_info(log, " %s\n", drv->name);
+ } else if (bstr_equals0(param, drv->name)) {
+ return 1;
+ }
+ }
+ if (help) {
+ if (include_modes) {
+ mp_info(log, " auto (behavior depends on context)\n"
+ " all (load all hwdecs)\n"
+ " no (do not load any and block loading on demand)\n");
+ }
+ return M_OPT_EXIT;
+ }
+ if (!param.len)
+ return 1; // "" is treated specially
+ if (include_modes &&
+ (bstr_equals0(param, "all") || bstr_equals0(param, "auto") ||
+ bstr_equals0(param, "no")))
+ return 1;
+ mp_fatal(log, "No hwdec backend named '%.*s' found!\n", BSTR_P(param));
+ return M_OPT_INVALID;
+}
+
+int ra_hwdec_validate_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ return ra_hwdec_validate_opt_full(log, true, opt, name, value);
+}
+
+int ra_hwdec_validate_drivers_only_opt(struct mp_log *log,
+ const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ return ra_hwdec_validate_opt_full(log, false, opt, name, value);
+}
+
+static void load_add_hwdec(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ const struct ra_hwdec_driver *drv, bool is_auto)
+{
+ // Don't load duplicate hwdecs
+ for (int j = 0; j < ctx->num_hwdecs; j++) {
+ if (ctx->hwdecs[j]->driver == drv)
+ return;
+ }
+
+ struct ra_hwdec *hwdec =
+ ra_hwdec_load_driver(ctx->ra_ctx, ctx->log, ctx->global, devs, drv, is_auto);
+ if (hwdec)
+ MP_TARRAY_APPEND(NULL, ctx->hwdecs, ctx->num_hwdecs, hwdec);
+}
+
+static void load_hwdecs_all(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs)
+{
+ if (!ctx->loading_done) {
+ for (int n = 0; ra_hwdec_drivers[n]; n++)
+ load_add_hwdec(ctx, devs, ra_hwdec_drivers[n], true);
+ ctx->loading_done = true;
+ }
+}
+
+void ra_hwdec_ctx_init(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ const char *type, bool load_all_by_default)
+{
+ assert(ctx->ra_ctx);
+
+ /*
+ * By default, or if the option value is "auto", we will not pre-emptively
+ * load any interops, and instead allow them to be loaded on-demand.
+ *
+ * If the option value is "no", then no interops will be loaded now, and
+ * no interops will be loaded, even if requested later.
+ *
+ * If the option value is "all", then all interops will be loaded now, and
+ * obviously no interops will need to be loaded later.
+ *
+ * Finally, if a specific interop is requested, it will be loaded now, and
+ * other interops can be loaded, if requested later.
+ */
+ if (!type || !type[0] || strcmp(type, "auto") == 0) {
+ if (!load_all_by_default)
+ return;
+ type = "all";
+ }
+ if (strcmp(type, "no") == 0) {
+ // do nothing, just block further loading
+ } else if (strcmp(type, "all") == 0) {
+ load_hwdecs_all(ctx, devs);
+ } else {
+ for (int n = 0; ra_hwdec_drivers[n]; n++) {
+ const struct ra_hwdec_driver *drv = ra_hwdec_drivers[n];
+ if (strcmp(type, drv->name) == 0) {
+ load_add_hwdec(ctx, devs, drv, false);
+ break;
+ }
+ }
+ }
+ ctx->loading_done = true;
+}
+
+void ra_hwdec_ctx_uninit(struct ra_hwdec_ctx *ctx)
+{
+ for (int n = 0; n < ctx->num_hwdecs; n++)
+ ra_hwdec_uninit(ctx->hwdecs[n]);
+
+ talloc_free(ctx->hwdecs);
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+void ra_hwdec_ctx_load_fmt(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params)
+{
+ int imgfmt = params->imgfmt;
+ if (ctx->loading_done) {
+ /*
+ * If we previously marked interop loading as done (for reasons
+ * discussed above), then do not load any other interops regardless
+ * of imgfmt.
+ */
+ return;
+ }
+
+ if (imgfmt == IMGFMT_NONE) {
+ MP_VERBOSE(ctx, "Loading hwdec drivers for all formats\n");
+ load_hwdecs_all(ctx, devs);
+ return;
+ }
+
+ MP_VERBOSE(ctx, "Loading hwdec drivers for format: '%s'\n",
+ mp_imgfmt_to_name(imgfmt));
+ for (int i = 0; ra_hwdec_drivers[i]; i++) {
+ bool matched_fmt = false;
+ const struct ra_hwdec_driver *drv = ra_hwdec_drivers[i];
+ for (int j = 0; drv->imgfmts[j]; j++) {
+ if (imgfmt == drv->imgfmts[j]) {
+ matched_fmt = true;
+ break;
+ }
+ }
+ if (!matched_fmt) {
+ continue;
+ }
+
+ load_add_hwdec(ctx, devs, drv, params->probing);
+ }
+}
+
+struct ra_hwdec *ra_hwdec_get(struct ra_hwdec_ctx *ctx, int imgfmt)
+{
+ for (int n = 0; n < ctx->num_hwdecs; n++) {
+ if (ra_hwdec_test_format(ctx->hwdecs[n], imgfmt))
+ return ctx->hwdecs[n];
+ }
+
+ return NULL;
+}
+
+int ra_hwdec_driver_get_imgfmt_for_name(const char *name)
+{
+ for (int i = 0; ra_hwdec_drivers[i]; i++) {
+ if (!strcmp(ra_hwdec_drivers[i]->name, name)) {
+ return ra_hwdec_drivers[i]->imgfmts[0];
+ }
+ }
+ return IMGFMT_NONE;
+}
diff --git a/video/out/gpu/hwdec.h b/video/out/gpu/hwdec.h
new file mode 100644
index 0000000..7766073
--- /dev/null
+++ b/video/out/gpu/hwdec.h
@@ -0,0 +1,156 @@
+#ifndef MPGL_HWDEC_H_
+#define MPGL_HWDEC_H_
+
+#include "video/mp_image.h"
+#include "context.h"
+#include "ra.h"
+#include "video/hwdec.h"
+
+// Helper to organize/load hwdecs dynamically
+struct ra_hwdec_ctx {
+ // Set these before calling `ra_hwdec_ctx_init`
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct ra_ctx *ra_ctx;
+
+ bool loading_done;
+ struct ra_hwdec **hwdecs;
+ int num_hwdecs;
+};
+
+int ra_hwdec_validate_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+int ra_hwdec_validate_drivers_only_opt(struct mp_log *log,
+ const m_option_t *opt,
+ struct bstr name, const char **value);
+
+void ra_hwdec_ctx_init(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ const char *opt, bool load_all_by_default);
+void ra_hwdec_ctx_uninit(struct ra_hwdec_ctx *ctx);
+
+void ra_hwdec_ctx_load_fmt(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params);
+
+// Gets the right `ra_hwdec` for a format, if any
+struct ra_hwdec *ra_hwdec_get(struct ra_hwdec_ctx *ctx, int imgfmt);
+
+struct ra_hwdec {
+ const struct ra_hwdec_driver *driver;
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct ra_ctx *ra_ctx;
+ struct mp_hwdec_devices *devs;
+ // GLSL extensions required to sample textures from this.
+ const char **glsl_extensions;
+ // For free use by hwdec driver
+ void *priv;
+ // For working around the vdpau vs. vaapi mess.
+ bool probing;
+ // Used in overlay mode only.
+ float overlay_colorkey[4];
+};
+
+struct ra_hwdec_mapper {
+ const struct ra_hwdec_mapper_driver *driver;
+ struct mp_log *log;
+ struct ra *ra;
+ void *priv;
+ struct ra_hwdec *owner;
+ // Input frame parameters. (Set before init(), immutable.)
+ struct mp_image_params src_params;
+ // Output frame parameters (represents the format the textures return). Must
+ // be set by init(), immutable afterwards,
+ struct mp_image_params dst_params;
+
+ // The currently mapped source image (or the image about to be mapped in
+ // ->map()). NULL if unmapped. The mapper can also clear this reference if
+ // the mapped textures contain a full copy.
+ struct mp_image *src;
+
+ // The mapped textures and metadata about them. These fields change if a
+ // new frame is mapped (or unmapped), but otherwise remain constant.
+ // The common code won't mess with these, so you can e.g. set them in the
+ // .init() callback.
+ struct ra_tex *tex[4];
+};
+
+// This can be used to map frames of a specific hw format as GL textures.
+struct ra_hwdec_mapper_driver {
+ // Used to create ra_hwdec_mapper.priv.
+ size_t priv_size;
+
+ // Init the mapper implementation. At this point, the field src_params,
+ // fns, devs, priv are initialized.
+ int (*init)(struct ra_hwdec_mapper *mapper);
+ // Destroy the mapper. unmap is called before this.
+ void (*uninit)(struct ra_hwdec_mapper *mapper);
+
+ // Map mapper->src as texture, and set mapper->frame to textures using it.
+ // It is expected that the textures remain valid until the next unmap
+ // or uninit call.
+ // The function is allowed to unref mapper->src if it's not needed (i.e.
+ // this function creates a copy).
+ // The underlying format can change, so you might need to do some form
+ // of change detection. You also must reject unsupported formats with an
+ // error.
+ // On error, returns negative value on error and remains unmapped.
+ int (*map)(struct ra_hwdec_mapper *mapper);
+ // Unmap the frame. Does nothing if already unmapped. Optional.
+ void (*unmap)(struct ra_hwdec_mapper *mapper);
+};
+
+struct ra_hwdec_driver {
+ // Name of the interop backend. This is used for informational purposes and
+ // for use with debugging options.
+ const char *name;
+ // Used to create ra_hwdec.priv.
+ size_t priv_size;
+ // One of the hardware surface IMGFMT_ that must be passed to map_image later.
+ // Terminated with a 0 entry. (Extend the array size as needed.)
+ const int imgfmts[3];
+
+ // Create the hwdec device. It must add it to hw->devs, if applicable.
+ int (*init)(struct ra_hwdec *hw);
+ void (*uninit)(struct ra_hwdec *hw);
+
+ // This will be used to create a ra_hwdec_mapper from ra_hwdec.
+ const struct ra_hwdec_mapper_driver *mapper;
+
+ // The following function provides an alternative API. Each ra_hwdec_driver
+ // must have either provide a mapper or overlay_frame (not both or none), and
+ // if overlay_frame is set, it operates in overlay mode. In this mode,
+ // OSD etc. is rendered via OpenGL, but the video is rendered as a separate
+ // layer below it.
+ // Non-overlay mode is strictly preferred, so try not to use overlay mode.
+ // Set the given frame as overlay, replacing the previous one. This can also
+ // just change the position of the overlay.
+ // hw_image==src==dst==NULL is passed to clear the overlay.
+ int (*overlay_frame)(struct ra_hwdec *hw, struct mp_image *hw_image,
+ struct mp_rect *src, struct mp_rect *dst, bool newframe);
+};
+
+extern const struct ra_hwdec_driver *const ra_hwdec_drivers[];
+
+struct ra_hwdec *ra_hwdec_load_driver(struct ra_ctx *ra_ctx,
+ struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_hwdec_devices *devs,
+ const struct ra_hwdec_driver *drv,
+ bool is_auto);
+
+void ra_hwdec_uninit(struct ra_hwdec *hwdec);
+
+bool ra_hwdec_test_format(struct ra_hwdec *hwdec, int imgfmt);
+
+struct ra_hwdec_mapper *ra_hwdec_mapper_create(struct ra_hwdec *hwdec,
+ const struct mp_image_params *params);
+void ra_hwdec_mapper_free(struct ra_hwdec_mapper **mapper);
+void ra_hwdec_mapper_unmap(struct ra_hwdec_mapper *mapper);
+int ra_hwdec_mapper_map(struct ra_hwdec_mapper *mapper, struct mp_image *img);
+
+// Get the primary image format for the given driver name.
+// Returns IMGFMT_NONE if the name doesn't get matched.
+int ra_hwdec_driver_get_imgfmt_for_name(const char *name);
+
+#endif
diff --git a/video/out/gpu/lcms.c b/video/out/gpu/lcms.c
new file mode 100644
index 0000000..7006a96
--- /dev/null
+++ b/video/out/gpu/lcms.c
@@ -0,0 +1,526 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <math.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+
+#include "stream/stream.h"
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "common/msg.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "video/csputils.h"
+#include "lcms.h"
+
+#include "osdep/io.h"
+
+#if HAVE_LCMS2
+
+#include <lcms2.h>
+#include <libavutil/sha.h>
+#include <libavutil/mem.h>
+
+struct gl_lcms {
+ void *icc_data;
+ size_t icc_size;
+ struct AVBufferRef *vid_profile;
+ char *current_profile;
+ bool using_memory_profile;
+ bool changed;
+ enum mp_csp_prim current_prim;
+ enum mp_csp_trc current_trc;
+
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct mp_icc_opts *opts;
+};
+
+static void lcms2_error_handler(cmsContext ctx, cmsUInt32Number code,
+ const char *msg)
+{
+ struct gl_lcms *p = cmsGetContextUserData(ctx);
+ MP_ERR(p, "lcms2: %s\n", msg);
+}
+
+static void load_profile(struct gl_lcms *p)
+{
+ talloc_free(p->icc_data);
+ p->icc_data = NULL;
+ p->icc_size = 0;
+ p->using_memory_profile = false;
+ talloc_free(p->current_profile);
+ p->current_profile = NULL;
+
+ if (!p->opts->profile || !p->opts->profile[0])
+ return;
+
+ char *fname = mp_get_user_path(NULL, p->global, p->opts->profile);
+ MP_VERBOSE(p, "Opening ICC profile '%s'\n", fname);
+ struct bstr iccdata = stream_read_file(fname, p, p->global,
+ 100000000); // 100 MB
+ talloc_free(fname);
+ if (!iccdata.len)
+ return;
+
+ talloc_free(p->icc_data);
+
+ p->icc_data = iccdata.start;
+ p->icc_size = iccdata.len;
+ p->current_profile = talloc_strdup(p, p->opts->profile);
+}
+
+static void gl_lcms_destructor(void *ptr)
+{
+ struct gl_lcms *p = ptr;
+ av_buffer_unref(&p->vid_profile);
+}
+
+struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_icc_opts *opts)
+{
+ struct gl_lcms *p = talloc_ptrtype(talloc_ctx, p);
+ talloc_set_destructor(p, gl_lcms_destructor);
+ *p = (struct gl_lcms) {
+ .global = global,
+ .log = log,
+ .opts = opts,
+ };
+ gl_lcms_update_options(p);
+ return p;
+}
+
+void gl_lcms_update_options(struct gl_lcms *p)
+{
+ if ((p->using_memory_profile && !p->opts->profile_auto) ||
+ !bstr_equals(bstr0(p->opts->profile), bstr0(p->current_profile)))
+ {
+ load_profile(p);
+ }
+
+ p->changed = true; // probably
+}
+
+// Warning: profile.start must point to a ta allocation, and the function
+// takes over ownership.
+// Returns whether the internal profile was changed.
+bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile)
+{
+ if (!p->opts->profile_auto || (p->opts->profile && p->opts->profile[0])) {
+ talloc_free(profile.start);
+ return false;
+ }
+
+ if (p->using_memory_profile &&
+ p->icc_data && profile.start &&
+ profile.len == p->icc_size &&
+ memcmp(profile.start, p->icc_data, p->icc_size) == 0)
+ {
+ talloc_free(profile.start);
+ return false;
+ }
+
+ p->changed = true;
+ p->using_memory_profile = true;
+
+ talloc_free(p->icc_data);
+
+ p->icc_data = talloc_steal(p, profile.start);
+ p->icc_size = profile.len;
+
+ return true;
+}
+
+// Guards against NULL and uses bstr_equals to short-circuit some special cases
+static bool vid_profile_eq(struct AVBufferRef *a, struct AVBufferRef *b)
+{
+ if (!a || !b)
+ return a == b;
+
+ return bstr_equals((struct bstr){ a->data, a->size },
+ (struct bstr){ b->data, b->size });
+}
+
+// Return whether the profile or config has changed since the last time it was
+// retrieved. If it has changed, gl_lcms_get_lut3d() should be called.
+bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc, struct AVBufferRef *vid_profile)
+{
+ if (p->changed || p->current_prim != prim || p->current_trc != trc)
+ return true;
+
+ return !vid_profile_eq(p->vid_profile, vid_profile);
+}
+
+// Whether a profile is set. (gl_lcms_get_lut3d() is expected to return a lut,
+// but it could still fail due to runtime errors, such as invalid icc data.)
+bool gl_lcms_has_profile(struct gl_lcms *p)
+{
+ return p->icc_size > 0;
+}
+
+static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
+ cmsHPROFILE disp_profile,
+ enum mp_csp_prim prim, enum mp_csp_trc trc)
+{
+ if (p->opts->use_embedded && p->vid_profile) {
+ // Try using the embedded ICC profile
+ cmsHPROFILE prof = cmsOpenProfileFromMemTHR(cms, p->vid_profile->data,
+ p->vid_profile->size);
+ if (prof) {
+ MP_VERBOSE(p, "Successfully opened embedded ICC profile\n");
+ return prof;
+ }
+
+ // Otherwise, warn the user and generate the profile as usual
+ MP_WARN(p, "Video contained an invalid ICC profile! Ignoring...\n");
+ }
+
+ // The input profile for the transformation is dependent on the video
+ // primaries and transfer characteristics
+ struct mp_csp_primaries csp = mp_get_csp_primaries(prim);
+ cmsCIExyY wp_xyY = {csp.white.x, csp.white.y, 1.0};
+ cmsCIExyYTRIPLE prim_xyY = {
+ .Red = {csp.red.x, csp.red.y, 1.0},
+ .Green = {csp.green.x, csp.green.y, 1.0},
+ .Blue = {csp.blue.x, csp.blue.y, 1.0},
+ };
+
+ cmsToneCurve *tonecurve[3] = {0};
+ switch (trc) {
+ case MP_CSP_TRC_LINEAR: tonecurve[0] = cmsBuildGamma(cms, 1.0); break;
+ case MP_CSP_TRC_GAMMA18: tonecurve[0] = cmsBuildGamma(cms, 1.8); break;
+ case MP_CSP_TRC_GAMMA20: tonecurve[0] = cmsBuildGamma(cms, 2.0); break;
+ case MP_CSP_TRC_GAMMA22: tonecurve[0] = cmsBuildGamma(cms, 2.2); break;
+ case MP_CSP_TRC_GAMMA24: tonecurve[0] = cmsBuildGamma(cms, 2.4); break;
+ case MP_CSP_TRC_GAMMA26: tonecurve[0] = cmsBuildGamma(cms, 2.6); break;
+ case MP_CSP_TRC_GAMMA28: tonecurve[0] = cmsBuildGamma(cms, 2.8); break;
+
+ case MP_CSP_TRC_SRGB:
+ // Values copied from Little-CMS
+ tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
+ (double[5]){2.40, 1/1.055, 0.055/1.055, 1/12.92, 0.04045});
+ break;
+
+ case MP_CSP_TRC_PRO_PHOTO:
+ tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
+ (double[5]){1.8, 1.0, 0.0, 1/16.0, 0.03125});
+ break;
+
+ case MP_CSP_TRC_BT_1886: {
+ double src_black[3];
+ if (p->opts->contrast < 0) {
+ // User requested infinite contrast, return 2.4 profile
+ tonecurve[0] = cmsBuildGamma(cms, 2.4);
+ break;
+ } else if (p->opts->contrast > 0) {
+ MP_VERBOSE(p, "Using specified contrast: %d\n", p->opts->contrast);
+ for (int i = 0; i < 3; i++)
+ src_black[i] = 1.0 / p->opts->contrast;
+ } else {
+ // To build an appropriate BT.1886 transformation we need access to
+ // the display's black point, so we use LittleCMS' detection
+ // function. Relative colorimetric is used since we want to
+ // approximate the BT.1886 to the target device's actual black
+ // point even in e.g. perceptual mode
+ const int intent = MP_INTENT_RELATIVE_COLORIMETRIC;
+ cmsCIEXYZ bp_XYZ;
+ if (!cmsDetectBlackPoint(&bp_XYZ, disp_profile, intent, 0))
+ return false;
+
+ // Map this XYZ value back into the (linear) source space
+ cmsHPROFILE rev_profile;
+ cmsToneCurve *linear = cmsBuildGamma(cms, 1.0);
+ rev_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
+ (cmsToneCurve*[3]){linear, linear, linear});
+ cmsHPROFILE xyz_profile = cmsCreateXYZProfile();
+ cmsHTRANSFORM xyz2src = cmsCreateTransformTHR(cms,
+ xyz_profile, TYPE_XYZ_DBL, rev_profile, TYPE_RGB_DBL,
+ intent, cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
+ cmsFreeToneCurve(linear);
+ cmsCloseProfile(rev_profile);
+ cmsCloseProfile(xyz_profile);
+ if (!xyz2src)
+ return false;
+
+ cmsDoTransform(xyz2src, &bp_XYZ, src_black, 1);
+ cmsDeleteTransform(xyz2src);
+
+ double contrast = 3.0 / (src_black[0] + src_black[1] + src_black[2]);
+ MP_VERBOSE(p, "Detected ICC profile contrast: %f\n", contrast);
+ }
+
+ // Build the parametric BT.1886 transfer curve, one per channel
+ for (int i = 0; i < 3; i++) {
+ const double gamma = 2.40;
+ double binv = pow(src_black[i], 1.0/gamma);
+ tonecurve[i] = cmsBuildParametricToneCurve(cms, 6,
+ (double[4]){gamma, 1.0 - binv, binv, 0.0});
+ }
+ break;
+ }
+
+ default:
+ abort();
+ }
+
+ if (!tonecurve[0])
+ return false;
+
+ if (!tonecurve[1]) tonecurve[1] = tonecurve[0];
+ if (!tonecurve[2]) tonecurve[2] = tonecurve[0];
+
+ cmsHPROFILE *vid_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
+ tonecurve);
+
+ if (tonecurve[2] != tonecurve[0]) cmsFreeToneCurve(tonecurve[2]);
+ if (tonecurve[1] != tonecurve[0]) cmsFreeToneCurve(tonecurve[1]);
+ cmsFreeToneCurve(tonecurve[0]);
+
+ return vid_profile;
+}
+
+bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
+ enum mp_csp_prim prim, enum mp_csp_trc trc,
+ struct AVBufferRef *vid_profile)
+{
+ int s_r, s_g, s_b;
+ bool result = false;
+
+ p->changed = false;
+ p->current_prim = prim;
+ p->current_trc = trc;
+
+ // We need to hold on to a reference to the video's ICC profile for as long
+ // as we still need to perform equality checking, so generate a new
+ // reference here
+ av_buffer_unref(&p->vid_profile);
+ if (vid_profile) {
+ MP_VERBOSE(p, "Got an embedded ICC profile.\n");
+ p->vid_profile = av_buffer_ref(vid_profile);
+ MP_HANDLE_OOM(p->vid_profile);
+ }
+
+ if (!gl_parse_3dlut_size(p->opts->size_str, &s_r, &s_g, &s_b))
+ return false;
+
+ if (!gl_lcms_has_profile(p))
+ return false;
+
+ // For simplicity, default to 65x65x65, which is large enough to cover
+ // typical profiles with good accuracy while not being too wasteful
+ s_r = s_r ? s_r : 65;
+ s_g = s_g ? s_g : 65;
+ s_b = s_b ? s_b : 65;
+
+ void *tmp = talloc_new(NULL);
+ uint16_t *output = talloc_array(tmp, uint16_t, s_r * s_g * s_b * 4);
+ struct lut3d *lut = NULL;
+ cmsContext cms = NULL;
+
+ char *cache_file = NULL;
+ if (p->opts->cache) {
+ // Gamma is included in the header to help uniquely identify it,
+ // because we may change the parameter in the future or make it
+ // customizable, same for the primaries.
+ char *cache_info = talloc_asprintf(tmp,
+ "ver=1.4, intent=%d, size=%dx%dx%d, prim=%d, trc=%d, "
+ "contrast=%d\n",
+ p->opts->intent, s_r, s_g, s_b, prim, trc, p->opts->contrast);
+
+ uint8_t hash[32];
+ struct AVSHA *sha = av_sha_alloc();
+ MP_HANDLE_OOM(sha);
+ av_sha_init(sha, 256);
+ av_sha_update(sha, cache_info, strlen(cache_info));
+ if (vid_profile)
+ av_sha_update(sha, vid_profile->data, vid_profile->size);
+ av_sha_update(sha, p->icc_data, p->icc_size);
+ av_sha_final(sha, hash);
+ av_free(sha);
+
+ char *cache_dir = p->opts->cache_dir;
+ if (cache_dir && cache_dir[0]) {
+ cache_dir = mp_get_user_path(tmp, p->global, cache_dir);
+ } else {
+ cache_dir = mp_find_user_file(tmp, p->global, "cache", "");
+ }
+
+ if (cache_dir && cache_dir[0]) {
+ cache_file = talloc_strdup(tmp, "");
+ for (int i = 0; i < sizeof(hash); i++)
+ cache_file = talloc_asprintf_append(cache_file, "%02X", hash[i]);
+ cache_file = mp_path_join(tmp, cache_dir, cache_file);
+ mp_mkdirp(cache_dir);
+ }
+ }
+
+ // check cache
+ if (cache_file && stat(cache_file, &(struct stat){0}) == 0) {
+ MP_VERBOSE(p, "Opening 3D LUT cache in file '%s'.\n", cache_file);
+ struct bstr cachedata = stream_read_file(cache_file, tmp, p->global,
+ 1000000000); // 1 GB
+ if (cachedata.len == talloc_get_size(output)) {
+ memcpy(output, cachedata.start, cachedata.len);
+ goto done;
+ } else {
+ MP_WARN(p, "3D LUT cache invalid!\n");
+ }
+ }
+
+ cms = cmsCreateContext(NULL, p);
+ if (!cms)
+ goto error_exit;
+ cmsSetLogErrorHandlerTHR(cms, lcms2_error_handler);
+
+ cmsHPROFILE profile =
+ cmsOpenProfileFromMemTHR(cms, p->icc_data, p->icc_size);
+ if (!profile)
+ goto error_exit;
+
+ cmsHPROFILE vid_hprofile = get_vid_profile(p, cms, profile, prim, trc);
+ if (!vid_hprofile) {
+ cmsCloseProfile(profile);
+ goto error_exit;
+ }
+
+ cmsHTRANSFORM trafo = cmsCreateTransformTHR(cms, vid_hprofile, TYPE_RGB_16,
+ profile, TYPE_RGBA_16,
+ p->opts->intent,
+ cmsFLAGS_NOCACHE |
+ cmsFLAGS_NOOPTIMIZE |
+ cmsFLAGS_BLACKPOINTCOMPENSATION);
+ cmsCloseProfile(profile);
+ cmsCloseProfile(vid_hprofile);
+
+ if (!trafo)
+ goto error_exit;
+
+ // transform a (s_r)x(s_g)x(s_b) cube, with 3 components per channel
+ uint16_t *input = talloc_array(tmp, uint16_t, s_r * 3);
+ for (int b = 0; b < s_b; b++) {
+ for (int g = 0; g < s_g; g++) {
+ for (int r = 0; r < s_r; r++) {
+ input[r * 3 + 0] = r * 65535 / (s_r - 1);
+ input[r * 3 + 1] = g * 65535 / (s_g - 1);
+ input[r * 3 + 2] = b * 65535 / (s_b - 1);
+ }
+ size_t base = (b * s_r * s_g + g * s_r) * 4;
+ cmsDoTransform(trafo, input, output + base, s_r);
+ }
+ }
+
+ cmsDeleteTransform(trafo);
+
+ if (cache_file) {
+ FILE *out = fopen(cache_file, "wb");
+ if (out) {
+ fwrite(output, talloc_get_size(output), 1, out);
+ fclose(out);
+ }
+ }
+
+done: ;
+
+ lut = talloc_ptrtype(NULL, lut);
+ *lut = (struct lut3d) {
+ .data = talloc_steal(lut, output),
+ .size = {s_r, s_g, s_b},
+ };
+
+ *result_lut3d = lut;
+ result = true;
+
+error_exit:
+
+ if (cms)
+ cmsDeleteContext(cms);
+
+ if (!lut)
+ MP_FATAL(p, "Error loading ICC profile.\n");
+
+ talloc_free(tmp);
+ return result;
+}
+
+#else /* HAVE_LCMS2 */
+
+struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_icc_opts *opts)
+{
+ return (struct gl_lcms *) talloc_new(talloc_ctx);
+}
+
+void gl_lcms_update_options(struct gl_lcms *p) { }
+bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile) {return false;}
+
+bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc, struct AVBufferRef *vid_profile)
+{
+ return false;
+}
+
+bool gl_lcms_has_profile(struct gl_lcms *p)
+{
+ return false;
+}
+
+bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
+ enum mp_csp_prim prim, enum mp_csp_trc trc,
+ struct AVBufferRef *vid_profile)
+{
+ return false;
+}
+
+#endif
+
+static int validate_3dlut_size_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ int p1, p2, p3;
+ return gl_parse_3dlut_size(*value, &p1, &p2, &p3) ? 0 : M_OPT_INVALID;
+}
+
+#define OPT_BASE_STRUCT struct mp_icc_opts
+const struct m_sub_options mp_icc_conf = {
+ .opts = (const m_option_t[]) {
+ {"use-embedded-icc-profile", OPT_BOOL(use_embedded)},
+ {"icc-profile", OPT_STRING(profile), .flags = M_OPT_FILE},
+ {"icc-profile-auto", OPT_BOOL(profile_auto)},
+ {"icc-cache", OPT_BOOL(cache)},
+ {"icc-cache-dir", OPT_STRING(cache_dir), .flags = M_OPT_FILE},
+ {"icc-intent", OPT_INT(intent)},
+ {"icc-force-contrast", OPT_CHOICE(contrast, {"no", 0}, {"inf", -1}),
+ M_RANGE(0, 1000000)},
+ {"icc-3dlut-size", OPT_STRING_VALIDATE(size_str, validate_3dlut_size_opt)},
+ {"icc-use-luma", OPT_BOOL(icc_use_luma)},
+ {0}
+ },
+ .size = sizeof(struct mp_icc_opts),
+ .defaults = &(const struct mp_icc_opts) {
+ .size_str = "auto",
+ .intent = MP_INTENT_RELATIVE_COLORIMETRIC,
+ .use_embedded = true,
+ .cache = true,
+ },
+};
diff --git a/video/out/gpu/lcms.h b/video/out/gpu/lcms.h
new file mode 100644
index 0000000..607353a
--- /dev/null
+++ b/video/out/gpu/lcms.h
@@ -0,0 +1,61 @@
+#ifndef MP_GL_LCMS_H
+#define MP_GL_LCMS_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include "misc/bstr.h"
+#include "video/csputils.h"
+#include <libavutil/buffer.h>
+
+extern const struct m_sub_options mp_icc_conf;
+
+struct mp_icc_opts {
+ bool use_embedded;
+ char *profile;
+ bool profile_auto;
+ bool cache;
+ char *cache_dir;
+ char *size_str;
+ int intent;
+ int contrast;
+ bool icc_use_luma;
+};
+
+struct lut3d {
+ uint16_t *data;
+ int size[3];
+};
+
+struct mp_log;
+struct mpv_global;
+struct gl_lcms;
+
+struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_icc_opts *opts);
+void gl_lcms_update_options(struct gl_lcms *p);
+bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile);
+bool gl_lcms_has_profile(struct gl_lcms *p);
+bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **,
+ enum mp_csp_prim prim, enum mp_csp_trc trc,
+ struct AVBufferRef *vid_profile);
+bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc, struct AVBufferRef *vid_profile);
+
+static inline bool gl_parse_3dlut_size(const char *arg, int *p1, int *p2, int *p3)
+{
+ if (!strcmp(arg, "auto")) {
+ *p1 = *p2 = *p3 = 0;
+ return true;
+ }
+ if (sscanf(arg, "%dx%dx%d", p1, p2, p3) != 3)
+ return false;
+ for (int n = 0; n < 3; n++) {
+ int s = ((int[]) { *p1, *p2, *p3 })[n];
+ if (s < 2 || s > 512)
+ return false;
+ }
+ return true;
+}
+
+#endif
diff --git a/video/out/gpu/libmpv_gpu.c b/video/out/gpu/libmpv_gpu.c
new file mode 100644
index 0000000..aae1d18
--- /dev/null
+++ b/video/out/gpu/libmpv_gpu.c
@@ -0,0 +1,248 @@
+#include "config.h"
+#include "hwdec.h"
+#include "libmpv_gpu.h"
+#include "libmpv/render_gl.h"
+#include "video.h"
+#include "video/out/libmpv.h"
+
+static const struct libmpv_gpu_context_fns *context_backends[] = {
+#if HAVE_GL
+ &libmpv_gpu_context_gl,
+#endif
+ NULL
+};
+
+struct priv {
+ struct libmpv_gpu_context *context;
+
+ struct gl_video *renderer;
+};
+
+struct native_resource_entry {
+ const char *name; // ra_add_native_resource() internal name argument
+ size_t size; // size of struct pointed to (0 for no copy)
+};
+
+static const struct native_resource_entry native_resource_map[] = {
+ [MPV_RENDER_PARAM_X11_DISPLAY] = {
+ .name = "x11",
+ .size = 0,
+ },
+ [MPV_RENDER_PARAM_WL_DISPLAY] = {
+ .name = "wl",
+ .size = 0,
+ },
+ [MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE] = {
+ .name = "drm_draw_surface_size",
+ .size = sizeof (mpv_opengl_drm_draw_surface_size),
+ },
+ [MPV_RENDER_PARAM_DRM_DISPLAY_V2] = {
+ .name = "drm_params_v2",
+ .size = sizeof (mpv_opengl_drm_params_v2),
+ },
+};
+
+static int init(struct render_backend *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ char *api = get_mpv_render_param(params, MPV_RENDER_PARAM_API_TYPE, NULL);
+ if (!api)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ for (int n = 0; context_backends[n]; n++) {
+ const struct libmpv_gpu_context_fns *backend = context_backends[n];
+ if (strcmp(backend->api_name, api) == 0) {
+ p->context = talloc_zero(NULL, struct libmpv_gpu_context);
+ *p->context = (struct libmpv_gpu_context){
+ .global = ctx->global,
+ .log = ctx->log,
+ .fns = backend,
+ };
+ break;
+ }
+ }
+
+ if (!p->context)
+ return MPV_ERROR_NOT_IMPLEMENTED;
+
+ int err = p->context->fns->init(p->context, params);
+ if (err < 0)
+ return err;
+
+ for (int n = 0; params && params[n].type; n++) {
+ if (params[n].type > 0 &&
+ params[n].type < MP_ARRAY_SIZE(native_resource_map) &&
+ native_resource_map[params[n].type].name)
+ {
+ const struct native_resource_entry *entry =
+ &native_resource_map[params[n].type];
+ void *data = params[n].data;
+ if (entry->size)
+ data = talloc_memdup(p, data, entry->size);
+ ra_add_native_resource(p->context->ra_ctx->ra, entry->name, data);
+ }
+ }
+
+ p->renderer = gl_video_init(p->context->ra_ctx->ra, ctx->log, ctx->global);
+
+ ctx->hwdec_devs = hwdec_devices_create();
+ gl_video_init_hwdecs(p->renderer, p->context->ra_ctx, ctx->hwdec_devs, true);
+ ctx->driver_caps = VO_CAP_ROTATE90;
+ return 0;
+}
+
+static bool check_format(struct render_backend *ctx, int imgfmt)
+{
+ struct priv *p = ctx->priv;
+
+ return gl_video_check_format(p->renderer, imgfmt);
+}
+
+static int set_parameter(struct render_backend *ctx, mpv_render_param param)
+{
+ struct priv *p = ctx->priv;
+
+ switch (param.type) {
+ case MPV_RENDER_PARAM_ICC_PROFILE: {
+ mpv_byte_array *data = param.data;
+ gl_video_set_icc_profile(p->renderer, (bstr){data->data, data->size});
+ return 0;
+ }
+ case MPV_RENDER_PARAM_AMBIENT_LIGHT: {
+ int lux = *(int *)param.data;
+ gl_video_set_ambient_lux(p->renderer, lux);
+ return 0;
+ }
+ default:
+ return MPV_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+static void reconfig(struct render_backend *ctx, struct mp_image_params *params)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_config(p->renderer, params);
+}
+
+static void reset(struct render_backend *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_reset(p->renderer);
+}
+
+static void update_external(struct render_backend *ctx, struct vo *vo)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_set_osd_source(p->renderer, vo ? vo->osd : NULL);
+ if (vo)
+ gl_video_configure_queue(p->renderer, vo);
+}
+
+static void resize(struct render_backend *ctx, struct mp_rect *src,
+ struct mp_rect *dst, struct mp_osd_res *osd)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_resize(p->renderer, src, dst, osd);
+}
+
+static int get_target_size(struct render_backend *ctx, mpv_render_param *params,
+ int *out_w, int *out_h)
+{
+ struct priv *p = ctx->priv;
+
+ // Mapping the surface is cheap, better than adding new backend entrypoints.
+ struct ra_tex *tex;
+ int err = p->context->fns->wrap_fbo(p->context, params, &tex);
+ if (err < 0)
+ return err;
+ *out_w = tex->params.w;
+ *out_h = tex->params.h;
+ return 0;
+}
+
+static int render(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame)
+{
+ struct priv *p = ctx->priv;
+
+ // Mapping the surface is cheap, better than adding new backend entrypoints.
+ struct ra_tex *tex;
+ int err = p->context->fns->wrap_fbo(p->context, params, &tex);
+ if (err < 0)
+ return err;
+
+ int depth = *(int *)get_mpv_render_param(params, MPV_RENDER_PARAM_DEPTH,
+ &(int){0});
+ gl_video_set_fb_depth(p->renderer, depth);
+
+ bool flip = *(int *)get_mpv_render_param(params, MPV_RENDER_PARAM_FLIP_Y,
+ &(int){0});
+
+ struct ra_fbo target = {.tex = tex, .flip = flip};
+ gl_video_render_frame(p->renderer, frame, target, RENDER_FRAME_DEF);
+ p->context->fns->done_frame(p->context, frame->display_synced);
+
+ return 0;
+}
+
+static struct mp_image *get_image(struct render_backend *ctx, int imgfmt,
+ int w, int h, int stride_align, int flags)
+{
+ struct priv *p = ctx->priv;
+
+ return gl_video_get_image(p->renderer, imgfmt, w, h, stride_align, flags);
+}
+
+static void screenshot(struct render_backend *ctx, struct vo_frame *frame,
+ struct voctrl_screenshot *args)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_screenshot(p->renderer, frame, args);
+}
+
+static void perfdata(struct render_backend *ctx,
+ struct voctrl_performance_data *out)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_perfdata(p->renderer, out);
+}
+
+static void destroy(struct render_backend *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->renderer)
+ gl_video_uninit(p->renderer);
+
+ hwdec_devices_destroy(ctx->hwdec_devs);
+
+ if (p->context) {
+ p->context->fns->destroy(p->context);
+ talloc_free(p->context->priv);
+ talloc_free(p->context);
+ }
+}
+
+const struct render_backend_fns render_backend_gpu = {
+ .init = init,
+ .check_format = check_format,
+ .set_parameter = set_parameter,
+ .reconfig = reconfig,
+ .reset = reset,
+ .update_external = update_external,
+ .resize = resize,
+ .get_target_size = get_target_size,
+ .render = render,
+ .get_image = get_image,
+ .screenshot = screenshot,
+ .perfdata = perfdata,
+ .destroy = destroy,
+};
diff --git a/video/out/gpu/libmpv_gpu.h b/video/out/gpu/libmpv_gpu.h
new file mode 100644
index 0000000..497dcc3
--- /dev/null
+++ b/video/out/gpu/libmpv_gpu.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "video/out/libmpv.h"
+
+struct ra_tex;
+
+struct libmpv_gpu_context {
+ struct mpv_global *global;
+ struct mp_log *log;
+ const struct libmpv_gpu_context_fns *fns;
+
+ struct ra_ctx *ra_ctx;
+ void *priv;
+};
+
+// Manage backend specific interaction between libmpv and ra backend, that can't
+// be managed by ra itself (initialization and passing FBOs).
+struct libmpv_gpu_context_fns {
+ // The libmpv API type name, see MPV_RENDER_PARAM_API_TYPE.
+ const char *api_name;
+ // Pretty much works like render_backend_fns.init, except that the
+ // API type is already checked by the caller.
+ // Successful init must set ctx->ra.
+ int (*init)(struct libmpv_gpu_context *ctx, mpv_render_param *params);
+ // Wrap the surface passed to mpv_render_context_render() (via the params
+ // array) into a ra_tex and return it. Returns a libmpv error code, and sets
+ // *out to a temporary object on success. The returned object is valid until
+ // another wrap_fbo() or done_frame() is called.
+ // This does not need to care about generic attributes, like flipping.
+ int (*wrap_fbo)(struct libmpv_gpu_context *ctx, mpv_render_param *params,
+ struct ra_tex **out);
+ // Signal that the ra_tex object obtained with wrap_fbo is no longer used.
+ // For certain backends, this might also be used to signal the end of
+ // rendering (like OpenGL doing weird crap).
+ void (*done_frame)(struct libmpv_gpu_context *ctx, bool ds);
+ // Free all data in ctx->priv.
+ void (*destroy)(struct libmpv_gpu_context *ctx);
+};
+
+extern const struct libmpv_gpu_context_fns libmpv_gpu_context_gl;
diff --git a/video/out/gpu/osd.c b/video/out/gpu/osd.c
new file mode 100644
index 0000000..91505a9
--- /dev/null
+++ b/video/out/gpu/osd.c
@@ -0,0 +1,363 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+#include <limits.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "osd.h"
+
+#define GLSL(x) gl_sc_add(sc, #x "\n");
+
+// glBlendFuncSeparate() arguments
+static const int blend_factors[SUBBITMAP_COUNT][4] = {
+ [SUBBITMAP_LIBASS] = {RA_BLEND_SRC_ALPHA, RA_BLEND_ONE_MINUS_SRC_ALPHA,
+ RA_BLEND_ONE, RA_BLEND_ONE_MINUS_SRC_ALPHA},
+ [SUBBITMAP_BGRA] = {RA_BLEND_ONE, RA_BLEND_ONE_MINUS_SRC_ALPHA,
+ RA_BLEND_ONE, RA_BLEND_ONE_MINUS_SRC_ALPHA},
+};
+
+struct vertex {
+ float position[2];
+ float texcoord[2];
+ uint8_t ass_color[4];
+};
+
+static const struct ra_renderpass_input vertex_vao[] = {
+ {"position", RA_VARTYPE_FLOAT, 2, 1, offsetof(struct vertex, position)},
+ {"texcoord" , RA_VARTYPE_FLOAT, 2, 1, offsetof(struct vertex, texcoord)},
+ {"ass_color", RA_VARTYPE_BYTE_UNORM, 4, 1, offsetof(struct vertex, ass_color)},
+};
+
+struct mpgl_osd_part {
+ enum sub_bitmap_format format;
+ int change_id;
+ struct ra_tex *texture;
+ int w, h;
+ int num_subparts;
+ int prev_num_subparts;
+ struct sub_bitmap *subparts;
+ int num_vertices;
+ struct vertex *vertices;
+};
+
+struct mpgl_osd {
+ struct mp_log *log;
+ struct osd_state *osd;
+ struct ra *ra;
+ struct mpgl_osd_part *parts[MAX_OSD_PARTS];
+ const struct ra_format *fmt_table[SUBBITMAP_COUNT];
+ bool formats[SUBBITMAP_COUNT];
+ bool change_flag; // for reporting to API user only
+ // temporary
+ int stereo_mode;
+ struct mp_osd_res osd_res;
+ void *scratch;
+};
+
+struct mpgl_osd *mpgl_osd_init(struct ra *ra, struct mp_log *log,
+ struct osd_state *osd)
+{
+ struct mpgl_osd *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct mpgl_osd) {
+ .log = log,
+ .osd = osd,
+ .ra = ra,
+ .change_flag = true,
+ .scratch = talloc_zero_size(ctx, 1),
+ };
+
+ ctx->fmt_table[SUBBITMAP_LIBASS] = ra_find_unorm_format(ra, 1, 1);
+ ctx->fmt_table[SUBBITMAP_BGRA] = ra_find_unorm_format(ra, 1, 4);
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ ctx->parts[n] = talloc_zero(ctx, struct mpgl_osd_part);
+
+ for (int n = 0; n < SUBBITMAP_COUNT; n++)
+ ctx->formats[n] = !!ctx->fmt_table[n];
+
+ return ctx;
+}
+
+void mpgl_osd_destroy(struct mpgl_osd *ctx)
+{
+ if (!ctx)
+ return;
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct mpgl_osd_part *p = ctx->parts[n];
+ ra_tex_free(ctx->ra, &p->texture);
+ }
+ talloc_free(ctx);
+}
+
+static int next_pow2(int v)
+{
+ for (int x = 0; x < 30; x++) {
+ if ((1 << x) >= v)
+ return 1 << x;
+ }
+ return INT_MAX;
+}
+
+static bool upload_osd(struct mpgl_osd *ctx, struct mpgl_osd_part *osd,
+ struct sub_bitmaps *imgs)
+{
+ struct ra *ra = ctx->ra;
+ bool ok = false;
+
+ assert(imgs->packed);
+
+ int req_w = next_pow2(imgs->packed_w);
+ int req_h = next_pow2(imgs->packed_h);
+
+ const struct ra_format *fmt = ctx->fmt_table[imgs->format];
+ assert(fmt);
+
+ if (!osd->texture || req_w > osd->w || req_h > osd->h ||
+ osd->format != imgs->format)
+ {
+ ra_tex_free(ra, &osd->texture);
+
+ osd->format = imgs->format;
+ osd->w = MPMAX(32, req_w);
+ osd->h = MPMAX(32, req_h);
+
+ MP_VERBOSE(ctx, "Reallocating OSD texture to %dx%d.\n", osd->w, osd->h);
+
+ if (osd->w > ra->max_texture_wh || osd->h > ra->max_texture_wh) {
+ MP_ERR(ctx, "OSD bitmaps do not fit on a surface with the maximum "
+ "supported size %dx%d.\n", ra->max_texture_wh,
+ ra->max_texture_wh);
+ goto done;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = osd->w,
+ .h = osd->h,
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .host_mutable = true,
+ };
+ osd->texture = ra_tex_create(ra, &params);
+ if (!osd->texture)
+ goto done;
+ }
+
+ struct ra_tex_upload_params params = {
+ .tex = osd->texture,
+ .src = imgs->packed->planes[0],
+ .invalidate = true,
+ .rc = &(struct mp_rect){0, 0, imgs->packed_w, imgs->packed_h},
+ .stride = imgs->packed->stride[0],
+ };
+
+ ok = ra->fns->tex_upload(ra, &params);
+
+done:
+ return ok;
+}
+
+static void gen_osd_cb(void *pctx, struct sub_bitmaps *imgs)
+{
+ struct mpgl_osd *ctx = pctx;
+
+ if (imgs->num_parts == 0 || !ctx->formats[imgs->format])
+ return;
+
+ struct mpgl_osd_part *osd = ctx->parts[imgs->render_index];
+
+ bool ok = true;
+ if (imgs->change_id != osd->change_id) {
+ if (!upload_osd(ctx, osd, imgs))
+ ok = false;
+
+ osd->change_id = imgs->change_id;
+ ctx->change_flag = true;
+ }
+ osd->num_subparts = ok ? imgs->num_parts : 0;
+
+ MP_TARRAY_GROW(osd, osd->subparts, osd->num_subparts);
+ memcpy(osd->subparts, imgs->parts,
+ osd->num_subparts * sizeof(osd->subparts[0]));
+}
+
+bool mpgl_osd_draw_prepare(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc)
+{
+ assert(index >= 0 && index < MAX_OSD_PARTS);
+ struct mpgl_osd_part *part = ctx->parts[index];
+
+ enum sub_bitmap_format fmt = part->format;
+ if (!fmt || !part->num_subparts || !part->texture)
+ return false;
+
+ gl_sc_uniform_texture(sc, "osdtex", part->texture);
+ switch (fmt) {
+ case SUBBITMAP_BGRA: {
+ GLSL(color = texture(osdtex, texcoord).bgra;)
+ break;
+ }
+ case SUBBITMAP_LIBASS: {
+ GLSL(color =
+ vec4(ass_color.rgb, ass_color.a * texture(osdtex, texcoord).r);)
+ break;
+ }
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ return true;
+}
+
+static void write_quad(struct vertex *va, struct gl_transform t,
+ float x0, float y0, float x1, float y1,
+ float tx0, float ty0, float tx1, float ty1,
+ float tex_w, float tex_h, const uint8_t color[4])
+{
+ gl_transform_vec(t, &x0, &y0);
+ gl_transform_vec(t, &x1, &y1);
+
+#define COLOR_INIT {color[0], color[1], color[2], color[3]}
+ va[0] = (struct vertex){ {x0, y0}, {tx0 / tex_w, ty0 / tex_h}, COLOR_INIT };
+ va[1] = (struct vertex){ {x0, y1}, {tx0 / tex_w, ty1 / tex_h}, COLOR_INIT };
+ va[2] = (struct vertex){ {x1, y0}, {tx1 / tex_w, ty0 / tex_h}, COLOR_INIT };
+ va[3] = (struct vertex){ {x1, y1}, {tx1 / tex_w, ty1 / tex_h}, COLOR_INIT };
+ va[4] = va[2];
+ va[5] = va[1];
+#undef COLOR_INIT
+}
+
+static void generate_verts(struct mpgl_osd_part *part, struct gl_transform t)
+{
+ MP_TARRAY_GROW(part, part->vertices,
+ part->num_vertices + part->num_subparts * 6);
+
+ for (int n = 0; n < part->num_subparts; n++) {
+ struct sub_bitmap *b = &part->subparts[n];
+ struct vertex *va = &part->vertices[part->num_vertices];
+
+ // NOTE: the blend color is used with SUBBITMAP_LIBASS only, so it
+ // doesn't matter that we upload garbage for the other formats
+ uint32_t c = b->libass.color;
+ uint8_t color[4] = { c >> 24, (c >> 16) & 0xff,
+ (c >> 8) & 0xff, 255 - (c & 0xff) };
+
+ write_quad(va, t,
+ b->x, b->y, b->x + b->dw, b->y + b->dh,
+ b->src_x, b->src_y, b->src_x + b->w, b->src_y + b->h,
+ part->w, part->h, color);
+
+ part->num_vertices += 6;
+ }
+}
+
+// number of screen divisions per axis (x=0, y=1) for the current 3D mode
+static void get_3d_side_by_side(int stereo_mode, int div[2])
+{
+ div[0] = div[1] = 1;
+ switch (stereo_mode) {
+ case MP_STEREO3D_SBS2L:
+ case MP_STEREO3D_SBS2R: div[0] = 2; break;
+ case MP_STEREO3D_AB2R:
+ case MP_STEREO3D_AB2L: div[1] = 2; break;
+ }
+}
+
+void mpgl_osd_draw_finish(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc, struct ra_fbo fbo)
+{
+ struct mpgl_osd_part *part = ctx->parts[index];
+
+ int div[2];
+ get_3d_side_by_side(ctx->stereo_mode, div);
+
+ part->num_vertices = 0;
+
+ for (int x = 0; x < div[0]; x++) {
+ for (int y = 0; y < div[1]; y++) {
+ struct gl_transform t;
+ gl_transform_ortho_fbo(&t, fbo);
+
+ float a_x = ctx->osd_res.w * x;
+ float a_y = ctx->osd_res.h * y;
+ t.t[0] += a_x * t.m[0][0] + a_y * t.m[1][0];
+ t.t[1] += a_x * t.m[0][1] + a_y * t.m[1][1];
+
+ generate_verts(part, t);
+ }
+ }
+
+ const int *factors = &blend_factors[part->format][0];
+ gl_sc_blend(sc, factors[0], factors[1], factors[2], factors[3]);
+
+ gl_sc_dispatch_draw(sc, fbo.tex, false, vertex_vao, MP_ARRAY_SIZE(vertex_vao),
+ sizeof(struct vertex), part->vertices, part->num_vertices);
+}
+
+static void set_res(struct mpgl_osd *ctx, struct mp_osd_res res, int stereo_mode)
+{
+ int div[2];
+ get_3d_side_by_side(stereo_mode, div);
+
+ res.w /= div[0];
+ res.h /= div[1];
+ ctx->osd_res = res;
+}
+
+void mpgl_osd_generate(struct mpgl_osd *ctx, struct mp_osd_res res, double pts,
+ int stereo_mode, int draw_flags)
+{
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ ctx->parts[n]->num_subparts = 0;
+
+ set_res(ctx, res, stereo_mode);
+
+ osd_draw(ctx->osd, ctx->osd_res, pts, draw_flags, ctx->formats, gen_osd_cb, ctx);
+ ctx->stereo_mode = stereo_mode;
+
+ // Parts going away does not necessarily result in gen_osd_cb() being called
+ // (not even with num_parts==0), so check this separately.
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct mpgl_osd_part *part = ctx->parts[n];
+ if (part->num_subparts != part->prev_num_subparts)
+ ctx->change_flag = true;
+ part->prev_num_subparts = part->num_subparts;
+ }
+}
+
+// See osd_resize() for remarks. This function is an optional optimization too.
+void mpgl_osd_resize(struct mpgl_osd *ctx, struct mp_osd_res res, int stereo_mode)
+{
+ set_res(ctx, res, stereo_mode);
+ osd_resize(ctx->osd, ctx->osd_res);
+}
+
+bool mpgl_osd_check_change(struct mpgl_osd *ctx, struct mp_osd_res *res,
+ double pts)
+{
+ ctx->change_flag = false;
+ mpgl_osd_generate(ctx, *res, pts, 0, 0);
+ return ctx->change_flag;
+}
diff --git a/video/out/gpu/osd.h b/video/out/gpu/osd.h
new file mode 100644
index 0000000..00fbc49
--- /dev/null
+++ b/video/out/gpu/osd.h
@@ -0,0 +1,25 @@
+#ifndef MPLAYER_GL_OSD_H
+#define MPLAYER_GL_OSD_H
+
+#include <stdbool.h>
+#include <inttypes.h>
+
+#include "utils.h"
+#include "shader_cache.h"
+#include "sub/osd.h"
+
+struct mpgl_osd *mpgl_osd_init(struct ra *ra, struct mp_log *log,
+ struct osd_state *osd);
+void mpgl_osd_destroy(struct mpgl_osd *ctx);
+
+void mpgl_osd_generate(struct mpgl_osd *ctx, struct mp_osd_res res, double pts,
+ int stereo_mode, int draw_flags);
+void mpgl_osd_resize(struct mpgl_osd *ctx, struct mp_osd_res res, int stereo_mode);
+bool mpgl_osd_draw_prepare(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc);
+void mpgl_osd_draw_finish(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc, struct ra_fbo fbo);
+bool mpgl_osd_check_change(struct mpgl_osd *ctx, struct mp_osd_res *res,
+ double pts);
+
+#endif
diff --git a/video/out/gpu/ra.c b/video/out/gpu/ra.c
new file mode 100644
index 0000000..855f9b6
--- /dev/null
+++ b/video/out/gpu/ra.c
@@ -0,0 +1,424 @@
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/img_format.h"
+
+#include "ra.h"
+
+void ra_add_native_resource(struct ra *ra, const char *name, void *data)
+{
+ struct ra_native_resource r = {
+ .name = name,
+ .data = data,
+ };
+ MP_TARRAY_APPEND(ra, ra->native_resources, ra->num_native_resources, r);
+}
+
+void *ra_get_native_resource(struct ra *ra, const char *name)
+{
+ for (int n = 0; n < ra->num_native_resources; n++) {
+ struct ra_native_resource *r = &ra->native_resources[n];
+ if (strcmp(r->name, name) == 0)
+ return r->data;
+ }
+
+ return NULL;
+}
+
+struct ra_tex *ra_tex_create(struct ra *ra, const struct ra_tex_params *params)
+{
+ switch (params->dimensions) {
+ case 1:
+ assert(params->h == 1 && params->d == 1);
+ break;
+ case 2:
+ assert(params->d == 1);
+ break;
+ default:
+ assert(params->dimensions >= 1 && params->dimensions <= 3);
+ }
+ return ra->fns->tex_create(ra, params);
+}
+
+void ra_tex_free(struct ra *ra, struct ra_tex **tex)
+{
+ if (*tex)
+ ra->fns->tex_destroy(ra, *tex);
+ *tex = NULL;
+}
+
+struct ra_buf *ra_buf_create(struct ra *ra, const struct ra_buf_params *params)
+{
+ return ra->fns->buf_create(ra, params);
+}
+
+void ra_buf_free(struct ra *ra, struct ra_buf **buf)
+{
+ if (*buf)
+ ra->fns->buf_destroy(ra, *buf);
+ *buf = NULL;
+}
+
+void ra_free(struct ra **ra)
+{
+ if (*ra)
+ (*ra)->fns->destroy(*ra);
+ talloc_free(*ra);
+ *ra = NULL;
+}
+
+size_t ra_vartype_size(enum ra_vartype type)
+{
+ switch (type) {
+ case RA_VARTYPE_INT: return sizeof(int);
+ case RA_VARTYPE_FLOAT: return sizeof(float);
+ case RA_VARTYPE_BYTE_UNORM: return 1;
+ default: return 0;
+ }
+}
+
+struct ra_layout ra_renderpass_input_layout(struct ra_renderpass_input *input)
+{
+ size_t el_size = ra_vartype_size(input->type);
+ if (!el_size)
+ return (struct ra_layout){0};
+
+ // host data is always tightly packed
+ return (struct ra_layout) {
+ .align = 1,
+ .stride = el_size * input->dim_v,
+ .size = el_size * input->dim_v * input->dim_m,
+ };
+}
+
+static struct ra_renderpass_input *dup_inputs(void *ta_parent,
+ const struct ra_renderpass_input *inputs, int num_inputs)
+{
+ struct ra_renderpass_input *res =
+ talloc_memdup(ta_parent, (void *)inputs, num_inputs * sizeof(inputs[0]));
+ for (int n = 0; n < num_inputs; n++)
+ res[n].name = talloc_strdup(res, res[n].name);
+ return res;
+}
+
+// Return a newly allocated deep-copy of params.
+struct ra_renderpass_params *ra_renderpass_params_copy(void *ta_parent,
+ const struct ra_renderpass_params *params)
+{
+ struct ra_renderpass_params *res = talloc_ptrtype(ta_parent, res);
+ *res = *params;
+ res->inputs = dup_inputs(res, res->inputs, res->num_inputs);
+ res->vertex_attribs =
+ dup_inputs(res, res->vertex_attribs, res->num_vertex_attribs);
+ res->cached_program = bstrdup(res, res->cached_program);
+ res->vertex_shader = talloc_strdup(res, res->vertex_shader);
+ res->frag_shader = talloc_strdup(res, res->frag_shader);
+ res->compute_shader = talloc_strdup(res, res->compute_shader);
+ return res;
+}
+
+struct glsl_fmt {
+ enum ra_ctype ctype;
+ int num_components;
+ int component_depth[4];
+ const char *glsl_format;
+};
+
+// List taken from the GLSL specification, sans snorm and sint formats
+static const struct glsl_fmt ra_glsl_fmts[] = {
+ {RA_CTYPE_FLOAT, 1, {16}, "r16f"},
+ {RA_CTYPE_FLOAT, 1, {32}, "r32f"},
+ {RA_CTYPE_FLOAT, 2, {16, 16}, "rg16f"},
+ {RA_CTYPE_FLOAT, 2, {32, 32}, "rg32f"},
+ {RA_CTYPE_FLOAT, 4, {16, 16, 16, 16}, "rgba16f"},
+ {RA_CTYPE_FLOAT, 4, {32, 32, 32, 32}, "rgba32f"},
+ {RA_CTYPE_FLOAT, 3, {11, 11, 10}, "r11f_g11f_b10f"},
+
+ {RA_CTYPE_UNORM, 1, {8}, "r8"},
+ {RA_CTYPE_UNORM, 1, {16}, "r16"},
+ {RA_CTYPE_UNORM, 2, {8, 8}, "rg8"},
+ {RA_CTYPE_UNORM, 2, {16, 16}, "rg16"},
+ {RA_CTYPE_UNORM, 4, {8, 8, 8, 8}, "rgba8"},
+ {RA_CTYPE_UNORM, 4, {16, 16, 16, 16}, "rgba16"},
+ {RA_CTYPE_UNORM, 4, {10, 10, 10, 2}, "rgb10_a2"},
+
+ {RA_CTYPE_UINT, 1, {8}, "r8ui"},
+ {RA_CTYPE_UINT, 1, {16}, "r16ui"},
+ {RA_CTYPE_UINT, 1, {32}, "r32ui"},
+ {RA_CTYPE_UINT, 2, {8, 8}, "rg8ui"},
+ {RA_CTYPE_UINT, 2, {16, 16}, "rg16ui"},
+ {RA_CTYPE_UINT, 2, {32, 32}, "rg32ui"},
+ {RA_CTYPE_UINT, 4, {8, 8, 8, 8}, "rgba8ui"},
+ {RA_CTYPE_UINT, 4, {16, 16, 16, 16}, "rgba16ui"},
+ {RA_CTYPE_UINT, 4, {32, 32, 32, 32}, "rgba32ui"},
+ {RA_CTYPE_UINT, 4, {10, 10, 10, 2}, "rgb10_a2ui"},
+};
+
+const char *ra_fmt_glsl_format(const struct ra_format *fmt)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(ra_glsl_fmts); n++) {
+ const struct glsl_fmt *gfmt = &ra_glsl_fmts[n];
+
+ if (fmt->ctype != gfmt->ctype)
+ continue;
+ if (fmt->num_components != gfmt->num_components)
+ continue;
+
+ for (int i = 0; i < fmt->num_components; i++) {
+ if (fmt->component_depth[i] != gfmt->component_depth[i])
+ goto next_fmt;
+ }
+
+ return gfmt->glsl_format;
+
+next_fmt: ; // equivalent to `continue`
+ }
+
+ return NULL;
+}
+
+// Return whether this is a tightly packed format with no external padding and
+// with the same bit size/depth in all components, and the shader returns
+// components in the same order as in memory.
+static bool ra_format_is_regular(const struct ra_format *fmt)
+{
+ if (!fmt->pixel_size || !fmt->num_components || !fmt->ordered)
+ return false;
+ for (int n = 1; n < fmt->num_components; n++) {
+ if (fmt->component_size[n] != fmt->component_size[0] ||
+ fmt->component_depth[n] != fmt->component_depth[0])
+ return false;
+ }
+ if (fmt->component_size[0] * fmt->num_components != fmt->pixel_size * 8)
+ return false;
+ return true;
+}
+
+// Return a regular filterable format using RA_CTYPE_UNORM.
+const struct ra_format *ra_find_unorm_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components)
+{
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (fmt->ctype == RA_CTYPE_UNORM && fmt->num_components == n_components &&
+ fmt->pixel_size == bytes_per_component * n_components &&
+ fmt->component_depth[0] == bytes_per_component * 8 &&
+ fmt->linear_filter && ra_format_is_regular(fmt))
+ return fmt;
+ }
+ return NULL;
+}
+
+// Return a regular format using RA_CTYPE_UINT.
+const struct ra_format *ra_find_uint_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components)
+{
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (fmt->ctype == RA_CTYPE_UINT && fmt->num_components == n_components &&
+ fmt->pixel_size == bytes_per_component * n_components &&
+ fmt->component_depth[0] == bytes_per_component * 8 &&
+ ra_format_is_regular(fmt))
+ return fmt;
+ }
+ return NULL;
+}
+
+// Find a float format of any precision that matches the C type of the same
+// size for upload.
+// May drop bits from the mantissa (such as selecting float16 even if
+// bytes_per_component == 32); prefers possibly faster formats first.
+static const struct ra_format *ra_find_float_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components)
+{
+ // Assumes ra_format are ordered by performance.
+ // The >=16 check is to avoid catching fringe formats.
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (fmt->ctype == RA_CTYPE_FLOAT && fmt->num_components == n_components &&
+ fmt->pixel_size == bytes_per_component * n_components &&
+ fmt->component_depth[0] >= 16 &&
+ fmt->linear_filter && ra_format_is_regular(fmt))
+ return fmt;
+ }
+ return NULL;
+}
+
+// Return a filterable regular format that uses at least float16 internally, and
+// uses a normal C float for transfer on the CPU side. (This is just so we don't
+// need 32->16 bit conversion on CPU, which would be messy.)
+const struct ra_format *ra_find_float16_format(struct ra *ra, int n_components)
+{
+ return ra_find_float_format(ra, sizeof(float), n_components);
+}
+
+const struct ra_format *ra_find_named_format(struct ra *ra, const char *name)
+{
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (strcmp(fmt->name, name) == 0)
+ return fmt;
+ }
+ return NULL;
+}
+
+// Like ra_find_unorm_format(), but if no fixed point format is available,
+// return an unsigned integer format.
+static const struct ra_format *find_plane_format(struct ra *ra, int bytes,
+ int n_channels,
+ enum mp_component_type ctype)
+{
+ switch (ctype) {
+ case MP_COMPONENT_TYPE_UINT: {
+ const struct ra_format *f = ra_find_unorm_format(ra, bytes, n_channels);
+ if (f)
+ return f;
+ return ra_find_uint_format(ra, bytes, n_channels);
+ }
+ case MP_COMPONENT_TYPE_FLOAT:
+ return ra_find_float_format(ra, bytes, n_channels);
+ default: return NULL;
+ }
+}
+
+// Put a mapping of imgfmt to texture formats into *out. Basically it selects
+// the correct texture formats needed to represent an imgfmt in a shader, with
+// textures using the same memory organization as on the CPU.
+// Each plane is represented by a texture, and each texture has a RGBA
+// component order. out->components describes the meaning of them.
+// May return integer formats for >8 bit formats, if the driver has no
+// normalized 16 bit formats.
+// Returns false (and *out is not touched) if no format found.
+bool ra_get_imgfmt_desc(struct ra *ra, int imgfmt, struct ra_imgfmt_desc *out)
+{
+ struct ra_imgfmt_desc res = {.component_type = RA_CTYPE_UNKNOWN};
+
+ struct mp_regular_imgfmt regfmt;
+ if (mp_get_regular_imgfmt(&regfmt, imgfmt)) {
+ res.num_planes = regfmt.num_planes;
+ res.component_bits = regfmt.component_size * 8;
+ res.component_pad = regfmt.component_pad;
+ for (int n = 0; n < regfmt.num_planes; n++) {
+ struct mp_regular_imgfmt_plane *plane = &regfmt.planes[n];
+ res.planes[n] = find_plane_format(ra, regfmt.component_size,
+ plane->num_components,
+ regfmt.component_type);
+ if (!res.planes[n])
+ return false;
+ for (int i = 0; i < plane->num_components; i++)
+ res.components[n][i] = plane->components[i];
+ // Dropping LSBs when shifting will lead to dropped MSBs.
+ if (res.component_bits > res.planes[n]->component_depth[0] &&
+ res.component_pad < 0)
+ return false;
+ // Renderer restriction, but actually an unwanted corner case.
+ if (res.component_type != RA_CTYPE_UNKNOWN &&
+ res.component_type != res.planes[n]->ctype)
+ return false;
+ res.component_type = res.planes[n]->ctype;
+ }
+ res.chroma_w = 1 << regfmt.chroma_xs;
+ res.chroma_h = 1 << regfmt.chroma_ys;
+ goto supported;
+ }
+
+ for (int n = 0; n < ra->num_formats; n++) {
+ if (imgfmt && ra->formats[n]->special_imgfmt == imgfmt) {
+ res = *ra->formats[n]->special_imgfmt_desc;
+ goto supported;
+ }
+ }
+
+ // Unsupported format
+ return false;
+
+supported:
+
+ *out = res;
+ return true;
+}
+
+static const char *ctype_to_str(enum ra_ctype ctype)
+{
+ switch (ctype) {
+ case RA_CTYPE_UNORM: return "unorm";
+ case RA_CTYPE_UINT: return "uint ";
+ case RA_CTYPE_FLOAT: return "float";
+ default: return "unknown";
+ }
+}
+
+void ra_dump_tex_formats(struct ra *ra, int msgl)
+{
+ if (!mp_msg_test(ra->log, msgl))
+ return;
+ MP_MSG(ra, msgl, "Texture formats:\n");
+ MP_MSG(ra, msgl, " NAME COMP*TYPE SIZE DEPTH PER COMP.\n");
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ const char *ctype = ctype_to_str(fmt->ctype);
+ char cl[40] = "";
+ for (int i = 0; i < fmt->num_components; i++) {
+ mp_snprintf_cat(cl, sizeof(cl), "%s%d", i ? " " : "",
+ fmt->component_size[i]);
+ if (fmt->component_size[i] != fmt->component_depth[i])
+ mp_snprintf_cat(cl, sizeof(cl), "/%d", fmt->component_depth[i]);
+ }
+ MP_MSG(ra, msgl, " %-10s %d*%s %3dB %s %s %s %s {%s}\n", fmt->name,
+ fmt->num_components, ctype, fmt->pixel_size,
+ fmt->luminance_alpha ? "LA" : " ",
+ fmt->linear_filter ? "LF" : " ",
+ fmt->renderable ? "CR" : " ",
+ fmt->storable ? "ST" : " ", cl);
+ }
+ MP_MSG(ra, msgl, " LA = LUMINANCE_ALPHA hack format\n");
+ MP_MSG(ra, msgl, " LF = linear filterable\n");
+ MP_MSG(ra, msgl, " CR = can be used for render targets\n");
+ MP_MSG(ra, msgl, " ST = can be used for storable images\n");
+}
+
+void ra_dump_imgfmt_desc(struct ra *ra, const struct ra_imgfmt_desc *desc,
+ int msgl)
+{
+ char pl[80] = "";
+ char pf[80] = "";
+ for (int n = 0; n < desc->num_planes; n++) {
+ if (n > 0) {
+ mp_snprintf_cat(pl, sizeof(pl), "/");
+ mp_snprintf_cat(pf, sizeof(pf), "/");
+ }
+ char t[5] = {0};
+ for (int i = 0; i < 4; i++)
+ t[i] = "_rgba"[desc->components[n][i]];
+ for (int i = 3; i > 0 && t[i] == '_'; i--)
+ t[i] = '\0';
+ mp_snprintf_cat(pl, sizeof(pl), "%s", t);
+ mp_snprintf_cat(pf, sizeof(pf), "%s", desc->planes[n]->name);
+ }
+ MP_MSG(ra, msgl, "%d planes %dx%d %d/%d [%s] (%s) [%s]\n",
+ desc->num_planes, desc->chroma_w, desc->chroma_h,
+ desc->component_bits, desc->component_pad, pf, pl,
+ ctype_to_str(desc->component_type));
+}
+
+void ra_dump_img_formats(struct ra *ra, int msgl)
+{
+ if (!mp_msg_test(ra->log, msgl))
+ return;
+ MP_MSG(ra, msgl, "Image formats:\n");
+ for (int imgfmt = IMGFMT_START; imgfmt < IMGFMT_END; imgfmt++) {
+ const char *name = mp_imgfmt_to_name(imgfmt);
+ if (strcmp(name, "unknown") == 0)
+ continue;
+ MP_MSG(ra, msgl, " %s", name);
+ struct ra_imgfmt_desc desc;
+ if (ra_get_imgfmt_desc(ra, imgfmt, &desc)) {
+ MP_MSG(ra, msgl, " => ");
+ ra_dump_imgfmt_desc(ra, &desc, msgl);
+ } else {
+ MP_MSG(ra, msgl, "\n");
+ }
+ }
+}
diff --git a/video/out/gpu/ra.h b/video/out/gpu/ra.h
new file mode 100644
index 0000000..5f229f8
--- /dev/null
+++ b/video/out/gpu/ra.h
@@ -0,0 +1,559 @@
+#pragma once
+
+#include "common/common.h"
+#include "misc/bstr.h"
+
+// Handle for a rendering API backend.
+struct ra {
+ struct ra_fns *fns;
+ void *priv;
+
+ int glsl_version; // GLSL version (e.g. 300 => 3.0)
+ bool glsl_es; // use ES dialect
+ bool glsl_vulkan; // use vulkan dialect
+
+ struct mp_log *log;
+
+ // RA_CAP_* bit field. The RA backend must set supported features at init
+ // time.
+ uint64_t caps;
+
+ // Maximum supported width and height of a 2D texture. Set by the RA backend
+ // at init time.
+ int max_texture_wh;
+
+ // Maximum shared memory for compute shaders. Set by the RA backend at init
+ // time.
+ size_t max_shmem;
+
+ // Maximum number of threads in a compute work group. Set by the RA backend
+ // at init time.
+ size_t max_compute_group_threads;
+
+ // Maximum push constant size. Set by the RA backend at init time.
+ size_t max_pushc_size;
+
+ // Set of supported texture formats. Must be added by RA backend at init time.
+ // If there are equivalent formats with different caveats, the preferred
+ // formats should have a lower index. (E.g. GLES3 should put rg8 before la.)
+ struct ra_format **formats;
+ int num_formats;
+
+ // Accelerate texture uploads via an extra PBO even when
+ // RA_CAP_DIRECT_UPLOAD is supported. This is basically only relevant for
+ // OpenGL. Set by the RA user.
+ bool use_pbo;
+
+ // Array of native resources. For the most part an "escape" mechanism, and
+ // usually does not contain parameters required for basic functionality.
+ struct ra_native_resource *native_resources;
+ int num_native_resources;
+};
+
+// For passing through windowing system specific parameters and such. The
+// names are always internal (the libmpv render API uses mpv_render_param_type
+// and maps them to names internally).
+// For example, a name="x11" entry has a X11 display as (Display*)data.
+struct ra_native_resource {
+ const char *name;
+ void *data;
+};
+
+// Add a ra_native_resource entry. Both name and data pointers must stay valid
+// until ra termination.
+void ra_add_native_resource(struct ra *ra, const char *name, void *data);
+
+// Search ra->native_resources, returns NULL on failure.
+void *ra_get_native_resource(struct ra *ra, const char *name);
+
+enum {
+ RA_CAP_TEX_1D = 1 << 0, // supports 1D textures (as shader inputs)
+ RA_CAP_TEX_3D = 1 << 1, // supports 3D textures (as shader inputs)
+ RA_CAP_BLIT = 1 << 2, // supports ra_fns.blit
+ RA_CAP_COMPUTE = 1 << 3, // supports compute shaders
+ RA_CAP_DIRECT_UPLOAD = 1 << 4, // supports tex_upload without ra_buf
+ RA_CAP_BUF_RO = 1 << 5, // supports RA_VARTYPE_BUF_RO
+ RA_CAP_BUF_RW = 1 << 6, // supports RA_VARTYPE_BUF_RW
+ RA_CAP_NESTED_ARRAY = 1 << 7, // supports nested arrays
+ RA_CAP_GLOBAL_UNIFORM = 1 << 8, // supports using "naked" uniforms (not UBO)
+ RA_CAP_GATHER = 1 << 9, // supports textureGather in GLSL
+ RA_CAP_FRAGCOORD = 1 << 10, // supports reading from gl_FragCoord
+ RA_CAP_PARALLEL_COMPUTE = 1 << 11, // supports parallel compute shaders
+ RA_CAP_NUM_GROUPS = 1 << 12, // supports gl_NumWorkGroups
+ RA_CAP_SLOW_DR = 1 << 13, // direct rendering is assumed to be slow
+};
+
+enum ra_ctype {
+ RA_CTYPE_UNKNOWN = 0, // also used for inconsistent multi-component formats
+ RA_CTYPE_UNORM, // unsigned normalized integer (fixed point) formats
+ RA_CTYPE_UINT, // full integer formats
+ RA_CTYPE_FLOAT, // float formats (signed, any bit size)
+};
+
+// All formats must be useable as texture formats. All formats must be byte
+// aligned (all pixels start and end on a byte boundary), at least as far CPU
+// transfers are concerned.
+struct ra_format {
+ // All fields are read-only after creation.
+ const char *name; // symbolic name for user interaction/debugging
+ void *priv;
+ enum ra_ctype ctype; // data type of each component
+ bool ordered; // components are sequential in memory, and returned
+ // by the shader in memory order (the shader can
+ // return arbitrary values for unused components)
+ int num_components; // component count, 0 if not applicable, max. 4
+ int component_size[4]; // in bits, all entries 0 if not applicable
+ int component_depth[4]; // bits in use for each component, 0 if not applicable
+ // (_must_ be set if component_size[] includes padding,
+ // and the real procession as seen by shader is lower)
+ int pixel_size; // in bytes, total pixel size (0 if opaque)
+ bool luminance_alpha; // pre-GL_ARB_texture_rg hack for 2 component textures
+ // if this is set, shader must use .ra instead of .rg
+ // only applies to 2-component textures
+ bool linear_filter; // linear filtering available from shader
+ bool renderable; // can be used for render targets
+ bool storable; // can be used for storage images
+ bool dummy_format; // is not a real ra_format but a fake one (e.g. FBO).
+ // dummy formats cannot be used to create textures
+
+ // If not 0, the format represents some sort of packed fringe format, whose
+ // shader representation is given by the special_imgfmt_desc pointer.
+ int special_imgfmt;
+ const struct ra_imgfmt_desc *special_imgfmt_desc;
+
+ // This gives the GLSL image format corresponding to the format, if any.
+ // (e.g. rgba16ui)
+ const char *glsl_format;
+};
+
+struct ra_tex_params {
+ int dimensions; // 1-3 for 1D-3D textures
+ // Size of the texture. 1D textures require h=d=1, 2D textures require d=1.
+ int w, h, d;
+ const struct ra_format *format;
+ bool render_src; // must be useable as source texture in a shader
+ bool render_dst; // must be useable as target texture in a shader
+ bool storage_dst; // must be usable as a storage image (RA_VARTYPE_IMG_W)
+ bool blit_src; // must be usable as a blit source
+ bool blit_dst; // must be usable as a blit destination
+ bool host_mutable; // texture may be updated with tex_upload
+ bool downloadable; // texture can be read with tex_download
+ // When used as render source texture.
+ bool src_linear; // if false, use nearest sampling (whether this can
+ // be true depends on ra_format.linear_filter)
+ bool src_repeat; // if false, clamp texture coordinates to edge
+ // if true, repeat texture coordinates
+ bool non_normalized; // hack for GL_TEXTURE_RECTANGLE OSX idiocy
+ // always set to false, except in OSX code
+ bool external_oes; // hack for GL_TEXTURE_EXTERNAL_OES idiocy
+ // If non-NULL, the texture will be created with these contents. Using
+ // this does *not* require setting host_mutable. Otherwise, the initial
+ // data is undefined.
+ void *initial_data;
+};
+
+// Conflates the following typical GPU API concepts:
+// - texture itself
+// - sampler state
+// - staging buffers for texture upload
+// - framebuffer objects
+// - wrappers for swapchain framebuffers
+// - synchronization needed for upload/rendering/etc.
+struct ra_tex {
+ // All fields are read-only after creation.
+ struct ra_tex_params params;
+ void *priv;
+};
+
+struct ra_tex_upload_params {
+ struct ra_tex *tex; // Texture to upload to
+ bool invalidate; // Discard pre-existing data not in the region uploaded
+ // Uploading from buffer:
+ struct ra_buf *buf; // Buffer to upload from (mutually exclusive with `src`)
+ size_t buf_offset; // Start of data within buffer (bytes)
+ // Uploading directly: (Note: If RA_CAP_DIRECT_UPLOAD is not set, then this
+ // will be internally translated to a tex_upload buffer by the RA)
+ const void *src; // Address of data
+ // For 2D textures only:
+ struct mp_rect *rc; // Region to upload. NULL means entire image
+ ptrdiff_t stride; // The size of a horizontal line in bytes (*not* texels!)
+};
+
+struct ra_tex_download_params {
+ struct ra_tex *tex; // Texture to download from
+ // Downloading directly (set by caller, data written to by callee):
+ void *dst; // Address of data (packed with no alignment)
+ ptrdiff_t stride; // The size of a horizontal line in bytes (*not* texels!)
+};
+
+// Buffer usage type. This restricts what types of operations may be performed
+// on a buffer.
+enum ra_buf_type {
+ RA_BUF_TYPE_INVALID,
+ RA_BUF_TYPE_TEX_UPLOAD, // texture upload buffer (pixel buffer object)
+ RA_BUF_TYPE_SHADER_STORAGE, // shader buffer (SSBO), for RA_VARTYPE_BUF_RW
+ RA_BUF_TYPE_UNIFORM, // uniform buffer (UBO), for RA_VARTYPE_BUF_RO
+ RA_BUF_TYPE_VERTEX, // not publicly usable (RA-internal usage)
+ RA_BUF_TYPE_SHARED_MEMORY, // device memory for sharing with external API
+};
+
+struct ra_buf_params {
+ enum ra_buf_type type;
+ size_t size;
+ bool host_mapped; // create a read-writable persistent mapping (ra_buf.data)
+ bool host_mutable; // contents may be updated via buf_update()
+ // If non-NULL, the buffer will be created with these contents. Otherwise,
+ // the initial data is undefined.
+ void *initial_data;
+};
+
+// A generic buffer, which can be used for many purposes (texture upload,
+// storage buffer, uniform buffer, etc.)
+struct ra_buf {
+ // All fields are read-only after creation.
+ struct ra_buf_params params;
+ void *data; // for persistently mapped buffers, points to the first byte
+ void *priv;
+};
+
+// Type of a shader uniform variable, or a vertex attribute. In all cases,
+// vectors are matrices are done by having more than 1 value.
+enum ra_vartype {
+ RA_VARTYPE_INVALID,
+ RA_VARTYPE_INT, // C: int, GLSL: int, ivec*
+ RA_VARTYPE_FLOAT, // C: float, GLSL: float, vec*, mat*
+ RA_VARTYPE_TEX, // C: ra_tex*, GLSL: various sampler types
+ // ra_tex.params.render_src must be true
+ RA_VARTYPE_IMG_W, // C: ra_tex*, GLSL: various image types
+ // write-only (W) image for compute shaders
+ // ra_tex.params.storage_dst must be true
+ RA_VARTYPE_BYTE_UNORM, // C: uint8_t, GLSL: int, vec* (vertex data only)
+ RA_VARTYPE_BUF_RO, // C: ra_buf*, GLSL: uniform buffer block
+ // buf type must be RA_BUF_TYPE_UNIFORM
+ RA_VARTYPE_BUF_RW, // C: ra_buf*, GLSL: shader storage buffer block
+ // buf type must be RA_BUF_TYPE_SHADER_STORAGE
+ RA_VARTYPE_COUNT
+};
+
+// Returns the host size of a ra_vartype, or 0 for abstract vartypes (e.g. tex)
+size_t ra_vartype_size(enum ra_vartype type);
+
+// Represents a uniform, texture input parameter, and similar things.
+struct ra_renderpass_input {
+ const char *name; // name as used in the shader
+ enum ra_vartype type;
+ // The total number of values is given by dim_v * dim_m.
+ int dim_v; // vector dimension (1 for non-vector and non-matrix)
+ int dim_m; // additional matrix dimension (dim_v x dim_m)
+ // Vertex data: byte offset of the attribute into the vertex struct
+ size_t offset;
+ // RA_VARTYPE_TEX: texture unit
+ // RA_VARTYPE_IMG_W: image unit
+ // RA_VARTYPE_BUF_* buffer binding point
+ // Other uniforms: unused
+ // Bindings must be unique within each namespace, as specified by
+ // desc_namespace()
+ int binding;
+};
+
+// Represents the layout requirements of an input value
+struct ra_layout {
+ size_t align; // the alignment requirements (always a power of two)
+ size_t stride; // the delta between two rows of an array/matrix
+ size_t size; // the total size of the input
+};
+
+// Returns the host layout of a render pass input. Returns {0} for renderpass
+// inputs without a corresponding host representation (e.g. textures/buffers)
+struct ra_layout ra_renderpass_input_layout(struct ra_renderpass_input *input);
+
+enum ra_blend {
+ RA_BLEND_ZERO,
+ RA_BLEND_ONE,
+ RA_BLEND_SRC_ALPHA,
+ RA_BLEND_ONE_MINUS_SRC_ALPHA,
+};
+
+enum ra_renderpass_type {
+ RA_RENDERPASS_TYPE_INVALID,
+ RA_RENDERPASS_TYPE_RASTER, // vertex+fragment shader
+ RA_RENDERPASS_TYPE_COMPUTE, // compute shader
+};
+
+// Static part of a rendering pass. It conflates the following:
+// - compiled shader and its list of uniforms
+// - vertex attributes and its shader mappings
+// - blending parameters
+// (For Vulkan, this would be shader module + pipeline state.)
+// Upon creation, the values of dynamic values such as uniform contents (whose
+// initial values are not provided here) are required to be 0.
+struct ra_renderpass_params {
+ enum ra_renderpass_type type;
+
+ // Uniforms, including texture/sampler inputs.
+ struct ra_renderpass_input *inputs;
+ int num_inputs;
+ size_t push_constants_size; // must be <= ra.max_pushc_size and a multiple of 4
+
+ // Highly implementation-specific byte array storing a compiled version
+ // of the program. Can be used to speed up shader compilation. A backend
+ // xan read this in renderpass_create, or set this on the newly created
+ // ra_renderpass params field.
+ bstr cached_program;
+
+ // --- type==RA_RENDERPASS_TYPE_RASTER only
+
+ // Describes the format of the vertex data. When using ra.glsl_vulkan,
+ // the order of this array must match the vertex attribute locations.
+ struct ra_renderpass_input *vertex_attribs;
+ int num_vertex_attribs;
+ int vertex_stride;
+
+ // Format of the target texture
+ const struct ra_format *target_format;
+
+ // Shader text, in GLSL. (Yes, you need a GLSL compiler.)
+ // These are complete shaders, including prelude and declarations.
+ const char *vertex_shader;
+ const char *frag_shader;
+
+ // Target blending mode. If enable_blend is false, the blend_ fields can
+ // be ignored.
+ bool enable_blend;
+ enum ra_blend blend_src_rgb;
+ enum ra_blend blend_dst_rgb;
+ enum ra_blend blend_src_alpha;
+ enum ra_blend blend_dst_alpha;
+
+ // If true, the contents of `target` not written to will become undefined
+ bool invalidate_target;
+
+ // --- type==RA_RENDERPASS_TYPE_COMPUTE only
+
+ // Shader text, like vertex_shader/frag_shader.
+ const char *compute_shader;
+};
+
+struct ra_renderpass_params *ra_renderpass_params_copy(void *ta_parent,
+ const struct ra_renderpass_params *params);
+
+// Conflates the following typical GPU API concepts:
+// - various kinds of shaders
+// - rendering pipelines
+// - descriptor sets, uniforms, other bindings
+// - all synchronization necessary
+// - the current values of all uniforms (this one makes it relatively stateful
+// from an API perspective)
+struct ra_renderpass {
+ // All fields are read-only after creation.
+ struct ra_renderpass_params params;
+ void *priv;
+};
+
+// An input value (see ra_renderpass_input).
+struct ra_renderpass_input_val {
+ int index; // index into ra_renderpass_params.inputs[]
+ void *data; // pointer to data according to ra_renderpass_input
+ // (e.g. type==RA_VARTYPE_FLOAT+dim_v=3,dim_m=3 => float[9])
+};
+
+// Parameters for performing a rendering pass (basically the dynamic params).
+// These change potentially every time.
+struct ra_renderpass_run_params {
+ struct ra_renderpass *pass;
+
+ // Generally this lists parameters only which changed since the last
+ // invocation and need to be updated. The ra_renderpass instance is
+ // supposed to keep unchanged values from the previous run.
+ // For non-primitive types like textures, these entries are always added,
+ // even if they do not change.
+ struct ra_renderpass_input_val *values;
+ int num_values;
+ void *push_constants; // must be set if params.push_constants_size > 0
+
+ // --- pass->params.type==RA_RENDERPASS_TYPE_RASTER only
+
+ // target->params.render_dst must be true, and target->params.format must
+ // match pass->params.target_format.
+ struct ra_tex *target;
+ struct mp_rect viewport;
+ struct mp_rect scissors;
+
+ // (The primitive type is always a triangle list.)
+ void *vertex_data;
+ int vertex_count; // number of vertex elements, not bytes
+
+ // --- pass->params.type==RA_RENDERPASS_TYPE_COMPUTE only
+
+ // Number of work groups to be run in X/Y/Z dimensions.
+ int compute_groups[3];
+};
+
+// This is an opaque type provided by the implementation, but we want to at
+// least give it a saner name than void* for code readability purposes.
+typedef void ra_timer;
+
+// Rendering API entrypoints. (Note: there are some additional hidden features
+// you need to take care of. For example, hwdec mapping will be provided
+// separately from ra, but might need to call into ra private code.)
+struct ra_fns {
+ void (*destroy)(struct ra *ra);
+
+ // Create a texture (with undefined contents). Return NULL on failure.
+ // This is a rare operation, and normally textures and even FBOs for
+ // temporary rendering intermediate data are cached.
+ struct ra_tex *(*tex_create)(struct ra *ra,
+ const struct ra_tex_params *params);
+
+ void (*tex_destroy)(struct ra *ra, struct ra_tex *tex);
+
+ // Upload data to a texture. This is an extremely common operation. When
+ // using a buffer, the contents of the buffer must exactly match the image
+ // - conversions between bit depth etc. are not supported. The buffer *may*
+ // be marked as "in use" while this operation is going on, and the contents
+ // must not be touched again by the API user until buf_poll returns true.
+ // Returns whether successful.
+ bool (*tex_upload)(struct ra *ra, const struct ra_tex_upload_params *params);
+
+ // Copy data from the texture to memory. ra_tex_params.downloadable must
+ // have been set to true on texture creation.
+ bool (*tex_download)(struct ra *ra, struct ra_tex_download_params *params);
+
+ // Create a buffer. This can be used as a persistently mapped buffer,
+ // a uniform buffer, a shader storage buffer or possibly others.
+ // Not all usage types must be supported; may return NULL if unavailable.
+ struct ra_buf *(*buf_create)(struct ra *ra,
+ const struct ra_buf_params *params);
+
+ void (*buf_destroy)(struct ra *ra, struct ra_buf *buf);
+
+ // Update the contents of a buffer, starting at a given offset (*must* be a
+ // multiple of 4) and up to a given size, with the contents of *data. This
+ // is an extremely common operation. Calling this while the buffer is
+ // considered "in use" is an error. (See: buf_poll)
+ void (*buf_update)(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size);
+
+ // Returns if a buffer is currently "in use" or not. Updating the contents
+ // of a buffer (via buf_update or writing to buf->data) while it is still
+ // in use is an error and may result in graphical corruption. Optional, if
+ // NULL then all buffers are always usable.
+ bool (*buf_poll)(struct ra *ra, struct ra_buf *buf);
+
+ // Returns the layout requirements of a uniform buffer element. Optional,
+ // but must be implemented if RA_CAP_BUF_RO is supported.
+ struct ra_layout (*uniform_layout)(struct ra_renderpass_input *inp);
+
+ // Returns the layout requirements of a push constant element. Optional,
+ // but must be implemented if ra.max_pushc_size > 0.
+ struct ra_layout (*push_constant_layout)(struct ra_renderpass_input *inp);
+
+ // Returns an abstract namespace index for a given renderpass input type.
+ // This will always be a value >= 0 and < RA_VARTYPE_COUNT. This is used to
+ // figure out which inputs may share the same value of `binding`.
+ int (*desc_namespace)(struct ra *ra, enum ra_vartype type);
+
+ // Clear the dst with the given color (rgba) and within the given scissor.
+ // dst must have dst->params.render_dst==true. Content outside of the
+ // scissor is preserved.
+ void (*clear)(struct ra *ra, struct ra_tex *dst, float color[4],
+ struct mp_rect *scissor);
+
+ // Copy a sub-rectangle from one texture to another. The source/dest region
+ // is always within the texture bounds. Areas outside the dest region are
+ // preserved. The formats of the textures must be loosely compatible. The
+ // dst texture can be a swapchain framebuffer, but src can not. Only 2D
+ // textures are supported.
+ // The textures must have blit_src and blit_dst set, respectively.
+ // Rectangles with negative width/height lead to flipping, different src/dst
+ // sizes lead to point scaling. Coordinates are always in pixels.
+ // Optional. Only available if RA_CAP_BLIT is set (if it's not set, it must
+ // not be called, even if it's non-NULL).
+ void (*blit)(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc);
+
+ // Compile a shader and create a pipeline. This is a rare operation.
+ // The params pointer and anything it points to must stay valid until
+ // renderpass_destroy.
+ struct ra_renderpass *(*renderpass_create)(struct ra *ra,
+ const struct ra_renderpass_params *params);
+
+ void (*renderpass_destroy)(struct ra *ra, struct ra_renderpass *pass);
+
+ // Perform a render pass, basically drawing a list of triangles to a FBO.
+ // This is an extremely common operation.
+ void (*renderpass_run)(struct ra *ra,
+ const struct ra_renderpass_run_params *params);
+
+ // Create a timer object. Returns NULL on failure, or if timers are
+ // unavailable for some reason. Optional.
+ ra_timer *(*timer_create)(struct ra *ra);
+
+ void (*timer_destroy)(struct ra *ra, ra_timer *timer);
+
+ // Start recording a timer. Note that valid usage requires you to pair
+ // every start with a stop. Trying to start a timer twice, or trying to
+ // stop a timer before having started it, consistutes invalid usage.
+ void (*timer_start)(struct ra *ra, ra_timer *timer);
+
+ // Stop recording a timer. This also returns any results that have been
+ // measured since the last usage of this ra_timer. It's important to note
+ // that GPU timer measurement are asynchronous, so this function does not
+ // always produce a value - and the values it does produce are typically
+ // delayed by a few frames. When no value is available, this returns 0.
+ uint64_t (*timer_stop)(struct ra *ra, ra_timer *timer);
+
+ // Associates a marker with any past error messages, for debugging
+ // purposes. Optional.
+ void (*debug_marker)(struct ra *ra, const char *msg);
+};
+
+struct ra_tex *ra_tex_create(struct ra *ra, const struct ra_tex_params *params);
+void ra_tex_free(struct ra *ra, struct ra_tex **tex);
+
+struct ra_buf *ra_buf_create(struct ra *ra, const struct ra_buf_params *params);
+void ra_buf_free(struct ra *ra, struct ra_buf **buf);
+
+void ra_free(struct ra **ra);
+
+const struct ra_format *ra_find_unorm_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components);
+const struct ra_format *ra_find_uint_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components);
+const struct ra_format *ra_find_float16_format(struct ra *ra, int n_components);
+const struct ra_format *ra_find_named_format(struct ra *ra, const char *name);
+
+struct ra_imgfmt_desc {
+ int num_planes;
+ const struct ra_format *planes[4];
+ // Chroma pixel size (1x1 is 4:4:4)
+ uint8_t chroma_w, chroma_h;
+ // Component storage size in bits (possibly padded). For formats with
+ // different sizes per component, this is arbitrary. For padded formats
+ // like P010 or YUV420P10, padding is included.
+ int component_bits;
+ // Like mp_regular_imgfmt.component_pad.
+ int component_pad;
+ // == planes[n].ctype (RA_CTYPE_UNKNOWN if not applicable)
+ enum ra_ctype component_type;
+ // For each texture and each texture output (rgba order) describe what
+ // component it returns.
+ // The values are like the values in mp_regular_imgfmt_plane.components[].
+ // Access as components[plane_nr][component_index]. Set unused items to 0.
+ // For ra_format.luminance_alpha, this returns 1/2 ("rg") instead of 1/4
+ // ("ra"). the logic is that the texture format has 2 channels, thus the
+ // data must be returned in the first two components. The renderer fixes
+ // this later.
+ uint8_t components[4][4];
+};
+
+const char *ra_fmt_glsl_format(const struct ra_format *fmt);
+
+bool ra_get_imgfmt_desc(struct ra *ra, int imgfmt, struct ra_imgfmt_desc *out);
+
+void ra_dump_tex_formats(struct ra *ra, int msgl);
+void ra_dump_imgfmt_desc(struct ra *ra, const struct ra_imgfmt_desc *desc,
+ int msgl);
+void ra_dump_img_formats(struct ra *ra, int msgl);
diff --git a/video/out/gpu/shader_cache.c b/video/out/gpu/shader_cache.c
new file mode 100644
index 0000000..3e05173
--- /dev/null
+++ b/video/out/gpu/shader_cache.c
@@ -0,0 +1,1056 @@
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include <libavutil/sha.h>
+#include <libavutil/mem.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "options/path.h"
+#include "stream/stream.h"
+#include "shader_cache.h"
+#include "utils.h"
+
+// Force cache flush if more than this number of shaders is created.
+#define SC_MAX_ENTRIES 256
+
+union uniform_val {
+ float f[9]; // RA_VARTYPE_FLOAT
+ int i[4]; // RA_VARTYPE_INT
+ struct ra_tex *tex; // RA_VARTYPE_TEX, RA_VARTYPE_IMG_*
+ struct ra_buf *buf; // RA_VARTYPE_BUF_*
+};
+
+enum sc_uniform_type {
+ SC_UNIFORM_TYPE_GLOBAL = 0, // global uniform (RA_CAP_GLOBAL_UNIFORM)
+ SC_UNIFORM_TYPE_UBO = 1, // uniform buffer (RA_CAP_BUF_RO)
+ SC_UNIFORM_TYPE_PUSHC = 2, // push constant (ra.max_pushc_size)
+};
+
+struct sc_uniform {
+ enum sc_uniform_type type;
+ struct ra_renderpass_input input;
+ const char *glsl_type;
+ union uniform_val v;
+ char *buffer_format;
+ // for SC_UNIFORM_TYPE_UBO/PUSHC:
+ struct ra_layout layout;
+ size_t offset; // byte offset within the buffer
+};
+
+struct sc_cached_uniform {
+ union uniform_val v;
+ int index; // for ra_renderpass_input_val
+ bool set; // whether the uniform has ever been set
+};
+
+struct sc_entry {
+ struct ra_renderpass *pass;
+ struct sc_cached_uniform *cached_uniforms;
+ int num_cached_uniforms;
+ bstr total;
+ struct timer_pool *timer;
+ struct ra_buf *ubo;
+ int ubo_index; // for ra_renderpass_input_val.index
+ void *pushc;
+};
+
+struct gl_shader_cache {
+ struct ra *ra;
+ struct mp_log *log;
+
+ // permanent
+ char **exts;
+ int num_exts;
+
+ // this is modified during use (gl_sc_add() etc.) and reset for each shader
+ bstr prelude_text;
+ bstr header_text;
+ bstr text;
+
+ // Next binding point (texture unit, image unit, buffer binding, etc.)
+ // In OpenGL these are separate for each input type
+ int next_binding[RA_VARTYPE_COUNT];
+ bool next_uniform_dynamic;
+
+ struct ra_renderpass_params params;
+
+ struct sc_entry **entries;
+ int num_entries;
+
+ struct sc_entry *current_shader; // set by gl_sc_generate()
+
+ struct sc_uniform *uniforms;
+ int num_uniforms;
+
+ int ubo_binding;
+ size_t ubo_size;
+ size_t pushc_size;
+
+ struct ra_renderpass_input_val *values;
+ int num_values;
+
+ // For checking that the user is calling gl_sc_reset() properly.
+ bool needs_reset;
+
+ bool error_state; // true if an error occurred
+
+ // temporary buffers (avoids frequent reallocations)
+ bstr tmp[6];
+
+ // For the disk-cache.
+ char *cache_dir;
+ struct mpv_global *global; // can be NULL
+};
+
+struct gl_shader_cache *gl_sc_create(struct ra *ra, struct mpv_global *global,
+ struct mp_log *log)
+{
+ struct gl_shader_cache *sc = talloc_ptrtype(NULL, sc);
+ *sc = (struct gl_shader_cache){
+ .ra = ra,
+ .global = global,
+ .log = log,
+ };
+ gl_sc_reset(sc);
+ return sc;
+}
+
+// Reset the previous pass. This must be called after gl_sc_generate and before
+// starting a new shader. It may also be called on errors.
+void gl_sc_reset(struct gl_shader_cache *sc)
+{
+ sc->prelude_text.len = 0;
+ sc->header_text.len = 0;
+ sc->text.len = 0;
+ for (int n = 0; n < sc->num_uniforms; n++)
+ talloc_free((void *)sc->uniforms[n].input.name);
+ sc->num_uniforms = 0;
+ sc->ubo_binding = 0;
+ sc->ubo_size = 0;
+ sc->pushc_size = 0;
+ for (int i = 0; i < RA_VARTYPE_COUNT; i++)
+ sc->next_binding[i] = 0;
+ sc->next_uniform_dynamic = false;
+ sc->current_shader = NULL;
+ sc->params = (struct ra_renderpass_params){0};
+ sc->needs_reset = false;
+}
+
+static void sc_flush_cache(struct gl_shader_cache *sc)
+{
+ MP_DBG(sc, "flushing shader cache\n");
+
+ for (int n = 0; n < sc->num_entries; n++) {
+ struct sc_entry *e = sc->entries[n];
+ ra_buf_free(sc->ra, &e->ubo);
+ if (e->pass)
+ sc->ra->fns->renderpass_destroy(sc->ra, e->pass);
+ timer_pool_destroy(e->timer);
+ talloc_free(e);
+ }
+ sc->num_entries = 0;
+}
+
+void gl_sc_destroy(struct gl_shader_cache *sc)
+{
+ if (!sc)
+ return;
+ gl_sc_reset(sc);
+ sc_flush_cache(sc);
+ talloc_free(sc);
+}
+
+bool gl_sc_error_state(struct gl_shader_cache *sc)
+{
+ return sc->error_state;
+}
+
+void gl_sc_reset_error(struct gl_shader_cache *sc)
+{
+ sc->error_state = false;
+}
+
+void gl_sc_enable_extension(struct gl_shader_cache *sc, char *name)
+{
+ for (int n = 0; n < sc->num_exts; n++) {
+ if (strcmp(sc->exts[n], name) == 0)
+ return;
+ }
+ MP_TARRAY_APPEND(sc, sc->exts, sc->num_exts, talloc_strdup(sc, name));
+}
+
+#define bstr_xappend0(sc, b, s) bstr_xappend(sc, b, bstr0(s))
+
+void gl_sc_add(struct gl_shader_cache *sc, const char *text)
+{
+ bstr_xappend0(sc, &sc->text, text);
+}
+
+void gl_sc_addf(struct gl_shader_cache *sc, const char *textf, ...)
+{
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(sc, &sc->text, textf, ap);
+ va_end(ap);
+}
+
+void gl_sc_hadd(struct gl_shader_cache *sc, const char *text)
+{
+ bstr_xappend0(sc, &sc->header_text, text);
+}
+
+void gl_sc_haddf(struct gl_shader_cache *sc, const char *textf, ...)
+{
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(sc, &sc->header_text, textf, ap);
+ va_end(ap);
+}
+
+void gl_sc_hadd_bstr(struct gl_shader_cache *sc, struct bstr text)
+{
+ bstr_xappend(sc, &sc->header_text, text);
+}
+
+void gl_sc_paddf(struct gl_shader_cache *sc, const char *textf, ...)
+{
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(sc, &sc->prelude_text, textf, ap);
+ va_end(ap);
+}
+
+static struct sc_uniform *find_uniform(struct gl_shader_cache *sc,
+ const char *name)
+{
+ struct sc_uniform new = {
+ .input = {
+ .dim_v = 1,
+ .dim_m = 1,
+ },
+ };
+
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (strcmp(u->input.name, name) == 0) {
+ const char *allocname = u->input.name;
+ *u = new;
+ u->input.name = allocname;
+ return u;
+ }
+ }
+
+ // not found -> add it
+ new.input.name = talloc_strdup(NULL, name);
+ MP_TARRAY_APPEND(sc, sc->uniforms, sc->num_uniforms, new);
+ return &sc->uniforms[sc->num_uniforms - 1];
+}
+
+static int gl_sc_next_binding(struct gl_shader_cache *sc, enum ra_vartype type)
+{
+ return sc->next_binding[sc->ra->fns->desc_namespace(sc->ra, type)]++;
+}
+
+void gl_sc_uniform_dynamic(struct gl_shader_cache *sc)
+{
+ sc->next_uniform_dynamic = true;
+}
+
+// Updates the metadata for the given sc_uniform. Assumes sc_uniform->input
+// and glsl_type/buffer_format are already set.
+static void update_uniform_params(struct gl_shader_cache *sc, struct sc_uniform *u)
+{
+ bool dynamic = sc->next_uniform_dynamic;
+ sc->next_uniform_dynamic = false;
+
+ // Try not using push constants for "large" values like matrices, since
+ // this is likely to both exceed the VGPR budget as well as the pushc size
+ // budget
+ bool try_pushc = u->input.dim_m == 1 || dynamic;
+
+ // Attempt using push constants first
+ if (try_pushc && sc->ra->glsl_vulkan && sc->ra->max_pushc_size) {
+ struct ra_layout layout = sc->ra->fns->push_constant_layout(&u->input);
+ size_t offset = MP_ALIGN_UP(sc->pushc_size, layout.align);
+ // Push constants have limited size, so make sure we don't exceed this
+ size_t new_size = offset + layout.size;
+ if (new_size <= sc->ra->max_pushc_size) {
+ u->type = SC_UNIFORM_TYPE_PUSHC;
+ u->layout = layout;
+ u->offset = offset;
+ sc->pushc_size = new_size;
+ return;
+ }
+ }
+
+ // Attempt using uniform buffer next. The GLSL version 440 check is due
+ // to explicit offsets on UBO entries. In theory we could leave away
+ // the offsets and support UBOs for older GL as well, but this is a nice
+ // safety net for driver bugs (and also rules out potentially buggy drivers)
+ // Also avoid UBOs for highly dynamic stuff since that requires synchronizing
+ // the UBO writes every frame
+ bool try_ubo = !(sc->ra->caps & RA_CAP_GLOBAL_UNIFORM) || !dynamic;
+ if (try_ubo && sc->ra->glsl_version >= 440 && (sc->ra->caps & RA_CAP_BUF_RO)) {
+ u->type = SC_UNIFORM_TYPE_UBO;
+ u->layout = sc->ra->fns->uniform_layout(&u->input);
+ u->offset = MP_ALIGN_UP(sc->ubo_size, u->layout.align);
+ sc->ubo_size = u->offset + u->layout.size;
+ return;
+ }
+
+ // If all else fails, use global uniforms
+ assert(sc->ra->caps & RA_CAP_GLOBAL_UNIFORM);
+ u->type = SC_UNIFORM_TYPE_GLOBAL;
+}
+
+void gl_sc_uniform_texture(struct gl_shader_cache *sc, char *name,
+ struct ra_tex *tex)
+{
+ const char *glsl_type = "sampler2D";
+ if (tex->params.dimensions == 1) {
+ glsl_type = "sampler1D";
+ } else if (tex->params.dimensions == 3) {
+ glsl_type = "sampler3D";
+ } else if (tex->params.non_normalized) {
+ glsl_type = "sampler2DRect";
+ } else if (tex->params.external_oes) {
+ glsl_type = "samplerExternalOES";
+ } else if (tex->params.format->ctype == RA_CTYPE_UINT) {
+ glsl_type = sc->ra->glsl_es ? "highp usampler2D" : "usampler2D";
+ }
+
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_TEX;
+ u->glsl_type = glsl_type;
+ u->input.binding = gl_sc_next_binding(sc, u->input.type);
+ u->v.tex = tex;
+}
+
+void gl_sc_uniform_image2D_wo(struct gl_shader_cache *sc, const char *name,
+ struct ra_tex *tex)
+{
+ gl_sc_enable_extension(sc, "GL_ARB_shader_image_load_store");
+
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_IMG_W;
+ u->glsl_type = sc->ra->glsl_es ? "writeonly highp image2D" : "writeonly image2D";
+ u->input.binding = gl_sc_next_binding(sc, u->input.type);
+ u->v.tex = tex;
+}
+
+void gl_sc_ssbo(struct gl_shader_cache *sc, char *name, struct ra_buf *buf,
+ char *format, ...)
+{
+ assert(sc->ra->caps & RA_CAP_BUF_RW);
+ gl_sc_enable_extension(sc, "GL_ARB_shader_storage_buffer_object");
+
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_BUF_RW;
+ u->glsl_type = "";
+ u->input.binding = gl_sc_next_binding(sc, u->input.type);
+ u->v.buf = buf;
+
+ va_list ap;
+ va_start(ap, format);
+ u->buffer_format = ta_vasprintf(sc, format, ap);
+ va_end(ap);
+}
+
+void gl_sc_uniform_f(struct gl_shader_cache *sc, char *name, float f)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->glsl_type = "float";
+ update_uniform_params(sc, u);
+ u->v.f[0] = f;
+}
+
+void gl_sc_uniform_i(struct gl_shader_cache *sc, char *name, int i)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_INT;
+ u->glsl_type = "int";
+ update_uniform_params(sc, u);
+ u->v.i[0] = i;
+}
+
+void gl_sc_uniform_vec2(struct gl_shader_cache *sc, char *name, float f[2])
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 2;
+ u->glsl_type = "vec2";
+ update_uniform_params(sc, u);
+ u->v.f[0] = f[0];
+ u->v.f[1] = f[1];
+}
+
+void gl_sc_uniform_vec3(struct gl_shader_cache *sc, char *name, float f[3])
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 3;
+ u->glsl_type = "vec3";
+ update_uniform_params(sc, u);
+ u->v.f[0] = f[0];
+ u->v.f[1] = f[1];
+ u->v.f[2] = f[2];
+}
+
+static void transpose2x2(float r[2 * 2])
+{
+ MPSWAP(float, r[0+2*1], r[1+2*0]);
+}
+
+void gl_sc_uniform_mat2(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 2;
+ u->input.dim_m = 2;
+ u->glsl_type = "mat2";
+ update_uniform_params(sc, u);
+ for (int n = 0; n < 4; n++)
+ u->v.f[n] = v[n];
+ if (transpose)
+ transpose2x2(&u->v.f[0]);
+}
+
+static void transpose3x3(float r[3 * 3])
+{
+ MPSWAP(float, r[0+3*1], r[1+3*0]);
+ MPSWAP(float, r[0+3*2], r[2+3*0]);
+ MPSWAP(float, r[1+3*2], r[2+3*1]);
+}
+
+void gl_sc_uniform_mat3(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 3;
+ u->input.dim_m = 3;
+ u->glsl_type = "mat3";
+ update_uniform_params(sc, u);
+ for (int n = 0; n < 9; n++)
+ u->v.f[n] = v[n];
+ if (transpose)
+ transpose3x3(&u->v.f[0]);
+}
+
+void gl_sc_blend(struct gl_shader_cache *sc,
+ enum ra_blend blend_src_rgb,
+ enum ra_blend blend_dst_rgb,
+ enum ra_blend blend_src_alpha,
+ enum ra_blend blend_dst_alpha)
+{
+ sc->params.enable_blend = true;
+ sc->params.blend_src_rgb = blend_src_rgb;
+ sc->params.blend_dst_rgb = blend_dst_rgb;
+ sc->params.blend_src_alpha = blend_src_alpha;
+ sc->params.blend_dst_alpha = blend_dst_alpha;
+}
+
+const char *gl_sc_bvec(struct gl_shader_cache *sc, int dims)
+{
+ static const char *bvecs[] = {
+ [1] = "bool",
+ [2] = "bvec2",
+ [3] = "bvec3",
+ [4] = "bvec4",
+ };
+
+ static const char *vecs[] = {
+ [1] = "float",
+ [2] = "vec2",
+ [3] = "vec3",
+ [4] = "vec4",
+ };
+
+ assert(dims > 0 && dims < MP_ARRAY_SIZE(bvecs));
+ return sc->ra->glsl_version >= 130 ? bvecs[dims] : vecs[dims];
+}
+
+static const char *vao_glsl_type(const struct ra_renderpass_input *e)
+{
+ // pretty dumb... too dumb, but works for us
+ switch (e->dim_v) {
+ case 1: return "float";
+ case 2: return "vec2";
+ case 3: return "vec3";
+ case 4: return "vec4";
+ default: MP_ASSERT_UNREACHABLE();
+ }
+}
+
+static void update_ubo(struct ra *ra, struct ra_buf *ubo, struct sc_uniform *u)
+{
+ uintptr_t src = (uintptr_t) &u->v;
+ size_t dst = u->offset;
+ struct ra_layout src_layout = ra_renderpass_input_layout(&u->input);
+ struct ra_layout dst_layout = u->layout;
+
+ for (int i = 0; i < u->input.dim_m; i++) {
+ ra->fns->buf_update(ra, ubo, dst, (void *)src, src_layout.stride);
+ src += src_layout.stride;
+ dst += dst_layout.stride;
+ }
+}
+
+static void update_pushc(struct ra *ra, void *pushc, struct sc_uniform *u)
+{
+ uintptr_t src = (uintptr_t) &u->v;
+ uintptr_t dst = (uintptr_t) pushc + (ptrdiff_t) u->offset;
+ struct ra_layout src_layout = ra_renderpass_input_layout(&u->input);
+ struct ra_layout dst_layout = u->layout;
+
+ for (int i = 0; i < u->input.dim_m; i++) {
+ memcpy((void *)dst, (void *)src, src_layout.stride);
+ src += src_layout.stride;
+ dst += dst_layout.stride;
+ }
+}
+
+static void update_uniform(struct gl_shader_cache *sc, struct sc_entry *e,
+ struct sc_uniform *u, int n)
+{
+ struct sc_cached_uniform *un = &e->cached_uniforms[n];
+ struct ra_layout layout = ra_renderpass_input_layout(&u->input);
+ if (layout.size > 0 && un->set && memcmp(&un->v, &u->v, layout.size) == 0)
+ return;
+
+ un->v = u->v;
+ un->set = true;
+
+ static const char *desc[] = {
+ [SC_UNIFORM_TYPE_UBO] = "UBO",
+ [SC_UNIFORM_TYPE_PUSHC] = "PC",
+ [SC_UNIFORM_TYPE_GLOBAL] = "global",
+ };
+ MP_TRACE(sc, "Updating %s uniform '%s'\n", desc[u->type], u->input.name);
+
+ switch (u->type) {
+ case SC_UNIFORM_TYPE_GLOBAL: {
+ struct ra_renderpass_input_val value = {
+ .index = un->index,
+ .data = &un->v,
+ };
+ MP_TARRAY_APPEND(sc, sc->values, sc->num_values, value);
+ break;
+ }
+ case SC_UNIFORM_TYPE_UBO:
+ assert(e->ubo);
+ update_ubo(sc->ra, e->ubo, u);
+ break;
+ case SC_UNIFORM_TYPE_PUSHC:
+ assert(e->pushc);
+ update_pushc(sc->ra, e->pushc, u);
+ break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+}
+
+void gl_sc_set_cache_dir(struct gl_shader_cache *sc, char *dir)
+{
+ talloc_free(sc->cache_dir);
+ if (dir && dir[0]) {
+ dir = mp_get_user_path(NULL, sc->global, dir);
+ } else {
+ dir = mp_find_user_file(NULL, sc->global, "cache", "");
+ }
+ sc->cache_dir = talloc_strdup(sc, dir);
+ talloc_free(dir);
+}
+
+static bool create_pass(struct gl_shader_cache *sc, struct sc_entry *entry)
+{
+ bool ret = false;
+
+ void *tmp = talloc_new(NULL);
+ struct ra_renderpass_params params = sc->params;
+
+ const char *cache_header = "mpv shader cache v1\n";
+ char *cache_filename = NULL;
+ char *cache_dir = NULL;
+
+ if (sc->cache_dir && sc->cache_dir[0]) {
+ // Try to load it from a disk cache.
+ cache_dir = mp_get_user_path(tmp, sc->global, sc->cache_dir);
+
+ struct AVSHA *sha = av_sha_alloc();
+ MP_HANDLE_OOM(sha);
+ av_sha_init(sha, 256);
+ av_sha_update(sha, entry->total.start, entry->total.len);
+
+ uint8_t hash[256 / 8];
+ av_sha_final(sha, hash);
+ av_free(sha);
+
+ char hashstr[256 / 8 * 2 + 1];
+ for (int n = 0; n < 256 / 8; n++)
+ snprintf(hashstr + n * 2, sizeof(hashstr) - n * 2, "%02X", hash[n]);
+
+ cache_filename = mp_path_join(tmp, cache_dir, hashstr);
+ if (stat(cache_filename, &(struct stat){0}) == 0) {
+ MP_DBG(sc, "Trying to load shader from disk...\n");
+ struct bstr cachedata =
+ stream_read_file(cache_filename, tmp, sc->global, 1000000000);
+ if (bstr_eatstart0(&cachedata, cache_header))
+ params.cached_program = cachedata;
+ }
+ }
+
+ // If using a UBO, also make sure to add it as an input value so the RA
+ // can see it
+ if (sc->ubo_size) {
+ entry->ubo_index = sc->params.num_inputs;
+ struct ra_renderpass_input ubo_input = {
+ .name = "UBO",
+ .type = RA_VARTYPE_BUF_RO,
+ .dim_v = 1,
+ .dim_m = 1,
+ .binding = sc->ubo_binding,
+ };
+ MP_TARRAY_APPEND(sc, params.inputs, params.num_inputs, ubo_input);
+ }
+
+ if (sc->pushc_size) {
+ params.push_constants_size = MP_ALIGN_UP(sc->pushc_size, 4);
+ entry->pushc = talloc_zero_size(entry, params.push_constants_size);
+ }
+
+ if (sc->ubo_size) {
+ struct ra_buf_params ubo_params = {
+ .type = RA_BUF_TYPE_UNIFORM,
+ .size = sc->ubo_size,
+ .host_mutable = true,
+ };
+
+ entry->ubo = ra_buf_create(sc->ra, &ubo_params);
+ if (!entry->ubo) {
+ MP_ERR(sc, "Failed creating uniform buffer!\n");
+ goto error;
+ }
+ }
+
+ entry->pass = sc->ra->fns->renderpass_create(sc->ra, &params);
+ if (!entry->pass)
+ goto error;
+
+ if (entry->pass && cache_filename) {
+ bstr nc = entry->pass->params.cached_program;
+ if (nc.len && !bstr_equals(params.cached_program, nc)) {
+ mp_mkdirp(cache_dir);
+
+ MP_DBG(sc, "Writing shader cache file: %s\n", cache_filename);
+ FILE *out = fopen(cache_filename, "wb");
+ if (out) {
+ fwrite(cache_header, strlen(cache_header), 1, out);
+ fwrite(nc.start, nc.len, 1, out);
+ fclose(out);
+ }
+ }
+ }
+
+ ret = true;
+
+error:
+ talloc_free(tmp);
+ return ret;
+}
+
+#define ADD(x, ...) bstr_xappend_asprintf(sc, (x), __VA_ARGS__)
+#define ADD_BSTR(x, s) bstr_xappend(sc, (x), (s))
+
+static void add_uniforms(struct gl_shader_cache *sc, bstr *dst)
+{
+ // Add all of the UBO entries separately as members of their own buffer
+ if (sc->ubo_size > 0) {
+ ADD(dst, "layout(std140, binding=%d) uniform UBO {\n", sc->ubo_binding);
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (u->type != SC_UNIFORM_TYPE_UBO)
+ continue;
+ ADD(dst, "layout(offset=%zu) %s %s;\n", u->offset, u->glsl_type,
+ u->input.name);
+ }
+ ADD(dst, "};\n");
+ }
+
+ // Ditto for push constants
+ if (sc->pushc_size > 0) {
+ ADD(dst, "layout(std430, push_constant) uniform PushC {\n");
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (u->type != SC_UNIFORM_TYPE_PUSHC)
+ continue;
+ ADD(dst, "layout(offset=%zu) %s %s;\n", u->offset, u->glsl_type,
+ u->input.name);
+ }
+ ADD(dst, "};\n");
+ }
+
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (u->type != SC_UNIFORM_TYPE_GLOBAL)
+ continue;
+ switch (u->input.type) {
+ case RA_VARTYPE_INT:
+ case RA_VARTYPE_FLOAT:
+ assert(sc->ra->caps & RA_CAP_GLOBAL_UNIFORM);
+ MP_FALLTHROUGH;
+ case RA_VARTYPE_TEX:
+ // Vulkan requires explicitly assigning the bindings in the shader
+ // source. For OpenGL it's optional, but requires higher GL version
+ // so we don't do it (and instead have ra_gl update the bindings
+ // after program creation).
+ if (sc->ra->glsl_vulkan)
+ ADD(dst, "layout(binding=%d) ", u->input.binding);
+ ADD(dst, "uniform %s %s;\n", u->glsl_type, u->input.name);
+ break;
+ case RA_VARTYPE_BUF_RO:
+ ADD(dst, "layout(std140, binding=%d) uniform %s { %s };\n",
+ u->input.binding, u->input.name, u->buffer_format);
+ break;
+ case RA_VARTYPE_BUF_RW:
+ ADD(dst, "layout(std430, binding=%d) restrict coherent buffer %s { %s };\n",
+ u->input.binding, u->input.name, u->buffer_format);
+ break;
+ case RA_VARTYPE_IMG_W: {
+ // For better compatibility, we have to explicitly label the
+ // type of data we will be reading/writing to this image.
+ const char *fmt = u->v.tex->params.format->glsl_format;
+
+ if (sc->ra->glsl_vulkan) {
+ if (fmt) {
+ ADD(dst, "layout(binding=%d, %s) ", u->input.binding, fmt);
+ } else {
+ ADD(dst, "layout(binding=%d) ", u->input.binding);
+ }
+ } else if (fmt) {
+ ADD(dst, "layout(%s) ", fmt);
+ }
+ ADD(dst, "uniform restrict %s %s;\n", u->glsl_type, u->input.name);
+ }
+ }
+ }
+}
+
+// 1. Generate vertex and fragment shaders from the fragment shader text added
+// with gl_sc_add(). The generated shader program is cached (based on the
+// text), so actual compilation happens only the first time.
+// 2. Update the uniforms and textures set with gl_sc_uniform_*.
+// 3. Make the new shader program current (glUseProgram()).
+// After that, you render, and then you call gc_sc_reset(), which does:
+// 1. Unbind the program and all textures.
+// 2. Reset the sc state and prepare for a new shader program. (All uniforms
+// and fragment operations needed for the next program have to be re-added.)
+static void gl_sc_generate(struct gl_shader_cache *sc,
+ enum ra_renderpass_type type,
+ const struct ra_format *target_format,
+ const struct ra_renderpass_input *vao,
+ int vao_len, size_t vertex_stride)
+{
+ int glsl_version = sc->ra->glsl_version;
+ int glsl_es = sc->ra->glsl_es ? glsl_version : 0;
+
+ sc->params.type = type;
+
+ // gl_sc_reset() must be called after ending the previous render process,
+ // and before starting a new one.
+ assert(!sc->needs_reset);
+ sc->needs_reset = true;
+
+ // If using a UBO, pick a binding (needed for shader generation)
+ if (sc->ubo_size)
+ sc->ubo_binding = gl_sc_next_binding(sc, RA_VARTYPE_BUF_RO);
+
+ for (int n = 0; n < MP_ARRAY_SIZE(sc->tmp); n++)
+ sc->tmp[n].len = 0;
+
+ // set up shader text (header + uniforms + body)
+ bstr *header = &sc->tmp[0];
+ ADD(header, "#version %d%s\n", glsl_version, glsl_es >= 300 ? " es" : "");
+ if (type == RA_RENDERPASS_TYPE_COMPUTE) {
+ // This extension cannot be enabled in fragment shader. Enable it as
+ // an exception for compute shader.
+ ADD(header, "#extension GL_ARB_compute_shader : enable\n");
+ }
+ for (int n = 0; n < sc->num_exts; n++)
+ ADD(header, "#extension %s : enable\n", sc->exts[n]);
+ if (glsl_es) {
+ ADD(header, "#ifdef GL_FRAGMENT_PRECISION_HIGH\n");
+ ADD(header, "precision highp float;\n");
+ ADD(header, "#else\n");
+ ADD(header, "precision mediump float;\n");
+ ADD(header, "#endif\n");
+
+ ADD(header, "precision mediump sampler2D;\n");
+ if (sc->ra->caps & RA_CAP_TEX_3D)
+ ADD(header, "precision mediump sampler3D;\n");
+ }
+
+ if (glsl_version >= 130) {
+ ADD(header, "#define tex1D texture\n");
+ ADD(header, "#define tex3D texture\n");
+ } else {
+ ADD(header, "#define tex1D texture1D\n");
+ ADD(header, "#define tex3D texture3D\n");
+ ADD(header, "#define texture texture2D\n");
+ }
+
+ // Additional helpers.
+ ADD(header, "#define LUT_POS(x, lut_size)"
+ " mix(0.5 / (lut_size), 1.0 - 0.5 / (lut_size), (x))\n");
+
+ char *vert_in = glsl_version >= 130 ? "in" : "attribute";
+ char *vert_out = glsl_version >= 130 ? "out" : "varying";
+ char *frag_in = glsl_version >= 130 ? "in" : "varying";
+
+ struct bstr *vert = NULL, *frag = NULL, *comp = NULL;
+
+ if (type == RA_RENDERPASS_TYPE_RASTER) {
+ // vertex shader: we don't use the vertex shader, so just setup a
+ // dummy, which passes through the vertex array attributes.
+ bstr *vert_head = &sc->tmp[1];
+ ADD_BSTR(vert_head, *header);
+ bstr *vert_body = &sc->tmp[2];
+ ADD(vert_body, "void main() {\n");
+ bstr *frag_vaos = &sc->tmp[3];
+ for (int n = 0; n < vao_len; n++) {
+ const struct ra_renderpass_input *e = &vao[n];
+ const char *glsl_type = vao_glsl_type(e);
+ char loc[32] = {0};
+ if (sc->ra->glsl_vulkan)
+ snprintf(loc, sizeof(loc), "layout(location=%d) ", n);
+ if (strcmp(e->name, "position") == 0) {
+ // setting raster pos. requires setting gl_Position magic variable
+ assert(e->dim_v == 2 && e->type == RA_VARTYPE_FLOAT);
+ ADD(vert_head, "%s%s vec2 vertex_position;\n", loc, vert_in);
+ ADD(vert_body, "gl_Position = vec4(vertex_position, 1.0, 1.0);\n");
+ } else {
+ ADD(vert_head, "%s%s %s vertex_%s;\n", loc, vert_in, glsl_type, e->name);
+ ADD(vert_head, "%s%s %s %s;\n", loc, vert_out, glsl_type, e->name);
+ ADD(vert_body, "%s = vertex_%s;\n", e->name, e->name);
+ ADD(frag_vaos, "%s%s %s %s;\n", loc, frag_in, glsl_type, e->name);
+ }
+ }
+ ADD(vert_body, "}\n");
+ vert = vert_head;
+ ADD_BSTR(vert, *vert_body);
+
+ // fragment shader; still requires adding used uniforms and VAO elements
+ frag = &sc->tmp[4];
+ ADD_BSTR(frag, *header);
+ if (glsl_version >= 130) {
+ ADD(frag, "%sout vec4 out_color;\n",
+ sc->ra->glsl_vulkan ? "layout(location=0) " : "");
+ }
+ ADD_BSTR(frag, *frag_vaos);
+ add_uniforms(sc, frag);
+
+ ADD_BSTR(frag, sc->prelude_text);
+ ADD_BSTR(frag, sc->header_text);
+
+ ADD(frag, "void main() {\n");
+ // we require _all_ frag shaders to write to a "vec4 color"
+ ADD(frag, "vec4 color = vec4(0.0, 0.0, 0.0, 1.0);\n");
+ ADD_BSTR(frag, sc->text);
+ if (glsl_version >= 130) {
+ ADD(frag, "out_color = color;\n");
+ } else {
+ ADD(frag, "gl_FragColor = color;\n");
+ }
+ ADD(frag, "}\n");
+
+ // We need to fix the format of the render dst at renderpass creation
+ // time
+ assert(target_format);
+ sc->params.target_format = target_format;
+ }
+
+ if (type == RA_RENDERPASS_TYPE_COMPUTE) {
+ comp = &sc->tmp[4];
+ ADD_BSTR(comp, *header);
+
+ add_uniforms(sc, comp);
+
+ ADD_BSTR(comp, sc->prelude_text);
+ ADD_BSTR(comp, sc->header_text);
+
+ ADD(comp, "void main() {\n");
+ ADD(comp, "vec4 color = vec4(0.0, 0.0, 0.0, 1.0);\n"); // convenience
+ ADD_BSTR(comp, sc->text);
+ ADD(comp, "}\n");
+ }
+
+ bstr *hash_total = &sc->tmp[5];
+
+ ADD(hash_total, "type %d\n", sc->params.type);
+
+ if (frag) {
+ ADD_BSTR(hash_total, *frag);
+ sc->params.frag_shader = frag->start;
+ }
+ ADD(hash_total, "\n");
+ if (vert) {
+ ADD_BSTR(hash_total, *vert);
+ sc->params.vertex_shader = vert->start;
+ }
+ ADD(hash_total, "\n");
+ if (comp) {
+ ADD_BSTR(hash_total, *comp);
+ sc->params.compute_shader = comp->start;
+ }
+ ADD(hash_total, "\n");
+
+ if (sc->params.enable_blend) {
+ ADD(hash_total, "blend %d %d %d %d\n",
+ sc->params.blend_src_rgb, sc->params.blend_dst_rgb,
+ sc->params.blend_src_alpha, sc->params.blend_dst_alpha);
+ }
+
+ if (sc->params.target_format)
+ ADD(hash_total, "format %s\n", sc->params.target_format->name);
+
+ struct sc_entry *entry = NULL;
+ for (int n = 0; n < sc->num_entries; n++) {
+ struct sc_entry *cur = sc->entries[n];
+ if (bstr_equals(cur->total, *hash_total)) {
+ entry = cur;
+ break;
+ }
+ }
+ if (!entry) {
+ if (sc->num_entries == SC_MAX_ENTRIES)
+ sc_flush_cache(sc);
+ entry = talloc_ptrtype(NULL, entry);
+ *entry = (struct sc_entry){
+ .total = bstrdup(entry, *hash_total),
+ .timer = timer_pool_create(sc->ra),
+ };
+
+ // The vertex shader uses mangled names for the vertex attributes, so
+ // that the fragment shader can use the "real" names. But the shader is
+ // expecting the vertex attribute names (at least with older GLSL
+ // targets for GL).
+ sc->params.vertex_stride = vertex_stride;
+ for (int n = 0; n < vao_len; n++) {
+ struct ra_renderpass_input attrib = vao[n];
+ attrib.name = talloc_asprintf(entry, "vertex_%s", attrib.name);
+ MP_TARRAY_APPEND(sc, sc->params.vertex_attribs,
+ sc->params.num_vertex_attribs, attrib);
+ }
+
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_cached_uniform u = {0};
+ if (sc->uniforms[n].type == SC_UNIFORM_TYPE_GLOBAL) {
+ // global uniforms need to be made visible to the ra_renderpass
+ u.index = sc->params.num_inputs;
+ MP_TARRAY_APPEND(sc, sc->params.inputs, sc->params.num_inputs,
+ sc->uniforms[n].input);
+ }
+ MP_TARRAY_APPEND(entry, entry->cached_uniforms,
+ entry->num_cached_uniforms, u);
+ }
+ if (!create_pass(sc, entry))
+ sc->error_state = true;
+ MP_TARRAY_APPEND(sc, sc->entries, sc->num_entries, entry);
+ }
+
+ if (!entry->pass) {
+ sc->current_shader = NULL;
+ return;
+ }
+
+ assert(sc->num_uniforms == entry->num_cached_uniforms);
+
+ sc->num_values = 0;
+ for (int n = 0; n < sc->num_uniforms; n++)
+ update_uniform(sc, entry, &sc->uniforms[n], n);
+
+ // If we're using a UBO, make sure to bind it as well
+ if (sc->ubo_size) {
+ struct ra_renderpass_input_val ubo_val = {
+ .index = entry->ubo_index,
+ .data = &entry->ubo,
+ };
+ MP_TARRAY_APPEND(sc, sc->values, sc->num_values, ubo_val);
+ }
+
+ sc->current_shader = entry;
+}
+
+struct mp_pass_perf gl_sc_dispatch_draw(struct gl_shader_cache *sc,
+ struct ra_tex *target, bool discard,
+ const struct ra_renderpass_input *vao,
+ int vao_len, size_t vertex_stride,
+ void *vertices, size_t num_vertices)
+{
+ struct timer_pool *timer = NULL;
+
+ sc->params.invalidate_target = discard;
+ gl_sc_generate(sc, RA_RENDERPASS_TYPE_RASTER, target->params.format,
+ vao, vao_len, vertex_stride);
+ if (!sc->current_shader)
+ goto error;
+
+ timer = sc->current_shader->timer;
+
+ struct mp_rect full_rc = {0, 0, target->params.w, target->params.h};
+
+ struct ra_renderpass_run_params run = {
+ .pass = sc->current_shader->pass,
+ .values = sc->values,
+ .num_values = sc->num_values,
+ .push_constants = sc->current_shader->pushc,
+ .target = target,
+ .vertex_data = vertices,
+ .vertex_count = num_vertices,
+ .viewport = full_rc,
+ .scissors = full_rc,
+ };
+
+ timer_pool_start(timer);
+ sc->ra->fns->renderpass_run(sc->ra, &run);
+ timer_pool_stop(timer);
+
+error:
+ gl_sc_reset(sc);
+ return timer_pool_measure(timer);
+}
+
+struct mp_pass_perf gl_sc_dispatch_compute(struct gl_shader_cache *sc,
+ int w, int h, int d)
+{
+ struct timer_pool *timer = NULL;
+
+ gl_sc_generate(sc, RA_RENDERPASS_TYPE_COMPUTE, NULL, NULL, 0, 0);
+ if (!sc->current_shader)
+ goto error;
+
+ timer = sc->current_shader->timer;
+
+ struct ra_renderpass_run_params run = {
+ .pass = sc->current_shader->pass,
+ .values = sc->values,
+ .num_values = sc->num_values,
+ .push_constants = sc->current_shader->pushc,
+ .compute_groups = {w, h, d},
+ };
+
+ timer_pool_start(timer);
+ sc->ra->fns->renderpass_run(sc->ra, &run);
+ timer_pool_stop(timer);
+
+error:
+ gl_sc_reset(sc);
+ return timer_pool_measure(timer);
+}
diff --git a/video/out/gpu/shader_cache.h b/video/out/gpu/shader_cache.h
new file mode 100644
index 0000000..7c51c7a
--- /dev/null
+++ b/video/out/gpu/shader_cache.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "ra.h"
+
+// For mp_pass_perf
+#include "video/out/vo.h"
+
+struct mp_log;
+struct mpv_global;
+struct gl_shader_cache;
+
+struct gl_shader_cache *gl_sc_create(struct ra *ra, struct mpv_global *global,
+ struct mp_log *log);
+void gl_sc_destroy(struct gl_shader_cache *sc);
+bool gl_sc_error_state(struct gl_shader_cache *sc);
+void gl_sc_reset_error(struct gl_shader_cache *sc);
+void gl_sc_add(struct gl_shader_cache *sc, const char *text);
+void gl_sc_addf(struct gl_shader_cache *sc, const char *textf, ...)
+ PRINTF_ATTRIBUTE(2, 3);
+void gl_sc_hadd(struct gl_shader_cache *sc, const char *text);
+void gl_sc_haddf(struct gl_shader_cache *sc, const char *textf, ...)
+ PRINTF_ATTRIBUTE(2, 3);
+void gl_sc_hadd_bstr(struct gl_shader_cache *sc, struct bstr text);
+void gl_sc_paddf(struct gl_shader_cache *sc, const char *textf, ...)
+ PRINTF_ATTRIBUTE(2, 3);
+
+// A hint that the next data-type (i.e. non-binding) uniform is expected to
+// change frequently. This refers to the _f, _i, _vecN etc. uniform types.
+void gl_sc_uniform_dynamic(struct gl_shader_cache *sc);
+void gl_sc_uniform_texture(struct gl_shader_cache *sc, char *name,
+ struct ra_tex *tex);
+void gl_sc_uniform_image2D_wo(struct gl_shader_cache *sc, const char *name,
+ struct ra_tex *tex);
+void gl_sc_ssbo(struct gl_shader_cache *sc, char *name, struct ra_buf *buf,
+ char *format, ...) PRINTF_ATTRIBUTE(4, 5);
+void gl_sc_uniform_f(struct gl_shader_cache *sc, char *name, float f);
+void gl_sc_uniform_i(struct gl_shader_cache *sc, char *name, int f);
+void gl_sc_uniform_vec2(struct gl_shader_cache *sc, char *name, float f[2]);
+void gl_sc_uniform_vec3(struct gl_shader_cache *sc, char *name, float f[3]);
+void gl_sc_uniform_mat2(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v);
+void gl_sc_uniform_mat3(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v);
+
+// Return the correct bvecN() variant for using mix() in this GLSL version
+const char *gl_sc_bvec(struct gl_shader_cache *sc, int dims);
+
+void gl_sc_blend(struct gl_shader_cache *sc,
+ enum ra_blend blend_src_rgb,
+ enum ra_blend blend_dst_rgb,
+ enum ra_blend blend_src_alpha,
+ enum ra_blend blend_dst_alpha);
+void gl_sc_enable_extension(struct gl_shader_cache *sc, char *name);
+struct mp_pass_perf gl_sc_dispatch_draw(struct gl_shader_cache *sc,
+ struct ra_tex *target, bool discard,
+ const struct ra_renderpass_input *vao,
+ int vao_len, size_t vertex_stride,
+ void *ptr, size_t num);
+struct mp_pass_perf gl_sc_dispatch_compute(struct gl_shader_cache *sc,
+ int w, int h, int d);
+// The application can call this on errors, to reset the current shader. This
+// is normally done implicitly by gl_sc_dispatch_*
+void gl_sc_reset(struct gl_shader_cache *sc);
+void gl_sc_set_cache_dir(struct gl_shader_cache *sc, char *dir);
diff --git a/video/out/gpu/spirv.c b/video/out/gpu/spirv.c
new file mode 100644
index 0000000..67088bc
--- /dev/null
+++ b/video/out/gpu/spirv.c
@@ -0,0 +1,70 @@
+#include "common/msg.h"
+#include "options/m_config.h"
+
+#include "spirv.h"
+#include "config.h"
+
+extern const struct spirv_compiler_fns spirv_shaderc;
+
+// in probe-order
+enum {
+ SPIRV_AUTO = 0,
+ SPIRV_SHADERC, // generally preferred, but not packaged everywhere
+};
+
+static const struct spirv_compiler_fns *compilers[] = {
+#if HAVE_SHADERC
+ [SPIRV_SHADERC] = &spirv_shaderc,
+#endif
+};
+
+static const struct m_opt_choice_alternatives compiler_choices[] = {
+ {"auto", SPIRV_AUTO},
+#if HAVE_SHADERC
+ {"shaderc", SPIRV_SHADERC},
+#endif
+ {0}
+};
+
+struct spirv_opts {
+ int compiler;
+};
+
+#define OPT_BASE_STRUCT struct spirv_opts
+const struct m_sub_options spirv_conf = {
+ .opts = (const struct m_option[]) {
+ {"spirv-compiler", OPT_CHOICE_C(compiler, compiler_choices)},
+ {0}
+ },
+ .size = sizeof(struct spirv_opts),
+};
+
+bool spirv_compiler_init(struct ra_ctx *ctx)
+{
+ void *tmp = talloc_new(NULL);
+ struct spirv_opts *opts = mp_get_config_group(tmp, ctx->global, &spirv_conf);
+ int compiler = opts->compiler;
+ talloc_free(tmp);
+
+ for (int i = SPIRV_AUTO+1; i < MP_ARRAY_SIZE(compilers); i++) {
+ if (compiler != SPIRV_AUTO && i != compiler)
+ continue;
+ if (!compilers[i])
+ continue;
+
+ ctx->spirv = talloc_zero(ctx, struct spirv_compiler);
+ ctx->spirv->log = ctx->log,
+ ctx->spirv->fns = compilers[i];
+
+ const char *name = m_opt_choice_str(compiler_choices, i);
+ strncpy(ctx->spirv->name, name, sizeof(ctx->spirv->name) - 1);
+ MP_VERBOSE(ctx, "Initializing SPIR-V compiler '%s'\n", name);
+ if (ctx->spirv->fns->init(ctx))
+ return true;
+ talloc_free(ctx->spirv);
+ ctx->spirv = NULL;
+ }
+
+ MP_ERR(ctx, "Failed initializing SPIR-V compiler!\n");
+ return false;
+}
diff --git a/video/out/gpu/spirv.h b/video/out/gpu/spirv.h
new file mode 100644
index 0000000..e3dbd4f
--- /dev/null
+++ b/video/out/gpu/spirv.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "common/msg.h"
+#include "common/common.h"
+#include "context.h"
+
+enum glsl_shader {
+ GLSL_SHADER_VERTEX,
+ GLSL_SHADER_FRAGMENT,
+ GLSL_SHADER_COMPUTE,
+};
+
+#define SPIRV_NAME_MAX_LEN 32
+
+struct spirv_compiler {
+ char name[SPIRV_NAME_MAX_LEN];
+ const struct spirv_compiler_fns *fns;
+ struct mp_log *log;
+ void *priv;
+
+ const char *required_ext; // or NULL
+ int glsl_version; // GLSL version supported
+ int compiler_version; // for cache invalidation, may be left as 0
+ int ra_caps; // RA_CAP_* provided by this implementation, if any
+};
+
+struct spirv_compiler_fns {
+ // Compile GLSL to SPIR-V, under GL_KHR_vulkan_glsl semantics.
+ bool (*compile_glsl)(struct spirv_compiler *spirv, void *tactx,
+ enum glsl_shader type, const char *glsl,
+ struct bstr *out_spirv);
+
+ // Called by spirv_compiler_init / ra_ctx_destroy. These don't need to
+ // allocate/free ctx->spirv, that is done by the caller
+ bool (*init)(struct ra_ctx *ctx);
+ void (*uninit)(struct ra_ctx *ctx); // optional
+};
+
+// Initializes ctx->spirv to a valid SPIR-V compiler, or returns false on
+// failure. Cleanup will be handled by ra_ctx_destroy.
+bool spirv_compiler_init(struct ra_ctx *ctx);
diff --git a/video/out/gpu/spirv_shaderc.c b/video/out/gpu/spirv_shaderc.c
new file mode 100644
index 0000000..f285631
--- /dev/null
+++ b/video/out/gpu/spirv_shaderc.c
@@ -0,0 +1,125 @@
+#include "common/msg.h"
+
+#include "context.h"
+#include "spirv.h"
+
+#include <shaderc/shaderc.h>
+
+struct priv {
+ shaderc_compiler_t compiler;
+ shaderc_compile_options_t opts;
+};
+
+static void shaderc_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->spirv->priv;
+ if (!p)
+ return;
+
+ shaderc_compile_options_release(p->opts);
+ shaderc_compiler_release(p->compiler);
+}
+
+static bool shaderc_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->spirv->priv = talloc_zero(ctx->spirv, struct priv);
+
+ p->compiler = shaderc_compiler_initialize();
+ if (!p->compiler)
+ goto error;
+ p->opts = shaderc_compile_options_initialize();
+ if (!p->opts)
+ goto error;
+
+ shaderc_compile_options_set_optimization_level(p->opts,
+ shaderc_optimization_level_performance);
+ if (ctx->opts.debug)
+ shaderc_compile_options_set_generate_debug_info(p->opts);
+
+ int ver, rev;
+ shaderc_get_spv_version(&ver, &rev);
+ ctx->spirv->compiler_version = ver * 100 + rev; // forwards compatibility
+ ctx->spirv->glsl_version = 450; // impossible to query?
+ return true;
+
+error:
+ shaderc_uninit(ctx);
+ return false;
+}
+
+static shaderc_compilation_result_t compile(struct priv *p,
+ enum glsl_shader type,
+ const char *glsl, bool debug)
+{
+ static const shaderc_shader_kind kinds[] = {
+ [GLSL_SHADER_VERTEX] = shaderc_glsl_vertex_shader,
+ [GLSL_SHADER_FRAGMENT] = shaderc_glsl_fragment_shader,
+ [GLSL_SHADER_COMPUTE] = shaderc_glsl_compute_shader,
+ };
+
+ if (debug) {
+ return shaderc_compile_into_spv_assembly(p->compiler, glsl, strlen(glsl),
+ kinds[type], "input", "main", p->opts);
+ } else {
+ return shaderc_compile_into_spv(p->compiler, glsl, strlen(glsl),
+ kinds[type], "input", "main", p->opts);
+ }
+}
+
+static bool shaderc_compile(struct spirv_compiler *spirv, void *tactx,
+ enum glsl_shader type, const char *glsl,
+ struct bstr *out_spirv)
+{
+ struct priv *p = spirv->priv;
+
+ shaderc_compilation_result_t res = compile(p, type, glsl, false);
+ int errs = shaderc_result_get_num_errors(res),
+ warn = shaderc_result_get_num_warnings(res),
+ msgl = errs ? MSGL_ERR : warn ? MSGL_WARN : MSGL_V;
+
+ const char *msg = shaderc_result_get_error_message(res);
+ if (msg[0])
+ MP_MSG(spirv, msgl, "shaderc output:\n%s", msg);
+
+ int s = shaderc_result_get_compilation_status(res);
+ bool success = s == shaderc_compilation_status_success;
+
+ static const char *results[] = {
+ [shaderc_compilation_status_success] = "success",
+ [shaderc_compilation_status_invalid_stage] = "invalid stage",
+ [shaderc_compilation_status_compilation_error] = "error",
+ [shaderc_compilation_status_internal_error] = "internal error",
+ [shaderc_compilation_status_null_result_object] = "no result",
+ [shaderc_compilation_status_invalid_assembly] = "invalid assembly",
+ };
+
+ const char *status = s < MP_ARRAY_SIZE(results) ? results[s] : "unknown";
+ MP_MSG(spirv, msgl, "shaderc compile status '%s' (%d errors, %d warnings)\n",
+ status, errs, warn);
+
+ if (success) {
+ void *bytes = (void *) shaderc_result_get_bytes(res);
+ out_spirv->len = shaderc_result_get_length(res);
+ out_spirv->start = talloc_memdup(tactx, bytes, out_spirv->len);
+ }
+
+ // Also print SPIR-V disassembly for debugging purposes. Unfortunately
+ // there doesn't seem to be a way to get this except compiling the shader
+ // a second time..
+ if (mp_msg_test(spirv->log, MSGL_TRACE)) {
+ shaderc_compilation_result_t dis = compile(p, type, glsl, true);
+ MP_TRACE(spirv, "Generated SPIR-V:\n%.*s",
+ (int)shaderc_result_get_length(dis),
+ shaderc_result_get_bytes(dis));
+ shaderc_result_release(dis);
+ }
+
+ shaderc_result_release(res);
+ return success;
+}
+
+const struct spirv_compiler_fns spirv_shaderc = {
+ .compile_glsl = shaderc_compile,
+ .init = shaderc_init,
+ .uninit = shaderc_uninit,
+};
diff --git a/video/out/gpu/user_shaders.c b/video/out/gpu/user_shaders.c
new file mode 100644
index 0000000..708de87
--- /dev/null
+++ b/video/out/gpu/user_shaders.c
@@ -0,0 +1,463 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <math.h>
+
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "user_shaders.h"
+
+static bool parse_rpn_szexpr(struct bstr line, struct szexp out[MAX_SZEXP_SIZE])
+{
+ int pos = 0;
+
+ while (line.len > 0) {
+ struct bstr word = bstr_strip(bstr_splitchar(line, &line, ' '));
+ if (word.len == 0)
+ continue;
+
+ if (pos >= MAX_SZEXP_SIZE)
+ return false;
+
+ struct szexp *exp = &out[pos++];
+
+ if (bstr_eatend0(&word, ".w") || bstr_eatend0(&word, ".width")) {
+ exp->tag = SZEXP_VAR_W;
+ exp->val.varname = word;
+ continue;
+ }
+
+ if (bstr_eatend0(&word, ".h") || bstr_eatend0(&word, ".height")) {
+ exp->tag = SZEXP_VAR_H;
+ exp->val.varname = word;
+ continue;
+ }
+
+ switch (word.start[0]) {
+ case '+': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_ADD; continue;
+ case '-': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_SUB; continue;
+ case '*': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MUL; continue;
+ case '/': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_DIV; continue;
+ case '%': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MOD; continue;
+ case '!': exp->tag = SZEXP_OP1; exp->val.op = SZEXP_OP_NOT; continue;
+ case '>': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_GT; continue;
+ case '<': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_LT; continue;
+ case '=': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_EQ; continue;
+ }
+
+ if (mp_isdigit(word.start[0])) {
+ exp->tag = SZEXP_CONST;
+ if (bstr_sscanf(word, "%f", &exp->val.cval) != 1)
+ return false;
+ continue;
+ }
+
+ // Some sort of illegal expression
+ return false;
+ }
+
+ return true;
+}
+
+// Returns whether successful. 'result' is left untouched on failure
+bool eval_szexpr(struct mp_log *log, void *priv,
+ bool (*lookup)(void *priv, struct bstr var, float size[2]),
+ struct szexp expr[MAX_SZEXP_SIZE], float *result)
+{
+ float stack[MAX_SZEXP_SIZE] = {0};
+ int idx = 0; // points to next element to push
+
+ for (int i = 0; i < MAX_SZEXP_SIZE; i++) {
+ switch (expr[i].tag) {
+ case SZEXP_END:
+ goto done;
+
+ case SZEXP_CONST:
+ // Since our SZEXPs are bound by MAX_SZEXP_SIZE, it should be
+ // impossible to overflow the stack
+ assert(idx < MAX_SZEXP_SIZE);
+ stack[idx++] = expr[i].val.cval;
+ continue;
+
+ case SZEXP_OP1:
+ if (idx < 1) {
+ mp_warn(log, "Stack underflow in RPN expression!\n");
+ return false;
+ }
+
+ switch (expr[i].val.op) {
+ case SZEXP_OP_NOT: stack[idx-1] = !stack[idx-1]; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+ continue;
+
+ case SZEXP_OP2:
+ if (idx < 2) {
+ mp_warn(log, "Stack underflow in RPN expression!\n");
+ return false;
+ }
+
+ // Pop the operands in reverse order
+ float op2 = stack[--idx];
+ float op1 = stack[--idx];
+ float res = 0.0;
+ switch (expr[i].val.op) {
+ case SZEXP_OP_ADD: res = op1 + op2; break;
+ case SZEXP_OP_SUB: res = op1 - op2; break;
+ case SZEXP_OP_MUL: res = op1 * op2; break;
+ case SZEXP_OP_DIV: res = op1 / op2; break;
+ case SZEXP_OP_MOD: res = fmodf(op1, op2); break;
+ case SZEXP_OP_GT: res = op1 > op2; break;
+ case SZEXP_OP_LT: res = op1 < op2; break;
+ case SZEXP_OP_EQ: res = op1 == op2; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ if (!isfinite(res)) {
+ mp_warn(log, "Illegal operation in RPN expression!\n");
+ return false;
+ }
+
+ stack[idx++] = res;
+ continue;
+
+ case SZEXP_VAR_W:
+ case SZEXP_VAR_H: {
+ struct bstr name = expr[i].val.varname;
+ float size[2];
+
+ if (!lookup(priv, name, size)) {
+ mp_warn(log, "Variable %.*s not found in RPN expression!\n",
+ BSTR_P(name));
+ return false;
+ }
+
+ stack[idx++] = (expr[i].tag == SZEXP_VAR_W) ? size[0] : size[1];
+ continue;
+ }
+ }
+ }
+
+done:
+ // Return the single stack element
+ if (idx != 1) {
+ mp_warn(log, "Malformed stack after RPN expression!\n");
+ return false;
+ }
+
+ *result = stack[0];
+ return true;
+}
+
+static bool parse_hook(struct mp_log *log, struct bstr *body,
+ struct gl_user_shader_hook *out)
+{
+ *out = (struct gl_user_shader_hook){
+ .pass_desc = bstr0("(unknown)"),
+ .offset = identity_trans,
+ .align_offset = false,
+ .width = {{ SZEXP_VAR_W, { .varname = bstr0("HOOKED") }}},
+ .height = {{ SZEXP_VAR_H, { .varname = bstr0("HOOKED") }}},
+ .cond = {{ SZEXP_CONST, { .cval = 1.0 }}},
+ };
+
+ int hook_idx = 0;
+ int bind_idx = 0;
+
+ // Parse all headers
+ while (true) {
+ struct bstr rest;
+ struct bstr line = bstr_strip(bstr_getline(*body, &rest));
+
+ // Check for the presence of the magic line beginning
+ if (!bstr_eatstart0(&line, "//!"))
+ break;
+
+ *body = rest;
+
+ // Parse the supported commands
+ if (bstr_eatstart0(&line, "HOOK")) {
+ if (hook_idx == SHADER_MAX_HOOKS) {
+ mp_err(log, "Passes may only hook up to %d textures!\n",
+ SHADER_MAX_HOOKS);
+ return false;
+ }
+ out->hook_tex[hook_idx++] = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "BIND")) {
+ if (bind_idx == SHADER_MAX_BINDS) {
+ mp_err(log, "Passes may only bind up to %d textures!\n",
+ SHADER_MAX_BINDS);
+ return false;
+ }
+ out->bind_tex[bind_idx++] = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "SAVE")) {
+ out->save_tex = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "DESC")) {
+ out->pass_desc = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "OFFSET")) {
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "ALIGN")) {
+ out->align_offset = true;
+ } else {
+ float ox, oy;
+ if (bstr_sscanf(line, "%f %f", &ox, &oy) != 2) {
+ mp_err(log, "Error while parsing OFFSET!\n");
+ return false;
+ }
+ out->offset.t[0] = ox;
+ out->offset.t[1] = oy;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "WIDTH")) {
+ if (!parse_rpn_szexpr(line, out->width)) {
+ mp_err(log, "Error while parsing WIDTH!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "HEIGHT")) {
+ if (!parse_rpn_szexpr(line, out->height)) {
+ mp_err(log, "Error while parsing HEIGHT!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "WHEN")) {
+ if (!parse_rpn_szexpr(line, out->cond)) {
+ mp_err(log, "Error while parsing WHEN!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "COMPONENTS")) {
+ if (bstr_sscanf(line, "%d", &out->components) != 1) {
+ mp_err(log, "Error while parsing COMPONENTS!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "COMPUTE")) {
+ struct compute_info *ci = &out->compute;
+ int num = bstr_sscanf(line, "%d %d %d %d", &ci->block_w, &ci->block_h,
+ &ci->threads_w, &ci->threads_h);
+
+ if (num == 2 || num == 4) {
+ ci->active = true;
+ ci->directly_writes = true;
+ } else {
+ mp_err(log, "Error while parsing COMPUTE!\n");
+ return false;
+ }
+ continue;
+ }
+
+ // Unknown command type
+ mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+
+ // The rest of the file up until the next magic line beginning (if any)
+ // shall be the shader body
+ if (bstr_split_tok(*body, "//!", &out->pass_body, body)) {
+ // Make sure the magic line is part of the rest
+ body->start -= 3;
+ body->len += 3;
+ }
+
+ // Sanity checking
+ if (hook_idx == 0)
+ mp_warn(log, "Pass has no hooked textures (will be ignored)!\n");
+
+ return true;
+}
+
+static bool parse_tex(struct mp_log *log, struct ra *ra, struct bstr *body,
+ struct gl_user_shader_tex *out)
+{
+ *out = (struct gl_user_shader_tex){
+ .name = bstr0("USER_TEX"),
+ .params = {
+ .dimensions = 2,
+ .w = 1, .h = 1, .d = 1,
+ .render_src = true,
+ .src_linear = true,
+ },
+ };
+ struct ra_tex_params *p = &out->params;
+
+ while (true) {
+ struct bstr rest;
+ struct bstr line = bstr_strip(bstr_getline(*body, &rest));
+
+ if (!bstr_eatstart0(&line, "//!"))
+ break;
+
+ *body = rest;
+
+ if (bstr_eatstart0(&line, "TEXTURE")) {
+ out->name = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "SIZE")) {
+ p->dimensions = bstr_sscanf(line, "%d %d %d", &p->w, &p->h, &p->d);
+ if (p->dimensions < 1 || p->dimensions > 3 ||
+ p->w < 1 || p->h < 1 || p->d < 1)
+ {
+ mp_err(log, "Error while parsing SIZE!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "FORMAT ")) {
+ p->format = NULL;
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (bstr_equals0(line, fmt->name)) {
+ p->format = fmt;
+ break;
+ }
+ }
+ // (pixel_size==0 is for opaque formats)
+ if (!p->format || !p->format->pixel_size) {
+ mp_err(log, "Unrecognized/unavailable FORMAT name: '%.*s'!\n",
+ BSTR_P(line));
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "FILTER")) {
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "LINEAR")) {
+ p->src_linear = true;
+ } else if (bstr_equals0(line, "NEAREST")) {
+ p->src_linear = false;
+ } else {
+ mp_err(log, "Unrecognized FILTER: '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "BORDER")) {
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "CLAMP")) {
+ p->src_repeat = false;
+ } else if (bstr_equals0(line, "REPEAT")) {
+ p->src_repeat = true;
+ } else {
+ mp_err(log, "Unrecognized BORDER: '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+ continue;
+ }
+
+ mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+
+ if (!p->format) {
+ mp_err(log, "No FORMAT specified.\n");
+ return false;
+ }
+
+ if (p->src_linear && !p->format->linear_filter) {
+ mp_err(log, "The specified texture format cannot be filtered!\n");
+ return false;
+ }
+
+ // Decode the rest of the section (up to the next //! marker) as raw hex
+ // data for the texture
+ struct bstr hexdata;
+ if (bstr_split_tok(*body, "//!", &hexdata, body)) {
+ // Make sure the magic line is part of the rest
+ body->start -= 3;
+ body->len += 3;
+ }
+
+ struct bstr tex;
+ if (!bstr_decode_hex(NULL, bstr_strip(hexdata), &tex)) {
+ mp_err(log, "Error while parsing TEXTURE body: must be a valid "
+ "hexadecimal sequence, on a single line!\n");
+ return false;
+ }
+
+ int expected_len = p->w * p->h * p->d * p->format->pixel_size;
+ if (tex.len != expected_len) {
+ mp_err(log, "Shader TEXTURE size mismatch: got %zd bytes, expected %d!\n",
+ tex.len, expected_len);
+ talloc_free(tex.start);
+ return false;
+ }
+
+ p->initial_data = tex.start;
+ return true;
+}
+
+void parse_user_shader(struct mp_log *log, struct ra *ra, struct bstr shader,
+ void *priv,
+ bool (*dohook)(void *p, struct gl_user_shader_hook hook),
+ bool (*dotex)(void *p, struct gl_user_shader_tex tex))
+{
+ if (!dohook || !dotex || !shader.len)
+ return;
+
+ // Skip all garbage (e.g. comments) before the first header
+ int pos = bstr_find(shader, bstr0("//!"));
+ if (pos < 0) {
+ mp_warn(log, "Shader appears to contain no headers!\n");
+ return;
+ }
+ shader = bstr_cut(shader, pos);
+
+ // Loop over the file
+ while (shader.len > 0)
+ {
+ // Peek at the first header to dispatch the right type
+ if (bstr_startswith0(shader, "//!TEXTURE")) {
+ struct gl_user_shader_tex t;
+ if (!parse_tex(log, ra, &shader, &t) || !dotex(priv, t))
+ return;
+ continue;
+ }
+
+ struct gl_user_shader_hook h;
+ if (!parse_hook(log, &shader, &h) || !dohook(priv, h))
+ return;
+ }
+}
diff --git a/video/out/gpu/user_shaders.h b/video/out/gpu/user_shaders.h
new file mode 100644
index 0000000..4bb7c22
--- /dev/null
+++ b/video/out/gpu/user_shaders.h
@@ -0,0 +1,99 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_GL_USER_SHADERS_H
+#define MP_GL_USER_SHADERS_H
+
+#include "utils.h"
+#include "ra.h"
+
+#define SHADER_MAX_HOOKS 16
+#define SHADER_MAX_BINDS 16
+#define MAX_SZEXP_SIZE 32
+
+enum szexp_op {
+ SZEXP_OP_ADD,
+ SZEXP_OP_SUB,
+ SZEXP_OP_MUL,
+ SZEXP_OP_DIV,
+ SZEXP_OP_MOD,
+ SZEXP_OP_NOT,
+ SZEXP_OP_GT,
+ SZEXP_OP_LT,
+ SZEXP_OP_EQ,
+};
+
+enum szexp_tag {
+ SZEXP_END = 0, // End of an RPN expression
+ SZEXP_CONST, // Push a constant value onto the stack
+ SZEXP_VAR_W, // Get the width/height of a named texture (variable)
+ SZEXP_VAR_H,
+ SZEXP_OP2, // Pop two elements and push the result of a dyadic operation
+ SZEXP_OP1, // Pop one element and push the result of a monadic operation
+};
+
+struct szexp {
+ enum szexp_tag tag;
+ union {
+ float cval;
+ struct bstr varname;
+ enum szexp_op op;
+ } val;
+};
+
+struct compute_info {
+ bool active;
+ int block_w, block_h; // Block size (each block corresponds to one WG)
+ int threads_w, threads_h; // How many threads form a working group
+ bool directly_writes; // If true, shader is assumed to imageStore(out_image)
+};
+
+struct gl_user_shader_hook {
+ struct bstr pass_desc;
+ struct bstr hook_tex[SHADER_MAX_HOOKS];
+ struct bstr bind_tex[SHADER_MAX_BINDS];
+ struct bstr save_tex;
+ struct bstr pass_body;
+ struct gl_transform offset;
+ bool align_offset;
+ struct szexp width[MAX_SZEXP_SIZE];
+ struct szexp height[MAX_SZEXP_SIZE];
+ struct szexp cond[MAX_SZEXP_SIZE];
+ int components;
+ struct compute_info compute;
+};
+
+struct gl_user_shader_tex {
+ struct bstr name;
+ struct ra_tex_params params;
+ // for video.c
+ struct ra_tex *tex;
+};
+
+// Parse the next shader block from `body`. The callbacks are invoked on every
+// valid shader block parsed.
+void parse_user_shader(struct mp_log *log, struct ra *ra, struct bstr shader,
+ void *priv,
+ bool (*dohook)(void *p, struct gl_user_shader_hook hook),
+ bool (*dotex)(void *p, struct gl_user_shader_tex tex));
+
+// Evaluate a szexp, given a lookup function for named textures
+bool eval_szexpr(struct mp_log *log, void *priv,
+ bool (*lookup)(void *priv, struct bstr var, float size[2]),
+ struct szexp expr[MAX_SZEXP_SIZE], float *result);
+
+#endif
diff --git a/video/out/gpu/utils.c b/video/out/gpu/utils.c
new file mode 100644
index 0000000..8a1aacf
--- /dev/null
+++ b/video/out/gpu/utils.c
@@ -0,0 +1,349 @@
+#include "common/msg.h"
+#include "video/out/vo.h"
+#include "utils.h"
+
+// Standard parallel 2D projection, except y1 < y0 means that the coordinate
+// system is flipped, not the projection.
+void gl_transform_ortho(struct gl_transform *t, float x0, float x1,
+ float y0, float y1)
+{
+ if (y1 < y0) {
+ float tmp = y0;
+ y0 = tmp - y1;
+ y1 = tmp;
+ }
+
+ t->m[0][0] = 2.0f / (x1 - x0);
+ t->m[0][1] = 0.0f;
+ t->m[1][0] = 0.0f;
+ t->m[1][1] = 2.0f / (y1 - y0);
+ t->t[0] = -(x1 + x0) / (x1 - x0);
+ t->t[1] = -(y1 + y0) / (y1 - y0);
+}
+
+// Apply the effects of one transformation to another, transforming it in the
+// process. In other words: post-composes t onto x
+void gl_transform_trans(struct gl_transform t, struct gl_transform *x)
+{
+ struct gl_transform xt = *x;
+ x->m[0][0] = t.m[0][0] * xt.m[0][0] + t.m[0][1] * xt.m[1][0];
+ x->m[1][0] = t.m[1][0] * xt.m[0][0] + t.m[1][1] * xt.m[1][0];
+ x->m[0][1] = t.m[0][0] * xt.m[0][1] + t.m[0][1] * xt.m[1][1];
+ x->m[1][1] = t.m[1][0] * xt.m[0][1] + t.m[1][1] * xt.m[1][1];
+ gl_transform_vec(t, &x->t[0], &x->t[1]);
+}
+
+void gl_transform_ortho_fbo(struct gl_transform *t, struct ra_fbo fbo)
+{
+ int y_dir = fbo.flip ? -1 : 1;
+ gl_transform_ortho(t, 0, fbo.tex->params.w, 0, fbo.tex->params.h * y_dir);
+}
+
+float gl_video_scale_ambient_lux(float lmin, float lmax,
+ float rmin, float rmax, float lux)
+{
+ assert(lmax > lmin);
+
+ float num = (rmax - rmin) * (log10(lux) - log10(lmin));
+ float den = log10(lmax) - log10(lmin);
+ float result = num / den + rmin;
+
+ // clamp the result
+ float max = MPMAX(rmax, rmin);
+ float min = MPMIN(rmax, rmin);
+ return MPMAX(MPMIN(result, max), min);
+}
+
+void ra_buf_pool_uninit(struct ra *ra, struct ra_buf_pool *pool)
+{
+ for (int i = 0; i < pool->num_buffers; i++)
+ ra_buf_free(ra, &pool->buffers[i]);
+
+ talloc_free(pool->buffers);
+ *pool = (struct ra_buf_pool){0};
+}
+
+static bool ra_buf_params_compatible(const struct ra_buf_params *new,
+ const struct ra_buf_params *old)
+{
+ return new->type == old->type &&
+ new->size <= old->size &&
+ new->host_mapped == old->host_mapped &&
+ new->host_mutable == old->host_mutable;
+}
+
+static bool ra_buf_pool_grow(struct ra *ra, struct ra_buf_pool *pool)
+{
+ struct ra_buf *buf = ra_buf_create(ra, &pool->current_params);
+ if (!buf)
+ return false;
+
+ MP_TARRAY_INSERT_AT(NULL, pool->buffers, pool->num_buffers, pool->index, buf);
+ MP_VERBOSE(ra, "Resized buffer pool of type %u to size %d\n",
+ pool->current_params.type, pool->num_buffers);
+ return true;
+}
+
+struct ra_buf *ra_buf_pool_get(struct ra *ra, struct ra_buf_pool *pool,
+ const struct ra_buf_params *params)
+{
+ assert(!params->initial_data);
+
+ if (!ra_buf_params_compatible(params, &pool->current_params)) {
+ ra_buf_pool_uninit(ra, pool);
+ pool->current_params = *params;
+ }
+
+ // Make sure we have at least one buffer available
+ if (!pool->buffers && !ra_buf_pool_grow(ra, pool))
+ return NULL;
+
+ // Make sure the next buffer is available for use
+ if (!ra->fns->buf_poll(ra, pool->buffers[pool->index]) &&
+ !ra_buf_pool_grow(ra, pool))
+ {
+ return NULL;
+ }
+
+ struct ra_buf *buf = pool->buffers[pool->index++];
+ pool->index %= pool->num_buffers;
+
+ return buf;
+}
+
+bool ra_tex_upload_pbo(struct ra *ra, struct ra_buf_pool *pbo,
+ const struct ra_tex_upload_params *params)
+{
+ if (params->buf)
+ return ra->fns->tex_upload(ra, params);
+
+ struct ra_tex *tex = params->tex;
+ size_t row_size = tex->params.dimensions == 2 ? params->stride :
+ tex->params.w * tex->params.format->pixel_size;
+
+ int height = tex->params.h;
+ if (tex->params.dimensions == 2 && params->rc)
+ height = mp_rect_h(*params->rc);
+
+ struct ra_buf_params bufparams = {
+ .type = RA_BUF_TYPE_TEX_UPLOAD,
+ .size = row_size * height * tex->params.d,
+ .host_mutable = true,
+ };
+
+ struct ra_buf *buf = ra_buf_pool_get(ra, pbo, &bufparams);
+ if (!buf)
+ return false;
+
+ ra->fns->buf_update(ra, buf, 0, params->src, bufparams.size);
+
+ struct ra_tex_upload_params newparams = *params;
+ newparams.buf = buf;
+ newparams.src = NULL;
+
+ return ra->fns->tex_upload(ra, &newparams);
+}
+
+struct ra_layout std140_layout(struct ra_renderpass_input *inp)
+{
+ size_t el_size = ra_vartype_size(inp->type);
+
+ // std140 packing rules:
+ // 1. The alignment of generic values is their size in bytes
+ // 2. The alignment of vectors is the vector length * the base count, with
+ // the exception of vec3 which is always aligned like vec4
+ // 3. The alignment of arrays is that of the element size rounded up to
+ // the nearest multiple of vec4
+ // 4. Matrices are treated like arrays of vectors
+ // 5. Arrays/matrices are laid out with a stride equal to the alignment
+ size_t stride = el_size * inp->dim_v;
+ size_t align = stride;
+ if (inp->dim_v == 3)
+ align += el_size;
+ if (inp->dim_m > 1)
+ stride = align = MP_ALIGN_UP(stride, sizeof(float[4]));
+
+ return (struct ra_layout) {
+ .align = align,
+ .stride = stride,
+ .size = stride * inp->dim_m,
+ };
+}
+
+struct ra_layout std430_layout(struct ra_renderpass_input *inp)
+{
+ size_t el_size = ra_vartype_size(inp->type);
+
+ // std430 packing rules: like std140, except arrays/matrices are always
+ // "tightly" packed, even arrays/matrices of vec3s
+ size_t stride = el_size * inp->dim_v;
+ size_t align = stride;
+ if (inp->dim_v == 3 && inp->dim_m == 1)
+ align += el_size;
+
+ return (struct ra_layout) {
+ .align = align,
+ .stride = stride,
+ .size = stride * inp->dim_m,
+ };
+}
+
+// Resize a texture to a new desired size and format if necessary
+bool ra_tex_resize(struct ra *ra, struct mp_log *log, struct ra_tex **tex,
+ int w, int h, const struct ra_format *fmt)
+{
+ if (*tex) {
+ struct ra_tex_params cur_params = (*tex)->params;
+ if (cur_params.w == w && cur_params.h == h && cur_params.format == fmt)
+ return true;
+ }
+
+ mp_dbg(log, "Resizing texture: %dx%d\n", w, h);
+
+ if (!fmt || !fmt->renderable || !fmt->linear_filter) {
+ mp_err(log, "Format %s not supported.\n", fmt ? fmt->name : "(unset)");
+ return false;
+ }
+
+ ra_tex_free(ra, tex);
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = w,
+ .h = h,
+ .d = 1,
+ .format = fmt,
+ .src_linear = true,
+ .render_src = true,
+ .render_dst = true,
+ .storage_dst = fmt->storable,
+ .blit_src = true,
+ };
+
+ *tex = ra_tex_create(ra, &params);
+ if (!*tex)
+ mp_err(log, "Error: texture could not be created.\n");
+
+ return *tex;
+}
+
+struct timer_pool {
+ struct ra *ra;
+ ra_timer *timer;
+ bool running; // detect invalid usage
+
+ uint64_t samples[VO_PERF_SAMPLE_COUNT];
+ int sample_idx;
+ int sample_count;
+
+ uint64_t sum;
+ uint64_t peak;
+};
+
+struct timer_pool *timer_pool_create(struct ra *ra)
+{
+ if (!ra->fns->timer_create)
+ return NULL;
+
+ ra_timer *timer = ra->fns->timer_create(ra);
+ if (!timer)
+ return NULL;
+
+ struct timer_pool *pool = talloc(NULL, struct timer_pool);
+ if (!pool) {
+ ra->fns->timer_destroy(ra, timer);
+ return NULL;
+ }
+
+ *pool = (struct timer_pool){ .ra = ra, .timer = timer };
+ return pool;
+}
+
+void timer_pool_destroy(struct timer_pool *pool)
+{
+ if (!pool)
+ return;
+
+ pool->ra->fns->timer_destroy(pool->ra, pool->timer);
+ talloc_free(pool);
+}
+
+void timer_pool_start(struct timer_pool *pool)
+{
+ if (!pool)
+ return;
+
+ assert(!pool->running);
+ pool->ra->fns->timer_start(pool->ra, pool->timer);
+ pool->running = true;
+}
+
+void timer_pool_stop(struct timer_pool *pool)
+{
+ if (!pool)
+ return;
+
+ assert(pool->running);
+ uint64_t res = pool->ra->fns->timer_stop(pool->ra, pool->timer);
+ pool->running = false;
+
+ if (res) {
+ // Input res into the buffer and grab the previous value
+ uint64_t old = pool->samples[pool->sample_idx];
+ pool->sample_count = MPMIN(pool->sample_count + 1, VO_PERF_SAMPLE_COUNT);
+ pool->samples[pool->sample_idx++] = res;
+ pool->sample_idx %= VO_PERF_SAMPLE_COUNT;
+ pool->sum = pool->sum + res - old;
+
+ // Update peak if necessary
+ if (res >= pool->peak) {
+ pool->peak = res;
+ } else if (pool->peak == old) {
+ // It's possible that the last peak was the value we just removed,
+ // if so we need to scan for the new peak
+ uint64_t peak = res;
+ for (int i = 0; i < VO_PERF_SAMPLE_COUNT; i++)
+ peak = MPMAX(peak, pool->samples[i]);
+ pool->peak = peak;
+ }
+ }
+}
+
+struct mp_pass_perf timer_pool_measure(struct timer_pool *pool)
+{
+ if (!pool)
+ return (struct mp_pass_perf){0};
+
+ struct mp_pass_perf res = {
+ .peak = pool->peak,
+ .count = pool->sample_count,
+ };
+
+ int idx = pool->sample_idx - pool->sample_count + VO_PERF_SAMPLE_COUNT;
+ for (int i = 0; i < res.count; i++) {
+ idx %= VO_PERF_SAMPLE_COUNT;
+ res.samples[i] = pool->samples[idx++];
+ }
+
+ if (res.count > 0) {
+ res.last = res.samples[res.count - 1];
+ res.avg = pool->sum / res.count;
+ }
+
+ return res;
+}
+
+void mp_log_source(struct mp_log *log, int lev, const char *src)
+{
+ int line = 1;
+ if (!src)
+ return;
+ while (*src) {
+ const char *end = strchr(src, '\n');
+ const char *next = end + 1;
+ if (!end)
+ next = end = src + strlen(src);
+ mp_msg(log, lev, "[%3d] %.*s\n", line, (int)(end - src), src);
+ line++;
+ src = next;
+ }
+}
diff --git a/video/out/gpu/utils.h b/video/out/gpu/utils.h
new file mode 100644
index 0000000..215873e
--- /dev/null
+++ b/video/out/gpu/utils.h
@@ -0,0 +1,108 @@
+#pragma once
+
+#include <stdbool.h>
+#include <math.h>
+
+#include "ra.h"
+#include "context.h"
+
+// A 3x2 matrix, with the translation part separate.
+struct gl_transform {
+ // row-major, e.g. in mathematical notation:
+ // | m[0][0] m[0][1] |
+ // | m[1][0] m[1][1] |
+ float m[2][2];
+ float t[2];
+};
+
+static const struct gl_transform identity_trans = {
+ .m = {{1.0, 0.0}, {0.0, 1.0}},
+ .t = {0.0, 0.0},
+};
+
+void gl_transform_ortho(struct gl_transform *t, float x0, float x1,
+ float y0, float y1);
+
+// This treats m as an affine transformation, in other words m[2][n] gets
+// added to the output.
+static inline void gl_transform_vec(struct gl_transform t, float *x, float *y)
+{
+ float vx = *x, vy = *y;
+ *x = vx * t.m[0][0] + vy * t.m[0][1] + t.t[0];
+ *y = vx * t.m[1][0] + vy * t.m[1][1] + t.t[1];
+}
+
+struct mp_rect_f {
+ float x0, y0, x1, y1;
+};
+
+// Semantic equality (fuzzy comparison)
+static inline bool mp_rect_f_seq(struct mp_rect_f a, struct mp_rect_f b)
+{
+ return fabs(a.x0 - b.x0) < 1e-6 && fabs(a.x1 - b.x1) < 1e-6 &&
+ fabs(a.y0 - b.y0) < 1e-6 && fabs(a.y1 - b.y1) < 1e-6;
+}
+
+static inline void gl_transform_rect(struct gl_transform t, struct mp_rect_f *r)
+{
+ gl_transform_vec(t, &r->x0, &r->y0);
+ gl_transform_vec(t, &r->x1, &r->y1);
+}
+
+static inline bool gl_transform_eq(struct gl_transform a, struct gl_transform b)
+{
+ for (int x = 0; x < 2; x++) {
+ for (int y = 0; y < 2; y++) {
+ if (a.m[x][y] != b.m[x][y])
+ return false;
+ }
+ }
+
+ return a.t[0] == b.t[0] && a.t[1] == b.t[1];
+}
+
+void gl_transform_trans(struct gl_transform t, struct gl_transform *x);
+
+void gl_transform_ortho_fbo(struct gl_transform *t, struct ra_fbo fbo);
+
+float gl_video_scale_ambient_lux(float lmin, float lmax,
+ float rmin, float rmax, float lux);
+
+// A pool of buffers, which can grow as needed
+struct ra_buf_pool {
+ struct ra_buf_params current_params;
+ struct ra_buf **buffers;
+ int num_buffers;
+ int index;
+};
+
+void ra_buf_pool_uninit(struct ra *ra, struct ra_buf_pool *pool);
+
+// Note: params->initial_data is *not* supported
+struct ra_buf *ra_buf_pool_get(struct ra *ra, struct ra_buf_pool *pool,
+ const struct ra_buf_params *params);
+
+// Helper that wraps ra_tex_upload using texture upload buffers to ensure that
+// params->buf is always set. This is intended for RA-internal usage.
+bool ra_tex_upload_pbo(struct ra *ra, struct ra_buf_pool *pbo,
+ const struct ra_tex_upload_params *params);
+
+// Layout rules for GLSL's packing modes
+struct ra_layout std140_layout(struct ra_renderpass_input *inp);
+struct ra_layout std430_layout(struct ra_renderpass_input *inp);
+
+bool ra_tex_resize(struct ra *ra, struct mp_log *log, struct ra_tex **tex,
+ int w, int h, const struct ra_format *fmt);
+
+// A wrapper around ra_timer that does result pooling, averaging etc.
+struct timer_pool;
+
+struct timer_pool *timer_pool_create(struct ra *ra);
+void timer_pool_destroy(struct timer_pool *pool);
+void timer_pool_start(struct timer_pool *pool);
+void timer_pool_stop(struct timer_pool *pool);
+struct mp_pass_perf timer_pool_measure(struct timer_pool *pool);
+
+// print a multi line string with line numbers (e.g. for shader sources)
+// log, lev: module and log level, as in mp_msg()
+void mp_log_source(struct mp_log *log, int lev, const char *src);
diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c
new file mode 100644
index 0000000..852ee78
--- /dev/null
+++ b/video/out/gpu/video.c
@@ -0,0 +1,4364 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <libavutil/common.h>
+#include <libavutil/lfg.h>
+
+#include "video.h"
+
+#include "misc/bstr.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "common/global.h"
+#include "options/options.h"
+#include "utils.h"
+#include "hwdec.h"
+#include "osd.h"
+#include "ra.h"
+#include "stream/stream.h"
+#include "video_shaders.h"
+#include "user_shaders.h"
+#include "error_diffusion.h"
+#include "video/out/filter_kernels.h"
+#include "video/out/aspect.h"
+#include "video/out/dither.h"
+#include "video/out/vo.h"
+
+// scale/cscale arguments that map directly to shader filter routines.
+// Note that the convolution filters are not included in this list.
+static const char *const fixed_scale_filters[] = {
+ "bilinear",
+ "bicubic_fast",
+ "oversample",
+ NULL
+};
+static const char *const fixed_tscale_filters[] = {
+ "oversample",
+ "linear",
+ NULL
+};
+
+// must be sorted, and terminated with 0
+int filter_sizes[] =
+ {2, 4, 6, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 0};
+int tscale_sizes[] = {2, 4, 6, 8, 0};
+
+struct vertex_pt {
+ float x, y;
+};
+
+struct texplane {
+ struct ra_tex *tex;
+ int w, h;
+ bool flipped;
+};
+
+struct video_image {
+ struct texplane planes[4];
+ struct mp_image *mpi; // original input image
+ uint64_t id; // unique ID identifying mpi contents
+ bool hwdec_mapped;
+};
+
+enum plane_type {
+ PLANE_NONE = 0,
+ PLANE_RGB,
+ PLANE_LUMA,
+ PLANE_CHROMA,
+ PLANE_ALPHA,
+ PLANE_XYZ,
+};
+
+static const char *plane_names[] = {
+ [PLANE_NONE] = "unknown",
+ [PLANE_RGB] = "rgb",
+ [PLANE_LUMA] = "luma",
+ [PLANE_CHROMA] = "chroma",
+ [PLANE_ALPHA] = "alpha",
+ [PLANE_XYZ] = "xyz",
+};
+
+// A self-contained description of a source image which can be bound to a
+// texture unit and sampled from. Contains metadata about how it's to be used
+struct image {
+ enum plane_type type; // must be set to something non-zero
+ int components; // number of relevant coordinates
+ float multiplier; // multiplier to be used when sampling
+ struct ra_tex *tex;
+ int w, h; // logical size (after transformation)
+ struct gl_transform transform; // rendering transformation
+ int padding; // number of leading padding components (e.g. 2 = rg is padding)
+};
+
+// A named image, for user scripting purposes
+struct saved_img {
+ const char *name;
+ struct image img;
+};
+
+// A texture hook. This is some operation that transforms a named texture as
+// soon as it's generated
+struct tex_hook {
+ const char *save_tex;
+ const char *hook_tex[SHADER_MAX_HOOKS];
+ const char *bind_tex[SHADER_MAX_BINDS];
+ int components; // how many components are relevant (0 = same as input)
+ bool align_offset; // whether to align hooked tex with reference.
+ void *priv; // this gets talloc_freed when the tex_hook is removed
+ void (*hook)(struct gl_video *p, struct image img, // generates GLSL
+ struct gl_transform *trans, void *priv);
+ bool (*cond)(struct gl_video *p, struct image img, void *priv);
+};
+
+struct surface {
+ struct ra_tex *tex;
+ uint64_t id;
+ double pts;
+};
+
+#define SURFACES_MAX 10
+
+struct cached_file {
+ char *path;
+ struct bstr body;
+};
+
+struct pass_info {
+ struct bstr desc;
+ struct mp_pass_perf perf;
+};
+
+struct dr_buffer {
+ struct ra_buf *buf;
+ // The mpi reference will keep the data from being recycled (or from other
+ // references gaining write access) while the GPU is accessing the buffer.
+ struct mp_image *mpi;
+};
+
+struct gl_video {
+ struct ra *ra;
+
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct gl_video_opts opts;
+ struct m_config_cache *opts_cache;
+ struct gl_lcms *cms;
+
+ int fb_depth; // actual bits available in GL main framebuffer
+ struct m_color clear_color;
+ bool force_clear_color;
+
+ struct gl_shader_cache *sc;
+
+ struct osd_state *osd_state;
+ struct mpgl_osd *osd;
+ double osd_pts;
+
+ struct ra_tex *lut_3d_texture;
+ bool use_lut_3d;
+ int lut_3d_size[3];
+
+ struct ra_tex *dither_texture;
+
+ struct mp_image_params real_image_params; // configured format
+ struct mp_image_params image_params; // texture format (mind hwdec case)
+ struct ra_imgfmt_desc ra_format; // texture format
+ int plane_count;
+
+ bool is_gray;
+ bool has_alpha;
+ char color_swizzle[5];
+ bool use_integer_conversion;
+
+ struct video_image image;
+
+ struct dr_buffer *dr_buffers;
+ int num_dr_buffers;
+
+ bool using_dr_path;
+
+ bool dumb_mode;
+ bool forced_dumb_mode;
+
+ // Cached vertex array, to avoid re-allocation per frame. For simplicity,
+ // our vertex format is simply a list of `vertex_pt`s, since this greatly
+ // simplifies offset calculation at the cost of (unneeded) flexibility.
+ struct vertex_pt *tmp_vertex;
+ struct ra_renderpass_input *vao;
+ int vao_len;
+
+ const struct ra_format *fbo_format;
+ struct ra_tex *merge_tex[4];
+ struct ra_tex *scale_tex[4];
+ struct ra_tex *integer_tex[4];
+ struct ra_tex *indirect_tex;
+ struct ra_tex *blend_subs_tex;
+ struct ra_tex *error_diffusion_tex[2];
+ struct ra_tex *screen_tex;
+ struct ra_tex *output_tex;
+ struct ra_tex **hook_textures;
+ int num_hook_textures;
+ int idx_hook_textures;
+
+ struct ra_buf *hdr_peak_ssbo;
+ struct surface surfaces[SURFACES_MAX];
+
+ // user pass descriptions and textures
+ struct tex_hook *tex_hooks;
+ int num_tex_hooks;
+ struct gl_user_shader_tex *user_textures;
+ int num_user_textures;
+
+ int surface_idx;
+ int surface_now;
+ int frames_drawn;
+ bool is_interpolated;
+ bool output_tex_valid;
+
+ // state for configured scalers
+ struct scaler scaler[SCALER_COUNT];
+
+ struct mp_csp_equalizer_state *video_eq;
+
+ struct mp_rect src_rect; // displayed part of the source video
+ struct mp_rect dst_rect; // video rectangle on output window
+ struct mp_osd_res osd_rect; // OSD size/margins
+
+ // temporary during rendering
+ struct compute_info pass_compute; // compute shader metadata for this pass
+ struct image *pass_imgs; // bound images for this pass
+ int num_pass_imgs;
+ struct saved_img *saved_imgs; // saved (named) images for this frame
+ int num_saved_imgs;
+
+ // effective current texture metadata - this will essentially affect the
+ // next render pass target, as well as implicitly tracking what needs to
+ // be done with the image
+ int texture_w, texture_h;
+ struct gl_transform texture_offset; // texture transform without rotation
+ int components;
+ bool use_linear;
+ float user_gamma;
+
+ // pass info / metrics
+ struct pass_info pass_fresh[VO_PASS_PERF_MAX];
+ struct pass_info pass_redraw[VO_PASS_PERF_MAX];
+ struct pass_info *pass;
+ int pass_idx;
+ struct timer_pool *upload_timer;
+ struct timer_pool *blit_timer;
+ struct timer_pool *osd_timer;
+
+ int frames_uploaded;
+ int frames_rendered;
+ AVLFG lfg;
+
+ // Cached because computing it can take relatively long
+ int last_dither_matrix_size;
+ float *last_dither_matrix;
+
+ struct cached_file *files;
+ int num_files;
+
+ struct ra_hwdec_ctx hwdec_ctx;
+ struct ra_hwdec_mapper *hwdec_mapper;
+ struct ra_hwdec *hwdec_overlay;
+ bool hwdec_active;
+
+ bool dsi_warned;
+ bool broken_frame; // temporary error state
+
+ bool colorspace_override_warned;
+ bool correct_downscaling_warned;
+};
+
+static const struct gl_video_opts gl_video_opts_def = {
+ .dither_algo = DITHER_FRUIT,
+ .dither_size = 6,
+ .temporal_dither_period = 1,
+ .error_diffusion = "sierra-lite",
+ .fbo_format = "auto",
+ .sigmoid_center = 0.75,
+ .sigmoid_slope = 6.5,
+ .scaler = {
+ {{"lanczos", .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // scale
+ {{"hermite", .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // dscale
+ {{NULL, .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // cscale
+ {{"oversample", .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // tscale
+ },
+ .scaler_resizes_only = true,
+ .correct_downscaling = true,
+ .linear_downscaling = true,
+ .sigmoid_upscaling = true,
+ .interpolation_threshold = 0.01,
+ .alpha_mode = ALPHA_BLEND_TILES,
+ .background = {0, 0, 0, 255},
+ .gamma = 1.0f,
+ .tone_map = {
+ .curve = TONE_MAPPING_AUTO,
+ .curve_param = NAN,
+ .max_boost = 1.0,
+ .decay_rate = 20.0,
+ .scene_threshold_low = 1.0,
+ .scene_threshold_high = 3.0,
+ .contrast_smoothness = 3.5,
+ },
+ .early_flush = -1,
+ .shader_cache = true,
+ .hwdec_interop = "auto",
+};
+
+static int validate_scaler_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+static int validate_window_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+static int validate_error_diffusion_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+#define OPT_BASE_STRUCT struct gl_video_opts
+
+// Use for options which use NAN for defaults.
+#define OPT_FLOATDEF(field) \
+ OPT_FLOAT(field), \
+ .flags = M_OPT_DEFAULT_NAN
+
+#define SCALER_OPTS(n, i) \
+ {n, OPT_STRING_VALIDATE(scaler[i].kernel.name, validate_scaler_opt)}, \
+ {n"-param1", OPT_FLOATDEF(scaler[i].kernel.params[0])}, \
+ {n"-param2", OPT_FLOATDEF(scaler[i].kernel.params[1])}, \
+ {n"-blur", OPT_FLOAT(scaler[i].kernel.blur)}, \
+ {n"-cutoff", OPT_REMOVED("Hard-coded as 0.001")}, \
+ {n"-taper", OPT_FLOAT(scaler[i].kernel.taper), M_RANGE(0.0, 1.0)}, \
+ {n"-wparam", OPT_FLOATDEF(scaler[i].window.params[0])}, \
+ {n"-wblur", OPT_REMOVED("Just adjust filter radius directly")}, \
+ {n"-wtaper", OPT_FLOAT(scaler[i].window.taper), M_RANGE(0.0, 1.0)}, \
+ {n"-clamp", OPT_FLOAT(scaler[i].clamp), M_RANGE(0.0, 1.0)}, \
+ {n"-radius", OPT_FLOAT(scaler[i].radius), M_RANGE(0.5, 16.0)}, \
+ {n"-antiring", OPT_FLOAT(scaler[i].antiring), M_RANGE(0.0, 1.0)}, \
+ {n"-window", OPT_STRING_VALIDATE(scaler[i].window.name, validate_window_opt)}
+
+const struct m_sub_options gl_video_conf = {
+ .opts = (const m_option_t[]) {
+ {"gpu-dumb-mode", OPT_CHOICE(dumb_mode,
+ {"auto", 0}, {"yes", 1}, {"no", -1})},
+ {"gamma-factor", OPT_FLOAT(gamma), M_RANGE(0.1, 2.0),
+ .deprecation_message = "no replacement"},
+ {"gamma-auto", OPT_BOOL(gamma_auto),
+ .deprecation_message = "no replacement"},
+ {"target-prim", OPT_CHOICE_C(target_prim, mp_csp_prim_names)},
+ {"target-trc", OPT_CHOICE_C(target_trc, mp_csp_trc_names)},
+ {"target-peak", OPT_CHOICE(target_peak, {"auto", 0}),
+ M_RANGE(10, 10000)},
+ {"target-contrast", OPT_CHOICE(target_contrast, {"auto", 0}, {"inf", -1}),
+ M_RANGE(10, 1000000)},
+ {"target-gamut", OPT_CHOICE_C(target_gamut, mp_csp_prim_names)},
+ {"tone-mapping", OPT_CHOICE(tone_map.curve,
+ {"auto", TONE_MAPPING_AUTO},
+ {"clip", TONE_MAPPING_CLIP},
+ {"mobius", TONE_MAPPING_MOBIUS},
+ {"reinhard", TONE_MAPPING_REINHARD},
+ {"hable", TONE_MAPPING_HABLE},
+ {"gamma", TONE_MAPPING_GAMMA},
+ {"linear", TONE_MAPPING_LINEAR},
+ {"spline", TONE_MAPPING_SPLINE},
+ {"bt.2390", TONE_MAPPING_BT_2390},
+ {"bt.2446a", TONE_MAPPING_BT_2446A},
+ {"st2094-40", TONE_MAPPING_ST2094_40},
+ {"st2094-10", TONE_MAPPING_ST2094_10})},
+ {"tone-mapping-param", OPT_FLOATDEF(tone_map.curve_param)},
+ {"inverse-tone-mapping", OPT_BOOL(tone_map.inverse)},
+ {"tone-mapping-max-boost", OPT_FLOAT(tone_map.max_boost),
+ M_RANGE(1.0, 10.0)},
+ {"tone-mapping-visualize", OPT_BOOL(tone_map.visualize)},
+ {"gamut-mapping-mode", OPT_CHOICE(tone_map.gamut_mode,
+ {"auto", GAMUT_AUTO},
+ {"clip", GAMUT_CLIP},
+ {"perceptual", GAMUT_PERCEPTUAL},
+ {"relative", GAMUT_RELATIVE},
+ {"saturation", GAMUT_SATURATION},
+ {"absolute", GAMUT_ABSOLUTE},
+ {"desaturate", GAMUT_DESATURATE},
+ {"darken", GAMUT_DARKEN},
+ {"warn", GAMUT_WARN},
+ {"linear", GAMUT_LINEAR})},
+ {"hdr-compute-peak", OPT_CHOICE(tone_map.compute_peak,
+ {"auto", 0},
+ {"yes", 1},
+ {"no", -1})},
+ {"hdr-peak-percentile", OPT_FLOAT(tone_map.peak_percentile),
+ M_RANGE(0.0, 100.0)},
+ {"hdr-peak-decay-rate", OPT_FLOAT(tone_map.decay_rate),
+ M_RANGE(0.0, 1000.0)},
+ {"hdr-scene-threshold-low", OPT_FLOAT(tone_map.scene_threshold_low),
+ M_RANGE(0, 20.0)},
+ {"hdr-scene-threshold-high", OPT_FLOAT(tone_map.scene_threshold_high),
+ M_RANGE(0, 20.0)},
+ {"hdr-contrast-recovery", OPT_FLOAT(tone_map.contrast_recovery),
+ M_RANGE(0, 2.0)},
+ {"hdr-contrast-smoothness", OPT_FLOAT(tone_map.contrast_smoothness),
+ M_RANGE(1.0, 100.0)},
+ {"opengl-pbo", OPT_BOOL(pbo)},
+ SCALER_OPTS("scale", SCALER_SCALE),
+ SCALER_OPTS("dscale", SCALER_DSCALE),
+ SCALER_OPTS("cscale", SCALER_CSCALE),
+ SCALER_OPTS("tscale", SCALER_TSCALE),
+ {"scaler-lut-size", OPT_REMOVED("hard-coded as 8")},
+ {"scaler-resizes-only", OPT_BOOL(scaler_resizes_only)},
+ {"correct-downscaling", OPT_BOOL(correct_downscaling)},
+ {"linear-downscaling", OPT_BOOL(linear_downscaling)},
+ {"linear-upscaling", OPT_BOOL(linear_upscaling)},
+ {"sigmoid-upscaling", OPT_BOOL(sigmoid_upscaling)},
+ {"sigmoid-center", OPT_FLOAT(sigmoid_center), M_RANGE(0.0, 1.0)},
+ {"sigmoid-slope", OPT_FLOAT(sigmoid_slope), M_RANGE(1.0, 20.0)},
+ {"fbo-format", OPT_STRING(fbo_format)},
+ {"dither-depth", OPT_CHOICE(dither_depth, {"no", -1}, {"auto", 0}),
+ M_RANGE(-1, 16)},
+ {"dither", OPT_CHOICE(dither_algo,
+ {"fruit", DITHER_FRUIT},
+ {"ordered", DITHER_ORDERED},
+ {"error-diffusion", DITHER_ERROR_DIFFUSION},
+ {"no", DITHER_NONE})},
+ {"dither-size-fruit", OPT_INT(dither_size), M_RANGE(2, 8)},
+ {"temporal-dither", OPT_BOOL(temporal_dither)},
+ {"temporal-dither-period", OPT_INT(temporal_dither_period),
+ M_RANGE(1, 128)},
+ {"error-diffusion",
+ OPT_STRING_VALIDATE(error_diffusion, validate_error_diffusion_opt)},
+ {"alpha", OPT_CHOICE(alpha_mode,
+ {"no", ALPHA_NO},
+ {"yes", ALPHA_YES},
+ {"blend", ALPHA_BLEND},
+ {"blend-tiles", ALPHA_BLEND_TILES})},
+ {"opengl-rectangle-textures", OPT_BOOL(use_rectangle)},
+ {"background", OPT_COLOR(background)},
+ {"interpolation", OPT_BOOL(interpolation)},
+ {"interpolation-threshold", OPT_FLOAT(interpolation_threshold)},
+ {"blend-subtitles", OPT_CHOICE(blend_subs,
+ {"no", BLEND_SUBS_NO},
+ {"yes", BLEND_SUBS_YES},
+ {"video", BLEND_SUBS_VIDEO})},
+ {"glsl-shaders", OPT_PATHLIST(user_shaders), .flags = M_OPT_FILE},
+ {"glsl-shader", OPT_CLI_ALIAS("glsl-shaders-append")},
+ {"glsl-shader-opts", OPT_KEYVALUELIST(user_shader_opts)},
+ {"deband", OPT_BOOL(deband)},
+ {"deband", OPT_SUBSTRUCT(deband_opts, deband_conf)},
+ {"sharpen", OPT_FLOAT(unsharp)},
+ {"gpu-tex-pad-x", OPT_INT(tex_pad_x), M_RANGE(0, 4096)},
+ {"gpu-tex-pad-y", OPT_INT(tex_pad_y), M_RANGE(0, 4096)},
+ {"", OPT_SUBSTRUCT(icc_opts, mp_icc_conf)},
+ {"gpu-shader-cache", OPT_BOOL(shader_cache)},
+ {"gpu-shader-cache-dir", OPT_STRING(shader_cache_dir), .flags = M_OPT_FILE},
+ {"gpu-hwdec-interop",
+ OPT_STRING_VALIDATE(hwdec_interop, ra_hwdec_validate_opt)},
+ {"gamut-warning", OPT_REMOVED("Replaced by --gamut-mapping-mode=warn")},
+ {"gamut-clipping", OPT_REMOVED("Replaced by --gamut-mapping-mode=desaturate")},
+ {"tone-mapping-desaturate", OPT_REMOVED("Replaced by --tone-mapping-mode")},
+ {"tone-mapping-desaturate-exponent", OPT_REMOVED("Replaced by --tone-mapping-mode")},
+ {"tone-mapping-crosstalk", OPT_REMOVED("Hard-coded as 0.04")},
+ {"tone-mapping-mode", OPT_REMOVED("no replacement")},
+ {0}
+ },
+ .size = sizeof(struct gl_video_opts),
+ .defaults = &gl_video_opts_def,
+};
+
+static void uninit_rendering(struct gl_video *p);
+static void uninit_scaler(struct gl_video *p, struct scaler *scaler);
+static void check_gl_features(struct gl_video *p);
+static bool pass_upload_image(struct gl_video *p, struct mp_image *mpi, uint64_t id);
+static const char *handle_scaler_opt(const char *name, bool tscale);
+static void reinit_from_options(struct gl_video *p);
+static void get_scale_factors(struct gl_video *p, bool transpose_rot, double xy[2]);
+static void gl_video_setup_hooks(struct gl_video *p);
+static void gl_video_update_options(struct gl_video *p);
+
+#define GLSL(x) gl_sc_add(p->sc, #x "\n");
+#define GLSLF(...) gl_sc_addf(p->sc, __VA_ARGS__)
+#define GLSLHF(...) gl_sc_haddf(p->sc, __VA_ARGS__)
+#define PRELUDE(...) gl_sc_paddf(p->sc, __VA_ARGS__)
+
+static struct bstr load_cached_file(struct gl_video *p, const char *path)
+{
+ if (!path || !path[0])
+ return (struct bstr){0};
+ for (int n = 0; n < p->num_files; n++) {
+ if (strcmp(p->files[n].path, path) == 0)
+ return p->files[n].body;
+ }
+ // not found -> load it
+ char *fname = mp_get_user_path(NULL, p->global, path);
+ struct bstr s = stream_read_file(fname, p, p->global, 1000000000); // 1GB
+ talloc_free(fname);
+ if (s.len) {
+ struct cached_file new = {
+ .path = talloc_strdup(p, path),
+ .body = s,
+ };
+ MP_TARRAY_APPEND(p, p->files, p->num_files, new);
+ return new.body;
+ }
+ return (struct bstr){0};
+}
+
+static void debug_check_gl(struct gl_video *p, const char *msg)
+{
+ if (p->ra->fns->debug_marker)
+ p->ra->fns->debug_marker(p->ra, msg);
+}
+
+static void gl_video_reset_surfaces(struct gl_video *p)
+{
+ for (int i = 0; i < SURFACES_MAX; i++) {
+ p->surfaces[i].id = 0;
+ p->surfaces[i].pts = MP_NOPTS_VALUE;
+ }
+ p->surface_idx = 0;
+ p->surface_now = 0;
+ p->frames_drawn = 0;
+ p->output_tex_valid = false;
+}
+
+static void gl_video_reset_hooks(struct gl_video *p)
+{
+ for (int i = 0; i < p->num_tex_hooks; i++)
+ talloc_free(p->tex_hooks[i].priv);
+
+ for (int i = 0; i < p->num_user_textures; i++)
+ ra_tex_free(p->ra, &p->user_textures[i].tex);
+
+ p->num_tex_hooks = 0;
+ p->num_user_textures = 0;
+}
+
+static inline int surface_wrap(int id)
+{
+ id = id % SURFACES_MAX;
+ return id < 0 ? id + SURFACES_MAX : id;
+}
+
+static void reinit_osd(struct gl_video *p)
+{
+ mpgl_osd_destroy(p->osd);
+ p->osd = NULL;
+ if (p->osd_state)
+ p->osd = mpgl_osd_init(p->ra, p->log, p->osd_state);
+}
+
+static void uninit_rendering(struct gl_video *p)
+{
+ for (int n = 0; n < SCALER_COUNT; n++)
+ uninit_scaler(p, &p->scaler[n]);
+
+ ra_tex_free(p->ra, &p->dither_texture);
+
+ for (int n = 0; n < 4; n++) {
+ ra_tex_free(p->ra, &p->merge_tex[n]);
+ ra_tex_free(p->ra, &p->scale_tex[n]);
+ ra_tex_free(p->ra, &p->integer_tex[n]);
+ }
+
+ ra_tex_free(p->ra, &p->indirect_tex);
+ ra_tex_free(p->ra, &p->blend_subs_tex);
+ ra_tex_free(p->ra, &p->screen_tex);
+ ra_tex_free(p->ra, &p->output_tex);
+
+ for (int n = 0; n < 2; n++)
+ ra_tex_free(p->ra, &p->error_diffusion_tex[n]);
+
+ for (int n = 0; n < SURFACES_MAX; n++)
+ ra_tex_free(p->ra, &p->surfaces[n].tex);
+
+ for (int n = 0; n < p->num_hook_textures; n++)
+ ra_tex_free(p->ra, &p->hook_textures[n]);
+
+ gl_video_reset_surfaces(p);
+ gl_video_reset_hooks(p);
+
+ gl_sc_reset_error(p->sc);
+}
+
+bool gl_video_gamma_auto_enabled(struct gl_video *p)
+{
+ return p->opts.gamma_auto;
+}
+
+struct mp_colorspace gl_video_get_output_colorspace(struct gl_video *p)
+{
+ return (struct mp_colorspace) {
+ .primaries = p->opts.target_prim,
+ .gamma = p->opts.target_trc,
+ .hdr.max_luma = p->opts.target_peak,
+ };
+}
+
+// Warning: profile.start must point to a ta allocation, and the function
+// takes over ownership.
+void gl_video_set_icc_profile(struct gl_video *p, bstr icc_data)
+{
+ if (gl_lcms_set_memory_profile(p->cms, icc_data))
+ reinit_from_options(p);
+}
+
+bool gl_video_icc_auto_enabled(struct gl_video *p)
+{
+ return p->opts.icc_opts ? p->opts.icc_opts->profile_auto : false;
+}
+
+static bool gl_video_get_lut3d(struct gl_video *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc)
+{
+ if (!p->use_lut_3d)
+ return false;
+
+ struct AVBufferRef *icc = NULL;
+ if (p->image.mpi)
+ icc = p->image.mpi->icc_profile;
+
+ if (p->lut_3d_texture && !gl_lcms_has_changed(p->cms, prim, trc, icc))
+ return true;
+
+ // GLES3 doesn't provide filtered 16 bit integer textures
+ // GLES2 doesn't even provide 3D textures
+ const struct ra_format *fmt = ra_find_unorm_format(p->ra, 2, 4);
+ if (!fmt || !(p->ra->caps & RA_CAP_TEX_3D)) {
+ p->use_lut_3d = false;
+ MP_WARN(p, "Disabling color management (no RGBA16 3D textures).\n");
+ return false;
+ }
+
+ struct lut3d *lut3d = NULL;
+ if (!fmt || !gl_lcms_get_lut3d(p->cms, &lut3d, prim, trc, icc) || !lut3d) {
+ p->use_lut_3d = false;
+ return false;
+ }
+
+ ra_tex_free(p->ra, &p->lut_3d_texture);
+
+ struct ra_tex_params params = {
+ .dimensions = 3,
+ .w = lut3d->size[0],
+ .h = lut3d->size[1],
+ .d = lut3d->size[2],
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .initial_data = lut3d->data,
+ };
+ p->lut_3d_texture = ra_tex_create(p->ra, &params);
+
+ debug_check_gl(p, "after 3d lut creation");
+
+ for (int i = 0; i < 3; i++)
+ p->lut_3d_size[i] = lut3d->size[i];
+
+ talloc_free(lut3d);
+
+ if (!p->lut_3d_texture) {
+ p->use_lut_3d = false;
+ return false;
+ }
+
+ return true;
+}
+
+// Fill an image struct from a ra_tex + some metadata
+static struct image image_wrap(struct ra_tex *tex, enum plane_type type,
+ int components)
+{
+ assert(type != PLANE_NONE);
+ return (struct image){
+ .type = type,
+ .tex = tex,
+ .multiplier = 1.0,
+ .w = tex ? tex->params.w : 1,
+ .h = tex ? tex->params.h : 1,
+ .transform = identity_trans,
+ .components = components,
+ };
+}
+
+// Bind an image to a free texture unit and return its ID.
+static int pass_bind(struct gl_video *p, struct image img)
+{
+ int idx = p->num_pass_imgs;
+ MP_TARRAY_APPEND(p, p->pass_imgs, p->num_pass_imgs, img);
+ return idx;
+}
+
+// Rotation by 90° and flipping.
+// w/h is used for recentering.
+static void get_transform(float w, float h, int rotate, bool flip,
+ struct gl_transform *out_tr)
+{
+ int a = rotate % 90 ? 0 : rotate / 90;
+ int sin90[4] = {0, 1, 0, -1}; // just to avoid rounding issues etc.
+ int cos90[4] = {1, 0, -1, 0};
+ struct gl_transform tr = {{{ cos90[a], sin90[a]},
+ {-sin90[a], cos90[a]}}};
+
+ // basically, recenter to keep the whole image in view
+ float b[2] = {1, 1};
+ gl_transform_vec(tr, &b[0], &b[1]);
+ tr.t[0] += b[0] < 0 ? w : 0;
+ tr.t[1] += b[1] < 0 ? h : 0;
+
+ if (flip) {
+ struct gl_transform fliptr = {{{1, 0}, {0, -1}}, {0, h}};
+ gl_transform_trans(fliptr, &tr);
+ }
+
+ *out_tr = tr;
+}
+
+// Return the chroma plane upscaled to luma size, but with additional padding
+// for image sizes not aligned to subsampling.
+static int chroma_upsize(int size, int pixel)
+{
+ return (size + pixel - 1) / pixel * pixel;
+}
+
+// If a and b are on the same plane, return what plane type should be used.
+// If a or b are none, the other type always wins.
+// Usually: LUMA/RGB/XYZ > CHROMA > ALPHA
+static enum plane_type merge_plane_types(enum plane_type a, enum plane_type b)
+{
+ if (a == PLANE_NONE)
+ return b;
+ if (b == PLANE_LUMA || b == PLANE_RGB || b == PLANE_XYZ)
+ return b;
+ if (b != PLANE_NONE && a == PLANE_ALPHA)
+ return b;
+ return a;
+}
+
+// Places a video_image's image textures + associated metadata into img[]. The
+// number of textures is equal to p->plane_count. Any necessary plane offsets
+// are stored in off. (e.g. chroma position)
+static void pass_get_images(struct gl_video *p, struct video_image *vimg,
+ struct image img[4], struct gl_transform off[4])
+{
+ assert(vimg->mpi);
+
+ int w = p->image_params.w;
+ int h = p->image_params.h;
+
+ // Determine the chroma offset
+ float ls_w = 1.0 / p->ra_format.chroma_w;
+ float ls_h = 1.0 / p->ra_format.chroma_h;
+
+ struct gl_transform chroma = {{{ls_w, 0.0}, {0.0, ls_h}}};
+
+ if (p->image_params.chroma_location != MP_CHROMA_CENTER) {
+ int cx, cy;
+ mp_get_chroma_location(p->image_params.chroma_location, &cx, &cy);
+ // By default texture coordinates are such that chroma is centered with
+ // any chroma subsampling. If a specific direction is given, make it
+ // so that the luma and chroma sample line up exactly.
+ // For 4:4:4, setting chroma location should have no effect at all.
+ // luma sample size (in chroma coord. space)
+ chroma.t[0] = ls_w < 1 ? ls_w * -cx / 2 : 0;
+ chroma.t[1] = ls_h < 1 ? ls_h * -cy / 2 : 0;
+ }
+
+ memset(img, 0, 4 * sizeof(img[0]));
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *t = &vimg->planes[n];
+
+ enum plane_type type = PLANE_NONE;
+ int padding = 0;
+ for (int i = 0; i < 4; i++) {
+ int c = p->ra_format.components[n][i];
+ enum plane_type ctype;
+ if (c == 0) {
+ ctype = PLANE_NONE;
+ } else if (c == 4) {
+ ctype = PLANE_ALPHA;
+ } else if (p->image_params.color.space == MP_CSP_RGB) {
+ ctype = PLANE_RGB;
+ } else if (p->image_params.color.space == MP_CSP_XYZ) {
+ ctype = PLANE_XYZ;
+ } else {
+ ctype = c == 1 ? PLANE_LUMA : PLANE_CHROMA;
+ }
+ type = merge_plane_types(type, ctype);
+ if (!c && padding == i)
+ padding = i + 1;
+ }
+
+ int msb_valid_bits =
+ p->ra_format.component_bits + MPMIN(p->ra_format.component_pad, 0);
+ int csp = type == PLANE_ALPHA ? MP_CSP_RGB : p->image_params.color.space;
+ float tex_mul =
+ 1.0 / mp_get_csp_mul(csp, msb_valid_bits, p->ra_format.component_bits);
+ if (p->ra_format.component_type == RA_CTYPE_FLOAT)
+ tex_mul = 1.0;
+
+ img[n] = (struct image){
+ .type = type,
+ .tex = t->tex,
+ .multiplier = tex_mul,
+ .w = t->w,
+ .h = t->h,
+ .padding = padding,
+ };
+
+ for (int i = 0; i < 4; i++)
+ img[n].components += !!p->ra_format.components[n][i];
+
+ get_transform(t->w, t->h, p->image_params.rotate, t->flipped,
+ &img[n].transform);
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(int, img[n].w, img[n].h);
+
+ off[n] = identity_trans;
+
+ if (type == PLANE_CHROMA) {
+ struct gl_transform rot;
+ get_transform(0, 0, p->image_params.rotate, true, &rot);
+
+ struct gl_transform tr = chroma;
+ gl_transform_vec(rot, &tr.t[0], &tr.t[1]);
+
+ float dx = (chroma_upsize(w, p->ra_format.chroma_w) - w) * ls_w;
+ float dy = (chroma_upsize(h, p->ra_format.chroma_h) - h) * ls_h;
+
+ // Adjust the chroma offset if the real chroma size is fractional
+ // due image sizes not aligned to chroma subsampling.
+ struct gl_transform rot2;
+ get_transform(0, 0, p->image_params.rotate, t->flipped, &rot2);
+ if (rot2.m[0][0] < 0)
+ tr.t[0] += dx;
+ if (rot2.m[1][0] < 0)
+ tr.t[0] += dy;
+ if (rot2.m[0][1] < 0)
+ tr.t[1] += dx;
+ if (rot2.m[1][1] < 0)
+ tr.t[1] += dy;
+
+ off[n] = tr;
+ }
+ }
+}
+
+// Return the index of the given component (assuming all non-padding components
+// of all planes are concatenated into a linear list).
+static int find_comp(struct ra_imgfmt_desc *desc, int component)
+{
+ int cur = 0;
+ for (int n = 0; n < desc->num_planes; n++) {
+ for (int i = 0; i < 4; i++) {
+ if (desc->components[n][i]) {
+ if (desc->components[n][i] == component)
+ return cur;
+ cur++;
+ }
+ }
+ }
+ return -1;
+}
+
+static void init_video(struct gl_video *p)
+{
+ p->use_integer_conversion = false;
+
+ struct ra_hwdec *hwdec = ra_hwdec_get(&p->hwdec_ctx, p->image_params.imgfmt);
+ if (hwdec) {
+ if (hwdec->driver->overlay_frame) {
+ MP_WARN(p, "Using HW-overlay mode. No GL filtering is performed "
+ "on the video!\n");
+ p->hwdec_overlay = hwdec;
+ } else {
+ p->hwdec_mapper = ra_hwdec_mapper_create(hwdec, &p->image_params);
+ if (!p->hwdec_mapper)
+ MP_ERR(p, "Initializing texture for hardware decoding failed.\n");
+ }
+ if (p->hwdec_mapper)
+ p->image_params = p->hwdec_mapper->dst_params;
+ const char **exts = hwdec->glsl_extensions;
+ for (int n = 0; exts && exts[n]; n++)
+ gl_sc_enable_extension(p->sc, (char *)exts[n]);
+ p->hwdec_active = true;
+ }
+
+ p->ra_format = (struct ra_imgfmt_desc){0};
+ ra_get_imgfmt_desc(p->ra, p->image_params.imgfmt, &p->ra_format);
+
+ p->plane_count = p->ra_format.num_planes;
+
+ p->has_alpha = false;
+ p->is_gray = true;
+
+ for (int n = 0; n < p->ra_format.num_planes; n++) {
+ for (int i = 0; i < 4; i++) {
+ if (p->ra_format.components[n][i]) {
+ p->has_alpha |= p->ra_format.components[n][i] == 4;
+ p->is_gray &= p->ra_format.components[n][i] == 1 ||
+ p->ra_format.components[n][i] == 4;
+ }
+ }
+ }
+
+ for (int c = 0; c < 4; c++) {
+ int loc = find_comp(&p->ra_format, c + 1);
+ p->color_swizzle[c] = "rgba"[loc >= 0 && loc < 4 ? loc : 0];
+ }
+ p->color_swizzle[4] = '\0';
+
+ mp_image_params_guess_csp(&p->image_params);
+
+ av_lfg_init(&p->lfg, 1);
+
+ debug_check_gl(p, "before video texture creation");
+
+ if (!p->hwdec_active) {
+ struct video_image *vimg = &p->image;
+
+ struct mp_image layout = {0};
+ mp_image_set_params(&layout, &p->image_params);
+
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *plane = &vimg->planes[n];
+ const struct ra_format *format = p->ra_format.planes[n];
+
+ plane->w = mp_image_plane_w(&layout, n);
+ plane->h = mp_image_plane_h(&layout, n);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = plane->w + p->opts.tex_pad_x,
+ .h = plane->h + p->opts.tex_pad_y,
+ .d = 1,
+ .format = format,
+ .render_src = true,
+ .src_linear = format->linear_filter,
+ .non_normalized = p->opts.use_rectangle,
+ .host_mutable = true,
+ };
+
+ MP_VERBOSE(p, "Texture for plane %d: %dx%d\n", n,
+ params.w, params.h);
+
+ plane->tex = ra_tex_create(p->ra, &params);
+ p->use_integer_conversion |= format->ctype == RA_CTYPE_UINT;
+ }
+ }
+
+ debug_check_gl(p, "after video texture creation");
+
+ // Format-dependent checks.
+ check_gl_features(p);
+
+ gl_video_setup_hooks(p);
+}
+
+static struct dr_buffer *gl_find_dr_buffer(struct gl_video *p, uint8_t *ptr)
+{
+ for (int i = 0; i < p->num_dr_buffers; i++) {
+ struct dr_buffer *buffer = &p->dr_buffers[i];
+ uint8_t *bufptr = buffer->buf->data;
+ size_t size = buffer->buf->params.size;
+ if (ptr >= bufptr && ptr < bufptr + size)
+ return buffer;
+ }
+
+ return NULL;
+}
+
+static void gc_pending_dr_fences(struct gl_video *p, bool force)
+{
+again:;
+ for (int n = 0; n < p->num_dr_buffers; n++) {
+ struct dr_buffer *buffer = &p->dr_buffers[n];
+ if (!buffer->mpi)
+ continue;
+
+ bool res = p->ra->fns->buf_poll(p->ra, buffer->buf);
+ if (res || force) {
+ // Unreferencing the image could cause gl_video_dr_free_buffer()
+ // to be called by the talloc destructor (if it was the last
+ // reference). This will implicitly invalidate the buffer pointer
+ // and change the p->dr_buffers array. To make it worse, it could
+ // free multiple dr_buffers due to weird theoretical corner cases.
+ // This is also why we use the goto to iterate again from the
+ // start, because everything gets fucked up. Hail satan!
+ struct mp_image *ref = buffer->mpi;
+ buffer->mpi = NULL;
+ talloc_free(ref);
+ goto again;
+ }
+ }
+}
+
+static void unref_current_image(struct gl_video *p)
+{
+ struct video_image *vimg = &p->image;
+
+ if (vimg->hwdec_mapped) {
+ assert(p->hwdec_active && p->hwdec_mapper);
+ ra_hwdec_mapper_unmap(p->hwdec_mapper);
+ memset(vimg->planes, 0, sizeof(vimg->planes));
+ vimg->hwdec_mapped = false;
+ }
+
+ vimg->id = 0;
+
+ mp_image_unrefp(&vimg->mpi);
+
+ // While we're at it, also garbage collect pending fences in here to
+ // get it out of the way.
+ gc_pending_dr_fences(p, false);
+}
+
+// If overlay mode is used, make sure to remove the overlay.
+// Be careful with this. Removing the overlay and adding another one will
+// lead to flickering artifacts.
+static void unmap_overlay(struct gl_video *p)
+{
+ if (p->hwdec_overlay)
+ p->hwdec_overlay->driver->overlay_frame(p->hwdec_overlay, NULL, NULL, NULL, true);
+}
+
+static void uninit_video(struct gl_video *p)
+{
+ uninit_rendering(p);
+
+ struct video_image *vimg = &p->image;
+
+ unmap_overlay(p);
+ unref_current_image(p);
+
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *plane = &vimg->planes[n];
+ ra_tex_free(p->ra, &plane->tex);
+ }
+ *vimg = (struct video_image){0};
+
+ // Invalidate image_params to ensure that gl_video_config() will call
+ // init_video() on uninitialized gl_video.
+ p->real_image_params = (struct mp_image_params){0};
+ p->image_params = p->real_image_params;
+ p->hwdec_active = false;
+ p->hwdec_overlay = NULL;
+ ra_hwdec_mapper_free(&p->hwdec_mapper);
+}
+
+static void pass_record(struct gl_video *p, struct mp_pass_perf perf)
+{
+ if (!p->pass || p->pass_idx == VO_PASS_PERF_MAX)
+ return;
+
+ struct pass_info *pass = &p->pass[p->pass_idx];
+ pass->perf = perf;
+
+ if (pass->desc.len == 0)
+ bstr_xappend(p, &pass->desc, bstr0("(unknown)"));
+
+ p->pass_idx++;
+}
+
+PRINTF_ATTRIBUTE(2, 3)
+static void pass_describe(struct gl_video *p, const char *textf, ...)
+{
+ if (!p->pass || p->pass_idx == VO_PASS_PERF_MAX)
+ return;
+
+ struct pass_info *pass = &p->pass[p->pass_idx];
+
+ if (pass->desc.len > 0)
+ bstr_xappend(p, &pass->desc, bstr0(" + "));
+
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(p, &pass->desc, textf, ap);
+ va_end(ap);
+}
+
+static void pass_info_reset(struct gl_video *p, bool is_redraw)
+{
+ p->pass = is_redraw ? p->pass_redraw : p->pass_fresh;
+ p->pass_idx = 0;
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ p->pass[i].desc.len = 0;
+ p->pass[i].perf = (struct mp_pass_perf){0};
+ }
+}
+
+static void pass_report_performance(struct gl_video *p)
+{
+ if (!p->pass)
+ return;
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ struct pass_info *pass = &p->pass[i];
+ if (pass->desc.len) {
+ MP_TRACE(p, "pass '%.*s': last %dus avg %dus peak %dus\n",
+ BSTR_P(pass->desc),
+ (int)pass->perf.last/1000,
+ (int)pass->perf.avg/1000,
+ (int)pass->perf.peak/1000);
+ }
+ }
+}
+
+static void pass_prepare_src_tex(struct gl_video *p)
+{
+ struct gl_shader_cache *sc = p->sc;
+
+ for (int n = 0; n < p->num_pass_imgs; n++) {
+ struct image *s = &p->pass_imgs[n];
+ if (!s->tex)
+ continue;
+
+ char *texture_name = mp_tprintf(32, "texture%d", n);
+ char *texture_size = mp_tprintf(32, "texture_size%d", n);
+ char *texture_rot = mp_tprintf(32, "texture_rot%d", n);
+ char *texture_off = mp_tprintf(32, "texture_off%d", n);
+ char *pixel_size = mp_tprintf(32, "pixel_size%d", n);
+
+ gl_sc_uniform_texture(sc, texture_name, s->tex);
+ float f[2] = {1, 1};
+ if (!s->tex->params.non_normalized) {
+ f[0] = s->tex->params.w;
+ f[1] = s->tex->params.h;
+ }
+ gl_sc_uniform_vec2(sc, texture_size, f);
+ gl_sc_uniform_mat2(sc, texture_rot, true, (float *)s->transform.m);
+ gl_sc_uniform_vec2(sc, texture_off, (float *)s->transform.t);
+ gl_sc_uniform_vec2(sc, pixel_size, (float[]){1.0f / f[0],
+ 1.0f / f[1]});
+ }
+}
+
+static void cleanup_binds(struct gl_video *p)
+{
+ p->num_pass_imgs = 0;
+}
+
+// Sets the appropriate compute shader metadata for an implicit compute pass
+// bw/bh: block size
+static void pass_is_compute(struct gl_video *p, int bw, int bh, bool flexible)
+{
+ if (p->pass_compute.active && flexible) {
+ // Avoid overwriting existing block sizes when using a flexible pass
+ bw = p->pass_compute.block_w;
+ bh = p->pass_compute.block_h;
+ }
+
+ p->pass_compute = (struct compute_info){
+ .active = true,
+ .block_w = bw,
+ .block_h = bh,
+ };
+}
+
+// w/h: the width/height of the compute shader's operating domain (e.g. the
+// target target that needs to be written, or the source texture that needs to
+// be reduced)
+static void dispatch_compute(struct gl_video *p, int w, int h,
+ struct compute_info info)
+{
+ PRELUDE("layout (local_size_x = %d, local_size_y = %d) in;\n",
+ info.threads_w > 0 ? info.threads_w : info.block_w,
+ info.threads_h > 0 ? info.threads_h : info.block_h);
+
+ pass_prepare_src_tex(p);
+
+ // Since we don't actually have vertices, we pretend for convenience
+ // reasons that we do and calculate the right texture coordinates based on
+ // the output sample ID
+ gl_sc_uniform_vec2(p->sc, "out_scale", (float[2]){ 1.0 / w, 1.0 / h });
+ PRELUDE("#define outcoord(id) (out_scale * (vec2(id) + vec2(0.5)))\n");
+
+ for (int n = 0; n < p->num_pass_imgs; n++) {
+ struct image *s = &p->pass_imgs[n];
+ if (!s->tex)
+ continue;
+
+ PRELUDE("#define texmap%d(id) (texture_rot%d * outcoord(id) + "
+ "pixel_size%d * texture_off%d)\n", n, n, n, n);
+ PRELUDE("#define texcoord%d texmap%d(gl_GlobalInvocationID)\n", n, n);
+ }
+
+ // always round up when dividing to make sure we don't leave off a part of
+ // the image
+ int num_x = info.block_w > 0 ? (w + info.block_w - 1) / info.block_w : 1,
+ num_y = info.block_h > 0 ? (h + info.block_h - 1) / info.block_h : 1;
+
+ if (!(p->ra->caps & RA_CAP_NUM_GROUPS))
+ PRELUDE("#define gl_NumWorkGroups uvec3(%d, %d, 1)\n", num_x, num_y);
+
+ pass_record(p, gl_sc_dispatch_compute(p->sc, num_x, num_y, 1));
+ cleanup_binds(p);
+}
+
+static struct mp_pass_perf render_pass_quad(struct gl_video *p,
+ struct ra_fbo fbo, bool discard,
+ const struct mp_rect *dst)
+{
+ // The first element is reserved for `vec2 position`
+ int num_vertex_attribs = 1 + p->num_pass_imgs;
+ size_t vertex_stride = num_vertex_attribs * sizeof(struct vertex_pt);
+
+ // Expand the VAO if necessary
+ while (p->vao_len < num_vertex_attribs) {
+ MP_TARRAY_APPEND(p, p->vao, p->vao_len, (struct ra_renderpass_input) {
+ .name = talloc_asprintf(p, "texcoord%d", p->vao_len - 1),
+ .type = RA_VARTYPE_FLOAT,
+ .dim_v = 2,
+ .dim_m = 1,
+ .offset = p->vao_len * sizeof(struct vertex_pt),
+ });
+ }
+
+ int num_vertices = 6; // quad as triangle list
+ int num_attribs_total = num_vertices * num_vertex_attribs;
+ MP_TARRAY_GROW(p, p->tmp_vertex, num_attribs_total);
+
+ struct gl_transform t;
+ gl_transform_ortho_fbo(&t, fbo);
+
+ float x[2] = {dst->x0, dst->x1};
+ float y[2] = {dst->y0, dst->y1};
+ gl_transform_vec(t, &x[0], &y[0]);
+ gl_transform_vec(t, &x[1], &y[1]);
+
+ for (int n = 0; n < 4; n++) {
+ struct vertex_pt *vs = &p->tmp_vertex[num_vertex_attribs * n];
+ // vec2 position in idx 0
+ vs[0].x = x[n / 2];
+ vs[0].y = y[n % 2];
+ for (int i = 0; i < p->num_pass_imgs; i++) {
+ struct image *s = &p->pass_imgs[i];
+ if (!s->tex)
+ continue;
+ struct gl_transform tr = s->transform;
+ float tx = (n / 2) * s->w;
+ float ty = (n % 2) * s->h;
+ gl_transform_vec(tr, &tx, &ty);
+ bool rect = s->tex->params.non_normalized;
+ // vec2 texcoordN in idx N+1
+ vs[i + 1].x = tx / (rect ? 1 : s->tex->params.w);
+ vs[i + 1].y = ty / (rect ? 1 : s->tex->params.h);
+ }
+ }
+
+ memmove(&p->tmp_vertex[num_vertex_attribs * 4],
+ &p->tmp_vertex[num_vertex_attribs * 2],
+ vertex_stride);
+
+ memmove(&p->tmp_vertex[num_vertex_attribs * 5],
+ &p->tmp_vertex[num_vertex_attribs * 1],
+ vertex_stride);
+
+ return gl_sc_dispatch_draw(p->sc, fbo.tex, discard, p->vao, num_vertex_attribs,
+ vertex_stride, p->tmp_vertex, num_vertices);
+}
+
+static void finish_pass_fbo(struct gl_video *p, struct ra_fbo fbo,
+ bool discard, const struct mp_rect *dst)
+{
+ pass_prepare_src_tex(p);
+ pass_record(p, render_pass_quad(p, fbo, discard, dst));
+ debug_check_gl(p, "after rendering");
+ cleanup_binds(p);
+}
+
+// dst_fbo: this will be used for rendering; possibly reallocating the whole
+// FBO, if the required parameters have changed
+// w, h: required FBO target dimension, and also defines the target rectangle
+// used for rasterization
+static void finish_pass_tex(struct gl_video *p, struct ra_tex **dst_tex,
+ int w, int h)
+{
+ if (!ra_tex_resize(p->ra, p->log, dst_tex, w, h, p->fbo_format)) {
+ cleanup_binds(p);
+ gl_sc_reset(p->sc);
+ return;
+ }
+
+ // If RA_CAP_PARALLEL_COMPUTE is set, try to prefer compute shaders
+ // over fragment shaders wherever possible.
+ if (!p->pass_compute.active && (p->ra->caps & RA_CAP_PARALLEL_COMPUTE) &&
+ (*dst_tex)->params.storage_dst)
+ {
+ pass_is_compute(p, 16, 16, true);
+ }
+
+ if (p->pass_compute.active) {
+ gl_sc_uniform_image2D_wo(p->sc, "out_image", *dst_tex);
+ if (!p->pass_compute.directly_writes)
+ GLSL(imageStore(out_image, ivec2(gl_GlobalInvocationID), color);)
+
+ dispatch_compute(p, w, h, p->pass_compute);
+ p->pass_compute = (struct compute_info){0};
+
+ debug_check_gl(p, "after dispatching compute shader");
+ } else {
+ struct ra_fbo fbo = { .tex = *dst_tex, };
+ finish_pass_fbo(p, fbo, true, &(struct mp_rect){0, 0, w, h});
+ }
+}
+
+static const char *get_tex_swizzle(struct image *img)
+{
+ if (!img->tex)
+ return "rgba";
+ return img->tex->params.format->luminance_alpha ? "raaa" : "rgba";
+}
+
+// Copy a texture to the vec4 color, while increasing offset. Also applies
+// the texture multiplier to the sampled color
+static void copy_image(struct gl_video *p, unsigned int *offset, struct image img)
+{
+ const unsigned int count = img.components;
+ char src[5] = {0};
+ char dst[5] = {0};
+
+ assert(*offset + count < sizeof(dst));
+ assert(img.padding + count < sizeof(src));
+
+ int id = pass_bind(p, img);
+
+ const char *tex_fmt = get_tex_swizzle(&img);
+ const char *dst_fmt = "rgba";
+ for (unsigned int i = 0; i < count; i++) {
+ src[i] = tex_fmt[img.padding + i];
+ dst[i] = dst_fmt[*offset + i];
+ }
+
+ if (img.tex && img.tex->params.format->ctype == RA_CTYPE_UINT) {
+ uint64_t tex_max = 1ull << p->ra_format.component_bits;
+ img.multiplier *= 1.0 / (tex_max - 1);
+ }
+
+ GLSLF("color.%s = %f * vec4(texture(texture%d, texcoord%d)).%s;\n",
+ dst, img.multiplier, id, id, src);
+
+ *offset += count;
+}
+
+static void skip_unused(struct gl_video *p, int num_components)
+{
+ for (int i = num_components; i < 4; i++)
+ GLSLF("color.%c = %f;\n", "rgba"[i], i < 3 ? 0.0 : 1.0);
+}
+
+static void uninit_scaler(struct gl_video *p, struct scaler *scaler)
+{
+ ra_tex_free(p->ra, &scaler->sep_fbo);
+ ra_tex_free(p->ra, &scaler->lut);
+ scaler->kernel = NULL;
+ scaler->initialized = false;
+}
+
+static void hook_prelude(struct gl_video *p, const char *name, int id,
+ struct image img)
+{
+ GLSLHF("#define %s_raw texture%d\n", name, id);
+ GLSLHF("#define %s_pos texcoord%d\n", name, id);
+ GLSLHF("#define %s_size texture_size%d\n", name, id);
+ GLSLHF("#define %s_rot texture_rot%d\n", name, id);
+ GLSLHF("#define %s_off texture_off%d\n", name, id);
+ GLSLHF("#define %s_pt pixel_size%d\n", name, id);
+ GLSLHF("#define %s_map texmap%d\n", name, id);
+ GLSLHF("#define %s_mul %f\n", name, img.multiplier);
+
+ char crap[5] = "";
+ snprintf(crap, sizeof(crap), "%s", get_tex_swizzle(&img));
+
+ // Remove leading padding by rotating the swizzle mask.
+ int len = strlen(crap);
+ for (int n = 0; n < img.padding; n++) {
+ if (len) {
+ char f = crap[0];
+ memmove(crap, crap + 1, len - 1);
+ crap[len - 1] = f;
+ }
+ }
+
+ // Set up the sampling functions
+ GLSLHF("#define %s_tex(pos) (%s_mul * vec4(texture(%s_raw, pos)).%s)\n",
+ name, name, name, crap);
+
+ if (p->ra->caps & RA_CAP_GATHER) {
+ GLSLHF("#define %s_gather(pos, c) (%s_mul * vec4("
+ "textureGather(%s_raw, pos, c)))\n", name, name, name);
+ }
+
+ // Since the extra matrix multiplication impacts performance,
+ // skip it unless the texture was actually rotated
+ if (gl_transform_eq(img.transform, identity_trans)) {
+ GLSLHF("#define %s_texOff(off) %s_tex(%s_pos + %s_pt * vec2(off))\n",
+ name, name, name, name);
+ } else {
+ GLSLHF("#define %s_texOff(off) "
+ "%s_tex(%s_pos + %s_rot * vec2(off)/%s_size)\n",
+ name, name, name, name, name);
+ }
+}
+
+static bool saved_img_find(struct gl_video *p, const char *name,
+ struct image *out)
+{
+ if (!name || !out)
+ return false;
+
+ for (int i = 0; i < p->num_saved_imgs; i++) {
+ if (strcmp(p->saved_imgs[i].name, name) == 0) {
+ *out = p->saved_imgs[i].img;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static void saved_img_store(struct gl_video *p, const char *name,
+ struct image img)
+{
+ assert(name);
+
+ for (int i = 0; i < p->num_saved_imgs; i++) {
+ if (strcmp(p->saved_imgs[i].name, name) == 0) {
+ p->saved_imgs[i].img = img;
+ return;
+ }
+ }
+
+ MP_TARRAY_APPEND(p, p->saved_imgs, p->num_saved_imgs, (struct saved_img) {
+ .name = name,
+ .img = img
+ });
+}
+
+static bool pass_hook_setup_binds(struct gl_video *p, const char *name,
+ struct image img, struct tex_hook *hook)
+{
+ for (int t = 0; t < SHADER_MAX_BINDS; t++) {
+ char *bind_name = (char *)hook->bind_tex[t];
+
+ if (!bind_name)
+ continue;
+
+ // This is a special name that means "currently hooked texture"
+ if (strcmp(bind_name, "HOOKED") == 0) {
+ int id = pass_bind(p, img);
+ hook_prelude(p, "HOOKED", id, img);
+ hook_prelude(p, name, id, img);
+ continue;
+ }
+
+ // BIND can also be used to load user-defined textures, in which
+ // case we will directly load them as a uniform instead of
+ // generating the hook_prelude boilerplate
+ for (int u = 0; u < p->num_user_textures; u++) {
+ struct gl_user_shader_tex *utex = &p->user_textures[u];
+ if (bstr_equals0(utex->name, bind_name)) {
+ gl_sc_uniform_texture(p->sc, bind_name, utex->tex);
+ goto next_bind;
+ }
+ }
+
+ struct image bind_img;
+ if (!saved_img_find(p, bind_name, &bind_img)) {
+ // Clean up texture bindings and move on to the next hook
+ MP_TRACE(p, "Skipping hook on %s due to no texture named %s.\n",
+ name, bind_name);
+ p->num_pass_imgs -= t;
+ return false;
+ }
+
+ hook_prelude(p, bind_name, pass_bind(p, bind_img), bind_img);
+
+next_bind: ;
+ }
+
+ return true;
+}
+
+static struct ra_tex **next_hook_tex(struct gl_video *p)
+{
+ if (p->idx_hook_textures == p->num_hook_textures)
+ MP_TARRAY_APPEND(p, p->hook_textures, p->num_hook_textures, NULL);
+
+ return &p->hook_textures[p->idx_hook_textures++];
+}
+
+// Process hooks for a plane, saving the result and returning a new image
+// If 'trans' is NULL, the shader is forbidden from transforming img
+static struct image pass_hook(struct gl_video *p, const char *name,
+ struct image img, struct gl_transform *trans)
+{
+ if (!name)
+ return img;
+
+ saved_img_store(p, name, img);
+
+ MP_TRACE(p, "Running hooks for %s\n", name);
+ for (int i = 0; i < p->num_tex_hooks; i++) {
+ struct tex_hook *hook = &p->tex_hooks[i];
+
+ // Figure out if this pass hooks this texture
+ for (int h = 0; h < SHADER_MAX_HOOKS; h++) {
+ if (hook->hook_tex[h] && strcmp(hook->hook_tex[h], name) == 0)
+ goto found;
+ }
+
+ continue;
+
+found:
+ // Check the hook's condition
+ if (hook->cond && !hook->cond(p, img, hook->priv)) {
+ MP_TRACE(p, "Skipping hook on %s due to condition.\n", name);
+ continue;
+ }
+
+ const char *store_name = hook->save_tex ? hook->save_tex : name;
+ bool is_overwrite = strcmp(store_name, name) == 0;
+
+ // If user shader is set to align HOOKED with reference and fix its
+ // offset, it requires HOOKED to be resizable and overwrited.
+ if (is_overwrite && hook->align_offset) {
+ if (!trans) {
+ MP_ERR(p, "Hook tried to align unresizable texture %s!\n",
+ name);
+ return img;
+ }
+
+ struct gl_transform align_off = identity_trans;
+ align_off.t[0] = trans->t[0];
+ align_off.t[1] = trans->t[1];
+
+ gl_transform_trans(align_off, &img.transform);
+ }
+
+ if (!pass_hook_setup_binds(p, name, img, hook))
+ continue;
+
+ // Run the actual hook. This generates a series of GLSL shader
+ // instructions sufficient for drawing the hook's output
+ struct gl_transform hook_off = identity_trans;
+ hook->hook(p, img, &hook_off, hook->priv);
+
+ int comps = hook->components ? hook->components : img.components;
+ skip_unused(p, comps);
+
+ // Compute the updated FBO dimensions and store the result
+ struct mp_rect_f sz = {0, 0, img.w, img.h};
+ gl_transform_rect(hook_off, &sz);
+ int w = lroundf(fabs(sz.x1 - sz.x0));
+ int h = lroundf(fabs(sz.y1 - sz.y0));
+
+ struct ra_tex **tex = next_hook_tex(p);
+ finish_pass_tex(p, tex, w, h);
+ struct image saved_img = image_wrap(*tex, img.type, comps);
+
+ // If the texture we're saving overwrites the "current" texture, also
+ // update the tex parameter so that the future loop cycles will use the
+ // updated values, and export the offset
+ if (is_overwrite) {
+ if (!trans && !gl_transform_eq(hook_off, identity_trans)) {
+ MP_ERR(p, "Hook tried changing size of unscalable texture %s!\n",
+ name);
+ return img;
+ }
+
+ img = saved_img;
+ if (trans) {
+ gl_transform_trans(hook_off, trans);
+
+ // If user shader is set to align HOOKED, the offset it produces
+ // is dynamic (with static resizing factor though).
+ // Align it with reference manually to get offset fixed.
+ if (hook->align_offset) {
+ trans->t[0] = 0.0;
+ trans->t[1] = 0.0;
+ }
+ }
+ }
+
+ saved_img_store(p, store_name, saved_img);
+ }
+
+ return img;
+}
+
+// This can be used at any time in the middle of rendering to specify an
+// optional hook point, which if triggered will render out to a new FBO and
+// load the result back into vec4 color. Offsets applied by the hooks are
+// accumulated in tex_trans, and the FBO is dimensioned according
+// to p->texture_w/h
+static void pass_opt_hook_point(struct gl_video *p, const char *name,
+ struct gl_transform *tex_trans)
+{
+ if (!name)
+ return;
+
+ for (int i = 0; i < p->num_tex_hooks; i++) {
+ struct tex_hook *hook = &p->tex_hooks[i];
+
+ for (int h = 0; h < SHADER_MAX_HOOKS; h++) {
+ if (hook->hook_tex[h] && strcmp(hook->hook_tex[h], name) == 0)
+ goto found;
+ }
+
+ for (int b = 0; b < SHADER_MAX_BINDS; b++) {
+ if (hook->bind_tex[b] && strcmp(hook->bind_tex[b], name) == 0)
+ goto found;
+ }
+ }
+
+ // Nothing uses this texture, don't bother storing it
+ return;
+
+found: ;
+ struct ra_tex **tex = next_hook_tex(p);
+ finish_pass_tex(p, tex, p->texture_w, p->texture_h);
+ struct image img = image_wrap(*tex, PLANE_RGB, p->components);
+ img = pass_hook(p, name, img, tex_trans);
+ copy_image(p, &(int){0}, img);
+ p->texture_w = img.w;
+ p->texture_h = img.h;
+ p->components = img.components;
+ pass_describe(p, "(remainder pass)");
+}
+
+static void load_shader(struct gl_video *p, struct bstr body)
+{
+ gl_sc_hadd_bstr(p->sc, body);
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_f(p->sc, "random", (double)av_lfg_get(&p->lfg) / UINT32_MAX);
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_i(p->sc, "frame", p->frames_uploaded);
+ gl_sc_uniform_vec2(p->sc, "input_size",
+ (float[]){(p->src_rect.x1 - p->src_rect.x0) *
+ p->texture_offset.m[0][0],
+ (p->src_rect.y1 - p->src_rect.y0) *
+ p->texture_offset.m[1][1]});
+ gl_sc_uniform_vec2(p->sc, "target_size",
+ (float[]){p->dst_rect.x1 - p->dst_rect.x0,
+ p->dst_rect.y1 - p->dst_rect.y0});
+ gl_sc_uniform_vec2(p->sc, "tex_offset",
+ (float[]){p->src_rect.x0 * p->texture_offset.m[0][0] +
+ p->texture_offset.t[0],
+ p->src_rect.y0 * p->texture_offset.m[1][1] +
+ p->texture_offset.t[1]});
+}
+
+// Semantic equality
+static bool double_seq(double a, double b)
+{
+ return (isnan(a) && isnan(b)) || a == b;
+}
+
+static bool scaler_fun_eq(struct scaler_fun a, struct scaler_fun b)
+{
+ if ((a.name && !b.name) || (b.name && !a.name))
+ return false;
+
+ return ((!a.name && !b.name) || strcmp(a.name, b.name) == 0) &&
+ double_seq(a.params[0], b.params[0]) &&
+ double_seq(a.params[1], b.params[1]) &&
+ a.blur == b.blur &&
+ a.taper == b.taper;
+}
+
+static bool scaler_conf_eq(struct scaler_config a, struct scaler_config b)
+{
+ // Note: antiring isn't compared because it doesn't affect LUT
+ // generation
+ return scaler_fun_eq(a.kernel, b.kernel) &&
+ scaler_fun_eq(a.window, b.window) &&
+ a.radius == b.radius &&
+ a.clamp == b.clamp;
+}
+
+static void reinit_scaler(struct gl_video *p, struct scaler *scaler,
+ const struct scaler_config *conf,
+ double scale_factor,
+ int sizes[])
+{
+ assert(conf);
+ if (scaler_conf_eq(scaler->conf, *conf) &&
+ scaler->scale_factor == scale_factor &&
+ scaler->initialized)
+ return;
+
+ uninit_scaler(p, scaler);
+
+ if (scaler->index == SCALER_DSCALE && (!conf->kernel.name ||
+ !conf->kernel.name[0]))
+ {
+ conf = &p->opts.scaler[SCALER_SCALE];
+ }
+
+ if (scaler->index == SCALER_CSCALE && (!conf->kernel.name ||
+ !conf->kernel.name[0]))
+ {
+ conf = &p->opts.scaler[SCALER_SCALE];
+ }
+
+ struct filter_kernel bare_window;
+ const struct filter_kernel *t_kernel = mp_find_filter_kernel(conf->kernel.name);
+ const struct filter_window *t_window = mp_find_filter_window(conf->window.name);
+ bool is_tscale = scaler->index == SCALER_TSCALE;
+ if (!t_kernel) {
+ const struct filter_window *window = mp_find_filter_window(conf->kernel.name);
+ if (window) {
+ bare_window = (struct filter_kernel) { .f = *window };
+ t_kernel = &bare_window;
+ }
+ }
+
+ scaler->conf = *conf;
+ scaler->conf.kernel.name = (char *)handle_scaler_opt(conf->kernel.name, is_tscale);
+ scaler->conf.window.name = t_window ? (char *)t_window->name : NULL;
+ scaler->scale_factor = scale_factor;
+ scaler->insufficient = false;
+ scaler->initialized = true;
+ if (!t_kernel)
+ return;
+
+ scaler->kernel_storage = *t_kernel;
+ scaler->kernel = &scaler->kernel_storage;
+
+ if (!t_window) {
+ // fall back to the scaler's default window if available
+ t_window = mp_find_filter_window(t_kernel->window);
+ }
+ if (t_window)
+ scaler->kernel->w = *t_window;
+
+ for (int n = 0; n < 2; n++) {
+ if (!isnan(conf->kernel.params[n]))
+ scaler->kernel->f.params[n] = conf->kernel.params[n];
+ if (!isnan(conf->window.params[n]))
+ scaler->kernel->w.params[n] = conf->window.params[n];
+ }
+
+ if (conf->kernel.blur > 0.0)
+ scaler->kernel->f.blur = conf->kernel.blur;
+ if (conf->window.blur > 0.0)
+ scaler->kernel->w.blur = conf->window.blur;
+
+ if (conf->kernel.taper > 0.0)
+ scaler->kernel->f.taper = conf->kernel.taper;
+ if (conf->window.taper > 0.0)
+ scaler->kernel->w.taper = conf->window.taper;
+
+ if (scaler->kernel->f.resizable && conf->radius > 0.0)
+ scaler->kernel->f.radius = conf->radius;
+
+ scaler->kernel->clamp = conf->clamp;
+ scaler->insufficient = !mp_init_filter(scaler->kernel, sizes, scale_factor);
+
+ int size = scaler->kernel->size;
+ int num_components = size > 2 ? 4 : size;
+ const struct ra_format *fmt = ra_find_float16_format(p->ra, num_components);
+ assert(fmt);
+
+ int width = (size + num_components - 1) / num_components; // round up
+ int stride = width * num_components;
+ assert(size <= stride);
+
+ static const int lut_size = 256;
+ float *weights = talloc_array(NULL, float, lut_size * stride);
+ mp_compute_lut(scaler->kernel, lut_size, stride, weights);
+
+ bool use_1d = scaler->kernel->polar && (p->ra->caps & RA_CAP_TEX_1D);
+
+ struct ra_tex_params lut_params = {
+ .dimensions = use_1d ? 1 : 2,
+ .w = use_1d ? lut_size : width,
+ .h = use_1d ? 1 : lut_size,
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .initial_data = weights,
+ };
+ scaler->lut = ra_tex_create(p->ra, &lut_params);
+
+ talloc_free(weights);
+
+ debug_check_gl(p, "after initializing scaler");
+}
+
+// Special helper for sampling from two separated stages
+static void pass_sample_separated(struct gl_video *p, struct image src,
+ struct scaler *scaler, int w, int h)
+{
+ // Separate the transformation into x and y components, per pass
+ struct gl_transform t_x = {
+ .m = {{src.transform.m[0][0], 0.0}, {src.transform.m[1][0], 1.0}},
+ .t = {src.transform.t[0], 0.0},
+ };
+ struct gl_transform t_y = {
+ .m = {{1.0, src.transform.m[0][1]}, {0.0, src.transform.m[1][1]}},
+ .t = {0.0, src.transform.t[1]},
+ };
+
+ // First pass (scale only in the y dir)
+ src.transform = t_y;
+ sampler_prelude(p->sc, pass_bind(p, src));
+ GLSLF("// first pass\n");
+ pass_sample_separated_gen(p->sc, scaler, 0, 1);
+ GLSLF("color *= %f;\n", src.multiplier);
+ finish_pass_tex(p, &scaler->sep_fbo, src.w, h);
+
+ // Second pass (scale only in the x dir)
+ src = image_wrap(scaler->sep_fbo, src.type, src.components);
+ src.transform = t_x;
+ pass_describe(p, "%s second pass", scaler->conf.kernel.name);
+ sampler_prelude(p->sc, pass_bind(p, src));
+ pass_sample_separated_gen(p->sc, scaler, 1, 0);
+}
+
+// Picks either the compute shader version or the regular sampler version
+// depending on hardware support
+static void pass_dispatch_sample_polar(struct gl_video *p, struct scaler *scaler,
+ struct image img, int w, int h)
+{
+ uint64_t reqs = RA_CAP_COMPUTE;
+ if ((p->ra->caps & reqs) != reqs)
+ goto fallback;
+
+ int bound = ceil(scaler->kernel->radius_cutoff);
+ int offset = bound - 1; // padding top/left
+ int padding = offset + bound; // total padding
+
+ float ratiox = (float)w / img.w,
+ ratioy = (float)h / img.h;
+
+ // For performance we want to load at least as many pixels
+ // horizontally as there are threads in a warp (32 for nvidia), as
+ // well as enough to take advantage of shmem parallelism
+ const int warp_size = 32, threads = 256;
+ int bw = warp_size;
+ int bh = threads / bw;
+
+ // We need to sample everything from base_min to base_max, so make sure
+ // we have enough room in shmem
+ int iw = (int)ceil(bw / ratiox) + padding + 1,
+ ih = (int)ceil(bh / ratioy) + padding + 1;
+
+ int shmem_req = iw * ih * img.components * sizeof(float);
+ if (shmem_req > p->ra->max_shmem)
+ goto fallback;
+
+ pass_is_compute(p, bw, bh, false);
+ pass_compute_polar(p->sc, scaler, img.components, bw, bh, iw, ih);
+ return;
+
+fallback:
+ // Fall back to regular polar shader when compute shaders are unsupported
+ // or the kernel is too big for shmem
+ pass_sample_polar(p->sc, scaler, img.components,
+ p->ra->caps & RA_CAP_GATHER);
+}
+
+// Sample from image, with the src rectangle given by it.
+// The dst rectangle is implicit by what the caller will do next, but w and h
+// must still be what is going to be used (to dimension FBOs correctly).
+// This will write the scaled contents to the vec4 "color".
+// The scaler unit is initialized by this function; in order to avoid cache
+// thrashing, the scaler unit should usually use the same parameters.
+static void pass_sample(struct gl_video *p, struct image img,
+ struct scaler *scaler, const struct scaler_config *conf,
+ double scale_factor, int w, int h)
+{
+ reinit_scaler(p, scaler, conf, scale_factor, filter_sizes);
+
+ // Describe scaler
+ const char *scaler_opt[] = {
+ [SCALER_SCALE] = "scale",
+ [SCALER_DSCALE] = "dscale",
+ [SCALER_CSCALE] = "cscale",
+ [SCALER_TSCALE] = "tscale",
+ };
+
+ pass_describe(p, "%s=%s (%s)", scaler_opt[scaler->index],
+ scaler->conf.kernel.name, plane_names[img.type]);
+
+ bool is_separated = scaler->kernel && !scaler->kernel->polar;
+
+ // Set up the transformation+prelude and bind the texture, for everything
+ // other than separated scaling (which does this in the subfunction)
+ if (!is_separated)
+ sampler_prelude(p->sc, pass_bind(p, img));
+
+ // Dispatch the scaler. They're all wildly different.
+ const char *name = scaler->conf.kernel.name;
+ if (strcmp(name, "bilinear") == 0) {
+ GLSL(color = texture(tex, pos);)
+ } else if (strcmp(name, "bicubic_fast") == 0) {
+ pass_sample_bicubic_fast(p->sc);
+ } else if (strcmp(name, "oversample") == 0) {
+ pass_sample_oversample(p->sc, scaler, w, h);
+ } else if (scaler->kernel && scaler->kernel->polar) {
+ pass_dispatch_sample_polar(p, scaler, img, w, h);
+ } else if (scaler->kernel) {
+ pass_sample_separated(p, img, scaler, w, h);
+ } else {
+ MP_ASSERT_UNREACHABLE(); // should never happen
+ }
+
+ // Apply any required multipliers. Separated scaling already does this in
+ // its first stage
+ if (!is_separated)
+ GLSLF("color *= %f;\n", img.multiplier);
+
+ // Micro-optimization: Avoid scaling unneeded channels
+ skip_unused(p, img.components);
+}
+
+// Returns true if two images are semantically equivalent (same metadata)
+static bool image_equiv(struct image a, struct image b)
+{
+ return a.type == b.type &&
+ a.components == b.components &&
+ a.multiplier == b.multiplier &&
+ a.tex->params.format == b.tex->params.format &&
+ a.tex->params.w == b.tex->params.w &&
+ a.tex->params.h == b.tex->params.h &&
+ a.w == b.w &&
+ a.h == b.h &&
+ gl_transform_eq(a.transform, b.transform);
+}
+
+static void deband_hook(struct gl_video *p, struct image img,
+ struct gl_transform *trans, void *priv)
+{
+ pass_describe(p, "debanding (%s)", plane_names[img.type]);
+ pass_sample_deband(p->sc, p->opts.deband_opts, &p->lfg,
+ p->image_params.color.gamma);
+}
+
+static void unsharp_hook(struct gl_video *p, struct image img,
+ struct gl_transform *trans, void *priv)
+{
+ pass_describe(p, "unsharp masking");
+ pass_sample_unsharp(p->sc, p->opts.unsharp);
+}
+
+struct szexp_ctx {
+ struct gl_video *p;
+ struct image img;
+};
+
+static bool szexp_lookup(void *priv, struct bstr var, float size[2])
+{
+ struct szexp_ctx *ctx = priv;
+ struct gl_video *p = ctx->p;
+
+ if (bstr_equals0(var, "NATIVE_CROPPED")) {
+ size[0] = (p->src_rect.x1 - p->src_rect.x0) * p->texture_offset.m[0][0];
+ size[1] = (p->src_rect.y1 - p->src_rect.y0) * p->texture_offset.m[1][1];
+ return true;
+ }
+
+ // The size of OUTPUT is determined. It could be useful for certain
+ // user shaders to skip passes.
+ if (bstr_equals0(var, "OUTPUT")) {
+ size[0] = p->dst_rect.x1 - p->dst_rect.x0;
+ size[1] = p->dst_rect.y1 - p->dst_rect.y0;
+ return true;
+ }
+
+ // HOOKED is a special case
+ if (bstr_equals0(var, "HOOKED")) {
+ size[0] = ctx->img.w;
+ size[1] = ctx->img.h;
+ return true;
+ }
+
+ for (int o = 0; o < p->num_saved_imgs; o++) {
+ if (bstr_equals0(var, p->saved_imgs[o].name)) {
+ size[0] = p->saved_imgs[o].img.w;
+ size[1] = p->saved_imgs[o].img.h;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool user_hook_cond(struct gl_video *p, struct image img, void *priv)
+{
+ struct gl_user_shader_hook *shader = priv;
+ assert(shader);
+
+ float res = false;
+ struct szexp_ctx ctx = {p, img};
+ eval_szexpr(p->log, &ctx, szexp_lookup, shader->cond, &res);
+ return res;
+}
+
+static void user_hook(struct gl_video *p, struct image img,
+ struct gl_transform *trans, void *priv)
+{
+ struct gl_user_shader_hook *shader = priv;
+ assert(shader);
+ load_shader(p, shader->pass_body);
+
+ pass_describe(p, "user shader: %.*s (%s)", BSTR_P(shader->pass_desc),
+ plane_names[img.type]);
+
+ if (shader->compute.active) {
+ p->pass_compute = shader->compute;
+ GLSLF("hook();\n");
+ } else {
+ GLSLF("color = hook();\n");
+ }
+
+ // Make sure we at least create a legal FBO on failure, since it's better
+ // to do this and display an error message than just crash OpenGL
+ float w = 1.0, h = 1.0;
+
+ eval_szexpr(p->log, &(struct szexp_ctx){p, img}, szexp_lookup, shader->width, &w);
+ eval_szexpr(p->log, &(struct szexp_ctx){p, img}, szexp_lookup, shader->height, &h);
+
+ *trans = (struct gl_transform){{{w / img.w, 0}, {0, h / img.h}}};
+ gl_transform_trans(shader->offset, trans);
+}
+
+static bool add_user_hook(void *priv, struct gl_user_shader_hook hook)
+{
+ struct gl_video *p = priv;
+ struct gl_user_shader_hook *copy = talloc_ptrtype(p, copy);
+ *copy = hook;
+
+ struct tex_hook texhook = {
+ .save_tex = bstrdup0(copy, hook.save_tex),
+ .components = hook.components,
+ .align_offset = hook.align_offset,
+ .hook = user_hook,
+ .cond = user_hook_cond,
+ .priv = copy,
+ };
+
+ for (int h = 0; h < SHADER_MAX_HOOKS; h++)
+ texhook.hook_tex[h] = bstrdup0(copy, hook.hook_tex[h]);
+ for (int h = 0; h < SHADER_MAX_BINDS; h++)
+ texhook.bind_tex[h] = bstrdup0(copy, hook.bind_tex[h]);
+
+ MP_TARRAY_APPEND(p, p->tex_hooks, p->num_tex_hooks, texhook);
+ return true;
+}
+
+static bool add_user_tex(void *priv, struct gl_user_shader_tex tex)
+{
+ struct gl_video *p = priv;
+
+ tex.tex = ra_tex_create(p->ra, &tex.params);
+ TA_FREEP(&tex.params.initial_data);
+
+ if (!tex.tex)
+ return false;
+
+ MP_TARRAY_APPEND(p, p->user_textures, p->num_user_textures, tex);
+ return true;
+}
+
+static void load_user_shaders(struct gl_video *p, char **shaders)
+{
+ if (!shaders)
+ return;
+
+ for (int n = 0; shaders[n] != NULL; n++) {
+ struct bstr file = load_cached_file(p, shaders[n]);
+ parse_user_shader(p->log, p->ra, file, p, add_user_hook, add_user_tex);
+ }
+}
+
+static void gl_video_setup_hooks(struct gl_video *p)
+{
+ gl_video_reset_hooks(p);
+
+ if (p->opts.deband) {
+ MP_TARRAY_APPEND(p, p->tex_hooks, p->num_tex_hooks, (struct tex_hook) {
+ .hook_tex = {"LUMA", "CHROMA", "RGB", "XYZ"},
+ .bind_tex = {"HOOKED"},
+ .hook = deband_hook,
+ });
+ }
+
+ if (p->opts.unsharp != 0.0) {
+ MP_TARRAY_APPEND(p, p->tex_hooks, p->num_tex_hooks, (struct tex_hook) {
+ .hook_tex = {"MAIN"},
+ .bind_tex = {"HOOKED"},
+ .hook = unsharp_hook,
+ });
+ }
+
+ load_user_shaders(p, p->opts.user_shaders);
+}
+
+// sample from video textures, set "color" variable to yuv value
+static void pass_read_video(struct gl_video *p)
+{
+ struct image img[4];
+ struct gl_transform offsets[4];
+ pass_get_images(p, &p->image, img, offsets);
+
+ // To keep the code as simple as possibly, we currently run all shader
+ // stages even if they would be unnecessary (e.g. no hooks for a texture).
+ // In the future, deferred image should optimize this away.
+
+ // Merge semantically identical textures. This loop is done from back
+ // to front so that merged textures end up in the right order while
+ // simultaneously allowing us to skip unnecessary merges
+ for (int n = 3; n >= 0; n--) {
+ if (img[n].type == PLANE_NONE)
+ continue;
+
+ int first = n;
+ int num = 0;
+
+ for (int i = 0; i < n; i++) {
+ if (image_equiv(img[n], img[i]) &&
+ gl_transform_eq(offsets[n], offsets[i]))
+ {
+ GLSLF("// merging plane %d ...\n", i);
+ copy_image(p, &num, img[i]);
+ first = MPMIN(first, i);
+ img[i] = (struct image){0};
+ }
+ }
+
+ if (num > 0) {
+ GLSLF("// merging plane %d ... into %d\n", n, first);
+ copy_image(p, &num, img[n]);
+ pass_describe(p, "merging planes");
+ finish_pass_tex(p, &p->merge_tex[n], img[n].w, img[n].h);
+ img[first] = image_wrap(p->merge_tex[n], img[n].type, num);
+ img[n] = (struct image){0};
+ }
+ }
+
+ // If any textures are still in integer format by this point, we need
+ // to introduce an explicit conversion pass to avoid breaking hooks/scaling
+ for (int n = 0; n < 4; n++) {
+ if (img[n].tex && img[n].tex->params.format->ctype == RA_CTYPE_UINT) {
+ GLSLF("// use_integer fix for plane %d\n", n);
+ copy_image(p, &(int){0}, img[n]);
+ pass_describe(p, "use_integer fix");
+ finish_pass_tex(p, &p->integer_tex[n], img[n].w, img[n].h);
+ img[n] = image_wrap(p->integer_tex[n], img[n].type,
+ img[n].components);
+ }
+ }
+
+ // The basic idea is we assume the rgb/luma texture is the "reference" and
+ // scale everything else to match, after all planes are finalized.
+ // We find the reference texture first, in order to maintain texture offset
+ // between hooks on different type of planes.
+ int reference_tex_num = 0;
+ for (int n = 0; n < 4; n++) {
+ switch (img[n].type) {
+ case PLANE_RGB:
+ case PLANE_XYZ:
+ case PLANE_LUMA: break;
+ default: continue;
+ }
+
+ reference_tex_num = n;
+ break;
+ }
+
+ // Dispatch the hooks for all of these textures, saving and perhaps
+ // modifying them in the process
+ for (int n = 0; n < 4; n++) {
+ const char *name;
+ switch (img[n].type) {
+ case PLANE_RGB: name = "RGB"; break;
+ case PLANE_LUMA: name = "LUMA"; break;
+ case PLANE_CHROMA: name = "CHROMA"; break;
+ case PLANE_ALPHA: name = "ALPHA"; break;
+ case PLANE_XYZ: name = "XYZ"; break;
+ default: continue;
+ }
+
+ img[n] = pass_hook(p, name, img[n], &offsets[n]);
+
+ if (reference_tex_num == n) {
+ // The reference texture is finalized now.
+ p->texture_w = img[n].w;
+ p->texture_h = img[n].h;
+ p->texture_offset = offsets[n];
+ }
+ }
+
+ // At this point all planes are finalized but they may not be at the
+ // required size yet. Furthermore, they may have texture offsets that
+ // require realignment.
+
+ // Compute the reference rect
+ struct mp_rect_f src = {0.0, 0.0, p->image_params.w, p->image_params.h};
+ struct mp_rect_f ref = src;
+ gl_transform_rect(p->texture_offset, &ref);
+
+ // Explicitly scale all of the textures that don't match
+ for (int n = 0; n < 4; n++) {
+ if (img[n].type == PLANE_NONE)
+ continue;
+
+ // If the planes are aligned identically, we will end up with the
+ // exact same source rectangle.
+ struct mp_rect_f rect = src;
+ gl_transform_rect(offsets[n], &rect);
+ if (mp_rect_f_seq(ref, rect))
+ continue;
+
+ // If the rectangles differ, then our planes have a different
+ // alignment and/or size. First of all, we have to compute the
+ // corrections required to meet the target rectangle
+ struct gl_transform fix = {
+ .m = {{(ref.x1 - ref.x0) / (rect.x1 - rect.x0), 0.0},
+ {0.0, (ref.y1 - ref.y0) / (rect.y1 - rect.y0)}},
+ .t = {ref.x0, ref.y0},
+ };
+
+ // Since the scale in texture space is different from the scale in
+ // absolute terms, we have to scale the coefficients down to be
+ // relative to the texture's physical dimensions and local offset
+ struct gl_transform scale = {
+ .m = {{(float)img[n].w / p->texture_w, 0.0},
+ {0.0, (float)img[n].h / p->texture_h}},
+ .t = {-rect.x0, -rect.y0},
+ };
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(double, scale.m[0][0], scale.m[1][1]);
+
+ gl_transform_trans(scale, &fix);
+
+ // Since the texture transform is a function of the texture coordinates
+ // to texture space, rather than the other way around, we have to
+ // actually apply the *inverse* of this. Fortunately, calculating
+ // the inverse is relatively easy here.
+ fix.m[0][0] = 1.0 / fix.m[0][0];
+ fix.m[1][1] = 1.0 / fix.m[1][1];
+ fix.t[0] = fix.m[0][0] * -fix.t[0];
+ fix.t[1] = fix.m[1][1] * -fix.t[1];
+ gl_transform_trans(fix, &img[n].transform);
+
+ int scaler_id = -1;
+ const char *name = NULL;
+ switch (img[n].type) {
+ case PLANE_RGB:
+ case PLANE_LUMA:
+ case PLANE_XYZ:
+ scaler_id = SCALER_SCALE;
+ // these aren't worth hooking, fringe hypothetical cases only
+ break;
+ case PLANE_CHROMA:
+ scaler_id = SCALER_CSCALE;
+ name = "CHROMA_SCALED";
+ break;
+ case PLANE_ALPHA:
+ // alpha always uses bilinear
+ name = "ALPHA_SCALED";
+ }
+
+ if (scaler_id < 0)
+ continue;
+
+ const struct scaler_config *conf = &p->opts.scaler[scaler_id];
+
+ if (scaler_id == SCALER_CSCALE && (!conf->kernel.name ||
+ !conf->kernel.name[0]))
+ {
+ conf = &p->opts.scaler[SCALER_SCALE];
+ }
+
+ struct scaler *scaler = &p->scaler[scaler_id];
+
+ // bilinear scaling is a free no-op thanks to GPU sampling
+ if (strcmp(conf->kernel.name, "bilinear") != 0) {
+ GLSLF("// upscaling plane %d\n", n);
+ pass_sample(p, img[n], scaler, conf, 1.0, p->texture_w, p->texture_h);
+ finish_pass_tex(p, &p->scale_tex[n], p->texture_w, p->texture_h);
+ img[n] = image_wrap(p->scale_tex[n], img[n].type, img[n].components);
+ }
+
+ // Run any post-scaling hooks
+ img[n] = pass_hook(p, name, img[n], NULL);
+ }
+
+ // All planes are of the same size and properly aligned at this point
+ pass_describe(p, "combining planes");
+ int coord = 0;
+ for (int i = 0; i < 4; i++) {
+ if (img[i].type != PLANE_NONE)
+ copy_image(p, &coord, img[i]);
+ }
+ p->components = coord;
+}
+
+// Utility function that simply binds a texture and reads from it, without any
+// transformations.
+static void pass_read_tex(struct gl_video *p, struct ra_tex *tex)
+{
+ struct image img = image_wrap(tex, PLANE_RGB, p->components);
+ copy_image(p, &(int){0}, img);
+}
+
+// yuv conversion, and any other conversions before main up/down-scaling
+static void pass_convert_yuv(struct gl_video *p)
+{
+ struct gl_shader_cache *sc = p->sc;
+
+ struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
+ cparams.gray = p->is_gray;
+ cparams.is_float = p->ra_format.component_type == RA_CTYPE_FLOAT;
+ mp_csp_set_image_params(&cparams, &p->image_params);
+ mp_csp_equalizer_state_get(p->video_eq, &cparams);
+ p->user_gamma = 1.0 / (cparams.gamma * p->opts.gamma);
+
+ pass_describe(p, "color conversion");
+
+ if (p->color_swizzle[0])
+ GLSLF("color = color.%s;\n", p->color_swizzle);
+
+ // Pre-colormatrix input gamma correction
+ if (cparams.color.space == MP_CSP_XYZ)
+ pass_linearize(p->sc, p->image_params.color.gamma);
+
+ // We always explicitly normalize the range in pass_read_video
+ cparams.input_bits = cparams.texture_bits = 0;
+
+ // Conversion to RGB. For RGB itself, this still applies e.g. brightness
+ // and contrast controls, or expansion of e.g. LSB-packed 10 bit data.
+ struct mp_cmat m = {{{0}}};
+ mp_get_csp_matrix(&cparams, &m);
+ gl_sc_uniform_mat3(sc, "colormatrix", true, &m.m[0][0]);
+ gl_sc_uniform_vec3(sc, "colormatrix_c", m.c);
+
+ GLSL(color.rgb = mat3(colormatrix) * color.rgb + colormatrix_c;)
+
+ if (cparams.color.space == MP_CSP_XYZ) {
+ pass_delinearize(p->sc, p->image_params.color.gamma);
+ // mp_get_csp_matrix implicitly converts XYZ to DCI-P3
+ p->image_params.color.space = MP_CSP_RGB;
+ p->image_params.color.primaries = MP_CSP_PRIM_DCI_P3;
+ }
+
+ if (p->image_params.color.space == MP_CSP_BT_2020_C) {
+ // Conversion for C'rcY'cC'bc via the BT.2020 CL system:
+ // C'bc = (B'-Y'c) / 1.9404 | C'bc <= 0
+ // = (B'-Y'c) / 1.5816 | C'bc > 0
+ //
+ // C'rc = (R'-Y'c) / 1.7184 | C'rc <= 0
+ // = (R'-Y'c) / 0.9936 | C'rc > 0
+ //
+ // as per the BT.2020 specification, table 4. This is a non-linear
+ // transformation because (constant) luminance receives non-equal
+ // contributions from the three different channels.
+ GLSLF("// constant luminance conversion \n"
+ "color.br = color.br * mix(vec2(1.5816, 0.9936), \n"
+ " vec2(1.9404, 1.7184), \n"
+ " %s(lessThanEqual(color.br, vec2(0))))\n"
+ " + color.gg; \n",
+ gl_sc_bvec(p->sc, 2));
+ // Expand channels to camera-linear light. This shader currently just
+ // assumes everything uses the BT.2020 12-bit gamma function, since the
+ // difference between 10 and 12-bit is negligible for anything other
+ // than 12-bit content.
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/4.5), \n"
+ " pow((color.rgb + vec3(0.0993))*vec3(1.0/1.0993), \n"
+ " vec3(1.0/0.45)), \n"
+ " %s(lessThanEqual(vec3(0.08145), color.rgb))); \n",
+ gl_sc_bvec(p->sc, 3));
+ // Calculate the green channel from the expanded RYcB
+ // The BT.2020 specification says Yc = 0.2627*R + 0.6780*G + 0.0593*B
+ GLSL(color.g = (color.g - 0.2627*color.r - 0.0593*color.b)*1.0/0.6780;)
+ // Recompress to receive the R'G'B' result, same as other systems
+ GLSLF("color.rgb = mix(color.rgb * vec3(4.5), \n"
+ " vec3(1.0993) * pow(color.rgb, vec3(0.45)) - vec3(0.0993), \n"
+ " %s(lessThanEqual(vec3(0.0181), color.rgb))); \n",
+ gl_sc_bvec(p->sc, 3));
+ }
+
+ p->components = 3;
+ if (!p->has_alpha || p->opts.alpha_mode == ALPHA_NO) {
+ GLSL(color.a = 1.0;)
+ } else if (p->image_params.alpha == MP_ALPHA_PREMUL) {
+ p->components = 4;
+ } else {
+ p->components = 4;
+ GLSL(color = vec4(color.rgb * color.a, color.a);) // straight -> premul
+ }
+}
+
+static void get_scale_factors(struct gl_video *p, bool transpose_rot, double xy[2])
+{
+ double target_w = p->src_rect.x1 - p->src_rect.x0;
+ double target_h = p->src_rect.y1 - p->src_rect.y0;
+ if (transpose_rot && p->image_params.rotate % 180 == 90)
+ MPSWAP(double, target_w, target_h);
+ xy[0] = (p->dst_rect.x1 - p->dst_rect.x0) / target_w;
+ xy[1] = (p->dst_rect.y1 - p->dst_rect.y0) / target_h;
+}
+
+// Cropping.
+static void compute_src_transform(struct gl_video *p, struct gl_transform *tr)
+{
+ float sx = (p->src_rect.x1 - p->src_rect.x0) / (float)p->texture_w,
+ sy = (p->src_rect.y1 - p->src_rect.y0) / (float)p->texture_h,
+ ox = p->src_rect.x0,
+ oy = p->src_rect.y0;
+ struct gl_transform transform = {{{sx, 0}, {0, sy}}, {ox, oy}};
+
+ gl_transform_trans(p->texture_offset, &transform);
+
+ *tr = transform;
+}
+
+// Takes care of the main scaling and pre/post-conversions
+static void pass_scale_main(struct gl_video *p)
+{
+ // Figure out the main scaler.
+ double xy[2];
+ get_scale_factors(p, true, xy);
+
+ // actual scale factor should be divided by the scale factor of prescaling.
+ xy[0] /= p->texture_offset.m[0][0];
+ xy[1] /= p->texture_offset.m[1][1];
+
+ // The calculation of scale factor involves 32-bit float(from gl_transform),
+ // use non-strict equality test to tolerate precision loss.
+ bool downscaling = xy[0] < 1.0 - FLT_EPSILON || xy[1] < 1.0 - FLT_EPSILON;
+ bool upscaling = !downscaling && (xy[0] > 1.0 + FLT_EPSILON ||
+ xy[1] > 1.0 + FLT_EPSILON);
+ double scale_factor = 1.0;
+
+ struct scaler *scaler = &p->scaler[SCALER_SCALE];
+ struct scaler_config scaler_conf = p->opts.scaler[SCALER_SCALE];
+ if (p->opts.scaler_resizes_only && !downscaling && !upscaling) {
+ scaler_conf.kernel.name = "bilinear";
+ // For scaler-resizes-only, we round the texture offset to
+ // the nearest round value in order to prevent ugly blurriness
+ // (in exchange for slightly shifting the image by up to half a
+ // subpixel)
+ p->texture_offset.t[0] = roundf(p->texture_offset.t[0]);
+ p->texture_offset.t[1] = roundf(p->texture_offset.t[1]);
+ }
+ if (downscaling && p->opts.scaler[SCALER_DSCALE].kernel.name) {
+ scaler_conf = p->opts.scaler[SCALER_DSCALE];
+ scaler = &p->scaler[SCALER_DSCALE];
+ }
+
+ // When requesting correct-downscaling and the clip is anamorphic, and
+ // because only a single scale factor is used for both axes, enable it only
+ // when both axes are downscaled, and use the milder of the factors to not
+ // end up with too much blur on one axis (even if we end up with sub-optimal
+ // scale factor on the other axis). This is better than not respecting
+ // correct scaling at all for anamorphic clips.
+ double f = MPMAX(xy[0], xy[1]);
+ if (p->opts.correct_downscaling && f < 1.0)
+ scale_factor = 1.0 / f;
+
+ // Pre-conversion, like linear light/sigmoidization
+ GLSLF("// scaler pre-conversion\n");
+ bool use_linear = false;
+ if (downscaling) {
+ use_linear = p->opts.linear_downscaling;
+
+ // Linear light downscaling results in nasty artifacts for HDR curves
+ // due to the potentially extreme brightness differences severely
+ // compounding any ringing. So just scale in gamma light instead.
+ if (mp_trc_is_hdr(p->image_params.color.gamma))
+ use_linear = false;
+ } else if (upscaling) {
+ use_linear = p->opts.linear_upscaling || p->opts.sigmoid_upscaling;
+ }
+
+ if (use_linear) {
+ p->use_linear = true;
+ pass_linearize(p->sc, p->image_params.color.gamma);
+ pass_opt_hook_point(p, "LINEAR", NULL);
+ }
+
+ bool use_sigmoid = use_linear && p->opts.sigmoid_upscaling && upscaling;
+ float sig_center, sig_slope, sig_offset, sig_scale;
+ if (use_sigmoid) {
+ // Coefficients for the sigmoidal transform are taken from the
+ // formula here: http://www.imagemagick.org/Usage/color_mods/#sigmoidal
+ sig_center = p->opts.sigmoid_center;
+ sig_slope = p->opts.sigmoid_slope;
+ // This function needs to go through (0,0) and (1,1) so we compute the
+ // values at 1 and 0, and then scale/shift them, respectively.
+ sig_offset = 1.0/(1+expf(sig_slope * sig_center));
+ sig_scale = 1.0/(1+expf(sig_slope * (sig_center-1))) - sig_offset;
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSLF("color.rgb = %f - log(1.0/(color.rgb * %f + %f) - 1.0) * 1.0/%f;\n",
+ sig_center, sig_scale, sig_offset, sig_slope);
+ pass_opt_hook_point(p, "SIGMOID", NULL);
+ }
+
+ pass_opt_hook_point(p, "PREKERNEL", NULL);
+
+ int vp_w = p->dst_rect.x1 - p->dst_rect.x0;
+ int vp_h = p->dst_rect.y1 - p->dst_rect.y0;
+ struct gl_transform transform;
+ compute_src_transform(p, &transform);
+
+ GLSLF("// main scaling\n");
+ finish_pass_tex(p, &p->indirect_tex, p->texture_w, p->texture_h);
+ struct image src = image_wrap(p->indirect_tex, PLANE_RGB, p->components);
+ gl_transform_trans(transform, &src.transform);
+ pass_sample(p, src, scaler, &scaler_conf, scale_factor, vp_w, vp_h);
+
+ // Changes the texture size to display size after main scaler.
+ p->texture_w = vp_w;
+ p->texture_h = vp_h;
+
+ pass_opt_hook_point(p, "POSTKERNEL", NULL);
+
+ GLSLF("// scaler post-conversion\n");
+ if (use_sigmoid) {
+ // Inverse of the transformation above
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSLF("color.rgb = (1.0/(1.0 + exp(%f * (%f - color.rgb))) - %f) * 1.0/%f;\n",
+ sig_slope, sig_center, sig_offset, sig_scale);
+ }
+}
+
+// Adapts the colors to the right output color space. (Final pass during
+// rendering)
+// If OSD is true, ignore any changes that may have been made to the video
+// by previous passes (i.e. linear scaling)
+static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
+ struct mp_colorspace fbo_csp, int flags, bool osd)
+{
+ struct ra *ra = p->ra;
+
+ // Configure the destination according to the FBO color space,
+ // unless specific transfer function, primaries or target peak
+ // is set. If values are set to _AUTO, the most likely intended
+ // values are guesstimated later in this function.
+ struct mp_colorspace dst = {
+ .gamma = p->opts.target_trc == MP_CSP_TRC_AUTO ?
+ fbo_csp.gamma : p->opts.target_trc,
+ .primaries = p->opts.target_prim == MP_CSP_PRIM_AUTO ?
+ fbo_csp.primaries : p->opts.target_prim,
+ .light = MP_CSP_LIGHT_DISPLAY,
+ .hdr.max_luma = !p->opts.target_peak ?
+ fbo_csp.hdr.max_luma : p->opts.target_peak,
+ };
+
+ if (!p->colorspace_override_warned &&
+ ((fbo_csp.gamma && dst.gamma != fbo_csp.gamma) ||
+ (fbo_csp.primaries && dst.primaries != fbo_csp.primaries)))
+ {
+ MP_WARN(p, "One or more colorspace value is being overridden "
+ "by user while the FBO provides colorspace information: "
+ "transfer function: (dst: %s, fbo: %s), "
+ "primaries: (dst: %s, fbo: %s). "
+ "Rendering can lead to incorrect results!\n",
+ m_opt_choice_str(mp_csp_trc_names, dst.gamma),
+ m_opt_choice_str(mp_csp_trc_names, fbo_csp.gamma),
+ m_opt_choice_str(mp_csp_prim_names, dst.primaries),
+ m_opt_choice_str(mp_csp_prim_names, fbo_csp.primaries));
+ p->colorspace_override_warned = true;
+ }
+
+ if (dst.gamma == MP_CSP_TRC_HLG)
+ dst.light = MP_CSP_LIGHT_SCENE_HLG;
+
+ if (p->use_lut_3d && (flags & RENDER_SCREEN_COLOR)) {
+ // The 3DLUT is always generated against the video's original source
+ // space, *not* the reference space. (To avoid having to regenerate
+ // the 3DLUT for the OSD on every frame)
+ enum mp_csp_prim prim_orig = p->image_params.color.primaries;
+ enum mp_csp_trc trc_orig = p->image_params.color.gamma;
+
+ // One exception: HDR is not implemented by LittleCMS for technical
+ // limitation reasons, so we use a gamma 2.2 input curve here instead.
+ // We could pick any value we want here, the difference is just coding
+ // efficiency.
+ if (mp_trc_is_hdr(trc_orig))
+ trc_orig = MP_CSP_TRC_GAMMA22;
+
+ if (gl_video_get_lut3d(p, prim_orig, trc_orig)) {
+ dst.primaries = prim_orig;
+ dst.gamma = trc_orig;
+ assert(dst.primaries && dst.gamma);
+ }
+ }
+
+ if (dst.primaries == MP_CSP_PRIM_AUTO) {
+ // The vast majority of people are on sRGB or BT.709 displays, so pick
+ // this as the default output color space.
+ dst.primaries = MP_CSP_PRIM_BT_709;
+
+ if (src.primaries == MP_CSP_PRIM_BT_601_525 ||
+ src.primaries == MP_CSP_PRIM_BT_601_625)
+ {
+ // Since we auto-pick BT.601 and BT.709 based on the dimensions,
+ // combined with the fact that they're very similar to begin with,
+ // and to avoid confusing the average user, just don't adapt BT.601
+ // content automatically at all.
+ dst.primaries = src.primaries;
+ }
+ }
+
+ if (dst.gamma == MP_CSP_TRC_AUTO) {
+ // Most people seem to complain when the image is darker or brighter
+ // than what they're "used to", so just avoid changing the gamma
+ // altogether by default. The only exceptions to this rule apply to
+ // very unusual TRCs, which even hardcode technoluddites would probably
+ // not enjoy viewing unaltered.
+ dst.gamma = src.gamma;
+
+ // Avoid outputting linear light or HDR content "by default". For these
+ // just pick gamma 2.2 as a default, since it's a good estimate for
+ // the response of typical displays
+ if (dst.gamma == MP_CSP_TRC_LINEAR || mp_trc_is_hdr(dst.gamma))
+ dst.gamma = MP_CSP_TRC_GAMMA22;
+ }
+
+ // If there's no specific signal peak known for the output display, infer
+ // it from the chosen transfer function. Also normalize the src peak, in
+ // case it was unknown
+ if (!dst.hdr.max_luma)
+ dst.hdr.max_luma = mp_trc_nom_peak(dst.gamma) * MP_REF_WHITE;
+ if (!src.hdr.max_luma)
+ src.hdr.max_luma = mp_trc_nom_peak(src.gamma) * MP_REF_WHITE;
+
+ // Whitelist supported modes
+ switch (p->opts.tone_map.curve) {
+ case TONE_MAPPING_AUTO:
+ case TONE_MAPPING_CLIP:
+ case TONE_MAPPING_MOBIUS:
+ case TONE_MAPPING_REINHARD:
+ case TONE_MAPPING_HABLE:
+ case TONE_MAPPING_GAMMA:
+ case TONE_MAPPING_LINEAR:
+ case TONE_MAPPING_BT_2390:
+ break;
+ default:
+ MP_WARN(p, "Tone mapping curve unsupported by vo_gpu, falling back.\n");
+ p->opts.tone_map.curve = TONE_MAPPING_AUTO;
+ break;
+ }
+
+ switch (p->opts.tone_map.gamut_mode) {
+ case GAMUT_AUTO:
+ case GAMUT_WARN:
+ case GAMUT_CLIP:
+ case GAMUT_DESATURATE:
+ break;
+ default:
+ MP_WARN(p, "Gamut mapping mode unsupported by vo_gpu, falling back.\n");
+ p->opts.tone_map.gamut_mode = GAMUT_AUTO;
+ break;
+ }
+
+ struct gl_tone_map_opts tone_map = p->opts.tone_map;
+ bool detect_peak = tone_map.compute_peak >= 0 && mp_trc_is_hdr(src.gamma)
+ && src.hdr.max_luma > dst.hdr.max_luma;
+
+ if (detect_peak && !p->hdr_peak_ssbo) {
+ struct {
+ float average[2];
+ int32_t frame_sum;
+ uint32_t frame_max;
+ uint32_t counter;
+ } peak_ssbo = {0};
+
+ struct ra_buf_params params = {
+ .type = RA_BUF_TYPE_SHADER_STORAGE,
+ .size = sizeof(peak_ssbo),
+ .initial_data = &peak_ssbo,
+ };
+
+ p->hdr_peak_ssbo = ra_buf_create(ra, &params);
+ if (!p->hdr_peak_ssbo) {
+ MP_WARN(p, "Failed to create HDR peak detection SSBO, disabling.\n");
+ tone_map.compute_peak = p->opts.tone_map.compute_peak = -1;
+ detect_peak = false;
+ }
+ }
+
+ if (detect_peak) {
+ pass_describe(p, "detect HDR peak");
+ pass_is_compute(p, 8, 8, true); // 8x8 is good for performance
+ gl_sc_ssbo(p->sc, "PeakDetect", p->hdr_peak_ssbo,
+ "vec2 average;"
+ "int frame_sum;"
+ "uint frame_max;"
+ "uint counter;"
+ );
+ } else {
+ tone_map.compute_peak = -1;
+ }
+
+ // Adapt from src to dst as necessary
+ pass_color_map(p->sc, p->use_linear && !osd, src, dst, &tone_map);
+
+ if (p->use_lut_3d && (flags & RENDER_SCREEN_COLOR)) {
+ gl_sc_uniform_texture(p->sc, "lut_3d", p->lut_3d_texture);
+ GLSL(vec3 cpos;)
+ for (int i = 0; i < 3; i++)
+ GLSLF("cpos[%d] = LUT_POS(color[%d], %d.0);\n", i, i, p->lut_3d_size[i]);
+ GLSL(color.rgb = tex3D(lut_3d, cpos).rgb;)
+ }
+}
+
+void gl_video_set_fb_depth(struct gl_video *p, int fb_depth)
+{
+ p->fb_depth = fb_depth;
+}
+
+static void pass_dither(struct gl_video *p)
+{
+ // Assume 8 bits per component if unknown.
+ int dst_depth = p->fb_depth > 0 ? p->fb_depth : 8;
+ if (p->opts.dither_depth > 0)
+ dst_depth = p->opts.dither_depth;
+
+ if (p->opts.dither_depth < 0 || p->opts.dither_algo == DITHER_NONE)
+ return;
+
+ if (p->opts.dither_algo == DITHER_ERROR_DIFFUSION) {
+ const struct error_diffusion_kernel *kernel =
+ mp_find_error_diffusion_kernel(p->opts.error_diffusion);
+ int o_w = p->dst_rect.x1 - p->dst_rect.x0,
+ o_h = p->dst_rect.y1 - p->dst_rect.y0;
+
+ int shmem_req = mp_ef_compute_shared_memory_size(kernel, o_h);
+ if (shmem_req > p->ra->max_shmem) {
+ MP_WARN(p, "Fallback to dither=fruit because there is no enough "
+ "shared memory (%d/%d).\n",
+ shmem_req, (int)p->ra->max_shmem);
+ p->opts.dither_algo = DITHER_FRUIT;
+ } else {
+ finish_pass_tex(p, &p->error_diffusion_tex[0], o_w, o_h);
+
+ struct image img = image_wrap(p->error_diffusion_tex[0], PLANE_RGB, p->components);
+
+ // Ensure the block size doesn't exceed the maximum of the
+ // implementation.
+ int block_size = MPMIN(p->ra->max_compute_group_threads, o_h);
+
+ pass_describe(p, "dither=error-diffusion (kernel=%s, depth=%d)",
+ kernel->name, dst_depth);
+
+ p->pass_compute = (struct compute_info) {
+ .active = true,
+ .threads_w = block_size,
+ .threads_h = 1,
+ .directly_writes = true
+ };
+
+ int tex_id = pass_bind(p, img);
+
+ pass_error_diffusion(p->sc, kernel, tex_id, o_w, o_h,
+ dst_depth, block_size);
+
+ finish_pass_tex(p, &p->error_diffusion_tex[1], o_w, o_h);
+
+ img = image_wrap(p->error_diffusion_tex[1], PLANE_RGB, p->components);
+ copy_image(p, &(int){0}, img);
+
+ return;
+ }
+ }
+
+ if (!p->dither_texture) {
+ MP_VERBOSE(p, "Dither to %d.\n", dst_depth);
+
+ int tex_size = 0;
+ void *tex_data = NULL;
+ const struct ra_format *fmt = NULL;
+ void *temp = NULL;
+
+ if (p->opts.dither_algo == DITHER_FRUIT) {
+ int sizeb = p->opts.dither_size;
+ int size = 1 << sizeb;
+
+ if (p->last_dither_matrix_size != size) {
+ p->last_dither_matrix = talloc_realloc(p, p->last_dither_matrix,
+ float, size * size);
+ mp_make_fruit_dither_matrix(p->last_dither_matrix, sizeb);
+ p->last_dither_matrix_size = size;
+ }
+
+ // Prefer R16 texture since they provide higher precision.
+ fmt = ra_find_unorm_format(p->ra, 2, 1);
+ if (!fmt)
+ fmt = ra_find_float16_format(p->ra, 1);
+ if (fmt) {
+ tex_size = size;
+ tex_data = p->last_dither_matrix;
+ if (fmt->ctype == RA_CTYPE_UNORM) {
+ uint16_t *t = temp = talloc_array(NULL, uint16_t, size * size);
+ for (int n = 0; n < size * size; n++)
+ t[n] = p->last_dither_matrix[n] * UINT16_MAX;
+ tex_data = t;
+ }
+ } else {
+ MP_VERBOSE(p, "GL too old. Falling back to ordered dither.\n");
+ p->opts.dither_algo = DITHER_ORDERED;
+ }
+ }
+
+ if (p->opts.dither_algo == DITHER_ORDERED) {
+ temp = talloc_array(NULL, char, 8 * 8);
+ mp_make_ordered_dither_matrix(temp, 8);
+
+ fmt = ra_find_unorm_format(p->ra, 1, 1);
+ tex_size = 8;
+ tex_data = temp;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = tex_size,
+ .h = tex_size,
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_repeat = true,
+ .initial_data = tex_data,
+ };
+ p->dither_texture = ra_tex_create(p->ra, &params);
+
+ debug_check_gl(p, "dither setup");
+
+ talloc_free(temp);
+
+ if (!p->dither_texture)
+ return;
+ }
+
+ GLSLF("// dithering\n");
+
+ // This defines how many bits are considered significant for output on
+ // screen. The superfluous bits will be used for rounding according to the
+ // dither matrix. The precision of the source implicitly decides how many
+ // dither patterns can be visible.
+ int dither_quantization = (1 << dst_depth) - 1;
+ int dither_size = p->dither_texture->params.w;
+
+ gl_sc_uniform_texture(p->sc, "dither", p->dither_texture);
+
+ GLSLF("vec2 dither_pos = gl_FragCoord.xy * 1.0/%d.0;\n", dither_size);
+
+ if (p->opts.temporal_dither) {
+ int phase = (p->frames_rendered / p->opts.temporal_dither_period) % 8u;
+ float r = phase * (M_PI / 2); // rotate
+ float m = phase < 4 ? 1 : -1; // mirror
+
+ float matrix[2][2] = {{cos(r), -sin(r) },
+ {sin(r) * m, cos(r) * m}};
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_mat2(p->sc, "dither_trafo", true, &matrix[0][0]);
+
+ GLSL(dither_pos = dither_trafo * dither_pos;)
+ }
+
+ GLSL(float dither_value = texture(dither, dither_pos).r;)
+ GLSLF("color = floor(color * %d.0 + dither_value + 0.5 / %d.0) * 1.0/%d.0;\n",
+ dither_quantization, dither_size * dither_size, dither_quantization);
+}
+
+// Draws the OSD, in scene-referred colors.. If cms is true, subtitles are
+// instead adapted to the display's gamut.
+static void pass_draw_osd(struct gl_video *p, int osd_flags, int frame_flags,
+ double pts, struct mp_osd_res rect, struct ra_fbo fbo,
+ bool cms)
+{
+ if (frame_flags & RENDER_FRAME_VF_SUBS)
+ osd_flags |= OSD_DRAW_SUB_FILTER;
+
+ if ((osd_flags & OSD_DRAW_SUB_ONLY) && (osd_flags & OSD_DRAW_OSD_ONLY))
+ return;
+
+ mpgl_osd_generate(p->osd, rect, pts, p->image_params.stereo3d, osd_flags);
+
+ timer_pool_start(p->osd_timer);
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ // (This returns false if this part is empty with nothing to draw.)
+ if (!mpgl_osd_draw_prepare(p->osd, n, p->sc))
+ continue;
+ // When subtitles need to be color managed, assume they're in sRGB
+ // (for lack of anything saner to do)
+ if (cms) {
+ static const struct mp_colorspace csp_srgb = {
+ .primaries = MP_CSP_PRIM_BT_709,
+ .gamma = MP_CSP_TRC_SRGB,
+ .light = MP_CSP_LIGHT_DISPLAY,
+ };
+
+ pass_colormanage(p, csp_srgb, fbo.color_space, frame_flags, true);
+ }
+ mpgl_osd_draw_finish(p->osd, n, p->sc, fbo);
+ }
+
+ timer_pool_stop(p->osd_timer);
+ pass_describe(p, "drawing osd");
+ pass_record(p, timer_pool_measure(p->osd_timer));
+}
+
+static float chroma_realign(int size, int pixel)
+{
+ return size / (float)chroma_upsize(size, pixel);
+}
+
+// Minimal rendering code path, for GLES or OpenGL 2.1 without proper FBOs.
+static void pass_render_frame_dumb(struct gl_video *p)
+{
+ struct image img[4];
+ struct gl_transform off[4];
+ pass_get_images(p, &p->image, img, off);
+
+ struct gl_transform transform;
+ compute_src_transform(p, &transform);
+
+ int index = 0;
+ for (int i = 0; i < p->plane_count; i++) {
+ int cw = img[i].type == PLANE_CHROMA ? p->ra_format.chroma_w : 1;
+ int ch = img[i].type == PLANE_CHROMA ? p->ra_format.chroma_h : 1;
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(int, cw, ch);
+
+ struct gl_transform t = transform;
+ t.m[0][0] *= chroma_realign(p->texture_w, cw);
+ t.m[1][1] *= chroma_realign(p->texture_h, ch);
+
+ t.t[0] /= cw;
+ t.t[1] /= ch;
+
+ t.t[0] += off[i].t[0];
+ t.t[1] += off[i].t[1];
+
+ gl_transform_trans(img[i].transform, &t);
+ img[i].transform = t;
+
+ copy_image(p, &index, img[i]);
+ }
+
+ pass_convert_yuv(p);
+}
+
+// The main rendering function, takes care of everything up to and including
+// upscaling. p->image is rendered.
+// flags: bit set of RENDER_FRAME_* flags
+static bool pass_render_frame(struct gl_video *p, struct mp_image *mpi,
+ uint64_t id, int flags)
+{
+ // initialize the texture parameters and temporary variables
+ p->texture_w = p->image_params.w;
+ p->texture_h = p->image_params.h;
+ p->texture_offset = identity_trans;
+ p->components = 0;
+ p->num_saved_imgs = 0;
+ p->idx_hook_textures = 0;
+ p->use_linear = false;
+
+ // try uploading the frame
+ if (!pass_upload_image(p, mpi, id))
+ return false;
+
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(int, p->texture_w, p->texture_h);
+
+ if (p->dumb_mode)
+ return true;
+
+ pass_read_video(p);
+ pass_opt_hook_point(p, "NATIVE", &p->texture_offset);
+ pass_convert_yuv(p);
+ pass_opt_hook_point(p, "MAINPRESUB", &p->texture_offset);
+
+ // For subtitles
+ double vpts = p->image.mpi->pts;
+ if (vpts == MP_NOPTS_VALUE)
+ vpts = p->osd_pts;
+
+ if (p->osd && p->opts.blend_subs == BLEND_SUBS_VIDEO &&
+ (flags & RENDER_FRAME_SUBS))
+ {
+ double scale[2];
+ get_scale_factors(p, false, scale);
+ struct mp_osd_res rect = {
+ .w = p->texture_w, .h = p->texture_h,
+ .display_par = scale[1] / scale[0], // counter compensate scaling
+ };
+ finish_pass_tex(p, &p->blend_subs_tex, rect.w, rect.h);
+ struct ra_fbo fbo = { p->blend_subs_tex };
+ pass_draw_osd(p, OSD_DRAW_SUB_ONLY, flags, vpts, rect, fbo, false);
+ pass_read_tex(p, p->blend_subs_tex);
+ pass_describe(p, "blend subs video");
+ }
+ pass_opt_hook_point(p, "MAIN", &p->texture_offset);
+
+ pass_scale_main(p);
+
+ int vp_w = p->dst_rect.x1 - p->dst_rect.x0,
+ vp_h = p->dst_rect.y1 - p->dst_rect.y0;
+ if (p->osd && p->opts.blend_subs == BLEND_SUBS_YES &&
+ (flags & RENDER_FRAME_SUBS))
+ {
+ // Recreate the real video size from the src/dst rects
+ struct mp_osd_res rect = {
+ .w = vp_w, .h = vp_h,
+ .ml = -p->src_rect.x0, .mr = p->src_rect.x1 - p->image_params.w,
+ .mt = -p->src_rect.y0, .mb = p->src_rect.y1 - p->image_params.h,
+ .display_par = 1.0,
+ };
+ // Adjust margins for scale
+ double scale[2];
+ get_scale_factors(p, true, scale);
+ rect.ml *= scale[0]; rect.mr *= scale[0];
+ rect.mt *= scale[1]; rect.mb *= scale[1];
+ // We should always blend subtitles in non-linear light
+ if (p->use_linear) {
+ pass_delinearize(p->sc, p->image_params.color.gamma);
+ p->use_linear = false;
+ }
+ finish_pass_tex(p, &p->blend_subs_tex, p->texture_w, p->texture_h);
+ struct ra_fbo fbo = { p->blend_subs_tex };
+ pass_draw_osd(p, OSD_DRAW_SUB_ONLY, flags, vpts, rect, fbo, false);
+ pass_read_tex(p, p->blend_subs_tex);
+ pass_describe(p, "blend subs");
+ }
+
+ pass_opt_hook_point(p, "SCALED", NULL);
+
+ return true;
+}
+
+static void pass_draw_to_screen(struct gl_video *p, struct ra_fbo fbo, int flags)
+{
+ if (p->dumb_mode)
+ pass_render_frame_dumb(p);
+
+ // Adjust the overall gamma before drawing to screen
+ if (p->user_gamma != 1) {
+ gl_sc_uniform_f(p->sc, "user_gamma", p->user_gamma);
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSL(color.rgb = pow(color.rgb, vec3(user_gamma));)
+ }
+
+ pass_colormanage(p, p->image_params.color, fbo.color_space, flags, false);
+
+ // Since finish_pass_fbo doesn't work with compute shaders, and neither
+ // does the checkerboard/dither code, we may need an indirection via
+ // p->screen_tex here.
+ if (p->pass_compute.active) {
+ int o_w = p->dst_rect.x1 - p->dst_rect.x0,
+ o_h = p->dst_rect.y1 - p->dst_rect.y0;
+ finish_pass_tex(p, &p->screen_tex, o_w, o_h);
+ struct image tmp = image_wrap(p->screen_tex, PLANE_RGB, p->components);
+ copy_image(p, &(int){0}, tmp);
+ }
+
+ if (p->has_alpha){
+ if (p->opts.alpha_mode == ALPHA_BLEND_TILES) {
+ // Draw checkerboard pattern to indicate transparency
+ GLSLF("// transparency checkerboard\n");
+ GLSL(bvec2 tile = lessThan(fract(gl_FragCoord.xy * 1.0/32.0), vec2(0.5));)
+ GLSL(vec3 background = vec3(tile.x == tile.y ? 0.93 : 0.87);)
+ GLSL(color.rgb += background.rgb * (1.0 - color.a);)
+ GLSL(color.a = 1.0;)
+ } else if (p->opts.alpha_mode == ALPHA_BLEND) {
+ // Blend into background color (usually black)
+ struct m_color c = p->opts.background;
+ GLSLF("vec4 background = vec4(%f, %f, %f, %f);\n",
+ c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
+ GLSL(color.rgb += background.rgb * (1.0 - color.a);)
+ GLSL(color.a = background.a;)
+ }
+ }
+
+ pass_opt_hook_point(p, "OUTPUT", NULL);
+
+ if (flags & RENDER_SCREEN_COLOR)
+ pass_dither(p);
+ pass_describe(p, "output to screen");
+ finish_pass_fbo(p, fbo, false, &p->dst_rect);
+}
+
+// flags: bit set of RENDER_FRAME_* flags
+static bool update_surface(struct gl_video *p, struct mp_image *mpi,
+ uint64_t id, struct surface *surf, int flags)
+{
+ int vp_w = p->dst_rect.x1 - p->dst_rect.x0,
+ vp_h = p->dst_rect.y1 - p->dst_rect.y0;
+
+ pass_info_reset(p, false);
+ if (!pass_render_frame(p, mpi, id, flags))
+ return false;
+
+ // Frame blending should always be done in linear light to preserve the
+ // overall brightness, otherwise this will result in flashing dark frames
+ // because mixing in compressed light artificially darkens the results
+ if (!p->use_linear) {
+ p->use_linear = true;
+ pass_linearize(p->sc, p->image_params.color.gamma);
+ }
+
+ finish_pass_tex(p, &surf->tex, vp_w, vp_h);
+ surf->id = id;
+ surf->pts = mpi->pts;
+ return true;
+}
+
+// Draws an interpolate frame to fbo, based on the frame timing in t
+// flags: bit set of RENDER_FRAME_* flags
+static void gl_video_interpolate_frame(struct gl_video *p, struct vo_frame *t,
+ struct ra_fbo fbo, int flags)
+{
+ bool is_new = false;
+
+ // Reset the queue completely if this is a still image, to avoid any
+ // interpolation artifacts from surrounding frames when unpausing or
+ // framestepping
+ if (t->still)
+ gl_video_reset_surfaces(p);
+
+ // First of all, figure out if we have a frame available at all, and draw
+ // it manually + reset the queue if not
+ if (p->surfaces[p->surface_now].id == 0) {
+ struct surface *now = &p->surfaces[p->surface_now];
+ if (!update_surface(p, t->current, t->frame_id, now, flags))
+ return;
+ p->surface_idx = p->surface_now;
+ is_new = true;
+ }
+
+ // Find the right frame for this instant
+ if (t->current) {
+ int next = surface_wrap(p->surface_now + 1);
+ while (p->surfaces[next].id &&
+ p->surfaces[next].id > p->surfaces[p->surface_now].id &&
+ p->surfaces[p->surface_now].id < t->frame_id)
+ {
+ p->surface_now = next;
+ next = surface_wrap(next + 1);
+ }
+ }
+
+ // Figure out the queue size. For illustration, a filter radius of 2 would
+ // look like this: _ A [B] C D _
+ // A is surface_bse, B is surface_now, C is surface_now+1 and D is
+ // surface_end.
+ struct scaler *tscale = &p->scaler[SCALER_TSCALE];
+ reinit_scaler(p, tscale, &p->opts.scaler[SCALER_TSCALE], 1, tscale_sizes);
+ bool oversample = strcmp(tscale->conf.kernel.name, "oversample") == 0;
+ bool linear = strcmp(tscale->conf.kernel.name, "linear") == 0;
+ int size;
+
+ if (oversample || linear) {
+ size = 2;
+ } else {
+ assert(tscale->kernel && !tscale->kernel->polar);
+ size = ceil(tscale->kernel->size);
+ }
+
+ int radius = size/2;
+ int surface_now = p->surface_now;
+ int surface_bse = surface_wrap(surface_now - (radius-1));
+ int surface_end = surface_wrap(surface_now + radius);
+ assert(surface_wrap(surface_bse + size-1) == surface_end);
+
+ // Render new frames while there's room in the queue. Note that technically,
+ // this should be done before the step where we find the right frame, but
+ // it only barely matters at the very beginning of playback, and this way
+ // makes the code much more linear.
+ int surface_dst = surface_wrap(p->surface_idx + 1);
+ for (int i = 0; i < t->num_frames; i++) {
+ // Avoid overwriting data we might still need
+ if (surface_dst == surface_bse - 1)
+ break;
+
+ struct mp_image *f = t->frames[i];
+ uint64_t f_id = t->frame_id + i;
+ if (!mp_image_params_equal(&f->params, &p->real_image_params))
+ continue;
+
+ if (f_id > p->surfaces[p->surface_idx].id) {
+ struct surface *dst = &p->surfaces[surface_dst];
+ if (!update_surface(p, f, f_id, dst, flags))
+ return;
+ p->surface_idx = surface_dst;
+ surface_dst = surface_wrap(surface_dst + 1);
+ is_new = true;
+ }
+ }
+
+ // Figure out whether the queue is "valid". A queue is invalid if the
+ // frames' PTS is not monotonically increasing. Anything else is invalid,
+ // so avoid blending incorrect data and just draw the latest frame as-is.
+ // Possible causes for failure of this condition include seeks, pausing,
+ // end of playback or start of playback.
+ bool valid = true;
+ for (int i = surface_bse, ii; valid && i != surface_end; i = ii) {
+ ii = surface_wrap(i + 1);
+ if (p->surfaces[i].id == 0 || p->surfaces[ii].id == 0) {
+ valid = false;
+ } else if (p->surfaces[ii].id < p->surfaces[i].id) {
+ valid = false;
+ MP_DBG(p, "interpolation queue underrun\n");
+ }
+ }
+
+ // Update OSD PTS to synchronize subtitles with the displayed frame
+ p->osd_pts = p->surfaces[surface_now].pts;
+
+ // Finally, draw the right mix of frames to the screen.
+ if (!is_new)
+ pass_info_reset(p, true);
+ pass_describe(p, "interpolation");
+ if (!valid || t->still) {
+ // surface_now is guaranteed to be valid, so we can safely use it.
+ pass_read_tex(p, p->surfaces[surface_now].tex);
+ p->is_interpolated = false;
+ } else {
+ double mix = t->vsync_offset / t->ideal_frame_duration;
+ // The scaler code always wants the fcoord to be between 0 and 1,
+ // so we try to adjust by using the previous set of N frames instead
+ // (which requires some extra checking to make sure it's valid)
+ if (mix < 0.0) {
+ int prev = surface_wrap(surface_bse - 1);
+ if (p->surfaces[prev].id != 0 &&
+ p->surfaces[prev].id < p->surfaces[surface_bse].id)
+ {
+ mix += 1.0;
+ surface_bse = prev;
+ } else {
+ mix = 0.0; // at least don't blow up, this should only
+ // ever happen at the start of playback
+ }
+ }
+
+ if (oversample) {
+ // Oversample uses the frame area as mix ratio, not the vsync
+ // position itself
+ double vsync_dist = t->vsync_interval / t->ideal_frame_duration,
+ threshold = tscale->conf.kernel.params[0];
+ threshold = isnan(threshold) ? 0.0 : threshold;
+ mix = (1 - mix) / vsync_dist;
+ mix = mix <= 0 + threshold ? 0 : mix;
+ mix = mix >= 1 - threshold ? 1 : mix;
+ mix = 1 - mix;
+ }
+
+ // Blend the frames together
+ if (oversample || linear) {
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_f(p->sc, "inter_coeff", mix);
+ GLSL(color = mix(texture(texture0, texcoord0),
+ texture(texture1, texcoord1),
+ inter_coeff);)
+ } else {
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_f(p->sc, "fcoord", mix);
+ pass_sample_separated_gen(p->sc, tscale, 0, 0);
+ }
+
+ // Load all the required frames
+ for (int i = 0; i < size; i++) {
+ struct image img =
+ image_wrap(p->surfaces[surface_wrap(surface_bse+i)].tex,
+ PLANE_RGB, p->components);
+ // Since the code in pass_sample_separated currently assumes
+ // the textures are bound in-order and starting at 0, we just
+ // assert to make sure this is the case (which it should always be)
+ int id = pass_bind(p, img);
+ assert(id == i);
+ }
+
+ MP_TRACE(p, "inter frame dur: %f vsync: %f, mix: %f\n",
+ t->ideal_frame_duration, t->vsync_interval, mix);
+ p->is_interpolated = true;
+ }
+ pass_draw_to_screen(p, fbo, flags);
+
+ p->frames_drawn += 1;
+}
+
+void gl_video_render_frame(struct gl_video *p, struct vo_frame *frame,
+ struct ra_fbo fbo, int flags)
+{
+ gl_video_update_options(p);
+
+ struct mp_rect target_rc = {0, 0, fbo.tex->params.w, fbo.tex->params.h};
+
+ p->broken_frame = false;
+
+ bool has_frame = !!frame->current;
+
+ struct m_color c = p->clear_color;
+ float clear_color[4] = {c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0};
+ p->ra->fns->clear(p->ra, fbo.tex, clear_color, &target_rc);
+
+ if (p->hwdec_overlay) {
+ if (has_frame) {
+ float *color = p->hwdec_overlay->overlay_colorkey;
+ p->ra->fns->clear(p->ra, fbo.tex, color, &p->dst_rect);
+ }
+
+ p->hwdec_overlay->driver->overlay_frame(p->hwdec_overlay, frame->current,
+ &p->src_rect, &p->dst_rect,
+ frame->frame_id != p->image.id);
+
+ if (frame->current)
+ p->osd_pts = frame->current->pts;
+
+ // Disable GL rendering
+ has_frame = false;
+ }
+
+ if (has_frame) {
+ bool interpolate = p->opts.interpolation && frame->display_synced &&
+ (p->frames_drawn || !frame->still);
+ if (interpolate) {
+ double ratio = frame->ideal_frame_duration / frame->vsync_interval;
+ if (fabs(ratio - 1.0) < p->opts.interpolation_threshold)
+ interpolate = false;
+ }
+
+ if (interpolate) {
+ gl_video_interpolate_frame(p, frame, fbo, flags);
+ } else {
+ bool is_new = frame->frame_id != p->image.id;
+
+ // Redrawing a frame might update subtitles.
+ if (frame->still && p->opts.blend_subs)
+ is_new = true;
+
+ if (is_new || !p->output_tex_valid) {
+ p->output_tex_valid = false;
+
+ pass_info_reset(p, !is_new);
+ if (!pass_render_frame(p, frame->current, frame->frame_id, flags))
+ goto done;
+
+ // For the non-interpolation case, we draw to a single "cache"
+ // texture to speed up subsequent re-draws (if any exist)
+ struct ra_fbo dest_fbo = fbo;
+ bool repeats = frame->num_vsyncs > 1 && frame->display_synced;
+ if ((repeats || frame->still) && !p->dumb_mode &&
+ (p->ra->caps & RA_CAP_BLIT) && fbo.tex->params.blit_dst)
+ {
+ // Attempt to use the same format as the destination FBO
+ // if possible. Some RAs use a wrapped dummy format here,
+ // so fall back to the fbo_format in that case.
+ const struct ra_format *fmt = fbo.tex->params.format;
+ if (fmt->dummy_format)
+ fmt = p->fbo_format;
+
+ bool r = ra_tex_resize(p->ra, p->log, &p->output_tex,
+ fbo.tex->params.w, fbo.tex->params.h,
+ fmt);
+ if (r) {
+ dest_fbo = (struct ra_fbo) { p->output_tex };
+ p->output_tex_valid = true;
+ }
+ }
+ pass_draw_to_screen(p, dest_fbo, flags);
+ }
+
+ // "output tex valid" and "output tex needed" are equivalent
+ if (p->output_tex_valid && fbo.tex->params.blit_dst) {
+ pass_info_reset(p, true);
+ pass_describe(p, "redraw cached frame");
+ struct mp_rect src = p->dst_rect;
+ struct mp_rect dst = src;
+ if (fbo.flip) {
+ dst.y0 = fbo.tex->params.h - src.y0;
+ dst.y1 = fbo.tex->params.h - src.y1;
+ }
+ timer_pool_start(p->blit_timer);
+ p->ra->fns->blit(p->ra, fbo.tex, p->output_tex, &dst, &src);
+ timer_pool_stop(p->blit_timer);
+ pass_record(p, timer_pool_measure(p->blit_timer));
+ }
+ }
+ }
+
+done:
+
+ debug_check_gl(p, "after video rendering");
+
+ if (p->osd && (flags & (RENDER_FRAME_SUBS | RENDER_FRAME_OSD))) {
+ // If we haven't actually drawn anything so far, then we technically
+ // need to consider this the start of a new pass. Let's call it a
+ // redraw just because, since it's basically a blank frame anyway
+ if (!has_frame)
+ pass_info_reset(p, true);
+
+ int osd_flags = p->opts.blend_subs ? OSD_DRAW_OSD_ONLY : 0;
+ if (!(flags & RENDER_FRAME_SUBS))
+ osd_flags |= OSD_DRAW_OSD_ONLY;
+ if (!(flags & RENDER_FRAME_OSD))
+ osd_flags |= OSD_DRAW_SUB_ONLY;
+
+ pass_draw_osd(p, osd_flags, flags, p->osd_pts, p->osd_rect, fbo, true);
+ debug_check_gl(p, "after OSD rendering");
+ }
+
+ p->broken_frame |= gl_sc_error_state(p->sc);
+ if (p->broken_frame) {
+ // Make the screen solid blue to make it visually clear that an
+ // error has occurred
+ float color[4] = {0.0, 0.05, 0.5, 1.0};
+ p->ra->fns->clear(p->ra, fbo.tex, color, &target_rc);
+ }
+
+ p->frames_rendered++;
+ pass_report_performance(p);
+}
+
+void gl_video_screenshot(struct gl_video *p, struct vo_frame *frame,
+ struct voctrl_screenshot *args)
+{
+ if (!p->ra->fns->tex_download)
+ return;
+
+ bool ok = false;
+ struct mp_image *res = NULL;
+ struct ra_tex *target = NULL;
+ struct mp_rect old_src = p->src_rect;
+ struct mp_rect old_dst = p->dst_rect;
+ struct mp_osd_res old_osd = p->osd_rect;
+ struct vo_frame *nframe = vo_frame_ref(frame);
+
+ // Disable interpolation and such.
+ nframe->redraw = true;
+ nframe->repeat = false;
+ nframe->still = true;
+ nframe->pts = 0;
+ nframe->duration = -1;
+
+ if (!args->scaled) {
+ int w, h;
+ mp_image_params_get_dsize(&p->image_params, &w, &h);
+ if (w < 1 || h < 1)
+ return;
+
+ int src_w = p->image_params.w;
+ int src_h = p->image_params.h;
+ struct mp_rect src = {0, 0, src_w, src_h};
+ struct mp_rect dst = {0, 0, w, h};
+
+ if (mp_image_crop_valid(&p->image_params))
+ src = p->image_params.crop;
+
+ if (p->image_params.rotate % 180 == 90) {
+ MPSWAP(int, w, h);
+ MPSWAP(int, src_w, src_h);
+ }
+ mp_rect_rotate(&src, src_w, src_h, p->image_params.rotate);
+ mp_rect_rotate(&dst, w, h, p->image_params.rotate);
+
+ struct mp_osd_res osd = {
+ .display_par = 1.0,
+ .w = mp_rect_w(dst),
+ .h = mp_rect_h(dst),
+ };
+ gl_video_resize(p, &src, &dst, &osd);
+ }
+
+ gl_video_reset_surfaces(p);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .downloadable = true,
+ .w = p->osd_rect.w,
+ .h = p->osd_rect.h,
+ .d = 1,
+ .render_dst = true,
+ };
+
+ params.format = ra_find_unorm_format(p->ra, 1, 4);
+ int mpfmt = IMGFMT_RGB0;
+ if (args->high_bit_depth && p->ra_format.component_bits > 8) {
+ const struct ra_format *fmt = ra_find_unorm_format(p->ra, 2, 4);
+ if (fmt && fmt->renderable) {
+ params.format = fmt;
+ mpfmt = IMGFMT_RGBA64;
+ }
+ }
+
+ if (!params.format || !params.format->renderable)
+ goto done;
+ target = ra_tex_create(p->ra, &params);
+ if (!target)
+ goto done;
+
+ int flags = 0;
+ if (args->subs)
+ flags |= RENDER_FRAME_SUBS;
+ if (args->osd)
+ flags |= RENDER_FRAME_OSD;
+ if (args->scaled)
+ flags |= RENDER_SCREEN_COLOR;
+ gl_video_render_frame(p, nframe, (struct ra_fbo){target}, flags);
+
+ res = mp_image_alloc(mpfmt, params.w, params.h);
+ if (!res)
+ goto done;
+
+ struct ra_tex_download_params download_params = {
+ .tex = target,
+ .dst = res->planes[0],
+ .stride = res->stride[0],
+ };
+ if (!p->ra->fns->tex_download(p->ra, &download_params))
+ goto done;
+
+ if (p->broken_frame)
+ goto done;
+
+ ok = true;
+done:
+ talloc_free(nframe);
+ ra_tex_free(p->ra, &target);
+ gl_video_resize(p, &old_src, &old_dst, &old_osd);
+ gl_video_reset_surfaces(p);
+ if (!ok)
+ TA_FREEP(&res);
+ args->res = res;
+}
+
+// Use this color instead of the global option.
+void gl_video_set_clear_color(struct gl_video *p, struct m_color c)
+{
+ p->force_clear_color = true;
+ p->clear_color = c;
+}
+
+void gl_video_set_osd_pts(struct gl_video *p, double pts)
+{
+ p->osd_pts = pts;
+}
+
+bool gl_video_check_osd_change(struct gl_video *p, struct mp_osd_res *res,
+ double pts)
+{
+ return p->osd ? mpgl_osd_check_change(p->osd, res, pts) : false;
+}
+
+void gl_video_resize(struct gl_video *p,
+ struct mp_rect *src, struct mp_rect *dst,
+ struct mp_osd_res *osd)
+{
+ if (mp_rect_equals(&p->src_rect, src) &&
+ mp_rect_equals(&p->dst_rect, dst) &&
+ osd_res_equals(p->osd_rect, *osd))
+ return;
+
+ p->src_rect = *src;
+ p->dst_rect = *dst;
+ p->osd_rect = *osd;
+
+ gl_video_reset_surfaces(p);
+
+ if (p->osd)
+ mpgl_osd_resize(p->osd, p->osd_rect, p->image_params.stereo3d);
+}
+
+static void frame_perf_data(struct pass_info pass[], struct mp_frame_perf *out)
+{
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ if (!pass[i].desc.len)
+ break;
+ out->perf[out->count] = pass[i].perf;
+ strncpy(out->desc[out->count], pass[i].desc.start,
+ sizeof(out->desc[out->count]) - 1);
+ out->desc[out->count][sizeof(out->desc[out->count]) - 1] = '\0';
+ out->count++;
+ }
+}
+
+void gl_video_perfdata(struct gl_video *p, struct voctrl_performance_data *out)
+{
+ *out = (struct voctrl_performance_data){0};
+ frame_perf_data(p->pass_fresh, &out->fresh);
+ frame_perf_data(p->pass_redraw, &out->redraw);
+}
+
+// Returns false on failure.
+static bool pass_upload_image(struct gl_video *p, struct mp_image *mpi, uint64_t id)
+{
+ struct video_image *vimg = &p->image;
+
+ if (vimg->id == id)
+ return true;
+
+ unref_current_image(p);
+
+ mpi = mp_image_new_ref(mpi);
+ if (!mpi)
+ goto error;
+
+ vimg->mpi = mpi;
+ vimg->id = id;
+ p->osd_pts = mpi->pts;
+ p->frames_uploaded++;
+
+ if (p->hwdec_active) {
+ // Hardware decoding
+
+ if (!p->hwdec_mapper)
+ goto error;
+
+ pass_describe(p, "map frame (hwdec)");
+ timer_pool_start(p->upload_timer);
+ bool ok = ra_hwdec_mapper_map(p->hwdec_mapper, vimg->mpi) >= 0;
+ timer_pool_stop(p->upload_timer);
+ pass_record(p, timer_pool_measure(p->upload_timer));
+
+ vimg->hwdec_mapped = true;
+ if (ok) {
+ struct mp_image layout = {0};
+ mp_image_set_params(&layout, &p->image_params);
+ struct ra_tex **tex = p->hwdec_mapper->tex;
+ for (int n = 0; n < p->plane_count; n++) {
+ vimg->planes[n] = (struct texplane){
+ .w = mp_image_plane_w(&layout, n),
+ .h = mp_image_plane_h(&layout, n),
+ .tex = tex[n],
+ };
+ }
+ } else {
+ MP_FATAL(p, "Mapping hardware decoded surface failed.\n");
+ goto error;
+ }
+ return true;
+ }
+
+ // Software decoding
+ assert(mpi->num_planes == p->plane_count);
+
+ timer_pool_start(p->upload_timer);
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *plane = &vimg->planes[n];
+ if (!plane->tex) {
+ timer_pool_stop(p->upload_timer);
+ goto error;
+ }
+
+ struct ra_tex_upload_params params = {
+ .tex = plane->tex,
+ .src = mpi->planes[n],
+ .invalidate = true,
+ .stride = mpi->stride[n],
+ };
+
+ plane->flipped = params.stride < 0;
+ if (plane->flipped) {
+ int h = mp_image_plane_h(mpi, n);
+ params.src = (char *)params.src + (h - 1) * params.stride;
+ params.stride = -params.stride;
+ }
+
+ struct dr_buffer *mapped = gl_find_dr_buffer(p, mpi->planes[n]);
+ if (mapped) {
+ params.buf = mapped->buf;
+ params.buf_offset = (uintptr_t)params.src -
+ (uintptr_t)mapped->buf->data;
+ params.src = NULL;
+ }
+
+ if (p->using_dr_path != !!mapped) {
+ p->using_dr_path = !!mapped;
+ MP_VERBOSE(p, "DR enabled: %s\n", p->using_dr_path ? "yes" : "no");
+ }
+
+ if (!p->ra->fns->tex_upload(p->ra, &params)) {
+ timer_pool_stop(p->upload_timer);
+ goto error;
+ }
+
+ if (mapped && !mapped->mpi)
+ mapped->mpi = mp_image_new_ref(mpi);
+ }
+ timer_pool_stop(p->upload_timer);
+
+ bool using_pbo = p->ra->use_pbo || !(p->ra->caps & RA_CAP_DIRECT_UPLOAD);
+ const char *mode = p->using_dr_path ? "DR" : using_pbo ? "PBO" : "naive";
+ pass_describe(p, "upload frame (%s)", mode);
+ pass_record(p, timer_pool_measure(p->upload_timer));
+
+ return true;
+
+error:
+ unref_current_image(p);
+ p->broken_frame = true;
+ return false;
+}
+
+static bool test_fbo(struct gl_video *p, const struct ra_format *fmt)
+{
+ MP_VERBOSE(p, "Testing FBO format %s\n", fmt->name);
+ struct ra_tex *tex = NULL;
+ bool success = ra_tex_resize(p->ra, p->log, &tex, 16, 16, fmt);
+ ra_tex_free(p->ra, &tex);
+ return success;
+}
+
+// Return whether dumb-mode can be used without disabling any features.
+// Essentially, vo_gpu with mostly default settings will return true.
+static bool check_dumb_mode(struct gl_video *p)
+{
+ struct gl_video_opts *o = &p->opts;
+ if (p->use_integer_conversion)
+ return false;
+ if (o->dumb_mode > 0) // requested by user
+ return true;
+ if (o->dumb_mode < 0) // disabled by user
+ return false;
+
+ // otherwise, use auto-detection
+ if (o->correct_downscaling || o->linear_downscaling ||
+ o->linear_upscaling || o->sigmoid_upscaling || o->interpolation ||
+ o->blend_subs || o->deband || o->unsharp)
+ return false;
+ // check remaining scalers (tscale is already implicitly excluded above)
+ for (int i = 0; i < SCALER_COUNT; i++) {
+ if (i != SCALER_TSCALE) {
+ const char *name = o->scaler[i].kernel.name;
+ if (name && strcmp(name, "bilinear") != 0)
+ return false;
+ }
+ }
+ if (o->user_shaders && o->user_shaders[0])
+ return false;
+ return true;
+}
+
+// Disable features that are not supported with the current OpenGL version.
+static void check_gl_features(struct gl_video *p)
+{
+ struct ra *ra = p->ra;
+ bool have_float_tex = !!ra_find_float16_format(ra, 1);
+ bool have_mglsl = ra->glsl_version >= 130; // modern GLSL
+ const struct ra_format *rg_tex = ra_find_unorm_format(p->ra, 1, 2);
+ bool have_texrg = rg_tex && !rg_tex->luminance_alpha;
+ bool have_compute = ra->caps & RA_CAP_COMPUTE;
+ bool have_ssbo = ra->caps & RA_CAP_BUF_RW;
+ bool have_fragcoord = ra->caps & RA_CAP_FRAGCOORD;
+
+ const char *auto_fbo_fmts[] = {"rgba16f", "rgba16hf", "rgba16",
+ "rgb10_a2", "rgba8", 0};
+ const char *user_fbo_fmts[] = {p->opts.fbo_format, 0};
+ const char **fbo_fmts = user_fbo_fmts[0] && strcmp(user_fbo_fmts[0], "auto")
+ ? user_fbo_fmts : auto_fbo_fmts;
+ bool user_specified_fbo_fmt = fbo_fmts == user_fbo_fmts;
+ bool fbo_test_result = false;
+ bool have_fbo = false;
+ p->fbo_format = NULL;
+ for (int n = 0; fbo_fmts[n]; n++) {
+ const char *fmt = fbo_fmts[n];
+ const struct ra_format *f = ra_find_named_format(p->ra, fmt);
+ if (!f && user_specified_fbo_fmt)
+ MP_WARN(p, "FBO format '%s' not found!\n", fmt);
+ if (f && f->renderable && f->linear_filter &&
+ (fbo_test_result = test_fbo(p, f))) {
+ MP_VERBOSE(p, "Using FBO format %s.\n", f->name);
+ have_fbo = true;
+ p->fbo_format = f;
+ break;
+ }
+
+ if (user_specified_fbo_fmt) {
+ MP_WARN(p, "User-specified FBO format '%s' failed to initialize! "
+ "(exists=%d, renderable=%d, linear_filter=%d, "
+ "fbo_test_result=%d)\n",
+ fmt, !!f, f ? f->renderable : 0, f ? f->linear_filter : 0,
+ fbo_test_result);
+ }
+ }
+
+ if (!have_fragcoord && p->opts.dither_depth >= 0 &&
+ p->opts.dither_algo != DITHER_NONE)
+ {
+ p->opts.dither_algo = DITHER_NONE;
+ MP_WARN(p, "Disabling dithering (no gl_FragCoord).\n");
+ }
+ if (!have_fragcoord && p->opts.alpha_mode == ALPHA_BLEND_TILES) {
+ p->opts.alpha_mode = ALPHA_BLEND;
+ // Verbose, since this is the default setting
+ MP_VERBOSE(p, "Disabling alpha checkerboard (no gl_FragCoord).\n");
+ }
+ if (!have_fbo && have_compute) {
+ have_compute = false;
+ MP_WARN(p, "Force-disabling compute shaders as an FBO format was not "
+ "available! See your FBO format configuration!\n");
+ }
+
+ if (have_compute && have_fbo && !p->fbo_format->storable) {
+ have_compute = false;
+ MP_WARN(p, "Force-disabling compute shaders as the chosen FBO format "
+ "is not storable! See your FBO format configuration!\n");
+ }
+
+ if (!have_compute && p->opts.dither_algo == DITHER_ERROR_DIFFUSION) {
+ MP_WARN(p, "Disabling error diffusion dithering because compute shader "
+ "was not supported. Fallback to dither=fruit instead.\n");
+ p->opts.dither_algo = DITHER_FRUIT;
+ }
+
+ bool have_compute_peak = have_compute && have_ssbo;
+ if (!have_compute_peak && p->opts.tone_map.compute_peak >= 0) {
+ int msgl = p->opts.tone_map.compute_peak == 1 ? MSGL_WARN : MSGL_V;
+ MP_MSG(p, msgl, "Disabling HDR peak computation (one or more of the "
+ "following is not supported: compute shaders=%d, "
+ "SSBO=%d).\n", have_compute, have_ssbo);
+ p->opts.tone_map.compute_peak = -1;
+ }
+
+ p->forced_dumb_mode = p->opts.dumb_mode > 0 || !have_fbo || !have_texrg;
+ bool voluntarily_dumb = check_dumb_mode(p);
+ if (p->forced_dumb_mode || voluntarily_dumb) {
+ if (voluntarily_dumb) {
+ MP_VERBOSE(p, "No advanced processing required. Enabling dumb mode.\n");
+ } else if (p->opts.dumb_mode <= 0) {
+ MP_WARN(p, "High bit depth FBOs unsupported. Enabling dumb mode.\n"
+ "Most extended features will be disabled.\n");
+ }
+ p->dumb_mode = true;
+ static const struct scaler_config dumb_scaler_config = {
+ {"bilinear", .params = {NAN, NAN}},
+ {.params = {NAN, NAN}},
+ };
+ // Most things don't work, so whitelist all options that still work.
+ p->opts = (struct gl_video_opts){
+ .scaler = {
+ [SCALER_SCALE] = dumb_scaler_config,
+ [SCALER_DSCALE] = dumb_scaler_config,
+ [SCALER_CSCALE] = dumb_scaler_config,
+ [SCALER_TSCALE] = dumb_scaler_config,
+ },
+ .gamma = p->opts.gamma,
+ .gamma_auto = p->opts.gamma_auto,
+ .pbo = p->opts.pbo,
+ .fbo_format = p->opts.fbo_format,
+ .alpha_mode = p->opts.alpha_mode,
+ .use_rectangle = p->opts.use_rectangle,
+ .background = p->opts.background,
+ .dither_algo = p->opts.dither_algo,
+ .dither_depth = p->opts.dither_depth,
+ .dither_size = p->opts.dither_size,
+ .error_diffusion = p->opts.error_diffusion,
+ .temporal_dither = p->opts.temporal_dither,
+ .temporal_dither_period = p->opts.temporal_dither_period,
+ .tex_pad_x = p->opts.tex_pad_x,
+ .tex_pad_y = p->opts.tex_pad_y,
+ .tone_map = p->opts.tone_map,
+ .early_flush = p->opts.early_flush,
+ .icc_opts = p->opts.icc_opts,
+ .hwdec_interop = p->opts.hwdec_interop,
+ .target_trc = p->opts.target_trc,
+ .target_prim = p->opts.target_prim,
+ .target_peak = p->opts.target_peak,
+ };
+ if (!have_fbo)
+ p->use_lut_3d = false;
+ return;
+ }
+ p->dumb_mode = false;
+
+ // Normally, we want to disable them by default if FBOs are unavailable,
+ // because they will be slow (not critically slow, but still slower).
+ // Without FP textures, we must always disable them.
+ // I don't know if luminance alpha float textures exist, so disregard them.
+ for (int n = 0; n < SCALER_COUNT; n++) {
+ const struct filter_kernel *kernel =
+ mp_find_filter_kernel(p->opts.scaler[n].kernel.name);
+ if (kernel) {
+ char *reason = NULL;
+ if (!have_float_tex)
+ reason = "(float tex. missing)";
+ if (!have_mglsl)
+ reason = "(GLSL version too old)";
+ if (reason) {
+ MP_WARN(p, "Disabling scaler #%d %s %s.\n", n,
+ p->opts.scaler[n].kernel.name, reason);
+ // p->opts is a copy => we can just mess with it.
+ p->opts.scaler[n].kernel.name = "bilinear";
+ if (n == SCALER_TSCALE)
+ p->opts.interpolation = false;
+ }
+ }
+ }
+
+ int use_cms = p->opts.target_prim != MP_CSP_PRIM_AUTO ||
+ p->opts.target_trc != MP_CSP_TRC_AUTO || p->use_lut_3d;
+
+ // mix() is needed for some gamma functions
+ if (!have_mglsl && (p->opts.linear_downscaling ||
+ p->opts.linear_upscaling || p->opts.sigmoid_upscaling))
+ {
+ p->opts.linear_downscaling = false;
+ p->opts.linear_upscaling = false;
+ p->opts.sigmoid_upscaling = false;
+ MP_WARN(p, "Disabling linear/sigmoid scaling (GLSL version too old).\n");
+ }
+ if (!have_mglsl && use_cms) {
+ p->opts.target_prim = MP_CSP_PRIM_AUTO;
+ p->opts.target_trc = MP_CSP_TRC_AUTO;
+ p->use_lut_3d = false;
+ MP_WARN(p, "Disabling color management (GLSL version too old).\n");
+ }
+ if (!have_mglsl && p->opts.deband) {
+ p->opts.deband = false;
+ MP_WARN(p, "Disabling debanding (GLSL version too old).\n");
+ }
+}
+
+static void init_gl(struct gl_video *p)
+{
+ debug_check_gl(p, "before init_gl");
+
+ p->upload_timer = timer_pool_create(p->ra);
+ p->blit_timer = timer_pool_create(p->ra);
+ p->osd_timer = timer_pool_create(p->ra);
+
+ debug_check_gl(p, "after init_gl");
+
+ ra_dump_tex_formats(p->ra, MSGL_DEBUG);
+ ra_dump_img_formats(p->ra, MSGL_DEBUG);
+}
+
+void gl_video_uninit(struct gl_video *p)
+{
+ if (!p)
+ return;
+
+ uninit_video(p);
+ ra_hwdec_ctx_uninit(&p->hwdec_ctx);
+ gl_sc_destroy(p->sc);
+
+ ra_tex_free(p->ra, &p->lut_3d_texture);
+ ra_buf_free(p->ra, &p->hdr_peak_ssbo);
+
+ timer_pool_destroy(p->upload_timer);
+ timer_pool_destroy(p->blit_timer);
+ timer_pool_destroy(p->osd_timer);
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ talloc_free(p->pass_fresh[i].desc.start);
+ talloc_free(p->pass_redraw[i].desc.start);
+ }
+
+ mpgl_osd_destroy(p->osd);
+
+ // Forcibly destroy possibly remaining image references. This should also
+ // cause gl_video_dr_free_buffer() to be called for the remaining buffers.
+ gc_pending_dr_fences(p, true);
+
+ // Should all have been unreffed already.
+ assert(!p->num_dr_buffers);
+
+ talloc_free(p);
+}
+
+void gl_video_reset(struct gl_video *p)
+{
+ gl_video_reset_surfaces(p);
+}
+
+bool gl_video_showing_interpolated_frame(struct gl_video *p)
+{
+ return p->is_interpolated;
+}
+
+static bool is_imgfmt_desc_supported(struct gl_video *p,
+ const struct ra_imgfmt_desc *desc)
+{
+ if (!desc->num_planes)
+ return false;
+
+ if (desc->planes[0]->ctype == RA_CTYPE_UINT && p->forced_dumb_mode)
+ return false;
+
+ return true;
+}
+
+bool gl_video_check_format(struct gl_video *p, int mp_format)
+{
+ struct ra_imgfmt_desc desc;
+ if (ra_get_imgfmt_desc(p->ra, mp_format, &desc) &&
+ is_imgfmt_desc_supported(p, &desc))
+ return true;
+ if (ra_hwdec_get(&p->hwdec_ctx, mp_format))
+ return true;
+ return false;
+}
+
+void gl_video_config(struct gl_video *p, struct mp_image_params *params)
+{
+ unmap_overlay(p);
+ unref_current_image(p);
+
+ if (!mp_image_params_equal(&p->real_image_params, params)) {
+ uninit_video(p);
+ p->real_image_params = *params;
+ p->image_params = *params;
+ if (params->imgfmt)
+ init_video(p);
+ }
+
+ gl_video_reset_surfaces(p);
+}
+
+void gl_video_set_osd_source(struct gl_video *p, struct osd_state *osd)
+{
+ mpgl_osd_destroy(p->osd);
+ p->osd = NULL;
+ p->osd_state = osd;
+ reinit_osd(p);
+}
+
+struct gl_video *gl_video_init(struct ra *ra, struct mp_log *log,
+ struct mpv_global *g)
+{
+ struct gl_video *p = talloc_ptrtype(NULL, p);
+ *p = (struct gl_video) {
+ .ra = ra,
+ .global = g,
+ .log = log,
+ .sc = gl_sc_create(ra, g, log),
+ .video_eq = mp_csp_equalizer_create(p, g),
+ .opts_cache = m_config_cache_alloc(p, g, &gl_video_conf),
+ };
+ // make sure this variable is initialized to *something*
+ p->pass = p->pass_fresh;
+ struct gl_video_opts *opts = p->opts_cache->opts;
+ p->cms = gl_lcms_init(p, log, g, opts->icc_opts),
+ p->opts = *opts;
+ for (int n = 0; n < SCALER_COUNT; n++)
+ p->scaler[n] = (struct scaler){.index = n};
+ // our VAO always has the vec2 position as the first element
+ MP_TARRAY_APPEND(p, p->vao, p->vao_len, (struct ra_renderpass_input) {
+ .name = "position",
+ .type = RA_VARTYPE_FLOAT,
+ .dim_v = 2,
+ .dim_m = 1,
+ .offset = 0,
+ });
+ init_gl(p);
+ reinit_from_options(p);
+ return p;
+}
+
+// Get static string for scaler shader. If "tscale" is set to true, the
+// scaler must be a separable convolution filter.
+static const char *handle_scaler_opt(const char *name, bool tscale)
+{
+ if (name && name[0]) {
+ const struct filter_kernel *kernel = mp_find_filter_kernel(name);
+ if (kernel && (!tscale || !kernel->polar))
+ return kernel->f.name;
+
+ const struct filter_window *window = mp_find_filter_window(name);
+ if (window)
+ return window->name;
+
+ for (const char *const *filter = tscale ? fixed_tscale_filters
+ : fixed_scale_filters;
+ *filter; filter++) {
+ if (strcmp(*filter, name) == 0)
+ return *filter;
+ }
+ }
+ return NULL;
+}
+
+static void gl_video_update_options(struct gl_video *p)
+{
+ if (m_config_cache_update(p->opts_cache)) {
+ gl_lcms_update_options(p->cms);
+ reinit_from_options(p);
+ }
+
+ if (mp_csp_equalizer_state_changed(p->video_eq))
+ p->output_tex_valid = false;
+}
+
+static void reinit_from_options(struct gl_video *p)
+{
+ p->use_lut_3d = gl_lcms_has_profile(p->cms);
+
+ // Copy the option fields, so that check_gl_features() can mutate them.
+ // This works only for the fields themselves of course, not for any memory
+ // referenced by them.
+ p->opts = *(struct gl_video_opts *)p->opts_cache->opts;
+
+ if (!p->force_clear_color)
+ p->clear_color = p->opts.background;
+
+ check_gl_features(p);
+ uninit_rendering(p);
+ if (p->opts.shader_cache)
+ gl_sc_set_cache_dir(p->sc, p->opts.shader_cache_dir);
+ p->ra->use_pbo = p->opts.pbo;
+ gl_video_setup_hooks(p);
+ reinit_osd(p);
+
+ struct mp_vo_opts *vo_opts = mp_get_config_group(p, p->global, &vo_sub_opts);
+ if (p->opts.interpolation && !vo_opts->video_sync && !p->dsi_warned) {
+ MP_WARN(p, "Interpolation now requires enabling display-sync mode.\n"
+ "E.g.: --video-sync=display-resample\n");
+ p->dsi_warned = true;
+ }
+ talloc_free(vo_opts);
+
+ if (p->opts.correct_downscaling && !p->correct_downscaling_warned) {
+ const char *name = p->opts.scaler[SCALER_DSCALE].kernel.name;
+ if (!name)
+ name = p->opts.scaler[SCALER_SCALE].kernel.name;
+ if (!name || !strcmp(name, "bilinear")) {
+ MP_WARN(p, "correct-downscaling requires non-bilinear scaler.\n");
+ p->correct_downscaling_warned = true;
+ }
+ }
+}
+
+void gl_video_configure_queue(struct gl_video *p, struct vo *vo)
+{
+ gl_video_update_options(p);
+
+ int queue_size = 1;
+
+ // Figure out an adequate size for the interpolation queue. The larger
+ // the radius, the earlier we need to queue frames.
+ if (p->opts.interpolation) {
+ const struct filter_kernel *kernel =
+ mp_find_filter_kernel(p->opts.scaler[SCALER_TSCALE].kernel.name);
+ if (kernel) {
+ // filter_scale wouldn't be correctly initialized were we to use it here.
+ // This is fine since we're always upsampling, but beware if downsampling
+ // is added!
+ double radius = kernel->f.radius;
+ radius = radius > 0 ? radius : p->opts.scaler[SCALER_TSCALE].radius;
+ queue_size += 1 + ceil(radius);
+ } else {
+ // Oversample/linear case
+ queue_size += 2;
+ }
+ }
+
+ vo_set_queue_params(vo, 0, queue_size);
+}
+
+static int validate_scaler_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ char s[32] = {0};
+ int r = 1;
+ bool tscale = bstr_equals0(name, "tscale");
+ if (bstr_equals0(param, "help")) {
+ r = M_OPT_EXIT;
+ } else if (bstr_equals0(name, "dscale") && !param.len) {
+ return r; // empty dscale means "use same as upscaler"
+ } else if (bstr_equals0(name, "cscale") && !param.len) {
+ return r; // empty cscale means "use same as upscaler"
+ } else {
+ snprintf(s, sizeof(s), "%.*s", BSTR_P(param));
+ if (!handle_scaler_opt(s, tscale))
+ r = M_OPT_INVALID;
+ }
+ if (r < 1) {
+ mp_info(log, "Available scalers:\n");
+ for (const char *const *filter = tscale ? fixed_tscale_filters
+ : fixed_scale_filters;
+ *filter; filter++) {
+ mp_info(log, " %s\n", *filter);
+ }
+ for (int n = 0; mp_filter_kernels[n].f.name; n++) {
+ if (!tscale || !mp_filter_kernels[n].polar)
+ mp_info(log, " %s\n", mp_filter_kernels[n].f.name);
+ }
+ for (int n = 0; mp_filter_windows[n].name; n++) {
+ for (int m = 0; mp_filter_kernels[m].f.name; m++) {
+ if (!strcmp(mp_filter_windows[n].name, mp_filter_kernels[m].f.name))
+ goto next_window; // don't log duplicates
+ }
+ mp_info(log, " %s\n", mp_filter_windows[n].name);
+next_window: ;
+ }
+ if (s[0])
+ mp_fatal(log, "No scaler named '%s' found!\n", s);
+ }
+ return r;
+}
+
+static int validate_window_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ char s[32] = {0};
+ int r = 1;
+ if (bstr_equals0(param, "help")) {
+ r = M_OPT_EXIT;
+ } else if (!param.len) {
+ return r; // empty string means "use preferred window"
+ } else {
+ snprintf(s, sizeof(s), "%.*s", BSTR_P(param));
+ const struct filter_window *window = mp_find_filter_window(s);
+ if (!window)
+ r = M_OPT_INVALID;
+ }
+ if (r < 1) {
+ mp_info(log, "Available windows:\n");
+ for (int n = 0; mp_filter_windows[n].name; n++)
+ mp_info(log, " %s\n", mp_filter_windows[n].name);
+ if (s[0])
+ mp_fatal(log, "No window named '%s' found!\n", s);
+ }
+ return r;
+}
+
+static int validate_error_diffusion_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ char s[32] = {0};
+ int r = 1;
+ if (bstr_equals0(param, "help")) {
+ r = M_OPT_EXIT;
+ } else {
+ snprintf(s, sizeof(s), "%.*s", BSTR_P(param));
+ const struct error_diffusion_kernel *k = mp_find_error_diffusion_kernel(s);
+ if (!k)
+ r = M_OPT_INVALID;
+ }
+ if (r < 1) {
+ mp_info(log, "Available error diffusion kernels:\n");
+ for (int n = 0; mp_error_diffusion_kernels[n].name; n++)
+ mp_info(log, " %s\n", mp_error_diffusion_kernels[n].name);
+ if (s[0])
+ mp_fatal(log, "No error diffusion kernel named '%s' found!\n", s);
+ }
+ return r;
+}
+
+void gl_video_set_ambient_lux(struct gl_video *p, int lux)
+{
+ if (p->opts.gamma_auto) {
+ p->opts.gamma = gl_video_scale_ambient_lux(16.0, 256.0, 1.0, 1.2, lux);
+ MP_TRACE(p, "ambient light changed: %d lux (gamma: %f)\n", lux,
+ p->opts.gamma);
+ }
+}
+
+static void *gl_video_dr_alloc_buffer(struct gl_video *p, size_t size)
+{
+ struct ra_buf_params params = {
+ .type = RA_BUF_TYPE_TEX_UPLOAD,
+ .host_mapped = true,
+ .size = size,
+ };
+
+ struct ra_buf *buf = ra_buf_create(p->ra, &params);
+ if (!buf)
+ return NULL;
+
+ MP_TARRAY_GROW(p, p->dr_buffers, p->num_dr_buffers);
+ p->dr_buffers[p->num_dr_buffers++] = (struct dr_buffer){ .buf = buf };
+
+ return buf->data;
+}
+
+static void gl_video_dr_free_buffer(void *opaque, uint8_t *data)
+{
+ struct gl_video *p = opaque;
+
+ for (int n = 0; n < p->num_dr_buffers; n++) {
+ struct dr_buffer *buffer = &p->dr_buffers[n];
+ if (buffer->buf->data == data) {
+ assert(!buffer->mpi); // can't be freed while it has a ref
+ ra_buf_free(p->ra, &buffer->buf);
+ MP_TARRAY_REMOVE_AT(p->dr_buffers, p->num_dr_buffers, n);
+ return;
+ }
+ }
+ // not found - must not happen
+ MP_ASSERT_UNREACHABLE();
+}
+
+struct mp_image *gl_video_get_image(struct gl_video *p, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ if (flags & VO_DR_FLAG_HOST_CACHED) {
+ if (p->ra->caps & RA_CAP_SLOW_DR) {
+ MP_VERBOSE(p, "DR path suspected slow/uncached, disabling.\n");
+ return NULL;
+ }
+ }
+
+ if (!gl_video_check_format(p, imgfmt))
+ return NULL;
+
+ int size = mp_image_get_alloc_size(imgfmt, w, h, stride_align);
+ if (size < 0)
+ return NULL;
+
+ int alloc_size = size + stride_align;
+ void *ptr = gl_video_dr_alloc_buffer(p, alloc_size);
+ if (!ptr)
+ return NULL;
+
+ // (we expect vo.c to proxy the free callback, so it happens in the same
+ // thread it was allocated in, removing the need for synchronization)
+ struct mp_image *res = mp_image_from_buffer(imgfmt, w, h, stride_align,
+ ptr, alloc_size, p,
+ gl_video_dr_free_buffer);
+ if (!res)
+ gl_video_dr_free_buffer(p, ptr);
+ return res;
+}
+
+void gl_video_init_hwdecs(struct gl_video *p, struct ra_ctx *ra_ctx,
+ struct mp_hwdec_devices *devs,
+ bool load_all_by_default)
+{
+ assert(!p->hwdec_ctx.ra_ctx);
+ p->hwdec_ctx = (struct ra_hwdec_ctx) {
+ .log = p->log,
+ .global = p->global,
+ .ra_ctx = ra_ctx,
+ };
+
+ ra_hwdec_ctx_init(&p->hwdec_ctx, devs, p->opts.hwdec_interop, load_all_by_default);
+}
+
+void gl_video_load_hwdecs_for_img_fmt(struct gl_video *p, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params)
+{
+ assert(p->hwdec_ctx.ra_ctx);
+ ra_hwdec_ctx_load_fmt(&p->hwdec_ctx, devs, params);
+}
diff --git a/video/out/gpu/video.h b/video/out/gpu/video.h
new file mode 100644
index 0000000..411d336
--- /dev/null
+++ b/video/out/gpu/video.h
@@ -0,0 +1,238 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_GL_VIDEO_H
+#define MP_GL_VIDEO_H
+
+#include <stdbool.h>
+
+#include "options/m_option.h"
+#include "sub/osd.h"
+#include "utils.h"
+#include "lcms.h"
+#include "shader_cache.h"
+#include "video/csputils.h"
+#include "video/out/filter_kernels.h"
+
+struct scaler_fun {
+ char *name;
+ float params[2];
+ float blur;
+ float taper;
+};
+
+struct scaler_config {
+ struct scaler_fun kernel;
+ struct scaler_fun window;
+ float radius;
+ float antiring;
+ float clamp;
+};
+
+struct scaler {
+ int index;
+ struct scaler_config conf;
+ double scale_factor;
+ bool initialized;
+ struct filter_kernel *kernel;
+ struct ra_tex *lut;
+ struct ra_tex *sep_fbo;
+ bool insufficient;
+
+ // kernel points here
+ struct filter_kernel kernel_storage;
+};
+
+enum scaler_unit {
+ SCALER_SCALE, // luma/video
+ SCALER_DSCALE, // luma-video downscaling
+ SCALER_CSCALE, // chroma upscaling
+ SCALER_TSCALE, // temporal scaling (interpolation)
+ SCALER_COUNT
+};
+
+enum dither_algo {
+ DITHER_NONE = 0,
+ DITHER_FRUIT,
+ DITHER_ORDERED,
+ DITHER_ERROR_DIFFUSION,
+};
+
+enum alpha_mode {
+ ALPHA_NO = 0,
+ ALPHA_YES,
+ ALPHA_BLEND,
+ ALPHA_BLEND_TILES,
+};
+
+enum blend_subs_mode {
+ BLEND_SUBS_NO = 0,
+ BLEND_SUBS_YES,
+ BLEND_SUBS_VIDEO,
+};
+
+enum tone_mapping {
+ TONE_MAPPING_AUTO,
+ TONE_MAPPING_CLIP,
+ TONE_MAPPING_MOBIUS,
+ TONE_MAPPING_REINHARD,
+ TONE_MAPPING_HABLE,
+ TONE_MAPPING_GAMMA,
+ TONE_MAPPING_LINEAR,
+ TONE_MAPPING_SPLINE,
+ TONE_MAPPING_BT_2390,
+ TONE_MAPPING_BT_2446A,
+ TONE_MAPPING_ST2094_40,
+ TONE_MAPPING_ST2094_10,
+};
+
+enum gamut_mode {
+ GAMUT_AUTO,
+ GAMUT_CLIP,
+ GAMUT_PERCEPTUAL,
+ GAMUT_RELATIVE,
+ GAMUT_SATURATION,
+ GAMUT_ABSOLUTE,
+ GAMUT_DESATURATE,
+ GAMUT_DARKEN,
+ GAMUT_WARN,
+ GAMUT_LINEAR,
+};
+
+struct gl_tone_map_opts {
+ int curve;
+ float curve_param;
+ float max_boost;
+ bool inverse;
+ int compute_peak;
+ float decay_rate;
+ float scene_threshold_low;
+ float scene_threshold_high;
+ float peak_percentile;
+ float contrast_recovery;
+ float contrast_smoothness;
+ int gamut_mode;
+ bool visualize;
+};
+
+struct gl_video_opts {
+ int dumb_mode;
+ struct scaler_config scaler[4];
+ float gamma;
+ bool gamma_auto;
+ int target_prim;
+ int target_trc;
+ int target_peak;
+ int target_contrast;
+ int target_gamut;
+ struct gl_tone_map_opts tone_map;
+ bool correct_downscaling;
+ bool linear_downscaling;
+ bool linear_upscaling;
+ bool sigmoid_upscaling;
+ float sigmoid_center;
+ float sigmoid_slope;
+ bool scaler_resizes_only;
+ bool pbo;
+ int dither_depth;
+ int dither_algo;
+ int dither_size;
+ bool temporal_dither;
+ int temporal_dither_period;
+ char *error_diffusion;
+ char *fbo_format;
+ int alpha_mode;
+ bool use_rectangle;
+ struct m_color background;
+ bool interpolation;
+ float interpolation_threshold;
+ int blend_subs;
+ char **user_shaders;
+ char **user_shader_opts;
+ bool deband;
+ struct deband_opts *deband_opts;
+ float unsharp;
+ int tex_pad_x, tex_pad_y;
+ struct mp_icc_opts *icc_opts;
+ bool shader_cache;
+ int early_flush;
+ char *shader_cache_dir;
+ char *hwdec_interop;
+};
+
+extern const struct m_sub_options gl_video_conf;
+
+struct gl_video;
+struct vo_frame;
+struct voctrl_screenshot;
+
+enum {
+ RENDER_FRAME_SUBS = 1 << 0,
+ RENDER_FRAME_OSD = 1 << 1,
+ RENDER_FRAME_VF_SUBS = 1 << 2,
+ RENDER_SCREEN_COLOR = 1 << 3, // 3D LUT and dithering
+ RENDER_FRAME_DEF = RENDER_FRAME_SUBS | RENDER_FRAME_OSD | RENDER_SCREEN_COLOR,
+};
+
+struct gl_video *gl_video_init(struct ra *ra, struct mp_log *log,
+ struct mpv_global *g);
+void gl_video_uninit(struct gl_video *p);
+void gl_video_set_osd_source(struct gl_video *p, struct osd_state *osd);
+bool gl_video_check_format(struct gl_video *p, int mp_format);
+void gl_video_config(struct gl_video *p, struct mp_image_params *params);
+void gl_video_render_frame(struct gl_video *p, struct vo_frame *frame,
+ struct ra_fbo fbo, int flags);
+void gl_video_resize(struct gl_video *p,
+ struct mp_rect *src, struct mp_rect *dst,
+ struct mp_osd_res *osd);
+void gl_video_set_fb_depth(struct gl_video *p, int fb_depth);
+void gl_video_perfdata(struct gl_video *p, struct voctrl_performance_data *out);
+void gl_video_set_clear_color(struct gl_video *p, struct m_color color);
+void gl_video_set_osd_pts(struct gl_video *p, double pts);
+bool gl_video_check_osd_change(struct gl_video *p, struct mp_osd_res *osd,
+ double pts);
+
+void gl_video_screenshot(struct gl_video *p, struct vo_frame *frame,
+ struct voctrl_screenshot *args);
+
+float gl_video_scale_ambient_lux(float lmin, float lmax,
+ float rmin, float rmax, float lux);
+void gl_video_set_ambient_lux(struct gl_video *p, int lux);
+void gl_video_set_icc_profile(struct gl_video *p, bstr icc_data);
+bool gl_video_icc_auto_enabled(struct gl_video *p);
+bool gl_video_gamma_auto_enabled(struct gl_video *p);
+struct mp_colorspace gl_video_get_output_colorspace(struct gl_video *p);
+
+void gl_video_reset(struct gl_video *p);
+bool gl_video_showing_interpolated_frame(struct gl_video *p);
+
+struct mp_hwdec_devices;
+void gl_video_init_hwdecs(struct gl_video *p, struct ra_ctx *ra_ctx,
+ struct mp_hwdec_devices *devs,
+ bool load_all_by_default);
+struct hwdec_imgfmt_request;
+void gl_video_load_hwdecs_for_img_fmt(struct gl_video *p, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params);
+
+struct vo;
+void gl_video_configure_queue(struct gl_video *p, struct vo *vo);
+
+struct mp_image *gl_video_get_image(struct gl_video *p, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+
+#endif
diff --git a/video/out/gpu/video_shaders.c b/video/out/gpu/video_shaders.c
new file mode 100644
index 0000000..6c0e8a8
--- /dev/null
+++ b/video/out/gpu/video_shaders.c
@@ -0,0 +1,1033 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include "video_shaders.h"
+#include "video.h"
+
+#define GLSL(x) gl_sc_add(sc, #x "\n");
+#define GLSLF(...) gl_sc_addf(sc, __VA_ARGS__)
+#define GLSLH(x) gl_sc_hadd(sc, #x "\n");
+#define GLSLHF(...) gl_sc_haddf(sc, __VA_ARGS__)
+
+// Set up shared/commonly used variables and macros
+void sampler_prelude(struct gl_shader_cache *sc, int tex_num)
+{
+ GLSLF("#undef tex\n");
+ GLSLF("#undef texmap\n");
+ GLSLF("#define tex texture%d\n", tex_num);
+ GLSLF("#define texmap texmap%d\n", tex_num);
+ GLSLF("vec2 pos = texcoord%d;\n", tex_num);
+ GLSLF("vec2 size = texture_size%d;\n", tex_num);
+ GLSLF("vec2 pt = pixel_size%d;\n", tex_num);
+}
+
+static void pass_sample_separated_get_weights(struct gl_shader_cache *sc,
+ struct scaler *scaler)
+{
+ gl_sc_uniform_texture(sc, "lut", scaler->lut);
+ GLSLF("float ypos = LUT_POS(fcoord, %d.0);\n", scaler->lut->params.h);
+
+ int N = scaler->kernel->size;
+ int width = (N + 3) / 4; // round up
+
+ GLSLF("float weights[%d];\n", N);
+ for (int i = 0; i < N; i++) {
+ if (i % 4 == 0)
+ GLSLF("c = texture(lut, vec2(%f, ypos));\n", (i / 4 + 0.5) / width);
+ GLSLF("weights[%d] = c[%d];\n", i, i % 4);
+ }
+}
+
+// Handle a single pass (either vertical or horizontal). The direction is given
+// by the vector (d_x, d_y). If the vector is 0, then planar interpolation is
+// used instead (samples from texture0 through textureN)
+void pass_sample_separated_gen(struct gl_shader_cache *sc, struct scaler *scaler,
+ int d_x, int d_y)
+{
+ int N = scaler->kernel->size;
+ bool use_ar = scaler->conf.antiring > 0;
+ bool planar = d_x == 0 && d_y == 0;
+ GLSL(color = vec4(0.0);)
+ GLSLF("{\n");
+ if (!planar) {
+ GLSLF("vec2 dir = vec2(%d.0, %d.0);\n", d_x, d_y);
+ GLSL(pt *= dir;)
+ GLSL(float fcoord = dot(fract(pos * size - vec2(0.5)), dir);)
+ GLSLF("vec2 base = pos - fcoord * pt - pt * vec2(%d.0);\n", N / 2 - 1);
+ }
+ GLSL(vec4 c;)
+ if (use_ar) {
+ GLSL(vec4 hi = vec4(0.0);)
+ GLSL(vec4 lo = vec4(1.0);)
+ }
+ pass_sample_separated_get_weights(sc, scaler);
+ GLSLF("// scaler samples\n");
+ for (int n = 0; n < N; n++) {
+ if (planar) {
+ GLSLF("c = texture(texture%d, texcoord%d);\n", n, n);
+ } else {
+ GLSLF("c = texture(tex, base + pt * vec2(%d.0));\n", n);
+ }
+ GLSLF("color += vec4(weights[%d]) * c;\n", n);
+ if (use_ar && (n == N/2-1 || n == N/2)) {
+ GLSL(lo = min(lo, c);)
+ GLSL(hi = max(hi, c);)
+ }
+ }
+ if (use_ar)
+ GLSLF("color = mix(color, clamp(color, lo, hi), %f);\n",
+ scaler->conf.antiring);
+ GLSLF("}\n");
+}
+
+// Subroutine for computing and adding an individual texel contribution
+// If planar is false, samples directly
+// If planar is true, takes the pixel from inX[idx] where X is the component and
+// `idx` must be defined by the caller
+static void polar_sample(struct gl_shader_cache *sc, struct scaler *scaler,
+ int x, int y, int components, bool planar)
+{
+ double radius = scaler->kernel->radius * scaler->kernel->filter_scale;
+ double radius_cutoff = scaler->kernel->radius_cutoff;
+
+ // Since we can't know the subpixel position in advance, assume a
+ // worst case scenario
+ int yy = y > 0 ? y-1 : y;
+ int xx = x > 0 ? x-1 : x;
+ double dmax = sqrt(xx*xx + yy*yy);
+ // Skip samples definitely outside the radius
+ if (dmax >= radius_cutoff)
+ return;
+ GLSLF("d = length(vec2(%d.0, %d.0) - fcoord);\n", x, y);
+ // Check for samples that might be skippable
+ bool maybe_skippable = dmax >= radius_cutoff - M_SQRT2;
+ if (maybe_skippable)
+ GLSLF("if (d < %f) {\n", radius_cutoff);
+
+ // get the weight for this pixel
+ if (scaler->lut->params.dimensions == 1) {
+ GLSLF("w = tex1D(lut, LUT_POS(d * 1.0/%f, %d.0)).r;\n",
+ radius, scaler->lut->params.w);
+ } else {
+ GLSLF("w = texture(lut, vec2(0.5, LUT_POS(d * 1.0/%f, %d.0))).r;\n",
+ radius, scaler->lut->params.h);
+ }
+ GLSL(wsum += w;)
+
+ if (planar) {
+ for (int n = 0; n < components; n++)
+ GLSLF("color[%d] += w * in%d[idx];\n", n, n);
+ } else {
+ GLSLF("in0 = texture(tex, base + pt * vec2(%d.0, %d.0));\n", x, y);
+ GLSL(color += vec4(w) * in0;)
+ }
+
+ if (maybe_skippable)
+ GLSLF("}\n");
+}
+
+void pass_sample_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, bool sup_gather)
+{
+ GLSL(color = vec4(0.0);)
+ GLSLF("{\n");
+ GLSL(vec2 fcoord = fract(pos * size - vec2(0.5));)
+ GLSL(vec2 base = pos - fcoord * pt;)
+ GLSLF("float w, d, wsum = 0.0;\n");
+ for (int n = 0; n < components; n++)
+ GLSLF("vec4 in%d;\n", n);
+ GLSL(int idx;)
+
+ gl_sc_uniform_texture(sc, "lut", scaler->lut);
+
+ GLSLF("// scaler samples\n");
+ int bound = ceil(scaler->kernel->radius_cutoff);
+ for (int y = 1-bound; y <= bound; y += 2) {
+ for (int x = 1-bound; x <= bound; x += 2) {
+ // First we figure out whether it's more efficient to use direct
+ // sampling or gathering. The problem is that gathering 4 texels
+ // only to discard some of them is very wasteful, so only do it if
+ // we suspect it will be a win rather than a loss. This is the case
+ // exactly when all four texels are within bounds
+ bool use_gather = sqrt(x*x + y*y) < scaler->kernel->radius_cutoff;
+
+ if (!sup_gather)
+ use_gather = false;
+
+ if (use_gather) {
+ // Gather the four surrounding texels simultaneously
+ for (int n = 0; n < components; n++) {
+ GLSLF("in%d = textureGatherOffset(tex, base, "
+ "ivec2(%d, %d), %d);\n", n, x, y, n);
+ }
+
+ // Mix in all of the points with their weights
+ for (int p = 0; p < 4; p++) {
+ // The four texels are gathered counterclockwise starting
+ // from the bottom left
+ static const int xo[4] = {0, 1, 1, 0};
+ static const int yo[4] = {1, 1, 0, 0};
+ if (x+xo[p] > bound || y+yo[p] > bound)
+ continue;
+ GLSLF("idx = %d;\n", p);
+ polar_sample(sc, scaler, x+xo[p], y+yo[p], components, true);
+ }
+ } else {
+ // switch to direct sampling instead, for efficiency/compatibility
+ for (int yy = y; yy <= bound && yy <= y+1; yy++) {
+ for (int xx = x; xx <= bound && xx <= x+1; xx++)
+ polar_sample(sc, scaler, xx, yy, components, false);
+ }
+ }
+ }
+ }
+
+ GLSL(color = color / vec4(wsum);)
+ GLSLF("}\n");
+}
+
+// bw/bh: block size
+// iw/ih: input size (pre-calculated to fit all required texels)
+void pass_compute_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, int bw, int bh, int iw, int ih)
+{
+ int bound = ceil(scaler->kernel->radius_cutoff);
+ int offset = bound - 1; // padding top/left
+
+ GLSL(color = vec4(0.0);)
+ GLSLF("{\n");
+ GLSL(vec2 wpos = texmap(gl_WorkGroupID * gl_WorkGroupSize);)
+ GLSL(vec2 wbase = wpos - pt * fract(wpos * size - vec2(0.5));)
+ GLSL(vec2 fcoord = fract(pos * size - vec2(0.5));)
+ GLSL(vec2 base = pos - pt * fcoord;)
+ GLSL(ivec2 rel = ivec2(round((base - wbase) * size));)
+ GLSL(int idx;)
+ GLSLF("float w, d, wsum = 0.0;\n");
+ gl_sc_uniform_texture(sc, "lut", scaler->lut);
+
+ // Load all relevant texels into shmem
+ for (int c = 0; c < components; c++)
+ GLSLHF("shared float in%d[%d];\n", c, ih * iw);
+
+ GLSL(vec4 c;)
+ GLSLF("for (int y = int(gl_LocalInvocationID.y); y < %d; y += %d) {\n", ih, bh);
+ GLSLF("for (int x = int(gl_LocalInvocationID.x); x < %d; x += %d) {\n", iw, bw);
+ GLSLF("c = texture(tex, wbase + pt * vec2(x - %d, y - %d));\n", offset, offset);
+ for (int c = 0; c < components; c++)
+ GLSLF("in%d[%d * y + x] = c[%d];\n", c, iw, c);
+ GLSLF("}}\n");
+ GLSL(groupMemoryBarrier();)
+ GLSL(barrier();)
+
+ // Dispatch the actual samples
+ GLSLF("// scaler samples\n");
+ for (int y = 1-bound; y <= bound; y++) {
+ for (int x = 1-bound; x <= bound; x++) {
+ GLSLF("idx = %d * rel.y + rel.x + %d;\n", iw,
+ iw * (y + offset) + x + offset);
+ polar_sample(sc, scaler, x, y, components, true);
+ }
+ }
+
+ GLSL(color = color / vec4(wsum);)
+ GLSLF("}\n");
+}
+
+static void bicubic_calcweights(struct gl_shader_cache *sc, const char *t, const char *s)
+{
+ // Explanation of how bicubic scaling with only 4 texel fetches is done:
+ // http://www.mate.tue.nl/mate/pdfs/10318.pdf
+ // 'Efficient GPU-Based Texture Interpolation using Uniform B-Splines'
+ // Explanation why this algorithm normally always blurs, even with unit
+ // scaling:
+ // http://bigwww.epfl.ch/preprints/ruijters1001p.pdf
+ // 'GPU Prefilter for Accurate Cubic B-spline Interpolation'
+ GLSLF("vec4 %s = vec4(-0.5, 0.1666, 0.3333, -0.3333) * %s"
+ " + vec4(1, 0, -0.5, 0.5);\n", t, s);
+ GLSLF("%s = %s * %s + vec4(0, 0, -0.5, 0.5);\n", t, t, s);
+ GLSLF("%s = %s * %s + vec4(-0.6666, 0, 0.8333, 0.1666);\n", t, t, s);
+ GLSLF("%s.xy *= vec2(1, 1) / vec2(%s.z, %s.w);\n", t, t, t);
+ GLSLF("%s.xy += vec2(1.0 + %s, 1.0 - %s);\n", t, s, s);
+}
+
+void pass_sample_bicubic_fast(struct gl_shader_cache *sc)
+{
+ GLSLF("{\n");
+ GLSL(vec2 fcoord = fract(pos * size + vec2(0.5, 0.5));)
+ bicubic_calcweights(sc, "parmx", "fcoord.x");
+ bicubic_calcweights(sc, "parmy", "fcoord.y");
+ GLSL(vec4 cdelta;)
+ GLSL(cdelta.xz = parmx.rg * vec2(-pt.x, pt.x);)
+ GLSL(cdelta.yw = parmy.rg * vec2(-pt.y, pt.y);)
+ // first y-interpolation
+ GLSL(vec4 ar = texture(tex, pos + cdelta.xy);)
+ GLSL(vec4 ag = texture(tex, pos + cdelta.xw);)
+ GLSL(vec4 ab = mix(ag, ar, parmy.b);)
+ // second y-interpolation
+ GLSL(vec4 br = texture(tex, pos + cdelta.zy);)
+ GLSL(vec4 bg = texture(tex, pos + cdelta.zw);)
+ GLSL(vec4 aa = mix(bg, br, parmy.b);)
+ // x-interpolation
+ GLSL(color = mix(aa, ab, parmx.b);)
+ GLSLF("}\n");
+}
+
+void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler,
+ int w, int h)
+{
+ GLSLF("{\n");
+ GLSL(vec2 pos = pos - vec2(0.5) * pt;) // round to nearest
+ GLSL(vec2 fcoord = fract(pos * size - vec2(0.5));)
+ // Determine the mixing coefficient vector
+ gl_sc_uniform_vec2(sc, "output_size", (float[2]){w, h});
+ GLSL(vec2 coeff = fcoord * output_size/size;)
+ float threshold = scaler->conf.kernel.params[0];
+ threshold = isnan(threshold) ? 0.0 : threshold;
+ GLSLF("coeff = (coeff - %f) * 1.0/%f;\n", threshold, 1.0 - 2 * threshold);
+ GLSL(coeff = clamp(coeff, 0.0, 1.0);)
+ // Compute the right blend of colors
+ GLSL(color = texture(tex, pos + pt * (coeff - fcoord));)
+ GLSLF("}\n");
+}
+
+// Common constants for SMPTE ST.2084 (HDR)
+static const float PQ_M1 = 2610./4096 * 1./4,
+ PQ_M2 = 2523./4096 * 128,
+ PQ_C1 = 3424./4096,
+ PQ_C2 = 2413./4096 * 32,
+ PQ_C3 = 2392./4096 * 32;
+
+// Common constants for ARIB STD-B67 (HLG)
+static const float HLG_A = 0.17883277,
+ HLG_B = 0.28466892,
+ HLG_C = 0.55991073;
+
+// Common constants for Panasonic V-Log
+static const float VLOG_B = 0.00873,
+ VLOG_C = 0.241514,
+ VLOG_D = 0.598206;
+
+// Common constants for Sony S-Log
+static const float SLOG_A = 0.432699,
+ SLOG_B = 0.037584,
+ SLOG_C = 0.616596 + 0.03,
+ SLOG_P = 3.538813,
+ SLOG_Q = 0.030001,
+ SLOG_K2 = 155.0 / 219.0;
+
+// Linearize (expand), given a TRC as input. In essence, this is the ITU-R
+// EOTF, calculated on an idealized (reference) monitor with a white point of
+// MP_REF_WHITE and infinite contrast.
+//
+// These functions always output to a normalized scale of [0,1], for
+// convenience of the video.c code that calls it. To get the values in an
+// absolute scale, multiply the result by `mp_trc_nom_peak(trc)`
+void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
+{
+ if (trc == MP_CSP_TRC_LINEAR)
+ return;
+
+ GLSLF("// linearize\n");
+
+ // Note that this clamp may technically violate the definition of
+ // ITU-R BT.2100, which allows for sub-blacks and super-whites to be
+ // displayed on the display where such would be possible. That said, the
+ // problem is that not all gamma curves are well-defined on the values
+ // outside this range, so we ignore it and just clip anyway for sanity.
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+
+ switch (trc) {
+ case MP_CSP_TRC_SRGB:
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/12.92), \n"
+ " pow((color.rgb + vec3(0.055))/vec3(1.055), vec3(2.4)), \n"
+ " %s(lessThan(vec3(0.04045), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_BT_1886:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA18:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.8));)
+ break;
+ case MP_CSP_TRC_GAMMA20:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.0));)
+ break;
+ case MP_CSP_TRC_GAMMA22:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.2));)
+ break;
+ case MP_CSP_TRC_GAMMA24:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA26:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.6));)
+ break;
+ case MP_CSP_TRC_GAMMA28:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.8));)
+ break;
+ case MP_CSP_TRC_PRO_PHOTO:
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/16.0), \n"
+ " pow(color.rgb, vec3(1.8)), \n"
+ " %s(lessThan(vec3(0.03125), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_PQ:
+ GLSLF("color.rgb = pow(color.rgb, vec3(1.0/%f));\n", PQ_M2);
+ GLSLF("color.rgb = max(color.rgb - vec3(%f), vec3(0.0)) \n"
+ " / (vec3(%f) - vec3(%f) * color.rgb);\n",
+ PQ_C1, PQ_C2, PQ_C3);
+ GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", 1.0 / PQ_M1);
+ // PQ's output range is 0-10000, but we need it to be relative to
+ // MP_REF_WHITE instead, so rescale
+ GLSLF("color.rgb *= vec3(%f);\n", 10000 / MP_REF_WHITE);
+ break;
+ case MP_CSP_TRC_HLG:
+ GLSLF("color.rgb = mix(vec3(4.0) * color.rgb * color.rgb,\n"
+ " exp((color.rgb - vec3(%f)) * vec3(1.0/%f)) + vec3(%f),\n"
+ " %s(lessThan(vec3(0.5), color.rgb)));\n",
+ HLG_C, HLG_A, HLG_B, gl_sc_bvec(sc, 3));
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", MP_REF_WHITE_HLG);
+ break;
+ case MP_CSP_TRC_V_LOG:
+ GLSLF("color.rgb = mix((color.rgb - vec3(0.125)) * vec3(1.0/5.6), \n"
+ " pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
+ " - vec3(%f), \n"
+ " %s(lessThanEqual(vec3(0.181), color.rgb))); \n",
+ VLOG_D, VLOG_C, VLOG_B, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_S_LOG1:
+ GLSLF("color.rgb = pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f))\n"
+ " - vec3(%f);\n",
+ SLOG_C, SLOG_A, SLOG_B);
+ break;
+ case MP_CSP_TRC_S_LOG2:
+ GLSLF("color.rgb = mix((color.rgb - vec3(%f)) * vec3(1.0/%f), \n"
+ " (pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
+ " - vec3(%f)) * vec3(1.0/%f), \n"
+ " %s(lessThanEqual(vec3(%f), color.rgb))); \n",
+ SLOG_Q, SLOG_P, SLOG_C, SLOG_A, SLOG_B, SLOG_K2, gl_sc_bvec(sc, 3), SLOG_Q);
+ break;
+ case MP_CSP_TRC_ST428:
+ GLSL(color.rgb = vec3(52.37/48.0) * pow(color.rgb, vec3(2.6)););
+ break;
+ default:
+ abort();
+ }
+
+ // Rescale to prevent clipping on non-float textures
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", mp_trc_nom_peak(trc));
+}
+
+// Delinearize (compress), given a TRC as output. This corresponds to the
+// inverse EOTF (not the OETF) in ITU-R terminology, again assuming a
+// reference monitor.
+//
+// Like pass_linearize, this functions ingests values on an normalized scale
+void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
+{
+ if (trc == MP_CSP_TRC_LINEAR)
+ return;
+
+ GLSLF("// delinearize\n");
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(trc));
+
+ switch (trc) {
+ case MP_CSP_TRC_SRGB:
+ GLSLF("color.rgb = mix(color.rgb * vec3(12.92), \n"
+ " vec3(1.055) * pow(color.rgb, vec3(1.0/2.4)) \n"
+ " - vec3(0.055), \n"
+ " %s(lessThanEqual(vec3(0.0031308), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_BT_1886:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA18:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/1.8));)
+ break;
+ case MP_CSP_TRC_GAMMA20:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.0));)
+ break;
+ case MP_CSP_TRC_GAMMA22:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.2));)
+ break;
+ case MP_CSP_TRC_GAMMA24:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA26:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.6));)
+ break;
+ case MP_CSP_TRC_GAMMA28:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.8));)
+ break;
+ case MP_CSP_TRC_PRO_PHOTO:
+ GLSLF("color.rgb = mix(color.rgb * vec3(16.0), \n"
+ " pow(color.rgb, vec3(1.0/1.8)), \n"
+ " %s(lessThanEqual(vec3(0.001953), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_PQ:
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", 10000 / MP_REF_WHITE);
+ GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", PQ_M1);
+ GLSLF("color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
+ " / (vec3(1.0) + vec3(%f) * color.rgb);\n",
+ PQ_C1, PQ_C2, PQ_C3);
+ GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", PQ_M2);
+ break;
+ case MP_CSP_TRC_HLG:
+ GLSLF("color.rgb *= vec3(%f);\n", MP_REF_WHITE_HLG);
+ GLSLF("color.rgb = mix(vec3(0.5) * sqrt(color.rgb),\n"
+ " vec3(%f) * log(color.rgb - vec3(%f)) + vec3(%f),\n"
+ " %s(lessThan(vec3(1.0), color.rgb)));\n",
+ HLG_A, HLG_B, HLG_C, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_V_LOG:
+ GLSLF("color.rgb = mix(vec3(5.6) * color.rgb + vec3(0.125), \n"
+ " vec3(%f) * log(color.rgb + vec3(%f)) \n"
+ " + vec3(%f), \n"
+ " %s(lessThanEqual(vec3(0.01), color.rgb))); \n",
+ VLOG_C / M_LN10, VLOG_B, VLOG_D, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_S_LOG1:
+ GLSLF("color.rgb = vec3(%f) * log(color.rgb + vec3(%f)) + vec3(%f);\n",
+ SLOG_A / M_LN10, SLOG_B, SLOG_C);
+ break;
+ case MP_CSP_TRC_S_LOG2:
+ GLSLF("color.rgb = mix(vec3(%f) * color.rgb + vec3(%f), \n"
+ " vec3(%f) * log(vec3(%f) * color.rgb + vec3(%f)) \n"
+ " + vec3(%f), \n"
+ " %s(lessThanEqual(vec3(0.0), color.rgb))); \n",
+ SLOG_P, SLOG_Q, SLOG_A / M_LN10, SLOG_K2, SLOG_B, SLOG_C, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_ST428:
+ GLSL(color.rgb = pow(color.rgb * vec3(48.0/52.37), vec3(1.0/2.6)););
+ break;
+ default:
+ abort();
+ }
+}
+
+// Apply the OOTF mapping from a given light type to display-referred light.
+// Assumes absolute scale values. `peak` is used to tune the OOTF where
+// applicable (currently only HLG).
+static void pass_ootf(struct gl_shader_cache *sc, enum mp_csp_light light,
+ float peak)
+{
+ if (light == MP_CSP_LIGHT_DISPLAY)
+ return;
+
+ GLSLF("// apply ootf\n");
+
+ switch (light)
+ {
+ case MP_CSP_LIGHT_SCENE_HLG: {
+ // HLG OOTF from BT.2100, scaled to the chosen display peak
+ float gamma = MPMAX(1.0, 1.2 + 0.42 * log10(peak * MP_REF_WHITE / 1000.0));
+ GLSLF("color.rgb *= vec3(%f * pow(dot(src_luma, color.rgb), %f));\n",
+ peak / pow(12.0 / MP_REF_WHITE_HLG, gamma), gamma - 1.0);
+ break;
+ }
+ case MP_CSP_LIGHT_SCENE_709_1886:
+ // This OOTF is defined by encoding the result as 709 and then decoding
+ // it as 1886; although this is called 709_1886 we actually use the
+ // more precise (by one decimal) values from BT.2020 instead
+ GLSLF("color.rgb = mix(color.rgb * vec3(4.5), \n"
+ " vec3(1.0993) * pow(color.rgb, vec3(0.45)) - vec3(0.0993), \n"
+ " %s(lessThan(vec3(0.0181), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
+ break;
+ case MP_CSP_LIGHT_SCENE_1_2:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.2));)
+ break;
+ default:
+ abort();
+ }
+}
+
+// Inverse of the function pass_ootf, for completeness' sake.
+static void pass_inverse_ootf(struct gl_shader_cache *sc, enum mp_csp_light light,
+ float peak)
+{
+ if (light == MP_CSP_LIGHT_DISPLAY)
+ return;
+
+ GLSLF("// apply inverse ootf\n");
+
+ switch (light)
+ {
+ case MP_CSP_LIGHT_SCENE_HLG: {
+ float gamma = MPMAX(1.0, 1.2 + 0.42 * log10(peak * MP_REF_WHITE / 1000.0));
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", peak / pow(12.0 / MP_REF_WHITE_HLG, gamma));
+ GLSLF("color.rgb /= vec3(max(1e-6, pow(dot(src_luma, color.rgb), %f)));\n",
+ (gamma - 1.0) / gamma);
+ break;
+ }
+ case MP_CSP_LIGHT_SCENE_709_1886:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/4.5), \n"
+ " pow((color.rgb + vec3(0.0993)) * vec3(1.0/1.0993), \n"
+ " vec3(1/0.45)), \n"
+ " %s(lessThan(vec3(0.08145), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_LIGHT_SCENE_1_2:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/1.2));)
+ break;
+ default:
+ abort();
+ }
+}
+
+// Average light level for SDR signals. This is equal to a signal level of 0.5
+// under a typical presentation gamma of about 2.0.
+static const float sdr_avg = 0.25;
+
+static void hdr_update_peak(struct gl_shader_cache *sc,
+ const struct gl_tone_map_opts *opts)
+{
+ // Update the sig_peak/sig_avg from the old SSBO state
+ GLSL(if (average.y > 0.0) {)
+ GLSL( sig_avg = max(1e-3, average.x);)
+ GLSL( sig_peak = max(1.00, average.y);)
+ GLSL(})
+
+ // Chosen to avoid overflowing on an 8K buffer
+ const float log_min = 1e-3, log_scale = 400.0, sig_scale = 10000.0;
+
+ // For performance, and to avoid overflows, we tally up the sub-results per
+ // pixel using shared memory first
+ GLSLH(shared int wg_sum;)
+ GLSLH(shared uint wg_max;)
+ GLSL(wg_sum = 0; wg_max = 0u;)
+ GLSL(barrier();)
+ GLSLF("float sig_log = log(max(sig_max, %f));\n", log_min);
+ GLSLF("atomicAdd(wg_sum, int(sig_log * %f));\n", log_scale);
+ GLSLF("atomicMax(wg_max, uint(sig_max * %f));\n", sig_scale);
+
+ // Have one thread per work group update the global atomics
+ GLSL(memoryBarrierShared();)
+ GLSL(barrier();)
+ GLSL(if (gl_LocalInvocationIndex == 0u) {)
+ GLSL( int wg_avg = wg_sum / int(gl_WorkGroupSize.x * gl_WorkGroupSize.y);)
+ GLSL( atomicAdd(frame_sum, wg_avg);)
+ GLSL( atomicMax(frame_max, wg_max);)
+ GLSL( memoryBarrierBuffer();)
+ GLSL(})
+ GLSL(barrier();)
+
+ // Finally, to update the global state, we increment a counter per dispatch
+ GLSL(uint num_wg = gl_NumWorkGroups.x * gl_NumWorkGroups.y;)
+ GLSL(if (gl_LocalInvocationIndex == 0u && atomicAdd(counter, 1u) == num_wg - 1u) {)
+ GLSL( counter = 0u;)
+ GLSL( vec2 cur = vec2(float(frame_sum) / float(num_wg), frame_max);)
+ GLSLF(" cur *= vec2(1.0/%f, 1.0/%f);\n", log_scale, sig_scale);
+ GLSL( cur.x = exp(cur.x);)
+ GLSL( if (average.y == 0.0))
+ GLSL( average = cur;)
+
+ // Use an IIR low-pass filter to smooth out the detected values, with a
+ // configurable decay rate based on the desired time constant (tau)
+ if (opts->decay_rate) {
+ float decay = 1.0f - expf(-1.0f / opts->decay_rate);
+ GLSLF(" average += %f * (cur - average);\n", decay);
+ } else {
+ GLSLF(" average = cur;\n");
+ }
+
+ // Scene change hysteresis
+ float log_db = 10.0 / log(10.0);
+ GLSLF(" float weight = smoothstep(%f, %f, abs(log(cur.x / average.x)));\n",
+ opts->scene_threshold_low / log_db,
+ opts->scene_threshold_high / log_db);
+ GLSL( average = mix(average, cur, weight);)
+
+ // Reset SSBO state for the next frame
+ GLSL( frame_sum = 0; frame_max = 0u;)
+ GLSL( memoryBarrierBuffer();)
+ GLSL(})
+}
+
+static inline float pq_delinearize(float x)
+{
+ x *= MP_REF_WHITE / 10000.0;
+ x = powf(x, PQ_M1);
+ x = (PQ_C1 + PQ_C2 * x) / (1.0 + PQ_C3 * x);
+ x = pow(x, PQ_M2);
+ return x;
+}
+
+// Tone map from a known peak brightness to the range [0,1]. If ref_peak
+// is 0, we will use peak detection instead
+static void pass_tone_map(struct gl_shader_cache *sc,
+ float src_peak, float dst_peak,
+ const struct gl_tone_map_opts *opts)
+{
+ GLSLF("// HDR tone mapping\n");
+
+ // To prevent discoloration due to out-of-bounds clipping, we need to make
+ // sure to reduce the value range as far as necessary to keep the entire
+ // signal in range, so tone map based on the brightest component.
+ GLSL(int sig_idx = 0;)
+ GLSL(if (color[1] > color[sig_idx]) sig_idx = 1;)
+ GLSL(if (color[2] > color[sig_idx]) sig_idx = 2;)
+ GLSL(float sig_max = color[sig_idx];)
+ GLSLF("float sig_peak = %f;\n", src_peak);
+ GLSLF("float sig_avg = %f;\n", sdr_avg);
+
+ if (opts->compute_peak >= 0)
+ hdr_update_peak(sc, opts);
+
+ // Always hard-clip the upper bound of the signal range to avoid functions
+ // exploding on inputs greater than 1.0
+ GLSLF("vec3 sig = min(color.rgb, sig_peak);\n");
+
+ // This function always operates on an absolute scale, so ignore the
+ // dst_peak normalization for it
+ float dst_scale = dst_peak;
+ enum tone_mapping curve = opts->curve ? opts->curve : TONE_MAPPING_BT_2390;
+ if (curve == TONE_MAPPING_BT_2390)
+ dst_scale = 1.0;
+
+ // Rescale the variables in order to bring it into a representation where
+ // 1.0 represents the dst_peak. This is because all of the tone mapping
+ // algorithms are defined in such a way that they map to the range [0.0, 1.0].
+ if (dst_scale > 1.0) {
+ GLSLF("sig *= 1.0/%f;\n", dst_scale);
+ GLSLF("sig_peak *= 1.0/%f;\n", dst_scale);
+ }
+
+ GLSL(float sig_orig = sig[sig_idx];)
+ GLSLF("float slope = min(%f, %f / sig_avg);\n", opts->max_boost, sdr_avg);
+ GLSL(sig *= slope;)
+ GLSL(sig_peak *= slope;)
+
+ float param = opts->curve_param;
+ switch (curve) {
+ case TONE_MAPPING_CLIP:
+ GLSLF("sig = min(%f * sig, 1.0);\n", isnan(param) ? 1.0 : param);
+ break;
+
+ case TONE_MAPPING_MOBIUS:
+ GLSLF("if (sig_peak > (1.0 + 1e-6)) {\n");
+ GLSLF("const float j = %f;\n", isnan(param) ? 0.3 : param);
+ // solve for M(j) = j; M(sig_peak) = 1.0; M'(j) = 1.0
+ // where M(x) = scale * (x+a)/(x+b)
+ GLSLF("float a = -j*j * (sig_peak - 1.0) / (j*j - 2.0*j + sig_peak);\n");
+ GLSLF("float b = (j*j - 2.0*j*sig_peak + sig_peak) / "
+ "max(1e-6, sig_peak - 1.0);\n");
+ GLSLF("float scale = (b*b + 2.0*b*j + j*j) / (b-a);\n");
+ GLSLF("sig = mix(sig, scale * (sig + vec3(a)) / (sig + vec3(b)),"
+ " %s(greaterThan(sig, vec3(j))));\n",
+ gl_sc_bvec(sc, 3));
+ GLSLF("}\n");
+ break;
+
+ case TONE_MAPPING_REINHARD: {
+ float contrast = isnan(param) ? 0.5 : param,
+ offset = (1.0 - contrast) / contrast;
+ GLSLF("sig = sig / (sig + vec3(%f));\n", offset);
+ GLSLF("float scale = (sig_peak + %f) / sig_peak;\n", offset);
+ GLSL(sig *= scale;)
+ break;
+ }
+
+ case TONE_MAPPING_HABLE: {
+ float A = 0.15, B = 0.50, C = 0.10, D = 0.20, E = 0.02, F = 0.30;
+ GLSLHF("vec3 hable(vec3 x) {\n");
+ GLSLHF("return (x * (%f*x + vec3(%f)) + vec3(%f)) / "
+ " (x * (%f*x + vec3(%f)) + vec3(%f)) "
+ " - vec3(%f);\n",
+ A, C*B, D*E,
+ A, B, D*F,
+ E/F);
+ GLSLHF("}\n");
+ GLSLF("sig = hable(max(vec3(0.0), sig)) / hable(vec3(sig_peak)).x;\n");
+ break;
+ }
+
+ case TONE_MAPPING_GAMMA: {
+ float gamma = isnan(param) ? 1.8 : param;
+ GLSLF("const float cutoff = 0.05, gamma = 1.0/%f;\n", gamma);
+ GLSL(float scale = pow(cutoff / sig_peak, gamma.x) / cutoff;)
+ GLSLF("sig = mix(scale * sig,"
+ " pow(sig / sig_peak, vec3(gamma)),"
+ " %s(greaterThan(sig, vec3(cutoff))));\n",
+ gl_sc_bvec(sc, 3));
+ break;
+ }
+
+ case TONE_MAPPING_LINEAR: {
+ float coeff = isnan(param) ? 1.0 : param;
+ GLSLF("sig = min(%f / sig_peak, 1.0) * sig;\n", coeff);
+ break;
+ }
+
+ case TONE_MAPPING_BT_2390:
+ // We first need to encode both sig and sig_peak into PQ space
+ GLSLF("vec4 sig_pq = vec4(sig.rgb, sig_peak); \n"
+ "sig_pq *= vec4(1.0/%f); \n"
+ "sig_pq = pow(sig_pq, vec4(%f)); \n"
+ "sig_pq = (vec4(%f) + vec4(%f) * sig_pq) \n"
+ " / (vec4(1.0) + vec4(%f) * sig_pq); \n"
+ "sig_pq = pow(sig_pq, vec4(%f)); \n",
+ 10000.0 / MP_REF_WHITE, PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
+ // Encode both the signal and the target brightness to be relative to
+ // the source peak brightness, and figure out the target peak in this space
+ GLSLF("float scale = 1.0 / sig_pq.a; \n"
+ "sig_pq.rgb *= vec3(scale); \n"
+ "float maxLum = %f * scale; \n",
+ pq_delinearize(dst_peak));
+ // Apply piece-wise hermite spline
+ GLSLF("float ks = 1.5 * maxLum - 0.5; \n"
+ "vec3 tb = (sig_pq.rgb - vec3(ks)) / vec3(1.0 - ks); \n"
+ "vec3 tb2 = tb * tb; \n"
+ "vec3 tb3 = tb2 * tb; \n"
+ "vec3 pb = (2.0 * tb3 - 3.0 * tb2 + vec3(1.0)) * vec3(ks) + \n"
+ " (tb3 - 2.0 * tb2 + tb) * vec3(1.0 - ks) + \n"
+ " (-2.0 * tb3 + 3.0 * tb2) * vec3(maxLum); \n"
+ "sig = mix(pb, sig_pq.rgb, %s(lessThan(sig_pq.rgb, vec3(ks)))); \n",
+ gl_sc_bvec(sc, 3));
+ // Convert back from PQ space to linear light
+ GLSLF("sig *= vec3(sig_pq.a); \n"
+ "sig = pow(sig, vec3(1.0/%f)); \n"
+ "sig = max(sig - vec3(%f), 0.0) / \n"
+ " (vec3(%f) - vec3(%f) * sig); \n"
+ "sig = pow(sig, vec3(1.0/%f)); \n"
+ "sig *= vec3(%f); \n",
+ PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1, 10000.0 / MP_REF_WHITE);
+ break;
+
+ default:
+ abort();
+ }
+
+ GLSLF("float coeff = max(sig[sig_idx] - %f, 1e-6) / \n"
+ " max(sig[sig_idx], 1.0); \n"
+ "coeff = %f * pow(coeff / %f, %f); \n"
+ "color.rgb *= sig[sig_idx] / sig_orig; \n"
+ "color.rgb = mix(color.rgb, %f * sig, coeff); \n",
+ 0.18 / dst_scale, 0.90, dst_scale, 0.20, dst_scale);
+}
+
+// Map colors from one source space to another. These source spaces must be
+// known (i.e. not MP_CSP_*_AUTO), as this function won't perform any
+// auto-guessing. If is_linear is true, we assume the input has already been
+// linearized (e.g. for linear-scaling). If `opts->compute_peak` is true, we
+// will detect the peak instead of relying on metadata. Note that this requires
+// the caller to have already bound the appropriate SSBO and set up the compute
+// shader metadata
+void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
+ struct mp_colorspace src, struct mp_colorspace dst,
+ const struct gl_tone_map_opts *opts)
+{
+ GLSLF("// color mapping\n");
+
+ // Some operations need access to the video's luma coefficients, so make
+ // them available
+ float rgb2xyz[3][3];
+ mp_get_rgb2xyz_matrix(mp_get_csp_primaries(src.primaries), rgb2xyz);
+ gl_sc_uniform_vec3(sc, "src_luma", rgb2xyz[1]);
+ mp_get_rgb2xyz_matrix(mp_get_csp_primaries(dst.primaries), rgb2xyz);
+ gl_sc_uniform_vec3(sc, "dst_luma", rgb2xyz[1]);
+
+ bool need_ootf = src.light != dst.light;
+ if (src.light == MP_CSP_LIGHT_SCENE_HLG && src.hdr.max_luma != dst.hdr.max_luma)
+ need_ootf = true;
+
+ // All operations from here on require linear light as a starting point,
+ // so we linearize even if src.gamma == dst.gamma when one of the other
+ // operations needs it
+ bool need_linear = src.gamma != dst.gamma ||
+ src.primaries != dst.primaries ||
+ src.hdr.max_luma != dst.hdr.max_luma ||
+ need_ootf;
+
+ if (need_linear && !is_linear) {
+ // We also pull it up so that 1.0 is the reference white
+ pass_linearize(sc, src.gamma);
+ is_linear = true;
+ }
+
+ // Pre-scale the incoming values into an absolute scale
+ GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(src.gamma));
+
+ if (need_ootf)
+ pass_ootf(sc, src.light, src.hdr.max_luma / MP_REF_WHITE);
+
+ // Tone map to prevent clipping due to excessive brightness
+ if (src.hdr.max_luma > dst.hdr.max_luma) {
+ pass_tone_map(sc, src.hdr.max_luma / MP_REF_WHITE,
+ dst.hdr.max_luma / MP_REF_WHITE, opts);
+ }
+
+ // Adapt to the right colorspace if necessary
+ if (src.primaries != dst.primaries) {
+ struct mp_csp_primaries csp_src = mp_get_csp_primaries(src.primaries),
+ csp_dst = mp_get_csp_primaries(dst.primaries);
+ float m[3][3] = {{0}};
+ mp_get_cms_matrix(csp_src, csp_dst, MP_INTENT_RELATIVE_COLORIMETRIC, m);
+ gl_sc_uniform_mat3(sc, "cms_matrix", true, &m[0][0]);
+ GLSL(color.rgb = cms_matrix * color.rgb;)
+
+ if (!opts->gamut_mode || opts->gamut_mode == GAMUT_DESATURATE) {
+ GLSL(float cmin = min(min(color.r, color.g), color.b);)
+ GLSL(if (cmin < 0.0) {
+ float luma = dot(dst_luma, color.rgb);
+ float coeff = cmin / (cmin - luma);
+ color.rgb = mix(color.rgb, vec3(luma), coeff);
+ })
+ GLSLF("float cmax = 1.0/%f * max(max(color.r, color.g), color.b);\n",
+ dst.hdr.max_luma / MP_REF_WHITE);
+ GLSL(if (cmax > 1.0) color.rgb /= cmax;)
+ }
+ }
+
+ if (need_ootf)
+ pass_inverse_ootf(sc, dst.light, dst.hdr.max_luma / MP_REF_WHITE);
+
+ // Post-scale the outgoing values from absolute scale to normalized.
+ // For SDR, we normalize to the chosen signal peak. For HDR, we normalize
+ // to the encoding range of the transfer function.
+ float dst_range = dst.hdr.max_luma / MP_REF_WHITE;
+ if (mp_trc_is_hdr(dst.gamma))
+ dst_range = mp_trc_nom_peak(dst.gamma);
+
+ GLSLF("color.rgb *= vec3(%f);\n", 1.0 / dst_range);
+
+ // Warn for remaining out-of-gamut colors if enabled
+ if (opts->gamut_mode == GAMUT_WARN) {
+ GLSL(if (any(greaterThan(color.rgb, vec3(1.005))) ||
+ any(lessThan(color.rgb, vec3(-0.005)))))
+ GLSL(color.rgb = vec3(1.0) - color.rgb;) // invert
+ }
+
+ if (is_linear)
+ pass_delinearize(sc, dst.gamma);
+}
+
+// Wide usage friendly PRNG, shamelessly stolen from a GLSL tricks forum post.
+// Obtain random numbers by calling rand(h), followed by h = permute(h) to
+// update the state. Assumes the texture was hooked.
+// permute() was modified from the original to avoid "large" numbers in
+// calculations, since low-end mobile GPUs choke on them (overflow).
+static void prng_init(struct gl_shader_cache *sc, AVLFG *lfg)
+{
+ GLSLH(float mod289(float x) { return x - floor(x * 1.0/289.0) * 289.0; })
+ GLSLHF("float permute(float x) {\n");
+ GLSLH(return mod289( mod289(34.0*x + 1.0) * (fract(x) + 1.0) );)
+ GLSLHF("}\n");
+ GLSLH(float rand(float x) { return fract(x * 1.0/41.0); })
+
+ // Initialize the PRNG by hashing the position + a random uniform
+ GLSL(vec3 _m = vec3(HOOKED_pos, random) + vec3(1.0);)
+ GLSL(float h = permute(permute(permute(_m.x)+_m.y)+_m.z);)
+ gl_sc_uniform_dynamic(sc);
+ gl_sc_uniform_f(sc, "random", (double)av_lfg_get(lfg) / UINT32_MAX);
+}
+
+const struct deband_opts deband_opts_def = {
+ .iterations = 1,
+ .threshold = 48.0,
+ .range = 16.0,
+ .grain = 32.0,
+};
+
+#define OPT_BASE_STRUCT struct deband_opts
+const struct m_sub_options deband_conf = {
+ .opts = (const m_option_t[]) {
+ {"iterations", OPT_INT(iterations), M_RANGE(0, 16)},
+ {"threshold", OPT_FLOAT(threshold), M_RANGE(0.0, 4096.0)},
+ {"range", OPT_FLOAT(range), M_RANGE(1.0, 64.0)},
+ {"grain", OPT_FLOAT(grain), M_RANGE(0.0, 4096.0)},
+ {0}
+ },
+ .size = sizeof(struct deband_opts),
+ .defaults = &deband_opts_def,
+};
+
+// Stochastically sample a debanded result from a hooked texture.
+void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
+ AVLFG *lfg, enum mp_csp_trc trc)
+{
+ // Initialize the PRNG
+ GLSLF("{\n");
+ prng_init(sc, lfg);
+
+ // Helper: Compute a stochastic approximation of the avg color around a
+ // pixel
+ GLSLHF("vec4 average(float range, inout float h) {\n");
+ // Compute a random rangle and distance
+ GLSLH(float dist = rand(h) * range; h = permute(h);)
+ GLSLH(float dir = rand(h) * 6.2831853; h = permute(h);)
+ GLSLH(vec2 o = dist * vec2(cos(dir), sin(dir));)
+
+ // Sample at quarter-turn intervals around the source pixel
+ GLSLH(vec4 ref[4];)
+ GLSLH(ref[0] = HOOKED_texOff(vec2( o.x, o.y));)
+ GLSLH(ref[1] = HOOKED_texOff(vec2(-o.y, o.x));)
+ GLSLH(ref[2] = HOOKED_texOff(vec2(-o.x, -o.y));)
+ GLSLH(ref[3] = HOOKED_texOff(vec2( o.y, -o.x));)
+
+ // Return the (normalized) average
+ GLSLH(return (ref[0] + ref[1] + ref[2] + ref[3])*0.25;)
+ GLSLHF("}\n");
+
+ // Sample the source pixel
+ GLSL(color = HOOKED_tex(HOOKED_pos);)
+ GLSLF("vec4 avg, diff;\n");
+ for (int i = 1; i <= opts->iterations; i++) {
+ // Sample the average pixel and use it instead of the original if
+ // the difference is below the given threshold
+ GLSLF("avg = average(%f, h);\n", i * opts->range);
+ GLSL(diff = abs(color - avg);)
+ GLSLF("color = mix(avg, color, %s(greaterThan(diff, vec4(%f))));\n",
+ gl_sc_bvec(sc, 4), opts->threshold / (i * 16384.0));
+ }
+
+ // Add some random noise to smooth out residual differences
+ GLSL(vec3 noise;)
+ GLSL(noise.x = rand(h); h = permute(h);)
+ GLSL(noise.y = rand(h); h = permute(h);)
+ GLSL(noise.z = rand(h); h = permute(h);)
+
+ // Noise is scaled to the signal level to prevent extreme noise for HDR
+ float gain = opts->grain/8192.0 / mp_trc_nom_peak(trc);
+ GLSLF("color.xyz += %f * (noise - vec3(0.5));\n", gain);
+ GLSLF("}\n");
+}
+
+// Assumes the texture was hooked
+void pass_sample_unsharp(struct gl_shader_cache *sc, float param) {
+ GLSLF("{\n");
+ GLSL(float st1 = 1.2;)
+ GLSL(vec4 p = HOOKED_tex(HOOKED_pos);)
+ GLSL(vec4 sum1 = HOOKED_texOff(st1 * vec2(+1, +1))
+ + HOOKED_texOff(st1 * vec2(+1, -1))
+ + HOOKED_texOff(st1 * vec2(-1, +1))
+ + HOOKED_texOff(st1 * vec2(-1, -1));)
+ GLSL(float st2 = 1.5;)
+ GLSL(vec4 sum2 = HOOKED_texOff(st2 * vec2(+1, 0))
+ + HOOKED_texOff(st2 * vec2( 0, +1))
+ + HOOKED_texOff(st2 * vec2(-1, 0))
+ + HOOKED_texOff(st2 * vec2( 0, -1));)
+ GLSL(vec4 t = p * 0.859375 + sum2 * -0.1171875 + sum1 * -0.09765625;)
+ GLSLF("color = p + t * %f;\n", param);
+ GLSLF("}\n");
+}
diff --git a/video/out/gpu/video_shaders.h b/video/out/gpu/video_shaders.h
new file mode 100644
index 0000000..27e7874
--- /dev/null
+++ b/video/out/gpu/video_shaders.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_GL_VIDEO_SHADERS_H
+#define MP_GL_VIDEO_SHADERS_H
+
+#include <libavutil/lfg.h>
+
+#include "utils.h"
+#include "video.h"
+
+struct deband_opts {
+ int iterations;
+ float threshold;
+ float range;
+ float grain;
+};
+
+extern const struct deband_opts deband_opts_def;
+extern const struct m_sub_options deband_conf;
+
+void sampler_prelude(struct gl_shader_cache *sc, int tex_num);
+void pass_sample_separated_gen(struct gl_shader_cache *sc, struct scaler *scaler,
+ int d_x, int d_y);
+void pass_sample_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, bool sup_gather);
+void pass_compute_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, int bw, int bh, int iw, int ih);
+void pass_sample_bicubic_fast(struct gl_shader_cache *sc);
+void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler,
+ int w, int h);
+
+void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
+void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
+
+void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
+ struct mp_colorspace src, struct mp_colorspace dst,
+ const struct gl_tone_map_opts *opts);
+
+void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
+ AVLFG *lfg, enum mp_csp_trc trc);
+
+void pass_sample_unsharp(struct gl_shader_cache *sc, float param);
+
+#endif
diff --git a/video/out/gpu_next/context.c b/video/out/gpu_next/context.c
new file mode 100644
index 0000000..2887cff
--- /dev/null
+++ b/video/out/gpu_next/context.c
@@ -0,0 +1,240 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <libplacebo/config.h>
+
+#ifdef PL_HAVE_D3D11
+#include <libplacebo/d3d11.h>
+#endif
+
+#ifdef PL_HAVE_OPENGL
+#include <libplacebo/opengl.h>
+#endif
+
+#include "context.h"
+#include "config.h"
+#include "common/common.h"
+#include "options/m_config.h"
+#include "video/out/placebo/utils.h"
+#include "video/out/gpu/video.h"
+
+#if HAVE_D3D11
+#include "osdep/windows_utils.h"
+#include "video/out/d3d11/ra_d3d11.h"
+#include "video/out/d3d11/context.h"
+#endif
+
+#if HAVE_GL
+#include "video/out/opengl/context.h"
+#include "video/out/opengl/ra_gl.h"
+# if HAVE_EGL
+#include <EGL/egl.h>
+# endif
+#endif
+
+#if HAVE_VULKAN
+#include "video/out/vulkan/context.h"
+#endif
+
+#if HAVE_D3D11
+static bool d3d11_pl_init(struct vo *vo, struct gpu_ctx *ctx,
+ struct ra_ctx_opts *ctx_opts)
+{
+#if !defined(PL_HAVE_D3D11)
+ MP_MSG(ctx, vo->probing ? MSGL_V : MSGL_ERR,
+ "libplacebo was built without D3D11 support.\n");
+ return false;
+#else // defined(PL_HAVE_D3D11)
+ bool success = false;
+
+ ID3D11Device *device = ra_d3d11_get_device(ctx->ra_ctx->ra);
+ IDXGISwapChain *swapchain = ra_d3d11_ctx_get_swapchain(ctx->ra_ctx);
+ if (!device || !swapchain) {
+ mp_err(ctx->log,
+ "Failed to receive required components from the mpv d3d11 "
+ "context! (device: %s, swap chain: %s)\n",
+ device ? "OK" : "failed",
+ swapchain ? "OK" : "failed");
+ goto err_out;
+ }
+
+ pl_d3d11 d3d11 = pl_d3d11_create(ctx->pllog,
+ pl_d3d11_params(
+ .device = device,
+ )
+ );
+ if (!d3d11) {
+ mp_err(ctx->log, "Failed to acquire a d3d11 libplacebo context!\n");
+ goto err_out;
+ }
+ ctx->gpu = d3d11->gpu;
+
+ mppl_log_set_probing(ctx->pllog, false);
+
+ ctx->swapchain = pl_d3d11_create_swapchain(d3d11,
+ pl_d3d11_swapchain_params(
+ .swapchain = swapchain,
+ )
+ );
+ if (!ctx->swapchain) {
+ mp_err(ctx->log, "Failed to acquire a d3d11 libplacebo swap chain!\n");
+ goto err_out;
+ }
+
+ success = true;
+
+err_out:
+ SAFE_RELEASE(swapchain);
+ SAFE_RELEASE(device);
+
+ return success;
+#endif // defined(PL_HAVE_D3D11)
+}
+#endif // HAVE_D3D11
+
+struct gpu_ctx *gpu_ctx_create(struct vo *vo, struct gl_video_opts *gl_opts)
+{
+ struct gpu_ctx *ctx = talloc_zero(NULL, struct gpu_ctx);
+ ctx->log = vo->log;
+
+ struct ra_ctx_opts *ctx_opts = mp_get_config_group(ctx, vo->global, &ra_ctx_conf);
+ ctx_opts->want_alpha = gl_opts->alpha_mode == ALPHA_YES;
+ ctx->ra_ctx = ra_ctx_create(vo, *ctx_opts);
+ if (!ctx->ra_ctx)
+ goto err_out;
+
+#if HAVE_VULKAN
+ struct mpvk_ctx *vkctx = ra_vk_ctx_get(ctx->ra_ctx);
+ if (vkctx) {
+ ctx->pllog = vkctx->pllog;
+ ctx->gpu = vkctx->gpu;
+ ctx->swapchain = vkctx->swapchain;
+ return ctx;
+ }
+#endif
+
+ ctx->pllog = mppl_log_create(ctx, ctx->log);
+ if (!ctx->pllog)
+ goto err_out;
+
+ mppl_log_set_probing(ctx->pllog, vo->probing);
+
+#if HAVE_D3D11
+ if (ra_is_d3d11(ctx->ra_ctx->ra)) {
+ if (!d3d11_pl_init(vo, ctx, ctx_opts))
+ goto err_out;
+
+ return ctx;
+ }
+#endif
+
+#if HAVE_GL && defined(PL_HAVE_OPENGL)
+ if (ra_is_gl(ctx->ra_ctx->ra)) {
+ struct GL *gl = ra_gl_get(ctx->ra_ctx->ra);
+ pl_opengl opengl = pl_opengl_create(ctx->pllog,
+ pl_opengl_params(
+ .debug = ctx_opts->debug,
+ .allow_software = ctx_opts->allow_sw,
+ .get_proc_addr_ex = (void *) gl->get_fn,
+ .proc_ctx = gl->fn_ctx,
+# if HAVE_EGL
+ .egl_display = eglGetCurrentDisplay(),
+ .egl_context = eglGetCurrentContext(),
+# endif
+ )
+ );
+ if (!opengl)
+ goto err_out;
+ ctx->gpu = opengl->gpu;
+
+ mppl_log_set_probing(ctx->pllog, false);
+
+ ctx->swapchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(
+ .max_swapchain_depth = vo->opts->swapchain_depth,
+ .framebuffer.flipped = gl->flipped,
+ ));
+ if (!ctx->swapchain)
+ goto err_out;
+
+ return ctx;
+ }
+#elif HAVE_GL
+ if (ra_is_gl(ctx->ra_ctx->ra)) {
+ MP_MSG(ctx, vo->probing ? MSGL_V : MSGL_ERR,
+ "libplacebo was built without OpenGL support.\n");
+ }
+#endif
+
+err_out:
+ gpu_ctx_destroy(&ctx);
+ return NULL;
+}
+
+bool gpu_ctx_resize(struct gpu_ctx *ctx, int w, int h)
+{
+#if HAVE_VULKAN
+ if (ra_vk_ctx_get(ctx->ra_ctx))
+ // vulkan RA handles this by itself
+ return true;
+#endif
+
+ return pl_swapchain_resize(ctx->swapchain, &w, &h);
+}
+
+void gpu_ctx_destroy(struct gpu_ctx **ctxp)
+{
+ struct gpu_ctx *ctx = *ctxp;
+ if (!ctx)
+ return;
+ if (!ctx->ra_ctx)
+ goto skip_common_pl_cleanup;
+
+#if HAVE_VULKAN
+ if (ra_vk_ctx_get(ctx->ra_ctx))
+ // vulkan RA context handles pl cleanup by itself,
+ // skip common local clean-up.
+ goto skip_common_pl_cleanup;
+#endif
+
+ if (ctx->swapchain)
+ pl_swapchain_destroy(&ctx->swapchain);
+
+ if (ctx->gpu) {
+#if HAVE_GL && defined(PL_HAVE_OPENGL)
+ if (ra_is_gl(ctx->ra_ctx->ra)) {
+ pl_opengl opengl = pl_opengl_get(ctx->gpu);
+ pl_opengl_destroy(&opengl);
+ }
+#endif
+
+#if HAVE_D3D11 && defined(PL_HAVE_D3D11)
+ if (ra_is_d3d11(ctx->ra_ctx->ra)) {
+ pl_d3d11 d3d11 = pl_d3d11_get(ctx->gpu);
+ pl_d3d11_destroy(&d3d11);
+ }
+#endif
+ }
+
+ if (ctx->pllog)
+ pl_log_destroy(&ctx->pllog);
+
+skip_common_pl_cleanup:
+ ra_ctx_destroy(&ctx->ra_ctx);
+
+ talloc_free(ctx);
+ *ctxp = NULL;
+}
diff --git a/video/out/gpu_next/context.h b/video/out/gpu_next/context.h
new file mode 100644
index 0000000..b98b9e7
--- /dev/null
+++ b/video/out/gpu_next/context.h
@@ -0,0 +1,40 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libplacebo/renderer.h>
+
+struct mp_log;
+struct ra_ctx;
+struct vo;
+struct gl_video_opts;
+
+struct gpu_ctx {
+ struct mp_log *log;
+ struct ra_ctx *ra_ctx;
+
+ pl_log pllog;
+ pl_gpu gpu;
+ pl_swapchain swapchain;
+
+ void *priv;
+};
+
+struct gpu_ctx *gpu_ctx_create(struct vo *vo, struct gl_video_opts *gl_opts);
+bool gpu_ctx_resize(struct gpu_ctx *ctx, int w, int h);
+void gpu_ctx_destroy(struct gpu_ctx **ctxp);
diff --git a/video/out/hwdec/dmabuf_interop.h b/video/out/hwdec/dmabuf_interop.h
new file mode 100644
index 0000000..e9b3e8e
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libavutil/hwcontext_drm.h>
+
+#include "video/out/gpu/hwdec.h"
+
+struct dmabuf_interop {
+ bool use_modifiers;
+ bool composed_layers;
+
+ bool (*interop_init)(struct ra_hwdec_mapper *mapper,
+ const struct ra_imgfmt_desc *desc);
+ void (*interop_uninit)(const struct ra_hwdec_mapper *mapper);
+
+ bool (*interop_map)(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing);
+ void (*interop_unmap)(struct ra_hwdec_mapper *mapper);
+};
+
+struct dmabuf_interop_priv {
+ int num_planes;
+ struct mp_image layout;
+ struct ra_tex *tex[4];
+
+ AVDRMFrameDescriptor desc;
+ bool surface_acquired;
+
+ void *interop_mapper_priv;
+};
+
+typedef bool (*dmabuf_interop_init)(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
+
+bool dmabuf_interop_gl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
+bool dmabuf_interop_pl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
+bool dmabuf_interop_wl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
diff --git a/video/out/hwdec/dmabuf_interop_gl.c b/video/out/hwdec/dmabuf_interop_gl.c
new file mode 100644
index 0000000..e7fb103
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop_gl.c
@@ -0,0 +1,311 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "dmabuf_interop.h"
+
+#include <drm_fourcc.h>
+#include <EGL/egl.h>
+#include "video/out/opengl/ra_gl.h"
+
+typedef void* GLeglImageOES;
+typedef void *EGLImageKHR;
+
+// Any EGL_EXT_image_dma_buf_import definitions used in this source file.
+#define EGL_LINUX_DMA_BUF_EXT 0x3270
+#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
+#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
+#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
+#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
+#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275
+#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276
+#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277
+#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278
+#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279
+#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A
+
+
+// Any EGL_EXT_image_dma_buf_import definitions used in this source file.
+#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440
+#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441
+#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442
+#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
+#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
+#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445
+#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446
+#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447
+#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448
+#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449
+#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A
+
+struct vaapi_gl_mapper_priv {
+ GLuint gl_textures[4];
+ EGLImageKHR images[4];
+
+ EGLImageKHR (EGLAPIENTRY *CreateImageKHR)(EGLDisplay, EGLContext,
+ EGLenum, EGLClientBuffer,
+ const EGLint *);
+ EGLBoolean (EGLAPIENTRY *DestroyImageKHR)(EGLDisplay, EGLImageKHR);
+ void (EGLAPIENTRY *EGLImageTargetTexture2DOES)(GLenum, GLeglImageOES);
+};
+
+static bool vaapi_gl_mapper_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_imgfmt_desc *desc)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = talloc_ptrtype(NULL, p);
+ p_mapper->interop_mapper_priv = p;
+
+ *p = (struct vaapi_gl_mapper_priv) {
+ // EGL_KHR_image_base
+ .CreateImageKHR = (void *)eglGetProcAddress("eglCreateImageKHR"),
+ .DestroyImageKHR = (void *)eglGetProcAddress("eglDestroyImageKHR"),
+ // GL_OES_EGL_image
+ .EGLImageTargetTexture2DOES =
+ (void *)eglGetProcAddress("glEGLImageTargetTexture2DOES"),
+ };
+
+ if (!p->CreateImageKHR || !p->DestroyImageKHR ||
+ !p->EGLImageTargetTexture2DOES)
+ return false;
+
+ GL *gl = ra_gl_get(mapper->ra);
+ gl->GenTextures(4, p->gl_textures);
+ for (int n = 0; n < desc->num_planes; n++) {
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_textures[n]);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mp_image_plane_w(&p_mapper->layout, n),
+ .h = mp_image_plane_h(&p_mapper->layout, n),
+ .d = 1,
+ .format = desc->planes[n],
+ .render_src = true,
+ .src_linear = true,
+ };
+
+ if (params.format->ctype != RA_CTYPE_UNORM)
+ return false;
+
+ p_mapper->tex[n] = ra_create_wrapped_tex(mapper->ra, &params,
+ p->gl_textures[n]);
+ if (!p_mapper->tex[n])
+ return false;
+ }
+
+ return true;
+}
+
+static void vaapi_gl_mapper_uninit(const struct ra_hwdec_mapper *mapper)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = p_mapper->interop_mapper_priv;
+
+ if (p) {
+ GL *gl = ra_gl_get(mapper->ra);
+ gl->DeleteTextures(4, p->gl_textures);
+ for (int n = 0; n < 4; n++) {
+ p->gl_textures[n] = 0;
+ ra_tex_free(mapper->ra, &p_mapper->tex[n]);
+ }
+ talloc_free(p);
+ p_mapper->interop_mapper_priv = NULL;
+ }
+}
+
+#define ADD_ATTRIB(name, value) \
+ do { \
+ assert(num_attribs + 3 < MP_ARRAY_SIZE(attribs)); \
+ attribs[num_attribs++] = (name); \
+ attribs[num_attribs++] = (value); \
+ attribs[num_attribs] = EGL_NONE; \
+ } while(0)
+
+#define ADD_PLANE_ATTRIBS(plane) do { \
+ uint64_t drm_format_modifier = p_mapper->desc.objects[p_mapper->desc.layers[i].planes[j].object_index].format_modifier; \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _FD_EXT, \
+ p_mapper->desc.objects[p_mapper->desc.layers[i].planes[j].object_index].fd); \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _OFFSET_EXT, \
+ p_mapper->desc.layers[i].planes[j].offset); \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _PITCH_EXT, \
+ p_mapper->desc.layers[i].planes[j].pitch); \
+ if (dmabuf_interop->use_modifiers && drm_format_modifier != DRM_FORMAT_MOD_INVALID) { \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _MODIFIER_LO_EXT, drm_format_modifier & 0xfffffffful); \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _MODIFIER_HI_EXT, drm_format_modifier >> 32); \
+ } \
+ } while (0)
+
+static bool vaapi_gl_map(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = p_mapper->interop_mapper_priv;
+
+ GL *gl = ra_gl_get(mapper->ra);
+
+ for (int i = 0, n = 0; i < p_mapper->desc.nb_layers; i++) {
+ /*
+ * As we must map surfaces as one texture per plane, we can only support
+ * a subset of possible multi-plane layer formats. This is due to having
+ * to manually establish what DRM format each synthetic layer should
+ * have.
+ */
+ uint32_t format[AV_DRM_MAX_PLANES] = {
+ p_mapper->desc.layers[i].format,
+ };
+
+ if (p_mapper->desc.layers[i].nb_planes > 1) {
+ switch (p_mapper->desc.layers[i].format) {
+ case DRM_FORMAT_NV12:
+ case DRM_FORMAT_NV16:
+ format[0] = DRM_FORMAT_R8;
+ format[1] = DRM_FORMAT_GR88;
+ break;
+ case DRM_FORMAT_YUV420:
+ format[0] = DRM_FORMAT_R8;
+ format[1] = DRM_FORMAT_R8;
+ format[2] = DRM_FORMAT_R8;
+ break;
+ case DRM_FORMAT_P010:
+#ifdef DRM_FORMAT_P030 /* Format added in a newer libdrm version than minimum */
+ case DRM_FORMAT_P030:
+#endif
+ format[0] = DRM_FORMAT_R16;
+ format[1] = DRM_FORMAT_GR1616;
+ break;
+ default:
+ mp_msg(mapper->log, probing ? MSGL_DEBUG : MSGL_ERR,
+ "Cannot map unknown multi-plane format: 0x%08X\n",
+ p_mapper->desc.layers[i].format);
+ return false;
+ }
+ } else {
+ /*
+ * As OpenGL only has one guaranteed rgba format (rgba8), drivers
+ * that support importing dmabuf formats with different channel
+ * orders do implicit swizzling to get to rgba. However, we look at
+ * the original imgfmt to decide channel order, and we then swizzle
+ * based on that. So, we can get into a situation where we swizzle
+ * twice and end up with a mess.
+ *
+ * The simplest way to avoid that is to lie to OpenGL and say that
+ * the surface we are importing is in the natural channel order, so
+ * that our swizzling does the right thing.
+ *
+ * DRM ABGR corresponds to OpenGL RGBA due to different naming
+ * conventions.
+ */
+ switch (format[0]) {
+ case DRM_FORMAT_ARGB8888:
+ case DRM_FORMAT_RGBA8888:
+ case DRM_FORMAT_BGRA8888:
+ format[0] = DRM_FORMAT_ABGR8888;
+ break;
+ case DRM_FORMAT_XRGB8888:
+ format[0] = DRM_FORMAT_XBGR8888;
+ break;
+ case DRM_FORMAT_RGBX8888:
+ case DRM_FORMAT_BGRX8888:
+ // Logically, these two formats should be handled as above,
+ // but there appear to be additional problems that make the
+ // format change here insufficient or incorrect, so we're
+ // doing nothing for now.
+ break;
+ }
+ }
+
+ for (int j = 0; j < p_mapper->desc.layers[i].nb_planes; j++, n++) {
+ int attribs[48] = {EGL_NONE};
+ int num_attribs = 0;
+
+ ADD_ATTRIB(EGL_LINUX_DRM_FOURCC_EXT, format[j]);
+ ADD_ATTRIB(EGL_WIDTH, p_mapper->tex[n]->params.w);
+ ADD_ATTRIB(EGL_HEIGHT, p_mapper->tex[n]->params.h);
+ ADD_PLANE_ATTRIBS(0);
+
+ p->images[n] = p->CreateImageKHR(eglGetCurrentDisplay(),
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
+ if (!p->images[n]) {
+ mp_msg(mapper->log, probing ? MSGL_DEBUG : MSGL_ERR,
+ "Failed to import surface in EGL: %u\n", eglGetError());
+ return false;
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_textures[n]);
+ p->EGLImageTargetTexture2DOES(GL_TEXTURE_2D, p->images[n]);
+
+ mapper->tex[n] = p_mapper->tex[n];
+ }
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+ return true;
+}
+
+static void vaapi_gl_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = p_mapper->interop_mapper_priv;
+
+ if (p) {
+ for (int n = 0; n < 4; n++) {
+ if (p->images[n])
+ p->DestroyImageKHR(eglGetCurrentDisplay(), p->images[n]);
+ p->images[n] = 0;
+ }
+ }
+}
+
+bool dmabuf_interop_gl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop)
+{
+ if (!ra_is_gl(hw->ra_ctx->ra)) {
+ // This is not an OpenGL RA.
+ return false;
+ }
+
+ if (!eglGetCurrentContext())
+ return false;
+
+ const char *exts = eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS);
+ if (!exts)
+ return false;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (!gl_check_extension(exts, "EGL_EXT_image_dma_buf_import") ||
+ !gl_check_extension(exts, "EGL_KHR_image_base") ||
+ !gl_check_extension(gl->extensions, "GL_OES_EGL_image") ||
+ !(gl->mpgl_caps & MPGL_CAP_TEX_RG))
+ return false;
+
+ dmabuf_interop->use_modifiers =
+ gl_check_extension(exts, "EGL_EXT_image_dma_buf_import_modifiers");
+
+ MP_VERBOSE(hw, "using EGL dmabuf interop\n");
+
+ dmabuf_interop->interop_init = vaapi_gl_mapper_init;
+ dmabuf_interop->interop_uninit = vaapi_gl_mapper_uninit;
+ dmabuf_interop->interop_map = vaapi_gl_map;
+ dmabuf_interop->interop_unmap = vaapi_gl_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/dmabuf_interop_pl.c b/video/out/hwdec/dmabuf_interop_pl.c
new file mode 100644
index 0000000..0a8ec5b
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop_pl.c
@@ -0,0 +1,138 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+
+#include "dmabuf_interop.h"
+#include "video/out/placebo/ra_pl.h"
+#include "video/out/placebo/utils.h"
+
+static bool vaapi_pl_map(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing)
+{
+ struct dmabuf_interop_priv *p = mapper->priv;
+ pl_gpu gpu = ra_pl_get(mapper->ra);
+
+ struct ra_imgfmt_desc desc = {0};
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return false;
+
+ // The calling code validates that the total number of exported planes
+ // equals the number we expected in p->num_planes.
+ int layer = 0;
+ int layer_plane = 0;
+ for (int n = 0; n < p->num_planes; n++) {
+
+ const struct ra_format *format = desc.planes[n];
+ int id = p->desc.layers[layer].planes[layer_plane].object_index;
+ int fd = p->desc.objects[id].fd;
+ uint32_t size = p->desc.objects[id].size;
+ uint32_t offset = p->desc.layers[layer].planes[layer_plane].offset;
+ uint32_t pitch = p->desc.layers[layer].planes[layer_plane].pitch;
+
+ // AMD drivers do not return the size in the surface description, so we
+ // need to query it manually.
+ if (size == 0) {
+ size = lseek(fd, 0, SEEK_END);
+ if (size == -1) {
+ MP_ERR(mapper, "Cannot obtain size of object with fd %d: %s\n",
+ fd, mp_strerror(errno));
+ return false;
+ }
+ off_t err = lseek(fd, 0, SEEK_SET);
+ if (err == -1) {
+ MP_ERR(mapper, "Failed to reset offset for fd %d: %s\n",
+ fd, mp_strerror(errno));
+ return false;
+ }
+ }
+
+ struct pl_tex_params tex_params = {
+ .w = mp_image_plane_w(&p->layout, n),
+ .h = mp_image_plane_h(&p->layout, n),
+ .d = 0,
+ .format = format->priv,
+ .sampleable = true,
+ .import_handle = PL_HANDLE_DMA_BUF,
+ .shared_mem = (struct pl_shared_mem) {
+ .handle = {
+ .fd = fd,
+ },
+ .size = size,
+ .offset = offset,
+ .drm_format_mod = p->desc.objects[id].format_modifier,
+ .stride_w = pitch,
+ },
+ };
+
+ mppl_log_set_probing(gpu->log, probing);
+ pl_tex pltex = pl_tex_create(gpu, &tex_params);
+ mppl_log_set_probing(gpu->log, false);
+ if (!pltex)
+ return false;
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ int ret = mppl_wrap_tex(mapper->ra, pltex, ratex);
+ if (!ret) {
+ pl_tex_destroy(gpu, &pltex);
+ talloc_free(ratex);
+ return false;
+ }
+ mapper->tex[n] = ratex;
+
+ MP_TRACE(mapper, "Object %d with fd %d imported as %p\n",
+ id, fd, ratex);
+
+ layer_plane++;
+ if (layer_plane == p->desc.layers[layer].nb_planes) {
+ layer_plane = 0;
+ layer++;
+ }
+ }
+ return true;
+}
+
+static void vaapi_pl_unmap(struct ra_hwdec_mapper *mapper)
+{
+ for (int n = 0; n < 4; n++)
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+}
+
+bool dmabuf_interop_pl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop)
+{
+ pl_gpu gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (!gpu) {
+ // This is not a libplacebo RA;
+ return false;
+ }
+
+ if (!(gpu->import_caps.tex & PL_HANDLE_DMA_BUF)) {
+ MP_VERBOSE(hw, "libplacebo dmabuf interop requires support for "
+ "PL_HANDLE_DMA_BUF import.\n");
+ return false;
+ }
+
+ MP_VERBOSE(hw, "using libplacebo dmabuf interop\n");
+
+ dmabuf_interop->interop_map = vaapi_pl_map;
+ dmabuf_interop->interop_unmap = vaapi_pl_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/dmabuf_interop_wl.c b/video/out/hwdec/dmabuf_interop_wl.c
new file mode 100644
index 0000000..606a0aa
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop_wl.c
@@ -0,0 +1,83 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "video/out/wldmabuf/ra_wldmabuf.h"
+#include "dmabuf_interop.h"
+
+static bool mapper_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_imgfmt_desc *desc)
+{
+ return true;
+}
+
+static void mapper_uninit(const struct ra_hwdec_mapper *mapper)
+{
+}
+
+static bool map(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing)
+{
+ // 1. only validate format when composed layers is enabled (i.e. vaapi)
+ // 2. for drmprime, just return true for now, as this use case
+ // has not been tested.
+ if (!dmabuf_interop->composed_layers)
+ return true;
+
+ int layer_no = 0;
+ struct dmabuf_interop_priv *mapper_p = mapper->priv;
+ uint32_t drm_format = mapper_p->desc.layers[layer_no].format;
+
+ if (mapper_p->desc.nb_layers != 1) {
+ MP_VERBOSE(mapper, "Mapped surface has separate layers - expected composed layers.\n");
+ return false;
+ } else if (!ra_compatible_format(mapper->ra, drm_format,
+ mapper_p->desc.objects[0].format_modifier)) {
+ MP_VERBOSE(mapper, "Mapped surface with format %s; drm format '%s(%016lx)' "
+ "is not supported by compositor.\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ mp_tag_str(drm_format),
+ mapper_p->desc.objects[0].format_modifier);
+ return false;
+ }
+
+ MP_VERBOSE(mapper, "Supported Wayland display format %s: '%s(%016lx)'\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ mp_tag_str(drm_format), mapper_p->desc.objects[0].format_modifier);
+
+ return true;
+}
+
+static void unmap(struct ra_hwdec_mapper *mapper)
+{
+}
+
+bool dmabuf_interop_wl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop)
+{
+ if (!ra_is_wldmabuf(hw->ra_ctx->ra))
+ return false;
+
+ if (strstr(hw->driver->name, "vaapi") != NULL)
+ dmabuf_interop->composed_layers = true;
+
+ dmabuf_interop->interop_init = mapper_init;
+ dmabuf_interop->interop_uninit = mapper_uninit;
+ dmabuf_interop->interop_map = map;
+ dmabuf_interop->interop_unmap = unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_aimagereader.c b/video/out/hwdec/hwdec_aimagereader.c
new file mode 100644
index 0000000..0dd5497
--- /dev/null
+++ b/video/out/hwdec/hwdec_aimagereader.c
@@ -0,0 +1,402 @@
+/*
+ * Copyright (c) 2021 sfan5 <sfan5@live.de>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <dlfcn.h>
+#include <EGL/egl.h>
+#include <media/NdkImageReader.h>
+#include <android/native_window_jni.h>
+#include <libavcodec/mediacodec.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_mediacodec.h>
+
+#include "misc/jni.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/opengl/ra_gl.h"
+
+typedef void *GLeglImageOES;
+typedef void *EGLImageKHR;
+#define EGL_NATIVE_BUFFER_ANDROID 0x3140
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ AImageReader *reader;
+ jobject surface;
+ void *lib_handle;
+
+ media_status_t (*AImageReader_newWithUsage)(
+ int32_t, int32_t, int32_t, uint64_t, int32_t, AImageReader **);
+ media_status_t (*AImageReader_getWindow)(
+ AImageReader *, ANativeWindow **);
+ media_status_t (*AImageReader_setImageListener)(
+ AImageReader *, AImageReader_ImageListener *);
+ media_status_t (*AImageReader_acquireLatestImage)(AImageReader *, AImage **);
+ void (*AImageReader_delete)(AImageReader *);
+ media_status_t (*AImage_getHardwareBuffer)(const AImage *, AHardwareBuffer **);
+ void (*AImage_delete)(AImage *);
+ void (*AHardwareBuffer_describe)(const AHardwareBuffer *, AHardwareBuffer_Desc *);
+ jobject (*ANativeWindow_toSurface)(JNIEnv *, ANativeWindow *);
+};
+
+struct priv {
+ struct mp_log *log;
+
+ GLuint gl_texture;
+ AImage *image;
+ EGLImageKHR egl_image;
+
+ mp_mutex lock;
+ mp_cond cond;
+ bool image_available;
+
+ EGLImageKHR (EGLAPIENTRY *CreateImageKHR)(
+ EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint *);
+ EGLBoolean (EGLAPIENTRY *DestroyImageKHR)(EGLDisplay, EGLImageKHR);
+ EGLClientBuffer (EGLAPIENTRY *GetNativeClientBufferANDROID)(
+ const struct AHardwareBuffer *);
+ void (EGLAPIENTRY *EGLImageTargetTexture2DOES)(GLenum, GLeglImageOES);
+};
+
+const static struct { const char *symbol; int offset; } lib_functions[] = {
+ { "AImageReader_newWithUsage", offsetof(struct priv_owner, AImageReader_newWithUsage) },
+ { "AImageReader_getWindow", offsetof(struct priv_owner, AImageReader_getWindow) },
+ { "AImageReader_setImageListener", offsetof(struct priv_owner, AImageReader_setImageListener) },
+ { "AImageReader_acquireLatestImage", offsetof(struct priv_owner, AImageReader_acquireLatestImage) },
+ { "AImageReader_delete", offsetof(struct priv_owner, AImageReader_delete) },
+ { "AImage_getHardwareBuffer", offsetof(struct priv_owner, AImage_getHardwareBuffer) },
+ { "AImage_delete", offsetof(struct priv_owner, AImage_delete) },
+ { "AHardwareBuffer_describe", offsetof(struct priv_owner, AHardwareBuffer_describe) },
+ { "ANativeWindow_toSurface", offsetof(struct priv_owner, ANativeWindow_toSurface) },
+ { NULL, 0 },
+};
+
+
+static AVBufferRef *create_mediacodec_device_ref(jobject surface)
+{
+ AVBufferRef *device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_MEDIACODEC);
+ if (!device_ref)
+ return NULL;
+
+ AVHWDeviceContext *ctx = (void *)device_ref->data;
+ AVMediaCodecDeviceContext *hwctx = ctx->hwctx;
+ hwctx->surface = surface;
+
+ if (av_hwdevice_ctx_init(device_ref) < 0)
+ av_buffer_unref(&device_ref);
+
+ return device_ref;
+}
+
+static bool load_lib_functions(struct priv_owner *p, struct mp_log *log)
+{
+ p->lib_handle = dlopen("libmediandk.so", RTLD_NOW | RTLD_GLOBAL);
+ if (!p->lib_handle)
+ return false;
+ for (int i = 0; lib_functions[i].symbol; i++) {
+ const char *sym = lib_functions[i].symbol;
+ void *fun = dlsym(p->lib_handle, sym);
+ if (!fun)
+ fun = dlsym(RTLD_DEFAULT, sym);
+ if (!fun) {
+ mp_warn(log, "Could not resolve symbol %s\n", sym);
+ return false;
+ }
+
+ *(void **) ((uint8_t*)p + lib_functions[i].offset) = fun;
+ }
+ return true;
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return -1;
+ if (!eglGetCurrentContext())
+ return -1;
+
+ const char *exts = eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_ANDROID_image_native_buffer"))
+ return -1;
+
+ if (!load_lib_functions(p, hw->log))
+ return -1;
+
+ static const char *es2_exts[] = {"GL_OES_EGL_image_external", 0};
+ static const char *es3_exts[] = {"GL_OES_EGL_image_external_essl3", 0};
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl_check_extension(gl->extensions, es3_exts[0]))
+ hw->glsl_extensions = es3_exts;
+ else
+ hw->glsl_extensions = es2_exts;
+
+ // dummy dimensions, AImageReader only transports hardware buffers
+ media_status_t ret = p->AImageReader_newWithUsage(16, 16,
+ AIMAGE_FORMAT_PRIVATE, AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
+ 5, &p->reader);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(hw, "newWithUsage failed: %d\n", ret);
+ return -1;
+ }
+ assert(p->reader);
+
+ ANativeWindow *window;
+ ret = p->AImageReader_getWindow(p->reader, &window);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(hw, "getWindow failed: %d\n", ret);
+ return -1;
+ }
+ assert(window);
+
+ JNIEnv *env = MP_JNI_GET_ENV(hw);
+ assert(env);
+ jobject surface = p->ANativeWindow_toSurface(env, window);
+ p->surface = (*env)->NewGlobalRef(env, surface);
+ (*env)->DeleteLocalRef(env, surface);
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .av_device_ref = create_mediacodec_device_ref(p->surface),
+ .hw_imgfmt = IMGFMT_MEDIACODEC,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+}
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(hw);
+ assert(env);
+
+ if (p->surface) {
+ (*env)->DeleteGlobalRef(env, p->surface);
+ p->surface = NULL;
+ }
+
+ if (p->reader) {
+ p->AImageReader_delete(p->reader);
+ p->reader = NULL;
+ }
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->lib_handle) {
+ dlclose(p->lib_handle);
+ p->lib_handle = NULL;
+ }
+}
+
+static void image_callback(void *context, AImageReader *reader)
+{
+ struct priv *p = context;
+
+ mp_mutex_lock(&p->lock);
+ p->image_available = true;
+ mp_cond_signal(&p->cond);
+ mp_mutex_unlock(&p->lock);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ p->log = mapper->log;
+ mp_mutex_init(&p->lock);
+ mp_cond_init(&p->cond);
+
+ p->CreateImageKHR = (void *)eglGetProcAddress("eglCreateImageKHR");
+ p->DestroyImageKHR = (void *)eglGetProcAddress("eglDestroyImageKHR");
+ p->GetNativeClientBufferANDROID =
+ (void *)eglGetProcAddress("eglGetNativeClientBufferANDROID");
+ p->EGLImageTargetTexture2DOES =
+ (void *)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+
+ if (!p->CreateImageKHR || !p->DestroyImageKHR ||
+ !p->GetNativeClientBufferANDROID || !p->EGLImageTargetTexture2DOES)
+ return -1;
+
+ AImageReader_ImageListener listener = {
+ .context = p,
+ .onImageAvailable = image_callback,
+ };
+ o->AImageReader_setImageListener(o->reader, &listener);
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+
+ // texture creation
+ gl->GenTextures(1, &p->gl_texture);
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, p->gl_texture);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, 4),
+ .render_src = true,
+ .src_linear = true,
+ .external_oes = true,
+ };
+
+ if (params.format->ctype != RA_CTYPE_UNORM)
+ return -1;
+
+ mapper->tex[0] = ra_create_wrapped_tex(mapper->ra, &params, p->gl_texture);
+ if (!mapper->tex[0])
+ return -1;
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ o->AImageReader_setImageListener(o->reader, NULL);
+
+ gl->DeleteTextures(1, &p->gl_texture);
+ p->gl_texture = 0;
+
+ ra_tex_free(mapper->ra, &mapper->tex[0]);
+
+ mp_mutex_destroy(&p->lock);
+ mp_cond_destroy(&p->cond);
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+
+ if (p->egl_image) {
+ p->DestroyImageKHR(eglGetCurrentDisplay(), p->egl_image);
+ p->egl_image = 0;
+ }
+
+ if (p->image) {
+ o->AImage_delete(p->image);
+ p->image = NULL;
+ }
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ {
+ if (mapper->src->imgfmt != IMGFMT_MEDIACODEC)
+ return -1;
+ AVMediaCodecBuffer *buffer = (AVMediaCodecBuffer *)mapper->src->planes[3];
+ av_mediacodec_release_buffer(buffer, 1);
+ }
+
+ bool image_available = false;
+ mp_mutex_lock(&p->lock);
+ if (!p->image_available) {
+ mp_cond_timedwait(&p->cond, &p->lock, MP_TIME_MS_TO_NS(100));
+ if (!p->image_available)
+ MP_WARN(mapper, "Waiting for frame timed out!\n");
+ }
+ image_available = p->image_available;
+ p->image_available = false;
+ mp_mutex_unlock(&p->lock);
+
+ media_status_t ret = o->AImageReader_acquireLatestImage(o->reader, &p->image);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(mapper, "acquireLatestImage failed: %d\n", ret);
+ // If we merely timed out waiting return success anyway to avoid
+ // flashing frames of render errors.
+ return image_available ? -1 : 0;
+ }
+ assert(p->image);
+
+ AHardwareBuffer *hwbuf = NULL;
+ ret = o->AImage_getHardwareBuffer(p->image, &hwbuf);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(mapper, "getHardwareBuffer failed: %d\n", ret);
+ return -1;
+ }
+ assert(hwbuf);
+
+ // Update texture size since it may differ
+ AHardwareBuffer_Desc d;
+ o->AHardwareBuffer_describe(hwbuf, &d);
+ if (mapper->tex[0]->params.w != d.width || mapper->tex[0]->params.h != d.height) {
+ MP_VERBOSE(p, "Texture dimensions changed to %dx%d\n", d.width, d.height);
+ mapper->tex[0]->params.w = d.width;
+ mapper->tex[0]->params.h = d.height;
+ }
+
+ EGLClientBuffer buf = p->GetNativeClientBufferANDROID(hwbuf);
+ if (!buf)
+ return -1;
+
+ const int attribs[] = {EGL_NONE};
+ p->egl_image = p->CreateImageKHR(eglGetCurrentDisplay(),
+ EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, buf, attribs);
+ if (!p->egl_image)
+ return -1;
+
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, p->gl_texture);
+ p->EGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, p->egl_image);
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ return 0;
+}
+
+
+const struct ra_hwdec_driver ra_hwdec_aimagereader = {
+ .name = "aimagereader",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_MEDIACODEC, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_cuda.c b/video/out/hwdec/hwdec_cuda.c
new file mode 100644
index 0000000..68ad60d
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda.c
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2016 Philip Langdale <philipl@overt.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * This hwdec implements an optimized output path using CUDA->OpenGL
+ * or CUDA->Vulkan interop for frame data that is stored in CUDA
+ * device memory. Although it is not explicit in the code here, the
+ * only practical way to get data in this form is from the
+ * nvdec/cuvid decoder.
+ */
+
+#include "config.h"
+#include "hwdec_cuda.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+
+int check_cu(const struct ra_hwdec *hw, CUresult err, const char *func)
+{
+ const char *err_name;
+ const char *err_string;
+
+ struct cuda_hw_priv *p = hw->priv;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+
+ MP_TRACE(hw, "Calling %s\n", func);
+
+ if (err == CUDA_SUCCESS)
+ return 0;
+
+ p->cu->cuGetErrorName(err, &err_name);
+ p->cu->cuGetErrorString(err, &err_string);
+
+ MP_MSG(hw, level, "%s failed", func);
+ if (err_name && err_string)
+ MP_MSG(hw, level, " -> %s: %s", err_name, err_string);
+ MP_MSG(hw, level, "\n");
+
+ return -1;
+}
+
+#define CHECK_CU(x) check_cu(hw, (x), #x)
+
+const static cuda_interop_init interop_inits[] = {
+#if HAVE_GL
+ cuda_gl_init,
+#endif
+#if HAVE_VULKAN
+ cuda_vk_init,
+#endif
+ NULL
+};
+
+static int cuda_init(struct ra_hwdec *hw)
+{
+ AVBufferRef *hw_device_ctx = NULL;
+ CUcontext dummy;
+ int ret = 0;
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+
+ ret = cuda_load_functions(&p->cu, NULL);
+ if (ret != 0) {
+ MP_MSG(hw, level, "Failed to load CUDA symbols\n");
+ return -1;
+ }
+ cu = p->cu;
+
+ ret = CHECK_CU(cu->cuInit(0));
+ if (ret < 0)
+ return -1;
+
+ // Initialise CUDA context from backend.
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw)) {
+ break;
+ }
+ }
+
+ if (!p->ext_init || !p->ext_uninit) {
+ MP_MSG(hw, level,
+ "CUDA hwdec only works with OpenGL or Vulkan backends.\n");
+ return -1;
+ }
+
+ hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
+ if (!hw_device_ctx)
+ goto error;
+
+ AVHWDeviceContext *device_ctx = (void *)hw_device_ctx->data;
+
+ AVCUDADeviceContext *device_hwctx = device_ctx->hwctx;
+ device_hwctx->cuda_ctx = p->decode_ctx;
+
+ ret = av_hwdevice_ctx_init(hw_device_ctx);
+ if (ret < 0) {
+ MP_MSG(hw, level, "av_hwdevice_ctx_init failed\n");
+ goto error;
+ }
+
+ ret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (ret < 0)
+ goto error;
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .av_device_ref = hw_device_ctx,
+ .hw_imgfmt = IMGFMT_CUDA,
+ };
+ hwdec_devices_add(hw->devs, &p->hwctx);
+ return 0;
+
+ error:
+ av_buffer_unref(&hw_device_ctx);
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+
+ return -1;
+}
+
+static void cuda_uninit(struct ra_hwdec *hw)
+{
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu = p->cu;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->decode_ctx && p->decode_ctx != p->display_ctx)
+ CHECK_CU(cu->cuCtxDestroy(p->decode_ctx));
+
+ if (p->display_ctx)
+ CHECK_CU(cu->cuCtxDestroy(p->display_ctx));
+
+ cuda_free_functions(&p->cu);
+}
+
+#undef CHECK_CU
+#define CHECK_CU(x) check_cu((mapper)->owner, (x), #x)
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CUcontext dummy;
+ CudaFunctions *cu = p_owner->cu;
+ int ret = 0, eret = 0;
+
+ p->display_ctx = p_owner->display_ctx;
+
+ int imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = imgfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ struct ra_imgfmt_desc desc;
+ if (!ra_get_imgfmt_desc(mapper->ra, imgfmt, &desc)) {
+ MP_ERR(mapper, "Unsupported format: %s\n", mp_imgfmt_to_name(imgfmt));
+ return -1;
+ }
+
+ ret = CHECK_CU(cu->cuCtxPushCurrent(p->display_ctx));
+ if (ret < 0)
+ return ret;
+
+ for (int n = 0; n < desc.num_planes; n++) {
+ if (!p_owner->ext_init(mapper, desc.planes[n], n))
+ goto error;
+ }
+
+ error:
+ eret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (eret < 0)
+ return eret;
+
+ return ret;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct cuda_mapper_priv *p = mapper->priv;
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ CudaFunctions *cu = p_owner->cu;
+ CUcontext dummy;
+
+ // Don't bail if any CUDA calls fail. This is all best effort.
+ CHECK_CU(cu->cuCtxPushCurrent(p->display_ctx));
+ for (int n = 0; n < 4; n++) {
+ p_owner->ext_uninit(mapper, n);
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+ }
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct cuda_mapper_priv *p = mapper->priv;
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ CudaFunctions *cu = p_owner->cu;
+ CUcontext dummy;
+ int ret = 0, eret = 0;
+
+ ret = CHECK_CU(cu->cuCtxPushCurrent(p->display_ctx));
+ if (ret < 0)
+ return ret;
+
+ for (int n = 0; n < p->layout.num_planes; n++) {
+ if (p_owner->ext_wait) {
+ if (!p_owner->ext_wait(mapper, n))
+ goto error;
+ }
+
+ CUDA_MEMCPY2D cpy = {
+ .srcMemoryType = CU_MEMORYTYPE_DEVICE,
+ .srcDevice = (CUdeviceptr)mapper->src->planes[n],
+ .srcPitch = mapper->src->stride[n],
+ .srcY = 0,
+ .dstMemoryType = CU_MEMORYTYPE_ARRAY,
+ .dstArray = p->cu_array[n],
+ .WidthInBytes = mp_image_plane_w(&p->layout, n) *
+ mapper->tex[n]->params.format->pixel_size,
+ .Height = mp_image_plane_h(&p->layout, n),
+ };
+
+ ret = CHECK_CU(cu->cuMemcpy2DAsync(&cpy, 0));
+ if (ret < 0)
+ goto error;
+
+ if (p_owner->ext_signal) {
+ if (!p_owner->ext_signal(mapper, n))
+ goto error;
+ }
+ }
+ if (p_owner->do_full_sync)
+ CHECK_CU(cu->cuStreamSynchronize(0));
+
+ // fall through
+ error:
+
+ // Regardless of success or failure, we no longer need the source image,
+ // because this hwdec makes an explicit memcpy into the mapper textures
+ mp_image_unrefp(&mapper->src);
+
+ eret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (eret < 0)
+ return eret;
+
+ return ret;
+}
+
+const struct ra_hwdec_driver ra_hwdec_cuda = {
+ .name = "cuda",
+ .imgfmts = {IMGFMT_CUDA, 0},
+ .priv_size = sizeof(struct cuda_hw_priv),
+ .init = cuda_init,
+ .uninit = cuda_uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct cuda_mapper_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_cuda.h b/video/out/hwdec/hwdec_cuda.h
new file mode 100644
index 0000000..9c55053
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019 Philip Langdale <philipl@overt.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <ffnvcodec/dynlink_loader.h>
+
+#include "video/out/gpu/hwdec.h"
+
+struct cuda_hw_priv {
+ struct mp_hwdec_ctx hwctx;
+ CudaFunctions *cu;
+ CUcontext display_ctx;
+ CUcontext decode_ctx;
+
+ // Do we need to do a full CPU sync after copying
+ bool do_full_sync;
+
+ bool (*ext_init)(struct ra_hwdec_mapper *mapper,
+ const struct ra_format *format, int n);
+ void (*ext_uninit)(const struct ra_hwdec_mapper *mapper, int n);
+
+ // These are only necessary if the gpu api requires synchronisation
+ bool (*ext_wait)(const struct ra_hwdec_mapper *mapper, int n);
+ bool (*ext_signal)(const struct ra_hwdec_mapper *mapper, int n);
+};
+
+struct cuda_mapper_priv {
+ struct mp_image layout;
+ CUarray cu_array[4];
+
+ CUcontext display_ctx;
+
+ void *ext[4];
+};
+
+typedef bool (*cuda_interop_init)(const struct ra_hwdec *hw);
+
+bool cuda_gl_init(const struct ra_hwdec *hw);
+
+bool cuda_vk_init(const struct ra_hwdec *hw);
+
+int check_cu(const struct ra_hwdec *hw, CUresult err, const char *func);
diff --git a/video/out/hwdec/hwdec_cuda_gl.c b/video/out/hwdec/hwdec_cuda_gl.c
new file mode 100644
index 0000000..f20540e
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda_gl.c
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2019 Philip Langdale <philipl@overt.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "hwdec_cuda.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "video/out/opengl/formats.h"
+#include "video/out/opengl/ra_gl.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <unistd.h>
+
+#define CHECK_CU(x) check_cu((mapper)->owner, (x), #x)
+
+struct ext_gl {
+ CUgraphicsResource cu_res;
+};
+
+static bool cuda_ext_gl_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_format *format, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int ret = 0;
+ CUcontext dummy;
+
+ struct ext_gl *egl = talloc_ptrtype(NULL, egl);
+ p->ext[n] = egl;
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mp_image_plane_w(&p->layout, n),
+ .h = mp_image_plane_h(&p->layout, n),
+ .d = 1,
+ .format = format,
+ .render_src = true,
+ .src_linear = format->linear_filter,
+ };
+
+ mapper->tex[n] = ra_tex_create(mapper->ra, &params);
+ if (!mapper->tex[n]) {
+ goto error;
+ }
+
+ GLuint texture;
+ GLenum target;
+ ra_gl_get_raw_tex(mapper->ra, mapper->tex[n], &texture, &target);
+
+ ret = CHECK_CU(cu->cuGraphicsGLRegisterImage(&egl->cu_res, texture, target,
+ CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuGraphicsMapResources(1, &egl->cu_res, 0));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuGraphicsSubResourceGetMappedArray(&p->cu_array[n], egl->cu_res,
+ 0, 0));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuGraphicsUnmapResources(1, &egl->cu_res, 0));
+ if (ret < 0)
+ goto error;
+
+ return true;
+
+error:
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ return false;
+}
+
+static void cuda_ext_gl_uninit(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+
+ struct ext_gl *egl = p->ext[n];
+ if (egl && egl->cu_res) {
+ CHECK_CU(cu->cuGraphicsUnregisterResource(egl->cu_res));
+ egl->cu_res = 0;
+ }
+ talloc_free(egl);
+}
+
+#undef CHECK_CU
+#define CHECK_CU(x) check_cu(hw, (x), #x)
+
+bool cuda_gl_init(const struct ra_hwdec *hw) {
+ int ret = 0;
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu = p->cu;
+
+ if (ra_is_gl(hw->ra_ctx->ra)) {
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl->version < 210 && gl->es < 300) {
+ MP_VERBOSE(hw, "need OpenGL >= 2.1 or OpenGL-ES >= 3.0\n");
+ return false;
+ }
+ } else {
+ // This is not an OpenGL RA.
+ return false;
+ }
+
+ CUdevice display_dev;
+ unsigned int device_count;
+ ret = CHECK_CU(cu->cuGLGetDevices(&device_count, &display_dev, 1,
+ CU_GL_DEVICE_LIST_ALL));
+ if (ret < 0)
+ return false;
+
+ ret = CHECK_CU(cu->cuCtxCreate(&p->display_ctx, CU_CTX_SCHED_BLOCKING_SYNC,
+ display_dev));
+ if (ret < 0)
+ return false;
+
+ p->decode_ctx = p->display_ctx;
+
+ struct cuda_opts *opts = mp_get_config_group(NULL, hw->global, &cuda_conf);
+ int decode_dev_idx = opts->cuda_device;
+ talloc_free(opts);
+
+ if (decode_dev_idx > -1) {
+ CUcontext dummy;
+ CUdevice decode_dev;
+ ret = CHECK_CU(cu->cuDeviceGet(&decode_dev, decode_dev_idx));
+ if (ret < 0) {
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ return false;
+ }
+
+ if (decode_dev != display_dev) {
+ MP_INFO(hw, "Using separate decoder and display devices\n");
+
+ // Pop the display context. We won't use it again during init()
+ ret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (ret < 0)
+ return false;
+
+ ret = CHECK_CU(cu->cuCtxCreate(&p->decode_ctx, CU_CTX_SCHED_BLOCKING_SYNC,
+ decode_dev));
+ if (ret < 0)
+ return false;
+ }
+ }
+
+ // We don't have a way to do a GPU sync after copying
+ p->do_full_sync = true;
+
+ p->ext_init = cuda_ext_gl_init;
+ p->ext_uninit = cuda_ext_gl_uninit;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_cuda_vk.c b/video/out/hwdec/hwdec_cuda_vk.c
new file mode 100644
index 0000000..b9f8caa
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda_vk.c
@@ -0,0 +1,344 @@
+/*
+ * Copyright (c) 2019 Philip Langdale <philipl@overt.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "hwdec_cuda.h"
+#include "video/out/placebo/ra_pl.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libplacebo/vulkan.h>
+#include <unistd.h>
+
+#if HAVE_WIN32_DESKTOP
+#include <versionhelpers.h>
+#define HANDLE_TYPE PL_HANDLE_WIN32
+#else
+#define HANDLE_TYPE PL_HANDLE_FD
+#endif
+
+#define CHECK_CU(x) check_cu((mapper)->owner, (x), #x)
+
+struct ext_vk {
+ CUexternalMemory mem;
+ CUmipmappedArray mma;
+
+ pl_tex pltex;
+ pl_vulkan_sem vk_sem;
+ union pl_handle sem_handle;
+ CUexternalSemaphore cuda_sem;
+};
+
+static bool cuda_ext_vk_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_format *format, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int mem_fd = -1;
+ int ret = 0;
+
+ struct ext_vk *evk = talloc_ptrtype(NULL, evk);
+ p->ext[n] = evk;
+
+ pl_gpu gpu = ra_pl_get(mapper->ra);
+
+ struct pl_tex_params tex_params = {
+ .w = mp_image_plane_w(&p->layout, n),
+ .h = mp_image_plane_h(&p->layout, n),
+ .d = 0,
+ .format = ra_pl_fmt_get(format),
+ .sampleable = true,
+ .export_handle = HANDLE_TYPE,
+ };
+
+ evk->pltex = pl_tex_create(gpu, &tex_params);
+ if (!evk->pltex) {
+ goto error;
+ }
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ ret = mppl_wrap_tex(mapper->ra, evk->pltex, ratex);
+ if (!ret) {
+ pl_tex_destroy(gpu, &evk->pltex);
+ talloc_free(ratex);
+ goto error;
+ }
+ mapper->tex[n] = ratex;
+
+#if !HAVE_WIN32_DESKTOP
+ mem_fd = dup(evk->pltex->shared_mem.handle.fd);
+ if (mem_fd < 0)
+ goto error;
+#endif
+
+ CUDA_EXTERNAL_MEMORY_HANDLE_DESC ext_desc = {
+#if HAVE_WIN32_DESKTOP
+ .type = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32,
+ .handle.win32.handle = evk->pltex->shared_mem.handle.handle,
+#else
+ .type = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD,
+ .handle.fd = mem_fd,
+#endif
+ .size = evk->pltex->shared_mem.size,
+ .flags = 0,
+ };
+ ret = CHECK_CU(cu->cuImportExternalMemory(&evk->mem, &ext_desc));
+ if (ret < 0)
+ goto error;
+ // CUDA takes ownership of imported memory
+ mem_fd = -1;
+
+ CUarray_format cufmt;
+ switch (format->pixel_size / format->num_components) {
+ case 1:
+ cufmt = CU_AD_FORMAT_UNSIGNED_INT8;
+ break;
+ case 2:
+ cufmt = CU_AD_FORMAT_UNSIGNED_INT16;
+ break;
+ default:
+ ret = -1;
+ goto error;
+ }
+
+ CUDA_EXTERNAL_MEMORY_MIPMAPPED_ARRAY_DESC tex_desc = {
+ .offset = evk->pltex->shared_mem.offset,
+ .arrayDesc = {
+ .Width = mp_image_plane_w(&p->layout, n),
+ .Height = mp_image_plane_h(&p->layout, n),
+ .Depth = 0,
+ .Format = cufmt,
+ .NumChannels = format->num_components,
+ .Flags = 0,
+ },
+ .numLevels = 1,
+ };
+
+ ret = CHECK_CU(cu->cuExternalMemoryGetMappedMipmappedArray(&evk->mma, evk->mem, &tex_desc));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuMipmappedArrayGetLevel(&p->cu_array[n], evk->mma, 0));
+ if (ret < 0)
+ goto error;
+
+ evk->vk_sem.sem = pl_vulkan_sem_create(gpu, pl_vulkan_sem_params(
+ .type = VK_SEMAPHORE_TYPE_TIMELINE,
+ .export_handle = HANDLE_TYPE,
+ .out_handle = &(evk->sem_handle),
+ ));
+ if (evk->vk_sem.sem == VK_NULL_HANDLE) {
+ ret = -1;
+ goto error;
+ }
+ // The returned FD or Handle is owned by the caller (us).
+
+ CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC w_desc = {
+#if HAVE_WIN32_DESKTOP
+ .type = CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_TIMELINE_SEMAPHORE_WIN32,
+ .handle.win32.handle = evk->sem_handle.handle,
+#else
+ .type = CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_TIMELINE_SEMAPHORE_FD,
+ .handle.fd = evk->sem_handle.fd,
+#endif
+ };
+ ret = CHECK_CU(cu->cuImportExternalSemaphore(&evk->cuda_sem, &w_desc));
+ if (ret < 0)
+ goto error;
+ // CUDA takes ownership of an imported FD *but not* an imported Handle.
+ evk->sem_handle.fd = -1;
+
+ return true;
+
+error:
+ MP_ERR(mapper, "cuda_ext_vk_init failed\n");
+ if (mem_fd > -1)
+ close(mem_fd);
+#if HAVE_WIN32_DESKTOP
+ if (evk->sem_handle.handle != NULL)
+ CloseHandle(evk->sem_handle.handle);
+#else
+ if (evk->sem_handle.fd > -1)
+ close(evk->sem_handle.fd);
+#endif
+ return false;
+}
+
+static void cuda_ext_vk_uninit(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+
+ struct ext_vk *evk = p->ext[n];
+ if (evk) {
+ if (evk->mma) {
+ CHECK_CU(cu->cuMipmappedArrayDestroy(evk->mma));
+ evk->mma = 0;
+ }
+ if (evk->mem) {
+ CHECK_CU(cu->cuDestroyExternalMemory(evk->mem));
+ evk->mem = 0;
+ }
+ if (evk->cuda_sem) {
+ CHECK_CU(cu->cuDestroyExternalSemaphore(evk->cuda_sem));
+ evk->cuda_sem = 0;
+ }
+ pl_vulkan_sem_destroy(ra_pl_get(mapper->ra), &evk->vk_sem.sem);
+#if HAVE_WIN32_DESKTOP
+ CloseHandle(evk->sem_handle.handle);
+#endif
+ }
+ talloc_free(evk);
+}
+
+static bool cuda_ext_vk_wait(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int ret;
+ struct ext_vk *evk = p->ext[n];
+
+ evk->vk_sem.value += 1;
+ ret = pl_vulkan_hold_ex(ra_pl_get(mapper->ra), pl_vulkan_hold_params(
+ .tex = evk->pltex,
+ .layout = VK_IMAGE_LAYOUT_GENERAL,
+ .qf = VK_QUEUE_FAMILY_EXTERNAL,
+ .semaphore = evk->vk_sem,
+ ));
+ if (!ret)
+ return false;
+
+ CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS wp = {
+ .params = {
+ .fence = {
+ .value = evk->vk_sem.value
+ }
+ }
+ };
+ ret = CHECK_CU(cu->cuWaitExternalSemaphoresAsync(&evk->cuda_sem,
+ &wp, 1, 0));
+ return ret == 0;
+}
+
+static bool cuda_ext_vk_signal(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int ret;
+ struct ext_vk *evk = p->ext[n];
+
+ evk->vk_sem.value += 1;
+ CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS sp = {
+ .params = {
+ .fence = {
+ .value = evk->vk_sem.value
+ }
+ }
+ };
+ ret = CHECK_CU(cu->cuSignalExternalSemaphoresAsync(&evk->cuda_sem,
+ &sp, 1, 0));
+ if (ret != 0)
+ return false;
+
+ pl_vulkan_release_ex(ra_pl_get(mapper->ra), pl_vulkan_release_params(
+ .tex = evk->pltex,
+ .layout = VK_IMAGE_LAYOUT_GENERAL,
+ .qf = VK_QUEUE_FAMILY_EXTERNAL,
+ .semaphore = evk->vk_sem,
+ ));
+ return ret == 0;
+}
+
+#undef CHECK_CU
+#define CHECK_CU(x) check_cu(hw, (x), #x)
+
+bool cuda_vk_init(const struct ra_hwdec *hw) {
+ int ret = 0;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu = p->cu;
+
+ pl_gpu gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (gpu != NULL) {
+ if (!(gpu->export_caps.tex & HANDLE_TYPE)) {
+ MP_VERBOSE(hw, "CUDA hwdec with Vulkan requires exportable texture memory of type 0x%X.\n",
+ HANDLE_TYPE);
+ return false;
+ } else if (!(gpu->export_caps.sync & HANDLE_TYPE)) {
+ MP_VERBOSE(hw, "CUDA hwdec with Vulkan requires exportable semaphores of type 0x%X.\n",
+ HANDLE_TYPE);
+ return false;
+ }
+ } else {
+ // This is not a Vulkan RA.
+ return false;
+ }
+
+ if (!cu->cuImportExternalMemory) {
+ MP_MSG(hw, level, "CUDA hwdec with Vulkan requires driver version 410.48 or newer.\n");
+ return false;
+ }
+
+ int device_count;
+ ret = CHECK_CU(cu->cuDeviceGetCount(&device_count));
+ if (ret < 0)
+ return false;
+
+ CUdevice display_dev = -1;
+ for (int i = 0; i < device_count; i++) {
+ CUdevice dev;
+ ret = CHECK_CU(cu->cuDeviceGet(&dev, i));
+ if (ret < 0)
+ continue;
+
+ CUuuid uuid;
+ ret = CHECK_CU(cu->cuDeviceGetUuid(&uuid, dev));
+ if (ret < 0)
+ continue;
+
+ if (memcmp(gpu->uuid, uuid.bytes, sizeof (gpu->uuid)) == 0) {
+ display_dev = dev;
+ break;
+ }
+ }
+
+ if (display_dev == -1) {
+ MP_MSG(hw, level, "Could not match Vulkan display device in CUDA.\n");
+ return false;
+ }
+
+ ret = CHECK_CU(cu->cuCtxCreate(&p->display_ctx, CU_CTX_SCHED_BLOCKING_SYNC,
+ display_dev));
+ if (ret < 0)
+ return false;
+
+ p->decode_ctx = p->display_ctx;
+
+ p->ext_init = cuda_ext_vk_init;
+ p->ext_uninit = cuda_ext_vk_uninit;
+ p->ext_wait = cuda_ext_vk_wait;
+ p->ext_signal = cuda_ext_vk_signal;
+
+ return true;
+}
+
diff --git a/video/out/hwdec/hwdec_drmprime.c b/video/out/hwdec/hwdec_drmprime.c
new file mode 100644
index 0000000..f7c6250
--- /dev/null
+++ b/video/out/hwdec/hwdec_drmprime.c
@@ -0,0 +1,294 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <fcntl.h>
+#include <stddef.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_drm.h>
+#include <xf86drm.h>
+
+#include "config.h"
+
+#include "libmpv/render_gl.h"
+#include "options/m_config.h"
+#include "video/fmt-conversion.h"
+#include "video/out/drm_common.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/hwdec/dmabuf_interop.h"
+
+extern const struct m_sub_options drm_conf;
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ int *formats;
+
+ struct dmabuf_interop dmabuf_interop;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ if (p->hwctx.driver_name)
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+const static dmabuf_interop_init interop_inits[] = {
+#if HAVE_DMABUF_INTEROP_GL
+ dmabuf_interop_gl_init,
+#endif
+#if HAVE_VAAPI
+ dmabuf_interop_pl_init,
+#endif
+#if HAVE_DMABUF_WAYLAND
+ dmabuf_interop_wl_init,
+#endif
+ NULL
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw, &p->dmabuf_interop)) {
+ break;
+ }
+ }
+
+ if (!p->dmabuf_interop.interop_map || !p->dmabuf_interop.interop_unmap) {
+ MP_VERBOSE(hw, "drmprime hwdec requires at least one dmabuf interop backend.\n");
+ return -1;
+ }
+
+ /*
+ * The drm_params resource is not provided when using X11 or Wayland, but
+ * there are extensions that supposedly provide this information from the
+ * drivers. Not properly documented. Of course.
+ */
+ mpv_opengl_drm_params_v2 *params = ra_get_native_resource(hw->ra_ctx->ra,
+ "drm_params_v2");
+
+ /*
+ * Respect drm_device option, so there is a way to control this when not
+ * using a DRM gpu context. If drm_params_v2 are present, they will already
+ * respect this option.
+ */
+ void *tmp = talloc_new(NULL);
+ struct drm_opts *drm_opts = mp_get_config_group(tmp, hw->global, &drm_conf);
+ const char *opt_path = drm_opts->device_path;
+
+ const char *device_path = params && params->render_fd > -1 ?
+ drmGetRenderDeviceNameFromFd(params->render_fd) :
+ opt_path ? opt_path : "/dev/dri/renderD128";
+ MP_VERBOSE(hw, "Using DRM device: %s\n", device_path);
+
+ int ret = av_hwdevice_ctx_create(&p->hwctx.av_device_ref,
+ AV_HWDEVICE_TYPE_DRM,
+ device_path, NULL, 0);
+ talloc_free(tmp);
+ if (ret != 0) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx: %s\n", av_err2str(ret));
+ return -1;
+ }
+
+ /*
+ * At the moment, there is no way to discover compatible formats
+ * from the hwdevice_ctx, and in fact the ffmpeg hwaccels hard-code
+ * formats too, so we're not missing out on anything.
+ */
+ int num_formats = 0;
+ MP_TARRAY_APPEND(p, p->formats, num_formats, IMGFMT_NV12);
+ MP_TARRAY_APPEND(p, p->formats, num_formats, IMGFMT_420P);
+ MP_TARRAY_APPEND(p, p->formats, num_formats, pixfmt2imgfmt(AV_PIX_FMT_NV16));
+ MP_TARRAY_APPEND(p, p->formats, num_formats, 0); // terminate it
+
+ p->hwctx.hw_imgfmt = IMGFMT_DRMPRIME;
+ p->hwctx.supported_formats = p->formats;
+ p->hwctx.driver_name = hw->driver->name;
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ p_owner->dmabuf_interop.interop_unmap(mapper);
+
+ if (p->surface_acquired) {
+ for (int n = 0; n < p->desc.nb_objects; n++) {
+ if (p->desc.objects[n].fd > -1)
+ close(p->desc.objects[n].fd);
+ }
+ p->surface_acquired = false;
+ }
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ if (p_owner->dmabuf_interop.interop_uninit) {
+ p_owner->dmabuf_interop.interop_uninit(mapper);
+ }
+}
+
+static bool check_fmt(struct ra_hwdec_mapper *mapper, int fmt)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ for (int n = 0; p_owner->formats && p_owner->formats[n]; n++) {
+ if (p_owner->formats[n] == fmt)
+ return true;
+ }
+ return false;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+
+ /*
+ * rpi4_8 and rpi4_10 function identically to NV12. These two pixel
+ * formats however are not defined in upstream ffmpeg so a string
+ * comparison is used to identify them instead of a mpv IMGFMT.
+ */
+ const char* fmt_name = mp_imgfmt_to_name(mapper->src_params.hw_subfmt);
+ if (strcmp(fmt_name, "rpi4_8") == 0 || strcmp(fmt_name, "rpi4_10") == 0)
+ mapper->dst_params.imgfmt = IMGFMT_NV12;
+ else
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ struct ra_imgfmt_desc desc = {0};
+
+ if (mapper->ra->num_formats &&
+ !ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ p->num_planes = desc.num_planes;
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ if (p_owner->dmabuf_interop.interop_init)
+ if (!p_owner->dmabuf_interop.interop_init(mapper, &desc))
+ return -1;
+
+ if (!check_fmt(mapper, mapper->dst_params.imgfmt))
+ {
+ MP_FATAL(mapper, "unsupported DRM image format %s\n",
+ mp_imgfmt_to_name(mapper->dst_params.imgfmt));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ /*
+ * Although we use the same AVDRMFrameDescriptor to hold the dmabuf
+ * properties, we additionally need to dup the fds to ensure the
+ * frame doesn't disappear out from under us. And then for clarity,
+ * we copy all the individual fields.
+ */
+ const AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)mapper->src->planes[0];
+ p->desc.nb_layers = desc->nb_layers;
+ p->desc.nb_objects = desc->nb_objects;
+ for (int i = 0; i < desc->nb_layers; i++) {
+ p->desc.layers[i].format = desc->layers[i].format;
+ p->desc.layers[i].nb_planes = desc->layers[i].nb_planes;
+ for (int j = 0; j < desc->layers[i].nb_planes; j++) {
+ p->desc.layers[i].planes[j].object_index = desc->layers[i].planes[j].object_index;
+ p->desc.layers[i].planes[j].offset = desc->layers[i].planes[j].offset;
+ p->desc.layers[i].planes[j].pitch = desc->layers[i].planes[j].pitch;
+ }
+ }
+ for (int i = 0; i < desc->nb_objects; i++) {
+ p->desc.objects[i].format_modifier = desc->objects[i].format_modifier;
+ p->desc.objects[i].size = desc->objects[i].size;
+ // Initialise fds to -1 to make partial failure cleanup easier.
+ p->desc.objects[i].fd = -1;
+ }
+ // Surface is now safe to treat as acquired to allow for unmapping to run.
+ p->surface_acquired = true;
+
+ // Now actually dup the fds
+ for (int i = 0; i < desc->nb_objects; i++) {
+ p->desc.objects[i].fd = fcntl(desc->objects[i].fd, F_DUPFD_CLOEXEC, 0);
+ if (p->desc.objects[i].fd == -1) {
+ MP_ERR(mapper, "Failed to duplicate dmabuf fd: %s\n",
+ mp_strerror(errno));
+ goto err;
+ }
+ }
+
+ // We can handle composed formats if the total number of planes is still
+ // equal the number of planes we expect. Complex formats with auxiliary
+ // planes cannot be supported.
+
+ int num_returned_planes = 0;
+ for (int i = 0; i < p->desc.nb_layers; i++) {
+ num_returned_planes += p->desc.layers[i].nb_planes;
+ }
+
+ if (p->num_planes != 0 && p->num_planes != num_returned_planes) {
+ MP_ERR(mapper,
+ "Mapped surface with format '%s' has unexpected number of planes. "
+ "(%d layers and %d planes, but expected %d planes)\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ p->desc.nb_layers, num_returned_planes, p->num_planes);
+ goto err;
+ }
+
+ if (!p_owner->dmabuf_interop.interop_map(mapper, &p_owner->dmabuf_interop,
+ false))
+ goto err;
+
+ return 0;
+
+err:
+ mapper_unmap(mapper);
+
+ MP_FATAL(mapper, "mapping DRM dmabuf failed\n");
+ return -1;
+}
+
+const struct ra_hwdec_driver ra_hwdec_drmprime = {
+ .name = "drmprime",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DRMPRIME, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct dmabuf_interop_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_drmprime_overlay.c b/video/out/hwdec/hwdec_drmprime_overlay.c
new file mode 100644
index 0000000..6b6aae6
--- /dev/null
+++ b/video/out/hwdec/hwdec_drmprime_overlay.c
@@ -0,0 +1,334 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_drm.h>
+
+#include "video/hwdec.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "libmpv/render_gl.h"
+#include "video/out/drm_atomic.h"
+#include "video/out/drm_common.h"
+#include "video/out/drm_prime.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/mp_image.h"
+
+extern const struct m_sub_options drm_conf;
+
+struct drm_frame {
+ struct drm_prime_framebuffer fb;
+ struct mp_image *image; // associated mpv image
+};
+
+struct priv {
+ struct mp_log *log;
+ struct mp_hwdec_ctx hwctx;
+
+ struct mp_image_params params;
+
+ struct drm_atomic_context *ctx;
+ struct drm_frame current_frame, last_frame, old_frame;
+
+ struct mp_rect src, dst;
+
+ int display_w, display_h;
+
+ struct drm_prime_handle_refs handle_refs;
+};
+
+static void set_current_frame(struct ra_hwdec *hw, struct drm_frame *frame)
+{
+ struct priv *p = hw->priv;
+
+ // frame will be on screen after next vsync
+ // current_frame is currently the displayed frame and will be replaced
+ // by frame after next vsync.
+ // We used old frame as triple buffering to make sure that the drm framebuffer
+ // is not being displayed when we release it.
+
+ if (p->ctx) {
+ drm_prime_destroy_framebuffer(p->log, p->ctx->fd, &p->old_frame.fb, &p->handle_refs);
+ }
+
+ mp_image_setrefp(&p->old_frame.image, p->last_frame.image);
+ p->old_frame.fb = p->last_frame.fb;
+
+ mp_image_setrefp(&p->last_frame.image, p->current_frame.image);
+ p->last_frame.fb = p->current_frame.fb;
+
+ if (frame) {
+ p->current_frame.fb = frame->fb;
+ mp_image_setrefp(&p->current_frame.image, frame->image);
+ } else {
+ memset(&p->current_frame.fb, 0, sizeof(p->current_frame.fb));
+ mp_image_setrefp(&p->current_frame.image, NULL);
+ }
+}
+
+static void scale_dst_rect(struct ra_hwdec *hw, int source_w, int source_h ,struct mp_rect *src, struct mp_rect *dst)
+{
+ struct priv *p = hw->priv;
+
+ // drm can allow to have a layer that has a different size from framebuffer
+ // we scale here the destination size to video mode
+ double hratio = p->display_w / (double)source_w;
+ double vratio = p->display_h / (double)source_h;
+ double ratio = hratio <= vratio ? hratio : vratio;
+
+ dst->x0 = src->x0 * ratio;
+ dst->x1 = src->x1 * ratio;
+ dst->y0 = src->y0 * ratio;
+ dst->y1 = src->y1 * ratio;
+
+ int offset_x = (p->display_w - ratio * source_w) / 2;
+ int offset_y = (p->display_h - ratio * source_h) / 2;
+
+ dst->x0 += offset_x;
+ dst->x1 += offset_x;
+ dst->y0 += offset_y;
+ dst->y1 += offset_y;
+}
+
+static void disable_video_plane(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ if (!p->ctx)
+ return;
+
+ if (!p->ctx->drmprime_video_plane)
+ return;
+
+ // Disabling the drmprime video plane is needed on some devices when using
+ // the primary plane for video. Primary buffer can't be active with no
+ // framebuffer associated. So we need this function to commit it right away
+ // as mpv will free all framebuffers on playback end.
+ drmModeAtomicReqPtr request = drmModeAtomicAlloc();
+ if (request) {
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "FB_ID", 0);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_ID", 0);
+
+ int ret = drmModeAtomicCommit(p->ctx->fd, request,
+ 0, NULL);
+
+ if (ret)
+ MP_ERR(hw, "Failed to commit disable plane request (code %d)", ret);
+ drmModeAtomicFree(request);
+ }
+}
+
+static int overlay_frame(struct ra_hwdec *hw, struct mp_image *hw_image,
+ struct mp_rect *src, struct mp_rect *dst, bool newframe)
+{
+ struct priv *p = hw->priv;
+ AVDRMFrameDescriptor *desc = NULL;
+ drmModeAtomicReq *request = NULL;
+ struct drm_frame next_frame = {0};
+ int ret;
+
+ struct ra *ra = hw->ra_ctx->ra;
+
+ // grab atomic request from native resources
+ if (p->ctx) {
+ struct mpv_opengl_drm_params_v2 *drm_params;
+ drm_params = (mpv_opengl_drm_params_v2 *)ra_get_native_resource(ra, "drm_params_v2");
+ if (!drm_params) {
+ MP_ERR(hw, "Failed to retrieve drm params from native resources\n");
+ return -1;
+ }
+ if (drm_params->atomic_request_ptr) {
+ request = *drm_params->atomic_request_ptr;
+ } else {
+ MP_ERR(hw, "drm params pointer to atomic request is invalid\n");
+ return -1;
+ }
+ }
+
+ if (hw_image) {
+
+ // grab draw plane windowing info to eventually upscale the overlay
+ // as egl windows could be upscaled to draw plane.
+ struct mpv_opengl_drm_draw_surface_size *draw_surface_size = ra_get_native_resource(ra, "drm_draw_surface_size");
+ if (draw_surface_size) {
+ scale_dst_rect(hw, draw_surface_size->width, draw_surface_size->height, dst, &p->dst);
+ } else {
+ p->dst = *dst;
+ }
+ p->src = *src;
+
+ next_frame.image = hw_image;
+ desc = (AVDRMFrameDescriptor *)hw_image->planes[0];
+
+ if (desc) {
+ int srcw = p->src.x1 - p->src.x0;
+ int srch = p->src.y1 - p->src.y0;
+ int dstw = MP_ALIGN_UP(p->dst.x1 - p->dst.x0, 2);
+ int dsth = MP_ALIGN_UP(p->dst.y1 - p->dst.y0, 2);
+
+ if (drm_prime_create_framebuffer(p->log, p->ctx->fd, desc, srcw, srch, &next_frame.fb, &p->handle_refs)) {
+ ret = -1;
+ goto fail;
+ }
+
+ if (request) {
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "FB_ID", next_frame.fb.fb_id);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_ID", p->ctx->crtc->id);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_X", p->src.x0 << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_Y", p->src.y0 << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_W", srcw << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_H", srch << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_X", MP_ALIGN_DOWN(p->dst.x0, 2));
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_Y", MP_ALIGN_DOWN(p->dst.y0, 2));
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_W", dstw);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_H", dsth);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "ZPOS", 0);
+ } else {
+ ret = drmModeSetPlane(p->ctx->fd, p->ctx->drmprime_video_plane->id, p->ctx->crtc->id, next_frame.fb.fb_id, 0,
+ MP_ALIGN_DOWN(p->dst.x0, 2), MP_ALIGN_DOWN(p->dst.y0, 2), dstw, dsth,
+ p->src.x0 << 16, p->src.y0 << 16 , srcw << 16, srch << 16);
+ if (ret < 0) {
+ MP_ERR(hw, "Failed to set the drmprime video plane %d (buffer %d).\n",
+ p->ctx->drmprime_video_plane->id, next_frame.fb.fb_id);
+ goto fail;
+ }
+ }
+ }
+ } else {
+ disable_video_plane(hw);
+
+ while (p->old_frame.fb.fb_id)
+ set_current_frame(hw, NULL);
+ }
+
+ set_current_frame(hw, &next_frame);
+ return 0;
+
+ fail:
+ drm_prime_destroy_framebuffer(p->log, p->ctx->fd, &next_frame.fb, &p->handle_refs);
+ return ret;
+}
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+
+ disable_video_plane(hw);
+ set_current_frame(hw, NULL);
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->ctx) {
+ drm_atomic_destroy_context(p->ctx);
+ p->ctx = NULL;
+ }
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ int draw_plane, drmprime_video_plane;
+
+ p->log = hw->log;
+
+ void *tmp = talloc_new(NULL);
+ struct drm_opts *opts = mp_get_config_group(tmp, hw->global, &drm_conf);
+ draw_plane = opts->draw_plane;
+ drmprime_video_plane = opts->drmprime_video_plane;
+ talloc_free(tmp);
+
+ struct mpv_opengl_drm_params_v2 *drm_params;
+
+ drm_params = ra_get_native_resource(hw->ra_ctx->ra, "drm_params_v2");
+ if (drm_params) {
+ p->ctx = drm_atomic_create_context(p->log, drm_params->fd, drm_params->crtc_id,
+ drm_params->connector_id, draw_plane, drmprime_video_plane);
+ if (!p->ctx) {
+ mp_err(p->log, "Failed to retrieve DRM atomic context.\n");
+ goto err;
+ }
+ if (!p->ctx->drmprime_video_plane) {
+ mp_warn(p->log, "No drmprime video plane. You might need to specify it manually using --drm-drmprime-video-plane\n");
+ goto err;
+ }
+ } else {
+ mp_verbose(p->log, "Failed to retrieve DRM fd from native display.\n");
+ goto err;
+ }
+
+ drmModeCrtcPtr crtc;
+ crtc = drmModeGetCrtc(p->ctx->fd, p->ctx->crtc->id);
+ if (crtc) {
+ p->display_w = crtc->mode.hdisplay;
+ p->display_h = crtc->mode.vdisplay;
+ drmModeFreeCrtc(crtc);
+ }
+
+ uint64_t has_prime;
+ if (drmGetCap(p->ctx->fd, DRM_CAP_PRIME, &has_prime) < 0) {
+ MP_ERR(hw, "Card does not support prime handles.\n");
+ goto err;
+ }
+
+ if (has_prime) {
+ drm_prime_init_handle_ref_count(p, &p->handle_refs);
+ }
+
+ disable_video_plane(hw);
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .hw_imgfmt = IMGFMT_DRMPRIME,
+ };
+
+ char *device = drmGetDeviceNameFromFd2(p->ctx->fd);
+ int ret = av_hwdevice_ctx_create(&p->hwctx.av_device_ref,
+ AV_HWDEVICE_TYPE_DRM, device, NULL, 0);
+
+ if (device)
+ free(device);
+
+ if (ret != 0) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx: %s\n", av_err2str(ret));
+ goto err;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+
+err:
+ uninit(hw);
+ return -1;
+}
+
+const struct ra_hwdec_driver ra_hwdec_drmprime_overlay = {
+ .name = "drmprime-overlay",
+ .priv_size = sizeof(struct priv),
+ .imgfmts = {IMGFMT_DRMPRIME, 0},
+ .init = init,
+ .overlay_frame = overlay_frame,
+ .uninit = uninit,
+};
diff --git a/video/out/hwdec/hwdec_ios_gl.m b/video/out/hwdec/hwdec_ios_gl.m
new file mode 100644
index 0000000..633cc3d
--- /dev/null
+++ b/video/out/hwdec/hwdec_ios_gl.m
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ * 2017 Aman Gupta <ffmpeg@tmm1.net>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <CoreVideo/CoreVideo.h>
+#include <OpenGLES/EAGL.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "video/out/gpu/hwdec.h"
+#include "video/mp_image_pool.h"
+#include "video/out/opengl/ra_gl.h"
+#include "hwdec_vt.h"
+
+static bool check_hwdec(const struct ra_hwdec *hw)
+{
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return false;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl->es < 200) {
+ MP_ERR(hw, "need OpenGLES 2.0 for CVOpenGLESTextureCacheCreateTextureFromImage()\n");
+ return false;
+ }
+
+ if ([EAGLContext currentContext] == nil) {
+ MP_ERR(hw, "need a current EAGLContext set\n");
+ return false;
+ }
+
+ return true;
+}
+
+// In GLES3 mode, CVOpenGLESTextureCacheCreateTextureFromImage()
+// will return error -6683 unless invoked with GL_LUMINANCE and
+// GL_LUMINANCE_ALPHA (http://stackoverflow.com/q/36213994/332798)
+// If a format trues to use GL_RED/GL_RG instead, try to find a format
+// that uses GL_LUMINANCE[_ALPHA] instead.
+static const struct ra_format *find_la_variant(struct ra *ra,
+ const struct ra_format *fmt)
+{
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+ ra_gl_get_format(fmt, &internal_format, &format, &type);
+
+ if (format == GL_RED) {
+ format = internal_format = GL_LUMINANCE;
+ } else if (format == GL_RG) {
+ format = internal_format = GL_LUMINANCE_ALPHA;
+ } else {
+ return fmt;
+ }
+
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt2 = ra->formats[n];
+ GLint internal_format2;
+ GLenum format2;
+ GLenum type2;
+ ra_gl_get_format(fmt2, &internal_format2, &format2, &type2);
+ if (internal_format2 == internal_format &&
+ format2 == format && type2 == type)
+ return fmt2;
+ }
+
+ return NULL;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int n = 0; n < p->desc.num_planes; n++) {
+ p->desc.planes[n] = find_la_variant(mapper->ra, p->desc.planes[n]);
+ if (!p->desc.planes[n] || p->desc.planes[n]->ctype != RA_CTYPE_UNORM) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+ }
+
+ CVReturn err = CVOpenGLESTextureCacheCreate(
+ kCFAllocatorDefault,
+ NULL,
+ [EAGLContext currentContext],
+ NULL,
+ &p->gl_texture_cache);
+
+ if (err != noErr) {
+ MP_ERR(mapper, "Failure in CVOpenGLESTextureCacheCreate: %d\n", err);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+ if (p->gl_planes[i]) {
+ CFRelease(p->gl_planes[i]);
+ p->gl_planes[i] = NULL;
+ }
+ }
+
+ CVOpenGLESTextureCacheFlush(p->gl_texture_cache, 0);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = (CVPixelBufferRef)mapper->src->planes[3];
+ CVPixelBufferRetain(p->pbuf);
+
+ const bool planar = CVPixelBufferIsPlanar(p->pbuf);
+ const int planes = CVPixelBufferGetPlaneCount(p->pbuf);
+ assert((planar && planes == p->desc.num_planes) || p->desc.num_planes == 1);
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ const struct ra_format *fmt = p->desc.planes[i];
+
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+ ra_gl_get_format(fmt, &internal_format, &format, &type);
+
+ CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage(
+ kCFAllocatorDefault,
+ p->gl_texture_cache,
+ p->pbuf,
+ NULL,
+ GL_TEXTURE_2D,
+ internal_format,
+ CVPixelBufferGetWidthOfPlane(p->pbuf, i),
+ CVPixelBufferGetHeightOfPlane(p->pbuf, i),
+ format,
+ type,
+ i,
+ &p->gl_planes[i]);
+
+ if (err != noErr) {
+ MP_ERR(mapper, "error creating texture for plane %d: %d\n", i, err);
+ return -1;
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, CVOpenGLESTextureGetName(p->gl_planes[i]));
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = CVPixelBufferGetWidthOfPlane(p->pbuf, i),
+ .h = CVPixelBufferGetHeightOfPlane(p->pbuf, i),
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ };
+
+ mapper->tex[i] = ra_create_wrapped_tex(
+ mapper->ra,
+ &params,
+ CVOpenGLESTextureGetName(p->gl_planes[i])
+ );
+ if (!mapper->tex[i])
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ CVPixelBufferRelease(p->pbuf);
+ if (p->gl_texture_cache) {
+ CFRelease(p->gl_texture_cache);
+ p->gl_texture_cache = NULL;
+ }
+}
+
+bool vt_gl_init(const struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!check_hwdec(hw))
+ return false;
+
+ p->interop_init = mapper_init;
+ p->interop_uninit = mapper_uninit;
+ p->interop_map = mapper_map;
+ p->interop_unmap = mapper_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_mac_gl.c b/video/out/hwdec/hwdec_mac_gl.c
new file mode 100644
index 0000000..b73f5b9
--- /dev/null
+++ b/video/out/hwdec/hwdec_mac_gl.c
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <IOSurface/IOSurface.h>
+#include <CoreVideo/CoreVideo.h>
+#include <OpenGL/OpenGL.h>
+#include <OpenGL/CGLIOSurface.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "video/mp_image_pool.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/opengl/ra_gl.h"
+#include "hwdec_vt.h"
+
+static bool check_hwdec(const struct ra_hwdec *hw)
+{
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return false;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl->version < 300) {
+ MP_ERR(hw, "need >= OpenGL 3.0 for core rectangle texture support\n");
+ return false;
+ }
+
+ if (!CGLGetCurrentContext()) {
+ MP_ERR(hw, "need cocoa opengl backend to be active");
+ return false;
+ }
+
+ return true;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ gl->GenTextures(MP_MAX_PLANES, p->gl_planes);
+
+ for (int n = 0; n < p->desc.num_planes; n++) {
+ if (p->desc.planes[n]->ctype != RA_CTYPE_UNORM) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ // Is this sane? No idea how to release the texture without deleting it.
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = NULL;
+
+ for (int i = 0; i < p->desc.num_planes; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = (CVPixelBufferRef)mapper->src->planes[3];
+ CVPixelBufferRetain(p->pbuf);
+ IOSurfaceRef surface = CVPixelBufferGetIOSurface(p->pbuf);
+ if (!surface) {
+ MP_ERR(mapper, "CVPixelBuffer has no IOSurface\n");
+ return -1;
+ }
+
+ const bool planar = CVPixelBufferIsPlanar(p->pbuf);
+ const int planes = CVPixelBufferGetPlaneCount(p->pbuf);
+ assert((planar && planes == p->desc.num_planes) || p->desc.num_planes == 1);
+
+ GLenum gl_target = GL_TEXTURE_RECTANGLE;
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ const struct ra_format *fmt = p->desc.planes[i];
+
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+ ra_gl_get_format(fmt, &internal_format, &format, &type);
+
+ gl->BindTexture(gl_target, p->gl_planes[i]);
+
+ CGLError err = CGLTexImageIOSurface2D(
+ CGLGetCurrentContext(), gl_target,
+ internal_format,
+ IOSurfaceGetWidthOfPlane(surface, i),
+ IOSurfaceGetHeightOfPlane(surface, i),
+ format, type, surface, i);
+
+ gl->BindTexture(gl_target, 0);
+
+ if (err != kCGLNoError) {
+ MP_ERR(mapper,
+ "error creating IOSurface texture for plane %d: %s (%x)\n",
+ i, CGLErrorString(err), gl->GetError());
+ return -1;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = IOSurfaceGetWidthOfPlane(surface, i),
+ .h = IOSurfaceGetHeightOfPlane(surface, i),
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .non_normalized = gl_target == GL_TEXTURE_RECTANGLE,
+ };
+
+ mapper->tex[i] = ra_create_wrapped_tex(mapper->ra, &params,
+ p->gl_planes[i]);
+ if (!mapper->tex[i])
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ gl->DeleteTextures(MP_MAX_PLANES, p->gl_planes);
+}
+
+bool vt_gl_init(const struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!check_hwdec(hw))
+ return false;
+
+ p->interop_init = mapper_init;
+ p->interop_uninit = mapper_uninit;
+ p->interop_map = mapper_map;
+ p->interop_unmap = mapper_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_vaapi.c b/video/out/hwdec/hwdec_vaapi.c
new file mode 100644
index 0000000..d8a4517
--- /dev/null
+++ b/video/out/hwdec/hwdec_vaapi.c
@@ -0,0 +1,557 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <va/va_drmcommon.h>
+
+#include "config.h"
+
+#include "video/out/gpu/hwdec.h"
+#include "video/out/hwdec/dmabuf_interop.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image_pool.h"
+#include "video/vaapi.h"
+
+#if HAVE_VAAPI_DRM
+#include "libmpv/render_gl.h"
+#endif
+
+#if HAVE_VAAPI_X11
+#include <va/va_x11.h>
+
+static VADisplay *create_x11_va_display(struct ra *ra)
+{
+ Display *x11 = ra_get_native_resource(ra, "x11");
+ return x11 ? vaGetDisplay(x11) : NULL;
+}
+#endif
+
+#if HAVE_VAAPI_WAYLAND
+#include <va/va_wayland.h>
+
+static VADisplay *create_wayland_va_display(struct ra *ra)
+{
+ struct wl_display *wl = ra_get_native_resource(ra, "wl");
+
+ return wl ? vaGetDisplayWl(wl) : NULL;
+}
+#endif
+
+#if HAVE_VAAPI_DRM
+#include <va/va_drm.h>
+
+static VADisplay *create_drm_va_display(struct ra *ra)
+{
+ mpv_opengl_drm_params_v2 *params = ra_get_native_resource(ra, "drm_params_v2");
+ if (!params || params->render_fd == -1)
+ return NULL;
+
+ return vaGetDisplayDRM(params->render_fd);
+}
+#endif
+
+struct va_create_native {
+ const char *name;
+ VADisplay *(*create)(struct ra *ra);
+};
+
+static const struct va_create_native create_native_cbs[] = {
+#if HAVE_VAAPI_X11
+ {"x11", create_x11_va_display},
+#endif
+#if HAVE_VAAPI_WAYLAND
+ {"wayland", create_wayland_va_display},
+#endif
+#if HAVE_VAAPI_DRM
+ {"drm", create_drm_va_display},
+#endif
+};
+
+static VADisplay *create_native_va_display(struct ra *ra, struct mp_log *log)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(create_native_cbs); n++) {
+ const struct va_create_native *disp = &create_native_cbs[n];
+ mp_verbose(log, "Trying to open a %s VA display...\n", disp->name);
+ VADisplay *display = disp->create(ra);
+ if (display)
+ return display;
+ }
+ return NULL;
+}
+
+static void determine_working_formats(struct ra_hwdec *hw);
+
+struct priv_owner {
+ struct mp_vaapi_ctx *ctx;
+ VADisplay *display;
+ int *formats;
+ bool probing_formats; // temporary during init
+
+ struct dmabuf_interop dmabuf_interop;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ if (p->ctx) {
+ hwdec_devices_remove(hw->devs, &p->ctx->hwctx);
+ if (p->ctx->hwctx.conversion_config) {
+ AVVAAPIHWConfig *hwconfig = p->ctx->hwctx.conversion_config;
+ vaDestroyConfig(p->ctx->display, hwconfig->config_id);
+ av_freep(&p->ctx->hwctx.conversion_config);
+ }
+ }
+ va_destroy(p->ctx);
+}
+
+const static dmabuf_interop_init interop_inits[] = {
+#if HAVE_DMABUF_INTEROP_GL
+ dmabuf_interop_gl_init,
+#endif
+ dmabuf_interop_pl_init,
+#if HAVE_DMABUF_WAYLAND
+ dmabuf_interop_wl_init,
+#endif
+ NULL
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ VAStatus vas;
+
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw, &p->dmabuf_interop)) {
+ break;
+ }
+ }
+
+ if (!p->dmabuf_interop.interop_map || !p->dmabuf_interop.interop_unmap) {
+ MP_VERBOSE(hw, "VAAPI hwdec only works with OpenGL or Vulkan backends.\n");
+ return -1;
+ }
+
+ p->display = create_native_va_display(hw->ra_ctx->ra, hw->log);
+ if (!p->display) {
+ MP_VERBOSE(hw, "Could not create a VA display.\n");
+ return -1;
+ }
+
+ p->ctx = va_initialize(p->display, hw->log, true);
+ if (!p->ctx) {
+ vaTerminate(p->display);
+ return -1;
+ }
+ if (!p->ctx->av_device_ref) {
+ MP_VERBOSE(hw, "libavutil vaapi code rejected the driver?\n");
+ return -1;
+ }
+
+ if (hw->probing && va_guess_if_emulated(p->ctx)) {
+ return -1;
+ }
+
+ determine_working_formats(hw);
+ if (!p->formats || !p->formats[0]) {
+ return -1;
+ }
+
+ VAConfigID config_id;
+ AVVAAPIHWConfig *hwconfig = NULL;
+ vas = vaCreateConfig(p->display, VAProfileNone, VAEntrypointVideoProc, NULL,
+ 0, &config_id);
+ if (vas == VA_STATUS_SUCCESS) {
+ hwconfig = av_hwdevice_hwconfig_alloc(p->ctx->av_device_ref);
+ hwconfig->config_id = config_id;
+ }
+
+ // it's now safe to set the display resource
+ ra_add_native_resource(hw->ra_ctx->ra, "VADisplay", p->display);
+
+ p->ctx->hwctx.hw_imgfmt = IMGFMT_VAAPI;
+ p->ctx->hwctx.supported_formats = p->formats;
+ p->ctx->hwctx.driver_name = hw->driver->name;
+ p->ctx->hwctx.conversion_filter_name = "scale_vaapi";
+ p->ctx->hwctx.conversion_config = hwconfig;
+ hwdec_devices_add(hw->devs, &p->ctx->hwctx);
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ p_owner->dmabuf_interop.interop_unmap(mapper);
+
+ if (p->surface_acquired) {
+ for (int n = 0; n < p->desc.nb_objects; n++)
+ close(p->desc.objects[n].fd);
+ p->surface_acquired = false;
+ }
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ if (p_owner->dmabuf_interop.interop_uninit) {
+ p_owner->dmabuf_interop.interop_uninit(mapper);
+ }
+}
+
+static bool check_fmt(struct ra_hwdec_mapper *mapper, int fmt)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ for (int n = 0; p_owner->formats && p_owner->formats[n]; n++) {
+ if (p_owner->formats[n] == fmt)
+ return true;
+ }
+ return false;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ struct ra_imgfmt_desc desc = {0};
+
+ if (mapper->ra->num_formats &&
+ !ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ p->num_planes = desc.num_planes;
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ if (p_owner->dmabuf_interop.interop_init)
+ if (!p_owner->dmabuf_interop.interop_init(mapper, &desc))
+ return -1;
+
+ if (!p_owner->probing_formats && !check_fmt(mapper, mapper->dst_params.imgfmt))
+ {
+ MP_FATAL(mapper, "unsupported VA image format %s\n",
+ mp_imgfmt_to_name(mapper->dst_params.imgfmt));
+ return -1;
+ }
+
+ return 0;
+}
+
+static void close_file_descriptors(VADRMPRIMESurfaceDescriptor desc)
+{
+ for (int i = 0; i < desc.num_objects; i++)
+ close(desc.objects[i].fd);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+ VAStatus status;
+ VADisplay *display = p_owner->display;
+ VADRMPRIMESurfaceDescriptor desc = {0};
+
+ uint32_t flags = p_owner->dmabuf_interop.composed_layers ?
+ VA_EXPORT_SURFACE_COMPOSED_LAYERS : VA_EXPORT_SURFACE_SEPARATE_LAYERS;
+ status = vaExportSurfaceHandle(display, va_surface_id(mapper->src),
+ VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_READ_ONLY |
+ flags,
+ &desc);
+ if (!CHECK_VA_STATUS_LEVEL(mapper, "vaExportSurfaceHandle()",
+ p_owner->probing_formats ? MSGL_DEBUG : MSGL_ERR))
+ {
+ close_file_descriptors(desc);
+ goto err;
+ }
+ vaSyncSurface(display, va_surface_id(mapper->src));
+ // No need to error out if sync fails, but good to know if it did.
+ CHECK_VA_STATUS(mapper, "vaSyncSurface()");
+ p->surface_acquired = true;
+
+ // We use AVDRMFrameDescriptor to store the dmabuf so we need to copy the
+ // values over.
+ int num_returned_planes = 0;
+ p->desc.nb_layers = desc.num_layers;
+ p->desc.nb_objects = desc.num_objects;
+ for (int i = 0; i < desc.num_layers; i++) {
+ p->desc.layers[i].format = desc.layers[i].drm_format;
+ p->desc.layers[i].nb_planes = desc.layers[i].num_planes;
+ for (int j = 0; j < desc.layers[i].num_planes; j++)
+ {
+ p->desc.layers[i].planes[j].object_index = desc.layers[i].object_index[j];
+ p->desc.layers[i].planes[j].offset = desc.layers[i].offset[j];
+ p->desc.layers[i].planes[j].pitch = desc.layers[i].pitch[j];
+ }
+
+ num_returned_planes += desc.layers[i].num_planes;
+ }
+ for (int i = 0; i < desc.num_objects; i++) {
+ p->desc.objects[i].format_modifier = desc.objects[i].drm_format_modifier;
+ p->desc.objects[i].fd = desc.objects[i].fd;
+ p->desc.objects[i].size = desc.objects[i].size;
+ }
+
+ // We can handle composed formats if the total number of planes is still
+ // equal the number of planes we expect. Complex formats with auxiliary
+ // planes cannot be supported.
+ if (p->num_planes != 0 && p->num_planes != num_returned_planes) {
+ mp_msg(mapper->log, p_owner->probing_formats ? MSGL_DEBUG : MSGL_ERR,
+ "Mapped surface with format '%s' has unexpected number of planes. "
+ "(%d layers and %d planes, but expected %d planes)\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ desc.num_layers, num_returned_planes, p->num_planes);
+ goto err;
+ }
+
+ if (!p_owner->dmabuf_interop.interop_map(mapper, &p_owner->dmabuf_interop,
+ p_owner->probing_formats))
+ goto err;
+
+ if (desc.fourcc == VA_FOURCC_YV12)
+ MPSWAP(struct ra_tex*, mapper->tex[1], mapper->tex[2]);
+
+ return 0;
+
+err:
+ mapper_unmap(mapper);
+
+ if (!p_owner->probing_formats)
+ MP_FATAL(mapper, "mapping VAAPI EGL image failed\n");
+ return -1;
+}
+
+static bool try_format_map(struct ra_hwdec *hw, struct mp_image *surface)
+{
+ struct ra_hwdec_mapper *mapper = ra_hwdec_mapper_create(hw, &surface->params);
+ if (!mapper) {
+ MP_DBG(hw, "Failed to create mapper\n");
+ return false;
+ }
+
+ bool ok = ra_hwdec_mapper_map(mapper, surface) >= 0;
+ ra_hwdec_mapper_free(&mapper);
+ return ok;
+}
+
+static void try_format_pixfmt(struct ra_hwdec *hw, enum AVPixelFormat pixfmt)
+{
+ bool supported = false;
+ struct priv_owner *p = hw->priv;
+
+ int mp_fmt = pixfmt2imgfmt(pixfmt);
+ if (!mp_fmt)
+ return;
+
+ int num_formats = 0;
+ for (int n = 0; p->formats && p->formats[n]; n++) {
+ if (p->formats[n] == mp_fmt)
+ return; // already added
+ num_formats += 1;
+ }
+
+ AVBufferRef *fref = NULL;
+ struct mp_image *s = NULL;
+ AVFrame *frame = NULL;
+ fref = av_hwframe_ctx_alloc(p->ctx->av_device_ref);
+ if (!fref)
+ goto err;
+ AVHWFramesContext *fctx = (void *)fref->data;
+ fctx->format = AV_PIX_FMT_VAAPI;
+ fctx->sw_format = pixfmt;
+ fctx->width = 128;
+ fctx->height = 128;
+ if (av_hwframe_ctx_init(fref) < 0)
+ goto err;
+ frame = av_frame_alloc();
+ if (!frame)
+ goto err;
+ if (av_hwframe_get_buffer(fref, frame, 0) < 0)
+ goto err;
+ s = mp_image_from_av_frame(frame);
+ if (!s || !mp_image_params_valid(&s->params))
+ goto err;
+ if (try_format_map(hw, s)) {
+ supported = true;
+ MP_TARRAY_APPEND(p, p->formats, num_formats, mp_fmt);
+ MP_TARRAY_APPEND(p, p->formats, num_formats, 0); // terminate it
+ }
+err:
+ if (!supported)
+ MP_DBG(hw, "Unsupported format: %s\n",
+ mp_imgfmt_to_name(mp_fmt));
+
+ talloc_free(s);
+ av_frame_free(&frame);
+ av_buffer_unref(&fref);
+}
+
+static void try_format_config(struct ra_hwdec *hw, AVVAAPIHWConfig *hwconfig)
+{
+ struct priv_owner *p = hw->priv;
+ enum AVPixelFormat *fmts = NULL;
+
+ AVHWFramesConstraints *fc =
+ av_hwdevice_get_hwframe_constraints(p->ctx->av_device_ref, hwconfig);
+ if (!fc) {
+ MP_WARN(hw, "failed to retrieve libavutil frame constraints\n");
+ return;
+ }
+
+ /*
+ * We need a hwframe_ctx to be able to get the valid formats, but to
+ * initialise it, we need a format, so we get the first format from the
+ * hwconfig. We don't care about the other formats in the config because the
+ * transfer formats list will already include them.
+ */
+ AVBufferRef *fref = NULL;
+ fref = av_hwframe_ctx_alloc(p->ctx->av_device_ref);
+ if (!fref) {
+ MP_WARN(hw, "failed to alloc libavutil frame context\n");
+ goto err;
+ }
+ AVHWFramesContext *fctx = (void *)fref->data;
+ fctx->format = AV_PIX_FMT_VAAPI;
+ fctx->sw_format = fc->valid_sw_formats[0];
+ fctx->width = 128;
+ fctx->height = 128;
+ if (av_hwframe_ctx_init(fref) < 0) {
+ MP_WARN(hw, "failed to init libavutil frame context\n");
+ goto err;
+ }
+
+ int ret = av_hwframe_transfer_get_formats(fref, AV_HWFRAME_TRANSFER_DIRECTION_TO, &fmts, 0);
+ if (ret) {
+ MP_WARN(hw, "failed to get libavutil frame context supported formats\n");
+ goto err;
+ }
+
+ for (int n = 0; fmts &&
+ fmts[n] != AV_PIX_FMT_NONE; n++)
+ try_format_pixfmt(hw, fmts[n]);
+
+err:
+ av_hwframe_constraints_free(&fc);
+ av_buffer_unref(&fref);
+ av_free(fmts);
+}
+
+static void determine_working_formats(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ VAStatus status;
+ VAProfile *profiles = NULL;
+ VAEntrypoint *entrypoints = NULL;
+
+ MP_VERBOSE(hw, "Going to probe surface formats (may log bogus errors)...\n");
+ p->probing_formats = true;
+
+ AVVAAPIHWConfig *hwconfig = av_hwdevice_hwconfig_alloc(p->ctx->av_device_ref);
+ if (!hwconfig) {
+ MP_WARN(hw, "Could not allocate FFmpeg AVVAAPIHWConfig\n");
+ goto done;
+ }
+
+ profiles = talloc_zero_array(NULL, VAProfile, vaMaxNumProfiles(p->display));
+ entrypoints = talloc_zero_array(NULL, VAEntrypoint,
+ vaMaxNumEntrypoints(p->display));
+ int num_profiles = 0;
+ status = vaQueryConfigProfiles(p->display, profiles, &num_profiles);
+ if (!CHECK_VA_STATUS(hw, "vaQueryConfigProfiles()"))
+ num_profiles = 0;
+
+ /*
+ * We need to find one declared format to bootstrap probing. So find a valid
+ * decoding profile and use its config. If try_format_config() finds any
+ * formats, they will be all the supported formats, and we don't need to
+ * look at any other profiles.
+ */
+ for (int n = 0; n < num_profiles; n++) {
+ VAProfile profile = profiles[n];
+ if (profile == VAProfileNone) {
+ // We don't use the None profile.
+ continue;
+ }
+ int num_ep = 0;
+ status = vaQueryConfigEntrypoints(p->display, profile, entrypoints,
+ &num_ep);
+ if (status != VA_STATUS_SUCCESS) {
+ MP_DBG(hw, "vaQueryConfigEntrypoints(): '%s' for profile %d",
+ vaErrorStr(status), (int)profile);
+ continue;
+ }
+ for (int ep = 0; ep < num_ep; ep++) {
+ if (entrypoints[ep] != VAEntrypointVLD) {
+ // We are only interested in decoding entrypoints.
+ continue;
+ }
+ VAConfigID config = VA_INVALID_ID;
+ status = vaCreateConfig(p->display, profile, entrypoints[ep],
+ NULL, 0, &config);
+ if (status != VA_STATUS_SUCCESS) {
+ MP_DBG(hw, "vaCreateConfig(): '%s' for profile %d",
+ vaErrorStr(status), (int)profile);
+ continue;
+ }
+
+ hwconfig->config_id = config;
+ try_format_config(hw, hwconfig);
+
+ vaDestroyConfig(p->display, config);
+ if (p->formats && p->formats[0]) {
+ goto done;
+ }
+ }
+ }
+
+done:
+ av_free(hwconfig);
+ talloc_free(profiles);
+ talloc_free(entrypoints);
+
+ p->probing_formats = false;
+
+ MP_DBG(hw, "Supported formats:\n");
+ for (int n = 0; p->formats && p->formats[n]; n++)
+ MP_DBG(hw, " %s\n", mp_imgfmt_to_name(p->formats[n]));
+ MP_VERBOSE(hw, "Done probing surface formats.\n");
+}
+
+const struct ra_hwdec_driver ra_hwdec_vaapi = {
+ .name = "vaapi",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_VAAPI, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct dmabuf_interop_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_vt.c b/video/out/hwdec/hwdec_vt.c
new file mode 100644
index 0000000..ab41d02
--- /dev/null
+++ b/video/out/hwdec/hwdec_vt.c
@@ -0,0 +1,141 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_videotoolbox.h>
+
+#include "config.h"
+
+#include "video/out/gpu/hwdec.h"
+#include "video/out/hwdec/hwdec_vt.h"
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+const static vt_interop_init interop_inits[] = {
+#if HAVE_VIDEOTOOLBOX_GL || HAVE_IOS_GL
+ vt_gl_init,
+#endif
+#if HAVE_VIDEOTOOLBOX_PL
+ vt_pl_init,
+#endif
+ NULL
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw)) {
+ break;
+ }
+ }
+
+ if (!p->interop_map || !p->interop_unmap) {
+ MP_VERBOSE(hw, "VT hwdec only works with OpenGL or Vulkan backends.\n");
+ return -1;
+ }
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .hw_imgfmt = IMGFMT_VIDEOTOOLBOX,
+ };
+
+ int ret = av_hwdevice_ctx_create(&p->hwctx.av_device_ref,
+ AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0);
+ if (ret != 0) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx: %s\n", av_err2str(ret));
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+
+ p_owner->interop_unmap(mapper);
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ if (p_owner->interop_uninit) {
+ p_owner->interop_uninit(mapper);
+ }
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ if (!mapper->dst_params.imgfmt) {
+ MP_ERR(mapper, "Unsupported CVPixelBuffer format.\n");
+ return -1;
+ }
+
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &p->desc)) {
+ MP_ERR(mapper, "Unsupported texture format.\n");
+ return -1;
+ }
+
+ if (p_owner->interop_init)
+ return p_owner->interop_init(mapper);
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+
+ return p_owner->interop_map(mapper);
+}
+
+const struct ra_hwdec_driver ra_hwdec_videotoolbox = {
+ .name = "videotoolbox",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_VIDEOTOOLBOX, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_vt.h b/video/out/hwdec/hwdec_vt.h
new file mode 100644
index 0000000..b79c641
--- /dev/null
+++ b/video/out/hwdec/hwdec_vt.h
@@ -0,0 +1,63 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <CoreVideo/CoreVideo.h>
+
+#include "config.h"
+#include "video/out/gpu/hwdec.h"
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+
+ int (*interop_init)(struct ra_hwdec_mapper *mapper);
+ void (*interop_uninit)(struct ra_hwdec_mapper *mapper);
+
+ int (*interop_map)(struct ra_hwdec_mapper *mapper);
+ void (*interop_unmap)(struct ra_hwdec_mapper *mapper);
+};
+
+#ifndef __OBJC__
+typedef struct __CVMetalTextureCache *CVMetalTextureCacheRef;
+typedef CVImageBufferRef CVMetalTextureRef;
+#endif
+
+struct priv {
+ void *interop_mapper_priv;
+
+ CVPixelBufferRef pbuf;
+
+#if HAVE_VIDEOTOOLBOX_GL
+ GLuint gl_planes[MP_MAX_PLANES];
+#elif HAVE_IOS_GL
+ CVOpenGLESTextureCacheRef gl_texture_cache;
+ CVOpenGLESTextureRef gl_planes[MP_MAX_PLANES];
+#endif
+
+#if HAVE_VIDEOTOOLBOX_PL
+ CVMetalTextureCacheRef mtl_texture_cache;
+ CVMetalTextureRef mtl_planes[MP_MAX_PLANES];
+#endif
+
+ struct ra_imgfmt_desc desc;
+};
+
+typedef bool (*vt_interop_init)(const struct ra_hwdec *hw);
+
+bool vt_gl_init(const struct ra_hwdec *hw);
+bool vt_pl_init(const struct ra_hwdec *hw);
diff --git a/video/out/hwdec/hwdec_vt_pl.m b/video/out/hwdec/hwdec_vt_pl.m
new file mode 100644
index 0000000..cd133a0
--- /dev/null
+++ b/video/out/hwdec/hwdec_vt_pl.m
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ * 2017 Aman Gupta <ffmpeg@tmm1.net>
+ * 2023 rcombs <rcombs@rcombs.me>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <CoreVideo/CoreVideo.h>
+#include <Metal/Metal.h>
+
+#include <libavutil/hwcontext.h>
+
+#include <libplacebo/renderer.h>
+
+#include "config.h"
+
+#include "video/out/gpu/hwdec.h"
+#include "video/out/placebo/ra_pl.h"
+#include "video/mp_image_pool.h"
+
+#if HAVE_VULKAN
+#include "video/out/vulkan/common.h"
+#endif
+
+#include "hwdec_vt.h"
+
+static bool check_hwdec(const struct ra_hwdec *hw)
+{
+ pl_gpu gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (!gpu) {
+ // This is not a libplacebo RA;
+ return false;
+ }
+
+ if (!(gpu->import_caps.tex & PL_HANDLE_MTL_TEX)) {
+ MP_VERBOSE(hw, "VideoToolbox libplacebo interop requires support for "
+ "PL_HANDLE_MTL_TEX import.\n");
+ return false;
+ }
+
+ return true;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ if (!mapper->dst_params.imgfmt) {
+ MP_ERR(mapper, "Unsupported CVPixelBuffer format.\n");
+ return -1;
+ }
+
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &p->desc)) {
+ MP_ERR(mapper, "Unsupported texture format.\n");
+ return -1;
+ }
+
+ for (int n = 0; n < p->desc.num_planes; n++) {
+ if (!p->desc.planes[n] || p->desc.planes[n]->ctype != RA_CTYPE_UNORM) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+ }
+
+ id<MTLDevice> mtl_device = nil;
+
+#ifdef VK_EXT_METAL_OBJECTS_SPEC_VERSION
+ pl_gpu gpu = ra_pl_get(mapper->ra);
+ if (gpu) {
+ pl_vulkan vulkan = pl_vulkan_get(gpu);
+ if (vulkan && vulkan->device && vulkan->instance && vulkan->get_proc_addr) {
+ PFN_vkExportMetalObjectsEXT pExportMetalObjects = (PFN_vkExportMetalObjectsEXT)vulkan->get_proc_addr(vulkan->instance, "vkExportMetalObjectsEXT");
+ if (pExportMetalObjects) {
+ VkExportMetalDeviceInfoEXT device_info = {
+ .sType = VK_STRUCTURE_TYPE_EXPORT_METAL_DEVICE_INFO_EXT,
+ .pNext = NULL,
+ .mtlDevice = nil,
+ };
+
+ VkExportMetalObjectsInfoEXT objects_info = {
+ .sType = VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECTS_INFO_EXT,
+ .pNext = &device_info,
+ };
+
+ pExportMetalObjects(vulkan->device, &objects_info);
+
+ mtl_device = device_info.mtlDevice;
+ [mtl_device retain];
+ }
+ }
+ }
+#endif
+
+ if (!mtl_device) {
+ mtl_device = MTLCreateSystemDefaultDevice();
+ }
+
+ CVReturn err = CVMetalTextureCacheCreate(
+ kCFAllocatorDefault,
+ NULL,
+ mtl_device,
+ NULL,
+ &p->mtl_texture_cache);
+
+ [mtl_device release];
+
+ if (err != noErr) {
+ MP_ERR(mapper, "Failure in CVOpenGLESTextureCacheCreate: %d\n", err);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+ if (p->mtl_planes[i]) {
+ CFRelease(p->mtl_planes[i]);
+ p->mtl_planes[i] = NULL;
+ }
+ }
+
+ CVMetalTextureCacheFlush(p->mtl_texture_cache, 0);
+}
+
+static const struct {
+ const char *glsl;
+ MTLPixelFormat mtl;
+} mtl_fmts[] = {
+ {"r16f", MTLPixelFormatR16Float },
+ {"r32f", MTLPixelFormatR32Float },
+ {"rg16f", MTLPixelFormatRG16Float },
+ {"rg32f", MTLPixelFormatRG32Float },
+ {"rgba16f", MTLPixelFormatRGBA16Float },
+ {"rgba32f", MTLPixelFormatRGBA32Float },
+ {"r11f_g11f_b10f", MTLPixelFormatRG11B10Float },
+
+ {"r8", MTLPixelFormatR8Unorm },
+ {"r16", MTLPixelFormatR16Unorm },
+ {"rg8", MTLPixelFormatRG8Unorm },
+ {"rg16", MTLPixelFormatRG16Unorm },
+ {"rgba8", MTLPixelFormatRGBA8Unorm },
+ {"rgba16", MTLPixelFormatRGBA16Unorm },
+ {"rgb10_a2", MTLPixelFormatRGB10A2Unorm },
+
+ {"r8_snorm", MTLPixelFormatR8Snorm },
+ {"r16_snorm", MTLPixelFormatR16Snorm },
+ {"rg8_snorm", MTLPixelFormatRG8Snorm },
+ {"rg16_snorm", MTLPixelFormatRG16Snorm },
+ {"rgba8_snorm", MTLPixelFormatRGBA8Snorm },
+ {"rgba16_snorm", MTLPixelFormatRGBA16Snorm },
+
+ {"r8ui", MTLPixelFormatR8Uint },
+ {"r16ui", MTLPixelFormatR16Uint },
+ {"r32ui", MTLPixelFormatR32Uint },
+ {"rg8ui", MTLPixelFormatRG8Uint },
+ {"rg16ui", MTLPixelFormatRG16Uint },
+ {"rg32ui", MTLPixelFormatRG32Uint },
+ {"rgba8ui", MTLPixelFormatRGBA8Uint },
+ {"rgba16ui", MTLPixelFormatRGBA16Uint },
+ {"rgba32ui", MTLPixelFormatRGBA32Uint },
+ {"rgb10_a2ui", MTLPixelFormatRGB10A2Uint },
+
+ {"r8i", MTLPixelFormatR8Sint },
+ {"r16i", MTLPixelFormatR16Sint },
+ {"r32i", MTLPixelFormatR32Sint },
+ {"rg8i", MTLPixelFormatRG8Sint },
+ {"rg16i", MTLPixelFormatRG16Sint },
+ {"rg32i", MTLPixelFormatRG32Sint },
+ {"rgba8i", MTLPixelFormatRGBA8Sint },
+ {"rgba16i", MTLPixelFormatRGBA16Sint },
+ {"rgba32i", MTLPixelFormatRGBA32Sint },
+
+ { NULL, MTLPixelFormatInvalid },
+};
+
+static MTLPixelFormat get_mtl_fmt(const char* glsl)
+{
+ if (!glsl)
+ return MTLPixelFormatInvalid;
+
+ for (int i = 0; mtl_fmts[i].glsl; i++) {
+ if (!strcmp(glsl, mtl_fmts[i].glsl))
+ return mtl_fmts[i].mtl;
+ }
+
+ return MTLPixelFormatInvalid;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ pl_gpu gpu = ra_pl_get(mapper->owner->ra_ctx->ra);
+
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = (CVPixelBufferRef)mapper->src->planes[3];
+ CVPixelBufferRetain(p->pbuf);
+
+ const bool planar = CVPixelBufferIsPlanar(p->pbuf);
+ const int planes = CVPixelBufferGetPlaneCount(p->pbuf);
+ assert((planar && planes == p->desc.num_planes) || p->desc.num_planes == 1);
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ const struct ra_format *fmt = p->desc.planes[i];
+
+ pl_fmt plfmt = ra_pl_fmt_get(fmt);
+ MTLPixelFormat format = get_mtl_fmt(plfmt->glsl_format);
+
+ if (!format) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+
+ size_t width = CVPixelBufferGetWidthOfPlane(p->pbuf, i),
+ height = CVPixelBufferGetHeightOfPlane(p->pbuf, i);
+
+ CVReturn err = CVMetalTextureCacheCreateTextureFromImage(
+ kCFAllocatorDefault,
+ p->mtl_texture_cache,
+ p->pbuf,
+ NULL,
+ format,
+ width,
+ height,
+ i,
+ &p->mtl_planes[i]);
+
+ if (err != noErr) {
+ MP_ERR(mapper, "error creating texture for plane %d: %d\n", i, err);
+ return -1;
+ }
+
+ struct pl_tex_params tex_params = {
+ .w = width,
+ .h = height,
+ .d = 0,
+ .format = plfmt,
+ .sampleable = true,
+ .import_handle = PL_HANDLE_MTL_TEX,
+ .shared_mem = (struct pl_shared_mem) {
+ .handle = {
+ .handle = CVMetalTextureGetTexture(p->mtl_planes[i]),
+ },
+ },
+ };
+
+ pl_tex pltex = pl_tex_create(gpu, &tex_params);
+ if (!pltex)
+ return -1;
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ int ret = mppl_wrap_tex(mapper->ra, pltex, ratex);
+ if (!ret) {
+ pl_tex_destroy(gpu, &pltex);
+ talloc_free(ratex);
+ return -1;
+ }
+ mapper->tex[i] = ratex;
+ }
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ CVPixelBufferRelease(p->pbuf);
+ if (p->mtl_texture_cache) {
+ CFRelease(p->mtl_texture_cache);
+ p->mtl_texture_cache = NULL;
+ }
+}
+
+bool vt_pl_init(const struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!check_hwdec(hw))
+ return false;
+
+ p->interop_init = mapper_init;
+ p->interop_uninit = mapper_uninit;
+ p->interop_map = mapper_map;
+ p->interop_unmap = mapper_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_vulkan.c b/video/out/hwdec/hwdec_vulkan.c
new file mode 100644
index 0000000..5f7354d
--- /dev/null
+++ b/video/out/hwdec/hwdec_vulkan.c
@@ -0,0 +1,333 @@
+/*
+ * Copyright (c) 2022 Philip Langdale <philipl@overt.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/vulkan/context.h"
+#include "video/out/placebo/ra_pl.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vulkan.h>
+
+struct vulkan_hw_priv {
+ struct mp_hwdec_ctx hwctx;
+ pl_gpu gpu;
+};
+
+struct vulkan_mapper_priv {
+ struct mp_image layout;
+ AVVkFrame *vkf;
+ pl_tex tex[4];
+};
+
+static void lock_queue(struct AVHWDeviceContext *ctx,
+ uint32_t queue_family, uint32_t index)
+{
+ pl_vulkan vulkan = ctx->user_opaque;
+ vulkan->lock_queue(vulkan, queue_family, index);
+}
+
+static void unlock_queue(struct AVHWDeviceContext *ctx,
+ uint32_t queue_family, uint32_t index)
+{
+ pl_vulkan vulkan = ctx->user_opaque;
+ vulkan->unlock_queue(vulkan, queue_family, index);
+}
+
+static int vulkan_init(struct ra_hwdec *hw)
+{
+ AVBufferRef *hw_device_ctx = NULL;
+ int ret = 0;
+ struct vulkan_hw_priv *p = hw->priv;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+
+ struct mpvk_ctx *vk = ra_vk_ctx_get(hw->ra_ctx);
+ if (!vk) {
+ MP_MSG(hw, level, "This is not a libplacebo vulkan gpu api context.\n");
+ return 0;
+ }
+
+ p->gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (!p->gpu) {
+ MP_MSG(hw, level, "Failed to obtain pl_gpu.\n");
+ return 0;
+ }
+
+ /*
+ * libplacebo initialises all queues, but we still need to discover which
+ * one is the decode queue.
+ */
+ uint32_t num_qf = 0;
+ VkQueueFamilyProperties *qf = NULL;
+ vkGetPhysicalDeviceQueueFamilyProperties(vk->vulkan->phys_device, &num_qf, NULL);
+ if (!num_qf)
+ goto error;
+
+ qf = talloc_array(NULL, VkQueueFamilyProperties, num_qf);
+ vkGetPhysicalDeviceQueueFamilyProperties(vk->vulkan->phys_device, &num_qf, qf);
+
+ int decode_index = -1, decode_count = 0;
+ for (int i = 0; i < num_qf; i++) {
+ /*
+ * Pick the first discovered decode queue that we find. Maybe a day will
+ * come when this needs to be smarter, but I'm sure a bunch of other
+ * things will have to change too.
+ */
+ if ((qf[i].queueFlags) & VK_QUEUE_VIDEO_DECODE_BIT_KHR) {
+ decode_index = i;
+ decode_count = qf[i].queueCount;
+ }
+ }
+
+ hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VULKAN);
+ if (!hw_device_ctx)
+ goto error;
+
+ AVHWDeviceContext *device_ctx = (void *)hw_device_ctx->data;
+ AVVulkanDeviceContext *device_hwctx = device_ctx->hwctx;
+
+ device_ctx->user_opaque = (void *)vk->vulkan;
+ device_hwctx->lock_queue = lock_queue;
+ device_hwctx->unlock_queue = unlock_queue;
+ device_hwctx->get_proc_addr = vk->vkinst->get_proc_addr;
+ device_hwctx->inst = vk->vkinst->instance;
+ device_hwctx->phys_dev = vk->vulkan->phys_device;
+ device_hwctx->act_dev = vk->vulkan->device;
+ device_hwctx->device_features = *vk->vulkan->features;
+ device_hwctx->enabled_inst_extensions = vk->vkinst->extensions;
+ device_hwctx->nb_enabled_inst_extensions = vk->vkinst->num_extensions;
+ device_hwctx->enabled_dev_extensions = vk->vulkan->extensions;
+ device_hwctx->nb_enabled_dev_extensions = vk->vulkan->num_extensions;
+ device_hwctx->queue_family_index = vk->vulkan->queue_graphics.index;
+ device_hwctx->nb_graphics_queues = vk->vulkan->queue_graphics.count;
+ device_hwctx->queue_family_tx_index = vk->vulkan->queue_transfer.index;
+ device_hwctx->nb_tx_queues = vk->vulkan->queue_transfer.count;
+ device_hwctx->queue_family_comp_index = vk->vulkan->queue_compute.index;
+ device_hwctx->nb_comp_queues = vk->vulkan->queue_compute.count;
+ device_hwctx->queue_family_decode_index = decode_index;
+ device_hwctx->nb_decode_queues = decode_count;
+
+ ret = av_hwdevice_ctx_init(hw_device_ctx);
+ if (ret < 0) {
+ MP_MSG(hw, level, "av_hwdevice_ctx_init failed\n");
+ goto error;
+ }
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .av_device_ref = hw_device_ctx,
+ .hw_imgfmt = IMGFMT_VULKAN,
+ };
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ talloc_free(qf);
+ return 0;
+
+ error:
+ talloc_free(qf);
+ av_buffer_unref(&hw_device_ctx);
+ return -1;
+}
+
+static void vulkan_uninit(struct ra_hwdec *hw)
+{
+ struct vulkan_hw_priv *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct vulkan_mapper_priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ struct ra_imgfmt_desc desc = {0};
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct vulkan_hw_priv *p_owner = mapper->owner->priv;
+ struct vulkan_mapper_priv *p = mapper->priv;
+ if (!mapper->src)
+ goto end;
+
+ AVHWFramesContext *hwfc = (AVHWFramesContext *) mapper->src->hwctx->data;;
+ const AVVulkanFramesContext *vkfc = hwfc->hwctx;;
+ AVVkFrame *vkf = p->vkf;
+
+ int num_images;
+ for (num_images = 0; (vkf->img[num_images] != VK_NULL_HANDLE); num_images++);
+
+ for (int i = 0; (p->tex[i] != NULL); i++) {
+ pl_tex *tex = &p->tex[i];
+ if (!*tex)
+ continue;
+
+ // If we have multiple planes and one image, then that is a multiplane
+ // frame. Anything else is treated as one-image-per-plane.
+ int index = p->layout.num_planes > 1 && num_images == 1 ? 0 : i;
+
+ // Update AVVkFrame state to reflect current layout
+ bool ok = pl_vulkan_hold_ex(p_owner->gpu, pl_vulkan_hold_params(
+ .tex = *tex,
+ .out_layout = &vkf->layout[index],
+ .qf = VK_QUEUE_FAMILY_IGNORED,
+ .semaphore = (pl_vulkan_sem) {
+ .sem = vkf->sem[index],
+ .value = vkf->sem_value[index] + 1,
+ },
+ ));
+
+ vkf->access[index] = 0;
+ vkf->sem_value[index] += !!ok;
+ *tex = NULL;
+ }
+
+ vkfc->unlock_frame(hwfc, vkf);
+
+ end:
+ for (int i = 0; i < p->layout.num_planes; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+
+ p->vkf = NULL;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ bool result = false;
+ struct vulkan_hw_priv *p_owner = mapper->owner->priv;
+ struct vulkan_mapper_priv *p = mapper->priv;
+ pl_vulkan vk = pl_vulkan_get(p_owner->gpu);
+ if (!vk)
+ return -1;
+
+ AVHWFramesContext *hwfc = (AVHWFramesContext *) mapper->src->hwctx->data;
+ const AVVulkanFramesContext *vkfc = hwfc->hwctx;
+ AVVkFrame *vkf = (AVVkFrame *) mapper->src->planes[0];
+
+ /*
+ * We need to use the dimensions from the HW Frames Context for the
+ * textures, as the underlying images may be larger than the logical frame
+ * size. This most often happens with 1080p content where the actual frame
+ * height is 1088.
+ */
+ struct mp_image raw_layout;
+ mp_image_setfmt(&raw_layout, p->layout.params.imgfmt);
+ mp_image_set_size(&raw_layout, hwfc->width, hwfc->height);
+
+ int num_images;
+ for (num_images = 0; (vkf->img[num_images] != VK_NULL_HANDLE); num_images++);
+ const VkFormat *vk_fmt = av_vkfmt_from_pixfmt(hwfc->sw_format);
+
+ vkfc->lock_frame(hwfc, vkf);
+
+ for (int i = 0; i < p->layout.num_planes; i++) {
+ pl_tex *tex = &p->tex[i];
+ VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
+ int index = i;
+
+ // If we have multiple planes and one image, then that is a multiplane
+ // frame. Anything else is treated as one-image-per-plane.
+ if (p->layout.num_planes > 1 && num_images == 1) {
+ index = 0;
+
+ switch (i) {
+ case 0:
+ aspect = VK_IMAGE_ASPECT_PLANE_0_BIT_KHR;
+ break;
+ case 1:
+ aspect = VK_IMAGE_ASPECT_PLANE_1_BIT_KHR;
+ break;
+ case 2:
+ aspect = VK_IMAGE_ASPECT_PLANE_2_BIT_KHR;
+ break;
+ default:
+ goto error;
+ }
+ }
+
+ *tex = pl_vulkan_wrap(p_owner->gpu, pl_vulkan_wrap_params(
+ .image = vkf->img[index],
+ .width = mp_image_plane_w(&raw_layout, i),
+ .height = mp_image_plane_h(&raw_layout, i),
+ .format = vk_fmt[i],
+ .usage = vkfc->usage,
+ .aspect = aspect,
+ ));
+ if (!*tex)
+ goto error;
+
+ pl_vulkan_release_ex(p_owner->gpu, pl_vulkan_release_params(
+ .tex = p->tex[i],
+ .layout = vkf->layout[index],
+ .qf = VK_QUEUE_FAMILY_IGNORED,
+ .semaphore = (pl_vulkan_sem) {
+ .sem = vkf->sem[index],
+ .value = vkf->sem_value[index],
+ },
+ ));
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ result = mppl_wrap_tex(mapper->ra, *tex, ratex);
+ if (!result) {
+ pl_tex_destroy(p_owner->gpu, tex);
+ talloc_free(ratex);
+ goto error;
+ }
+ mapper->tex[i] = ratex;
+ }
+
+ p->vkf = vkf;
+ return 0;
+
+ error:
+ vkfc->unlock_frame(hwfc, vkf);
+ mapper_unmap(mapper);
+ return -1;
+}
+
+const struct ra_hwdec_driver ra_hwdec_vulkan = {
+ .name = "vulkan",
+ .imgfmts = {IMGFMT_VULKAN, 0},
+ .priv_size = sizeof(struct vulkan_hw_priv),
+ .init = vulkan_init,
+ .uninit = vulkan_uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct vulkan_mapper_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/libmpv.h b/video/out/libmpv.h
new file mode 100644
index 0000000..a697eaf
--- /dev/null
+++ b/video/out/libmpv.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "libmpv/render.h"
+#include "vo.h"
+
+// Helper for finding a parameter value. It returns the direct pointer to the
+// value, and if not present, just returns the def argument. In particular, if
+// def is not NULL, this never returns NULL (unless a param value is defined
+// as accepting NULL, or the libmpv API user is triggering UB).
+void *get_mpv_render_param(mpv_render_param *params, mpv_render_param_type type,
+ void *def);
+
+#define GET_MPV_RENDER_PARAM(params, type, ctype, def) \
+ (*(ctype *)get_mpv_render_param(params, type, &(ctype){(def)}))
+
+typedef int (*mp_render_cb_control_fn)(struct vo *vo, void *cb_ctx, int *events,
+ uint32_t request, void *data);
+void mp_render_context_set_control_callback(mpv_render_context *ctx,
+ mp_render_cb_control_fn callback,
+ void *callback_ctx);
+bool mp_render_context_acquire(mpv_render_context *ctx);
+
+struct render_backend {
+ struct mpv_global *global;
+ struct mp_log *log;
+ const struct render_backend_fns *fns;
+
+ // Set on init, immutable afterwards.
+ int driver_caps;
+ struct mp_hwdec_devices *hwdec_devs;
+
+ void *priv;
+};
+
+// Generic backend for rendering via libmpv. This corresponds to vo/vo_driver,
+// except for rendering via the mpv_render_*() API. (As a consequence it's as
+// generic as the VO API.) Like with VOs, one backend can support multiple
+// underlying GPU APIs.
+struct render_backend_fns {
+ // Returns libmpv error code. In particular, this function has to check for
+ // MPV_RENDER_PARAM_API_TYPE, and silently return MPV_ERROR_NOT_IMPLEMENTED
+ // if the API is not included in this backend.
+ // If this fails, ->destroy() will be called.
+ int (*init)(struct render_backend *ctx, mpv_render_param *params);
+ // Check if the passed IMGFMT_ is supported.
+ bool (*check_format)(struct render_backend *ctx, int imgfmt);
+ // Implementation of mpv_render_context_set_parameter(). Optional.
+ int (*set_parameter)(struct render_backend *ctx, mpv_render_param param);
+ // Like vo_driver.reconfig().
+ void (*reconfig)(struct render_backend *ctx, struct mp_image_params *params);
+ // Like VOCTRL_RESET.
+ void (*reset)(struct render_backend *ctx);
+ void (*screenshot)(struct render_backend *ctx, struct vo_frame *frame,
+ struct voctrl_screenshot *args);
+ void (*perfdata)(struct render_backend *ctx,
+ struct voctrl_performance_data *out);
+ // Like vo_driver.get_image().
+ struct mp_image *(*get_image)(struct render_backend *ctx, int imgfmt,
+ int w, int h, int stride_align, int flags);
+ // This has two purposes: 1. set queue attributes on VO, 2. update the
+ // renderer's OSD pointer. Keep in mind that as soon as the caller releases
+ // the renderer lock, the VO pointer can become invalid. The OSD pointer
+ // will technically remain valid (even though it's a vo field), until it's
+ // unset with this function.
+ // Will be called if vo changes, or if renderer options change.
+ void (*update_external)(struct render_backend *ctx, struct vo *vo);
+ // Update screen area.
+ void (*resize)(struct render_backend *ctx, struct mp_rect *src,
+ struct mp_rect *dst, struct mp_osd_res *osd);
+ // Get target surface size from mpv_render_context_render() arguments.
+ int (*get_target_size)(struct render_backend *ctx, mpv_render_param *params,
+ int *out_w, int *out_h);
+ // Implementation of mpv_render_context_render().
+ int (*render)(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame);
+ // Free all data in ctx->priv.
+ void (*destroy)(struct render_backend *ctx);
+};
+
+extern const struct render_backend_fns render_backend_gpu;
+extern const struct render_backend_fns render_backend_sw;
diff --git a/video/out/libmpv_sw.c b/video/out/libmpv_sw.c
new file mode 100644
index 0000000..f1b08f0
--- /dev/null
+++ b/video/out/libmpv_sw.c
@@ -0,0 +1,208 @@
+#include "libmpv/render_gl.h"
+#include "libmpv.h"
+#include "sub/osd.h"
+#include "video/sws_utils.h"
+
+struct priv {
+ struct libmpv_gpu_context *context;
+
+ struct mp_sws_context *sws;
+ struct osd_state *osd;
+
+ struct mp_image_params src_params, dst_params;
+ struct mp_rect src_rc, dst_rc;
+ struct mp_osd_res osd_rc;
+ bool anything_changed;
+};
+
+static int init(struct render_backend *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ char *api = get_mpv_render_param(params, MPV_RENDER_PARAM_API_TYPE, NULL);
+ if (!api)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ if (strcmp(api, MPV_RENDER_API_TYPE_SW) != 0)
+ return MPV_ERROR_NOT_IMPLEMENTED;
+
+ p->sws = mp_sws_alloc(p);
+ mp_sws_enable_cmdline_opts(p->sws, ctx->global);
+
+ p->anything_changed = true;
+
+ return 0;
+}
+
+static bool check_format(struct render_backend *ctx, int imgfmt)
+{
+ struct priv *p = ctx->priv;
+
+ // Note: we don't know the output format yet. Using an arbitrary supported
+ // format is fine, because we know that any supported input format can
+ // be converted to any supported output format.
+ return mp_sws_supports_formats(p->sws, IMGFMT_RGB0, imgfmt);
+}
+
+static int set_parameter(struct render_backend *ctx, mpv_render_param param)
+{
+ return MPV_ERROR_NOT_IMPLEMENTED;
+}
+
+static void reconfig(struct render_backend *ctx, struct mp_image_params *params)
+{
+ struct priv *p = ctx->priv;
+
+ p->src_params = *params;
+ p->anything_changed = true;
+}
+
+static void reset(struct render_backend *ctx)
+{
+ // stateless
+}
+
+static void update_external(struct render_backend *ctx, struct vo *vo)
+{
+ struct priv *p = ctx->priv;
+
+ p->osd = vo ? vo->osd : NULL;
+}
+
+static void resize(struct render_backend *ctx, struct mp_rect *src,
+ struct mp_rect *dst, struct mp_osd_res *osd)
+{
+ struct priv *p = ctx->priv;
+
+ p->src_rc = *src;
+ p->dst_rc = *dst;
+ p->osd_rc = *osd;
+ p->anything_changed = true;
+}
+
+static int get_target_size(struct render_backend *ctx, mpv_render_param *params,
+ int *out_w, int *out_h)
+{
+ int *sz = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_SIZE, NULL);
+ if (!sz)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ *out_w = sz[0];
+ *out_h = sz[1];
+ return 0;
+}
+
+static int render(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame)
+{
+ struct priv *p = ctx->priv;
+
+ int *sz = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_SIZE, NULL);
+ char *fmt = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_FORMAT, NULL);
+ size_t *stride = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_STRIDE, NULL);
+ void *ptr = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_POINTER, NULL);
+
+ if (!sz || !fmt || !stride || !ptr)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ char *prev_fmt = mp_imgfmt_to_name(p->dst_params.imgfmt);
+ if (strcmp(prev_fmt, fmt) != 0)
+ p->anything_changed = true;
+
+ if (sz[0] != p->dst_params.w || sz[1] != p->dst_params.h)
+ p->anything_changed = true;
+
+ if (p->anything_changed) {
+ p->dst_params = (struct mp_image_params){
+ .imgfmt = mp_imgfmt_from_name(bstr0(fmt)),
+ .w = sz[0],
+ .h = sz[1],
+ };
+
+ // Exclude "problematic" formats. In particular, reject multi-plane and
+ // hw formats. Exclude non-byte-aligned formats for easier stride
+ // checking.
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p->dst_params.imgfmt);
+ if (!(desc.flags & MP_IMGFLAG_COLOR_RGB) ||
+ !(desc.flags & (MP_IMGFLAG_TYPE_UINT | MP_IMGFLAG_TYPE_FLOAT)) ||
+ (desc.flags & MP_IMGFLAG_TYPE_PAL8) ||
+ !(desc.flags & MP_IMGFLAG_BYTE_ALIGNED) ||
+ desc.num_planes != 1)
+ return MPV_ERROR_UNSUPPORTED;
+
+ mp_image_params_guess_csp(&p->dst_params);
+
+ // Can be unset if rendering before any video was loaded.
+ if (p->src_params.imgfmt) {
+ p->sws->src = p->src_params;
+ p->sws->src.w = mp_rect_w(p->src_rc);
+ p->sws->src.h = mp_rect_h(p->src_rc);
+
+ p->sws->dst = p->dst_params;
+ p->sws->dst.w = mp_rect_w(p->dst_rc);
+ p->sws->dst.h = mp_rect_h(p->dst_rc);
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return MPV_ERROR_UNSUPPORTED; // probably
+ }
+
+ p->anything_changed = false;
+ }
+
+ struct mp_image wrap_img = {0};
+ mp_image_set_params(&wrap_img, &p->dst_params);
+
+ size_t bpp = wrap_img.fmt.bpp[0] / 8;
+ if (!bpp || bpp * wrap_img.w > *stride || *stride % bpp)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ wrap_img.planes[0] = ptr;
+ wrap_img.stride[0] = *stride;
+
+ struct mp_image *img = frame->current;
+ if (img) {
+ assert(p->src_params.imgfmt);
+
+ mp_image_clear_rc_inv(&wrap_img, p->dst_rc);
+
+ struct mp_image src = *img;
+ struct mp_rect src_rc = p->src_rc;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, src.fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, src.fmt.align_y);
+ mp_image_crop_rc(&src, src_rc);
+
+ struct mp_image dst = wrap_img;
+ mp_image_crop_rc(&dst, p->dst_rc);
+
+ if (mp_sws_scale(p->sws, &dst, &src) < 0) {
+ mp_image_clear(&wrap_img, 0, 0, wrap_img.w, wrap_img.h);
+ return MPV_ERROR_GENERIC;
+ }
+ } else {
+ mp_image_clear(&wrap_img, 0, 0, wrap_img.w, wrap_img.h);
+ }
+
+ if (p->osd)
+ osd_draw_on_image(p->osd, p->osd_rc, img ? img->pts : 0, 0, &wrap_img);
+
+ return 0;
+}
+
+static void destroy(struct render_backend *ctx)
+{
+ // nop
+}
+
+const struct render_backend_fns render_backend_sw = {
+ .init = init,
+ .check_format = check_format,
+ .set_parameter = set_parameter,
+ .reconfig = reconfig,
+ .reset = reset,
+ .update_external = update_external,
+ .resize = resize,
+ .get_target_size = get_target_size,
+ .render = render,
+ .destroy = destroy,
+};
diff --git a/video/out/mac/common.swift b/video/out/mac/common.swift
new file mode 100644
index 0000000..aac7050
--- /dev/null
+++ b/video/out/mac/common.swift
@@ -0,0 +1,691 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+import IOKit.pwr_mgt
+
+class Common: NSObject {
+ var mpv: MPVHelper?
+ var log: LogHelper
+ let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue")
+
+ var window: Window?
+ var view: View?
+ var titleBar: TitleBar?
+
+ var link: CVDisplayLink?
+
+ let eventsLock = NSLock()
+ var events: Int = 0
+
+ var lightSensor: io_connect_t = 0
+ var lastLmu: UInt64 = 0
+ var lightSensorIOPort: IONotificationPortRef?
+
+ var displaySleepAssertion: IOPMAssertionID = IOPMAssertionID(0)
+
+ var appNotificationObservers: [NSObjectProtocol] = []
+
+ var cursorVisibilityWanted: Bool = true
+
+ var title: String = "mpv" {
+ didSet { if let window = window { window.title = title } }
+ }
+
+ init(_ mpLog: OpaquePointer?) {
+ log = LogHelper(mpLog)
+ }
+
+ func initMisc(_ vo: UnsafeMutablePointer<vo>) {
+ guard let mpv = mpv else {
+ log.sendError("Something went wrong, no MPVHelper was initialized")
+ exit(1)
+ }
+
+ startDisplayLink(vo)
+ initLightSensor()
+ addDisplayReconfigureObserver()
+ addAppNotifications()
+ mpv.setMacOptionCallback(macOptsWakeupCallback, context: self)
+ }
+
+ func initApp() {
+ guard let mpv = mpv else {
+ log.sendError("Something went wrong, no MPVHelper was initialized")
+ exit(1)
+ }
+
+ var policy: NSApplication.ActivationPolicy = .regular
+ switch mpv.macOpts.macos_app_activation_policy {
+ case 0:
+ policy = .regular
+ case 1:
+ policy = .accessory
+ case 2:
+ policy = .prohibited
+ default:
+ break
+ }
+
+ NSApp.setActivationPolicy(policy)
+ setAppIcon()
+ }
+
+ func initWindow(_ vo: UnsafeMutablePointer<vo>, _ previousActiveApp: NSRunningApplication?) {
+ let (mpv, targetScreen, wr) = getInitProperties(vo)
+
+ guard let view = self.view else {
+ log.sendError("Something went wrong, no View was initialized")
+ exit(1)
+ }
+
+ window = Window(contentRect: wr, screen: targetScreen, view: view, common: self)
+ guard let window = self.window else {
+ log.sendError("Something went wrong, no Window was initialized")
+ exit(1)
+ }
+
+ window.setOnTop(Bool(mpv.opts.ontop), Int(mpv.opts.ontop_level))
+ window.setOnAllWorkspaces(Bool(mpv.opts.all_workspaces))
+ window.keepAspect = Bool(mpv.opts.keepaspect_window)
+ window.title = title
+ window.border = Bool(mpv.opts.border)
+
+ titleBar = TitleBar(frame: wr, window: window, common: self)
+
+ let minimized = Bool(mpv.opts.window_minimized)
+ window.isRestorable = false
+ window.isReleasedWhenClosed = false
+ window.setMaximized(minimized ? false : Bool(mpv.opts.window_maximized))
+ window.setMinimized(minimized)
+ window.makeMain()
+ window.makeKey()
+
+ if !minimized {
+ window.orderFront(nil)
+ }
+
+ NSApp.activate(ignoringOtherApps: mpv.opts.focus_on_open)
+
+ // workaround for macOS 10.15 to refocus the previous App
+ if (!mpv.opts.focus_on_open) {
+ previousActiveApp?.activate(options: .activateAllWindows)
+ }
+ }
+
+ func initView(_ vo: UnsafeMutablePointer<vo>, _ layer: CALayer) {
+ let (_, _, wr) = getInitProperties(vo)
+
+ view = View(frame: wr, common: self)
+ guard let view = self.view else {
+ log.sendError("Something went wrong, no View was initialized")
+ exit(1)
+ }
+
+ view.layer = layer
+ view.wantsLayer = true
+ view.layerContentsPlacement = .scaleProportionallyToFit
+ }
+
+ func initWindowState() {
+ if mpv?.opts.fullscreen ?? false {
+ DispatchQueue.main.async {
+ self.window?.toggleFullScreen(nil)
+ }
+ } else {
+ window?.isMovableByWindowBackground = true
+ }
+ }
+
+ func uninitCommon() {
+ setCursorVisibility(true)
+ stopDisplaylink()
+ uninitLightSensor()
+ removeDisplayReconfigureObserver()
+ removeAppNotifications()
+ enableDisplaySleep()
+ window?.orderOut(nil)
+
+ titleBar?.removeFromSuperview()
+ view?.removeFromSuperview()
+ }
+
+ func displayLinkCallback(_ displayLink: CVDisplayLink,
+ _ inNow: UnsafePointer<CVTimeStamp>,
+ _ inOutputTime: UnsafePointer<CVTimeStamp>,
+ _ flagsIn: CVOptionFlags,
+ _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
+ {
+ return kCVReturnSuccess
+ }
+
+ func startDisplayLink(_ vo: UnsafeMutablePointer<vo>) {
+ CVDisplayLinkCreateWithActiveCGDisplays(&link)
+
+ guard let screen = getTargetScreen(forFullscreen: false) ?? NSScreen.main,
+ let link = self.link else
+ {
+ log.sendWarning("Couldn't start DisplayLink, no MPVHelper, Screen or DisplayLink available")
+ return
+ }
+
+ CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID)
+ CVDisplayLinkSetOutputHandler(link) { link, now, out, inFlags, outFlags -> CVReturn in
+ return self.displayLinkCallback(link, now, out, inFlags, outFlags)
+ }
+ CVDisplayLinkStart(link)
+ }
+
+ func stopDisplaylink() {
+ if let link = self.link, CVDisplayLinkIsRunning(link) {
+ CVDisplayLinkStop(link)
+ }
+ }
+
+ func updateDisplaylink() {
+ guard let screen = window?.screen, let link = self.link else {
+ log.sendWarning("Couldn't update DisplayLink, no Screen or DisplayLink available")
+ return
+ }
+
+ CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID)
+ queue.asyncAfter(deadline: DispatchTime.now() + 0.1) {
+ self.flagEvents(VO_EVENT_WIN_STATE)
+ }
+ }
+
+ func currentFps() -> Double {
+ if let link = self.link {
+ var actualFps = CVDisplayLinkGetActualOutputVideoRefreshPeriod(link)
+ let nominalData = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link)
+
+ if (nominalData.flags & Int32(CVTimeFlags.isIndefinite.rawValue)) < 1 {
+ let nominalFps = Double(nominalData.timeScale) / Double(nominalData.timeValue)
+
+ if actualFps > 0 {
+ actualFps = 1/actualFps
+ }
+
+ if fabs(actualFps - nominalFps) > 0.1 {
+ log.sendVerbose("Falling back to nominal display refresh rate: \(nominalFps)")
+ return nominalFps
+ } else {
+ return actualFps
+ }
+ }
+ } else {
+ log.sendWarning("No DisplayLink available")
+ }
+
+ log.sendWarning("Falling back to standard display refresh rate: 60Hz")
+ return 60.0
+ }
+
+ func enableDisplaySleep() {
+ IOPMAssertionRelease(displaySleepAssertion)
+ displaySleepAssertion = IOPMAssertionID(0)
+ }
+
+ func disableDisplaySleep() {
+ if displaySleepAssertion != IOPMAssertionID(0) { return }
+ IOPMAssertionCreateWithName(
+ kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
+ IOPMAssertionLevel(kIOPMAssertionLevelOn),
+ "io.mpv.video_playing_back" as CFString,
+ &displaySleepAssertion)
+ }
+
+ func lmuToLux(_ v: UInt64) -> Int {
+ // the polinomial approximation for apple lmu value -> lux was empirically
+ // derived by firefox developers (Apple provides no documentation).
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=793728
+ let power_c4: Double = 1 / pow(10, 27)
+ let power_c3: Double = 1 / pow(10, 19)
+ let power_c2: Double = 1 / pow(10, 12)
+ let power_c1: Double = 1 / pow(10, 5)
+
+ let lum = Double(v)
+ let term4: Double = -3.0 * power_c4 * pow(lum, 4.0)
+ let term3: Double = 2.6 * power_c3 * pow(lum, 3.0)
+ let term2: Double = -3.4 * power_c2 * pow(lum, 2.0)
+ let term1: Double = 3.9 * power_c1 * lum
+
+ let lux = Int(ceil(term4 + term3 + term2 + term1 - 0.19))
+ return lux > 0 ? lux : 0
+ }
+
+ var lightSensorCallback: IOServiceInterestCallback = { (ctx, service, messageType, messageArgument) -> Void in
+ let com = unsafeBitCast(ctx, to: Common.self)
+
+ var outputs: UInt32 = 2
+ var values: [UInt64] = [0, 0]
+
+ var kr = IOConnectCallMethod(com.lightSensor, 0, nil, 0, nil, 0, &values, &outputs, nil, nil)
+ if kr == KERN_SUCCESS {
+ var mean = (values[0] + values[1]) / 2
+ if com.lastLmu != mean {
+ com.lastLmu = mean
+ com.lightSensorUpdate()
+ }
+ }
+ }
+
+ func lightSensorUpdate() {
+ log.sendWarning("lightSensorUpdate not implemented")
+ }
+
+ func initLightSensor() {
+ let srv = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleLMUController"))
+ if srv == IO_OBJECT_NULL {
+ log.sendVerbose("Can't find an ambient light sensor")
+ return
+ }
+
+ lightSensorIOPort = IONotificationPortCreate(kIOMasterPortDefault)
+ IONotificationPortSetDispatchQueue(lightSensorIOPort, queue)
+ var n = io_object_t()
+ IOServiceAddInterestNotification(lightSensorIOPort, srv, kIOGeneralInterest, lightSensorCallback, MPVHelper.bridge(obj: self), &n)
+ let kr = IOServiceOpen(srv, mach_task_self_, 0, &lightSensor)
+ IOObjectRelease(srv)
+
+ if kr != KERN_SUCCESS {
+ log.sendVerbose("Can't start ambient light sensor connection")
+ return
+ }
+ lightSensorCallback(MPVHelper.bridge(obj: self), 0, 0, nil)
+ }
+
+ func uninitLightSensor() {
+ if lightSensorIOPort != nil {
+ IONotificationPortDestroy(lightSensorIOPort)
+ IOObjectRelease(lightSensor)
+ }
+ }
+
+ var reconfigureCallback: CGDisplayReconfigurationCallBack = { (display, flags, userInfo) in
+ if flags.contains(.setModeFlag) {
+ let com = unsafeBitCast(userInfo, to: Common.self)
+ let displayID = com.window?.screen?.displayID ?? display
+
+ if displayID == display {
+ com.log.sendVerbose("Detected display mode change, updating screen refresh rate")
+ com.flagEvents(VO_EVENT_WIN_STATE)
+ }
+ }
+ }
+
+ func addDisplayReconfigureObserver() {
+ CGDisplayRegisterReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
+ }
+
+ func removeDisplayReconfigureObserver() {
+ CGDisplayRemoveReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
+ }
+
+ func addAppNotifications() {
+ appNotificationObservers.append(NotificationCenter.default.addObserver(
+ forName: NSApplication.didBecomeActiveNotification,
+ object: nil,
+ queue: .main,
+ using: { [weak self] (_) in self?.appDidBecomeActive() }
+ ))
+ appNotificationObservers.append(NotificationCenter.default.addObserver(
+ forName: NSApplication.didResignActiveNotification,
+ object: nil,
+ queue: .main,
+ using: { [weak self] (_) in self?.appDidResignActive() }
+ ))
+ }
+
+ func removeAppNotifications() {
+ appNotificationObservers.forEach { NotificationCenter.default.removeObserver($0) }
+ appNotificationObservers.removeAll()
+ }
+
+ func appDidBecomeActive() {
+ flagEvents(VO_EVENT_FOCUS)
+ }
+
+ func appDidResignActive() {
+ flagEvents(VO_EVENT_FOCUS)
+ }
+
+ func setAppIcon() {
+ if let app = NSApp as? Application,
+ ProcessInfo.processInfo.environment["MPVBUNDLE"] != "true"
+ {
+ NSApp.applicationIconImage = app.getMPVIcon()
+ }
+ }
+
+ func updateCursorVisibility() {
+ setCursorVisibility(cursorVisibilityWanted)
+ }
+
+ func setCursorVisibility(_ visible: Bool) {
+ NSCursor.setHiddenUntilMouseMoves(!visible && (view?.canHideCursor() ?? false))
+ }
+
+ func updateICCProfile() {
+ log.sendWarning("updateICCProfile not implemented")
+ }
+
+ func getScreenBy(id screenID: Int) -> NSScreen? {
+ if screenID >= NSScreen.screens.count {
+ log.sendInfo("Screen ID \(screenID) does not exist, falling back to current device")
+ return nil
+ } else if screenID < 0 {
+ return nil
+ }
+ return NSScreen.screens[screenID]
+ }
+
+ func getScreenBy(name screenName: String?) -> NSScreen? {
+ for screen in NSScreen.screens {
+ if screen.localizedName == screenName {
+ return screen
+ }
+ }
+ return nil
+ }
+
+ func getTargetScreen(forFullscreen fs: Bool) -> NSScreen? {
+ guard let mpv = mpv else {
+ log.sendWarning("Unexpected nil value in getTargetScreen")
+ return nil
+ }
+
+ let screenID = fs ? mpv.opts.fsscreen_id : mpv.opts.screen_id
+ var name: String?
+ if let screenName = fs ? mpv.opts.fsscreen_name : mpv.opts.screen_name {
+ name = String(cString: screenName)
+ }
+ return getScreenBy(id: Int(screenID)) ?? getScreenBy(name: name)
+ }
+
+ func getCurrentScreen() -> NSScreen? {
+ return window != nil ? window?.screen :
+ getTargetScreen(forFullscreen: false) ??
+ NSScreen.main
+ }
+
+ func getWindowGeometry(forScreen screen: NSScreen,
+ videoOut vo: UnsafeMutablePointer<vo>) -> NSRect {
+ let r = screen.convertRectToBacking(screen.frame)
+ let targetFrame = (mpv?.macOpts.macos_geometry_calculation ?? Int32(FRAME_VISIBLE)) == FRAME_VISIBLE
+ ? screen.visibleFrame : screen.frame
+ let rv = screen.convertRectToBacking(targetFrame)
+
+ // convert origin to be relative to target screen
+ var originY = rv.origin.y - r.origin.y
+ let originX = rv.origin.x - r.origin.x
+ // flip the y origin, mp_rect expects the origin at the top-left
+ // macOS' windowing system operates from the bottom-left
+ originY = -(originY + rv.size.height)
+ var screenRC: mp_rect = mp_rect(x0: Int32(originX),
+ y0: Int32(originY),
+ x1: Int32(originX + rv.size.width),
+ y1: Int32(originY + rv.size.height))
+
+ var geo: vo_win_geometry = vo_win_geometry()
+ vo_calc_window_geometry2(vo, &screenRC, Double(screen.backingScaleFactor), &geo)
+ vo_apply_window_geometry(vo, &geo)
+
+ let height = CGFloat(geo.win.y1 - geo.win.y0)
+ let width = CGFloat(geo.win.x1 - geo.win.x0)
+ // flip the y origin again
+ let y = CGFloat(-geo.win.y1)
+ let x = CGFloat(geo.win.x0)
+ return screen.convertRectFromBacking(NSMakeRect(x, y, width, height))
+ }
+
+ func getInitProperties(_ vo: UnsafeMutablePointer<vo>) -> (MPVHelper, NSScreen, NSRect) {
+ guard let mpv = mpv else {
+ log.sendError("Something went wrong, no MPVHelper was initialized")
+ exit(1)
+ }
+ guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else {
+ log.sendError("Something went wrong, no Screen was found")
+ exit(1)
+ }
+
+ let wr = getWindowGeometry(forScreen: targetScreen, videoOut: vo)
+
+ return (mpv, targetScreen, wr)
+ }
+
+ // call before initApp, because on macOS +10.15 it changes the active App
+ func getActiveApp() -> NSRunningApplication? {
+ return NSWorkspace.shared.runningApplications.first(where: {$0.isActive})
+ }
+
+ func flagEvents(_ ev: Int) {
+ eventsLock.lock()
+ events |= ev
+ eventsLock.unlock()
+
+ guard let vout = mpv?.vo else {
+ log.sendWarning("vo nil in flagEvents")
+ return
+ }
+ vo_wakeup(vout)
+ }
+
+ func checkEvents() -> Int {
+ eventsLock.lock()
+ let ev = events
+ events = 0
+ eventsLock.unlock()
+ return ev
+ }
+
+ func windowDidEndAnimation() {}
+ func windowSetToFullScreen() {}
+ func windowSetToWindow() {}
+ func windowDidUpdateFrame() {}
+ func windowDidChangeScreen() {}
+ func windowDidChangeScreenProfile() {}
+ func windowDidChangeBackingProperties() {}
+ func windowWillStartLiveResize() {}
+ func windowDidEndLiveResize() {}
+ func windowDidResize() {}
+ func windowDidChangeOcclusionState() {}
+
+ @objc func control(_ vo: UnsafeMutablePointer<vo>,
+ events: UnsafeMutablePointer<Int32>,
+ request: UInt32,
+ data: UnsafeMutableRawPointer?) -> Int32
+ {
+ guard let mpv = mpv else {
+ log.sendWarning("Unexpected nil value in Control Callback")
+ return VO_FALSE
+ }
+
+ switch mp_voctrl(request) {
+ case VOCTRL_CHECK_EVENTS:
+ events.pointee |= Int32(checkEvents())
+ return VO_TRUE
+ case VOCTRL_VO_OPTS_CHANGED:
+ var opt: UnsafeMutableRawPointer?
+ while mpv.nextChangedOption(property: &opt) {
+ switch opt {
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.border):
+ DispatchQueue.main.async {
+ self.window?.border = Bool(mpv.opts.border)
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.fullscreen):
+ DispatchQueue.main.async {
+ self.window?.toggleFullScreen(nil)
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.ontop): fallthrough
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.ontop_level):
+ DispatchQueue.main.async {
+ self.window?.setOnTop(Bool(mpv.opts.ontop), Int(mpv.opts.ontop_level))
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.all_workspaces):
+ DispatchQueue.main.async {
+ self.window?.setOnAllWorkspaces(Bool(mpv.opts.all_workspaces))
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.keepaspect_window):
+ DispatchQueue.main.async {
+ self.window?.keepAspect = Bool(mpv.opts.keepaspect_window)
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.window_minimized):
+ DispatchQueue.main.async {
+ self.window?.setMinimized(Bool(mpv.opts.window_minimized))
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.window_maximized):
+ DispatchQueue.main.async {
+ self.window?.setMaximized(Bool(mpv.opts.window_maximized))
+ }
+ default:
+ break
+ }
+ }
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_FPS:
+ let fps = data!.assumingMemoryBound(to: CDouble.self)
+ fps.pointee = currentFps()
+ return VO_TRUE
+ case VOCTRL_GET_HIDPI_SCALE:
+ let scaleFactor = data!.assumingMemoryBound(to: CDouble.self)
+ let screen = getCurrentScreen()
+ let factor = window?.backingScaleFactor ??
+ screen?.backingScaleFactor ?? 1.0
+ scaleFactor.pointee = Double(factor)
+ return VO_TRUE
+ case VOCTRL_RESTORE_SCREENSAVER:
+ enableDisplaySleep()
+ return VO_TRUE
+ case VOCTRL_KILL_SCREENSAVER:
+ disableDisplaySleep()
+ return VO_TRUE
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ let cursorVisibility = data!.assumingMemoryBound(to: CBool.self)
+ cursorVisibilityWanted = cursorVisibility.pointee
+ DispatchQueue.main.async {
+ self.setCursorVisibility(self.cursorVisibilityWanted)
+ }
+ return VO_TRUE
+ case VOCTRL_GET_ICC_PROFILE:
+ let screen = getCurrentScreen()
+ guard var iccData = screen?.colorSpace?.iccProfileData else {
+ log.sendWarning("No Screen available to retrieve ICC profile")
+ return VO_TRUE
+ }
+
+ let icc = data!.assumingMemoryBound(to: bstr.self)
+ iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
+ guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return }
+ let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
+ icc.pointee = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count))
+ }
+ return VO_TRUE
+ case VOCTRL_GET_AMBIENT_LUX:
+ if lightSensor != 0 {
+ let lux = data!.assumingMemoryBound(to: Int32.self)
+ lux.pointee = Int32(lmuToLux(lastLmu))
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL
+ case VOCTRL_GET_UNFS_WINDOW_SIZE:
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeMutableBufferPointer(start: sizeData, count: 2)
+ var rect = window?.unfsContentFrame ?? NSRect(x: 0, y: 0, width: 1280, height: 720)
+ if let screen = window?.currentScreen, !Bool(mpv.opts.hidpi_window_scale) {
+ rect = screen.convertRectToBacking(rect)
+ }
+
+ size[0] = Int32(rect.size.width)
+ size[1] = Int32(rect.size.height)
+ return VO_TRUE
+ case VOCTRL_SET_UNFS_WINDOW_SIZE:
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeBufferPointer(start: sizeData, count: 2)
+ var rect = NSMakeRect(0, 0, CGFloat(size[0]), CGFloat(size[1]))
+ DispatchQueue.main.async {
+ if let screen = self.window?.currentScreen, !Bool(self.mpv?.opts.hidpi_window_scale ?? true) {
+ rect = screen.convertRectFromBacking(rect)
+ }
+ self.window?.updateSize(rect.size)
+ }
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_NAMES:
+ let dnames = data!.assumingMemoryBound(to: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?.self)
+ var array: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>? = nil
+ var count: Int32 = 0
+ let displayName = getCurrentScreen()?.localizedName ?? "Unknown"
+
+ SWIFT_TARRAY_STRING_APPEND(nil, &array, &count, ta_xstrdup(nil, displayName))
+ SWIFT_TARRAY_STRING_APPEND(nil, &array, &count, nil)
+ dnames.pointee = array
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_RES:
+ guard let screen = getCurrentScreen() else {
+ log.sendWarning("No Screen available to retrieve frame")
+ return VO_NOTAVAIL
+ }
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeMutableBufferPointer(start: sizeData, count: 2)
+ let frame = screen.convertRectToBacking(screen.frame)
+ size[0] = Int32(frame.size.width)
+ size[1] = Int32(frame.size.height)
+ return VO_TRUE
+ case VOCTRL_GET_FOCUSED:
+ let focus = data!.assumingMemoryBound(to: CBool.self)
+ focus.pointee = NSApp.isActive
+ return VO_TRUE
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ let titleData = data!.assumingMemoryBound(to: Int8.self)
+ DispatchQueue.main.async {
+ let title = NSString(utf8String: titleData) as String?
+ self.title = title ?? "Unknown Title"
+ }
+ return VO_TRUE
+ default:
+ return VO_NOTIMPL
+ }
+ }
+
+ let macOptsWakeupCallback: swift_wakeup_cb_fn = { ( ctx ) in
+ let com = unsafeBitCast(ctx, to: Common.self)
+ DispatchQueue.main.async {
+ com.macOptsUpdate()
+ }
+ }
+
+ func macOptsUpdate() {
+ guard let mpv = mpv else {
+ log.sendWarning("Unexpected nil value in mac opts update")
+ return
+ }
+
+ var opt: UnsafeMutableRawPointer?
+ while mpv.nextChangedMacOption(property: &opt) {
+ switch opt {
+ case MPVHelper.getPointer(&mpv.macOptsPtr.pointee.macos_title_bar_appearance):
+ titleBar?.set(appearance: Int(mpv.macOpts.macos_title_bar_appearance))
+ case MPVHelper.getPointer(&mpv.macOptsPtr.pointee.macos_title_bar_material):
+ titleBar?.set(material: Int(mpv.macOpts.macos_title_bar_material))
+ case MPVHelper.getPointer(&mpv.macOptsPtr.pointee.macos_title_bar_color):
+ titleBar?.set(color: mpv.macOpts.macos_title_bar_color)
+ default:
+ break
+ }
+ }
+ }
+}
diff --git a/video/out/mac/gl_layer.swift b/video/out/mac/gl_layer.swift
new file mode 100644
index 0000000..dd96af7
--- /dev/null
+++ b/video/out/mac/gl_layer.swift
@@ -0,0 +1,322 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+import OpenGL.GL
+import OpenGL.GL3
+
+let glVersions: [CGLOpenGLProfile] = [
+ kCGLOGLPVersion_3_2_Core,
+ kCGLOGLPVersion_Legacy
+]
+
+let glFormatBase: [CGLPixelFormatAttribute] = [
+ kCGLPFAOpenGLProfile,
+ kCGLPFAAccelerated,
+ kCGLPFADoubleBuffer
+]
+
+let glFormatSoftwareBase: [CGLPixelFormatAttribute] = [
+ kCGLPFAOpenGLProfile,
+ kCGLPFARendererID,
+ CGLPixelFormatAttribute(UInt32(kCGLRendererGenericFloatID)),
+ kCGLPFADoubleBuffer
+]
+
+let glFormatOptional: [[CGLPixelFormatAttribute]] = [
+ [kCGLPFABackingStore],
+ [kCGLPFAAllowOfflineRenderers]
+]
+
+let glFormat10Bit: [CGLPixelFormatAttribute] = [
+ kCGLPFAColorSize,
+ _CGLPixelFormatAttribute(rawValue: 64),
+ kCGLPFAColorFloat
+]
+
+let glFormatAutoGPU: [CGLPixelFormatAttribute] = [
+ kCGLPFASupportsAutomaticGraphicsSwitching
+]
+
+let attributeLookUp: [UInt32:String] = [
+ kCGLOGLPVersion_3_2_Core.rawValue: "kCGLOGLPVersion_3_2_Core",
+ kCGLOGLPVersion_Legacy.rawValue: "kCGLOGLPVersion_Legacy",
+ kCGLPFAOpenGLProfile.rawValue: "kCGLPFAOpenGLProfile",
+ UInt32(kCGLRendererGenericFloatID): "kCGLRendererGenericFloatID",
+ kCGLPFARendererID.rawValue: "kCGLPFARendererID",
+ kCGLPFAAccelerated.rawValue: "kCGLPFAAccelerated",
+ kCGLPFADoubleBuffer.rawValue: "kCGLPFADoubleBuffer",
+ kCGLPFABackingStore.rawValue: "kCGLPFABackingStore",
+ kCGLPFAColorSize.rawValue: "kCGLPFAColorSize",
+ kCGLPFAColorFloat.rawValue: "kCGLPFAColorFloat",
+ kCGLPFAAllowOfflineRenderers.rawValue: "kCGLPFAAllowOfflineRenderers",
+ kCGLPFASupportsAutomaticGraphicsSwitching.rawValue: "kCGLPFASupportsAutomaticGraphicsSwitching",
+]
+
+class GLLayer: CAOpenGLLayer {
+ unowned var cocoaCB: CocoaCB
+ var libmpv: LibmpvHelper { get { return cocoaCB.libmpv } }
+
+ let displayLock = NSLock()
+ let cglContext: CGLContextObj
+ let cglPixelFormat: CGLPixelFormatObj
+ var needsFlip: Bool = false
+ var forceDraw: Bool = false
+ var surfaceSize: NSSize = NSSize(width: 0, height: 0)
+ var bufferDepth: GLint = 8
+
+ enum Draw: Int { case normal = 1, atomic, atomicEnd }
+ var draw: Draw = .normal
+
+ let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue.draw")
+
+ var needsICCUpdate: Bool = false {
+ didSet {
+ if needsICCUpdate == true {
+ update()
+ }
+ }
+ }
+
+ var inLiveResize: Bool = false {
+ didSet {
+ if inLiveResize {
+ isAsynchronous = true
+ }
+ update(force: true)
+ }
+ }
+
+ init(cocoaCB ccb: CocoaCB) {
+ cocoaCB = ccb
+ (cglPixelFormat, bufferDepth) = GLLayer.createPixelFormat(ccb)
+ cglContext = GLLayer.createContext(ccb, cglPixelFormat)
+ super.init()
+ autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
+ backgroundColor = NSColor.black.cgColor
+
+ if bufferDepth > 8 {
+ contentsFormat = .RGBA16Float
+ }
+
+ var i: GLint = 1
+ CGLSetParameter(cglContext, kCGLCPSwapInterval, &i)
+ CGLSetCurrentContext(cglContext)
+
+ libmpv.initRender()
+ libmpv.setRenderUpdateCallback(updateCallback, context: self)
+ libmpv.setRenderControlCallback(cocoaCB.controlCallback, context: cocoaCB)
+ }
+
+ // necessary for when the layer containing window changes the screen
+ override init(layer: Any) {
+ guard let oldLayer = layer as? GLLayer else {
+ fatalError("init(layer: Any) passed an invalid layer")
+ }
+ cocoaCB = oldLayer.cocoaCB
+ surfaceSize = oldLayer.surfaceSize
+ cglPixelFormat = oldLayer.cglPixelFormat
+ cglContext = oldLayer.cglContext
+ super.init()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func canDraw(inCGLContext ctx: CGLContextObj,
+ pixelFormat pf: CGLPixelFormatObj,
+ forLayerTime t: CFTimeInterval,
+ displayTime ts: UnsafePointer<CVTimeStamp>?) -> Bool {
+ if inLiveResize == false {
+ isAsynchronous = false
+ }
+ return cocoaCB.backendState == .initialized &&
+ (forceDraw || libmpv.isRenderUpdateFrame())
+ }
+
+ override func draw(inCGLContext ctx: CGLContextObj,
+ pixelFormat pf: CGLPixelFormatObj,
+ forLayerTime t: CFTimeInterval,
+ displayTime ts: UnsafePointer<CVTimeStamp>?) {
+ needsFlip = false
+ forceDraw = false
+
+ if draw.rawValue >= Draw.atomic.rawValue {
+ if draw == .atomic {
+ draw = .atomicEnd
+ } else {
+ atomicDrawingEnd()
+ }
+ }
+
+ updateSurfaceSize()
+ libmpv.drawRender(surfaceSize, bufferDepth, ctx)
+
+ if needsICCUpdate {
+ needsICCUpdate = false
+ cocoaCB.updateICCProfile()
+ }
+ }
+
+ func updateSurfaceSize() {
+ var dims: [GLint] = [0, 0, 0, 0]
+ glGetIntegerv(GLenum(GL_VIEWPORT), &dims)
+ surfaceSize = NSSize(width: CGFloat(dims[2]), height: CGFloat(dims[3]))
+
+ if NSEqualSizes(surfaceSize, NSZeroSize) {
+ surfaceSize = bounds.size
+ surfaceSize.width *= contentsScale
+ surfaceSize.height *= contentsScale
+ }
+ }
+
+ func atomicDrawingStart() {
+ if draw == .normal {
+ NSDisableScreenUpdates()
+ draw = .atomic
+ }
+ }
+
+ func atomicDrawingEnd() {
+ if draw.rawValue >= Draw.atomic.rawValue {
+ NSEnableScreenUpdates()
+ draw = .normal
+ }
+ }
+
+ override func copyCGLPixelFormat(forDisplayMask mask: UInt32) -> CGLPixelFormatObj {
+ return cglPixelFormat
+ }
+
+ override func copyCGLContext(forPixelFormat pf: CGLPixelFormatObj) -> CGLContextObj {
+ contentsScale = cocoaCB.window?.backingScaleFactor ?? 1.0
+ return cglContext
+ }
+
+ let updateCallback: mpv_render_update_fn = { (ctx) in
+ let layer: GLLayer = unsafeBitCast(ctx, to: GLLayer.self)
+ layer.update()
+ }
+
+ override func display() {
+ displayLock.lock()
+ let isUpdate = needsFlip
+ super.display()
+ CATransaction.flush()
+ if isUpdate && needsFlip {
+ CGLSetCurrentContext(cglContext)
+ if libmpv.isRenderUpdateFrame() {
+ libmpv.drawRender(NSZeroSize, bufferDepth, cglContext, skip: true)
+ }
+ }
+ displayLock.unlock()
+ }
+
+ func update(force: Bool = false) {
+ if force { forceDraw = true }
+ queue.async {
+ if self.forceDraw || !self.inLiveResize {
+ self.needsFlip = true
+ self.display()
+ }
+ }
+ }
+
+ class func createPixelFormat(_ ccb: CocoaCB) -> (CGLPixelFormatObj, GLint) {
+ var pix: CGLPixelFormatObj?
+ var depth: GLint = 8
+ var err: CGLError = CGLError(rawValue: 0)
+ let swRender = ccb.libmpv.macOpts.cocoa_cb_sw_renderer
+
+ if swRender != 1 {
+ (pix, depth, err) = GLLayer.findPixelFormat(ccb)
+ }
+
+ if (err != kCGLNoError || pix == nil) && swRender != 0 {
+ (pix, depth, err) = GLLayer.findPixelFormat(ccb, software: true)
+ }
+
+ guard let pixelFormat = pix, err == kCGLNoError else {
+ ccb.log.sendError("Couldn't create any CGL pixel format")
+ exit(1)
+ }
+
+ return (pixelFormat, depth)
+ }
+
+ class func findPixelFormat(_ ccb: CocoaCB, software: Bool = false) -> (CGLPixelFormatObj?, GLint, CGLError) {
+ var pix: CGLPixelFormatObj?
+ var err: CGLError = CGLError(rawValue: 0)
+ var npix: GLint = 0
+
+ for ver in glVersions {
+ var glBase = software ? glFormatSoftwareBase : glFormatBase
+ glBase.insert(CGLPixelFormatAttribute(ver.rawValue), at: 1)
+
+ var glFormat = [glBase]
+ if ccb.libmpv.macOpts.cocoa_cb_10bit_context {
+ glFormat += [glFormat10Bit]
+ }
+ glFormat += glFormatOptional
+
+ if !ccb.libmpv.macOpts.macos_force_dedicated_gpu {
+ glFormat += [glFormatAutoGPU]
+ }
+
+ for index in stride(from: glFormat.count-1, through: 0, by: -1) {
+ let format = glFormat.flatMap { $0 } + [_CGLPixelFormatAttribute(rawValue: 0)]
+ err = CGLChoosePixelFormat(format, &pix, &npix)
+
+ if err == kCGLBadAttribute || err == kCGLBadPixelFormat || pix == nil {
+ glFormat.remove(at: index)
+ } else {
+ let attArray = format.map({ (value: _CGLPixelFormatAttribute) -> String in
+ return attributeLookUp[value.rawValue] ?? String(value.rawValue)
+ })
+
+ ccb.log.sendVerbose("Created CGL pixel format with attributes: " +
+ "\(attArray.joined(separator: ", "))")
+ return (pix, glFormat.contains(glFormat10Bit) ? 16 : 8, err)
+ }
+ }
+ }
+
+ let errS = String(cString: CGLErrorString(err))
+ ccb.log.sendWarning("Couldn't create a " +
+ "\(software ? "software" : "hardware accelerated") " +
+ "CGL pixel format: \(errS) (\(err.rawValue))")
+ if software == false && ccb.libmpv.macOpts.cocoa_cb_sw_renderer == -1 {
+ ccb.log.sendWarning("Falling back to software renderer")
+ }
+
+ return (pix, 8, err)
+ }
+
+ class func createContext(_ ccb: CocoaCB, _ pixelFormat: CGLPixelFormatObj) -> CGLContextObj {
+ var context: CGLContextObj?
+ let error = CGLCreateContext(pixelFormat, nil, &context)
+
+ guard let cglContext = context, error == kCGLNoError else {
+ let errS = String(cString: CGLErrorString(error))
+ ccb.log.sendError("Couldn't create a CGLContext: " + errS)
+ exit(1)
+ }
+
+ return cglContext
+ }
+}
diff --git a/video/out/mac/metal_layer.swift b/video/out/mac/metal_layer.swift
new file mode 100644
index 0000000..7cea87c
--- /dev/null
+++ b/video/out/mac/metal_layer.swift
@@ -0,0 +1,43 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class MetalLayer: CAMetalLayer {
+ unowned var common: MacCommon
+
+ init(common com: MacCommon) {
+ common = com
+ super.init()
+
+ pixelFormat = .rgba16Float
+ backgroundColor = NSColor.black.cgColor
+ }
+
+ // necessary for when the layer containing window changes the screen
+ override init(layer: Any) {
+ guard let oldLayer = layer as? MetalLayer else {
+ fatalError("init(layer: Any) passed an invalid layer")
+ }
+ common = oldLayer.common
+ super.init()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/video/out/mac/title_bar.swift b/video/out/mac/title_bar.swift
new file mode 100644
index 0000000..764c1ff
--- /dev/null
+++ b/video/out/mac/title_bar.swift
@@ -0,0 +1,229 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class TitleBar: NSVisualEffectView {
+ unowned var common: Common
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ var systemBar: NSView? {
+ get { return common.window?.standardWindowButton(.closeButton)?.superview }
+ }
+ static var height: CGFloat {
+ get { return NSWindow.frameRect(forContentRect: CGRect.zero, styleMask: .titled).size.height }
+ }
+ var buttons: [NSButton] {
+ get { return ([.closeButton, .miniaturizeButton, .zoomButton] as [NSWindow.ButtonType]).compactMap { common.window?.standardWindowButton($0) } }
+ }
+
+ override var material: NSVisualEffectView.Material {
+ get { return super.material }
+ set {
+ super.material = newValue
+ // fix for broken deprecated materials
+ if material == .light || material == .dark || material == .mediumLight ||
+ material == .ultraDark
+ {
+ state = .active
+ } else {
+ state = .followsWindowActiveState
+ }
+
+ }
+ }
+
+ init(frame: NSRect, window: NSWindow, common com: Common) {
+ let f = NSMakeRect(0, frame.size.height - TitleBar.height,
+ frame.size.width, TitleBar.height + 1)
+ common = com
+ super.init(frame: f)
+ buttons.forEach { $0.isHidden = true }
+ isHidden = true
+ alphaValue = 0
+ blendingMode = .withinWindow
+ autoresizingMask = [.width, .minYMargin]
+ systemBar?.alphaValue = 0
+ state = .followsWindowActiveState
+ wantsLayer = true
+
+ window.contentView?.addSubview(self, positioned: .above, relativeTo: nil)
+ window.titlebarAppearsTransparent = true
+ window.styleMask.insert(.fullSizeContentView)
+ set(appearance: Int(mpv?.macOpts.macos_title_bar_appearance ?? 0))
+ set(material: Int(mpv?.macOpts.macos_title_bar_material ?? 0))
+ set(color: mpv?.macOpts.macos_title_bar_color ?? "#00000000")
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // catch these events so they are not propagated to the underlying view
+ override func mouseDown(with event: NSEvent) { }
+
+ override func mouseUp(with event: NSEvent) {
+ if event.clickCount > 1 {
+ let def = UserDefaults.standard
+ var action = def.string(forKey: "AppleActionOnDoubleClick")
+
+ // macOS 10.10 and earlier
+ if action == nil {
+ action = def.bool(forKey: "AppleMiniaturizeOnDoubleClick") == true ?
+ "Minimize" : "Maximize"
+ }
+
+ if action == "Minimize" {
+ window?.miniaturize(self)
+ } else if action == "Maximize" {
+ window?.zoom(self)
+ }
+ }
+
+ common.window?.isMoving = false
+ }
+
+ func set(appearance: Any) {
+ if appearance is Int {
+ window?.appearance = appearanceFrom(string: String(appearance as? Int ?? 0))
+ } else {
+ window?.appearance = appearanceFrom(string: appearance as? String ?? "auto")
+ }
+ }
+
+ func set(material: Any) {
+ if material is Int {
+ self.material = materialFrom(string: String(material as? Int ?? 0))
+ } else {
+ self.material = materialFrom(string: material as? String ?? "titlebar")
+ }
+ }
+
+ func set(color: Any) {
+ if color is String {
+ layer?.backgroundColor = NSColor(hex: color as? String ?? "#00000000").cgColor
+ } else {
+ let col = color as? m_color ?? m_color(r: 0, g: 0, b: 0, a: 0)
+ let red = CGFloat(col.r)/255
+ let green = CGFloat(col.g)/255
+ let blue = CGFloat(col.b)/255
+ let alpha = CGFloat(col.a)/255
+ layer?.backgroundColor = NSColor(calibratedRed: red, green: green,
+ blue: blue, alpha: alpha).cgColor
+ }
+ }
+
+ func show() {
+ guard let window = common.window else { return }
+ if !window.border && !window.isInFullscreen { return }
+ let loc = common.view?.convert(window.mouseLocationOutsideOfEventStream, from: nil)
+
+ buttons.forEach { $0.isHidden = false }
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = 0.20
+ systemBar?.animator().alphaValue = 1
+ if !window.isInFullscreen && !window.isAnimating {
+ animator().alphaValue = 1
+ isHidden = false
+ }
+ }, completionHandler: nil )
+
+ if loc?.y ?? 0 > TitleBar.height {
+ hideDelayed()
+ } else {
+ NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hide), object: nil)
+ }
+ }
+
+ @objc func hide(_ duration: TimeInterval = 0.20) {
+ guard let window = common.window else { return }
+ if window.isInFullscreen && !window.isAnimating {
+ alphaValue = 0
+ isHidden = true
+ return
+ }
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = duration
+ systemBar?.animator().alphaValue = 0
+ animator().alphaValue = 0
+ }, completionHandler: {
+ self.buttons.forEach { $0.isHidden = true }
+ self.isHidden = true
+ })
+ }
+
+ func hideDelayed() {
+ NSObject.cancelPreviousPerformRequests(withTarget: self,
+ selector: #selector(hide),
+ object: nil)
+ perform(#selector(hide), with: nil, afterDelay: 0.5)
+ }
+
+ func appearanceFrom(string: String) -> NSAppearance? {
+ switch string {
+ case "1", "aqua":
+ return NSAppearance(named: .aqua)
+ case "2", "darkAqua":
+ return NSAppearance(named: .darkAqua)
+ case "3", "vibrantLight":
+ return NSAppearance(named: .vibrantLight)
+ case "4", "vibrantDark":
+ return NSAppearance(named: .vibrantDark)
+ case "5", "aquaHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastAqua)
+ case "6", "darkAquaHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastDarkAqua)
+ case "7", "vibrantLightHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastVibrantLight)
+ case "8", "vibrantDarkHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastVibrantDark)
+ case "0", "auto": fallthrough
+ default:
+ return nil
+ }
+
+
+ let style = UserDefaults.standard.string(forKey: "AppleInterfaceStyle")
+ return appearanceFrom(string: style == nil ? "aqua" : "vibrantDark")
+ }
+
+ func materialFrom(string: String) -> NSVisualEffectView.Material {
+ switch string {
+ case "0", "titlebar": return .titlebar
+ case "1", "selection": return .selection
+ case "2,", "menu": return .menu
+ case "3", "popover": return .popover
+ case "4", "sidebar": return .sidebar
+ case "5,", "headerView": return .headerView
+ case "6", "sheet": return .sheet
+ case "7", "windowBackground": return .windowBackground
+ case "8", "hudWindow": return .hudWindow
+ case "9", "fullScreen": return .fullScreenUI
+ case "10", "toolTip": return .toolTip
+ case "11", "contentBackground": return .contentBackground
+ case "12", "underWindowBackground": return .underWindowBackground
+ case "13", "underPageBackground": return .underPageBackground
+ case "14", "dark": return .dark
+ case "15", "light": return .light
+ case "16", "mediumLight": return .mediumLight
+ case "17", "ultraDark": return .ultraDark
+ default: break
+ }
+
+ return .titlebar
+ }
+}
diff --git a/video/out/mac/view.swift b/video/out/mac/view.swift
new file mode 100644
index 0000000..c4776c3
--- /dev/null
+++ b/video/out/mac/view.swift
@@ -0,0 +1,297 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class View: NSView {
+ unowned var common: Common
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ var tracker: NSTrackingArea?
+ var hasMouseDown: Bool = false
+
+ override var isFlipped: Bool { return true }
+ override var acceptsFirstResponder: Bool { return true }
+
+
+ init(frame: NSRect, common com: Common) {
+ common = com
+ super.init(frame: frame)
+ autoresizingMask = [.width, .height]
+ wantsBestResolutionOpenGLSurface = true
+ registerForDraggedTypes([ .fileURL, .URL, .string ])
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func updateTrackingAreas() {
+ if let tracker = self.tracker {
+ removeTrackingArea(tracker)
+ }
+
+ tracker = NSTrackingArea(rect: bounds,
+ options: [.activeAlways, .mouseEnteredAndExited, .mouseMoved, .enabledDuringMouseDrag],
+ owner: self, userInfo: nil)
+ // here tracker is guaranteed to be none-nil
+ addTrackingArea(tracker!)
+
+ if containsMouseLocation() {
+ cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0)
+ }
+ }
+
+ override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
+ guard let types = sender.draggingPasteboard.types else { return [] }
+ if types.contains(.fileURL) || types.contains(.URL) || types.contains(.string) {
+ return .copy
+ }
+ return []
+ }
+
+ func isURL(_ str: String) -> Bool {
+ // force unwrapping is fine here, regex is guaranteed to be valid
+ let regex = try! NSRegularExpression(pattern: "^(https?|ftp)://[^\\s/$.?#].[^\\s]*$",
+ options: .caseInsensitive)
+ let isURL = regex.numberOfMatches(in: str,
+ options: [],
+ range: NSRange(location: 0, length: str.count))
+ return isURL > 0
+ }
+
+ override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ let pb = sender.draggingPasteboard
+ guard let types = pb.types else { return false }
+
+ if types.contains(.fileURL) || types.contains(.URL) {
+ if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL] {
+ let files = urls.map { $0.absoluteString }
+ EventsResponder.sharedInstance().handleFilesArray(files)
+ return true
+ }
+ } else if types.contains(.string) {
+ guard let str = pb.string(forType: .string) else { return false }
+ var filesArray: [String] = []
+
+ for val in str.components(separatedBy: "\n") {
+ let url = val.trimmingCharacters(in: .whitespacesAndNewlines)
+ let path = (url as NSString).expandingTildeInPath
+ if isURL(url) {
+ filesArray.append(url)
+ } else if path.starts(with: "/") {
+ filesArray.append(path)
+ }
+ }
+ EventsResponder.sharedInstance().handleFilesArray(filesArray)
+ return true
+ }
+ return false
+ }
+
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+ return true
+ }
+
+ override func becomeFirstResponder() -> Bool {
+ return true
+ }
+
+ override func resignFirstResponder() -> Bool {
+ return true
+ }
+
+ override func mouseEntered(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_ENTER, 0)
+ }
+ common.updateCursorVisibility()
+ }
+
+ override func mouseExited(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0)
+ }
+ common.titleBar?.hide()
+ common.setCursorVisibility(true)
+ }
+
+ override func mouseMoved(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseMovement(event)
+ }
+ common.titleBar?.show()
+ }
+
+ override func mouseDragged(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseMovement(event)
+ }
+ }
+
+ override func mouseDown(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseDown(event)
+ }
+ }
+
+ override func mouseUp(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseUp(event)
+ }
+ common.window?.isMoving = false
+ }
+
+ override func rightMouseDown(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseDown(event)
+ }
+ }
+
+ override func rightMouseUp(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseUp(event)
+ }
+ }
+
+ override func otherMouseDown(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseDown(event)
+ }
+ }
+
+ override func otherMouseUp(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseUp(event)
+ }
+ }
+
+ override func magnify(with event: NSEvent) {
+ event.phase == .ended ?
+ common.windowDidEndLiveResize() : common.windowWillStartLiveResize()
+
+ common.window?.addWindowScale(Double(event.magnification))
+ }
+
+ func signalMouseDown(_ event: NSEvent) {
+ signalMouseEvent(event, MP_KEY_STATE_DOWN)
+ if event.clickCount > 1 {
+ signalMouseEvent(event, MP_KEY_STATE_UP)
+ }
+ }
+
+ func signalMouseUp(_ event: NSEvent) {
+ signalMouseEvent(event, MP_KEY_STATE_UP)
+ }
+
+ func signalMouseEvent(_ event: NSEvent, _ state: UInt32) {
+ hasMouseDown = state == MP_KEY_STATE_DOWN
+ let mpkey = getMpvButton(event)
+ cocoa_put_key_with_modifiers((mpkey | Int32(state)), Int32(event.modifierFlags.rawValue))
+ }
+
+ func signalMouseMovement(_ event: NSEvent) {
+ var point = convert(event.locationInWindow, from: nil)
+ point = convertToBacking(point)
+ point.y = -point.y
+
+ common.window?.updateMovableBackground(point)
+ if !(common.window?.isMoving ?? false) {
+ mpv?.setMousePosition(point)
+ }
+ }
+
+ func preciseScroll(_ event: NSEvent) {
+ var delta: Double
+ var cmd: Int32
+
+ if abs(event.deltaY) >= abs(event.deltaX) {
+ delta = Double(event.deltaY) * 0.1
+ cmd = delta > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
+ } else {
+ delta = Double(event.deltaX) * 0.1
+ cmd = delta > 0 ? SWIFT_WHEEL_LEFT : SWIFT_WHEEL_RIGHT
+ }
+
+ mpv?.putAxis(cmd, delta: abs(delta))
+ }
+
+ override func scrollWheel(with event: NSEvent) {
+ if !(mpv?.mouseEnabled() ?? true) {
+ return
+ }
+
+ if event.hasPreciseScrollingDeltas {
+ preciseScroll(event)
+ } else {
+ let modifiers = event.modifierFlags
+ let deltaX = modifiers.contains(.shift) ? event.scrollingDeltaY : event.scrollingDeltaX
+ let deltaY = modifiers.contains(.shift) ? event.scrollingDeltaX : event.scrollingDeltaY
+ var mpkey: Int32
+
+ if abs(deltaY) >= abs(deltaX) {
+ mpkey = deltaY > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
+ } else {
+ mpkey = deltaX > 0 ? SWIFT_WHEEL_LEFT : SWIFT_WHEEL_RIGHT
+ }
+
+ cocoa_put_key_with_modifiers(mpkey, Int32(modifiers.rawValue))
+ }
+ }
+
+ func containsMouseLocation() -> Bool {
+ var topMargin: CGFloat = 0.0
+ let menuBarHeight = NSApp.mainMenu?.menuBarHeight ?? 23.0
+
+ guard let window = common.window else { return false }
+ guard var vF = window.screen?.frame else { return false }
+
+ if window.isInFullscreen && (menuBarHeight > 0) {
+ topMargin = TitleBar.height + 1 + menuBarHeight
+ }
+
+ vF.size.height -= topMargin
+
+ let vFW = window.convertFromScreen(vF)
+ let vFV = convert(vFW, from: nil)
+ let pt = convert(window.mouseLocationOutsideOfEventStream, from: nil)
+
+ var clippedBounds = bounds.intersection(vFV)
+ if !window.isInFullscreen {
+ clippedBounds.origin.y += TitleBar.height
+ clippedBounds.size.height -= TitleBar.height
+ }
+ return clippedBounds.contains(pt)
+ }
+
+ func canHideCursor() -> Bool {
+ guard let window = common.window else { return false }
+ return !hasMouseDown && containsMouseLocation() && window.isKeyWindow
+ }
+
+ func getMpvButton(_ event: NSEvent) -> Int32 {
+ let buttonNumber = event.buttonNumber
+ switch (buttonNumber) {
+ case 0: return SWIFT_MBTN_LEFT
+ case 1: return SWIFT_MBTN_RIGHT
+ case 2: return SWIFT_MBTN_MID
+ case 3: return SWIFT_MBTN_BACK
+ case 4: return SWIFT_MBTN_FORWARD
+ default: return SWIFT_MBTN9 + Int32(buttonNumber - 5)
+ }
+ }
+}
diff --git a/video/out/mac/window.swift b/video/out/mac/window.swift
new file mode 100644
index 0000000..7b1a858
--- /dev/null
+++ b/video/out/mac/window.swift
@@ -0,0 +1,593 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class Window: NSWindow, NSWindowDelegate {
+ weak var common: Common! = nil
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ var targetScreen: NSScreen?
+ var previousScreen: NSScreen?
+ var currentScreen: NSScreen?
+ var unfScreen: NSScreen?
+
+ var unfsContentFrame: NSRect?
+ var isInFullscreen: Bool = false
+ var isMoving: Bool = false
+ var previousStyleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
+
+ var isAnimating: Bool = false
+ let animationLock: NSCondition = NSCondition()
+
+ var unfsContentFramePixel: NSRect { get { return convertToBacking(unfsContentFrame ?? NSRect(x: 0, y: 0, width: 160, height: 90)) } }
+ var framePixel: NSRect { get { return convertToBacking(frame) } }
+
+ var keepAspect: Bool = true {
+ didSet {
+ if let contentViewFrame = contentView?.frame, !isInFullscreen {
+ unfsContentFrame = convertToScreen(contentViewFrame)
+ }
+
+ if keepAspect {
+ contentAspectRatio = unfsContentFrame?.size ?? contentAspectRatio
+ } else {
+ resizeIncrements = NSSize(width: 1.0, height: 1.0)
+ }
+ }
+ }
+
+ var border: Bool = true {
+ didSet { if !border { common.titleBar?.hide() } }
+ }
+
+ override var canBecomeKey: Bool { return true }
+ override var canBecomeMain: Bool { return true }
+
+ override var styleMask: NSWindow.StyleMask {
+ get { return super.styleMask }
+ set {
+ let responder = firstResponder
+ let windowTitle = title
+ previousStyleMask = super.styleMask
+ super.styleMask = newValue
+ makeFirstResponder(responder)
+ title = windowTitle
+ }
+ }
+
+ convenience init(contentRect: NSRect, screen: NSScreen?, view: NSView, common com: Common) {
+ self.init(contentRect: contentRect,
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
+ backing: .buffered, defer: false, screen: screen)
+
+ // workaround for an AppKit bug where the NSWindow can't be placed on a
+ // none Main screen NSScreen outside the Main screen's frame bounds
+ if let wantedScreen = screen, screen != NSScreen.main {
+ var absoluteWantedOrigin = contentRect.origin
+ absoluteWantedOrigin.x += wantedScreen.frame.origin.x
+ absoluteWantedOrigin.y += wantedScreen.frame.origin.y
+
+ if !NSEqualPoints(absoluteWantedOrigin, self.frame.origin) {
+ self.setFrameOrigin(absoluteWantedOrigin)
+ }
+ }
+
+ common = com
+ title = com.title
+ minSize = NSMakeSize(160, 90)
+ collectionBehavior = .fullScreenPrimary
+ delegate = self
+
+ if let cView = contentView {
+ cView.addSubview(view)
+ view.frame = cView.frame
+ unfsContentFrame = convertToScreen(cView.frame)
+ }
+
+ targetScreen = screen
+ currentScreen = screen
+ unfScreen = screen
+
+ if let app = NSApp as? Application {
+ app.menuBar.register(#selector(setHalfWindowSize), for: MPM_H_SIZE)
+ app.menuBar.register(#selector(setNormalWindowSize), for: MPM_N_SIZE)
+ app.menuBar.register(#selector(setDoubleWindowSize), for: MPM_D_SIZE)
+ app.menuBar.register(#selector(performMiniaturize(_:)), for: MPM_MINIMIZE)
+ app.menuBar.register(#selector(performZoom(_:)), for: MPM_ZOOM)
+ }
+ }
+
+ override func toggleFullScreen(_ sender: Any?) {
+ if isAnimating {
+ return
+ }
+
+ animationLock.lock()
+ isAnimating = true
+ animationLock.unlock()
+
+ targetScreen = common.getTargetScreen(forFullscreen: !isInFullscreen)
+ if targetScreen == nil && previousScreen == nil {
+ targetScreen = screen
+ } else if targetScreen == nil {
+ targetScreen = previousScreen
+ previousScreen = nil
+ } else {
+ previousScreen = screen
+ }
+
+ if let contentViewFrame = contentView?.frame, !isInFullscreen {
+ unfsContentFrame = convertToScreen(contentViewFrame)
+ unfScreen = screen
+ }
+ // move window to target screen when going to fullscreen
+ if let tScreen = targetScreen, !isInFullscreen && (tScreen != screen) {
+ let frame = calculateWindowPosition(for: tScreen, withoutBounds: false)
+ setFrame(frame, display: true)
+ }
+
+ if Bool(mpv?.opts.native_fs ?? true) {
+ super.toggleFullScreen(sender)
+ } else {
+ if !isInFullscreen {
+ setToFullScreen()
+ }
+ else {
+ setToWindow()
+ }
+ }
+ }
+
+ func customWindowsToEnterFullScreen(for window: NSWindow) -> [NSWindow]? {
+ return [window]
+ }
+
+ func customWindowsToExitFullScreen(for window: NSWindow) -> [NSWindow]? {
+ return [window]
+ }
+
+ func window(_ window: NSWindow, startCustomAnimationToEnterFullScreenWithDuration duration: TimeInterval) {
+ guard let tScreen = targetScreen else { return }
+ common.view?.layerContentsPlacement = .scaleProportionallyToFit
+ common.titleBar?.hide()
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = getFsAnimationDuration(duration - 0.05)
+ window.animator().setFrame(tScreen.frame, display: true)
+ }, completionHandler: nil)
+ }
+
+ func window(_ window: NSWindow, startCustomAnimationToExitFullScreenWithDuration duration: TimeInterval) {
+ guard let tScreen = targetScreen, let currentScreen = screen else { return }
+ let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: tScreen == screen)
+ let intermediateFrame = aspectFit(rect: newFrame, in: currentScreen.frame)
+ common.titleBar?.hide(0.0)
+
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = 0.0
+ common.view?.layerContentsPlacement = .scaleProportionallyToFill
+ window.animator().setFrame(intermediateFrame, display: true)
+ }, completionHandler: {
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = self.getFsAnimationDuration(duration - 0.05)
+ self.styleMask.remove(.fullScreen)
+ window.animator().setFrame(newFrame, display: true)
+ }, completionHandler: nil)
+ })
+ }
+
+ func windowDidEnterFullScreen(_ notification: Notification) {
+ isInFullscreen = true
+ mpv?.setOption(fullscreen: isInFullscreen)
+ common.updateCursorVisibility()
+ endAnimation(frame)
+ common.titleBar?.show()
+ }
+
+ func windowDidExitFullScreen(_ notification: Notification) {
+ guard let tScreen = targetScreen else { return }
+ isInFullscreen = false
+ mpv?.setOption(fullscreen: isInFullscreen)
+ endAnimation(calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen))
+ common.view?.layerContentsPlacement = .scaleProportionallyToFit
+ }
+
+ func windowDidFailToEnterFullScreen(_ window: NSWindow) {
+ guard let tScreen = targetScreen else { return }
+ let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
+ setFrame(newFrame, display: true)
+ endAnimation()
+ }
+
+ func windowDidFailToExitFullScreen(_ window: NSWindow) {
+ guard let targetFrame = targetScreen?.frame else { return }
+ setFrame(targetFrame, display: true)
+ endAnimation()
+ common.view?.layerContentsPlacement = .scaleProportionallyToFit
+ }
+
+ func endAnimation(_ newFrame: NSRect = NSZeroRect) {
+ if !NSEqualRects(newFrame, NSZeroRect) && isAnimating {
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = 0.01
+ self.animator().setFrame(newFrame, display: true)
+ }, completionHandler: nil )
+ }
+
+ animationLock.lock()
+ isAnimating = false
+ animationLock.signal()
+ animationLock.unlock()
+ common.windowDidEndAnimation()
+ }
+
+ func setToFullScreen() {
+ guard let targetFrame = targetScreen?.frame else { return }
+
+ if #available(macOS 11.0, *) {
+ styleMask = .borderless
+ common.titleBar?.hide(0.0)
+ } else {
+ styleMask.insert(.fullScreen)
+ }
+
+ NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock]
+ setFrame(targetFrame, display: true)
+ endAnimation()
+ isInFullscreen = true
+ mpv?.setOption(fullscreen: isInFullscreen)
+ common.windowSetToFullScreen()
+ }
+
+ func setToWindow() {
+ guard let tScreen = targetScreen else { return }
+
+ if #available(macOS 11.0, *) {
+ styleMask = previousStyleMask
+ common.titleBar?.hide(0.0)
+ } else {
+ styleMask.remove(.fullScreen)
+ }
+
+ let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
+ NSApp.presentationOptions = []
+ setFrame(newFrame, display: true)
+ endAnimation()
+ isInFullscreen = false
+ mpv?.setOption(fullscreen: isInFullscreen)
+ common.windowSetToWindow()
+ }
+
+ func waitForAnimation() {
+ animationLock.lock()
+ while(isAnimating){
+ animationLock.wait()
+ }
+ animationLock.unlock()
+ }
+
+ func getFsAnimationDuration(_ def: Double) -> Double {
+ let duration = mpv?.macOpts.macos_fs_animation_duration ?? -1
+ if duration < 0 {
+ return def
+ } else {
+ return Double(duration)/1000
+ }
+ }
+
+ func setOnTop(_ state: Bool, _ ontopLevel: Int) {
+ if state {
+ switch ontopLevel {
+ case -1:
+ level = .floating
+ case -2:
+ level = .statusBar + 1
+ case -3:
+ level = NSWindow.Level(Int(CGWindowLevelForKey(.desktopWindow)))
+ default:
+ level = NSWindow.Level(ontopLevel)
+ }
+ collectionBehavior.remove(.transient)
+ collectionBehavior.insert(.managed)
+ } else {
+ level = .normal
+ }
+ }
+
+ func setOnAllWorkspaces(_ state: Bool) {
+ if state {
+ collectionBehavior.insert(.canJoinAllSpaces)
+ } else {
+ collectionBehavior.remove(.canJoinAllSpaces)
+ }
+ }
+
+ func setMinimized(_ stateWanted: Bool) {
+ if isMiniaturized == stateWanted { return }
+
+ if stateWanted {
+ performMiniaturize(self)
+ } else {
+ deminiaturize(self)
+ }
+ }
+
+ func setMaximized(_ stateWanted: Bool) {
+ if isZoomed == stateWanted { return }
+
+ zoom(self)
+ }
+
+ func updateMovableBackground(_ pos: NSPoint) {
+ if !isInFullscreen {
+ isMovableByWindowBackground = mpv?.canBeDraggedAt(pos) ?? true
+ } else {
+ isMovableByWindowBackground = false
+ }
+ }
+
+ func updateFrame(_ rect: NSRect) {
+ if rect != frame {
+ let cRect = frameRect(forContentRect: rect)
+ unfsContentFrame = rect
+ setFrame(cRect, display: true)
+ common.windowDidUpdateFrame()
+ }
+ }
+
+ func updateSize(_ size: NSSize) {
+ if let currentSize = contentView?.frame.size, size != currentSize {
+ let newContentFrame = centeredContentSize(for: frame, size: size)
+ if !isInFullscreen {
+ updateFrame(newContentFrame)
+ } else {
+ unfsContentFrame = newContentFrame
+ }
+ }
+ }
+
+ override func setFrame(_ frameRect: NSRect, display flag: Bool) {
+ if frameRect.width < minSize.width || frameRect.height < minSize.height {
+ common.log.sendVerbose("tried to set too small window size: \(frameRect.size)")
+ return
+ }
+
+ super.setFrame(frameRect, display: flag)
+
+ if let size = unfsContentFrame?.size, keepAspect {
+ contentAspectRatio = size
+ }
+ }
+
+ func centeredContentSize(for rect: NSRect, size sz: NSSize) -> NSRect {
+ let cRect = contentRect(forFrameRect: rect)
+ let dx = (cRect.size.width - sz.width) / 2
+ let dy = (cRect.size.height - sz.height) / 2
+ return NSInsetRect(cRect, dx, dy)
+ }
+
+ func aspectFit(rect r: NSRect, in rTarget: NSRect) -> NSRect {
+ var s = rTarget.width / r.width
+ if r.height*s > rTarget.height {
+ s = rTarget.height / r.height
+ }
+ let w = r.width * s
+ let h = r.height * s
+ return NSRect(x: rTarget.midX - w/2, y: rTarget.midY - h/2, width: w, height: h)
+ }
+
+ func calculateWindowPosition(for tScreen: NSScreen, withoutBounds: Bool) -> NSRect {
+ guard let contentFrame = unfsContentFrame, let screen = unfScreen else {
+ return frame
+ }
+ var newFrame = frameRect(forContentRect: contentFrame)
+ let targetFrame = tScreen.frame
+ let targetVisibleFrame = tScreen.visibleFrame
+ let unfsScreenFrame = screen.frame
+ let visibleWindow = NSIntersectionRect(unfsScreenFrame, newFrame)
+
+ // calculate visible area of every side
+ let left = newFrame.origin.x - unfsScreenFrame.origin.x
+ let right = unfsScreenFrame.size.width -
+ (newFrame.origin.x - unfsScreenFrame.origin.x + newFrame.size.width)
+ let bottom = newFrame.origin.y - unfsScreenFrame.origin.y
+ let top = unfsScreenFrame.size.height -
+ (newFrame.origin.y - unfsScreenFrame.origin.y + newFrame.size.height)
+
+ // normalize visible areas, decide which one to take horizontal/vertical
+ var xPer = (unfsScreenFrame.size.width - visibleWindow.size.width)
+ var yPer = (unfsScreenFrame.size.height - visibleWindow.size.height)
+ if xPer != 0 { xPer = (left >= 0 || right < 0 ? left : right) / xPer }
+ if yPer != 0 { yPer = (bottom >= 0 || top < 0 ? bottom : top) / yPer }
+
+ // calculate visible area for every side for target screen
+ let xNewLeft = targetFrame.origin.x +
+ (targetFrame.size.width - visibleWindow.size.width) * xPer
+ let xNewRight = targetFrame.origin.x + targetFrame.size.width -
+ (targetFrame.size.width - visibleWindow.size.width) * xPer - newFrame.size.width
+ let yNewBottom = targetFrame.origin.y +
+ (targetFrame.size.height - visibleWindow.size.height) * yPer
+ let yNewTop = targetFrame.origin.y + targetFrame.size.height -
+ (targetFrame.size.height - visibleWindow.size.height) * yPer - newFrame.size.height
+
+ // calculate new coordinates, decide which one to take horizontal/vertical
+ newFrame.origin.x = left >= 0 || right < 0 ? xNewLeft : xNewRight
+ newFrame.origin.y = bottom >= 0 || top < 0 ? yNewBottom : yNewTop
+
+ // don't place new window on top of a visible menubar
+ let topMar = targetFrame.size.height -
+ (newFrame.origin.y - targetFrame.origin.y + newFrame.size.height)
+ let menuBarHeight = targetFrame.size.height -
+ (targetVisibleFrame.size.height + targetVisibleFrame.origin.y)
+ if topMar < menuBarHeight {
+ newFrame.origin.y -= top - menuBarHeight
+ }
+
+ if withoutBounds {
+ return newFrame
+ }
+
+ // screen bounds right and left
+ if newFrame.origin.x + newFrame.size.width > targetFrame.origin.x + targetFrame.size.width {
+ newFrame.origin.x = targetFrame.origin.x + targetFrame.size.width - newFrame.size.width
+ }
+ if newFrame.origin.x < targetFrame.origin.x {
+ newFrame.origin.x = targetFrame.origin.x
+ }
+
+ // screen bounds top and bottom
+ if newFrame.origin.y + newFrame.size.height > targetFrame.origin.y + targetFrame.size.height {
+ newFrame.origin.y = targetFrame.origin.y + targetFrame.size.height - newFrame.size.height
+ }
+ if newFrame.origin.y < targetFrame.origin.y {
+ newFrame.origin.y = targetFrame.origin.y
+ }
+ return newFrame
+ }
+
+ override func constrainFrameRect(_ frameRect: NSRect, to tScreen: NSScreen?) -> NSRect {
+ if (isAnimating && !isInFullscreen) || (!isAnimating && isInFullscreen ||
+ level == NSWindow.Level(Int(CGWindowLevelForKey(.desktopWindow))))
+ {
+ return frameRect
+ }
+
+ guard let ts: NSScreen = tScreen ?? screen ?? NSScreen.main else {
+ return frameRect
+ }
+ var nf: NSRect = frameRect
+ let of: NSRect = frame
+ let vf: NSRect = (isAnimating ? (targetScreen ?? ts) : ts).visibleFrame
+ let ncf: NSRect = contentRect(forFrameRect: nf)
+
+ // screen bounds top and bottom
+ if NSMaxY(nf) > NSMaxY(vf) {
+ nf.origin.y = NSMaxY(vf) - NSHeight(nf)
+ }
+ if NSMaxY(ncf) < NSMinY(vf) {
+ nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf)
+ }
+
+ // screen bounds right and left
+ if NSMinX(nf) > NSMaxX(vf) {
+ nf.origin.x = NSMaxX(vf) - NSWidth(nf)
+ }
+ if NSMaxX(nf) < NSMinX(vf) {
+ nf.origin.x = NSMinX(vf)
+ }
+
+ if NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && !isInFullscreen {
+ // If the window height is smaller than the visible frame, but it was
+ // bigger previously recenter the smaller window vertically. This is
+ // needed to counter the 'snap to top' behaviour.
+ nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2
+ }
+ return nf
+ }
+
+ @objc func setNormalWindowSize() { setWindowScale(1.0) }
+ @objc func setHalfWindowSize() { setWindowScale(0.5) }
+ @objc func setDoubleWindowSize() { setWindowScale(2.0) }
+
+ func setWindowScale(_ scale: Double) {
+ mpv?.command("set window-scale \(scale)")
+ }
+
+ func addWindowScale(_ scale: Double) {
+ if !isInFullscreen {
+ mpv?.command("add window-scale \(scale)")
+ }
+ }
+
+ func windowDidChangeScreen(_ notification: Notification) {
+ if screen == nil {
+ return
+ }
+ if !isAnimating && (currentScreen != screen) {
+ previousScreen = screen
+ }
+ if currentScreen != screen {
+ common.updateDisplaylink()
+ common.windowDidChangeScreen()
+ }
+ currentScreen = screen
+ }
+
+ func windowDidChangeScreenProfile(_ notification: Notification) {
+ common.windowDidChangeScreenProfile()
+ }
+
+ func windowDidChangeBackingProperties(_ notification: Notification) {
+ common.windowDidChangeBackingProperties()
+ common.flagEvents(VO_EVENT_DPI)
+ }
+
+ func windowWillStartLiveResize(_ notification: Notification) {
+ common.windowWillStartLiveResize()
+ }
+
+ func windowDidEndLiveResize(_ notification: Notification) {
+ common.windowDidEndLiveResize()
+ mpv?.setOption(maximized: isZoomed)
+
+ if let contentViewFrame = contentView?.frame,
+ !isAnimating && !isInFullscreen
+ {
+ unfsContentFrame = convertToScreen(contentViewFrame)
+ }
+ }
+
+ func windowDidResize(_ notification: Notification) {
+ common.windowDidResize()
+ }
+
+ func windowShouldClose(_ sender: NSWindow) -> Bool {
+ cocoa_put_key(MP_KEY_CLOSE_WIN)
+ return false
+ }
+
+ func windowDidMiniaturize(_ notification: Notification) {
+ mpv?.setOption(minimized: true)
+ }
+
+ func windowDidDeminiaturize(_ notification: Notification) {
+ mpv?.setOption(minimized: false)
+ }
+
+ func windowDidResignKey(_ notification: Notification) {
+ common.setCursorVisibility(true)
+ }
+
+ func windowDidBecomeKey(_ notification: Notification) {
+ common.updateCursorVisibility()
+ }
+
+ func windowDidChangeOcclusionState(_ notification: Notification) {
+ if occlusionState.contains(.visible) {
+ common.windowDidChangeOcclusionState()
+ common.updateCursorVisibility()
+ }
+ }
+
+ func windowWillMove(_ notification: Notification) {
+ isMoving = true
+ }
+
+ func windowDidMove(_ notification: Notification) {
+ mpv?.setOption(maximized: isZoomed)
+ }
+}
diff --git a/video/out/mac_common.swift b/video/out/mac_common.swift
new file mode 100644
index 0000000..349712b
--- /dev/null
+++ b/video/out/mac_common.swift
@@ -0,0 +1,174 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Cocoa
+
+class MacCommon: Common {
+ @objc var layer: MetalLayer?
+
+ var timer: PreciseTimer?
+ var swapTime: UInt64 = 0
+ let swapLock: NSCondition = NSCondition()
+
+ var needsICCUpdate: Bool = false
+
+ @objc init(_ vo: UnsafeMutablePointer<vo>) {
+ let newlog = mp_log_new(vo, vo.pointee.log, "mac")
+ super.init(newlog)
+ mpv = MPVHelper(vo, log)
+ timer = PreciseTimer(common: self)
+
+ DispatchQueue.main.sync {
+ layer = MetalLayer(common: self)
+ initMisc(vo)
+ }
+ }
+
+ @objc func config(_ vo: UnsafeMutablePointer<vo>) -> Bool {
+ mpv?.vo = vo
+
+ DispatchQueue.main.sync {
+ let previousActiveApp = getActiveApp()
+ initApp()
+
+ let (_, _, wr) = getInitProperties(vo)
+
+ guard let layer = self.layer else {
+ log.sendError("Something went wrong, no MetalLayer was initialized")
+ exit(1)
+ }
+
+ if window == nil {
+ initView(vo, layer)
+ initWindow(vo, previousActiveApp)
+ initWindowState()
+ }
+
+ if !NSEqualSizes(window?.unfsContentFramePixel.size ?? NSZeroSize, wr.size) {
+ window?.updateSize(wr.size)
+ }
+
+ windowDidResize()
+ needsICCUpdate = true
+ }
+
+ return true
+ }
+
+ @objc func uninit(_ vo: UnsafeMutablePointer<vo>) {
+ window?.waitForAnimation()
+
+ timer?.terminate()
+
+ DispatchQueue.main.sync {
+ window?.delegate = nil
+ window?.close()
+
+ uninitCommon()
+ }
+ }
+
+ @objc func swapBuffer() {
+ if mpv?.macOpts.macos_render_timer ?? Int32(RENDER_TIMER_CALLBACK) != RENDER_TIMER_SYSTEM {
+ swapLock.lock()
+ while(swapTime < 1) {
+ swapLock.wait()
+ }
+ swapTime = 0
+ swapLock.unlock()
+ }
+
+ if needsICCUpdate {
+ needsICCUpdate = false
+ updateICCProfile()
+ }
+ }
+
+ func updateRenderSize(_ size: NSSize) {
+ mpv?.vo.pointee.dwidth = Int32(size.width)
+ mpv?.vo.pointee.dheight = Int32(size.height)
+ flagEvents(VO_EVENT_RESIZE | VO_EVENT_EXPOSE)
+ }
+
+ override func displayLinkCallback(_ displayLink: CVDisplayLink,
+ _ inNow: UnsafePointer<CVTimeStamp>,
+ _ inOutputTime: UnsafePointer<CVTimeStamp>,
+ _ flagsIn: CVOptionFlags,
+ _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
+ {
+ let frameTimer = mpv?.macOpts.macos_render_timer ?? Int32(RENDER_TIMER_CALLBACK)
+ let signalSwap = {
+ self.swapLock.lock()
+ self.swapTime += 1
+ self.swapLock.signal()
+ self.swapLock.unlock()
+ }
+
+ if frameTimer != RENDER_TIMER_SYSTEM {
+ if let timer = self.timer, frameTimer == RENDER_TIMER_PRECISE {
+ timer.scheduleAt(time: inOutputTime.pointee.hostTime, closure: signalSwap)
+ return kCVReturnSuccess
+ }
+
+ signalSwap()
+ }
+
+ return kCVReturnSuccess
+ }
+
+ override func startDisplayLink(_ vo: UnsafeMutablePointer<vo>) {
+ super.startDisplayLink(vo)
+ timer?.updatePolicy(periodSeconds: 1 / currentFps())
+ }
+
+ override func updateDisplaylink() {
+ super.updateDisplaylink()
+ timer?.updatePolicy(periodSeconds: 1 / currentFps())
+ }
+
+ override func lightSensorUpdate() {
+ flagEvents(VO_EVENT_AMBIENT_LIGHTING_CHANGED)
+ }
+
+ @objc override func updateICCProfile() {
+ guard let colorSpace = window?.screen?.colorSpace else {
+ log.sendWarning("Couldn't update ICC Profile, no color space available")
+ return
+ }
+
+ layer?.colorspace = colorSpace.cgColorSpace
+ flagEvents(VO_EVENT_ICC_PROFILE_CHANGED)
+ }
+
+ override func windowDidResize() {
+ guard let window = window else {
+ log.sendWarning("No window available on window resize event")
+ return
+ }
+
+ updateRenderSize(window.framePixel.size)
+ }
+
+ override func windowDidChangeScreenProfile() {
+ needsICCUpdate = true
+ }
+
+ override func windowDidChangeBackingProperties() {
+ layer?.contentsScale = window?.backingScaleFactor ?? 1
+ windowDidResize()
+ }
+}
diff --git a/video/out/meson.build b/video/out/meson.build
new file mode 100644
index 0000000..e2808d6
--- /dev/null
+++ b/video/out/meson.build
@@ -0,0 +1,51 @@
+wl_protocol_dir = wayland['deps'][2].get_variable(pkgconfig: 'pkgdatadir', internal: 'pkgdatadir')
+protocols = [[wl_protocol_dir, 'stable/presentation-time/presentation-time.xml'],
+ [wl_protocol_dir, 'stable/viewporter/viewporter.xml'],
+ [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'],
+ [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'],
+ [wl_protocol_dir, 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml'],
+ [wl_protocol_dir, 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml']]
+wl_protocols_source = []
+wl_protocols_headers = []
+
+foreach v: ['1.27', '1.31', '1.32']
+ features += {'wayland-protocols-' + v.replace('.', '-'):
+ wayland['deps'][2].version().version_compare('>=' + v)}
+endforeach
+
+if features['wayland-protocols-1-27']
+ protocols += [[wl_protocol_dir, 'staging/content-type/content-type-v1.xml'],
+ [wl_protocol_dir, 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml']]
+endif
+if features['wayland-protocols-1-31']
+ protocols += [[wl_protocol_dir, 'staging/fractional-scale/fractional-scale-v1.xml']]
+endif
+if features['wayland-protocols-1-32']
+ protocols += [[wl_protocol_dir, 'staging/cursor-shape/cursor-shape-v1.xml'],
+ [wl_protocol_dir, 'unstable/tablet/tablet-unstable-v2.xml']] # required by cursor-shape
+endif
+
+foreach p: protocols
+ xml = join_paths(p)
+ wl_protocols_source += custom_target(xml.underscorify() + '_c',
+ input: xml,
+ output: '@BASENAME@.c',
+ command: [wayland['scanner'], 'private-code', '@INPUT@', '@OUTPUT@'],
+ )
+ wl_protocols_headers += custom_target(xml.underscorify() + '_h',
+ input: xml,
+ output: '@BASENAME@.h',
+ command: [wayland['scanner'], 'client-header', '@INPUT@', '@OUTPUT@'],
+ )
+endforeach
+
+lib_client_protocols = static_library('protocols',
+ wl_protocols_source + wl_protocols_headers,
+ dependencies: wayland['deps'][0])
+
+client_protocols = declare_dependency(link_with: lib_client_protocols,
+ sources: wl_protocols_headers)
+
+dependencies += [client_protocols, wayland['deps']]
+
+sources += files('wayland_common.c')
diff --git a/video/out/opengl/angle_dynamic.c b/video/out/opengl/angle_dynamic.c
new file mode 100644
index 0000000..2483828
--- /dev/null
+++ b/video/out/opengl/angle_dynamic.c
@@ -0,0 +1,39 @@
+#include <windows.h>
+
+#include "angle_dynamic.h"
+
+#include "common/common.h"
+#include "osdep/threads.h"
+
+#if HAVE_EGL_ANGLE_LIB
+bool angle_load(void)
+{
+ return true;
+}
+#else
+#define ANGLE_DECL(NAME, VAR) \
+ VAR;
+ANGLE_FNS(ANGLE_DECL)
+
+static bool angle_loaded;
+static mp_once angle_load_once = MP_STATIC_ONCE_INITIALIZER;
+
+static void angle_do_load(void)
+{
+ // Note: we let this handle "leak", as the functions remain valid forever.
+ HANDLE angle_dll = LoadLibraryW(L"LIBEGL.DLL");
+ if (!angle_dll)
+ return;
+#define ANGLE_LOAD_ENTRY(NAME, VAR) \
+ NAME = (void *)GetProcAddress(angle_dll, #NAME); \
+ if (!NAME) return;
+ ANGLE_FNS(ANGLE_LOAD_ENTRY)
+ angle_loaded = true;
+}
+
+bool angle_load(void)
+{
+ mp_exec_once(&angle_load_once, angle_do_load);
+ return angle_loaded;
+}
+#endif
diff --git a/video/out/opengl/angle_dynamic.h b/video/out/opengl/angle_dynamic.h
new file mode 100644
index 0000000..d419c3f
--- /dev/null
+++ b/video/out/opengl/angle_dynamic.h
@@ -0,0 +1,89 @@
+// Based on Khronos headers, thus MIT licensed.
+
+#ifndef MP_ANGLE_DYNAMIC_H
+#define MP_ANGLE_DYNAMIC_H
+
+#include <stdbool.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "config.h"
+
+#define ANGLE_FNS(FN) \
+ FN(eglBindAPI, EGLBoolean (*EGLAPIENTRY PFN_eglBindAPI)(EGLenum)) \
+ FN(eglBindTexImage, EGLBoolean (*EGLAPIENTRY PFN_eglBindTexImage) \
+ (EGLDisplay, EGLSurface, EGLint)) \
+ FN(eglChooseConfig, EGLBoolean (*EGLAPIENTRY PFN_eglChooseConfig) \
+ (EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *)) \
+ FN(eglCreateContext, EGLContext (*EGLAPIENTRY PFN_eglCreateContext) \
+ (EGLDisplay, EGLConfig, EGLContext, const EGLint *)) \
+ FN(eglCreatePbufferFromClientBuffer, EGLSurface (*EGLAPIENTRY \
+ PFN_eglCreatePbufferFromClientBuffer)(EGLDisplay, EGLenum, \
+ EGLClientBuffer, EGLConfig, const EGLint *)) \
+ FN(eglCreateWindowSurface, EGLSurface (*EGLAPIENTRY \
+ PFN_eglCreateWindowSurface)(EGLDisplay, EGLConfig, \
+ EGLNativeWindowType, const EGLint *)) \
+ FN(eglDestroyContext, EGLBoolean (*EGLAPIENTRY PFN_eglDestroyContext) \
+ (EGLDisplay, EGLContext)) \
+ FN(eglDestroySurface, EGLBoolean (*EGLAPIENTRY PFN_eglDestroySurface) \
+ (EGLDisplay, EGLSurface)) \
+ FN(eglGetConfigAttrib, EGLBoolean (*EGLAPIENTRY PFN_eglGetConfigAttrib) \
+ (EGLDisplay, EGLConfig, EGLint, EGLint *)) \
+ FN(eglGetCurrentContext, EGLContext (*EGLAPIENTRY \
+ PFN_eglGetCurrentContext)(void)) \
+ FN(eglGetCurrentDisplay, EGLDisplay (*EGLAPIENTRY \
+ PFN_eglGetCurrentDisplay)(void)) \
+ FN(eglGetDisplay, EGLDisplay (*EGLAPIENTRY PFN_eglGetDisplay) \
+ (EGLNativeDisplayType)) \
+ FN(eglGetError, EGLint (*EGLAPIENTRY PFN_eglGetError)(void)) \
+ FN(eglGetProcAddress, void *(*EGLAPIENTRY \
+ PFN_eglGetProcAddress)(const char *)) \
+ FN(eglInitialize, EGLBoolean (*EGLAPIENTRY PFN_eglInitialize) \
+ (EGLDisplay, EGLint *, EGLint *)) \
+ FN(eglMakeCurrent, EGLBoolean (*EGLAPIENTRY PFN_eglMakeCurrent) \
+ (EGLDisplay, EGLSurface, EGLSurface, EGLContext)) \
+ FN(eglQueryString, const char *(*EGLAPIENTRY PFN_eglQueryString) \
+ (EGLDisplay, EGLint)) \
+ FN(eglSwapBuffers, EGLBoolean (*EGLAPIENTRY PFN_eglSwapBuffers) \
+ (EGLDisplay, EGLSurface)) \
+ FN(eglSwapInterval, EGLBoolean (*EGLAPIENTRY PFN_eglSwapInterval) \
+ (EGLDisplay, EGLint)) \
+ FN(eglReleaseTexImage, EGLBoolean (*EGLAPIENTRY PFN_eglReleaseTexImage) \
+ (EGLDisplay, EGLSurface, EGLint)) \
+ FN(eglTerminate, EGLBoolean (*EGLAPIENTRY PFN_eglTerminate)(EGLDisplay)) \
+ FN(eglWaitClient, EGLBoolean (*EGLAPIENTRY PFN_eglWaitClient)(void))
+
+#define ANGLE_EXT_DECL(NAME, VAR) \
+ extern VAR;
+ANGLE_FNS(ANGLE_EXT_DECL)
+
+bool angle_load(void);
+
+// Source compatibility to statically linked ANGLE.
+#if !HAVE_EGL_ANGLE_LIB
+#define eglBindAPI PFN_eglBindAPI
+#define eglBindTexImage PFN_eglBindTexImage
+#define eglChooseConfig PFN_eglChooseConfig
+#define eglCreateContext PFN_eglCreateContext
+#define eglCreatePbufferFromClientBuffer PFN_eglCreatePbufferFromClientBuffer
+#define eglCreateWindowSurface PFN_eglCreateWindowSurface
+#define eglDestroyContext PFN_eglDestroyContext
+#define eglDestroySurface PFN_eglDestroySurface
+#define eglGetConfigAttrib PFN_eglGetConfigAttrib
+#define eglGetCurrentContext PFN_eglGetCurrentContext
+#define eglGetCurrentDisplay PFN_eglGetCurrentDisplay
+#define eglGetDisplay PFN_eglGetDisplay
+#define eglGetError PFN_eglGetError
+#define eglGetProcAddress PFN_eglGetProcAddress
+#define eglInitialize PFN_eglInitialize
+#define eglMakeCurrent PFN_eglMakeCurrent
+#define eglQueryString PFN_eglQueryString
+#define eglReleaseTexImage PFN_eglReleaseTexImage
+#define eglSwapBuffers PFN_eglSwapBuffers
+#define eglSwapInterval PFN_eglSwapInterval
+#define eglTerminate PFN_eglTerminate
+#define eglWaitClient PFN_eglWaitClient
+#endif
+
+#endif
diff --git a/video/out/opengl/common.c b/video/out/opengl/common.c
new file mode 100644
index 0000000..ee26508
--- /dev/null
+++ b/video/out/opengl/common.c
@@ -0,0 +1,694 @@
+/*
+ * common OpenGL routines
+ *
+ * copyleft (C) 2005-2010 Reimar Döffinger <Reimar.Doeffinger@gmx.de>
+ * Special thanks go to the xine team and Matthias Hopf, whose video_out_opengl.c
+ * gave me lots of good ideas.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+
+#include "common.h"
+#include "common/common.h"
+#include "utils.h"
+
+// This guesses if the current GL context is a suspected software renderer.
+static bool is_software_gl(GL *gl)
+{
+ const char *renderer = gl->GetString(GL_RENDERER);
+ // Note we don't attempt to blacklist Microsoft's fallback implementation.
+ // It only provides OpenGL 1.1 and will be skipped anyway.
+ return !renderer ||
+ strcmp(renderer, "Software Rasterizer") == 0 ||
+ strstr(renderer, "llvmpipe") ||
+ strstr(renderer, "softpipe") ||
+ strcmp(renderer, "Mesa X11") == 0 ||
+ strcmp(renderer, "Apple Software Renderer") == 0;
+}
+
+// This guesses whether our DR path is fast or slow
+static bool is_fast_dr(GL *gl)
+{
+ const char *vendor = gl->GetString(GL_VENDOR);
+ if (!vendor)
+ return false;
+
+ return strcasecmp(vendor, "AMD") == 0 ||
+ strcasecmp(vendor, "NVIDIA Corporation") == 0 ||
+ strcasecmp(vendor, "ATI Technologies Inc.") == 0; // AMD on Windows
+}
+
+static void GLAPIENTRY dummy_glBindFramebuffer(GLenum target, GLuint framebuffer)
+{
+ assert(framebuffer == 0);
+}
+
+#define FN_OFFS(name) offsetof(GL, name)
+
+#define DEF_FN(name) {FN_OFFS(name), "gl" # name}
+#define DEF_FN_NAME(name, str) {FN_OFFS(name), str}
+
+struct gl_function {
+ ptrdiff_t offset;
+ char *name;
+};
+
+struct gl_functions {
+ const char *extension; // introduced with this extension in any version
+ int provides; // bitfield of MPGL_CAP_* constants
+ int ver_core; // introduced as required function
+ int ver_es_core; // introduced as required GL ES function
+ int ver_exclude; // not applicable to versions >= ver_exclude
+ int ver_es_exclude; // same for GLES
+ const struct gl_function *functions;
+};
+
+#define MAX_FN_COUNT 100 // max functions per gl_functions section
+
+// Note: to keep the number of sections low, some functions are in multiple
+// sections (if there are tricky combinations of GL/ES versions)
+static const struct gl_functions gl_functions[] = {
+ // GL 2.1+ desktop and GLES 2.0+ (anything we support)
+ // Probably all of these are in GL 2.0 too, but we require GLSL 120.
+ {
+ .ver_core = 210,
+ .ver_es_core = 200,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(ActiveTexture),
+ DEF_FN(AttachShader),
+ DEF_FN(BindAttribLocation),
+ DEF_FN(BindBuffer),
+ DEF_FN(BindTexture),
+ DEF_FN(BlendFuncSeparate),
+ DEF_FN(BufferData),
+ DEF_FN(BufferSubData),
+ DEF_FN(Clear),
+ DEF_FN(ClearColor),
+ DEF_FN(CompileShader),
+ DEF_FN(CreateProgram),
+ DEF_FN(CreateShader),
+ DEF_FN(DeleteBuffers),
+ DEF_FN(DeleteProgram),
+ DEF_FN(DeleteShader),
+ DEF_FN(DeleteTextures),
+ DEF_FN(Disable),
+ DEF_FN(DisableVertexAttribArray),
+ DEF_FN(DrawArrays),
+ DEF_FN(Enable),
+ DEF_FN(EnableVertexAttribArray),
+ DEF_FN(Finish),
+ DEF_FN(Flush),
+ DEF_FN(GenBuffers),
+ DEF_FN(GenTextures),
+ DEF_FN(GetAttribLocation),
+ DEF_FN(GetError),
+ DEF_FN(GetIntegerv),
+ DEF_FN(GetProgramInfoLog),
+ DEF_FN(GetProgramiv),
+ DEF_FN(GetShaderInfoLog),
+ DEF_FN(GetShaderiv),
+ DEF_FN(GetString),
+ DEF_FN(GetUniformLocation),
+ DEF_FN(LinkProgram),
+ DEF_FN(PixelStorei),
+ DEF_FN(ReadPixels),
+ DEF_FN(Scissor),
+ DEF_FN(ShaderSource),
+ DEF_FN(TexImage2D),
+ DEF_FN(TexParameteri),
+ DEF_FN(TexSubImage2D),
+ DEF_FN(Uniform1f),
+ DEF_FN(Uniform2f),
+ DEF_FN(Uniform3f),
+ DEF_FN(Uniform1i),
+ DEF_FN(UniformMatrix2fv),
+ DEF_FN(UniformMatrix3fv),
+ DEF_FN(UseProgram),
+ DEF_FN(VertexAttribPointer),
+ DEF_FN(Viewport),
+ {0}
+ },
+ },
+ // GL 2.1+ desktop only (and GLSL 120 shaders)
+ {
+ .ver_core = 210,
+ .provides = MPGL_CAP_ROW_LENGTH | MPGL_CAP_1D_TEX,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(DrawBuffer),
+ DEF_FN(GetTexLevelParameteriv),
+ DEF_FN(ReadBuffer),
+ DEF_FN(TexImage1D),
+ DEF_FN(UnmapBuffer),
+ {0}
+ },
+ },
+ // GL 2.1 has this as extension only.
+ {
+ .ver_exclude = 300,
+ .ver_es_exclude = 300,
+ .extension = "GL_ARB_map_buffer_range",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(MapBufferRange),
+ {0}
+ },
+ },
+ // GL 3.0+ and ES 3.x core only functions.
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BindBufferBase),
+ DEF_FN(BlitFramebuffer),
+ DEF_FN(GetStringi),
+ DEF_FN(MapBufferRange),
+ // for ES 3.0
+ DEF_FN(ReadBuffer),
+ DEF_FN(UnmapBuffer),
+ {0}
+ },
+ },
+ // For ES 3.1 core
+ {
+ .ver_es_core = 310,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GetTexLevelParameteriv),
+ {0}
+ },
+ },
+ {
+ .ver_core = 210,
+ .ver_es_core = 300,
+ .provides = MPGL_CAP_3D_TEX,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(TexImage3D),
+ {0}
+ },
+ },
+ // Useful for ES 2.0
+ {
+ .ver_core = 110,
+ .ver_es_core = 300,
+ .extension = "GL_EXT_unpack_subimage",
+ .provides = MPGL_CAP_ROW_LENGTH,
+ },
+ // Framebuffers, extension in GL 2.x, core in GL 3.x core.
+ {
+ .ver_core = 300,
+ .ver_es_core = 200,
+ .extension = "GL_ARB_framebuffer_object",
+ .provides = MPGL_CAP_FB,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BindFramebuffer),
+ DEF_FN(GenFramebuffers),
+ DEF_FN(DeleteFramebuffers),
+ DEF_FN(CheckFramebufferStatus),
+ DEF_FN(FramebufferTexture2D),
+ DEF_FN(GetFramebufferAttachmentParameteriv),
+ {0}
+ },
+ },
+ // VAOs, extension in GL 2.x, core in GL 3.x core.
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_vertex_array_object",
+ .provides = MPGL_CAP_VAO,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GenVertexArrays),
+ DEF_FN(BindVertexArray),
+ DEF_FN(DeleteVertexArrays),
+ {0}
+ }
+ },
+ // GL_RED / GL_RG textures, extension in GL 2.x, core in GL 3.x core.
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_texture_rg",
+ .provides = MPGL_CAP_TEX_RG,
+ },
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .extension = "GL_EXT_texture_rg",
+ .provides = MPGL_CAP_TEX_RG,
+ },
+ // GL_R16 etc.
+ {
+ .extension = "GL_EXT_texture_norm16",
+ .provides = MPGL_CAP_EXT16,
+ .ver_exclude = 1, // never in desktop GL
+ },
+ // Float texture support for GL 2.x
+ {
+ .extension = "GL_ARB_texture_float",
+ .provides = MPGL_CAP_ARB_FLOAT,
+ .ver_exclude = 300,
+ .ver_es_exclude = 1,
+ },
+ // 16 bit float textures that can be rendered to in GLES
+ {
+ .extension = "GL_EXT_color_buffer_half_float",
+ .provides = MPGL_CAP_EXT_CR_HFLOAT,
+ .ver_exclude = 1,
+ .ver_es_exclude = 320,
+ },
+ {
+ .ver_core = 320,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_sync",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(FenceSync),
+ DEF_FN(ClientWaitSync),
+ DEF_FN(DeleteSync),
+ {0}
+ },
+ },
+ {
+ .ver_core = 330,
+ .extension = "GL_ARB_timer_query",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GenQueries),
+ DEF_FN(DeleteQueries),
+ DEF_FN(BeginQuery),
+ DEF_FN(EndQuery),
+ DEF_FN(QueryCounter),
+ DEF_FN(IsQuery),
+ DEF_FN(GetQueryObjectiv),
+ DEF_FN(GetQueryObjecti64v),
+ DEF_FN(GetQueryObjectuiv),
+ DEF_FN(GetQueryObjectui64v),
+ {0}
+ },
+ },
+ {
+ .extension = "GL_EXT_disjoint_timer_query",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(GenQueries, "glGenQueriesEXT"),
+ DEF_FN_NAME(DeleteQueries, "glDeleteQueriesEXT"),
+ DEF_FN_NAME(BeginQuery, "glBeginQueryEXT"),
+ DEF_FN_NAME(EndQuery, "glEndQueryEXT"),
+ DEF_FN_NAME(QueryCounter, "glQueryCounterEXT"),
+ DEF_FN_NAME(IsQuery, "glIsQueryEXT"),
+ DEF_FN_NAME(GetQueryObjectiv, "glGetQueryObjectivEXT"),
+ DEF_FN_NAME(GetQueryObjecti64v, "glGetQueryObjecti64vEXT"),
+ DEF_FN_NAME(GetQueryObjectuiv, "glGetQueryObjectuivEXT"),
+ DEF_FN_NAME(GetQueryObjectui64v, "glGetQueryObjectui64vEXT"),
+ {0}
+ },
+ },
+ {
+ .ver_core = 430,
+ .extension = "GL_ARB_invalidate_subdata",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(InvalidateTexImage),
+ {0}
+ },
+ },
+ {
+ .ver_core = 430,
+ .ver_es_core = 300,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(InvalidateFramebuffer),
+ {0}
+ },
+ },
+ {
+ .ver_core = 410,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_get_program_binary",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GetProgramBinary),
+ DEF_FN(ProgramBinary),
+ {0}
+ },
+ },
+ {
+ .ver_core = 440,
+ .extension = "GL_ARB_buffer_storage",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BufferStorage),
+ {0}
+ },
+ },
+ // Equivalent extension for ES
+ {
+ .extension = "GL_EXT_buffer_storage",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(BufferStorage, "glBufferStorageEXT"),
+ {0}
+ },
+ },
+ {
+ .ver_core = 420,
+ .ver_es_core = 310,
+ .extension = "GL_ARB_shader_image_load_store",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BindImageTexture),
+ DEF_FN(MemoryBarrier),
+ {0}
+ },
+ },
+ {
+ .ver_core = 310,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_uniform_buffer_object",
+ .provides = MPGL_CAP_UBO,
+ },
+ {
+ .ver_core = 430,
+ .ver_es_core = 310,
+ .extension = "GL_ARB_shader_storage_buffer_object",
+ .provides = MPGL_CAP_SSBO,
+ },
+ {
+ .ver_core = 430,
+ .ver_es_core = 310,
+ .extension = "GL_ARB_compute_shader",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(DispatchCompute),
+ {0},
+ },
+ },
+ {
+ .ver_core = 430,
+ .extension = "GL_ARB_arrays_of_arrays",
+ .provides = MPGL_CAP_NESTED_ARRAY,
+ },
+ // Swap control, always an OS specific extension
+ // The OSX code loads this manually.
+ {
+ .extension = "GLX_SGI_swap_control",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(SwapInterval, "glXSwapIntervalSGI"),
+ {0},
+ },
+ },
+ // This one overrides GLX_SGI_swap_control on platforms using mesa. The
+ // only difference is that it supports glXSwapInterval(0).
+ {
+ .extension = "GLX_MESA_swap_control",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(SwapInterval, "glXSwapIntervalMESA"),
+ {0},
+ },
+ },
+ {
+ .extension = "WGL_EXT_swap_control",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(SwapInterval, "wglSwapIntervalEXT"),
+ {0},
+ },
+ },
+ {
+ .extension = "GLX_SGI_video_sync",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(GetVideoSync, "glXGetVideoSyncSGI"),
+ DEF_FN_NAME(WaitVideoSync, "glXWaitVideoSyncSGI"),
+ {0},
+ },
+ },
+ // For gl_hwdec_vdpau.c
+ // http://www.opengl.org/registry/specs/NV/vdpau_interop.txt
+ {
+ .extension = "GL_NV_vdpau_interop",
+ .provides = MPGL_CAP_VDPAU,
+ .functions = (const struct gl_function[]) {
+ // (only functions needed by us)
+ DEF_FN(VDPAUInitNV),
+ DEF_FN(VDPAUFiniNV),
+ DEF_FN(VDPAURegisterOutputSurfaceNV),
+ DEF_FN(VDPAURegisterVideoSurfaceNV),
+ DEF_FN(VDPAUUnregisterSurfaceNV),
+ DEF_FN(VDPAUSurfaceAccessNV),
+ DEF_FN(VDPAUMapSurfacesNV),
+ DEF_FN(VDPAUUnmapSurfacesNV),
+ {0}
+ },
+ },
+#if HAVE_GL_DXINTEROP
+ {
+ .extension = "WGL_NV_DX_interop",
+ .provides = MPGL_CAP_DXINTEROP,
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(DXSetResourceShareHandleNV, "wglDXSetResourceShareHandleNV"),
+ DEF_FN_NAME(DXOpenDeviceNV, "wglDXOpenDeviceNV"),
+ DEF_FN_NAME(DXCloseDeviceNV, "wglDXCloseDeviceNV"),
+ DEF_FN_NAME(DXRegisterObjectNV, "wglDXRegisterObjectNV"),
+ DEF_FN_NAME(DXUnregisterObjectNV, "wglDXUnregisterObjectNV"),
+ DEF_FN_NAME(DXLockObjectsNV, "wglDXLockObjectsNV"),
+ DEF_FN_NAME(DXUnlockObjectsNV, "wglDXUnlockObjectsNV"),
+ {0}
+ },
+ },
+#endif
+ // Apple Packed YUV Formats
+ // For gl_hwdec_vda.c
+ // http://www.opengl.org/registry/specs/APPLE/rgb_422.txt
+ {
+ .extension = "GL_APPLE_rgb_422",
+ .provides = MPGL_CAP_APPLE_RGB_422,
+ },
+ {
+ .ver_core = 430,
+ .extension = "GL_ARB_debug_output",
+ .provides = MPGL_CAP_DEBUG,
+ .functions = (const struct gl_function[]) {
+ // (only functions needed by us)
+ DEF_FN(DebugMessageCallback),
+ {0}
+ },
+ },
+ // ES version uses a different extension.
+ {
+ .ver_es_core = 320,
+ .extension = "GL_KHR_debug",
+ .provides = MPGL_CAP_DEBUG,
+ .functions = (const struct gl_function[]) {
+ // (only functions needed by us)
+ DEF_FN(DebugMessageCallback),
+ {0}
+ },
+ },
+ {
+ .extension = "GL_ANGLE_translated_shader_source",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GetTranslatedShaderSourceANGLE),
+ {0}
+ },
+ },
+};
+
+#undef FN_OFFS
+#undef DEF_FN_HARD
+#undef DEF_FN
+#undef DEF_FN_NAME
+
+// Fill the GL struct with function pointers and extensions from the current
+// GL context. Called by the backend.
+// get_fn: function to resolve function names
+// ext2: an extra extension string
+// log: used to output messages
+void mpgl_load_functions2(GL *gl, void *(*get_fn)(void *ctx, const char *n),
+ void *fn_ctx, const char *ext2, struct mp_log *log)
+{
+ talloc_free(gl->extensions);
+ *gl = (GL) {
+ .extensions = talloc_strdup(gl, ext2 ? ext2 : ""),
+ .get_fn = get_fn,
+ .fn_ctx = fn_ctx,
+ };
+
+ gl->GetString = get_fn(fn_ctx, "glGetString");
+ if (!gl->GetString) {
+ mp_err(log, "Can't load OpenGL functions.\n");
+ goto error;
+ }
+
+ int major = 0, minor = 0;
+ const char *version_string = gl->GetString(GL_VERSION);
+ if (!version_string) {
+ mp_fatal(log, "glGetString(GL_VERSION) returned NULL.\n");
+ goto error;
+ }
+ mp_verbose(log, "GL_VERSION='%s'\n", version_string);
+ if (strncmp(version_string, "OpenGL ES ", 10) == 0) {
+ version_string += 10;
+ gl->es = 100;
+ }
+ if (sscanf(version_string, "%d.%d", &major, &minor) < 2)
+ goto error;
+ gl->version = MPGL_VER(major, minor);
+ mp_verbose(log, "Detected %s %d.%d.\n", gl->es ? "GLES" : "desktop OpenGL",
+ major, minor);
+
+ if (gl->es) {
+ gl->es = gl->version;
+ gl->version = 0;
+ if (gl->es < 200) {
+ mp_fatal(log, "At least GLESv2 required.\n");
+ goto error;
+ }
+ }
+
+ mp_verbose(log, "GL_VENDOR='%s'\n", gl->GetString(GL_VENDOR));
+ mp_verbose(log, "GL_RENDERER='%s'\n", gl->GetString(GL_RENDERER));
+ const char *shader = gl->GetString(GL_SHADING_LANGUAGE_VERSION);
+ if (shader)
+ mp_verbose(log, "GL_SHADING_LANGUAGE_VERSION='%s'\n", shader);
+
+ if (gl->version >= 300) {
+ gl->GetStringi = get_fn(fn_ctx, "glGetStringi");
+ gl->GetIntegerv = get_fn(fn_ctx, "glGetIntegerv");
+
+ if (!(gl->GetStringi && gl->GetIntegerv))
+ goto error;
+
+ GLint exts;
+ gl->GetIntegerv(GL_NUM_EXTENSIONS, &exts);
+ for (int n = 0; n < exts; n++) {
+ const char *ext = gl->GetStringi(GL_EXTENSIONS, n);
+ gl->extensions = talloc_asprintf_append(gl->extensions, " %s", ext);
+ }
+
+ } else {
+ const char *ext = (char*)gl->GetString(GL_EXTENSIONS);
+ gl->extensions = talloc_asprintf_append(gl->extensions, " %s", ext);
+ }
+
+ mp_dbg(log, "Combined OpenGL extensions string:\n%s\n", gl->extensions);
+
+ for (int n = 0; n < MP_ARRAY_SIZE(gl_functions); n++) {
+ const struct gl_functions *section = &gl_functions[n];
+ int version = gl->es ? gl->es : gl->version;
+ int ver_core = gl->es ? section->ver_es_core : section->ver_core;
+
+ // NOTE: Function entrypoints can exist, even if they do not work.
+ // We must always check extension strings and versions.
+
+ if (gl->version && section->ver_exclude &&
+ gl->version >= section->ver_exclude)
+ continue;
+ if (gl->es && section->ver_es_exclude &&
+ gl->es >= section->ver_es_exclude)
+ continue;
+
+ bool exists = false, must_exist = false;
+ if (ver_core)
+ must_exist = version >= ver_core;
+
+ if (section->extension)
+ exists = gl_check_extension(gl->extensions, section->extension);
+
+ exists |= must_exist;
+ if (!exists)
+ continue;
+
+ void *loaded[MAX_FN_COUNT] = {0};
+ bool all_loaded = true;
+ const struct gl_function *fnlist = section->functions;
+
+ for (int i = 0; fnlist && fnlist[i].name; i++) {
+ const struct gl_function *fn = &fnlist[i];
+ void *ptr = get_fn(fn_ctx, fn->name);
+ if (!ptr) {
+ all_loaded = false;
+ if (must_exist) {
+ mp_err(log, "GL %d.%d function %s not found.\n",
+ MPGL_VER_GET_MAJOR(ver_core),
+ MPGL_VER_GET_MINOR(ver_core), fn->name);
+ goto error;
+ } else {
+ mp_warn(log, "Function %s from extension %s not found.\n",
+ fn->name, section->extension);
+ }
+ break;
+ }
+ assert(i < MAX_FN_COUNT);
+ loaded[i] = ptr;
+ }
+
+ if (all_loaded) {
+ gl->mpgl_caps |= section->provides;
+ for (int i = 0; fnlist && fnlist[i].name; i++) {
+ const struct gl_function *fn = &fnlist[i];
+ void **funcptr = (void**)(((char*)gl) + fn->offset);
+ if (loaded[i])
+ *funcptr = loaded[i];
+ }
+ if (!must_exist && section->extension)
+ mp_verbose(log, "Loaded extension %s.\n", section->extension);
+ }
+ }
+
+ gl->glsl_version = 0;
+ if (gl->es) {
+ if (gl->es >= 200)
+ gl->glsl_version = 100;
+ if (gl->es >= 300)
+ gl->glsl_version = gl->es;
+ } else {
+ gl->glsl_version = 120;
+ int glsl_major = 0, glsl_minor = 0;
+ if (shader && sscanf(shader, "%d.%d", &glsl_major, &glsl_minor) == 2)
+ gl->glsl_version = glsl_major * 100 + glsl_minor;
+ // restrict GLSL version to be forwards compatible
+ gl->glsl_version = MPMIN(gl->glsl_version, 440);
+ }
+
+ if (is_software_gl(gl)) {
+ gl->mpgl_caps |= MPGL_CAP_SW;
+ mp_verbose(log, "Detected suspected software renderer.\n");
+ }
+
+ if (!is_fast_dr(gl))
+ gl->mpgl_caps |= MPGL_CAP_SLOW_DR;
+
+ // GL_ARB_compute_shader & GL_ARB_shader_image_load_store
+ if (gl->DispatchCompute && gl->BindImageTexture)
+ gl->mpgl_caps |= MPGL_CAP_COMPUTE_SHADER;
+
+ // Provided for simpler handling if no framebuffer support is available.
+ if (!gl->BindFramebuffer)
+ gl->BindFramebuffer = &dummy_glBindFramebuffer;
+ return;
+
+error:
+ gl->version = 0;
+ gl->es = 0;
+ gl->mpgl_caps = 0;
+}
+
+static void *get_procaddr_wrapper(void *ctx, const char *name)
+{
+ void *(*getProcAddress)(const GLubyte *) = ctx;
+ return getProcAddress ? getProcAddress((const GLubyte*)name) : NULL;
+}
+
+void mpgl_load_functions(GL *gl, void *(*getProcAddress)(const GLubyte *),
+ const char *ext2, struct mp_log *log)
+{
+ mpgl_load_functions2(gl, get_procaddr_wrapper, getProcAddress, ext2, log);
+}
diff --git a/video/out/opengl/common.h b/video/out/opengl/common.h
new file mode 100644
index 0000000..a6b02c9
--- /dev/null
+++ b/video/out/opengl/common.h
@@ -0,0 +1,258 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_GL_COMMON_H
+#define MPLAYER_GL_COMMON_H
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "config.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "video/out/vo.h"
+#include "video/out/gpu/ra.h"
+
+#include "gl_headers.h"
+
+#if HAVE_GL_WIN32
+#include <windows.h>
+#endif
+
+struct GL;
+typedef struct GL GL;
+
+enum {
+ MPGL_CAP_ROW_LENGTH = (1 << 4), // GL_[UN]PACK_ROW_LENGTH
+ MPGL_CAP_FB = (1 << 5),
+ MPGL_CAP_VAO = (1 << 6),
+ MPGL_CAP_TEX_RG = (1 << 10), // GL_ARB_texture_rg / GL 3.x
+ MPGL_CAP_VDPAU = (1 << 11), // GL_NV_vdpau_interop
+ MPGL_CAP_APPLE_RGB_422 = (1 << 12), // GL_APPLE_rgb_422
+ MPGL_CAP_1D_TEX = (1 << 14),
+ MPGL_CAP_3D_TEX = (1 << 15),
+ MPGL_CAP_DEBUG = (1 << 16),
+ MPGL_CAP_DXINTEROP = (1 << 17), // WGL_NV_DX_interop
+ MPGL_CAP_EXT16 = (1 << 18), // GL_EXT_texture_norm16
+ MPGL_CAP_ARB_FLOAT = (1 << 19), // GL_ARB_texture_float
+ MPGL_CAP_EXT_CR_HFLOAT = (1 << 20), // GL_EXT_color_buffer_half_float
+ MPGL_CAP_UBO = (1 << 21), // GL_ARB_uniform_buffer_object
+ MPGL_CAP_SSBO = (1 << 22), // GL_ARB_shader_storage_buffer_object
+ MPGL_CAP_COMPUTE_SHADER = (1 << 23), // GL_ARB_compute_shader & GL_ARB_shader_image_load_store
+ MPGL_CAP_NESTED_ARRAY = (1 << 24), // GL_ARB_arrays_of_arrays
+
+ MPGL_CAP_SLOW_DR = (1 << 29), // direct rendering is assumed to be slow
+ MPGL_CAP_SW = (1 << 30), // indirect or sw renderer
+};
+
+// E.g. 310 means 3.1
+// Code doesn't have to use the macros; they are for convenience only.
+#define MPGL_VER(major, minor) (((major) * 100) + (minor) * 10)
+#define MPGL_VER_GET_MAJOR(ver) ((unsigned)(ver) / 100)
+#define MPGL_VER_GET_MINOR(ver) ((unsigned)(ver) % 100 / 10)
+
+#define MPGL_VER_P(ver) MPGL_VER_GET_MAJOR(ver), MPGL_VER_GET_MINOR(ver)
+
+void mpgl_load_functions(GL *gl, void *(*getProcAddress)(const GLubyte *),
+ const char *ext2, struct mp_log *log);
+void mpgl_load_functions2(GL *gl, void *(*get_fn)(void *ctx, const char *n),
+ void *fn_ctx, const char *ext2, struct mp_log *log);
+
+typedef void (GLAPIENTRY *MP_GLDEBUGPROC)(GLenum, GLenum, GLuint, GLenum,
+ GLsizei, const GLchar *,const void *);
+
+//function pointers loaded from the OpenGL library
+struct GL {
+ int version; // MPGL_VER() mangled (e.g. 210 for 2.1)
+ int es; // es version (e.g. 300), 0 for desktop GL
+ int glsl_version; // e.g. 130 for GLSL 1.30
+ char *extensions; // Equivalent to GL_EXTENSIONS
+ int mpgl_caps; // Bitfield of MPGL_CAP_* constants
+ bool debug_context; // use of e.g. GLX_CONTEXT_DEBUG_BIT_ARB
+
+ // Set to false if the implementation follows normal GL semantics, which is
+ // upside down. Set to true if it does *not*, i.e. if rendering is right
+ // side up
+ bool flipped;
+
+ // Copy of function pointer used to load GL.
+ // Caution: Not necessarily valid to use after VO init has completed!
+ void *(*get_fn)(void *ctx, const char *n);
+ void *fn_ctx;
+
+ void (GLAPIENTRY *Viewport)(GLint, GLint, GLsizei, GLsizei);
+ void (GLAPIENTRY *Clear)(GLbitfield);
+ void (GLAPIENTRY *GenTextures)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteTextures)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *ClearColor)(GLclampf, GLclampf, GLclampf, GLclampf);
+ void (GLAPIENTRY *Enable)(GLenum);
+ void (GLAPIENTRY *Disable)(GLenum);
+ const GLubyte *(GLAPIENTRY * GetString)(GLenum);
+ void (GLAPIENTRY *BlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum);
+ void (GLAPIENTRY *Flush)(void);
+ void (GLAPIENTRY *Finish)(void);
+ void (GLAPIENTRY *PixelStorei)(GLenum, GLint);
+ void (GLAPIENTRY *TexImage1D)(GLenum, GLint, GLint, GLsizei, GLint,
+ GLenum, GLenum, const GLvoid *);
+ void (GLAPIENTRY *TexImage2D)(GLenum, GLint, GLint, GLsizei, GLsizei,
+ GLint, GLenum, GLenum, const GLvoid *);
+ void (GLAPIENTRY *TexSubImage2D)(GLenum, GLint, GLint, GLint,
+ GLsizei, GLsizei, GLenum, GLenum,
+ const GLvoid *);
+ void (GLAPIENTRY *TexParameteri)(GLenum, GLenum, GLint);
+ void (GLAPIENTRY *GetIntegerv)(GLenum, GLint *);
+ void (GLAPIENTRY *ReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum,
+ GLenum, GLvoid *);
+ void (GLAPIENTRY *ReadBuffer)(GLenum);
+ void (GLAPIENTRY *DrawBuffer)(GLenum);
+ void (GLAPIENTRY *DrawArrays)(GLenum, GLint, GLsizei);
+ GLenum (GLAPIENTRY *GetError)(void);
+ void (GLAPIENTRY *GetTexLevelParameteriv)(GLenum, GLint, GLenum, GLint *);
+ void (GLAPIENTRY *Scissor)(GLint, GLint, GLsizei, GLsizei);
+
+ void (GLAPIENTRY *GenBuffers)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteBuffers)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *BindBuffer)(GLenum, GLuint);
+ void (GLAPIENTRY *BindBufferBase)(GLenum, GLuint, GLuint);
+ GLvoid * (GLAPIENTRY *MapBufferRange)(GLenum, GLintptr, GLsizeiptr,
+ GLbitfield);
+ GLboolean (GLAPIENTRY *UnmapBuffer)(GLenum);
+ void (GLAPIENTRY *BufferData)(GLenum, intptr_t, const GLvoid *, GLenum);
+ void (GLAPIENTRY *BufferSubData)(GLenum, GLintptr, GLsizeiptr, const GLvoid *);
+ void (GLAPIENTRY *ActiveTexture)(GLenum);
+ void (GLAPIENTRY *BindTexture)(GLenum, GLuint);
+ int (GLAPIENTRY *SwapInterval)(int);
+ void (GLAPIENTRY *TexImage3D)(GLenum, GLint, GLenum, GLsizei, GLsizei,
+ GLsizei, GLint, GLenum, GLenum,
+ const GLvoid *);
+
+ void (GLAPIENTRY *GenVertexArrays)(GLsizei, GLuint *);
+ void (GLAPIENTRY *BindVertexArray)(GLuint);
+ GLint (GLAPIENTRY *GetAttribLocation)(GLuint, const GLchar *);
+ void (GLAPIENTRY *EnableVertexAttribArray)(GLuint);
+ void (GLAPIENTRY *DisableVertexAttribArray)(GLuint);
+ void (GLAPIENTRY *VertexAttribPointer)(GLuint, GLint, GLenum, GLboolean,
+ GLsizei, const GLvoid *);
+ void (GLAPIENTRY *DeleteVertexArrays)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *UseProgram)(GLuint);
+ GLint (GLAPIENTRY *GetUniformLocation)(GLuint, const GLchar *);
+ void (GLAPIENTRY *CompileShader)(GLuint);
+ GLuint (GLAPIENTRY *CreateProgram)(void);
+ GLuint (GLAPIENTRY *CreateShader)(GLenum);
+ void (GLAPIENTRY *ShaderSource)(GLuint, GLsizei, const GLchar **,
+ const GLint *);
+ void (GLAPIENTRY *LinkProgram)(GLuint);
+ void (GLAPIENTRY *AttachShader)(GLuint, GLuint);
+ void (GLAPIENTRY *DeleteShader)(GLuint);
+ void (GLAPIENTRY *DeleteProgram)(GLuint);
+ void (GLAPIENTRY *GetShaderInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *);
+ void (GLAPIENTRY *GetShaderiv)(GLuint, GLenum, GLint *);
+ void (GLAPIENTRY *GetProgramInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *);
+ void (GLAPIENTRY *GetProgramiv)(GLenum, GLenum, GLint *);
+ void (GLAPIENTRY *GetProgramBinary)(GLuint, GLsizei, GLsizei *, GLenum *,
+ void *);
+ void (GLAPIENTRY *ProgramBinary)(GLuint, GLenum, const void *, GLsizei);
+
+ void (GLAPIENTRY *DispatchCompute)(GLuint, GLuint, GLuint);
+ void (GLAPIENTRY *BindImageTexture)(GLuint, GLuint, GLint, GLboolean,
+ GLint, GLenum, GLenum);
+ void (GLAPIENTRY *MemoryBarrier)(GLbitfield);
+
+ const GLubyte* (GLAPIENTRY *GetStringi)(GLenum, GLuint);
+ void (GLAPIENTRY *BindAttribLocation)(GLuint, GLuint, const GLchar *);
+ void (GLAPIENTRY *BindFramebuffer)(GLenum, GLuint);
+ void (GLAPIENTRY *GenFramebuffers)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteFramebuffers)(GLsizei, const GLuint *);
+ GLenum (GLAPIENTRY *CheckFramebufferStatus)(GLenum);
+ void (GLAPIENTRY *FramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint,
+ GLint);
+ void (GLAPIENTRY *BlitFramebuffer)(GLint, GLint, GLint, GLint, GLint, GLint,
+ GLint, GLint, GLbitfield, GLenum);
+ void (GLAPIENTRY *GetFramebufferAttachmentParameteriv)(GLenum, GLenum,
+ GLenum, GLint *);
+
+ void (GLAPIENTRY *Uniform1f)(GLint, GLfloat);
+ void (GLAPIENTRY *Uniform2f)(GLint, GLfloat, GLfloat);
+ void (GLAPIENTRY *Uniform3f)(GLint, GLfloat, GLfloat, GLfloat);
+ void (GLAPIENTRY *Uniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat);
+ void (GLAPIENTRY *Uniform1i)(GLint, GLint);
+ void (GLAPIENTRY *UniformMatrix2fv)(GLint, GLsizei, GLboolean,
+ const GLfloat *);
+ void (GLAPIENTRY *UniformMatrix3fv)(GLint, GLsizei, GLboolean,
+ const GLfloat *);
+
+ void (GLAPIENTRY *InvalidateTexImage)(GLuint, GLint);
+ void (GLAPIENTRY *InvalidateFramebuffer)(GLenum, GLsizei, const GLenum *);
+
+ GLsync (GLAPIENTRY *FenceSync)(GLenum, GLbitfield);
+ GLenum (GLAPIENTRY *ClientWaitSync)(GLsync, GLbitfield, GLuint64);
+ void (GLAPIENTRY *DeleteSync)(GLsync sync);
+
+ void (GLAPIENTRY *BufferStorage)(GLenum, intptr_t, const GLvoid *, GLenum);
+
+ void (GLAPIENTRY *GenQueries)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteQueries)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *BeginQuery)(GLenum, GLuint);
+ void (GLAPIENTRY *EndQuery)(GLenum);
+ void (GLAPIENTRY *QueryCounter)(GLuint, GLenum);
+ GLboolean (GLAPIENTRY *IsQuery)(GLuint);
+ void (GLAPIENTRY *GetQueryObjectiv)(GLuint, GLenum, GLint *);
+ void (GLAPIENTRY *GetQueryObjecti64v)(GLuint, GLenum, GLint64 *);
+ void (GLAPIENTRY *GetQueryObjectuiv)(GLuint, GLenum, GLuint *);
+ void (GLAPIENTRY *GetQueryObjectui64v)(GLuint, GLenum, GLuint64 *);
+
+ void (GLAPIENTRY *VDPAUInitNV)(const GLvoid *, const GLvoid *);
+ void (GLAPIENTRY *VDPAUFiniNV)(void);
+ GLvdpauSurfaceNV (GLAPIENTRY *VDPAURegisterOutputSurfaceNV)
+ (GLvoid *, GLenum, GLsizei, const GLuint *);
+ GLvdpauSurfaceNV (GLAPIENTRY *VDPAURegisterVideoSurfaceNV)
+ (GLvoid *, GLenum, GLsizei, const GLuint *);
+ void (GLAPIENTRY *VDPAUUnregisterSurfaceNV)(GLvdpauSurfaceNV);
+ void (GLAPIENTRY *VDPAUSurfaceAccessNV)(GLvdpauSurfaceNV, GLenum);
+ void (GLAPIENTRY *VDPAUMapSurfacesNV)(GLsizei, const GLvdpauSurfaceNV *);
+ void (GLAPIENTRY *VDPAUUnmapSurfacesNV)(GLsizei, const GLvdpauSurfaceNV *);
+
+#if HAVE_GL_WIN32
+ // The HANDLE type might not be present on non-Win32
+ BOOL (GLAPIENTRY *DXSetResourceShareHandleNV)(void *dxObject,
+ HANDLE shareHandle);
+ HANDLE (GLAPIENTRY *DXOpenDeviceNV)(void *dxDevice);
+ BOOL (GLAPIENTRY *DXCloseDeviceNV)(HANDLE hDevice);
+ HANDLE (GLAPIENTRY *DXRegisterObjectNV)(HANDLE hDevice, void *dxObject,
+ GLuint name, GLenum type, GLenum access);
+ BOOL (GLAPIENTRY *DXUnregisterObjectNV)(HANDLE hDevice, HANDLE hObject);
+ BOOL (GLAPIENTRY *DXLockObjectsNV)(HANDLE hDevice, GLint count,
+ HANDLE *hObjects);
+ BOOL (GLAPIENTRY *DXUnlockObjectsNV)(HANDLE hDevice, GLint count,
+ HANDLE *hObjects);
+#endif
+
+ GLint (GLAPIENTRY *GetVideoSync)(GLuint *);
+ GLint (GLAPIENTRY *WaitVideoSync)(GLint, GLint, unsigned int *);
+
+ void (GLAPIENTRY *GetTranslatedShaderSourceANGLE)(GLuint, GLsizei,
+ GLsizei*, GLchar* source);
+
+ void (GLAPIENTRY *DebugMessageCallback)(MP_GLDEBUGPROC callback,
+ const void *userParam);
+};
+
+#endif /* MPLAYER_GL_COMMON_H */
diff --git a/video/out/opengl/context.c b/video/out/opengl/context.c
new file mode 100644
index 0000000..05e279b
--- /dev/null
+++ b/video/out/opengl/context.c
@@ -0,0 +1,324 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "options/m_config.h"
+#include "context.h"
+#include "ra_gl.h"
+#include "utils.h"
+
+// 0-terminated list of desktop GL versions a backend should try to
+// initialize. Each entry is the minimum required version.
+const int mpgl_min_required_gl_versions[] = {
+ /*
+ * Nvidia drivers will not provide the highest supported version
+ * when 320 core is requested. Instead, it just returns 3.2. This
+ * would be bad, as we actually want compute shaders that require
+ * 4.2, so we have to request a sufficiently high version. We use
+ * 440 to maximise driver compatibility as we don't need anything
+ * from newer versions.
+ */
+ 440,
+ 320,
+ 210,
+ 0
+};
+
+enum {
+ FLUSH_NO = 0,
+ FLUSH_YES,
+ FLUSH_AUTO,
+};
+
+struct opengl_opts {
+ bool use_glfinish;
+ bool waitvsync;
+ int vsync_pattern[2];
+ int swapinterval;
+ int early_flush;
+ int gles_mode;
+};
+
+#define OPT_BASE_STRUCT struct opengl_opts
+const struct m_sub_options opengl_conf = {
+ .opts = (const struct m_option[]) {
+ {"opengl-glfinish", OPT_BOOL(use_glfinish)},
+ {"opengl-waitvsync", OPT_BOOL(waitvsync)},
+ {"opengl-swapinterval", OPT_INT(swapinterval)},
+ {"opengl-check-pattern-a", OPT_INT(vsync_pattern[0])},
+ {"opengl-check-pattern-b", OPT_INT(vsync_pattern[1])},
+ {"opengl-es", OPT_CHOICE(gles_mode,
+ {"auto", GLES_AUTO}, {"yes", GLES_YES}, {"no", GLES_NO})},
+ {"opengl-early-flush", OPT_CHOICE(early_flush,
+ {"no", FLUSH_NO}, {"yes", FLUSH_YES}, {"auto", FLUSH_AUTO})},
+ {0},
+ },
+ .defaults = &(const struct opengl_opts) {
+ .swapinterval = 1,
+ },
+ .size = sizeof(struct opengl_opts),
+};
+
+struct priv {
+ GL *gl;
+ struct mp_log *log;
+ struct ra_gl_ctx_params params;
+ struct opengl_opts *opts;
+ struct ra_swapchain_fns fns;
+ GLuint main_fb;
+ struct ra_tex *wrapped_fb; // corresponds to main_fb
+ // for debugging:
+ int frames_rendered;
+ unsigned int prev_sgi_sync_count;
+ // for gl_vsync_pattern
+ int last_pattern;
+ int matches, mismatches;
+ // for swapchain_depth simulation
+ GLsync *vsync_fences;
+ int num_vsync_fences;
+};
+
+enum gles_mode ra_gl_ctx_get_glesmode(struct ra_ctx *ctx)
+{
+ void *tmp = talloc_new(NULL);
+ struct opengl_opts *opts;
+ enum gles_mode mode;
+
+ opts = mp_get_config_group(tmp, ctx->global, &opengl_conf);
+ mode = opts->gles_mode;
+
+ talloc_free(tmp);
+ return mode;
+}
+
+void ra_gl_ctx_uninit(struct ra_ctx *ctx)
+{
+ if (ctx->swapchain) {
+ struct priv *p = ctx->swapchain->priv;
+ if (ctx->ra && p->wrapped_fb)
+ ra_tex_free(ctx->ra, &p->wrapped_fb);
+ talloc_free(ctx->swapchain);
+ ctx->swapchain = NULL;
+ }
+
+ // Clean up any potentially left-over debug callback
+ if (ctx->ra)
+ ra_gl_set_debug(ctx->ra, false);
+
+ ra_free(&ctx->ra);
+}
+
+static const struct ra_swapchain_fns ra_gl_swapchain_fns;
+
+bool ra_gl_ctx_init(struct ra_ctx *ctx, GL *gl, struct ra_gl_ctx_params params)
+{
+ struct ra_swapchain *sw = ctx->swapchain = talloc_ptrtype(NULL, sw);
+ *sw = (struct ra_swapchain) {
+ .ctx = ctx,
+ };
+
+ struct priv *p = sw->priv = talloc_ptrtype(sw, p);
+ *p = (struct priv) {
+ .gl = gl,
+ .log = ctx->log,
+ .params = params,
+ .opts = mp_get_config_group(p, ctx->global, &opengl_conf),
+ .fns = ra_gl_swapchain_fns,
+ };
+
+ sw->fns = &p->fns;
+
+ const struct ra_swapchain_fns *ext = p->params.external_swapchain;
+ if (ext) {
+ if (ext->color_depth)
+ p->fns.color_depth = ext->color_depth;
+ if (ext->start_frame)
+ p->fns.start_frame = ext->start_frame;
+ if (ext->submit_frame)
+ p->fns.submit_frame = ext->submit_frame;
+ if (ext->swap_buffers)
+ p->fns.swap_buffers = ext->swap_buffers;
+ }
+
+ if (!gl->version && !gl->es)
+ return false;
+
+ if (gl->mpgl_caps & MPGL_CAP_SW) {
+ MP_WARN(p, "Suspected software renderer or indirect context.\n");
+ if (ctx->opts.probing && !ctx->opts.allow_sw)
+ return false;
+ }
+
+ gl->debug_context = ctx->opts.debug;
+
+ if (gl->SwapInterval) {
+ gl->SwapInterval(p->opts->swapinterval);
+ } else {
+ MP_VERBOSE(p, "GL_*_swap_control extension missing.\n");
+ }
+
+ ctx->ra = ra_create_gl(p->gl, ctx->log);
+ return !!ctx->ra;
+}
+
+void ra_gl_ctx_resize(struct ra_swapchain *sw, int w, int h, int fbo)
+{
+ struct priv *p = sw->priv;
+ if (p->main_fb == fbo && p->wrapped_fb && p->wrapped_fb->params.w == w
+ && p->wrapped_fb->params.h == h)
+ return;
+
+ if (p->wrapped_fb)
+ ra_tex_free(sw->ctx->ra, &p->wrapped_fb);
+
+ p->main_fb = fbo;
+ p->wrapped_fb = ra_create_wrapped_fb(sw->ctx->ra, fbo, w, h);
+}
+
+int ra_gl_ctx_color_depth(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ GL *gl = p->gl;
+
+ if (!p->wrapped_fb)
+ return 0;
+
+ if ((gl->es < 300 && !gl->version) || !(gl->mpgl_caps & MPGL_CAP_FB))
+ return 0;
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, p->main_fb);
+
+ GLenum obj = gl->version ? GL_BACK_LEFT : GL_BACK;
+ if (p->main_fb)
+ obj = GL_COLOR_ATTACHMENT0;
+
+ GLint depth_g = 0;
+
+ gl->GetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, obj,
+ GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE, &depth_g);
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ return depth_g;
+}
+
+bool ra_gl_ctx_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct priv *p = sw->priv;
+
+ bool visible = true;
+ if (p->params.check_visible)
+ visible = p->params.check_visible(sw->ctx);
+
+ // If out_fbo is NULL, this was called from vo_gpu_next. Bail out.
+ if (!out_fbo || !visible)
+ return visible;
+
+ *out_fbo = (struct ra_fbo) {
+ .tex = p->wrapped_fb,
+ .flip = !p->gl->flipped, // OpenGL FBs are normally flipped
+ };
+ return true;
+}
+
+bool ra_gl_ctx_submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame)
+{
+ struct priv *p = sw->priv;
+ GL *gl = p->gl;
+
+ if (p->opts->use_glfinish)
+ gl->Finish();
+
+ if (gl->FenceSync && !p->params.external_swapchain) {
+ GLsync fence = gl->FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if (fence)
+ MP_TARRAY_APPEND(p, p->vsync_fences, p->num_vsync_fences, fence);
+ }
+
+ switch (p->opts->early_flush) {
+ case FLUSH_AUTO:
+ if (frame->display_synced)
+ break;
+ MP_FALLTHROUGH;
+ case FLUSH_YES:
+ gl->Flush();
+ }
+
+ return true;
+}
+
+static void check_pattern(struct priv *p, int item)
+{
+ int expected = p->opts->vsync_pattern[p->last_pattern];
+ if (item == expected) {
+ p->last_pattern++;
+ if (p->last_pattern >= 2)
+ p->last_pattern = 0;
+ p->matches++;
+ } else {
+ p->mismatches++;
+ MP_WARN(p, "wrong pattern, expected %d got %d (hit: %d, mis: %d)\n",
+ expected, item, p->matches, p->mismatches);
+ }
+}
+
+void ra_gl_ctx_swap_buffers(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ GL *gl = p->gl;
+
+ p->params.swap_buffers(sw->ctx);
+ p->frames_rendered++;
+
+ if (p->frames_rendered > 5 && !sw->ctx->opts.debug)
+ ra_gl_set_debug(sw->ctx->ra, false);
+
+ if ((p->opts->waitvsync || p->opts->vsync_pattern[0])
+ && gl->GetVideoSync)
+ {
+ unsigned int n1 = 0, n2 = 0;
+ gl->GetVideoSync(&n1);
+ if (p->opts->waitvsync)
+ gl->WaitVideoSync(2, (n1 + 1) % 2, &n2);
+ int step = n1 - p->prev_sgi_sync_count;
+ p->prev_sgi_sync_count = n1;
+ MP_DBG(p, "Flip counts: %u->%u, step=%d\n", n1, n2, step);
+ if (p->opts->vsync_pattern[0])
+ check_pattern(p, step);
+ }
+
+ while (p->num_vsync_fences >= sw->ctx->vo->opts->swapchain_depth) {
+ gl->ClientWaitSync(p->vsync_fences[0], GL_SYNC_FLUSH_COMMANDS_BIT, 1e9);
+ gl->DeleteSync(p->vsync_fences[0]);
+ MP_TARRAY_REMOVE_AT(p->vsync_fences, p->num_vsync_fences, 0);
+ }
+}
+
+static void ra_gl_ctx_get_vsync(struct ra_swapchain *sw,
+ struct vo_vsync_info *info)
+{
+ struct priv *p = sw->priv;
+ if (p->params.get_vsync)
+ p->params.get_vsync(sw->ctx, info);
+}
+
+static const struct ra_swapchain_fns ra_gl_swapchain_fns = {
+ .color_depth = ra_gl_ctx_color_depth,
+ .start_frame = ra_gl_ctx_start_frame,
+ .submit_frame = ra_gl_ctx_submit_frame,
+ .swap_buffers = ra_gl_ctx_swap_buffers,
+ .get_vsync = ra_gl_ctx_get_vsync,
+};
diff --git a/video/out/opengl/context.h b/video/out/opengl/context.h
new file mode 100644
index 0000000..c96450e
--- /dev/null
+++ b/video/out/opengl/context.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "common/global.h"
+#include "video/out/gpu/context.h"
+#include "common.h"
+
+extern const int mpgl_min_required_gl_versions[];
+
+enum gles_mode {
+ GLES_AUTO = 0,
+ GLES_YES,
+ GLES_NO,
+};
+
+// Returns the gles mode based on the --opengl opts.
+enum gles_mode ra_gl_ctx_get_glesmode(struct ra_ctx *ctx);
+
+// These are a set of helpers for ra_ctx providers based on ra_gl.
+// The init function also initializes ctx->ra and ctx->swapchain, so the user
+// doesn't have to do this manually. (Similarly, the uninit function will
+// clean them up)
+
+struct ra_gl_ctx_params {
+ // For special contexts (i.e. wayland) that want to check visibility
+ // before drawing a frame.
+ bool (*check_visible)(struct ra_ctx *ctx);
+
+ // Set to the platform-specific function to swap buffers, like
+ // glXSwapBuffers, eglSwapBuffers etc. This will be called by
+ // ra_gl_ctx_swap_buffers. Required unless you either never call that
+ // function or if you override it yourself.
+ void (*swap_buffers)(struct ra_ctx *ctx);
+
+ // See ra_swapchain_fns.get_vsync.
+ void (*get_vsync)(struct ra_ctx *ctx, struct vo_vsync_info *info);
+
+ // If this is set to non-NULL, then the ra_gl_ctx will consider the GL
+ // implementation to be using an external swapchain, which disables the
+ // software simulation of --swapchain-depth. Any functions defined by this
+ // ra_swapchain_fns structs will entirely replace the equivalent ra_gl_ctx
+ // functions in the resulting ra_swapchain.
+ const struct ra_swapchain_fns *external_swapchain;
+};
+
+void ra_gl_ctx_uninit(struct ra_ctx *ctx);
+bool ra_gl_ctx_init(struct ra_ctx *ctx, GL *gl, struct ra_gl_ctx_params params);
+
+// Call this any time the window size or main framebuffer changes
+void ra_gl_ctx_resize(struct ra_swapchain *sw, int w, int h, int fbo);
+
+// These functions are normally set in the ra_swapchain->fns, but if an
+// implementation has a need to override this fns struct with custom functions
+// for whatever reason, these can be used to inherit the original behavior.
+int ra_gl_ctx_color_depth(struct ra_swapchain *sw);
+struct mp_image *ra_gl_ctx_screenshot(struct ra_swapchain *sw);
+bool ra_gl_ctx_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo);
+bool ra_gl_ctx_submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame);
+void ra_gl_ctx_swap_buffers(struct ra_swapchain *sw);
diff --git a/video/out/opengl/context_android.c b/video/out/opengl/context_android.c
new file mode 100644
index 0000000..bc1717c
--- /dev/null
+++ b/video/out/opengl/context_android.c
@@ -0,0 +1,130 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "video/out/android_common.h"
+#include "egl_helpers.h"
+#include "common/common.h"
+#include "context.h"
+
+struct priv {
+ struct GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+};
+
+static void android_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+}
+
+static void android_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ if (p->egl_surface) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ }
+ if (p->egl_context)
+ eglDestroyContext(p->egl_display, p->egl_context);
+
+ vo_android_uninit(ctx->vo);
+}
+
+static bool android_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+
+ if (!vo_android_init(ctx->vo))
+ goto fail;
+
+ p->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(ctx, "EGL failed to initialize.\n");
+ goto fail;
+ }
+
+ EGLConfig config;
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context, &config))
+ goto fail;
+
+ ANativeWindow *native_window = vo_android_native_window(ctx->vo);
+ EGLint format;
+ eglGetConfigAttrib(p->egl_display, config, EGL_NATIVE_VISUAL_ID, &format);
+ ANativeWindow_setBuffersGeometry(native_window, 0, 0, format);
+
+ p->egl_surface = eglCreateWindowSurface(p->egl_display, config,
+ (EGLNativeWindowType)native_window, NULL);
+
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(ctx, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ if (!eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context)) {
+ MP_FATAL(ctx, "Failed to set context!\n");
+ goto fail;
+ }
+
+ mpegl_load_functions(&p->gl, ctx->log);
+
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = android_swap_buffers,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto fail;
+
+ return true;
+fail:
+ android_uninit(ctx);
+ return false;
+}
+
+static bool android_reconfig(struct ra_ctx *ctx)
+{
+ int w, h;
+ if (!vo_android_surface_size(ctx->vo, &w, &h))
+ return false;
+
+ ctx->vo->dwidth = w;
+ ctx->vo->dheight = h;
+ ra_gl_ctx_resize(ctx->swapchain, w, h, 0);
+ return true;
+}
+
+static int android_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ return VO_NOTIMPL;
+}
+
+const struct ra_ctx_fns ra_ctx_android = {
+ .type = "opengl",
+ .name = "android",
+ .reconfig = android_reconfig,
+ .control = android_control,
+ .init = android_init,
+ .uninit = android_uninit,
+};
diff --git a/video/out/opengl/context_angle.c b/video/out/opengl/context_angle.c
new file mode 100644
index 0000000..553718a
--- /dev/null
+++ b/video/out/opengl/context_angle.c
@@ -0,0 +1,653 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <EGL/eglext_angle.h>
+#include <d3d11.h>
+#include <dxgi1_2.h>
+#include <dwmapi.h>
+
+#include "angle_dynamic.h"
+#include "egl_helpers.h"
+#include "video/out/gpu/d3d11_helpers.h"
+
+#include "common/common.h"
+#include "options/m_config.h"
+#include "video/out/w32_common.h"
+#include "osdep/windows_utils.h"
+#include "context.h"
+#include "utils.h"
+
+#ifndef EGL_D3D_TEXTURE_ANGLE
+#define EGL_D3D_TEXTURE_ANGLE 0x33A3
+#endif
+#ifndef EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE
+#define EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE 0x33A7
+#define EGL_SURFACE_ORIENTATION_ANGLE 0x33A8
+#define EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE 0x0002
+#endif
+
+enum {
+ RENDERER_AUTO,
+ RENDERER_D3D9,
+ RENDERER_D3D11,
+};
+
+struct angle_opts {
+ int renderer;
+ int d3d11_warp;
+ int d3d11_feature_level;
+ int egl_windowing;
+ bool flip;
+};
+
+#define OPT_BASE_STRUCT struct angle_opts
+const struct m_sub_options angle_conf = {
+ .opts = (const struct m_option[]) {
+ {"angle-renderer", OPT_CHOICE(renderer,
+ {"auto", RENDERER_AUTO},
+ {"d3d9", RENDERER_D3D9},
+ {"d3d11", RENDERER_D3D11})},
+ {"angle-d3d11-warp", OPT_CHOICE(d3d11_warp,
+ {"auto", -1},
+ {"no", 0},
+ {"yes", 1})},
+ {"angle-d3d11-feature-level", OPT_CHOICE(d3d11_feature_level,
+ {"11_0", D3D_FEATURE_LEVEL_11_0},
+ {"10_1", D3D_FEATURE_LEVEL_10_1},
+ {"10_0", D3D_FEATURE_LEVEL_10_0},
+ {"9_3", D3D_FEATURE_LEVEL_9_3})},
+ {"angle-egl-windowing", OPT_CHOICE(egl_windowing,
+ {"auto", -1},
+ {"no", 0},
+ {"yes", 1})},
+ {"angle-flip", OPT_BOOL(flip)},
+ {0}
+ },
+ .defaults = &(const struct angle_opts) {
+ .renderer = RENDERER_AUTO,
+ .d3d11_warp = -1,
+ .d3d11_feature_level = D3D_FEATURE_LEVEL_11_0,
+ .egl_windowing = -1,
+ .flip = true,
+ },
+ .size = sizeof(struct angle_opts),
+};
+
+struct priv {
+ GL gl;
+
+ IDXGISwapChain *dxgi_swapchain;
+
+ ID3D11Device *d3d11_device;
+ ID3D11DeviceContext *d3d11_context;
+ ID3D11Texture2D *d3d11_backbuffer;
+
+ EGLConfig egl_config;
+ EGLDisplay egl_display;
+ EGLDeviceEXT egl_device;
+ EGLContext egl_context;
+ EGLSurface egl_window; // For the EGL windowing surface only
+ EGLSurface egl_backbuffer; // For the DXGI swap chain based surface
+
+ int sc_width, sc_height; // Swap chain width and height
+ int swapinterval;
+ bool flipped;
+
+ struct angle_opts *opts;
+};
+
+static __thread struct ra_ctx *current_ctx;
+
+static void update_sizes(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ p->sc_width = ctx->vo->dwidth ? ctx->vo->dwidth : 1;
+ p->sc_height = ctx->vo->dheight ? ctx->vo->dheight : 1;
+}
+
+static void d3d11_backbuffer_release(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->egl_backbuffer) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroySurface(p->egl_display, p->egl_backbuffer);
+ }
+ p->egl_backbuffer = EGL_NO_SURFACE;
+
+ SAFE_RELEASE(p->d3d11_backbuffer);
+}
+
+static bool d3d11_backbuffer_get(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ HRESULT hr;
+
+ hr = IDXGISwapChain_GetBuffer(p->dxgi_swapchain, 0, &IID_ID3D11Texture2D,
+ (void**)&p->d3d11_backbuffer);
+ if (FAILED(hr)) {
+ MP_FATAL(vo, "Couldn't get swap chain back buffer\n");
+ return false;
+ }
+
+ EGLint pbuffer_attributes[] = {
+ EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
+ EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
+ EGL_NONE,
+ };
+ p->egl_backbuffer = eglCreatePbufferFromClientBuffer(p->egl_display,
+ EGL_D3D_TEXTURE_ANGLE, p->d3d11_backbuffer, p->egl_config,
+ pbuffer_attributes);
+ if (!p->egl_backbuffer) {
+ MP_FATAL(vo, "Couldn't create EGL pbuffer\n");
+ return false;
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_backbuffer, p->egl_backbuffer,
+ p->egl_context);
+ return true;
+}
+
+static void d3d11_backbuffer_resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ HRESULT hr;
+
+ int old_sc_width = p->sc_width;
+ int old_sc_height = p->sc_height;
+
+ update_sizes(ctx);
+ // Avoid unnecessary resizing
+ if (old_sc_width == p->sc_width && old_sc_height == p->sc_height)
+ return;
+
+ // All references to backbuffers must be released before ResizeBuffers
+ // (including references held by ANGLE)
+ d3d11_backbuffer_release(ctx);
+
+ // The DirectX runtime may report errors related to the device like
+ // DXGI_ERROR_DEVICE_REMOVED at this point
+ hr = IDXGISwapChain_ResizeBuffers(p->dxgi_swapchain, 0, p->sc_width,
+ p->sc_height, DXGI_FORMAT_UNKNOWN, 0);
+ if (FAILED(hr))
+ MP_FATAL(vo, "Couldn't resize swapchain: %s\n", mp_HRESULT_to_str(hr));
+
+ if (!d3d11_backbuffer_get(ctx))
+ MP_FATAL(vo, "Couldn't get back buffer after resize\n");
+}
+
+static void d3d11_device_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ PFNEGLRELEASEDEVICEANGLEPROC eglReleaseDeviceANGLE =
+ (PFNEGLRELEASEDEVICEANGLEPROC)eglGetProcAddress("eglReleaseDeviceANGLE");
+
+ if (p->egl_display)
+ eglTerminate(p->egl_display);
+ p->egl_display = EGL_NO_DISPLAY;
+
+ if (p->egl_device && eglReleaseDeviceANGLE)
+ eglReleaseDeviceANGLE(p->egl_device);
+ p->egl_device = 0;
+
+ SAFE_RELEASE(p->d3d11_device);
+}
+
+static bool d3d11_device_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ struct angle_opts *o = p->opts;
+
+ struct d3d11_device_opts device_opts = {
+ .allow_warp = o->d3d11_warp != 0,
+ .force_warp = o->d3d11_warp == 1,
+ .max_feature_level = o->d3d11_feature_level,
+ .min_feature_level = D3D_FEATURE_LEVEL_9_3,
+ .max_frame_latency = ctx->vo->opts->swapchain_depth,
+ };
+ if (!mp_d3d11_create_present_device(vo->log, &device_opts, &p->d3d11_device))
+ return false;
+ ID3D11Device_GetImmediateContext(p->d3d11_device, &p->d3d11_context);
+
+ PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
+ (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
+ if (!eglGetPlatformDisplayEXT) {
+ MP_FATAL(vo, "Missing EGL_EXT_platform_base\n");
+ return false;
+ }
+ PFNEGLCREATEDEVICEANGLEPROC eglCreateDeviceANGLE =
+ (PFNEGLCREATEDEVICEANGLEPROC)eglGetProcAddress("eglCreateDeviceANGLE");
+ if (!eglCreateDeviceANGLE) {
+ MP_FATAL(vo, "Missing EGL_EXT_platform_device\n");
+ return false;
+ }
+
+ p->egl_device = eglCreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE,
+ p->d3d11_device, NULL);
+ if (!p->egl_device) {
+ MP_FATAL(vo, "Couldn't create EGL device\n");
+ return false;
+ }
+
+ p->egl_display = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT,
+ p->egl_device, NULL);
+ if (!p->egl_display) {
+ MP_FATAL(vo, "Couldn't get EGL display\n");
+ return false;
+ }
+
+ return true;
+}
+
+static void d3d11_swapchain_surface_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ bool had_swapchain = p->dxgi_swapchain;
+ SAFE_RELEASE(p->dxgi_swapchain);
+ d3d11_backbuffer_release(ctx);
+
+ // Ensure the swapchain is destroyed by flushing the D3D11 immediate
+ // context. This is needed because the HWND may be reused. See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ff476425.aspx
+ if (had_swapchain && p->d3d11_context)
+ ID3D11DeviceContext_Flush(p->d3d11_context);
+}
+
+static bool d3d11_swapchain_surface_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ struct angle_opts *o = p->opts;
+
+ if (!p->d3d11_device)
+ goto fail;
+
+ update_sizes(ctx);
+ struct d3d11_swapchain_opts swapchain_opts = {
+ .window = vo_w32_hwnd(vo),
+ .width = p->sc_width,
+ .height = p->sc_height,
+ .flip = o->flip,
+ // Add one frame for the backbuffer and one frame of "slack" to reduce
+ // contention with the window manager when acquiring the backbuffer
+ .length = ctx->vo->opts->swapchain_depth + 2,
+ .usage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT,
+ };
+ if (!mp_d3d11_create_swapchain(p->d3d11_device, vo->log, &swapchain_opts,
+ &p->dxgi_swapchain))
+ goto fail;
+ if (!d3d11_backbuffer_get(ctx))
+ goto fail;
+
+ p->flipped = true;
+ return true;
+
+fail:
+ d3d11_swapchain_surface_destroy(ctx);
+ return false;
+}
+
+static void d3d9_device_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->egl_display)
+ eglTerminate(p->egl_display);
+ p->egl_display = EGL_NO_DISPLAY;
+}
+
+static bool d3d9_device_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
+ (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
+ if (!eglGetPlatformDisplayEXT) {
+ MP_FATAL(vo, "Missing EGL_EXT_platform_base\n");
+ return false;
+ }
+
+ EGLint display_attributes[] = {
+ EGL_PLATFORM_ANGLE_TYPE_ANGLE,
+ EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE,
+ EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE,
+ EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE,
+ EGL_NONE,
+ };
+ p->egl_display = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE,
+ EGL_DEFAULT_DISPLAY, display_attributes);
+ if (p->egl_display == EGL_NO_DISPLAY) {
+ MP_FATAL(vo, "Couldn't get display\n");
+ return false;
+ }
+
+ return true;
+}
+
+static void egl_window_surface_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->egl_window) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ }
+}
+
+static bool egl_window_surface_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ int window_attribs_len = 0;
+ EGLint *window_attribs = NULL;
+
+ EGLint flip_val;
+ if (eglGetConfigAttrib(p->egl_display, p->egl_config,
+ EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE, &flip_val))
+ {
+ if (flip_val == EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE) {
+ MP_TARRAY_APPEND(NULL, window_attribs, window_attribs_len,
+ EGL_SURFACE_ORIENTATION_ANGLE);
+ MP_TARRAY_APPEND(NULL, window_attribs, window_attribs_len,
+ EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE);
+ p->flipped = true;
+ MP_VERBOSE(vo, "Rendering flipped.\n");
+ }
+ }
+
+ MP_TARRAY_APPEND(NULL, window_attribs, window_attribs_len, EGL_NONE);
+ p->egl_window = eglCreateWindowSurface(p->egl_display, p->egl_config,
+ vo_w32_hwnd(vo), window_attribs);
+ talloc_free(window_attribs);
+ if (!p->egl_window) {
+ MP_FATAL(vo, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_window, p->egl_window,
+ p->egl_context);
+ return true;
+fail:
+ egl_window_surface_destroy(ctx);
+ return false;
+}
+
+static void context_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->egl_context) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroyContext(p->egl_display, p->egl_context);
+ }
+ p->egl_context = EGL_NO_CONTEXT;
+}
+
+static bool context_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(vo, "Couldn't initialize EGL\n");
+ goto fail;
+ }
+
+ const char *exts = eglQueryString(p->egl_display, EGL_EXTENSIONS);
+ if (exts)
+ MP_DBG(vo, "EGL extensions: %s\n", exts);
+
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context,
+ &p->egl_config))
+ {
+ MP_FATAL(vo, "Could not create EGL context!\n");
+ goto fail;
+ }
+
+ return true;
+fail:
+ context_destroy(ctx);
+ return false;
+}
+
+static void angle_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_gl_ctx_uninit(ctx);
+
+ DwmEnableMMCSS(FALSE);
+
+ // Uninit the EGL surface implementation that is being used. Note: This may
+ // result in the *_destroy function being called twice since it is also
+ // called when the surface create function fails. This is fine because the
+ // *_destroy functions are idempotent.
+ if (p->dxgi_swapchain)
+ d3d11_swapchain_surface_destroy(ctx);
+ else
+ egl_window_surface_destroy(ctx);
+
+ context_destroy(ctx);
+
+ // Uninit the EGL device implementation that is being used
+ if (p->d3d11_device)
+ d3d11_device_destroy(ctx);
+ else
+ d3d9_device_destroy(ctx);
+
+ vo_w32_uninit(ctx->vo);
+}
+
+static int GLAPIENTRY angle_swap_interval(int interval)
+{
+ if (!current_ctx)
+ return 0;
+ struct priv *p = current_ctx->priv;
+
+ if (p->dxgi_swapchain) {
+ p->swapinterval = MPCLAMP(interval, 0, 4);
+ return 1;
+ } else {
+ return eglSwapInterval(p->egl_display, interval);
+ }
+}
+
+static void d3d11_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ // Calling Present() on a flip-sequential swap chain will silently change
+ // the underlying storage of the back buffer to point to the next buffer in
+ // the chain. This results in the RTVs for the back buffer becoming
+ // unbound. Since ANGLE doesn't know we called Present(), it will continue
+ // using the unbound RTVs, so we must save and restore them ourselves.
+ ID3D11RenderTargetView *rtvs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {0};
+ ID3D11DepthStencilView *dsv = NULL;
+ ID3D11DeviceContext_OMGetRenderTargets(p->d3d11_context,
+ MP_ARRAY_SIZE(rtvs), rtvs, &dsv);
+
+ HRESULT hr = IDXGISwapChain_Present(p->dxgi_swapchain, p->swapinterval, 0);
+ if (FAILED(hr))
+ MP_FATAL(ctx->vo, "Couldn't present: %s\n", mp_HRESULT_to_str(hr));
+
+ // Restore the RTVs and release the objects
+ ID3D11DeviceContext_OMSetRenderTargets(p->d3d11_context,
+ MP_ARRAY_SIZE(rtvs), rtvs, dsv);
+ for (int i = 0; i < MP_ARRAY_SIZE(rtvs); i++)
+ SAFE_RELEASE(rtvs[i]);
+ SAFE_RELEASE(dsv);
+}
+
+static void egl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ eglSwapBuffers(p->egl_display, p->egl_window);
+}
+
+static void angle_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->dxgi_swapchain)
+ d3d11_swap_buffers(ctx);
+ else
+ egl_swap_buffers(ctx);
+}
+
+
+static int angle_color_depth(struct ra_swapchain *sw)
+{
+ // Only 8-bit output is supported at the moment
+ return 8;
+}
+
+static bool angle_submit_frame(struct ra_swapchain *sw,
+ const struct vo_frame *frame)
+{
+ struct priv *p = sw->ctx->priv;
+ bool ret = ra_gl_ctx_submit_frame(sw, frame);
+ if (p->d3d11_context) {
+ // DXGI Present doesn't flush the immediate context, which can make
+ // timers inaccurate, since the end queries might not be sent until the
+ // next frame. Fix this by flushing the context now.
+ ID3D11DeviceContext_Flush(p->d3d11_context);
+ }
+ return ret;
+}
+
+static bool angle_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo *vo = ctx->vo;
+ GL *gl = &p->gl;
+
+ p->opts = mp_get_config_group(ctx, ctx->global, &angle_conf);
+ struct angle_opts *o = p->opts;
+
+ if (!angle_load()) {
+ MP_VERBOSE(vo, "Failed to load LIBEGL.DLL\n");
+ goto fail;
+ }
+
+ // Create the underlying EGL device implementation
+ bool context_ok = false;
+ if ((!context_ok && !o->renderer) || o->renderer == RENDERER_D3D11) {
+ context_ok = d3d11_device_create(ctx);
+ if (context_ok) {
+ context_ok = context_init(ctx);
+ if (!context_ok)
+ d3d11_device_destroy(ctx);
+ }
+ }
+ if ((!context_ok && !o->renderer) || o->renderer == RENDERER_D3D9) {
+ context_ok = d3d9_device_create(ctx);
+ if (context_ok) {
+ MP_VERBOSE(vo, "Using Direct3D 9\n");
+
+ context_ok = context_init(ctx);
+ if (!context_ok)
+ d3d9_device_destroy(ctx);
+ }
+ }
+ if (!context_ok)
+ goto fail;
+
+ if (!vo_w32_init(vo))
+ goto fail;
+
+ // Create the underlying EGL surface implementation
+ bool surface_ok = false;
+ if ((!surface_ok && o->egl_windowing == -1) || o->egl_windowing == 0) {
+ surface_ok = d3d11_swapchain_surface_create(ctx);
+ }
+ if ((!surface_ok && o->egl_windowing == -1) || o->egl_windowing == 1) {
+ surface_ok = egl_window_surface_create(ctx);
+ if (surface_ok)
+ MP_VERBOSE(vo, "Using EGL windowing\n");
+ }
+ if (!surface_ok)
+ goto fail;
+
+ mpegl_load_functions(gl, vo->log);
+
+ current_ctx = ctx;
+ gl->SwapInterval = angle_swap_interval;
+
+ // Custom swapchain impl for the D3D11 swapchain-based surface
+ static const struct ra_swapchain_fns dxgi_swapchain_fns = {
+ .color_depth = angle_color_depth,
+ .submit_frame = angle_submit_frame,
+ };
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = angle_swap_buffers,
+ .external_swapchain = p->dxgi_swapchain ? &dxgi_swapchain_fns : NULL,
+ };
+
+ gl->flipped = p->flipped;
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto fail;
+
+ DwmEnableMMCSS(TRUE); // DWM MMCSS cargo-cult. The dxgl backend also does this.
+
+ return true;
+fail:
+ angle_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->dxgi_swapchain)
+ d3d11_backbuffer_resize(ctx);
+ else
+ eglWaitClient(); // Should get ANGLE to resize its swapchain
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool angle_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int angle_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_angle = {
+ .type = "opengl",
+ .name = "angle",
+ .init = angle_init,
+ .reconfig = angle_reconfig,
+ .control = angle_control,
+ .uninit = angle_uninit,
+};
diff --git a/video/out/opengl/context_drm_egl.c b/video/out/opengl/context_drm_egl.c
new file mode 100644
index 0000000..2db428f
--- /dev/null
+++ b/video/out/opengl/context_drm_egl.c
@@ -0,0 +1,744 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <string.h>
+#include <poll.h>
+#include <unistd.h>
+
+#include <gbm.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <drm_fourcc.h>
+
+#include "libmpv/render_gl.h"
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "video/out/drm_atomic.h"
+#include "video/out/drm_common.h"
+#include "video/out/present_sync.h"
+
+#include "egl_helpers.h"
+#include "common.h"
+#include "context.h"
+
+#ifndef EGL_PLATFORM_GBM_MESA
+#define EGL_PLATFORM_GBM_MESA 0x31D7
+#endif
+
+#ifndef EGL_PLATFORM_GBM_KHR
+#define EGL_PLATFORM_GBM_KHR 0x31D7
+#endif
+
+struct gbm_frame {
+ struct gbm_bo *bo;
+};
+
+struct gbm {
+ struct gbm_surface *surface;
+ struct gbm_device *device;
+ struct gbm_frame **bo_queue;
+ unsigned int num_bos;
+};
+
+struct egl {
+ EGLDisplay display;
+ EGLContext context;
+ EGLSurface surface;
+};
+
+struct priv {
+ GL gl;
+
+ struct egl egl;
+ struct gbm gbm;
+
+ GLsync *vsync_fences;
+ unsigned int num_vsync_fences;
+
+ uint32_t gbm_format;
+ uint64_t *gbm_modifiers;
+ unsigned int num_gbm_modifiers;
+
+ struct mpv_opengl_drm_params_v2 drm_params;
+ struct mpv_opengl_drm_draw_surface_size draw_surface_size;
+};
+
+// Not general. Limited to only the formats being used in this module
+static const char *gbm_format_to_string(uint32_t format)
+{
+ switch (format) {
+ case GBM_FORMAT_XRGB8888:
+ return "GBM_FORMAT_XRGB8888";
+ case GBM_FORMAT_ARGB8888:
+ return "GBM_FORMAT_ARGB8888";
+ case GBM_FORMAT_XBGR8888:
+ return "GBM_FORMAT_XBGR8888";
+ case GBM_FORMAT_ABGR8888:
+ return "GBM_FORMAT_ABGR8888";
+ case GBM_FORMAT_XRGB2101010:
+ return "GBM_FORMAT_XRGB2101010";
+ case GBM_FORMAT_ARGB2101010:
+ return "GBM_FORMAT_ARGB2101010";
+ case GBM_FORMAT_XBGR2101010:
+ return "GBM_FORMAT_XBGR2101010";
+ case GBM_FORMAT_ABGR2101010:
+ return "GBM_FORMAT_ABGR2101010";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+// Allow falling back to an ARGB EGLConfig when we have an XRGB framebuffer.
+// Also allow falling back to an XRGB EGLConfig for ARGB framebuffers, since
+// this seems necessary to work with broken Mali drivers that don't report
+// their EGLConfigs as supporting alpha properly.
+static uint32_t fallback_format_for(uint32_t format)
+{
+ switch (format) {
+ case GBM_FORMAT_XRGB8888:
+ return GBM_FORMAT_ARGB8888;
+ case GBM_FORMAT_ARGB8888:
+ return GBM_FORMAT_XRGB8888;
+ case GBM_FORMAT_XBGR8888:
+ return GBM_FORMAT_ABGR8888;
+ case GBM_FORMAT_ABGR8888:
+ return GBM_FORMAT_XBGR8888;
+ case GBM_FORMAT_XRGB2101010:
+ return GBM_FORMAT_ARGB2101010;
+ case GBM_FORMAT_ARGB2101010:
+ return GBM_FORMAT_XRGB2101010;
+ case GBM_FORMAT_XBGR2101010:
+ return GBM_FORMAT_ABGR2101010;
+ case GBM_FORMAT_ABGR2101010:
+ return GBM_FORMAT_XBGR2101010;
+ default:
+ return 0;
+ }
+}
+
+static int match_config_to_visual(void *user_data, EGLConfig *configs, int num_configs)
+{
+ struct ra_ctx *ctx = (struct ra_ctx*)user_data;
+ struct priv *p = ctx->priv;
+ const EGLint visual_id[] = {
+ (EGLint)p->gbm_format,
+ (EGLint)fallback_format_for(p->gbm_format),
+ 0
+ };
+
+ for (unsigned int i = 0; visual_id[i] != 0; ++i) {
+ MP_VERBOSE(ctx, "Attempting to find EGLConfig matching %s\n",
+ gbm_format_to_string(visual_id[i]));
+ for (unsigned int j = 0; j < num_configs; ++j) {
+ EGLint id;
+
+ if (!eglGetConfigAttrib(p->egl.display, configs[j], EGL_NATIVE_VISUAL_ID, &id))
+ continue;
+
+ if (visual_id[i] == id) {
+ MP_VERBOSE(ctx, "Found matching EGLConfig for %s\n",
+ gbm_format_to_string(visual_id[i]));
+ return j;
+ }
+ }
+ MP_VERBOSE(ctx, "No matching EGLConfig for %s\n", gbm_format_to_string(visual_id[i]));
+ }
+
+ MP_ERR(ctx, "Could not find EGLConfig matching the GBM visual (%s).\n",
+ gbm_format_to_string(p->gbm_format));
+ return -1;
+}
+
+static EGLDisplay egl_get_display(struct gbm_device *gbm_device)
+{
+ EGLDisplay ret;
+
+ ret = mpegl_get_display(EGL_PLATFORM_GBM_MESA, "EGL_MESA_platform_gbm", gbm_device);
+ if (ret != EGL_NO_DISPLAY)
+ return ret;
+
+ ret = mpegl_get_display(EGL_PLATFORM_GBM_KHR, "EGL_KHR_platform_gbm", gbm_device);
+ if (ret != EGL_NO_DISPLAY)
+ return ret;
+
+ return eglGetDisplay(gbm_device);
+}
+
+static bool init_egl(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ MP_VERBOSE(ctx, "Initializing EGL\n");
+ p->egl.display = egl_get_display(p->gbm.device);
+
+ if (p->egl.display == EGL_NO_DISPLAY) {
+ MP_ERR(ctx, "Failed to get EGL display.\n");
+ return false;
+ }
+ if (!eglInitialize(p->egl.display, NULL, NULL)) {
+ MP_ERR(ctx, "Failed to initialize EGL.\n");
+ return false;
+ }
+ EGLConfig config;
+ if (!mpegl_create_context_cb(ctx,
+ p->egl.display,
+ (struct mpegl_cb){match_config_to_visual, ctx},
+ &p->egl.context,
+ &config))
+ return false;
+
+ MP_VERBOSE(ctx, "Initializing EGL surface\n");
+ p->egl.surface = mpegl_create_window_surface(
+ p->egl.display, config, p->gbm.surface);
+ if (p->egl.surface == EGL_NO_SURFACE) {
+ p->egl.surface = eglCreateWindowSurface(
+ p->egl.display, config, p->gbm.surface, NULL);
+ }
+ if (p->egl.surface == EGL_NO_SURFACE) {
+ MP_ERR(ctx, "Failed to create EGL surface.\n");
+ return false;
+ }
+ return true;
+}
+
+static bool init_gbm(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ MP_VERBOSE(ctx->vo, "Creating GBM device\n");
+ p->gbm.device = gbm_create_device(drm->fd);
+ if (!p->gbm.device) {
+ MP_ERR(ctx->vo, "Failed to create GBM device.\n");
+ return false;
+ }
+
+ MP_VERBOSE(ctx->vo, "Initializing GBM surface (%d x %d)\n",
+ p->draw_surface_size.width, p->draw_surface_size.height);
+ if (p->num_gbm_modifiers == 0) {
+ p->gbm.surface = gbm_surface_create(
+ p->gbm.device,
+ p->draw_surface_size.width,
+ p->draw_surface_size.height,
+ p->gbm_format,
+ GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+ } else {
+ p->gbm.surface = gbm_surface_create_with_modifiers(
+ p->gbm.device,
+ p->draw_surface_size.width,
+ p->draw_surface_size.height,
+ p->gbm_format,
+ p->gbm_modifiers,
+ p->num_gbm_modifiers);
+ }
+ if (!p->gbm.surface) {
+ MP_ERR(ctx->vo, "Failed to create GBM surface.\n");
+ return false;
+ }
+ return true;
+}
+
+static void framebuffer_destroy_callback(struct gbm_bo *bo, void *data)
+{
+ struct framebuffer *fb = data;
+ if (fb) {
+ drmModeRmFB(fb->fd, fb->id);
+ }
+}
+
+static void update_framebuffer_from_bo(struct ra_ctx *ctx, struct gbm_bo *bo)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ struct framebuffer *fb = gbm_bo_get_user_data(bo);
+ if (fb) {
+ drm->fb = fb;
+ return;
+ }
+
+ fb = talloc_zero(ctx, struct framebuffer);
+ fb->fd = drm->fd;
+ fb->width = gbm_bo_get_width(bo);
+ fb->height = gbm_bo_get_height(bo);
+ uint64_t modifier = gbm_bo_get_modifier(bo);
+
+ int ret;
+ if (p->num_gbm_modifiers == 0 || modifier == DRM_FORMAT_MOD_INVALID) {
+ uint32_t stride = gbm_bo_get_stride(bo);
+ uint32_t handle = gbm_bo_get_handle(bo).u32;
+ ret = drmModeAddFB2(fb->fd, fb->width, fb->height,
+ p->gbm_format,
+ (uint32_t[4]){handle, 0, 0, 0},
+ (uint32_t[4]){stride, 0, 0, 0},
+ (uint32_t[4]){0, 0, 0, 0},
+ &fb->id, 0);
+ } else {
+ MP_VERBOSE(ctx, "GBM surface using modifier 0x%"PRIX64"\n", modifier);
+
+ uint32_t handles[4] = {0};
+ uint32_t strides[4] = {0};
+ uint32_t offsets[4] = {0};
+ uint64_t modifiers[4] = {0};
+
+ const int num_planes = gbm_bo_get_plane_count(bo);
+ for (int i = 0; i < num_planes; ++i) {
+ handles[i] = gbm_bo_get_handle_for_plane(bo, i).u32;
+ strides[i] = gbm_bo_get_stride_for_plane(bo, i);
+ offsets[i] = gbm_bo_get_offset(bo, i);
+ modifiers[i] = modifier;
+ }
+
+ ret = drmModeAddFB2WithModifiers(fb->fd, fb->width, fb->height,
+ p->gbm_format,
+ handles, strides, offsets, modifiers,
+ &fb->id, DRM_MODE_FB_MODIFIERS);
+ }
+ if (ret) {
+ MP_ERR(ctx->vo, "Failed to create framebuffer: %s\n", mp_strerror(errno));
+ }
+ gbm_bo_set_user_data(bo, fb, framebuffer_destroy_callback);
+ drm->fb = fb;
+}
+
+static void queue_flip(struct ra_ctx *ctx, struct gbm_frame *frame)
+{
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ update_framebuffer_from_bo(ctx, frame->bo);
+
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+ drm_object_set_property(atomic_ctx->request, atomic_ctx->draw_plane, "FB_ID", drm->fb->id);
+ drm_object_set_property(atomic_ctx->request, atomic_ctx->draw_plane, "CRTC_ID", atomic_ctx->crtc->id);
+ drm_object_set_property(atomic_ctx->request, atomic_ctx->draw_plane, "ZPOS", 1);
+
+ int ret = drmModeAtomicCommit(drm->fd, atomic_ctx->request,
+ DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT, drm);
+
+ if (ret)
+ MP_WARN(ctx->vo, "Failed to commit atomic request: %s\n", mp_strerror(ret));
+ drm->waiting_for_flip = !ret;
+
+ drmModeAtomicFree(atomic_ctx->request);
+ atomic_ctx->request = drmModeAtomicAlloc();
+}
+
+static void enqueue_bo(struct ra_ctx *ctx, struct gbm_bo *bo)
+{
+ struct priv *p = ctx->priv;
+
+ struct gbm_frame *new_frame = talloc(p, struct gbm_frame);
+ new_frame->bo = bo;
+ MP_TARRAY_APPEND(p, p->gbm.bo_queue, p->gbm.num_bos, new_frame);
+}
+
+static void dequeue_bo(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ talloc_free(p->gbm.bo_queue[0]);
+ MP_TARRAY_REMOVE_AT(p->gbm.bo_queue, p->gbm.num_bos, 0);
+}
+
+static void swapchain_step(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!(p->gbm.num_bos > 0))
+ return;
+
+ if (p->gbm.bo_queue[0]->bo)
+ gbm_surface_release_buffer(p->gbm.surface, p->gbm.bo_queue[0]->bo);
+ dequeue_bo(ctx);
+}
+
+static void new_fence(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->gl.FenceSync) {
+ GLsync fence = p->gl.FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if (fence)
+ MP_TARRAY_APPEND(p, p->vsync_fences, p->num_vsync_fences, fence);
+ }
+}
+
+static void wait_fence(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ while (p->num_vsync_fences && (p->num_vsync_fences >= p->gbm.num_bos)) {
+ p->gl.ClientWaitSync(p->vsync_fences[0], GL_SYNC_FLUSH_COMMANDS_BIT, 1e9);
+ p->gl.DeleteSync(p->vsync_fences[0]);
+ MP_TARRAY_REMOVE_AT(p->vsync_fences, p->num_vsync_fences, 0);
+ }
+}
+
+static bool drm_egl_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct ra_ctx *ctx = sw->ctx;
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ if (!drm->atomic_context->request) {
+ drm->atomic_context->request = drmModeAtomicAlloc();
+ p->drm_params.atomic_request_ptr = &drm->atomic_context->request;
+ }
+
+ return ra_gl_ctx_start_frame(sw, out_fbo);
+}
+
+static bool drm_egl_submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame)
+{
+ struct ra_ctx *ctx = sw->ctx;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ drm->still = frame->still;
+
+ return ra_gl_ctx_submit_frame(sw, frame);
+}
+
+static void drm_egl_swap_buffers(struct ra_swapchain *sw)
+{
+ struct ra_ctx *ctx = sw->ctx;
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ const bool drain = drm->paused || drm->still; // True when we need to drain the swapchain
+
+ if (!drm->active)
+ return;
+
+ wait_fence(ctx);
+
+ eglSwapBuffers(p->egl.display, p->egl.surface);
+
+ struct gbm_bo *new_bo = gbm_surface_lock_front_buffer(p->gbm.surface);
+ if (!new_bo) {
+ MP_ERR(ctx->vo, "Couldn't lock front buffer\n");
+ return;
+ }
+ enqueue_bo(ctx, new_bo);
+ new_fence(ctx);
+
+ while (drain || p->gbm.num_bos > ctx->vo->opts->swapchain_depth ||
+ !gbm_surface_has_free_buffers(p->gbm.surface)) {
+ if (drm->waiting_for_flip) {
+ vo_drm_wait_on_flip(drm);
+ swapchain_step(ctx);
+ }
+ if (p->gbm.num_bos <= 1)
+ break;
+ if (!p->gbm.bo_queue[1] || !p->gbm.bo_queue[1]->bo) {
+ MP_ERR(ctx->vo, "Hole in swapchain?\n");
+ swapchain_step(ctx);
+ continue;
+ }
+ queue_flip(ctx, p->gbm.bo_queue[1]);
+ }
+}
+
+static const struct ra_swapchain_fns drm_egl_swapchain = {
+ .start_frame = drm_egl_start_frame,
+ .submit_frame = drm_egl_submit_frame,
+ .swap_buffers = drm_egl_swap_buffers,
+};
+
+static void drm_egl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ if (drm) {
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+
+ if (drmModeAtomicCommit(drm->fd, atomic_ctx->request, 0, NULL))
+ MP_ERR(ctx->vo, "Failed to commit atomic request: %s\n",
+ mp_strerror(errno));
+
+ drmModeAtomicFree(atomic_ctx->request);
+ }
+
+ ra_gl_ctx_uninit(ctx);
+ vo_drm_uninit(ctx->vo);
+
+ if (p) {
+ // According to GBM documentation all BO:s must be released
+ // before gbm_surface_destroy can be called on the surface.
+ while (p->gbm.num_bos) {
+ swapchain_step(ctx);
+ }
+
+ eglMakeCurrent(p->egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ if (p->egl.display != EGL_NO_DISPLAY) {
+ eglDestroySurface(p->egl.display, p->egl.surface);
+ eglDestroyContext(p->egl.display, p->egl.context);
+ }
+ if (p->gbm.surface)
+ gbm_surface_destroy(p->gbm.surface);
+ eglTerminate(p->egl.display);
+ gbm_device_destroy(p->gbm.device);
+
+ if (p->drm_params.render_fd != -1)
+ close(p->drm_params.render_fd);
+ }
+}
+
+// If the draw plane supports ARGB we want to use that, but if it doesn't we
+// fall back on XRGB. If we do not have atomic there is no particular reason to
+// be using ARGB (drmprime hwdec will not work without atomic, anyway), so we
+// fall back to XRGB (another reason is that we do not have the convenient
+// atomic_ctx and its convenient plane fields).
+static bool probe_gbm_format(struct ra_ctx *ctx, uint32_t argb_format, uint32_t xrgb_format)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ drmModePlane *drmplane = drmModeGetPlane(drm->fd, drm->atomic_context->draw_plane->id);
+ bool have_argb = false;
+ bool have_xrgb = false;
+ bool result = false;
+ for (unsigned int i = 0; i < drmplane->count_formats; ++i) {
+ if (drmplane->formats[i] == argb_format) {
+ have_argb = true;
+ } else if (drmplane->formats[i] == xrgb_format) {
+ have_xrgb = true;
+ }
+ }
+
+ if (have_argb) {
+ p->gbm_format = argb_format;
+ MP_VERBOSE(ctx->vo, "%s supported by draw plane.\n", gbm_format_to_string(argb_format));
+ result = true;
+ } else if (have_xrgb) {
+ p->gbm_format = xrgb_format;
+ MP_VERBOSE(ctx->vo, "%s not supported by draw plane: Falling back to %s.\n",
+ gbm_format_to_string(argb_format), gbm_format_to_string(xrgb_format));
+ result = true;
+ }
+
+ drmModeFreePlane(drmplane);
+ return result;
+}
+
+static bool probe_gbm_modifiers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ drmModePropertyBlobPtr blob = drm_object_get_property_blob(drm->atomic_context->draw_plane,
+ "IN_FORMATS");
+ if (!blob) {
+ MP_VERBOSE(ctx->vo, "Failed to find IN_FORMATS property\n");
+ return false;
+ }
+
+ struct drm_format_modifier_blob *data = blob->data;
+ uint32_t *fmts = (uint32_t *)((char *)data + data->formats_offset);
+ struct drm_format_modifier *mods =
+ (struct drm_format_modifier *)((char *)data + data->modifiers_offset);
+
+ for (unsigned int j = 0; j < data->count_modifiers; ++j) {
+ struct drm_format_modifier *mod = &mods[j];
+ for (uint64_t k = 0; k < 64; ++k) {
+ if (mod->formats & (1ull << k)) {
+ uint32_t fmt = fmts[k + mod->offset];
+ if (fmt == p->gbm_format) {
+ MP_TARRAY_APPEND(p, p->gbm_modifiers,
+ p->num_gbm_modifiers, mod->modifier);
+ MP_VERBOSE(ctx->vo, "Supported modifier: 0x%"PRIX64"\n",
+ (uint64_t)mod->modifier);
+ break;
+ }
+ }
+ }
+ }
+ drmModeFreePropertyBlob(blob);
+
+ if (p->num_gbm_modifiers == 0) {
+ MP_VERBOSE(ctx->vo, "No supported DRM modifiers found.\n");
+ }
+ return true;
+}
+
+static void drm_egl_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_drm_state *drm = ctx->vo->drm;
+ present_sync_get_info(drm->present, info);
+}
+
+static bool drm_egl_init(struct ra_ctx *ctx)
+{
+ if (!vo_drm_init(ctx->vo))
+ goto err;
+
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ if (ctx->vo->drm->opts->draw_surface_size.wh_valid) {
+ p->draw_surface_size.width = ctx->vo->drm->opts->draw_surface_size.w;
+ p->draw_surface_size.height = ctx->vo->drm->opts->draw_surface_size.h;
+ } else {
+ p->draw_surface_size.width = drm->mode.mode.hdisplay;
+ p->draw_surface_size.height = drm->mode.mode.vdisplay;
+ }
+
+ drm->width = p->draw_surface_size.width;
+ drm->height = p->draw_surface_size.height;
+
+ uint32_t argb_format;
+ uint32_t xrgb_format;
+ switch (ctx->vo->drm->opts->drm_format) {
+ case DRM_OPTS_FORMAT_XRGB2101010:
+ argb_format = GBM_FORMAT_ARGB2101010;
+ xrgb_format = GBM_FORMAT_XRGB2101010;
+ break;
+ case DRM_OPTS_FORMAT_XBGR2101010:
+ argb_format = GBM_FORMAT_ABGR2101010;
+ xrgb_format = GBM_FORMAT_XBGR2101010;
+ break;
+ case DRM_OPTS_FORMAT_XBGR8888:
+ argb_format = GBM_FORMAT_ABGR8888;
+ xrgb_format = GBM_FORMAT_XBGR8888;
+ break;
+ default:
+ argb_format = GBM_FORMAT_ARGB8888;
+ xrgb_format = GBM_FORMAT_XRGB8888;
+ break;
+ }
+
+ if (!probe_gbm_format(ctx, argb_format, xrgb_format)) {
+ MP_ERR(ctx->vo, "No suitable format found on draw plane (tried: %s and %s).\n",
+ gbm_format_to_string(argb_format), gbm_format_to_string(xrgb_format));
+ goto err;
+ }
+
+ // It is not fatal if this fails. We'll just try without modifiers.
+ probe_gbm_modifiers(ctx);
+
+ if (!init_gbm(ctx)) {
+ MP_ERR(ctx->vo, "Failed to setup GBM.\n");
+ goto err;
+ }
+
+ if (!init_egl(ctx)) {
+ MP_ERR(ctx->vo, "Failed to setup EGL.\n");
+ goto err;
+ }
+
+ if (!eglMakeCurrent(p->egl.display, p->egl.surface, p->egl.surface,
+ p->egl.context)) {
+ MP_ERR(ctx->vo, "Failed to make context current.\n");
+ goto err;
+ }
+
+ mpegl_load_functions(&p->gl, ctx->vo->log);
+ // required by gbm_surface_lock_front_buffer
+ eglSwapBuffers(p->egl.display, p->egl.surface);
+
+ MP_VERBOSE(ctx, "Preparing framebuffer\n");
+ struct gbm_bo *new_bo = gbm_surface_lock_front_buffer(p->gbm.surface);
+ if (!new_bo) {
+ MP_ERR(ctx, "Failed to lock GBM surface.\n");
+ goto err;
+ }
+
+ enqueue_bo(ctx, new_bo);
+ update_framebuffer_from_bo(ctx, new_bo);
+ if (!drm->fb || !drm->fb->id) {
+ MP_ERR(ctx, "Failed to create framebuffer.\n");
+ goto err;
+ }
+
+ if (!vo_drm_acquire_crtc(ctx->vo->drm)) {
+ MP_ERR(ctx, "Failed to set CRTC for connector %u: %s\n",
+ drm->connector->connector_id, mp_strerror(errno));
+ goto err;
+ }
+
+ vo_drm_set_monitor_par(ctx->vo);
+
+ p->drm_params.fd = drm->fd;
+ p->drm_params.crtc_id = drm->crtc_id;
+ p->drm_params.connector_id = drm->connector->connector_id;
+ p->drm_params.atomic_request_ptr = &drm->atomic_context->request;
+ char *rendernode_path = drmGetRenderDeviceNameFromFd(drm->fd);
+ if (rendernode_path) {
+ MP_VERBOSE(ctx, "Opening render node \"%s\"\n", rendernode_path);
+ p->drm_params.render_fd = open(rendernode_path, O_RDWR | O_CLOEXEC);
+ if (p->drm_params.render_fd == -1) {
+ MP_WARN(ctx, "Cannot open render node: %s\n", mp_strerror(errno));
+ }
+ free(rendernode_path);
+ } else {
+ p->drm_params.render_fd = -1;
+ MP_VERBOSE(ctx, "Could not find path to render node.\n");
+ }
+
+ struct ra_gl_ctx_params params = {
+ .external_swapchain = &drm_egl_swapchain,
+ .get_vsync = &drm_egl_get_vsync,
+ };
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto err;
+
+ ra_add_native_resource(ctx->ra, "drm_params_v2", &p->drm_params);
+ ra_add_native_resource(ctx->ra, "drm_draw_surface_size", &p->draw_surface_size);
+
+ return true;
+
+err:
+ drm_egl_uninit(ctx);
+ return false;
+}
+
+static bool drm_egl_reconfig(struct ra_ctx *ctx)
+{
+ struct vo_drm_state *drm = ctx->vo->drm;
+ ctx->vo->dwidth = drm->fb->width;
+ ctx->vo->dheight = drm->fb->height;
+ ra_gl_ctx_resize(ctx->swapchain, drm->fb->width, drm->fb->height, 0);
+ return true;
+}
+
+static int drm_egl_control(struct ra_ctx *ctx, int *events, int request,
+ void *arg)
+{
+ int ret = vo_drm_control(ctx->vo, events, request, arg);
+ return ret;
+}
+
+static void drm_egl_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_drm_wait_events(ctx->vo, until_time_ns);
+}
+
+static void drm_egl_wakeup(struct ra_ctx *ctx)
+{
+ vo_drm_wakeup(ctx->vo);
+}
+
+const struct ra_ctx_fns ra_ctx_drm_egl = {
+ .type = "opengl",
+ .name = "drm",
+ .reconfig = drm_egl_reconfig,
+ .control = drm_egl_control,
+ .init = drm_egl_init,
+ .uninit = drm_egl_uninit,
+ .wait_events = drm_egl_wait_events,
+ .wakeup = drm_egl_wakeup,
+};
diff --git a/video/out/opengl/context_dxinterop.c b/video/out/opengl/context_dxinterop.c
new file mode 100644
index 0000000..cda696f
--- /dev/null
+++ b/video/out/opengl/context_dxinterop.c
@@ -0,0 +1,605 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <versionhelpers.h>
+#include <d3d9.h>
+#include <dwmapi.h>
+#include "osdep/windows_utils.h"
+#include "video/out/w32_common.h"
+#include "context.h"
+#include "utils.h"
+
+// For WGL_ACCESS_WRITE_DISCARD_NV, etc.
+#include <GL/wglext.h>
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
+
+// mingw-w64 header typo?
+#ifndef IDirect3DSwapChain9Ex_GetBackBuffer
+#define IDirect3DSwapChain9Ex_GetBackBuffer IDirect3DSwapChain9EX_GetBackBuffer
+#endif
+
+struct priv {
+ GL gl;
+
+ HMODULE d3d9_dll;
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT SDKVersion, IDirect3D9Ex **ppD3D);
+
+ // Direct3D9 device and resources
+ IDirect3D9Ex *d3d9ex;
+ IDirect3DDevice9Ex *device;
+ HANDLE device_h;
+ IDirect3DSwapChain9Ex *swapchain;
+ IDirect3DSurface9 *backbuffer;
+ IDirect3DSurface9 *rtarget;
+ HANDLE rtarget_h;
+
+ // OpenGL offscreen context
+ HWND os_wnd;
+ HDC os_dc;
+ HGLRC os_ctx;
+
+ // OpenGL resources
+ GLuint texture;
+ GLuint main_fb;
+
+ // Did we lose the device?
+ bool lost_device;
+
+ // Requested and current parameters
+ int requested_swapinterval;
+ int width, height, swapinterval;
+};
+
+static __thread struct ra_ctx *current_ctx;
+
+static void pump_message_loop(void)
+{
+ // We have a hidden window on this thread (for the OpenGL context,) so pump
+ // its message loop at regular intervals to be safe
+ MSG message;
+ while (PeekMessageW(&message, NULL, 0, 0, PM_REMOVE))
+ DispatchMessageW(&message);
+}
+
+static void *w32gpa(const GLubyte *procName)
+{
+ HMODULE oglmod;
+ void *res = wglGetProcAddress(procName);
+ if (res)
+ return res;
+ oglmod = GetModuleHandleW(L"opengl32.dll");
+ return GetProcAddress(oglmod, procName);
+}
+
+static int os_ctx_create(struct ra_ctx *ctx)
+{
+ static const wchar_t os_wnd_class[] = L"mpv offscreen gl";
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HGLRC legacy_context = NULL;
+
+ RegisterClassExW(&(WNDCLASSEXW) {
+ .cbSize = sizeof(WNDCLASSEXW),
+ .style = CS_OWNDC,
+ .lpfnWndProc = DefWindowProc,
+ .hInstance = HINST_THISCOMPONENT,
+ .lpszClassName = os_wnd_class,
+ });
+
+ // Create a hidden window for an offscreen OpenGL context. It might also be
+ // possible to use the VO window, but MSDN recommends against drawing to
+ // the same window with flip mode present and other APIs, so play it safe.
+ p->os_wnd = CreateWindowExW(0, os_wnd_class, os_wnd_class, 0, 0, 0, 200,
+ 200, NULL, NULL, HINST_THISCOMPONENT, NULL);
+ p->os_dc = GetDC(p->os_wnd);
+ if (!p->os_dc) {
+ MP_FATAL(ctx->vo, "Couldn't create window for offscreen rendering\n");
+ goto fail;
+ }
+
+ // Choose a pixel format. It probably doesn't matter what this is because
+ // the primary framebuffer will not be used.
+ PIXELFORMATDESCRIPTOR pfd = {
+ .nSize = sizeof pfd,
+ .nVersion = 1,
+ .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
+ .iPixelType = PFD_TYPE_RGBA,
+ .cColorBits = 24,
+ .iLayerType = PFD_MAIN_PLANE,
+ };
+ int pf = ChoosePixelFormat(p->os_dc, &pfd);
+ if (!pf) {
+ MP_FATAL(ctx->vo,
+ "Couldn't choose pixelformat for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+ SetPixelFormat(p->os_dc, pf, &pfd);
+
+ legacy_context = wglCreateContext(p->os_dc);
+ if (!legacy_context || !wglMakeCurrent(p->os_dc, legacy_context)) {
+ MP_FATAL(ctx->vo, "Couldn't create OpenGL context for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ const char *(GLAPIENTRY *wglGetExtensionsStringARB)(HDC hdc)
+ = w32gpa((const GLubyte*)"wglGetExtensionsStringARB");
+ if (!wglGetExtensionsStringARB) {
+ MP_FATAL(ctx->vo, "The OpenGL driver does not support OpenGL 3.x\n");
+ goto fail;
+ }
+
+ const char *wgl_exts = wglGetExtensionsStringARB(p->os_dc);
+ if (!gl_check_extension(wgl_exts, "WGL_ARB_create_context")) {
+ MP_FATAL(ctx->vo, "The OpenGL driver does not support OpenGL 3.x\n");
+ goto fail;
+ }
+
+ HGLRC (GLAPIENTRY *wglCreateContextAttribsARB)(HDC hDC, HGLRC hShareContext,
+ const int *attribList)
+ = w32gpa((const GLubyte*)"wglCreateContextAttribsARB");
+ if (!wglCreateContextAttribsARB) {
+ MP_FATAL(ctx->vo, "The OpenGL driver does not support OpenGL 3.x\n");
+ goto fail;
+ }
+
+ int attribs[] = {
+ WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
+ WGL_CONTEXT_MINOR_VERSION_ARB, 0,
+ WGL_CONTEXT_FLAGS_ARB, 0,
+ WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
+ 0
+ };
+
+ p->os_ctx = wglCreateContextAttribsARB(p->os_dc, 0, attribs);
+ if (!p->os_ctx) {
+ // NVidia, instead of ignoring WGL_CONTEXT_FLAGS_ARB, will error out if
+ // it's present on pre-3.2 contexts.
+ // Remove it from attribs and retry the context creation.
+ attribs[6] = attribs[7] = 0;
+ p->os_ctx = wglCreateContextAttribsARB(p->os_dc, 0, attribs);
+ }
+ if (!p->os_ctx) {
+ MP_FATAL(ctx->vo,
+ "Couldn't create OpenGL 3.x context for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ wglMakeCurrent(p->os_dc, NULL);
+ wglDeleteContext(legacy_context);
+ legacy_context = NULL;
+
+ if (!wglMakeCurrent(p->os_dc, p->os_ctx)) {
+ MP_FATAL(ctx->vo,
+ "Couldn't activate OpenGL 3.x context for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ mpgl_load_functions(gl, w32gpa, wgl_exts, ctx->vo->log);
+ if (!(gl->mpgl_caps & MPGL_CAP_DXINTEROP)) {
+ MP_FATAL(ctx->vo, "WGL_NV_DX_interop is not supported\n");
+ goto fail;
+ }
+
+ return 0;
+fail:
+ if (legacy_context) {
+ wglMakeCurrent(p->os_dc, NULL);
+ wglDeleteContext(legacy_context);
+ }
+ return -1;
+}
+
+static void os_ctx_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->os_ctx) {
+ wglMakeCurrent(p->os_dc, NULL);
+ wglDeleteContext(p->os_ctx);
+ }
+ if (p->os_dc)
+ ReleaseDC(p->os_wnd, p->os_dc);
+ if (p->os_wnd)
+ DestroyWindow(p->os_wnd);
+}
+
+static int d3d_size_dependent_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HRESULT hr;
+
+ IDirect3DSwapChain9 *sw9;
+ hr = IDirect3DDevice9Ex_GetSwapChain(p->device, 0, &sw9);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't get swap chain: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ hr = IDirect3DSwapChain9_QueryInterface(sw9, &IID_IDirect3DSwapChain9Ex,
+ (void**)&p->swapchain);
+ if (FAILED(hr)) {
+ SAFE_RELEASE(sw9);
+ MP_ERR(ctx->vo, "Obtained swap chain is not IDirect3DSwapChain9Ex: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+ SAFE_RELEASE(sw9);
+
+ hr = IDirect3DSwapChain9Ex_GetBackBuffer(p->swapchain, 0,
+ D3DBACKBUFFER_TYPE_MONO, &p->backbuffer);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't get backbuffer: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ // Get the format of the backbuffer
+ D3DSURFACE_DESC bb_desc = { 0 };
+ IDirect3DSurface9_GetDesc(p->backbuffer, &bb_desc);
+
+ MP_VERBOSE(ctx->vo, "DX_interop backbuffer size: %ux%u\n",
+ (unsigned)bb_desc.Width, (unsigned)bb_desc.Height);
+ MP_VERBOSE(ctx->vo, "DX_interop backbuffer format: %u\n",
+ (unsigned)bb_desc.Format);
+
+ // Create a rendertarget with the same format as the backbuffer for
+ // rendering from OpenGL
+ HANDLE share_handle = NULL;
+ hr = IDirect3DDevice9Ex_CreateRenderTarget(p->device, bb_desc.Width,
+ bb_desc.Height, bb_desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE,
+ &p->rtarget, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't create rendertarget: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ // Register the share handle with WGL_NV_DX_interop. Nvidia does not
+ // require the use of share handles, but Intel does.
+ if (share_handle)
+ gl->DXSetResourceShareHandleNV(p->rtarget, share_handle);
+
+ // Create the OpenGL-side texture
+ gl->GenTextures(1, &p->texture);
+
+ // Now share the rendertarget with OpenGL as a texture
+ p->rtarget_h = gl->DXRegisterObjectNV(p->device_h, p->rtarget, p->texture,
+ GL_TEXTURE_2D, WGL_ACCESS_WRITE_DISCARD_NV);
+ if (!p->rtarget_h) {
+ MP_ERR(ctx->vo, "Couldn't share rendertarget with OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ // Lock the rendertarget for use from OpenGL. This will only be unlocked in
+ // swap_buffers() when it is blitted to the real Direct3D backbuffer.
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(ctx->vo, "Couldn't lock rendertarget: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, p->main_fb);
+ gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_TEXTURE_2D, p->texture, 0);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ return 0;
+}
+
+static void d3d_size_dependent_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+
+ if (p->rtarget_h) {
+ gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h);
+ gl->DXUnregisterObjectNV(p->device_h, p->rtarget_h);
+ }
+ p->rtarget_h = 0;
+ if (p->texture)
+ gl->DeleteTextures(1, &p->texture);
+ p->texture = 0;
+
+ SAFE_RELEASE(p->rtarget);
+ SAFE_RELEASE(p->backbuffer);
+ SAFE_RELEASE(p->swapchain);
+}
+
+static void fill_presentparams(struct ra_ctx *ctx,
+ D3DPRESENT_PARAMETERS *pparams)
+{
+ struct priv *p = ctx->priv;
+
+ // Present intervals other than IMMEDIATE and ONE don't seem to work. It's
+ // possible that they're not compatible with FLIPEX.
+ UINT presentation_interval;
+ switch (p->requested_swapinterval) {
+ case 0: presentation_interval = D3DPRESENT_INTERVAL_IMMEDIATE; break;
+ case 1: presentation_interval = D3DPRESENT_INTERVAL_ONE; break;
+ default: presentation_interval = D3DPRESENT_INTERVAL_ONE; break;
+ }
+
+ *pparams = (D3DPRESENT_PARAMETERS) {
+ .Windowed = TRUE,
+ .BackBufferWidth = ctx->vo->dwidth ? ctx->vo->dwidth : 1,
+ .BackBufferHeight = ctx->vo->dheight ? ctx->vo->dheight : 1,
+ // Add one frame for the backbuffer and one frame of "slack" to reduce
+ // contention with the window manager when acquiring the backbuffer
+ .BackBufferCount = ctx->vo->opts->swapchain_depth + 2,
+ .SwapEffect = IsWindows7OrGreater() ? D3DSWAPEFFECT_FLIPEX : D3DSWAPEFFECT_FLIP,
+ // Automatically get the backbuffer format from the display format
+ .BackBufferFormat = D3DFMT_UNKNOWN,
+ .PresentationInterval = presentation_interval,
+ .hDeviceWindow = vo_w32_hwnd(ctx->vo),
+ };
+}
+
+static int d3d_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HRESULT hr;
+
+ p->d3d9_dll = LoadLibraryW(L"d3d9.dll");
+ if (!p->d3d9_dll) {
+ MP_FATAL(ctx->vo, "Failed to load \"d3d9.dll\": %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ // WGL_NV_dx_interop requires Direct3D 9Ex on WDDM systems. Direct3D 9Ex
+ // also enables flip mode present for efficient rendering with the DWM.
+ p->Direct3DCreate9Ex = (void*)GetProcAddress(p->d3d9_dll,
+ "Direct3DCreate9Ex");
+ if (!p->Direct3DCreate9Ex) {
+ MP_FATAL(ctx->vo, "Direct3D 9Ex not supported\n");
+ return -1;
+ }
+
+ hr = p->Direct3DCreate9Ex(D3D_SDK_VERSION, &p->d3d9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(ctx->vo, "Couldn't create Direct3D9Ex: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ D3DPRESENT_PARAMETERS pparams;
+ fill_presentparams(ctx, &pparams);
+
+ hr = IDirect3D9Ex_CreateDeviceEx(p->d3d9ex, D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL, vo_w32_hwnd(ctx->vo),
+ D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_PUREDEVICE |
+ D3DCREATE_FPU_PRESERVE | D3DCREATE_MULTITHREADED |
+ D3DCREATE_NOWINDOWCHANGES,
+ &pparams, NULL, &p->device);
+ if (FAILED(hr)) {
+ MP_FATAL(ctx->vo, "Couldn't create device: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ IDirect3DDevice9Ex_SetMaximumFrameLatency(p->device, ctx->vo->opts->swapchain_depth);
+
+ // Register the Direct3D device with WGL_NV_dx_interop
+ p->device_h = gl->DXOpenDeviceNV(p->device);
+ if (!p->device_h) {
+ MP_FATAL(ctx->vo, "Couldn't open Direct3D device from OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ return 0;
+}
+
+static void d3d_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+
+ if (p->device_h)
+ gl->DXCloseDeviceNV(p->device_h);
+ SAFE_RELEASE(p->device);
+ SAFE_RELEASE(p->d3d9ex);
+ if (p->d3d9_dll)
+ FreeLibrary(p->d3d9_dll);
+}
+
+static void dxgl_uninit(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_uninit(ctx);
+ d3d_size_dependent_destroy(ctx);
+ d3d_destroy(ctx);
+ os_ctx_destroy(ctx);
+ vo_w32_uninit(ctx->vo);
+ DwmEnableMMCSS(FALSE);
+ pump_message_loop();
+}
+
+static void dxgl_reset(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HRESULT hr;
+
+ // Check if the device actually needs to be reset
+ if (ctx->vo->dwidth == p->width && ctx->vo->dheight == p->height &&
+ p->requested_swapinterval == p->swapinterval && !p->lost_device)
+ return;
+
+ d3d_size_dependent_destroy(ctx);
+
+ D3DPRESENT_PARAMETERS pparams;
+ fill_presentparams(ctx, &pparams);
+
+ hr = IDirect3DDevice9Ex_ResetEx(p->device, &pparams, NULL);
+ if (FAILED(hr)) {
+ p->lost_device = true;
+ MP_ERR(ctx->vo, "Couldn't reset device: %s\n", mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ if (d3d_size_dependent_create(ctx) < 0) {
+ p->lost_device = true;
+ MP_ERR(ctx->vo, "Couldn't recreate Direct3D objects after reset\n");
+ return;
+ }
+
+ MP_VERBOSE(ctx->vo, "Direct3D device reset\n");
+ p->width = ctx->vo->dwidth;
+ p->height = ctx->vo->dheight;
+ p->swapinterval = p->requested_swapinterval;
+ p->lost_device = false;
+}
+
+static int GLAPIENTRY dxgl_swap_interval(int interval)
+{
+ if (!current_ctx)
+ return 0;
+ struct priv *p = current_ctx->priv;
+
+ p->requested_swapinterval = interval;
+ dxgl_reset(current_ctx);
+ return 1;
+}
+
+static void dxgl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HRESULT hr;
+
+ pump_message_loop();
+
+ // If the device is still lost, try to reset it again
+ if (p->lost_device)
+ dxgl_reset(ctx);
+ if (p->lost_device)
+ return;
+
+ if (!gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(ctx->vo, "Couldn't unlock rendertarget for present: %s\n",
+ mp_LastError_to_str());
+ return;
+ }
+
+ // Blit the OpenGL rendertarget to the backbuffer
+ hr = IDirect3DDevice9Ex_StretchRect(p->device, p->rtarget, NULL,
+ p->backbuffer, NULL, D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't stretchrect for present: %s\n",
+ mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ hr = IDirect3DDevice9Ex_PresentEx(p->device, NULL, NULL, NULL, NULL, 0);
+ switch (hr) {
+ case D3DERR_DEVICELOST:
+ case D3DERR_DEVICEHUNG:
+ MP_VERBOSE(ctx->vo, "Direct3D device lost! Resetting.\n");
+ p->lost_device = true;
+ dxgl_reset(ctx);
+ return;
+ default:
+ if (FAILED(hr))
+ MP_ERR(ctx->vo, "Failed to present: %s\n", mp_HRESULT_to_str(hr));
+ }
+
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(ctx->vo, "Couldn't lock rendertarget after present: %s\n",
+ mp_LastError_to_str());
+ }
+}
+
+static bool dxgl_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ GL *gl = &p->gl;
+
+ p->requested_swapinterval = 1;
+
+ if (!vo_w32_init(ctx->vo))
+ goto fail;
+ if (os_ctx_create(ctx) < 0)
+ goto fail;
+
+ // Create the shared framebuffer
+ gl->GenFramebuffers(1, &p->main_fb);
+
+ current_ctx = ctx;
+ gl->SwapInterval = dxgl_swap_interval;
+
+ if (d3d_create(ctx) < 0)
+ goto fail;
+ if (d3d_size_dependent_create(ctx) < 0)
+ goto fail;
+
+ static const struct ra_swapchain_fns empty_swapchain_fns = {0};
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = dxgl_swap_buffers,
+ .external_swapchain = &empty_swapchain_fns,
+ };
+
+ gl->flipped = true;
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto fail;
+
+ ra_add_native_resource(ctx->ra, "IDirect3DDevice9Ex", p->device);
+ ra_add_native_resource(ctx->ra, "dxinterop_device_HANDLE", p->device_h);
+
+ DwmEnableMMCSS(TRUE);
+ return true;
+fail:
+ dxgl_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ dxgl_reset(ctx);
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, p->main_fb);
+}
+
+static bool dxgl_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int dxgl_control(struct ra_ctx *ctx, int *events, int request,
+ void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_dxgl = {
+ .type = "opengl",
+ .name = "dxinterop",
+ .init = dxgl_init,
+ .reconfig = dxgl_reconfig,
+ .control = dxgl_control,
+ .uninit = dxgl_uninit,
+};
diff --git a/video/out/opengl/context_glx.c b/video/out/opengl/context_glx.c
new file mode 100644
index 0000000..4062224
--- /dev/null
+++ b/video/out/opengl/context_glx.c
@@ -0,0 +1,351 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <X11/Xlib.h>
+#include <GL/glx.h>
+
+// FreeBSD 10.0-CURRENT lacks the GLX_ARB_create_context extension completely
+#ifndef GLX_CONTEXT_MAJOR_VERSION_ARB
+#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
+#define GLX_CONTEXT_FLAGS_ARB 0x2094
+#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126
+#ifndef __APPLE__
+// These are respectively 0x00000001 and 0x00000002 on OSX
+#define GLX_CONTEXT_DEBUG_BIT_ARB 0x0001
+#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x0002
+#endif
+#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
+#define GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB 0x00000002
+#endif
+// GLX_EXT_create_context_es2_profile
+#ifndef GLX_CONTEXT_ES2_PROFILE_BIT_EXT
+#define GLX_CONTEXT_ES2_PROFILE_BIT_EXT 0x00000004
+#endif
+
+#include "osdep/timer.h"
+#include "video/out/present_sync.h"
+#include "video/out/x11_common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ GL gl;
+ XVisualInfo *vinfo;
+ GLXContext context;
+ GLXFBConfig fbc;
+};
+
+static void glx_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ if (p->vinfo)
+ XFree(p->vinfo);
+ if (p->context) {
+ Display *display = ctx->vo->x11->display;
+ glXMakeCurrent(display, None, NULL);
+ glXDestroyContext(display, p->context);
+ }
+
+ vo_x11_uninit(ctx->vo);
+}
+
+typedef GLXContext (*glXCreateContextAttribsARBProc)
+ (Display*, GLXFBConfig, GLXContext, Bool, const int*);
+
+static bool create_context_x11(struct ra_ctx *ctx, GL *gl, bool es)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ glXCreateContextAttribsARBProc glXCreateContextAttribsARB =
+ (glXCreateContextAttribsARBProc)
+ glXGetProcAddressARB((const GLubyte *)"glXCreateContextAttribsARB");
+
+ const char *glxstr =
+ glXQueryExtensionsString(vo->x11->display, vo->x11->screen);
+ if (!glxstr) {
+ MP_ERR(ctx, "GLX did not advertise any extensions\n");
+ return false;
+ }
+
+ if (!gl_check_extension(glxstr, "GLX_ARB_create_context_profile") ||
+ !glXCreateContextAttribsARB) {
+ MP_ERR(ctx, "GLX does not support GLX_ARB_create_context_profile\n");
+ return false;
+ }
+
+ int ctx_flags = ctx->opts.debug ? GLX_CONTEXT_DEBUG_BIT_ARB : 0;
+ int profile_mask = GLX_CONTEXT_CORE_PROFILE_BIT_ARB;
+
+ if (es) {
+ profile_mask = GLX_CONTEXT_ES2_PROFILE_BIT_EXT;
+ if (!gl_check_extension(glxstr, "GLX_EXT_create_context_es2_profile"))
+ return false;
+ }
+
+ int context_attribs[] = {
+ GLX_CONTEXT_MAJOR_VERSION_ARB, 0,
+ GLX_CONTEXT_MINOR_VERSION_ARB, 0,
+ GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask,
+ GLX_CONTEXT_FLAGS_ARB, ctx_flags,
+ None
+ };
+
+ GLXContext context;
+
+ if (!es) {
+ for (int n = 0; mpgl_min_required_gl_versions[n]; n++) {
+ int version = mpgl_min_required_gl_versions[n];
+ MP_VERBOSE(ctx, "Creating OpenGL %d.%d context...\n",
+ MPGL_VER_P(version));
+
+ context_attribs[1] = MPGL_VER_GET_MAJOR(version);
+ context_attribs[3] = MPGL_VER_GET_MINOR(version);
+
+ vo_x11_silence_xlib(1);
+ context = glXCreateContextAttribsARB(vo->x11->display,
+ p->fbc, 0, True,
+ context_attribs);
+ vo_x11_silence_xlib(-1);
+
+ if (context)
+ break;
+ }
+ } else {
+ context_attribs[1] = 2;
+
+ vo_x11_silence_xlib(1);
+ context = glXCreateContextAttribsARB(vo->x11->display,
+ p->fbc, 0, True,
+ context_attribs);
+ vo_x11_silence_xlib(-1);
+ }
+
+ if (!context)
+ return false;
+
+ // set context
+ if (!glXMakeCurrent(vo->x11->display, vo->x11->window, context)) {
+ MP_FATAL(vo, "Could not set GLX context!\n");
+ glXDestroyContext(vo->x11->display, context);
+ return false;
+ }
+
+ p->context = context;
+
+ mpgl_load_functions(gl, (void *)glXGetProcAddressARB, glxstr, vo->log);
+ return true;
+}
+
+// The GL3/FBC initialization code roughly follows/copies from:
+// http://www.opengl.org/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX)
+// but also uses some of the old code.
+
+static GLXFBConfig select_fb_config(struct vo *vo, const int *attribs, bool alpha)
+{
+ int fbcount;
+ GLXFBConfig *fbc = glXChooseFBConfig(vo->x11->display, vo->x11->screen,
+ attribs, &fbcount);
+ if (!fbc)
+ return NULL;
+
+ // The list in fbc is sorted (so that the first element is the best).
+ GLXFBConfig fbconfig = fbcount > 0 ? fbc[0] : NULL;
+
+ if (alpha) {
+ for (int n = 0; n < fbcount; n++) {
+ XVisualInfo *v = glXGetVisualFromFBConfig(vo->x11->display, fbc[n]);
+ if (v) {
+ bool is_rgba = vo_x11_is_rgba_visual(v);
+ XFree(v);
+ if (is_rgba) {
+ fbconfig = fbc[n];
+ break;
+ }
+ }
+ }
+ }
+
+ XFree(fbc);
+
+ return fbconfig;
+}
+
+static void set_glx_attrib(int *attribs, int name, int value)
+{
+ for (int n = 0; attribs[n * 2 + 0] != None; n++) {
+ if (attribs[n * 2 + 0] == name) {
+ attribs[n * 2 + 1] = value;
+ break;
+ }
+ }
+}
+
+static bool glx_check_visible(struct ra_ctx *ctx)
+{
+ return vo_x11_check_visible(ctx->vo);
+}
+
+static void glx_swap_buffers(struct ra_ctx *ctx)
+{
+ glXSwapBuffers(ctx->vo->x11->display, ctx->vo->x11->window);
+ if (ctx->vo->x11->use_present)
+ present_sync_swap(ctx->vo->x11->present);
+}
+
+static void glx_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = ctx->vo->x11;
+ if (ctx->vo->x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static bool glx_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo *vo = ctx->vo;
+ GL *gl = &p->gl;
+
+ if (!vo_x11_init(ctx->vo))
+ goto uninit;
+
+ int glx_major, glx_minor;
+
+ if (!glXQueryVersion(vo->x11->display, &glx_major, &glx_minor)) {
+ MP_ERR(ctx, "GLX not found.\n");
+ goto uninit;
+ }
+ // FBConfigs were added in GLX version 1.3.
+ if (MPGL_VER(glx_major, glx_minor) < MPGL_VER(1, 3)) {
+ MP_ERR(ctx, "GLX version older than 1.3.\n");
+ goto uninit;
+ }
+
+ int glx_attribs[] = {
+ GLX_X_RENDERABLE, True,
+ GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
+ GLX_RED_SIZE, 1,
+ GLX_GREEN_SIZE, 1,
+ GLX_BLUE_SIZE, 1,
+ GLX_ALPHA_SIZE, 0,
+ GLX_DOUBLEBUFFER, True,
+ None
+ };
+ GLXFBConfig fbc = NULL;
+ if (ctx->opts.want_alpha) {
+ set_glx_attrib(glx_attribs, GLX_ALPHA_SIZE, 1);
+ fbc = select_fb_config(vo, glx_attribs, true);
+ if (!fbc)
+ set_glx_attrib(glx_attribs, GLX_ALPHA_SIZE, 0);
+ }
+ if (!fbc)
+ fbc = select_fb_config(vo, glx_attribs, false);
+ if (!fbc) {
+ MP_ERR(ctx, "no GLX support present\n");
+ goto uninit;
+ }
+
+ int fbid = -1;
+ if (!glXGetFBConfigAttrib(vo->x11->display, fbc, GLX_FBCONFIG_ID, &fbid))
+ MP_VERBOSE(ctx, "GLX chose FB config with ID 0x%x\n", fbid);
+
+ p->fbc = fbc;
+ p->vinfo = glXGetVisualFromFBConfig(vo->x11->display, fbc);
+ if (p->vinfo) {
+ MP_VERBOSE(ctx, "GLX chose visual with ID 0x%x\n",
+ (int)p->vinfo->visualid);
+ } else {
+ MP_WARN(ctx, "Selected GLX FB config has no associated X visual\n");
+ }
+
+ if (!vo_x11_create_vo_window(vo, p->vinfo, "gl"))
+ goto uninit;
+
+ bool success = false;
+ enum gles_mode mode = ra_gl_ctx_get_glesmode(ctx);
+
+ if (mode == GLES_NO || mode == GLES_AUTO)
+ success = create_context_x11(ctx, gl, false);
+ if (!success && (mode == GLES_YES || mode == GLES_AUTO))
+ success = create_context_x11(ctx, gl, true);
+ if (success && !glXIsDirect(vo->x11->display, p->context))
+ gl->mpgl_caps |= MPGL_CAP_SW;
+ if (!success)
+ goto uninit;
+
+ struct ra_gl_ctx_params params = {
+ .check_visible = glx_check_visible,
+ .swap_buffers = glx_swap_buffers,
+ .get_vsync = glx_get_vsync,
+ };
+
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto uninit;
+
+ ra_add_native_resource(ctx->ra, "x11", vo->x11->display);
+
+ return true;
+
+uninit:
+ glx_uninit(ctx);
+ return false;
+}
+
+
+static void resize(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool glx_reconfig(struct ra_ctx *ctx)
+{
+ vo_x11_config_vo_window(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int glx_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_x11_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+static void glx_wakeup(struct ra_ctx *ctx)
+{
+ vo_x11_wakeup(ctx->vo);
+}
+
+static void glx_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_x11_wait_events(ctx->vo, until_time_ns);
+}
+
+const struct ra_ctx_fns ra_ctx_glx = {
+ .type = "opengl",
+ .name = "x11",
+ .reconfig = glx_reconfig,
+ .control = glx_control,
+ .wakeup = glx_wakeup,
+ .wait_events = glx_wait_events,
+ .init = glx_init,
+ .uninit = glx_uninit,
+};
diff --git a/video/out/opengl/context_rpi.c b/video/out/opengl/context_rpi.c
new file mode 100644
index 0000000..0b6babb
--- /dev/null
+++ b/video/out/opengl/context_rpi.c
@@ -0,0 +1,327 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdatomic.h>
+#include <stddef.h>
+
+#include <bcm_host.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "common/common.h"
+#include "video/out/win_state.h"
+#include "context.h"
+#include "egl_helpers.h"
+
+struct priv {
+ struct GL gl;
+ DISPMANX_DISPLAY_HANDLE_T display;
+ DISPMANX_ELEMENT_HANDLE_T window;
+ DISPMANX_UPDATE_HANDLE_T update;
+ EGLDisplay egl_display;
+ EGLConfig egl_config;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+ // yep, the API keeps a pointer to it
+ EGL_DISPMANX_WINDOW_T egl_window;
+ int x, y, w, h;
+ double display_fps;
+ atomic_int reload_display;
+ int win_params[4];
+};
+
+static void tv_callback(void *callback_data, uint32_t reason, uint32_t param1,
+ uint32_t param2)
+{
+ struct ra_ctx *ctx = callback_data;
+ struct priv *p = ctx->priv;
+ atomic_store(&p->reload_display, true);
+ vo_wakeup(ctx->vo);
+}
+
+static void destroy_dispmanx(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->egl_surface) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ p->egl_surface = EGL_NO_SURFACE;
+ }
+
+ if (p->window)
+ vc_dispmanx_element_remove(p->update, p->window);
+ p->window = 0;
+ if (p->display)
+ vc_dispmanx_display_close(p->display);
+ p->display = 0;
+ if (p->update)
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = 0;
+}
+
+static void rpi_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ vc_tv_unregister_callback_full(tv_callback, ctx);
+
+ destroy_dispmanx(ctx);
+
+ if (p->egl_context)
+ eglDestroyContext(p->egl_display, p->egl_context);
+ p->egl_context = EGL_NO_CONTEXT;
+ eglReleaseThread();
+ p->egl_display = EGL_NO_DISPLAY;
+}
+
+static bool recreate_dispmanx(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ int display_nr = 0;
+ int layer = 0;
+
+ MP_VERBOSE(ctx, "Recreating DISPMANX state...\n");
+
+ destroy_dispmanx(ctx);
+
+ p->display = vc_dispmanx_display_open(display_nr);
+ p->update = vc_dispmanx_update_start(0);
+ if (!p->display || !p->update) {
+ MP_FATAL(ctx, "Could not get DISPMANX objects.\n");
+ goto fail;
+ }
+
+ uint32_t dispw, disph;
+ if (graphics_get_display_size(0, &dispw, &disph) < 0) {
+ MP_FATAL(ctx, "Could not get display size.\n");
+ goto fail;
+ }
+ p->w = dispw;
+ p->h = disph;
+
+ if (ctx->vo->opts->fullscreen) {
+ p->x = p->y = 0;
+ } else {
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc = {0, 0, p->w, p->h};
+
+ vo_calc_window_geometry(ctx->vo, &screenrc, &geo);
+
+ mp_rect_intersection(&geo.win, &screenrc);
+
+ p->x = geo.win.x0;
+ p->y = geo.win.y0;
+ p->w = geo.win.x1 - geo.win.x0;
+ p->h = geo.win.y1 - geo.win.y0;
+ }
+
+ // dispmanx is like a neanderthal version of Wayland - you can add an
+ // overlay any place on the screen.
+ VC_RECT_T dst = {.x = p->x, .y = p->y, .width = p->w, .height = p->h};
+ VC_RECT_T src = {.width = p->w << 16, .height = p->h << 16};
+ VC_DISPMANX_ALPHA_T alpha = {
+ .flags = DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS,
+ .opacity = 0xFF,
+ };
+ p->window = vc_dispmanx_element_add(p->update, p->display, layer, &dst, 0,
+ &src, DISPMANX_PROTECTION_NONE, &alpha,
+ 0, 0);
+ if (!p->window) {
+ MP_FATAL(ctx, "Could not add DISPMANX element.\n");
+ goto fail;
+ }
+
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = vc_dispmanx_update_start(0);
+
+ p->egl_window = (EGL_DISPMANX_WINDOW_T){
+ .element = p->window,
+ .width = p->w,
+ .height = p->h,
+ };
+ p->egl_surface = eglCreateWindowSurface(p->egl_display, p->egl_config,
+ &p->egl_window, NULL);
+
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(ctx, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ if (!eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context))
+ {
+ MP_FATAL(ctx, "Failed to set context!\n");
+ goto fail;
+ }
+
+ p->display_fps = 0;
+ TV_GET_STATE_RESP_T tvstate;
+ TV_DISPLAY_STATE_T tvstate_disp;
+ if (!vc_tv_get_state(&tvstate) && !vc_tv_get_display_state(&tvstate_disp)) {
+ if (tvstate_disp.state & (VC_HDMI_HDMI | VC_HDMI_DVI)) {
+ p->display_fps = tvstate_disp.display.hdmi.frame_rate;
+
+ HDMI_PROPERTY_PARAM_T param = {
+ .property = HDMI_PROPERTY_PIXEL_CLOCK_TYPE,
+ };
+ if (!vc_tv_hdmi_get_property(&param) &&
+ param.param1 == HDMI_PIXEL_CLOCK_TYPE_NTSC)
+ p->display_fps = p->display_fps / 1.001;
+ } else {
+ p->display_fps = tvstate_disp.display.sdtv.frame_rate;
+ }
+ }
+
+ p->win_params[0] = display_nr;
+ p->win_params[1] = layer;
+ p->win_params[2] = p->x;
+ p->win_params[3] = p->y;
+
+ ctx->vo->dwidth = p->w;
+ ctx->vo->dheight = p->h;
+ if (ctx->swapchain)
+ ra_gl_ctx_resize(ctx->swapchain, p->w, p->h, 0);
+
+ ctx->vo->want_redraw = true;
+
+ vo_event(ctx->vo, VO_EVENT_WIN_STATE);
+ return true;
+
+fail:
+ destroy_dispmanx(ctx);
+ return false;
+}
+
+static void rpi_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+}
+
+static bool rpi_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+
+ bcm_host_init();
+
+ vc_tv_register_callback(tv_callback, ctx);
+
+ p->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(ctx, "EGL failed to initialize.\n");
+ goto fail;
+ }
+
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context, &p->egl_config))
+ goto fail;
+
+ if (!recreate_dispmanx(ctx))
+ goto fail;
+
+ mpegl_load_functions(&p->gl, ctx->log);
+
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = rpi_swap_buffers,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto fail;
+
+ ra_add_native_resource(ctx->ra, "MPV_RPI_WINDOW", p->win_params);
+
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+ return true;
+
+fail:
+ rpi_uninit(ctx);
+ return false;
+}
+
+static bool rpi_reconfig(struct ra_ctx *ctx)
+{
+ return recreate_dispmanx(ctx);
+}
+
+static struct mp_image *take_screenshot(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!p->display)
+ return NULL;
+
+ struct mp_image *img = mp_image_alloc(IMGFMT_BGR0, p->w, p->h);
+ if (!img)
+ return NULL;
+
+ DISPMANX_RESOURCE_HANDLE_T resource =
+ vc_dispmanx_resource_create(VC_IMAGE_ARGB8888,
+ img->w | ((img->w * 4) << 16), img->h,
+ &(int32_t){0});
+ if (!resource)
+ goto fail;
+
+ if (vc_dispmanx_snapshot(p->display, resource, 0))
+ goto fail;
+
+ VC_RECT_T rc = {.width = img->w, .height = img->h};
+ if (vc_dispmanx_resource_read_data(resource, &rc, img->planes[0], img->stride[0]))
+ goto fail;
+
+ vc_dispmanx_resource_delete(resource);
+ return img;
+
+fail:
+ vc_dispmanx_resource_delete(resource);
+ talloc_free(img);
+ return NULL;
+}
+
+static int rpi_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ struct priv *p = ctx->priv;
+
+ switch (request) {
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)arg = take_screenshot(ctx);
+ return VO_TRUE;
+ case VOCTRL_CHECK_EVENTS:
+ if (atomic_fetch_and(&p->reload_display, 0)) {
+ MP_WARN(ctx, "Recovering from display mode switch...\n");
+ recreate_dispmanx(ctx);
+ }
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_FPS:
+ *(double *)arg = p->display_fps;
+ return VO_TRUE;
+ }
+
+ return VO_NOTIMPL;
+}
+
+const struct ra_ctx_fns ra_ctx_rpi = {
+ .type = "opengl",
+ .name = "rpi",
+ .reconfig = rpi_reconfig,
+ .control = rpi_control,
+ .init = rpi_init,
+ .uninit = rpi_uninit,
+};
diff --git a/video/out/opengl/context_wayland.c b/video/out/opengl/context_wayland.c
new file mode 100644
index 0000000..26c5268
--- /dev/null
+++ b/video/out/opengl/context_wayland.c
@@ -0,0 +1,230 @@
+/*
+ * This file is part of mpv video player.
+ * Copyright © 2013 Alexander Preisinger <alexander.preisinger@gmail.com>
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <wayland-egl.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "video/out/present_sync.h"
+#include "video/out/wayland_common.h"
+#include "context.h"
+#include "egl_helpers.h"
+#include "utils.h"
+
+#define EGL_PLATFORM_WAYLAND_EXT 0x31D8
+
+struct priv {
+ GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+ EGLConfig egl_config;
+ struct wl_egl_window *egl_window;
+};
+
+static void resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ MP_VERBOSE(wl, "Handling resize on the egl side\n");
+
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ if (p->egl_window)
+ wl_egl_window_resize(p->egl_window, width, height, 0, 0);
+
+ wl->vo->dwidth = width;
+ wl->vo->dheight = height;
+
+ vo_wayland_handle_fractional_scale(wl);
+}
+
+static bool wayland_egl_check_visible(struct ra_ctx *ctx)
+{
+ return vo_wayland_check_visible(ctx->vo);
+}
+
+static void wayland_egl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void wayland_egl_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+static bool egl_create_context(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ if (!(p->egl_display = mpegl_get_display(EGL_PLATFORM_WAYLAND_EXT,
+ "EGL_EXT_platform_wayland",
+ wl->display)))
+ return false;
+
+ if (eglInitialize(p->egl_display, NULL, NULL) != EGL_TRUE)
+ return false;
+
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context,
+ &p->egl_config))
+ return false;
+
+ eglMakeCurrent(p->egl_display, NULL, NULL, p->egl_context);
+
+ mpegl_load_functions(&p->gl, wl->log);
+
+ struct ra_gl_ctx_params params = {
+ .check_visible = wayland_egl_check_visible,
+ .swap_buffers = wayland_egl_swap_buffers,
+ .get_vsync = wayland_egl_get_vsync,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ return false;
+
+ ra_add_native_resource(ctx->ra, "wl", wl->display);
+
+ return true;
+}
+
+static void egl_create_window(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ p->egl_window = wl_egl_window_create(wl->surface,
+ mp_rect_w(wl->geometry),
+ mp_rect_h(wl->geometry));
+
+ p->egl_surface = mpegl_create_window_surface(
+ p->egl_display, p->egl_config, p->egl_window);
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ p->egl_surface = eglCreateWindowSurface(
+ p->egl_display, p->egl_config, p->egl_window, NULL);
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface, p->egl_context);
+ // eglMakeCurrent may not configure the draw or read buffers if the context
+ // has been made current previously. On nvidia GL_NONE is bound because EGL_NO_SURFACE
+ // is used initially and we must bind the read and draw buffers here.
+ if(!p->gl.es) {
+ p->gl.ReadBuffer(GL_BACK);
+ p->gl.DrawBuffer(GL_BACK);
+ }
+
+ eglSwapInterval(p->egl_display, 0);
+}
+
+static bool wayland_egl_reconfig(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!vo_wayland_reconfig(ctx->vo))
+ return false;
+
+ if (!p->egl_window)
+ egl_create_window(ctx);
+
+ return true;
+}
+
+static void wayland_egl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_gl_ctx_uninit(ctx);
+
+ if (p->egl_context) {
+ eglReleaseThread();
+ if (p->egl_window)
+ wl_egl_window_destroy(p->egl_window);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ eglMakeCurrent(p->egl_display, NULL, NULL, EGL_NO_CONTEXT);
+ eglDestroyContext(p->egl_display, p->egl_context);
+ p->egl_context = NULL;
+ }
+ eglTerminate(p->egl_display);
+
+ vo_wayland_uninit(ctx->vo);
+}
+
+static int wayland_egl_control(struct ra_ctx *ctx, int *events, int request,
+ void *data)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ int r = vo_wayland_control(ctx->vo, events, request, data);
+
+ if (*events & VO_EVENT_RESIZE) {
+ resize(ctx);
+ ra_gl_ctx_resize(ctx->swapchain, wl->vo->dwidth, wl->vo->dheight, 0);
+ }
+
+ return r;
+}
+
+static void wayland_egl_wakeup(struct ra_ctx *ctx)
+{
+ vo_wayland_wakeup(ctx->vo);
+}
+
+static void wayland_egl_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_wayland_wait_events(ctx->vo, until_time_ns);
+}
+
+static void wayland_egl_update_render_opts(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ wl_surface_commit(wl->surface);
+}
+
+static bool wayland_egl_init(struct ra_ctx *ctx)
+{
+ if (!vo_wayland_init(ctx->vo))
+ return false;
+ return egl_create_context(ctx);
+}
+
+const struct ra_ctx_fns ra_ctx_wayland_egl = {
+ .type = "opengl",
+ .name = "wayland",
+ .reconfig = wayland_egl_reconfig,
+ .control = wayland_egl_control,
+ .wakeup = wayland_egl_wakeup,
+ .wait_events = wayland_egl_wait_events,
+ .update_render_opts = wayland_egl_update_render_opts,
+ .init = wayland_egl_init,
+ .uninit = wayland_egl_uninit,
+};
diff --git a/video/out/opengl/context_win.c b/video/out/opengl/context_win.c
new file mode 100644
index 0000000..968b176
--- /dev/null
+++ b/video/out/opengl/context_win.c
@@ -0,0 +1,378 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <windows.h>
+#include <dwmapi.h>
+
+#include "options/m_config.h"
+#include "video/out/w32_common.h"
+#include "context.h"
+#include "utils.h"
+
+#if !defined(WGL_CONTEXT_MAJOR_VERSION_ARB)
+/* these are supposed to be defined in wingdi.h but mingw's is too old */
+/* only the bits actually used by mplayer are defined */
+/* reference: http://www.opengl.org/registry/specs/ARB/wgl_create_context.txt */
+
+#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
+#define WGL_CONTEXT_FLAGS_ARB 0x2094
+#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
+#define WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x0002
+#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
+#endif
+
+struct wingl_opts {
+ int wingl_dwm_flush;
+};
+
+#define OPT_BASE_STRUCT struct wingl_opts
+const struct m_sub_options wingl_conf = {
+ .opts = (const struct m_option[]) {
+ {"opengl-dwmflush", OPT_CHOICE(wingl_dwm_flush,
+ {"no", -1}, {"auto", 0}, {"windowed", 1}, {"yes", 2})},
+ {0}
+ },
+ .size = sizeof(struct wingl_opts),
+};
+
+struct priv {
+ GL gl;
+
+ int opt_swapinterval;
+ int current_swapinterval;
+
+ int (GLAPIENTRY *real_wglSwapInterval)(int);
+ struct m_config_cache *opts_cache;
+ struct wingl_opts *opts;
+
+ HGLRC context;
+ HDC hdc;
+};
+
+static void wgl_uninit(struct ra_ctx *ctx);
+
+static __thread struct priv *current_wgl_context;
+
+static int GLAPIENTRY wgl_swap_interval(int interval)
+{
+ if (current_wgl_context)
+ current_wgl_context->opt_swapinterval = interval;
+ return 0;
+}
+
+static bool create_dc(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HWND win = vo_w32_hwnd(ctx->vo);
+
+ if (p->hdc)
+ return true;
+
+ HDC hdc = GetDC(win);
+ if (!hdc)
+ return false;
+
+ PIXELFORMATDESCRIPTOR pfd;
+ memset(&pfd, 0, sizeof pfd);
+ pfd.nSize = sizeof pfd;
+ pfd.nVersion = 1;
+ pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
+
+ pfd.iPixelType = PFD_TYPE_RGBA;
+ pfd.cColorBits = 24;
+ pfd.iLayerType = PFD_MAIN_PLANE;
+ int pf = ChoosePixelFormat(hdc, &pfd);
+
+ if (!pf) {
+ MP_ERR(ctx->vo, "unable to select a valid pixel format!\n");
+ ReleaseDC(win, hdc);
+ return false;
+ }
+
+ SetPixelFormat(hdc, pf, &pfd);
+
+ p->hdc = hdc;
+ return true;
+}
+
+static void *wglgpa(const GLubyte *procName)
+{
+ HMODULE oglmod;
+ void *res = wglGetProcAddress(procName);
+ if (res)
+ return res;
+ oglmod = GetModuleHandle(L"opengl32.dll");
+ return GetProcAddress(oglmod, procName);
+}
+
+static bool create_context_wgl_old(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ HDC windc = p->hdc;
+ bool res = false;
+
+ HGLRC context = wglCreateContext(windc);
+ if (!context) {
+ MP_FATAL(ctx->vo, "Could not create GL context!\n");
+ return res;
+ }
+
+ if (!wglMakeCurrent(windc, context)) {
+ MP_FATAL(ctx->vo, "Could not set GL context!\n");
+ wglDeleteContext(context);
+ return res;
+ }
+
+ p->context = context;
+ return true;
+}
+
+static bool create_context_wgl_gl3(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ HDC windc = p->hdc;
+ HGLRC context = 0;
+
+ // A legacy context is needed to get access to the new functions.
+ HGLRC legacy_context = wglCreateContext(windc);
+ if (!legacy_context) {
+ MP_FATAL(ctx->vo, "Could not create GL context!\n");
+ return false;
+ }
+
+ // set context
+ if (!wglMakeCurrent(windc, legacy_context)) {
+ MP_FATAL(ctx->vo, "Could not set GL context!\n");
+ goto out;
+ }
+
+ const char *(GLAPIENTRY *wglGetExtensionsStringARB)(HDC hdc)
+ = wglgpa((const GLubyte*)"wglGetExtensionsStringARB");
+
+ if (!wglGetExtensionsStringARB)
+ goto unsupported;
+
+ const char *wgl_exts = wglGetExtensionsStringARB(windc);
+ if (!gl_check_extension(wgl_exts, "WGL_ARB_create_context"))
+ goto unsupported;
+
+ HGLRC (GLAPIENTRY *wglCreateContextAttribsARB)(HDC hDC, HGLRC hShareContext,
+ const int *attribList)
+ = wglgpa((const GLubyte*)"wglCreateContextAttribsARB");
+
+ if (!wglCreateContextAttribsARB)
+ goto unsupported;
+
+ int attribs[] = {
+ WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
+ WGL_CONTEXT_MINOR_VERSION_ARB, 0,
+ WGL_CONTEXT_FLAGS_ARB, 0,
+ WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
+ 0
+ };
+
+ context = wglCreateContextAttribsARB(windc, 0, attribs);
+ if (!context) {
+ // NVidia, instead of ignoring WGL_CONTEXT_FLAGS_ARB, will error out if
+ // it's present on pre-3.2 contexts.
+ // Remove it from attribs and retry the context creation.
+ attribs[6] = attribs[7] = 0;
+ context = wglCreateContextAttribsARB(windc, 0, attribs);
+ }
+ if (!context) {
+ int err = GetLastError();
+ MP_FATAL(ctx->vo, "Could not create an OpenGL 3.x context: error 0x%x\n", err);
+ goto out;
+ }
+
+ wglMakeCurrent(windc, NULL);
+ wglDeleteContext(legacy_context);
+
+ if (!wglMakeCurrent(windc, context)) {
+ MP_FATAL(ctx->vo, "Could not set GL3 context!\n");
+ wglDeleteContext(context);
+ return false;
+ }
+
+ p->context = context;
+ return true;
+
+unsupported:
+ MP_ERR(ctx->vo, "The OpenGL driver does not support OpenGL 3.x \n");
+out:
+ wglMakeCurrent(windc, NULL);
+ wglDeleteContext(legacy_context);
+ return false;
+}
+
+static void create_ctx(void *ptr)
+{
+ struct ra_ctx *ctx = ptr;
+ struct priv *p = ctx->priv;
+
+ if (!create_dc(ctx))
+ return;
+
+ create_context_wgl_gl3(ctx);
+ if (!p->context)
+ create_context_wgl_old(ctx);
+
+ wglMakeCurrent(p->hdc, NULL);
+}
+
+static bool compositor_active(struct ra_ctx *ctx)
+{
+ // For Windows 7.
+ BOOL enabled = 0;
+ if (FAILED(DwmIsCompositionEnabled(&enabled)) || !enabled)
+ return false;
+
+ // This works at least on Windows 8.1: it returns an error in fullscreen,
+ // which is also when we get consistent timings without DwmFlush. Might
+ // be cargo-cult.
+ DWM_TIMING_INFO info = { .cbSize = sizeof(DWM_TIMING_INFO) };
+ if (FAILED(DwmGetCompositionTimingInfo(0, &info)))
+ return false;
+
+ return true;
+}
+
+static void wgl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ SwapBuffers(p->hdc);
+
+ // default if we don't DwmFLush
+ int new_swapinterval = p->opt_swapinterval;
+
+ if (p->opts->wingl_dwm_flush >= 0) {
+ if ((p->opts->wingl_dwm_flush == 1 && !ctx->vo->opts->fullscreen) ||
+ (p->opts->wingl_dwm_flush == 2) ||
+ (p->opts->wingl_dwm_flush == 0 && compositor_active(ctx)))
+ {
+ if (DwmFlush() == S_OK)
+ new_swapinterval = 0;
+ }
+ }
+
+ if (new_swapinterval != p->current_swapinterval &&
+ p->real_wglSwapInterval)
+ {
+ p->real_wglSwapInterval(new_swapinterval);
+ MP_VERBOSE(ctx->vo, "set SwapInterval(%d)\n", new_swapinterval);
+ }
+ p->current_swapinterval = new_swapinterval;
+}
+
+static bool wgl_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ GL *gl = &p->gl;
+
+ p->opts_cache = m_config_cache_alloc(ctx, ctx->global, &wingl_conf);
+ p->opts = p->opts_cache->opts;
+
+ if (!vo_w32_init(ctx->vo))
+ goto fail;
+
+ vo_w32_run_on_thread(ctx->vo, create_ctx, ctx);
+ if (!p->context)
+ goto fail;
+
+ current_wgl_context = p;
+ wglMakeCurrent(p->hdc, p->context);
+
+ mpgl_load_functions(gl, wglgpa, NULL, ctx->vo->log);
+
+ if (!gl->SwapInterval)
+ MP_VERBOSE(ctx->vo, "WGL_EXT_swap_control missing.\n");
+ p->real_wglSwapInterval = gl->SwapInterval;
+ gl->SwapInterval = wgl_swap_interval;
+ p->current_swapinterval = -1;
+
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = wgl_swap_buffers,
+ };
+
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto fail;
+
+ DwmEnableMMCSS(TRUE);
+ return true;
+
+fail:
+ wgl_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool wgl_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static void destroy_gl(void *ptr)
+{
+ struct ra_ctx *ctx = ptr;
+ struct priv *p = ctx->priv;
+ if (p->context)
+ wglDeleteContext(p->context);
+ p->context = 0;
+ if (p->hdc)
+ ReleaseDC(vo_w32_hwnd(ctx->vo), p->hdc);
+ p->hdc = NULL;
+ current_wgl_context = NULL;
+}
+
+static void wgl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+ if (p->context)
+ wglMakeCurrent(p->hdc, 0);
+ vo_w32_run_on_thread(ctx->vo, destroy_gl, ctx);
+
+ DwmEnableMMCSS(FALSE);
+ vo_w32_uninit(ctx->vo);
+}
+
+static int wgl_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_wgl = {
+ .type = "opengl",
+ .name = "win",
+ .init = wgl_init,
+ .reconfig = wgl_reconfig,
+ .control = wgl_control,
+ .uninit = wgl_uninit,
+};
diff --git a/video/out/opengl/context_x11egl.c b/video/out/opengl/context_x11egl.c
new file mode 100644
index 0000000..3201f29
--- /dev/null
+++ b/video/out/opengl/context_x11egl.c
@@ -0,0 +1,225 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <X11/Xlib.h>
+#include <X11/extensions/Xpresent.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "common/common.h"
+#include "video/out/present_sync.h"
+#include "video/out/x11_common.h"
+#include "context.h"
+#include "egl_helpers.h"
+#include "utils.h"
+
+#define EGL_PLATFORM_X11_EXT 0x31D5
+
+struct priv {
+ GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+};
+
+static void mpegl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglTerminate(p->egl_display);
+ vo_x11_uninit(ctx->vo);
+}
+
+static int pick_xrgba_config(void *user_data, EGLConfig *configs, int num_configs)
+{
+ struct ra_ctx *ctx = user_data;
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ for (int n = 0; n < num_configs; n++) {
+ int vID = 0, num;
+ eglGetConfigAttrib(p->egl_display, configs[n], EGL_NATIVE_VISUAL_ID, &vID);
+ XVisualInfo template = {.visualid = vID};
+ XVisualInfo *vi = XGetVisualInfo(vo->x11->display, VisualIDMask,
+ &template, &num);
+ if (vi) {
+ bool is_rgba = vo_x11_is_rgba_visual(vi);
+ XFree(vi);
+ if (is_rgba)
+ return n;
+ }
+ }
+
+ return 0;
+}
+
+static bool mpegl_check_visible(struct ra_ctx *ctx)
+{
+ return vo_x11_check_visible(ctx->vo);
+}
+
+static void mpegl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+ if (ctx->vo->x11->use_present)
+ present_sync_swap(ctx->vo->x11->present);
+}
+
+static void mpegl_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = ctx->vo->x11;
+ if (ctx->vo->x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static bool mpegl_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo *vo = ctx->vo;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_FATAL;
+
+ if (!vo_x11_init(vo))
+ goto uninit;
+
+ p->egl_display = mpegl_get_display(EGL_PLATFORM_X11_EXT,
+ "EGL_EXT_platform_x11",
+ vo->x11->display);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_MSG(ctx, msgl, "Could not initialize EGL.\n");
+ goto uninit;
+ }
+
+ struct mpegl_cb cb = {
+ .user_data = ctx,
+ .refine_config = ctx->opts.want_alpha ? pick_xrgba_config : NULL,
+ };
+
+ EGLConfig config;
+ if (!mpegl_create_context_cb(ctx, p->egl_display, cb, &p->egl_context, &config))
+ goto uninit;
+
+ int cid, vID, n;
+ if (!eglGetConfigAttrib(p->egl_display, config, EGL_CONFIG_ID, &cid)) {
+ MP_FATAL(ctx, "Getting EGL_CONFIG_ID failed!\n");
+ goto uninit;
+ }
+ if (!eglGetConfigAttrib(p->egl_display, config, EGL_NATIVE_VISUAL_ID, &vID)) {
+ MP_FATAL(ctx, "Getting X visual ID failed!\n");
+ goto uninit;
+ }
+ MP_VERBOSE(ctx, "Choosing visual EGL config 0x%x, visual ID 0x%x\n", cid, vID);
+ XVisualInfo template = {.visualid = vID};
+ XVisualInfo *vi = XGetVisualInfo(vo->x11->display, VisualIDMask, &template, &n);
+
+ if (!vi) {
+ MP_FATAL(ctx, "Getting X visual failed!\n");
+ goto uninit;
+ }
+
+ if (!vo_x11_create_vo_window(vo, vi, "gl")) {
+ XFree(vi);
+ goto uninit;
+ }
+
+ XFree(vi);
+
+ p->egl_surface = mpegl_create_window_surface(
+ p->egl_display, config, &vo->x11->window);
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ p->egl_surface = eglCreateWindowSurface(
+ p->egl_display, config, (EGLNativeWindowType)vo->x11->window, NULL);
+ }
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(ctx, "Could not create EGL surface!\n");
+ goto uninit;
+ }
+
+ if (!eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context))
+ {
+ MP_FATAL(ctx, "Could not make context current!\n");
+ goto uninit;
+ }
+
+ mpegl_load_functions(&p->gl, ctx->log);
+
+ struct ra_gl_ctx_params params = {
+ .check_visible = mpegl_check_visible,
+ .swap_buffers = mpegl_swap_buffers,
+ .get_vsync = mpegl_get_vsync,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto uninit;
+
+ ra_add_native_resource(ctx->ra, "x11", vo->x11->display);
+
+ return true;
+
+uninit:
+ mpegl_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool mpegl_reconfig(struct ra_ctx *ctx)
+{
+ vo_x11_config_vo_window(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int mpegl_control(struct ra_ctx *ctx, int *events, int request,
+ void *arg)
+{
+ int ret = vo_x11_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+static void mpegl_wakeup(struct ra_ctx *ctx)
+{
+ vo_x11_wakeup(ctx->vo);
+}
+
+static void mpegl_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_x11_wait_events(ctx->vo, until_time_ns);
+}
+
+const struct ra_ctx_fns ra_ctx_x11_egl = {
+ .type = "opengl",
+ .name = "x11egl",
+ .reconfig = mpegl_reconfig,
+ .control = mpegl_control,
+ .wakeup = mpegl_wakeup,
+ .wait_events = mpegl_wait_events,
+ .init = mpegl_init,
+ .uninit = mpegl_uninit,
+};
diff --git a/video/out/opengl/egl_helpers.c b/video/out/opengl/egl_helpers.c
new file mode 100644
index 0000000..3bf6239
--- /dev/null
+++ b/video/out/opengl/egl_helpers.c
@@ -0,0 +1,381 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#if HAVE_LIBDL
+#include <dlfcn.h>
+#endif
+
+#include "common/common.h"
+
+#include "egl_helpers.h"
+#include "common.h"
+#include "utils.h"
+#include "context.h"
+
+#if HAVE_EGL_ANGLE
+// On Windows, egl_helpers.c is only used by ANGLE, where the EGL functions may
+// be loaded dynamically from ANGLE DLLs
+#include "angle_dynamic.h"
+#endif
+
+// EGL 1.5
+#ifndef EGL_CONTEXT_OPENGL_PROFILE_MASK
+#define EGL_CONTEXT_MAJOR_VERSION 0x3098
+#define EGL_CONTEXT_MINOR_VERSION 0x30FB
+#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
+#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
+#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1
+typedef intptr_t EGLAttrib;
+#endif
+
+// Not every EGL provider (like RPI) has these.
+#ifndef EGL_CONTEXT_FLAGS_KHR
+#define EGL_CONTEXT_FLAGS_KHR EGL_NONE
+#endif
+
+#ifndef EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR
+#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0
+#endif
+
+struct mp_egl_config_attr {
+ int attrib;
+ const char *name;
+};
+
+#define MP_EGL_ATTRIB(id) {id, # id}
+
+static const struct mp_egl_config_attr mp_egl_attribs[] = {
+ MP_EGL_ATTRIB(EGL_CONFIG_ID),
+ MP_EGL_ATTRIB(EGL_RED_SIZE),
+ MP_EGL_ATTRIB(EGL_GREEN_SIZE),
+ MP_EGL_ATTRIB(EGL_BLUE_SIZE),
+ MP_EGL_ATTRIB(EGL_ALPHA_SIZE),
+ MP_EGL_ATTRIB(EGL_COLOR_BUFFER_TYPE),
+ MP_EGL_ATTRIB(EGL_CONFIG_CAVEAT),
+ MP_EGL_ATTRIB(EGL_CONFORMANT),
+ MP_EGL_ATTRIB(EGL_NATIVE_VISUAL_ID),
+};
+
+static void dump_egl_config(struct mp_log *log, int msgl, EGLDisplay display,
+ EGLConfig config)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(mp_egl_attribs); n++) {
+ const char *name = mp_egl_attribs[n].name;
+ EGLint v = -1;
+ if (eglGetConfigAttrib(display, config, mp_egl_attribs[n].attrib, &v)) {
+ mp_msg(log, msgl, " %s=0x%x\n", name, v);
+ } else {
+ mp_msg(log, msgl, " %s=<error>\n", name);
+ }
+ }
+}
+
+static void *mpegl_get_proc_address(void *ctx, const char *name)
+{
+ void *p = eglGetProcAddress(name);
+#if defined(__GLIBC__) && HAVE_LIBDL
+ // Some crappy ARM/Linux things do not provide EGL 1.5, so above call does
+ // not necessarily return function pointers for core functions. Try to get
+ // them from a loaded GLES lib. As POSIX leaves RTLD_DEFAULT "reserved",
+ // use it only with glibc.
+ if (!p)
+ p = dlsym(RTLD_DEFAULT, name);
+#endif
+ return p;
+}
+
+static bool create_context(struct ra_ctx *ctx, EGLDisplay display,
+ bool es, struct mpegl_cb cb,
+ EGLContext *out_context, EGLConfig *out_config)
+{
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_FATAL;
+
+ EGLenum api;
+ EGLint rend;
+ const char *name;
+
+ if (!es) {
+ api = EGL_OPENGL_API;
+ rend = EGL_OPENGL_BIT;
+ name = "Desktop OpenGL";
+ } else {
+ api = EGL_OPENGL_ES_API;
+ rend = EGL_OPENGL_ES2_BIT;
+ name = "GLES 2.x +";
+ }
+
+ MP_VERBOSE(ctx, "Trying to create %s context.\n", name);
+
+ if (!eglBindAPI(api)) {
+ MP_VERBOSE(ctx, "Could not bind API!\n");
+ return false;
+ }
+
+ EGLint attributes[] = {
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_ALPHA_SIZE, ctx->opts.want_alpha ? 8 : 0,
+ EGL_RENDERABLE_TYPE, rend,
+ EGL_NONE
+ };
+
+ EGLint num_configs;
+ if (!eglChooseConfig(display, attributes, NULL, 0, &num_configs))
+ num_configs = 0;
+
+ EGLConfig *configs = talloc_array(NULL, EGLConfig, num_configs);
+ if (!eglChooseConfig(display, attributes, configs, num_configs, &num_configs))
+ num_configs = 0;
+
+ if (!num_configs) {
+ talloc_free(configs);
+ MP_MSG(ctx, msgl, "Could not choose EGLConfig for %s!\n", name);
+ return false;
+ }
+
+ for (int n = 0; n < num_configs; n++)
+ dump_egl_config(ctx->log, MSGL_TRACE, display, configs[n]);
+
+ int chosen = 0;
+ if (cb.refine_config)
+ chosen = cb.refine_config(cb.user_data, configs, num_configs);
+ if (chosen < 0) {
+ talloc_free(configs);
+ MP_MSG(ctx, msgl, "Could not refine EGLConfig for %s!\n", name);
+ return false;
+ }
+ EGLConfig config = configs[chosen];
+
+ talloc_free(configs);
+
+ MP_DBG(ctx, "Chosen EGLConfig:\n");
+ dump_egl_config(ctx->log, MSGL_DEBUG, display, config);
+
+ int ctx_flags = ctx->opts.debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0;
+ EGLContext *egl_ctx = NULL;
+
+ if (!es) {
+ for (int n = 0; mpgl_min_required_gl_versions[n]; n++) {
+ int ver = mpgl_min_required_gl_versions[n];
+
+ EGLint attrs[] = {
+ EGL_CONTEXT_MAJOR_VERSION, MPGL_VER_GET_MAJOR(ver),
+ EGL_CONTEXT_MINOR_VERSION, MPGL_VER_GET_MINOR(ver),
+ EGL_CONTEXT_OPENGL_PROFILE_MASK,
+ ver >= 320 ? EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT : 0,
+ EGL_CONTEXT_FLAGS_KHR, ctx_flags,
+ EGL_NONE
+ };
+
+ egl_ctx = eglCreateContext(display, config, EGL_NO_CONTEXT, attrs);
+ if (egl_ctx)
+ break;
+ }
+ }
+ if (!egl_ctx) {
+ // Fallback for EGL 1.4 without EGL_KHR_create_context or GLES
+ // Add the context flags only for GLES - GL has been attempted above
+ EGLint attrs[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ es ? EGL_CONTEXT_FLAGS_KHR : EGL_NONE, ctx_flags,
+ EGL_NONE
+ };
+
+ egl_ctx = eglCreateContext(display, config, EGL_NO_CONTEXT, attrs);
+ }
+
+ if (!egl_ctx) {
+ MP_MSG(ctx, msgl, "Could not create EGL context for %s!\n", name);
+ return false;
+ }
+
+ *out_context = egl_ctx;
+ *out_config = config;
+ return true;
+}
+
+#define STR_OR_ERR(s) ((s) ? (s) : "(error)")
+
+// Create a context and return it and the config it was created with. If it
+// returns false, the out_* pointers are set to NULL.
+// vo_flags is a combination of VOFLAG_* values.
+bool mpegl_create_context(struct ra_ctx *ctx, EGLDisplay display,
+ EGLContext *out_context, EGLConfig *out_config)
+{
+ return mpegl_create_context_cb(ctx, display, (struct mpegl_cb){0},
+ out_context, out_config);
+}
+
+// Create a context and return it and the config it was created with. If it
+// returns false, the out_* pointers are set to NULL.
+bool mpegl_create_context_cb(struct ra_ctx *ctx, EGLDisplay display,
+ struct mpegl_cb cb, EGLContext *out_context,
+ EGLConfig *out_config)
+{
+ *out_context = NULL;
+ *out_config = NULL;
+
+ const char *version = eglQueryString(display, EGL_VERSION);
+ const char *vendor = eglQueryString(display, EGL_VENDOR);
+ const char *apis = eglQueryString(display, EGL_CLIENT_APIS);
+ MP_VERBOSE(ctx, "EGL_VERSION=%s\nEGL_VENDOR=%s\nEGL_CLIENT_APIS=%s\n",
+ STR_OR_ERR(version), STR_OR_ERR(vendor), STR_OR_ERR(apis));
+
+ enum gles_mode mode = ra_gl_ctx_get_glesmode(ctx);
+
+ if ((mode == GLES_NO || mode == GLES_AUTO) &&
+ create_context(ctx, display, false, cb, out_context, out_config))
+ return true;
+
+ if ((mode == GLES_YES || mode == GLES_AUTO) &&
+ create_context(ctx, display, true, cb, out_context, out_config))
+ return true;
+
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+ MP_MSG(ctx, msgl, "Could not create a GL context.\n");
+ return false;
+}
+
+static int GLAPIENTRY swap_interval(int interval)
+{
+ EGLDisplay display = eglGetCurrentDisplay();
+ if (!display)
+ return 1;
+ return !eglSwapInterval(display, interval);
+}
+
+// Load gl version and function pointers into *gl.
+// Expects a current EGL context set.
+void mpegl_load_functions(struct GL *gl, struct mp_log *log)
+{
+ const char *egl_exts = "";
+ EGLDisplay display = eglGetCurrentDisplay();
+ if (display != EGL_NO_DISPLAY)
+ egl_exts = eglQueryString(display, EGL_EXTENSIONS);
+
+ mpgl_load_functions2(gl, mpegl_get_proc_address, NULL, egl_exts, log);
+ if (!gl->SwapInterval)
+ gl->SwapInterval = swap_interval;
+}
+
+static bool is_egl15(void)
+{
+ // It appears that EGL 1.4 is specified to _require_ an initialized display
+ // for EGL_VERSION, while EGL 1.5 is _required_ to return the EGL version.
+ const char *ver = eglQueryString(EGL_NO_DISPLAY, EGL_VERSION);
+ // Of course we have to go through the excruciating pain of parsing a
+ // version string, since EGL provides no other way without a display. In
+ // theory version!=NULL is already proof enough that it's 1.5, but be
+ // extra defensive, since this should have been true for EGL_EXTENSIONS as
+ // well, but then they added an extension that modified standard behavior.
+ int ma = 0, mi = 0;
+ return ver && sscanf(ver, "%d.%d", &ma, &mi) == 2 && (ma > 1 || mi >= 5);
+}
+
+// This is similar to eglGetPlatformDisplay(platform, native_display, NULL),
+// except that it 1. may use eglGetPlatformDisplayEXT, 2. checks for the
+// platform client extension platform_ext_name, and 3. does not support passing
+// an attrib list, because the type for that parameter is different in the EXT
+// and standard functions (EGL can't not fuck up, no matter what).
+// platform: e.g. EGL_PLATFORM_X11_KHR
+// platform_ext_name: e.g. "EGL_KHR_platform_x11"
+// native_display: e.g. X11 Display*
+// Returns EGL_NO_DISPLAY on failure.
+// Warning: the EGL version can be different at runtime depending on the chosen
+// platform, so this might return a display corresponding to some older EGL
+// version (often 1.4).
+// Often, there are two extension variants of a platform (KHR and EXT). If you
+// need to check both, call this function twice. (Why do they define them twice?
+// They're crazy.)
+EGLDisplay mpegl_get_display(EGLenum platform, const char *platform_ext_name,
+ void *native_display)
+{
+ // EGL is awful. Designed as ultra-portable library, it fails at dealing
+ // with slightly more complex environment than its short-sighted design
+ // could deal with. So they invented an awful, awful kludge that modifies
+ // EGL standard behavior, the EGL_EXT_client_extensions extension. EGL 1.4
+ // normally is to return NULL when querying EGL_EXTENSIONS on EGL_NO_DISPLAY,
+ // however, with that extension, it'll return the set of "client extensions",
+ // which may include EGL_EXT_platform_base.
+
+ // Prerequisite: check the platform extension.
+ // If this is either EGL 1.5, or 1.4 with EGL_EXT_client_extensions, then
+ // this must return a valid extension string.
+ const char *exts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, platform_ext_name))
+ return EGL_NO_DISPLAY;
+
+ // Before we go through the EGL 1.4 BS, try if we can use native EGL 1.5
+ if (is_egl15()) {
+ // This is EGL 1.5. It must support querying standard functions through
+ // eglGetProcAddress(). Note that on EGL 1.4, even if the function is
+ // unknown, it could return non-NULL anyway (because EGL is crazy).
+ EGLDisplay (EGLAPIENTRYP GetPlatformDisplay)
+ (EGLenum, void *, const EGLAttrib *) =
+ (void *)eglGetProcAddress("eglGetPlatformDisplay");
+ // (It should be impossible to be NULL, but uh.)
+ if (GetPlatformDisplay)
+ return GetPlatformDisplay(platform, native_display, NULL);
+ }
+
+ if (!gl_check_extension(exts, "EGL_EXT_platform_base"))
+ return EGL_NO_DISPLAY;
+
+ EGLDisplay (EGLAPIENTRYP GetPlatformDisplayEXT)(EGLenum, void*, const EGLint*)
+ = (void *)eglGetProcAddress("eglGetPlatformDisplayEXT");
+
+ // (It should be impossible to be NULL, but uh.)
+ if (GetPlatformDisplayEXT)
+ return GetPlatformDisplayEXT(platform, native_display, NULL);
+
+ return EGL_NO_DISPLAY;
+}
+
+// The same mess but with eglCreatePlatformWindowSurface(EXT)
+// again no support for an attribute list because the type differs
+// Returns EGL_NO_SURFACE on failure.
+EGLSurface mpegl_create_window_surface(EGLDisplay dpy, EGLConfig config,
+ void *native_window)
+{
+ // Use the EGL 1.5 function if possible
+ if (is_egl15()) {
+ EGLSurface (EGLAPIENTRYP CreatePlatformWindowSurface)
+ (EGLDisplay, EGLConfig, void *, const EGLAttrib *) =
+ (void *)eglGetProcAddress("eglCreatePlatformWindowSurface");
+ // (It should be impossible to be NULL, but uh.)
+ if (CreatePlatformWindowSurface)
+ return CreatePlatformWindowSurface(dpy, config, native_window, NULL);
+ }
+
+ // Check the extension that provides the *EXT function
+ const char *exts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_EXT_platform_base"))
+ return EGL_NO_SURFACE;
+
+ EGLSurface (EGLAPIENTRYP CreatePlatformWindowSurfaceEXT)
+ (EGLDisplay, EGLConfig, void *, const EGLint *) =
+ (void *)eglGetProcAddress("eglCreatePlatformWindowSurfaceEXT");
+ // (It should be impossible to be NULL, but uh.)
+ if (CreatePlatformWindowSurfaceEXT)
+ return CreatePlatformWindowSurfaceEXT(dpy, config, native_window, NULL);
+
+ return EGL_NO_SURFACE;
+}
diff --git a/video/out/opengl/egl_helpers.h b/video/out/opengl/egl_helpers.h
new file mode 100644
index 0000000..32ec5d1
--- /dev/null
+++ b/video/out/opengl/egl_helpers.h
@@ -0,0 +1,38 @@
+#ifndef MP_GL_EGL_HELPERS_H
+#define MP_GL_EGL_HELPERS_H
+
+#include <stdbool.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "video/out/gpu/context.h"
+
+struct mp_log;
+
+bool mpegl_create_context(struct ra_ctx *ctx, EGLDisplay display,
+ EGLContext *out_context, EGLConfig *out_config);
+
+struct mpegl_cb {
+ // if set, pick the desired config from the given list and return its index
+ // defaults to 0 (they are sorted by eglChooseConfig). return a negative
+ // number to indicate an error condition or that no suitable configs could
+ // be found.
+ int (*refine_config)(void *user_data, EGLConfig *configs, int num_configs);
+ void *user_data;
+};
+
+bool mpegl_create_context_cb(struct ra_ctx *ctx, EGLDisplay display,
+ struct mpegl_cb cb, EGLContext *out_context,
+ EGLConfig *out_config);
+
+struct GL;
+void mpegl_load_functions(struct GL *gl, struct mp_log *log);
+
+EGLDisplay mpegl_get_display(EGLenum platform, const char *platform_ext_name,
+ void *native_display);
+
+EGLSurface mpegl_create_window_surface(EGLDisplay dpy, EGLConfig config,
+ void *native_window);
+
+#endif
diff --git a/video/out/opengl/formats.c b/video/out/opengl/formats.c
new file mode 100644
index 0000000..a0b79e2
--- /dev/null
+++ b/video/out/opengl/formats.c
@@ -0,0 +1,196 @@
+#include "common/common.h"
+#include "formats.h"
+
+enum {
+ // --- GL type aliases (for readability)
+ T_U8 = GL_UNSIGNED_BYTE,
+ T_U16 = GL_UNSIGNED_SHORT,
+ T_FL = GL_FLOAT,
+};
+
+// List of allowed formats, and their usability for bilinear filtering and FBOs.
+// This is limited to combinations that are useful for our renderer.
+const struct gl_format gl_formats[] = {
+ // These are used for desktop GL 3+, and GLES 3+ with GL_EXT_texture_norm16.
+ {"r8", GL_R8, GL_RED, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"rg8", GL_RG8, GL_RG, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"rgb8", GL_RGB8, GL_RGB, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"rgba8", GL_RGBA8, GL_RGBA, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"r16", GL_R16, GL_RED, T_U16, F_CF | F_GL3 | F_GL2F | F_EXT16},
+ {"rg16", GL_RG16, GL_RG, T_U16, F_CF | F_GL3 | F_GL2F | F_EXT16},
+ {"rgb16", GL_RGB16, GL_RGB, T_U16, F_CF | F_GL3 | F_GL2F},
+ {"rgba16", GL_RGBA16, GL_RGBA, T_U16, F_CF | F_GL3 | F_GL2F | F_EXT16},
+
+ // Specifically not color-renderable.
+ {"rgb16", GL_RGB16, GL_RGB, T_U16, F_TF | F_EXT16},
+
+ // GL2 legacy. Ignores possibly present FBO extensions (no CF flag set).
+ {"l8", GL_LUMINANCE8, GL_LUMINANCE, T_U8, F_TF | F_GL2},
+ {"la8", GL_LUMINANCE8_ALPHA8, GL_LUMINANCE_ALPHA, T_U8, F_TF | F_GL2},
+ {"rgb8", GL_RGB8, GL_RGB, T_U8, F_TF | F_GL2},
+ {"rgba8", GL_RGBA8, GL_RGBA, T_U8, F_TF | F_GL2},
+ {"l16", GL_LUMINANCE16, GL_LUMINANCE, T_U16, F_TF | F_GL2},
+ {"la16", GL_LUMINANCE16_ALPHA16, GL_LUMINANCE_ALPHA, T_U16, F_TF | F_GL2},
+ {"rgb16", GL_RGB16, GL_RGB, T_U16, F_TF | F_GL2},
+ {"rgba16", GL_RGBA16, GL_RGBA, T_U16, F_TF | F_GL2},
+
+ // ES3 legacy. This is literally to compensate for Apple bugs in their iOS
+ // interop (can they do anything right?). ES3 still allows these formats,
+ // but they are deprecated.
+ {"l" , GL_LUMINANCE,GL_LUMINANCE, T_U8, F_CF | F_ES3},
+ {"la",GL_LUMINANCE_ALPHA,GL_LUMINANCE_ALPHA, T_U8, F_CF | F_ES3},
+
+ // ES2 legacy
+ {"l" , GL_LUMINANCE,GL_LUMINANCE, T_U8, F_TF | F_ES2},
+ {"la",GL_LUMINANCE_ALPHA,GL_LUMINANCE_ALPHA, T_U8, F_TF | F_ES2},
+ {"rgb", GL_RGB, GL_RGB, T_U8, F_TF | F_ES2},
+ {"rgba", GL_RGBA, GL_RGBA, T_U8, F_TF | F_ES2},
+
+ // Non-normalized integer formats.
+ // Follows ES 3.0 as to which are color-renderable.
+ {"r8ui", GL_R8UI, GL_RED_INTEGER, T_U8, F_CR | F_GL3 | F_ES3},
+ {"rg8ui", GL_RG8UI, GL_RG_INTEGER, T_U8, F_CR | F_GL3 | F_ES3},
+ {"rgb8ui", GL_RGB8UI, GL_RGB_INTEGER, T_U8, F_GL3 | F_ES3},
+ {"rgba8ui", GL_RGBA8UI, GL_RGBA_INTEGER, T_U8, F_CR | F_GL3 | F_ES3},
+ {"r16ui", GL_R16UI, GL_RED_INTEGER, T_U16, F_CR | F_GL3 | F_ES3},
+ {"rg16ui", GL_RG16UI, GL_RG_INTEGER, T_U16, F_CR | F_GL3 | F_ES3},
+ {"rgb16ui", GL_RGB16UI, GL_RGB_INTEGER, T_U16, F_GL3 | F_ES3},
+ {"rgba16ui",GL_RGBA16UI, GL_RGBA_INTEGER, T_U16, F_CR | F_GL3 | F_ES3},
+
+ // On GL3+ or GL2.1 with GL_ARB_texture_float, floats work fully.
+ {"r16f", GL_R16F, GL_RED, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"rg16f", GL_RG16F, GL_RG, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"rgb16f", GL_RGB16F, GL_RGB, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"rgba16f", GL_RGBA16F, GL_RGBA, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"r32f", GL_R32F, GL_RED, T_FL, F_CF | F_GL3 | F_GL2F},
+ {"rg32f", GL_RG32F, GL_RG, T_FL, F_CF | F_GL3 | F_GL2F},
+ {"rgb32f", GL_RGB32F, GL_RGB, T_FL, F_CF | F_GL3 | F_GL2F},
+ {"rgba32f", GL_RGBA32F, GL_RGBA, T_FL, F_CF | F_GL3 | F_GL2F},
+
+ // Note: we simply don't support float anything on ES2, despite extensions.
+ // We also don't bother with non-filterable float formats, and we ignore
+ // 32 bit float formats that are not blendable when rendering to them.
+
+ // On ES3.2+, both 16 bit floats work fully (except 3-component formats).
+ // F_EXTF16 implies extensions that also enable 16 bit floats fully.
+ {"r16f", GL_R16F, GL_RED, T_FL, F_F16 | F_CF | F_ES32 | F_EXTF16},
+ {"rg16f", GL_RG16F, GL_RG, T_FL, F_F16 | F_CF | F_ES32 | F_EXTF16},
+ {"rgb16f", GL_RGB16F, GL_RGB, T_FL, F_F16 | F_TF | F_ES32 | F_EXTF16},
+ {"rgba16f", GL_RGBA16F, GL_RGBA, T_FL, F_F16 | F_CF | F_ES32 | F_EXTF16},
+
+ // On ES3.0+, 16 bit floats are texture-filterable.
+ // Don't bother with 32 bit floats; they exist but are neither CR nor TF.
+ {"r16f", GL_R16F, GL_RED, T_FL, F_F16 | F_TF | F_ES3},
+ {"rg16f", GL_RG16F, GL_RG, T_FL, F_F16 | F_TF | F_ES3},
+ {"rgb16f", GL_RGB16F, GL_RGB, T_FL, F_F16 | F_TF | F_ES3},
+ {"rgba16f", GL_RGBA16F, GL_RGBA, T_FL, F_F16 | F_TF | F_ES3},
+
+ // These might be useful as FBO formats.
+ {"rgb10_a2",GL_RGB10_A2, GL_RGBA,
+ GL_UNSIGNED_INT_2_10_10_10_REV, F_CF | F_GL3 | F_ES3},
+ {"rgba12", GL_RGBA12, GL_RGBA, T_U16, F_CF | F_GL2 | F_GL3},
+ {"rgb10", GL_RGB10, GL_RGB, T_U16, F_CF | F_GL2 | F_GL3},
+
+ // Special formats.
+ {"rgb565", GL_RGB8, GL_RGB,
+ GL_UNSIGNED_SHORT_5_6_5, F_TF | F_GL2 | F_GL3},
+ // Worthless, but needed by OSX videotoolbox interop on old Apple hardware.
+ {"appleyp", GL_RGB, GL_RGB_422_APPLE,
+ GL_UNSIGNED_SHORT_8_8_APPLE, F_TF | F_APPL},
+
+ {0}
+};
+
+// Return an or-ed combination of all F_ flags that apply.
+int gl_format_feature_flags(GL *gl)
+{
+ return (gl->version == 210 ? F_GL2 : 0)
+ | (gl->version >= 300 ? F_GL3 : 0)
+ | (gl->es == 200 ? F_ES2 : 0)
+ | (gl->es >= 300 ? F_ES3 : 0)
+ | (gl->es >= 320 ? F_ES32 : 0)
+ | (gl->mpgl_caps & MPGL_CAP_EXT16 ? F_EXT16 : 0)
+ | ((gl->es >= 300 &&
+ (gl->mpgl_caps & MPGL_CAP_EXT_CR_HFLOAT)) ? F_EXTF16 : 0)
+ | ((gl->version == 210 &&
+ (gl->mpgl_caps & MPGL_CAP_ARB_FLOAT) &&
+ (gl->mpgl_caps & MPGL_CAP_TEX_RG) &&
+ (gl->mpgl_caps & MPGL_CAP_FB)) ? F_GL2F : 0)
+ | (gl->mpgl_caps & MPGL_CAP_APPLE_RGB_422 ? F_APPL : 0);
+}
+
+int gl_format_type(const struct gl_format *format)
+{
+ if (!format)
+ return 0;
+ if (format->type == GL_FLOAT)
+ return MPGL_TYPE_FLOAT;
+ if (gl_integer_format_to_base(format->format))
+ return MPGL_TYPE_UINT;
+ return MPGL_TYPE_UNORM;
+}
+
+// Return base internal format of an integer format, or 0 if it's not integer.
+// "format" is like in struct gl_format.
+GLenum gl_integer_format_to_base(GLenum format)
+{
+ switch (format) {
+ case GL_RED_INTEGER: return GL_RED;
+ case GL_RG_INTEGER: return GL_RG;
+ case GL_RGB_INTEGER: return GL_RGB;
+ case GL_RGBA_INTEGER: return GL_RGBA;
+ }
+ return 0;
+}
+
+// Return the number of bytes per component this format implies.
+// Returns 0 for formats with non-byte alignments and formats which
+// merge multiple components (like GL_UNSIGNED_SHORT_5_6_5).
+// "type" is like in struct gl_format.
+int gl_component_size(GLenum type)
+{
+ switch (type) {
+ case GL_UNSIGNED_BYTE: return 1;
+ case GL_UNSIGNED_SHORT: return 2;
+ case GL_FLOAT: return 4;
+ }
+ return 0;
+}
+
+// Return the number of separate color components.
+// "format" is like in struct gl_format.
+int gl_format_components(GLenum format)
+{
+ switch (format) {
+ case GL_RED:
+ case GL_RED_INTEGER:
+ case GL_LUMINANCE:
+ return 1;
+ case GL_RG:
+ case GL_RG_INTEGER:
+ case GL_LUMINANCE_ALPHA:
+ return 2;
+ case GL_RGB:
+ case GL_RGB_INTEGER:
+ return 3;
+ case GL_RGBA:
+ case GL_RGBA_INTEGER:
+ return 4;
+ }
+ return 0;
+}
+
+// Return the number of bytes per pixel for the given format.
+// Parameter names like in struct gl_format.
+int gl_bytes_per_pixel(GLenum format, GLenum type)
+{
+ // Formats with merged components are special.
+ switch (type) {
+ case GL_UNSIGNED_INT_2_10_10_10_REV: return 4;
+ case GL_UNSIGNED_SHORT_5_6_5: return 2;
+ case GL_UNSIGNED_SHORT_8_8_APPLE: return 2;
+ case GL_UNSIGNED_SHORT_8_8_REV_APPLE: return 2;
+ }
+
+ return gl_component_size(type) * gl_format_components(format);
+}
diff --git a/video/out/opengl/formats.h b/video/out/opengl/formats.h
new file mode 100644
index 0000000..f727a3b
--- /dev/null
+++ b/video/out/opengl/formats.h
@@ -0,0 +1,51 @@
+#ifndef MPGL_FORMATS_H_
+#define MPGL_FORMATS_H_
+
+#include "common.h"
+
+struct gl_format {
+ const char *name; // symbolic name for user interaction/debugging
+ GLint internal_format; // glTexImage argument
+ GLenum format; // glTexImage argument
+ GLenum type; // e.g. GL_UNSIGNED_SHORT
+ int flags; // F_* flags
+};
+
+enum {
+ // --- gl_format.flags
+
+ // Version flags. If at least 1 flag matches, the format entry is considered
+ // supported on the current GL context.
+ F_GL2 = 1 << 0, // GL2.1-only
+ F_GL3 = 1 << 1, // GL3.0 or later
+ F_ES2 = 1 << 2, // ES2-only
+ F_ES3 = 1 << 3, // ES3.0 or later
+ F_ES32 = 1 << 4, // ES3.2 or later
+ F_EXT16 = 1 << 5, // ES with GL_EXT_texture_norm16
+ F_EXTF16 = 1 << 6, // GL_EXT_color_buffer_half_float
+ F_GL2F = 1 << 7, // GL2.1-only with texture_rg + texture_float + FBOs
+ F_APPL = 1 << 8, // GL_APPLE_rgb_422
+
+ // Feature flags. They are additional and signal presence of features.
+ F_CR = 1 << 16, // color-renderable
+ F_TF = 1 << 17, // texture-filterable with GL_LINEAR
+ F_CF = F_CR | F_TF,
+ F_F16 = 1 << 18, // uses half-floats (16 bit) internally, even though
+ // the format is still GL_FLOAT (32 bit)
+
+ // --- Other constants.
+ MPGL_TYPE_UNORM = RA_CTYPE_UNORM, // normalized integer (fixed point) formats
+ MPGL_TYPE_UINT = RA_CTYPE_UINT, // full integer formats
+ MPGL_TYPE_FLOAT = RA_CTYPE_FLOAT, // float formats (both full and half)
+};
+
+extern const struct gl_format gl_formats[];
+
+int gl_format_feature_flags(GL *gl);
+int gl_format_type(const struct gl_format *format);
+GLenum gl_integer_format_to_base(GLenum format);
+int gl_component_size(GLenum type);
+int gl_format_components(GLenum format);
+int gl_bytes_per_pixel(GLenum format, GLenum type);
+
+#endif
diff --git a/video/out/opengl/gl_headers.h b/video/out/opengl/gl_headers.h
new file mode 100644
index 0000000..5c36718
--- /dev/null
+++ b/video/out/opengl/gl_headers.h
@@ -0,0 +1,799 @@
+/*
+ * Parts of OpenGL(ES) needed by the OpenGL renderer.
+ *
+ * This excludes function declarations.
+ *
+ * This header is based on:
+ * - Khronos GLES headers (MIT)
+ * - mpv or MPlayer code (LGPL 2.1 or later)
+ * - probably Mesa GL headers (MIT)
+ */
+
+#ifndef MPV_GL_HEADERS_H
+#define MPV_GL_HEADERS_H
+
+#include <stdint.h>
+
+// Enable this to use system headers instead.
+#if 0
+#include <GL/gl.h>
+#include <GLES3/gl3.h>
+#endif
+
+#ifndef GLAPIENTRY
+#ifdef _WIN32
+#define GLAPIENTRY __stdcall
+#else
+#define GLAPIENTRY
+#endif
+#endif
+
+// Typedefs. This needs to work with system headers too (consider GLX), and
+// before C11, duplicated typedefs were an error. So try to tolerate at least
+// Mesa.
+#ifdef GL_TRUE
+ // Tolerate old Mesa which has only definitions up to GL 2.0.
+ #define MP_GET_GL_TYPES_2_0 0
+ #ifdef GL_VERSION_3_2
+ #define MP_GET_GL_TYPES_3_2 0
+ #else
+ #define MP_GET_GL_TYPES_3_2 1
+ #endif
+#else
+ // Get them all.
+ #define MP_GET_GL_TYPES_2_0 1
+ #define MP_GET_GL_TYPES_3_2 1
+#endif
+
+#if MP_GET_GL_TYPES_2_0
+// GL_VERSION_1_0, GL_ES_VERSION_2_0
+typedef unsigned int GLbitfield;
+typedef unsigned char GLboolean;
+typedef unsigned int GLenum;
+typedef float GLfloat;
+typedef int GLint;
+typedef int GLsizei;
+typedef uint8_t GLubyte;
+typedef unsigned int GLuint;
+typedef void GLvoid;
+// GL 1.1 GL_VERSION_1_1, GL_ES_VERSION_2_0
+typedef float GLclampf;
+// GL 1.5 GL_VERSION_1_5, GL_ES_VERSION_2_0
+typedef intptr_t GLintptr;
+typedef ptrdiff_t GLsizeiptr;
+// GL 2.0 GL_VERSION_2_0, GL_ES_VERSION_2_0
+typedef int8_t GLbyte;
+typedef char GLchar;
+typedef short GLshort;
+typedef unsigned short GLushort;
+#endif
+
+#if MP_GET_GL_TYPES_3_2
+// GL 3.2 GL_VERSION_3_2, GL_ES_VERSION_2_0
+typedef int64_t GLint64;
+typedef struct __GLsync *GLsync;
+typedef uint64_t GLuint64;
+#endif
+
+// --- GL 1.1
+
+#define GL_BACK_LEFT 0x0402
+#define GL_TEXTURE_1D 0x0DE0
+#define GL_RGB16 0x8054
+#define GL_RGB10 0x8052
+#define GL_RGBA12 0x805A
+#define GL_RGBA16 0x805B
+#define GL_TEXTURE_RED_SIZE 0x805C
+#define GL_TEXTURE_GREEN_SIZE 0x805D
+#define GL_TEXTURE_BLUE_SIZE 0x805E
+#define GL_TEXTURE_ALPHA_SIZE 0x805F
+
+// --- GL 1.1 (removed from 3.0 core and not in GLES 2/3)
+
+#define GL_TEXTURE_LUMINANCE_SIZE 0x8060
+#define GL_LUMINANCE8 0x8040
+#define GL_LUMINANCE8_ALPHA8 0x8045
+#define GL_LUMINANCE16 0x8042
+#define GL_LUMINANCE16_ALPHA16 0x8048
+
+// --- GL 1.5
+
+#define GL_READ_ONLY 0x88B8
+#define GL_WRITE_ONLY 0x88B9
+#define GL_READ_WRITE 0x88BA
+
+// --- GL 3.0
+
+#define GL_R16 0x822A
+#define GL_RG16 0x822C
+
+// --- GL 3.1
+
+#define GL_TEXTURE_RECTANGLE 0x84F5
+
+// --- GL 3.3 or GL_ARB_timer_query
+
+#define GL_TIME_ELAPSED 0x88BF
+#define GL_TIMESTAMP 0x8E28
+
+// --- GL 4.3 or GL_ARB_debug_output
+
+#define GL_DEBUG_SEVERITY_HIGH 0x9146
+#define GL_DEBUG_SEVERITY_MEDIUM 0x9147
+#define GL_DEBUG_SEVERITY_LOW 0x9148
+#define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B
+
+// --- GL 4.4 or GL_ARB_buffer_storage
+
+#define GL_MAP_PERSISTENT_BIT 0x0040
+#define GL_MAP_COHERENT_BIT 0x0080
+#define GL_DYNAMIC_STORAGE_BIT 0x0100
+#define GL_CLIENT_STORAGE_BIT 0x0200
+
+// --- GL 4.2 or GL_ARB_image_load_store
+
+#define GL_TEXTURE_FETCH_BARRIER_BIT 0x00000008
+
+// --- GL 4.3 or GL_ARB_compute_shader
+
+#define GL_COMPUTE_SHADER 0x91B9
+#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
+#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB
+
+// --- GL 4.3 or GL_ARB_shader_storage_buffer_object
+
+#define GL_SHADER_STORAGE_BUFFER 0x90D2
+#define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000
+
+// --- GL_NV_vdpau_interop
+
+#define GLvdpauSurfaceNV GLintptr
+#define GL_WRITE_DISCARD_NV 0x88BE
+
+// --- GL_OES_EGL_image_external, GL_NV_EGL_stream_consumer_external
+
+#define GL_TEXTURE_EXTERNAL_OES 0x8D65
+
+// --- GL_APPLE_rgb_422
+
+#define GL_RGB_422_APPLE 0x8A1F
+#define GL_UNSIGNED_SHORT_8_8_APPLE 0x85BA
+#define GL_UNSIGNED_SHORT_8_8_REV_APPLE 0x85BB
+
+// --- GL_ANGLE_translated_shader_source
+
+#define GL_TRANSLATED_SHADER_SOURCE_LENGTH_ANGLE 0x93A0
+
+// ---- GLES 2
+
+#define GL_DEPTH_BUFFER_BIT 0x00000100
+#define GL_STENCIL_BUFFER_BIT 0x00000400
+#define GL_COLOR_BUFFER_BIT 0x00004000
+#define GL_FALSE 0
+#define GL_TRUE 1
+#define GL_POINTS 0x0000
+#define GL_LINES 0x0001
+#define GL_LINE_LOOP 0x0002
+#define GL_LINE_STRIP 0x0003
+#define GL_TRIANGLES 0x0004
+#define GL_TRIANGLE_STRIP 0x0005
+#define GL_TRIANGLE_FAN 0x0006
+#define GL_ZERO 0
+#define GL_ONE 1
+#define GL_SRC_COLOR 0x0300
+#define GL_ONE_MINUS_SRC_COLOR 0x0301
+#define GL_SRC_ALPHA 0x0302
+#define GL_ONE_MINUS_SRC_ALPHA 0x0303
+#define GL_DST_ALPHA 0x0304
+#define GL_ONE_MINUS_DST_ALPHA 0x0305
+#define GL_DST_COLOR 0x0306
+#define GL_ONE_MINUS_DST_COLOR 0x0307
+#define GL_SRC_ALPHA_SATURATE 0x0308
+#define GL_FUNC_ADD 0x8006
+#define GL_BLEND_EQUATION 0x8009
+#define GL_BLEND_EQUATION_RGB 0x8009
+#define GL_BLEND_EQUATION_ALPHA 0x883D
+#define GL_FUNC_SUBTRACT 0x800A
+#define GL_FUNC_REVERSE_SUBTRACT 0x800B
+#define GL_BLEND_DST_RGB 0x80C8
+#define GL_BLEND_SRC_RGB 0x80C9
+#define GL_BLEND_DST_ALPHA 0x80CA
+#define GL_BLEND_SRC_ALPHA 0x80CB
+#define GL_CONSTANT_COLOR 0x8001
+#define GL_ONE_MINUS_CONSTANT_COLOR 0x8002
+#define GL_CONSTANT_ALPHA 0x8003
+#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004
+#define GL_BLEND_COLOR 0x8005
+#define GL_ARRAY_BUFFER 0x8892
+#define GL_ELEMENT_ARRAY_BUFFER 0x8893
+#define GL_ARRAY_BUFFER_BINDING 0x8894
+#define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895
+#define GL_STREAM_DRAW 0x88E0
+#define GL_STATIC_DRAW 0x88E4
+#define GL_DYNAMIC_DRAW 0x88E8
+#define GL_BUFFER_SIZE 0x8764
+#define GL_BUFFER_USAGE 0x8765
+#define GL_CURRENT_VERTEX_ATTRIB 0x8626
+#define GL_FRONT 0x0404
+#define GL_BACK 0x0405
+#define GL_FRONT_AND_BACK 0x0408
+#define GL_TEXTURE_2D 0x0DE1
+#define GL_CULL_FACE 0x0B44
+#define GL_BLEND 0x0BE2
+#define GL_DITHER 0x0BD0
+#define GL_STENCIL_TEST 0x0B90
+#define GL_DEPTH_TEST 0x0B71
+#define GL_SCISSOR_TEST 0x0C11
+#define GL_POLYGON_OFFSET_FILL 0x8037
+#define GL_SAMPLE_ALPHA_TO_COVERAGE 0x809E
+#define GL_SAMPLE_COVERAGE 0x80A0
+#define GL_NO_ERROR 0
+#define GL_INVALID_ENUM 0x0500
+#define GL_INVALID_VALUE 0x0501
+#define GL_INVALID_OPERATION 0x0502
+#define GL_OUT_OF_MEMORY 0x0505
+#define GL_CW 0x0900
+#define GL_CCW 0x0901
+#define GL_LINE_WIDTH 0x0B21
+#define GL_ALIASED_POINT_SIZE_RANGE 0x846D
+#define GL_ALIASED_LINE_WIDTH_RANGE 0x846E
+#define GL_CULL_FACE_MODE 0x0B45
+#define GL_FRONT_FACE 0x0B46
+#define GL_DEPTH_RANGE 0x0B70
+#define GL_DEPTH_WRITEMASK 0x0B72
+#define GL_DEPTH_CLEAR_VALUE 0x0B73
+#define GL_DEPTH_FUNC 0x0B74
+#define GL_STENCIL_CLEAR_VALUE 0x0B91
+#define GL_STENCIL_FUNC 0x0B92
+#define GL_STENCIL_FAIL 0x0B94
+#define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95
+#define GL_STENCIL_PASS_DEPTH_PASS 0x0B96
+#define GL_STENCIL_REF 0x0B97
+#define GL_STENCIL_VALUE_MASK 0x0B93
+#define GL_STENCIL_WRITEMASK 0x0B98
+#define GL_STENCIL_BACK_FUNC 0x8800
+#define GL_STENCIL_BACK_FAIL 0x8801
+#define GL_STENCIL_BACK_PASS_DEPTH_FAIL 0x8802
+#define GL_STENCIL_BACK_PASS_DEPTH_PASS 0x8803
+#define GL_STENCIL_BACK_REF 0x8CA3
+#define GL_STENCIL_BACK_VALUE_MASK 0x8CA4
+#define GL_STENCIL_BACK_WRITEMASK 0x8CA5
+#define GL_VIEWPORT 0x0BA2
+#define GL_SCISSOR_BOX 0x0C10
+#define GL_COLOR_CLEAR_VALUE 0x0C22
+#define GL_COLOR_WRITEMASK 0x0C23
+#define GL_UNPACK_ALIGNMENT 0x0CF5
+#define GL_PACK_ALIGNMENT 0x0D05
+#define GL_MAX_TEXTURE_SIZE 0x0D33
+#define GL_MAX_VIEWPORT_DIMS 0x0D3A
+#define GL_SUBPIXEL_BITS 0x0D50
+#define GL_RED_BITS 0x0D52
+#define GL_GREEN_BITS 0x0D53
+#define GL_BLUE_BITS 0x0D54
+#define GL_ALPHA_BITS 0x0D55
+#define GL_DEPTH_BITS 0x0D56
+#define GL_STENCIL_BITS 0x0D57
+#define GL_POLYGON_OFFSET_UNITS 0x2A00
+#define GL_POLYGON_OFFSET_FACTOR 0x8038
+#define GL_TEXTURE_BINDING_2D 0x8069
+#define GL_SAMPLE_BUFFERS 0x80A8
+#define GL_SAMPLES 0x80A9
+#define GL_SAMPLE_COVERAGE_VALUE 0x80AA
+#define GL_SAMPLE_COVERAGE_INVERT 0x80AB
+#define GL_NUM_COMPRESSED_TEXTURE_FORMATS 0x86A2
+#define GL_COMPRESSED_TEXTURE_FORMATS 0x86A3
+#define GL_DONT_CARE 0x1100
+#define GL_FASTEST 0x1101
+#define GL_NICEST 0x1102
+#define GL_GENERATE_MIPMAP_HINT 0x8192
+#define GL_BYTE 0x1400
+#define GL_UNSIGNED_BYTE 0x1401
+#define GL_SHORT 0x1402
+#define GL_UNSIGNED_SHORT 0x1403
+#define GL_INT 0x1404
+#define GL_UNSIGNED_INT 0x1405
+#define GL_FLOAT 0x1406
+#define GL_FIXED 0x140C
+#define GL_DEPTH_COMPONENT 0x1902
+#define GL_ALPHA 0x1906
+#define GL_RGB 0x1907
+#define GL_RGBA 0x1908
+#define GL_LUMINANCE 0x1909
+#define GL_LUMINANCE_ALPHA 0x190A
+#define GL_UNSIGNED_SHORT_4_4_4_4 0x8033
+#define GL_UNSIGNED_SHORT_5_5_5_1 0x8034
+#define GL_UNSIGNED_SHORT_5_6_5 0x8363
+#define GL_FRAGMENT_SHADER 0x8B30
+#define GL_VERTEX_SHADER 0x8B31
+#define GL_MAX_VERTEX_ATTRIBS 0x8869
+#define GL_MAX_VERTEX_UNIFORM_VECTORS 0x8DFB
+#define GL_MAX_VARYING_VECTORS 0x8DFC
+#define GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 0x8B4D
+#define GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS 0x8B4C
+#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
+#define GL_MAX_FRAGMENT_UNIFORM_VECTORS 0x8DFD
+#define GL_SHADER_TYPE 0x8B4F
+#define GL_DELETE_STATUS 0x8B80
+#define GL_LINK_STATUS 0x8B82
+#define GL_VALIDATE_STATUS 0x8B83
+#define GL_ATTACHED_SHADERS 0x8B85
+#define GL_ACTIVE_UNIFORMS 0x8B86
+#define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87
+#define GL_ACTIVE_ATTRIBUTES 0x8B89
+#define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A
+#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
+#define GL_CURRENT_PROGRAM 0x8B8D
+#define GL_NEVER 0x0200
+#define GL_LESS 0x0201
+#define GL_EQUAL 0x0202
+#define GL_LEQUAL 0x0203
+#define GL_GREATER 0x0204
+#define GL_NOTEQUAL 0x0205
+#define GL_GEQUAL 0x0206
+#define GL_ALWAYS 0x0207
+#define GL_KEEP 0x1E00
+#define GL_REPLACE 0x1E01
+#define GL_INCR 0x1E02
+#define GL_DECR 0x1E03
+#define GL_INVERT 0x150A
+#define GL_INCR_WRAP 0x8507
+#define GL_DECR_WRAP 0x8508
+#define GL_VENDOR 0x1F00
+#define GL_RENDERER 0x1F01
+#define GL_VERSION 0x1F02
+#define GL_EXTENSIONS 0x1F03
+#define GL_NEAREST 0x2600
+#define GL_LINEAR 0x2601
+#define GL_NEAREST_MIPMAP_NEAREST 0x2700
+#define GL_LINEAR_MIPMAP_NEAREST 0x2701
+#define GL_NEAREST_MIPMAP_LINEAR 0x2702
+#define GL_LINEAR_MIPMAP_LINEAR 0x2703
+#define GL_TEXTURE_MAG_FILTER 0x2800
+#define GL_TEXTURE_MIN_FILTER 0x2801
+#define GL_TEXTURE_WRAP_S 0x2802
+#define GL_TEXTURE_WRAP_T 0x2803
+#define GL_TEXTURE 0x1702
+#define GL_TEXTURE_CUBE_MAP 0x8513
+#define GL_TEXTURE_BINDING_CUBE_MAP 0x8514
+#define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515
+#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516
+#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517
+#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518
+#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519
+#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A
+#define GL_MAX_CUBE_MAP_TEXTURE_SIZE 0x851C
+#define GL_TEXTURE0 0x84C0
+#define GL_TEXTURE1 0x84C1
+#define GL_TEXTURE2 0x84C2
+#define GL_TEXTURE3 0x84C3
+#define GL_TEXTURE4 0x84C4
+#define GL_TEXTURE5 0x84C5
+#define GL_TEXTURE6 0x84C6
+#define GL_TEXTURE7 0x84C7
+#define GL_TEXTURE8 0x84C8
+#define GL_TEXTURE9 0x84C9
+#define GL_TEXTURE10 0x84CA
+#define GL_TEXTURE11 0x84CB
+#define GL_TEXTURE12 0x84CC
+#define GL_TEXTURE13 0x84CD
+#define GL_TEXTURE14 0x84CE
+#define GL_TEXTURE15 0x84CF
+#define GL_TEXTURE16 0x84D0
+#define GL_TEXTURE17 0x84D1
+#define GL_TEXTURE18 0x84D2
+#define GL_TEXTURE19 0x84D3
+#define GL_TEXTURE20 0x84D4
+#define GL_TEXTURE21 0x84D5
+#define GL_TEXTURE22 0x84D6
+#define GL_TEXTURE23 0x84D7
+#define GL_TEXTURE24 0x84D8
+#define GL_TEXTURE25 0x84D9
+#define GL_TEXTURE26 0x84DA
+#define GL_TEXTURE27 0x84DB
+#define GL_TEXTURE28 0x84DC
+#define GL_TEXTURE29 0x84DD
+#define GL_TEXTURE30 0x84DE
+#define GL_TEXTURE31 0x84DF
+#define GL_ACTIVE_TEXTURE 0x84E0
+#define GL_REPEAT 0x2901
+#define GL_CLAMP_TO_EDGE 0x812F
+#define GL_MIRRORED_REPEAT 0x8370
+#define GL_FLOAT_VEC2 0x8B50
+#define GL_FLOAT_VEC3 0x8B51
+#define GL_FLOAT_VEC4 0x8B52
+#define GL_INT_VEC2 0x8B53
+#define GL_INT_VEC3 0x8B54
+#define GL_INT_VEC4 0x8B55
+#define GL_BOOL 0x8B56
+#define GL_BOOL_VEC2 0x8B57
+#define GL_BOOL_VEC3 0x8B58
+#define GL_BOOL_VEC4 0x8B59
+#define GL_FLOAT_MAT2 0x8B5A
+#define GL_FLOAT_MAT3 0x8B5B
+#define GL_FLOAT_MAT4 0x8B5C
+#define GL_SAMPLER_2D 0x8B5E
+#define GL_SAMPLER_CUBE 0x8B60
+#define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622
+#define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623
+#define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624
+#define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625
+#define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A
+#define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645
+#define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING 0x889F
+#define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A
+#define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B
+#define GL_COMPILE_STATUS 0x8B81
+#define GL_INFO_LOG_LENGTH 0x8B84
+#define GL_SHADER_SOURCE_LENGTH 0x8B88
+#define GL_SHADER_COMPILER 0x8DFA
+#define GL_SHADER_BINARY_FORMATS 0x8DF8
+#define GL_NUM_SHADER_BINARY_FORMATS 0x8DF9
+#define GL_LOW_FLOAT 0x8DF0
+#define GL_MEDIUM_FLOAT 0x8DF1
+#define GL_HIGH_FLOAT 0x8DF2
+#define GL_LOW_INT 0x8DF3
+#define GL_MEDIUM_INT 0x8DF4
+#define GL_HIGH_INT 0x8DF5
+#define GL_FRAMEBUFFER 0x8D40
+#define GL_RENDERBUFFER 0x8D41
+#define GL_RGBA4 0x8056
+#define GL_RGB5_A1 0x8057
+#define GL_RGB565 0x8D62
+#define GL_DEPTH_COMPONENT16 0x81A5
+#define GL_STENCIL_INDEX8 0x8D48
+#define GL_RENDERBUFFER_WIDTH 0x8D42
+#define GL_RENDERBUFFER_HEIGHT 0x8D43
+#define GL_RENDERBUFFER_INTERNAL_FORMAT 0x8D44
+#define GL_RENDERBUFFER_RED_SIZE 0x8D50
+#define GL_RENDERBUFFER_GREEN_SIZE 0x8D51
+#define GL_RENDERBUFFER_BLUE_SIZE 0x8D52
+#define GL_RENDERBUFFER_ALPHA_SIZE 0x8D53
+#define GL_RENDERBUFFER_DEPTH_SIZE 0x8D54
+#define GL_RENDERBUFFER_STENCIL_SIZE 0x8D55
+#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE 0x8CD0
+#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME 0x8CD1
+#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL 0x8CD2
+#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE 0x8CD3
+#define GL_COLOR_ATTACHMENT0 0x8CE0
+#define GL_DEPTH_ATTACHMENT 0x8D00
+#define GL_STENCIL_ATTACHMENT 0x8D20
+#define GL_NONE 0
+#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
+#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6
+#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
+#define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS 0x8CD9
+#define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD
+#define GL_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_RENDERBUFFER_BINDING 0x8CA7
+#define GL_MAX_RENDERBUFFER_SIZE 0x84E8
+#define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506
+
+// ---- GLES 3
+
+#ifndef GL_READ_BUFFER
+typedef unsigned short GLhalf;
+#endif
+
+#define GL_READ_BUFFER 0x0C02
+#define GL_UNPACK_ROW_LENGTH 0x0CF2
+#define GL_UNPACK_SKIP_ROWS 0x0CF3
+#define GL_UNPACK_SKIP_PIXELS 0x0CF4
+#define GL_PACK_ROW_LENGTH 0x0D02
+#define GL_PACK_SKIP_ROWS 0x0D03
+#define GL_PACK_SKIP_PIXELS 0x0D04
+#define GL_COLOR 0x1800
+#define GL_DEPTH 0x1801
+#define GL_STENCIL 0x1802
+#define GL_RED 0x1903
+#define GL_RGB8 0x8051
+#define GL_RGBA8 0x8058
+#define GL_RGB10_A2 0x8059
+#define GL_TEXTURE_BINDING_3D 0x806A
+#define GL_UNPACK_SKIP_IMAGES 0x806D
+#define GL_UNPACK_IMAGE_HEIGHT 0x806E
+#define GL_TEXTURE_3D 0x806F
+#define GL_TEXTURE_WRAP_R 0x8072
+#define GL_MAX_3D_TEXTURE_SIZE 0x8073
+#define GL_UNSIGNED_INT_2_10_10_10_REV 0x8368
+#define GL_MAX_ELEMENTS_VERTICES 0x80E8
+#define GL_MAX_ELEMENTS_INDICES 0x80E9
+#define GL_TEXTURE_MIN_LOD 0x813A
+#define GL_TEXTURE_MAX_LOD 0x813B
+#define GL_TEXTURE_BASE_LEVEL 0x813C
+#define GL_TEXTURE_MAX_LEVEL 0x813D
+#define GL_MIN 0x8007
+#define GL_MAX 0x8008
+#define GL_DEPTH_COMPONENT24 0x81A6
+#define GL_MAX_TEXTURE_LOD_BIAS 0x84FD
+#define GL_TEXTURE_COMPARE_MODE 0x884C
+#define GL_TEXTURE_COMPARE_FUNC 0x884D
+#define GL_CURRENT_QUERY 0x8865
+#define GL_QUERY_RESULT 0x8866
+#define GL_QUERY_RESULT_AVAILABLE 0x8867
+#define GL_BUFFER_MAPPED 0x88BC
+#define GL_BUFFER_MAP_POINTER 0x88BD
+#define GL_STREAM_READ 0x88E1
+#define GL_STREAM_COPY 0x88E2
+#define GL_STATIC_READ 0x88E5
+#define GL_STATIC_COPY 0x88E6
+#define GL_DYNAMIC_READ 0x88E9
+#define GL_DYNAMIC_COPY 0x88EA
+#define GL_MAX_DRAW_BUFFERS 0x8824
+#define GL_DRAW_BUFFER0 0x8825
+#define GL_DRAW_BUFFER1 0x8826
+#define GL_DRAW_BUFFER2 0x8827
+#define GL_DRAW_BUFFER3 0x8828
+#define GL_DRAW_BUFFER4 0x8829
+#define GL_DRAW_BUFFER5 0x882A
+#define GL_DRAW_BUFFER6 0x882B
+#define GL_DRAW_BUFFER7 0x882C
+#define GL_DRAW_BUFFER8 0x882D
+#define GL_DRAW_BUFFER9 0x882E
+#define GL_DRAW_BUFFER10 0x882F
+#define GL_DRAW_BUFFER11 0x8830
+#define GL_DRAW_BUFFER12 0x8831
+#define GL_DRAW_BUFFER13 0x8832
+#define GL_DRAW_BUFFER14 0x8833
+#define GL_DRAW_BUFFER15 0x8834
+#define GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 0x8B49
+#define GL_MAX_VERTEX_UNIFORM_COMPONENTS 0x8B4A
+#define GL_SAMPLER_3D 0x8B5F
+#define GL_SAMPLER_2D_SHADOW 0x8B62
+#define GL_FRAGMENT_SHADER_DERIVATIVE_HINT 0x8B8B
+#define GL_PIXEL_PACK_BUFFER 0x88EB
+#define GL_PIXEL_UNPACK_BUFFER 0x88EC
+#define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED
+#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
+#define GL_FLOAT_MAT2x3 0x8B65
+#define GL_FLOAT_MAT2x4 0x8B66
+#define GL_FLOAT_MAT3x2 0x8B67
+#define GL_FLOAT_MAT3x4 0x8B68
+#define GL_FLOAT_MAT4x2 0x8B69
+#define GL_FLOAT_MAT4x3 0x8B6A
+#define GL_SRGB 0x8C40
+#define GL_SRGB8 0x8C41
+#define GL_SRGB8_ALPHA8 0x8C43
+#define GL_COMPARE_REF_TO_TEXTURE 0x884E
+#define GL_MAJOR_VERSION 0x821B
+#define GL_MINOR_VERSION 0x821C
+#define GL_NUM_EXTENSIONS 0x821D
+#define GL_RGBA32F 0x8814
+#define GL_RGB32F 0x8815
+#define GL_RGBA16F 0x881A
+#define GL_RGB16F 0x881B
+#define GL_VERTEX_ATTRIB_ARRAY_INTEGER 0x88FD
+#define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF
+#define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904
+#define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905
+#define GL_MAX_VARYING_COMPONENTS 0x8B4B
+#define GL_TEXTURE_2D_ARRAY 0x8C1A
+#define GL_TEXTURE_BINDING_2D_ARRAY 0x8C1D
+#define GL_R11F_G11F_B10F 0x8C3A
+#define GL_UNSIGNED_INT_10F_11F_11F_REV 0x8C3B
+#define GL_RGB9_E5 0x8C3D
+#define GL_UNSIGNED_INT_5_9_9_9_REV 0x8C3E
+#define GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 0x8C76
+#define GL_TRANSFORM_FEEDBACK_BUFFER_MODE 0x8C7F
+#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS 0x8C80
+#define GL_TRANSFORM_FEEDBACK_VARYINGS 0x8C83
+#define GL_TRANSFORM_FEEDBACK_BUFFER_START 0x8C84
+#define GL_TRANSFORM_FEEDBACK_BUFFER_SIZE 0x8C85
+#define GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0x8C88
+#define GL_RASTERIZER_DISCARD 0x8C89
+#define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS 0x8C8A
+#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 0x8C8B
+#define GL_INTERLEAVED_ATTRIBS 0x8C8C
+#define GL_SEPARATE_ATTRIBS 0x8C8D
+#define GL_TRANSFORM_FEEDBACK_BUFFER 0x8C8E
+#define GL_TRANSFORM_FEEDBACK_BUFFER_BINDING 0x8C8F
+#define GL_RGBA32UI 0x8D70
+#define GL_RGB32UI 0x8D71
+#define GL_RGBA16UI 0x8D76
+#define GL_RGB16UI 0x8D77
+#define GL_RGBA8UI 0x8D7C
+#define GL_RGB8UI 0x8D7D
+#define GL_RGBA32I 0x8D82
+#define GL_RGB32I 0x8D83
+#define GL_RGBA16I 0x8D88
+#define GL_RGB16I 0x8D89
+#define GL_RGBA8I 0x8D8E
+#define GL_RGB8I 0x8D8F
+#define GL_RED_INTEGER 0x8D94
+#define GL_RGB_INTEGER 0x8D98
+#define GL_RGBA_INTEGER 0x8D99
+#define GL_SAMPLER_2D_ARRAY 0x8DC1
+#define GL_SAMPLER_2D_ARRAY_SHADOW 0x8DC4
+#define GL_SAMPLER_CUBE_SHADOW 0x8DC5
+#define GL_UNSIGNED_INT_VEC2 0x8DC6
+#define GL_UNSIGNED_INT_VEC3 0x8DC7
+#define GL_UNSIGNED_INT_VEC4 0x8DC8
+#define GL_INT_SAMPLER_2D 0x8DCA
+#define GL_INT_SAMPLER_3D 0x8DCB
+#define GL_INT_SAMPLER_CUBE 0x8DCC
+#define GL_INT_SAMPLER_2D_ARRAY 0x8DCF
+#define GL_UNSIGNED_INT_SAMPLER_2D 0x8DD2
+#define GL_UNSIGNED_INT_SAMPLER_3D 0x8DD3
+#define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4
+#define GL_UNSIGNED_INT_SAMPLER_2D_ARRAY 0x8DD7
+#define GL_BUFFER_ACCESS_FLAGS 0x911F
+#define GL_BUFFER_MAP_LENGTH 0x9120
+#define GL_BUFFER_MAP_OFFSET 0x9121
+#define GL_DEPTH_COMPONENT32F 0x8CAC
+#define GL_DEPTH32F_STENCIL8 0x8CAD
+#define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD
+#define GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING 0x8210
+#define GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE 0x8211
+#define GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE 0x8212
+#define GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE 0x8213
+#define GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE 0x8214
+#define GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE 0x8215
+#define GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE 0x8216
+#define GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE 0x8217
+#define GL_FRAMEBUFFER_DEFAULT 0x8218
+#define GL_FRAMEBUFFER_UNDEFINED 0x8219
+#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A
+#define GL_DEPTH_STENCIL 0x84F9
+#define GL_UNSIGNED_INT_24_8 0x84FA
+#define GL_DEPTH24_STENCIL8 0x88F0
+#define GL_UNSIGNED_NORMALIZED 0x8C17
+#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_READ_FRAMEBUFFER 0x8CA8
+#define GL_DRAW_FRAMEBUFFER 0x8CA9
+#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA
+#define GL_RENDERBUFFER_SAMPLES 0x8CAB
+#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER 0x8CD4
+#define GL_MAX_COLOR_ATTACHMENTS 0x8CDF
+#define GL_COLOR_ATTACHMENT1 0x8CE1
+#define GL_COLOR_ATTACHMENT2 0x8CE2
+#define GL_COLOR_ATTACHMENT3 0x8CE3
+#define GL_COLOR_ATTACHMENT4 0x8CE4
+#define GL_COLOR_ATTACHMENT5 0x8CE5
+#define GL_COLOR_ATTACHMENT6 0x8CE6
+#define GL_COLOR_ATTACHMENT7 0x8CE7
+#define GL_COLOR_ATTACHMENT8 0x8CE8
+#define GL_COLOR_ATTACHMENT9 0x8CE9
+#define GL_COLOR_ATTACHMENT10 0x8CEA
+#define GL_COLOR_ATTACHMENT11 0x8CEB
+#define GL_COLOR_ATTACHMENT12 0x8CEC
+#define GL_COLOR_ATTACHMENT13 0x8CED
+#define GL_COLOR_ATTACHMENT14 0x8CEE
+#define GL_COLOR_ATTACHMENT15 0x8CEF
+#define GL_COLOR_ATTACHMENT16 0x8CF0
+#define GL_COLOR_ATTACHMENT17 0x8CF1
+#define GL_COLOR_ATTACHMENT18 0x8CF2
+#define GL_COLOR_ATTACHMENT19 0x8CF3
+#define GL_COLOR_ATTACHMENT20 0x8CF4
+#define GL_COLOR_ATTACHMENT21 0x8CF5
+#define GL_COLOR_ATTACHMENT22 0x8CF6
+#define GL_COLOR_ATTACHMENT23 0x8CF7
+#define GL_COLOR_ATTACHMENT24 0x8CF8
+#define GL_COLOR_ATTACHMENT25 0x8CF9
+#define GL_COLOR_ATTACHMENT26 0x8CFA
+#define GL_COLOR_ATTACHMENT27 0x8CFB
+#define GL_COLOR_ATTACHMENT28 0x8CFC
+#define GL_COLOR_ATTACHMENT29 0x8CFD
+#define GL_COLOR_ATTACHMENT30 0x8CFE
+#define GL_COLOR_ATTACHMENT31 0x8CFF
+#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56
+#define GL_MAX_SAMPLES 0x8D57
+#define GL_HALF_FLOAT 0x140B
+#define GL_MAP_READ_BIT 0x0001
+#define GL_MAP_WRITE_BIT 0x0002
+#define GL_MAP_INVALIDATE_RANGE_BIT 0x0004
+#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008
+#define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010
+#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020
+#define GL_RG 0x8227
+#define GL_RG_INTEGER 0x8228
+#define GL_R8 0x8229
+#define GL_RG8 0x822B
+#define GL_R16F 0x822D
+#define GL_R32F 0x822E
+#define GL_RG16F 0x822F
+#define GL_RG32F 0x8230
+#define GL_R8I 0x8231
+#define GL_R8UI 0x8232
+#define GL_R16I 0x8233
+#define GL_R16UI 0x8234
+#define GL_R32I 0x8235
+#define GL_R32UI 0x8236
+#define GL_RG8I 0x8237
+#define GL_RG8UI 0x8238
+#define GL_RG16I 0x8239
+#define GL_RG16UI 0x823A
+#define GL_RG32I 0x823B
+#define GL_RG32UI 0x823C
+#define GL_VERTEX_ARRAY_BINDING 0x85B5
+#define GL_R8_SNORM 0x8F94
+#define GL_RG8_SNORM 0x8F95
+#define GL_RGB8_SNORM 0x8F96
+#define GL_RGBA8_SNORM 0x8F97
+#define GL_SIGNED_NORMALIZED 0x8F9C
+#define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69
+#define GL_COPY_READ_BUFFER 0x8F36
+#define GL_COPY_WRITE_BUFFER 0x8F37
+#define GL_COPY_READ_BUFFER_BINDING 0x8F36
+#define GL_COPY_WRITE_BUFFER_BINDING 0x8F37
+#define GL_UNIFORM_BUFFER 0x8A11
+#define GL_UNIFORM_BUFFER_BINDING 0x8A28
+#define GL_UNIFORM_BUFFER_START 0x8A29
+#define GL_UNIFORM_BUFFER_SIZE 0x8A2A
+#define GL_MAX_VERTEX_UNIFORM_BLOCKS 0x8A2B
+#define GL_MAX_FRAGMENT_UNIFORM_BLOCKS 0x8A2D
+#define GL_MAX_COMBINED_UNIFORM_BLOCKS 0x8A2E
+#define GL_MAX_UNIFORM_BUFFER_BINDINGS 0x8A2F
+#define GL_MAX_UNIFORM_BLOCK_SIZE 0x8A30
+#define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31
+#define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33
+#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
+#define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
+#define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36
+#define GL_UNIFORM_TYPE 0x8A37
+#define GL_UNIFORM_SIZE 0x8A38
+#define GL_UNIFORM_NAME_LENGTH 0x8A39
+#define GL_UNIFORM_BLOCK_INDEX 0x8A3A
+#define GL_UNIFORM_OFFSET 0x8A3B
+#define GL_UNIFORM_ARRAY_STRIDE 0x8A3C
+#define GL_UNIFORM_MATRIX_STRIDE 0x8A3D
+#define GL_UNIFORM_IS_ROW_MAJOR 0x8A3E
+#define GL_UNIFORM_BLOCK_BINDING 0x8A3F
+#define GL_UNIFORM_BLOCK_DATA_SIZE 0x8A40
+#define GL_UNIFORM_BLOCK_NAME_LENGTH 0x8A41
+#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS 0x8A42
+#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46
+#define GL_INVALID_INDEX 0xFFFFFFFFu
+#define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122
+#define GL_MAX_FRAGMENT_INPUT_COMPONENTS 0x9125
+#define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111
+#define GL_OBJECT_TYPE 0x9112
+#define GL_SYNC_CONDITION 0x9113
+#define GL_SYNC_STATUS 0x9114
+#define GL_SYNC_FLAGS 0x9115
+#define GL_SYNC_FENCE 0x9116
+#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
+#define GL_UNSIGNALED 0x9118
+#define GL_SIGNALED 0x9119
+#define GL_ALREADY_SIGNALED 0x911A
+#define GL_TIMEOUT_EXPIRED 0x911B
+#define GL_CONDITION_SATISFIED 0x911C
+#define GL_WAIT_FAILED 0x911D
+#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
+#define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull
+#define GL_VERTEX_ATTRIB_ARRAY_DIVISOR 0x88FE
+#define GL_ANY_SAMPLES_PASSED 0x8C2F
+#define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A
+#define GL_SAMPLER_BINDING 0x8919
+#define GL_RGB10_A2UI 0x906F
+#define GL_TEXTURE_SWIZZLE_R 0x8E42
+#define GL_TEXTURE_SWIZZLE_G 0x8E43
+#define GL_TEXTURE_SWIZZLE_B 0x8E44
+#define GL_TEXTURE_SWIZZLE_A 0x8E45
+#define GL_GREEN 0x1904
+#define GL_BLUE 0x1905
+#define GL_INT_2_10_10_10_REV 0x8D9F
+#define GL_TRANSFORM_FEEDBACK 0x8E22
+#define GL_TRANSFORM_FEEDBACK_PAUSED 0x8E23
+#define GL_TRANSFORM_FEEDBACK_ACTIVE 0x8E24
+#define GL_TRANSFORM_FEEDBACK_BINDING 0x8E25
+#define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257
+#define GL_PROGRAM_BINARY_LENGTH 0x8741
+#define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE
+#define GL_PROGRAM_BINARY_FORMATS 0x87FF
+#define GL_COMPRESSED_R11_EAC 0x9270
+#define GL_COMPRESSED_SIGNED_R11_EAC 0x9271
+#define GL_COMPRESSED_RG11_EAC 0x9272
+#define GL_COMPRESSED_SIGNED_RG11_EAC 0x9273
+#define GL_COMPRESSED_RGB8_ETC2 0x9274
+#define GL_COMPRESSED_SRGB8_ETC2 0x9275
+#define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276
+#define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277
+#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278
+#define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279
+#define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F
+#define GL_MAX_ELEMENT_INDEX 0x8D6B
+#define GL_NUM_SAMPLE_COUNTS 0x9380
+#define GL_TEXTURE_IMMUTABLE_LEVELS 0x82DF
+
+#endif
diff --git a/video/out/opengl/hwdec_d3d11egl.c b/video/out/opengl/hwdec_d3d11egl.c
new file mode 100644
index 0000000..c312091
--- /dev/null
+++ b/video/out/opengl/hwdec_d3d11egl.c
@@ -0,0 +1,363 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <windows.h>
+#include <d3d11.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "angle_dynamic.h"
+
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+
+#ifndef EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE
+#define EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE 0x33AB
+#endif
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+
+ ID3D11Device *d3d11_device;
+ EGLDisplay egl_display;
+
+ // EGL_KHR_stream
+ EGLStreamKHR (EGLAPIENTRY *CreateStreamKHR)(EGLDisplay dpy,
+ const EGLint *attrib_list);
+ EGLBoolean (EGLAPIENTRY *DestroyStreamKHR)(EGLDisplay dpy,
+ EGLStreamKHR stream);
+
+ // EGL_KHR_stream_consumer_gltexture
+ EGLBoolean (EGLAPIENTRY *StreamConsumerAcquireKHR)
+ (EGLDisplay dpy, EGLStreamKHR stream);
+ EGLBoolean (EGLAPIENTRY *StreamConsumerReleaseKHR)
+ (EGLDisplay dpy, EGLStreamKHR stream);
+
+ // EGL_NV_stream_consumer_gltexture_yuv
+ EGLBoolean (EGLAPIENTRY *StreamConsumerGLTextureExternalAttribsNV)
+ (EGLDisplay dpy, EGLStreamKHR stream, EGLAttrib *attrib_list);
+
+ // EGL_ANGLE_stream_producer_d3d_texture
+ EGLBoolean (EGLAPIENTRY *CreateStreamProducerD3DTextureANGLE)
+ (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+ EGLBoolean (EGLAPIENTRY *StreamPostD3DTextureANGLE)
+ (EGLDisplay dpy, EGLStreamKHR stream, void *texture,
+ const EGLAttrib *attrib_list);
+};
+
+struct priv {
+ EGLStreamKHR egl_stream;
+ GLuint gl_textures[2];
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+
+ if (p->d3d11_device)
+ ID3D11Device_Release(p->d3d11_device);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ HRESULT hr;
+
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return -1;
+ if (!angle_load())
+ return -1;
+
+ EGLDisplay egl_display = eglGetCurrentDisplay();
+ if (!egl_display)
+ return -1;
+
+ if (!eglGetCurrentContext())
+ return -1;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+
+ const char *exts = eglQueryString(egl_display, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_ANGLE_d3d_share_handle_client_buffer") ||
+ !gl_check_extension(exts, "EGL_ANGLE_stream_producer_d3d_texture") ||
+ !(gl_check_extension(gl->extensions, "GL_OES_EGL_image_external_essl3") ||
+ gl->es == 200) ||
+ !gl_check_extension(exts, "EGL_EXT_device_query") ||
+ !(gl->mpgl_caps & MPGL_CAP_TEX_RG))
+ return -1;
+
+ p->egl_display = egl_display;
+
+ p->CreateStreamKHR = (void *)eglGetProcAddress("eglCreateStreamKHR");
+ p->DestroyStreamKHR = (void *)eglGetProcAddress("eglDestroyStreamKHR");
+ p->StreamConsumerAcquireKHR =
+ (void *)eglGetProcAddress("eglStreamConsumerAcquireKHR");
+ p->StreamConsumerReleaseKHR =
+ (void *)eglGetProcAddress("eglStreamConsumerReleaseKHR");
+ p->StreamConsumerGLTextureExternalAttribsNV =
+ (void *)eglGetProcAddress("eglStreamConsumerGLTextureExternalAttribsNV");
+ p->CreateStreamProducerD3DTextureANGLE =
+ (void *)eglGetProcAddress("eglCreateStreamProducerD3DTextureANGLE");
+ p->StreamPostD3DTextureANGLE =
+ (void *)eglGetProcAddress("eglStreamPostD3DTextureANGLE");
+
+ if (!p->CreateStreamKHR || !p->DestroyStreamKHR ||
+ !p->StreamConsumerAcquireKHR || !p->StreamConsumerReleaseKHR ||
+ !p->StreamConsumerGLTextureExternalAttribsNV ||
+ !p->CreateStreamProducerD3DTextureANGLE ||
+ !p->StreamPostD3DTextureANGLE)
+ {
+ MP_ERR(hw, "Failed to load some EGLStream functions.\n");
+ goto fail;
+ }
+
+ static const char *es2_exts[] = {"GL_NV_EGL_stream_consumer_external", 0};
+ static const char *es3_exts[] = {"GL_NV_EGL_stream_consumer_external",
+ "GL_OES_EGL_image_external_essl3", 0};
+ hw->glsl_extensions = gl->es == 200 ? es2_exts : es3_exts;
+
+ PFNEGLQUERYDISPLAYATTRIBEXTPROC p_eglQueryDisplayAttribEXT =
+ (void *)eglGetProcAddress("eglQueryDisplayAttribEXT");
+ PFNEGLQUERYDEVICEATTRIBEXTPROC p_eglQueryDeviceAttribEXT =
+ (void *)eglGetProcAddress("eglQueryDeviceAttribEXT");
+ if (!p_eglQueryDisplayAttribEXT || !p_eglQueryDeviceAttribEXT)
+ goto fail;
+
+ EGLAttrib device = 0;
+ if (!p_eglQueryDisplayAttribEXT(egl_display, EGL_DEVICE_EXT, &device))
+ goto fail;
+ EGLAttrib d3d_device = 0;
+ if (!p_eglQueryDeviceAttribEXT((EGLDeviceEXT)device,
+ EGL_D3D11_DEVICE_ANGLE, &d3d_device))
+ {
+ MP_ERR(hw, "Could not get EGL_D3D11_DEVICE_ANGLE from ANGLE.\n");
+ goto fail;
+ }
+
+ p->d3d11_device = (ID3D11Device *)d3d_device;
+ if (!p->d3d11_device)
+ goto fail;
+ ID3D11Device_AddRef(p->d3d11_device);
+
+ if (!d3d11_check_decoding(p->d3d11_device)) {
+ MP_VERBOSE(hw, "D3D11 video decoding not supported on this system.\n");
+ goto fail;
+ }
+
+ ID3D10Multithread *multithread;
+ hr = ID3D11Device_QueryInterface(p->d3d11_device, &IID_ID3D10Multithread,
+ (void **)&multithread);
+ if (FAILED(hr)) {
+ MP_ERR(hw, "Failed to get Multithread interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+ ID3D10Multithread_SetMultithreadProtected(multithread, TRUE);
+ ID3D10Multithread_Release(multithread);
+
+ static const int subfmts[] = {IMGFMT_NV12, IMGFMT_P010, 0};
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d11_wrap_device_ref(p->d3d11_device),
+ .supported_formats = subfmts,
+ .hw_imgfmt = IMGFMT_D3D11,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+fail:
+ return -1;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ if (p->egl_stream)
+ o->DestroyStreamKHR(o->egl_display, p->egl_stream);
+ p->egl_stream = 0;
+
+ gl->DeleteTextures(2, p->gl_textures);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ struct ra_imgfmt_desc desc = {0};
+
+ ra_get_imgfmt_desc(mapper->ra, mapper->src_params.hw_subfmt, &desc);
+
+ // ANGLE hardcodes the list of accepted formats. This is a subset.
+ if ((mapper->src_params.hw_subfmt != IMGFMT_NV12 &&
+ mapper->src_params.hw_subfmt != IMGFMT_P010) ||
+ desc.num_planes < 1 || desc.num_planes > 2)
+ {
+ MP_FATAL(mapper, "Format not supported.\n");
+ return -1;
+ }
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ // The texture units need to be bound during init only, and are free for
+ // use again after the initialization here is done.
+ int texunits = 0; // [texunits, texunits + num_planes)
+ int num_planes = desc.num_planes;
+ int gl_target = GL_TEXTURE_EXTERNAL_OES;
+
+ p->egl_stream = o->CreateStreamKHR(o->egl_display, (EGLint[]){EGL_NONE});
+ if (!p->egl_stream)
+ goto fail;
+
+ EGLAttrib attrs[(2 + 2 + 1) * 2] = {
+ EGL_COLOR_BUFFER_TYPE, EGL_YUV_BUFFER_EXT,
+ EGL_YUV_NUMBER_OF_PLANES_EXT, num_planes,
+ };
+
+ for (int n = 0; n < num_planes; n++) {
+ gl->ActiveTexture(GL_TEXTURE0 + texunits + n);
+ gl->GenTextures(1, &p->gl_textures[n]);
+ gl->BindTexture(gl_target, p->gl_textures[n]);
+ gl->TexParameteri(gl_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(gl_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(gl_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(gl_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ attrs[(2 + n) * 2 + 0] = EGL_YUV_PLANE0_TEXTURE_UNIT_NV + n;
+ attrs[(2 + n) * 2 + 1] = texunits + n;
+ }
+
+ attrs[(2 + num_planes) * 2 + 0] = EGL_NONE;
+
+ if (!o->StreamConsumerGLTextureExternalAttribsNV(o->egl_display, p->egl_stream,
+ attrs))
+ goto fail;
+
+ if (!o->CreateStreamProducerD3DTextureANGLE(o->egl_display, p->egl_stream,
+ (EGLAttrib[]){EGL_NONE}))
+ goto fail;
+
+ for (int n = 0; n < num_planes; n++) {
+ gl->ActiveTexture(GL_TEXTURE0 + texunits + n);
+ gl->BindTexture(gl_target, 0);
+ }
+ gl->ActiveTexture(GL_TEXTURE0);
+ return 0;
+fail:
+ gl->ActiveTexture(GL_TEXTURE0);
+ MP_ERR(mapper, "Failed to create EGLStream\n");
+ return -1;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ ID3D11Texture2D *d3d_tex = (void *)mapper->src->planes[0];
+ int d3d_subindex = (intptr_t)mapper->src->planes[1];
+ if (!d3d_tex)
+ return -1;
+
+ EGLAttrib attrs[] = {
+ EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE, d3d_subindex,
+ EGL_NONE,
+ };
+ if (!o->StreamPostD3DTextureANGLE(o->egl_display, p->egl_stream,
+ (void *)d3d_tex, attrs))
+ {
+ // ANGLE changed the enum ID of this without warning at one point.
+ attrs[0] = attrs[0] == 0x33AB ? 0x3AAB : 0x33AB;
+ if (!o->StreamPostD3DTextureANGLE(o->egl_display, p->egl_stream,
+ (void *)d3d_tex, attrs))
+ return -1;
+ }
+
+ if (!o->StreamConsumerAcquireKHR(o->egl_display, p->egl_stream))
+ return -1;
+
+ D3D11_TEXTURE2D_DESC texdesc;
+ ID3D11Texture2D_GetDesc(d3d_tex, &texdesc);
+
+ for (int n = 0; n < 2; n++) {
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = texdesc.Width / (n ? 2 : 1),
+ .h = texdesc.Height / (n ? 2 : 1),
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, n ? 2 : 1),
+ .render_src = true,
+ .src_linear = true,
+ .external_oes = true,
+ };
+ if (!params.format)
+ return -1;
+
+ mapper->tex[n] = ra_create_wrapped_tex(mapper->ra, &params,
+ p->gl_textures[n]);
+ if (!mapper->tex[n])
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ for (int n = 0; n < 2; n++)
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+ if (p->egl_stream)
+ o->StreamConsumerReleaseKHR(o->egl_display, p->egl_stream);
+}
+
+const struct ra_hwdec_driver ra_hwdec_d3d11egl = {
+ .name = "d3d11-egl",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_D3D11, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/opengl/hwdec_dxva2egl.c b/video/out/opengl/hwdec_dxva2egl.c
new file mode 100644
index 0000000..979ef59
--- /dev/null
+++ b/video/out/opengl/hwdec_dxva2egl.c
@@ -0,0 +1,384 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <windows.h>
+#include <d3d9.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "angle_dynamic.h"
+
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ IDirect3D9Ex *d3d9ex;
+ IDirect3DDevice9Ex *device9ex;
+
+ EGLDisplay egl_display;
+ EGLConfig egl_config;
+ EGLint alpha;
+};
+
+struct priv {
+ IDirect3DDevice9Ex *device9ex; // (no own reference)
+ IDirect3DQuery9 *query9;
+ IDirect3DTexture9 *texture9;
+ IDirect3DSurface9 *surface9;
+
+ EGLDisplay egl_display;
+ EGLSurface egl_surface;
+
+ GLuint gl_texture;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->device9ex)
+ IDirect3DDevice9Ex_Release(p->device9ex);
+
+ if (p->d3d9ex)
+ IDirect3D9Ex_Release(p->d3d9ex);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ HRESULT hr;
+
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return -1;
+ if (!angle_load())
+ return -1;
+
+ d3d_load_dlls();
+
+ EGLDisplay egl_display = eglGetCurrentDisplay();
+ if (!egl_display)
+ return -1;
+
+ if (!eglGetCurrentContext())
+ return -1;
+
+ const char *exts = eglQueryString(egl_display, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_ANGLE_d3d_share_handle_client_buffer")) {
+ return -1;
+ }
+
+ p->egl_display = egl_display;
+
+ if (!d3d9_dll) {
+ MP_FATAL(hw, "Failed to load \"d3d9.dll\": %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT SDKVersion, IDirect3D9Ex **ppD3D);
+ Direct3DCreate9Ex = (void *)GetProcAddress(d3d9_dll, "Direct3DCreate9Ex");
+ if (!Direct3DCreate9Ex) {
+ MP_FATAL(hw, "Direct3D 9Ex not supported\n");
+ goto fail;
+ }
+
+ hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &p->d3d9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Couldn't create Direct3D9Ex: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ // We must create our own Direct3D9Ex device. ANGLE can give us the device
+ // it's using, but that's probably a ID3D11Device.
+ // (copied from chromium dxva_video_decode_accelerator_win.cc)
+ D3DPRESENT_PARAMETERS present_params = {
+ .BackBufferWidth = 1,
+ .BackBufferHeight = 1,
+ .BackBufferFormat = D3DFMT_UNKNOWN,
+ .BackBufferCount = 1,
+ .SwapEffect = D3DSWAPEFFECT_DISCARD,
+ .hDeviceWindow = NULL,
+ .Windowed = TRUE,
+ .Flags = D3DPRESENTFLAG_VIDEO,
+ .FullScreen_RefreshRateInHz = 0,
+ .PresentationInterval = 0,
+ };
+ hr = IDirect3D9Ex_CreateDeviceEx(p->d3d9ex,
+ D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL,
+ NULL,
+ D3DCREATE_FPU_PRESERVE |
+ D3DCREATE_HARDWARE_VERTEXPROCESSING |
+ D3DCREATE_DISABLE_PSGP_THREADING |
+ D3DCREATE_MULTITHREADED,
+ &present_params,
+ NULL,
+ &p->device9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Failed to create Direct3D9Ex device: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ EGLint attrs[] = {
+ EGL_BUFFER_SIZE, 32,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+ EGL_ALPHA_SIZE, 0,
+ EGL_NONE
+ };
+ EGLint count;
+ if (!eglChooseConfig(p->egl_display, attrs, &p->egl_config, 1, &count) ||
+ !count) {
+ MP_ERR(hw, "Failed to get EGL surface configuration\n");
+ goto fail;
+ }
+
+ if (!eglGetConfigAttrib(p->egl_display, p->egl_config,
+ EGL_BIND_TO_TEXTURE_RGBA, &p->alpha)) {
+ MP_FATAL(hw, "Failed to query EGL surface alpha\n");
+ goto fail;
+ }
+
+ struct mp_image_params dummy_params = {
+ .imgfmt = IMGFMT_DXVA2,
+ .w = 256,
+ .h = 256,
+ };
+ struct ra_hwdec_mapper *mapper = ra_hwdec_mapper_create(hw, &dummy_params);
+ if (!mapper)
+ goto fail;
+ ra_hwdec_mapper_free(&mapper);
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d9_wrap_device_ref((IDirect3DDevice9 *)p->device9ex),
+ .hw_imgfmt = IMGFMT_DXVA2,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ goto fail;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+fail:
+ return -1;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ ra_tex_free(mapper->ra, &mapper->tex[0]);
+ gl->DeleteTextures(1, &p->gl_texture);
+
+ if (p->egl_display && p->egl_surface) {
+ eglReleaseTexImage(p->egl_display, p->egl_surface, EGL_BACK_BUFFER);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ }
+
+ if (p->surface9)
+ IDirect3DSurface9_Release(p->surface9);
+
+ if (p->texture9)
+ IDirect3DTexture9_Release(p->texture9);
+
+ if (p->query9)
+ IDirect3DQuery9_Release(p->query9);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+ HRESULT hr;
+
+ p->device9ex = p_owner->device9ex;
+ p->egl_display = p_owner->egl_display;
+
+ hr = IDirect3DDevice9_CreateQuery(p->device9ex, D3DQUERYTYPE_EVENT,
+ &p->query9);
+ if (FAILED(hr)) {
+ MP_FATAL(mapper, "Failed to create Direct3D query interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ // Test the query API
+ hr = IDirect3DQuery9_Issue(p->query9, D3DISSUE_END);
+ if (FAILED(hr)) {
+ MP_FATAL(mapper, "Failed to issue Direct3D END test query: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ HANDLE share_handle = NULL;
+ hr = IDirect3DDevice9Ex_CreateTexture(p->device9ex,
+ mapper->src_params.w,
+ mapper->src_params.h,
+ 1, D3DUSAGE_RENDERTARGET,
+ p_owner->alpha ?
+ D3DFMT_A8R8G8B8 : D3DFMT_X8R8G8B8,
+ D3DPOOL_DEFAULT,
+ &p->texture9,
+ &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create Direct3D9 texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ hr = IDirect3DTexture9_GetSurfaceLevel(p->texture9, 0, &p->surface9);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get Direct3D9 surface from texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ EGLint attrib_list[] = {
+ EGL_WIDTH, mapper->src_params.w,
+ EGL_HEIGHT, mapper->src_params.h,
+ EGL_TEXTURE_FORMAT, p_owner->alpha ? EGL_TEXTURE_RGBA : EGL_TEXTURE_RGB,
+ EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
+ EGL_NONE
+ };
+ p->egl_surface = eglCreatePbufferFromClientBuffer(
+ p->egl_display, EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE,
+ share_handle, p_owner->egl_config, attrib_list);
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_ERR(mapper, "Failed to create EGL surface\n");
+ goto fail;
+ }
+
+ gl->GenTextures(1, &p->gl_texture);
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_texture);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, p_owner->alpha ? 4 : 3),
+ .render_src = true,
+ .src_linear = true,
+ };
+ if (!params.format)
+ goto fail;
+
+ mapper->tex[0] = ra_create_wrapped_tex(mapper->ra, &params, p->gl_texture);
+ if (!mapper->tex[0])
+ goto fail;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+ return 0;
+fail:
+ return -1;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ HRESULT hr;
+ RECT rc = {0, 0, mapper->src->w, mapper->src->h};
+ IDirect3DSurface9* hw_surface = (IDirect3DSurface9 *)mapper->src->planes[3];
+ hr = IDirect3DDevice9Ex_StretchRect(p->device9ex,
+ hw_surface, &rc,
+ p->surface9, &rc,
+ D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Direct3D RGB conversion failed: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ hr = IDirect3DQuery9_Issue(p->query9, D3DISSUE_END);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to issue Direct3D END query\n");
+ return -1;
+ }
+
+ // There doesn't appear to be an efficient way to do a blocking flush
+ // of the above StretchRect. Timeout of 8ms is required to reliably
+ // render 4k on Intel Haswell, Ivybridge and Cherry Trail Atom.
+ const int max_retries = 8;
+ const int64_t wait_ns = MP_TIME_MS_TO_NS(1);
+ int retries = 0;
+ while (true) {
+ hr = IDirect3DQuery9_GetData(p->query9, NULL, 0, D3DGETDATA_FLUSH);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to query Direct3D flush state\n");
+ return -1;
+ } else if (hr == S_FALSE) {
+ if (++retries > max_retries) {
+ MP_VERBOSE(mapper, "Failed to flush frame after %lld ms\n",
+ (long long)MP_TIME_MS_TO_NS(wait_ns * max_retries));
+ break;
+ }
+ mp_sleep_ns(wait_ns);
+ } else {
+ break;
+ }
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_texture);
+ eglBindTexImage(p->egl_display, p->egl_surface, EGL_BACK_BUFFER);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_dxva2egl = {
+ .name = "dxva2-egl",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DXVA2, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ },
+};
diff --git a/video/out/opengl/hwdec_dxva2gldx.c b/video/out/opengl/hwdec_dxva2gldx.c
new file mode 100644
index 0000000..0172813
--- /dev/null
+++ b/video/out/opengl/hwdec_dxva2gldx.c
@@ -0,0 +1,247 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <d3d9.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+
+// for WGL_ACCESS_READ_ONLY_NV
+#include <GL/wglext.h>
+
+#define SHARED_SURFACE_D3DFMT D3DFMT_X8R8G8B8
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ IDirect3DDevice9Ex *device;
+ HANDLE device_h;
+};
+
+struct priv {
+ IDirect3DDevice9Ex *device;
+ HANDLE device_h;
+ IDirect3DSurface9 *rtarget;
+ HANDLE rtarget_h;
+ GLuint texture;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->device)
+ IDirect3DDevice9Ex_Release(p->device);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ struct ra *ra = hw->ra_ctx->ra;
+
+ if (!ra_is_gl(ra))
+ return -1;
+ GL *gl = ra_gl_get(ra);
+ if (!(gl->mpgl_caps & MPGL_CAP_DXINTEROP))
+ return -1;
+
+ // AMD drivers won't open multiple dxinterop HANDLES on the same D3D device,
+ // so we request the one already in use by context_dxinterop
+ p->device_h = ra_get_native_resource(ra, "dxinterop_device_HANDLE");
+ if (!p->device_h)
+ return -1;
+
+ // But we also still need the actual D3D device
+ p->device = ra_get_native_resource(ra, "IDirect3DDevice9Ex");
+ if (!p->device)
+ return -1;
+ IDirect3DDevice9Ex_AddRef(p->device);
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d9_wrap_device_ref((IDirect3DDevice9 *)p->device),
+ .hw_imgfmt = IMGFMT_DXVA2,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ if (p->rtarget_h && p->device_h) {
+ if (!gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed unlocking texture for access by OpenGL: %s\n",
+ mp_LastError_to_str());
+ }
+ }
+
+ if (p->rtarget_h) {
+ if (!gl->DXUnregisterObjectNV(p->device_h, p->rtarget_h)) {
+ MP_ERR(mapper, "Failed to unregister Direct3D surface with OpenGL: %s\n",
+ mp_LastError_to_str());
+ } else {
+ p->rtarget_h = 0;
+ }
+ }
+
+ gl->DeleteTextures(1, &p->texture);
+ p->texture = 0;
+
+ if (p->rtarget) {
+ IDirect3DSurface9_Release(p->rtarget);
+ p->rtarget = NULL;
+ }
+
+ ra_tex_free(mapper->ra, &mapper->tex[0]);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+ HRESULT hr;
+
+ p->device = p_owner->device;
+ p->device_h = p_owner->device_h;
+
+ HANDLE share_handle = NULL;
+ hr = IDirect3DDevice9Ex_CreateRenderTarget(
+ p->device,
+ mapper->src_params.w, mapper->src_params.h,
+ SHARED_SURFACE_D3DFMT, D3DMULTISAMPLE_NONE, 0, FALSE,
+ &p->rtarget, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed creating offscreen Direct3D surface: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ if (share_handle &&
+ !gl->DXSetResourceShareHandleNV(p->rtarget, share_handle)) {
+ MP_ERR(mapper, "Failed setting Direct3D/OpenGL share handle for surface: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ gl->GenTextures(1, &p->texture);
+ gl->BindTexture(GL_TEXTURE_2D, p->texture);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ p->rtarget_h = gl->DXRegisterObjectNV(p->device_h, p->rtarget, p->texture,
+ GL_TEXTURE_2D,
+ WGL_ACCESS_READ_ONLY_NV);
+ if (!p->rtarget_h) {
+ MP_ERR(mapper, "Failed to register Direct3D surface with OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed locking texture for access by OpenGL %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, 4),
+ .render_src = true,
+ .src_linear = true,
+ };
+ if (!params.format)
+ return -1;
+
+ mapper->tex[0] = ra_create_wrapped_tex(mapper->ra, &params, p->texture);
+ if (!mapper->tex[0])
+ return -1;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+ HRESULT hr;
+
+ if (!gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed unlocking texture for access by OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ IDirect3DSurface9* hw_surface = (IDirect3DSurface9 *)mapper->src->planes[3];
+ RECT rc = {0, 0, mapper->src->w, mapper->src->h};
+ hr = IDirect3DDevice9Ex_StretchRect(p->device,
+ hw_surface, &rc,
+ p->rtarget, &rc,
+ D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Direct3D RGB conversion failed: %s", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed locking texture for access by OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_dxva2gldx = {
+ .name = "dxva2-dxinterop",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DXVA2, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ },
+};
diff --git a/video/out/opengl/hwdec_rpi.c b/video/out/opengl/hwdec_rpi.c
new file mode 100644
index 0000000..5362832
--- /dev/null
+++ b/video/out/opengl/hwdec_rpi.c
@@ -0,0 +1,384 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <bcm_host.h>
+#include <interface/mmal/mmal.h>
+#include <interface/mmal/util/mmal_util.h>
+#include <interface/mmal/util/mmal_default_components.h>
+#include <interface/mmal/vc/mmal_vc_api.h>
+
+#include <libavutil/rational.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/mp_image.h"
+#include "video/out/gpu/hwdec.h"
+
+#include "common.h"
+
+struct priv {
+ struct mp_log *log;
+
+ struct mp_image_params params;
+
+ MMAL_COMPONENT_T *renderer;
+ bool renderer_enabled;
+
+ // for RAM input
+ MMAL_POOL_T *swpool;
+
+ struct mp_image *current_frame;
+
+ struct mp_rect src, dst;
+ int cur_window[4]; // raw user params
+};
+
+// Magic alignments (in pixels) expected by the MMAL internals.
+#define ALIGN_W 32
+#define ALIGN_H 16
+
+// Make mpi point to buffer, assuming MMAL_ENCODING_I420.
+// buffer can be NULL.
+// Return the required buffer space.
+static size_t layout_buffer(struct mp_image *mpi, MMAL_BUFFER_HEADER_T *buffer,
+ struct mp_image_params *params)
+{
+ assert(params->imgfmt == IMGFMT_420P);
+ mp_image_set_params(mpi, params);
+ int w = MP_ALIGN_UP(params->w, ALIGN_W);
+ int h = MP_ALIGN_UP(params->h, ALIGN_H);
+ uint8_t *cur = buffer ? buffer->data : NULL;
+ size_t size = 0;
+ for (int i = 0; i < 3; i++) {
+ int div = i ? 2 : 1;
+ mpi->planes[i] = cur;
+ mpi->stride[i] = w / div;
+ size_t plane_size = h / div * mpi->stride[i];
+ if (cur)
+ cur += plane_size;
+ size += plane_size;
+ }
+ return size;
+}
+
+static MMAL_FOURCC_T map_csp(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_BT_601: return MMAL_COLOR_SPACE_ITUR_BT601;
+ case MP_CSP_BT_709: return MMAL_COLOR_SPACE_ITUR_BT709;
+ case MP_CSP_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
+ default: return MMAL_COLOR_SPACE_UNKNOWN;
+ }
+}
+
+static void control_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ mmal_buffer_header_release(buffer);
+}
+
+static void input_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ struct mp_image *mpi = buffer->user_data;
+ talloc_free(mpi);
+}
+
+static void disable_renderer(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+
+ if (p->renderer_enabled) {
+ mmal_port_disable(p->renderer->control);
+ mmal_port_disable(p->renderer->input[0]);
+
+ mmal_port_flush(p->renderer->control);
+ mmal_port_flush(p->renderer->input[0]);
+
+ mmal_component_disable(p->renderer);
+ }
+ mmal_pool_destroy(p->swpool);
+ p->swpool = NULL;
+ p->renderer_enabled = false;
+}
+
+// check_window_only: assume params and dst/src rc are unchanged
+static void update_overlay(struct ra_hwdec *hw, bool check_window_only)
+{
+ struct priv *p = hw->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+ struct mp_rect src = p->src;
+ struct mp_rect dst = p->dst;
+
+ int defs[4] = {0, 0, 0, 0};
+ int *z = ra_get_native_resource(hw->ra_ctx->ra, "MPV_RPI_WINDOW");
+ if (!z)
+ z = defs;
+
+ // As documented in the libmpv openglcb headers.
+ int display = z[0];
+ int layer = z[1];
+ int x = z[2];
+ int y = z[3];
+
+ if (check_window_only && memcmp(z, p->cur_window, sizeof(p->cur_window)) == 0)
+ return;
+
+ memcpy(p->cur_window, z, sizeof(p->cur_window));
+
+ int rotate[] = {MMAL_DISPLAY_ROT0,
+ MMAL_DISPLAY_ROT90,
+ MMAL_DISPLAY_ROT180,
+ MMAL_DISPLAY_ROT270};
+
+ int src_w = src.x1 - src.x0, src_h = src.y1 - src.y0,
+ dst_w = dst.x1 - dst.x0, dst_h = dst.y1 - dst.y0;
+ int p_x, p_y;
+ av_reduce(&p_x, &p_y, dst_w * src_h, src_w * dst_h, 16000);
+ MMAL_DISPLAYREGION_T dr = {
+ .hdr = { .id = MMAL_PARAMETER_DISPLAYREGION,
+ .size = sizeof(MMAL_DISPLAYREGION_T), },
+ .src_rect = { .x = src.x0, .y = src.y0,
+ .width = src_w, .height = src_h },
+ .dest_rect = { .x = dst.x0 + x, .y = dst.y0 + y,
+ .width = dst_w, .height = dst_h },
+ .layer = layer - 1, // under the GL layer
+ .display_num = display,
+ .pixel_x = p_x,
+ .pixel_y = p_y,
+ .transform = rotate[p->params.rotate / 90],
+ .fullscreen = 0,
+ .set = MMAL_DISPLAY_SET_SRC_RECT | MMAL_DISPLAY_SET_DEST_RECT |
+ MMAL_DISPLAY_SET_LAYER | MMAL_DISPLAY_SET_NUM |
+ MMAL_DISPLAY_SET_PIXEL | MMAL_DISPLAY_SET_TRANSFORM |
+ MMAL_DISPLAY_SET_FULLSCREEN,
+ };
+
+ if (p->params.rotate % 180 == 90) {
+ MPSWAP(int, dr.src_rect.x, dr.src_rect.y);
+ MPSWAP(int, dr.src_rect.width, dr.src_rect.height);
+ }
+
+ if (mmal_port_parameter_set(input, &dr.hdr))
+ MP_WARN(p, "could not set video rectangle\n");
+}
+
+static int enable_renderer(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+ struct mp_image_params *params = &p->params;
+
+ if (p->renderer_enabled)
+ return 0;
+
+ if (!params->imgfmt)
+ return -1;
+
+ bool opaque = params->imgfmt == IMGFMT_MMAL;
+
+ input->format->encoding = opaque ? MMAL_ENCODING_OPAQUE : MMAL_ENCODING_I420;
+ input->format->es->video.width = MP_ALIGN_UP(params->w, ALIGN_W);
+ input->format->es->video.height = MP_ALIGN_UP(params->h, ALIGN_H);
+ input->format->es->video.crop = (MMAL_RECT_T){0, 0, params->w, params->h};
+ input->format->es->video.par = (MMAL_RATIONAL_T){params->p_w, params->p_h};
+ input->format->es->video.color_space = map_csp(params->color.space);
+
+ if (mmal_port_format_commit(input))
+ return -1;
+
+ input->buffer_num = MPMAX(input->buffer_num_min,
+ input->buffer_num_recommended) + 3;
+ input->buffer_size = MPMAX(input->buffer_size_min,
+ input->buffer_size_recommended);
+
+ if (!opaque) {
+ size_t size = layout_buffer(&(struct mp_image){0}, NULL, params);
+ if (input->buffer_size != size) {
+ MP_FATAL(hw, "We disagree with MMAL about buffer sizes.\n");
+ return -1;
+ }
+
+ p->swpool = mmal_pool_create(input->buffer_num, input->buffer_size);
+ if (!p->swpool) {
+ MP_FATAL(hw, "Could not allocate buffer pool.\n");
+ return -1;
+ }
+ }
+
+ update_overlay(hw, false);
+
+ p->renderer_enabled = true;
+
+ if (mmal_port_enable(p->renderer->control, control_port_cb))
+ return -1;
+
+ if (mmal_port_enable(input, input_port_cb))
+ return -1;
+
+ if (mmal_component_enable(p->renderer)) {
+ MP_FATAL(hw, "Failed to enable video renderer.\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+static void free_mmal_buffer(void *arg)
+{
+ MMAL_BUFFER_HEADER_T *buffer = arg;
+ mmal_buffer_header_release(buffer);
+}
+
+static struct mp_image *upload(struct ra_hwdec *hw, struct mp_image *hw_image)
+{
+ struct priv *p = hw->priv;
+
+ MMAL_BUFFER_HEADER_T *buffer = mmal_queue_wait(p->swpool->queue);
+ if (!buffer) {
+ MP_ERR(hw, "Can't allocate buffer.\n");
+ return NULL;
+ }
+ mmal_buffer_header_reset(buffer);
+
+ struct mp_image *new_ref = mp_image_new_custom_ref(NULL, buffer,
+ free_mmal_buffer);
+ if (!new_ref) {
+ mmal_buffer_header_release(buffer);
+ MP_ERR(hw, "Out of memory.\n");
+ return NULL;
+ }
+
+ mp_image_setfmt(new_ref, IMGFMT_MMAL);
+ new_ref->planes[3] = (void *)buffer;
+
+ struct mp_image dmpi = {0};
+ buffer->length = layout_buffer(&dmpi, buffer, &p->params);
+ mp_image_copy(&dmpi, hw_image);
+
+ return new_ref;
+}
+
+static int overlay_frame(struct ra_hwdec *hw, struct mp_image *hw_image,
+ struct mp_rect *src, struct mp_rect *dst, bool newframe)
+{
+ struct priv *p = hw->priv;
+
+ if (hw_image && !mp_image_params_equal(&p->params, &hw_image->params)) {
+ p->params = hw_image->params;
+
+ disable_renderer(hw);
+ mp_image_unrefp(&p->current_frame);
+
+ if (enable_renderer(hw) < 0)
+ return -1;
+ }
+
+ if (hw_image && p->current_frame && !newframe) {
+ if (!mp_rect_equals(&p->src, src) ||mp_rect_equals(&p->dst, dst)) {
+ p->src = *src;
+ p->dst = *dst;
+ update_overlay(hw, false);
+ }
+ return 0; // don't reupload
+ }
+
+ mp_image_unrefp(&p->current_frame);
+
+ if (!hw_image) {
+ disable_renderer(hw);
+ return 0;
+ }
+
+ if (enable_renderer(hw) < 0)
+ return -1;
+
+ update_overlay(hw, true);
+
+ struct mp_image *mpi = NULL;
+ if (hw_image->imgfmt == IMGFMT_MMAL) {
+ mpi = mp_image_new_ref(hw_image);
+ } else {
+ mpi = upload(hw, hw_image);
+ }
+
+ if (!mpi) {
+ disable_renderer(hw);
+ return -1;
+ }
+
+ MMAL_BUFFER_HEADER_T *ref = (void *)mpi->planes[3];
+
+ // Assume this field is free for use by us.
+ ref->user_data = mpi;
+
+ if (mmal_port_send_buffer(p->renderer->input[0], ref)) {
+ MP_ERR(hw, "could not queue picture!\n");
+ talloc_free(mpi);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void destroy(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+
+ disable_renderer(hw);
+
+ if (p->renderer)
+ mmal_component_release(p->renderer);
+
+ mmal_vc_deinit();
+}
+
+static int create(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ p->log = hw->log;
+
+ bcm_host_init();
+
+ if (mmal_vc_init()) {
+ MP_FATAL(hw, "Could not initialize MMAL.\n");
+ return -1;
+ }
+
+ if (mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &p->renderer))
+ {
+ MP_FATAL(hw, "Could not create MMAL renderer.\n");
+ mmal_vc_deinit();
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_rpi_overlay = {
+ .name = "rpi-overlay",
+ .priv_size = sizeof(struct priv),
+ .imgfmts = {IMGFMT_MMAL, IMGFMT_420P, 0},
+ .init = create,
+ .overlay_frame = overlay_frame,
+ .uninit = destroy,
+};
diff --git a/video/out/opengl/hwdec_vdpau.c b/video/out/opengl/hwdec_vdpau.c
new file mode 100644
index 0000000..acdc703
--- /dev/null
+++ b/video/out/opengl/hwdec_vdpau.c
@@ -0,0 +1,251 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <assert.h>
+
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/vdpau.h"
+#include "video/vdpau_mixer.h"
+
+// This is a GL_NV_vdpau_interop specification bug, and headers (unfortunately)
+// follow it. I'm not sure about the original nvidia headers.
+#define BRAINDEATH(x) ((void *)(uintptr_t)(x))
+
+struct priv_owner {
+ struct mp_vdpau_ctx *ctx;
+};
+
+struct priv {
+ struct mp_vdpau_ctx *ctx;
+ GL *gl;
+ uint64_t preemption_counter;
+ GLuint gl_texture;
+ bool vdpgl_initialized;
+ GLvdpauSurfaceNV vdpgl_surface;
+ VdpOutputSurface vdp_surface;
+ struct mp_vdpau_mixer *mixer;
+ struct ra_imgfmt_desc direct_desc;
+ bool mapped;
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct ra *ra = hw->ra_ctx->ra;
+ Display *x11disp = ra_get_native_resource(ra, "x11");
+ if (!x11disp || !ra_is_gl(ra))
+ return -1;
+ GL *gl = ra_gl_get(ra);
+ if (!(gl->mpgl_caps & MPGL_CAP_VDPAU))
+ return -1;
+ struct priv_owner *p = hw->priv;
+ p->ctx = mp_vdpau_create_device_x11(hw->log, x11disp, true);
+ if (!p->ctx)
+ return -1;
+ if (mp_vdpau_handle_preemption(p->ctx, NULL) < 1)
+ return -1;
+ if (hw->probing && mp_vdpau_guess_if_emulated(p->ctx))
+ return -1;
+ p->ctx->hwctx.driver_name = hw->driver->name;
+ p->ctx->hwctx.hw_imgfmt = IMGFMT_VDPAU;
+ hwdec_devices_add(hw->devs, &p->ctx->hwctx);
+ return 0;
+}
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (p->ctx)
+ hwdec_devices_remove(hw->devs, &p->ctx->hwctx);
+ mp_vdpau_destroy(p->ctx);
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = p->gl;
+
+ for (int n = 0; n < 4; n++)
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+
+ if (p->mapped) {
+ gl->VDPAUUnmapSurfacesNV(1, &p->vdpgl_surface);
+ }
+ p->mapped = false;
+}
+
+static void mark_vdpau_objects_uninitialized(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ p->vdp_surface = VDP_INVALID_HANDLE;
+ p->mapped = false;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = p->gl;
+ struct vdp_functions *vdp = &p->ctx->vdp;
+ VdpStatus vdp_st;
+
+ assert(!p->mapped);
+
+ if (p->vdpgl_surface)
+ gl->VDPAUUnregisterSurfaceNV(p->vdpgl_surface);
+ p->vdpgl_surface = 0;
+
+ gl->DeleteTextures(1, &p->gl_texture);
+
+ if (p->vdp_surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(p->vdp_surface);
+ CHECK_VDP_WARNING(mapper, "Error when calling vdp_output_surface_destroy");
+ }
+ p->vdp_surface = VDP_INVALID_HANDLE;
+
+ gl_check_error(gl, mapper->log, "Before uninitializing OpenGL interop");
+
+ if (p->vdpgl_initialized)
+ gl->VDPAUFiniNV();
+
+ p->vdpgl_initialized = false;
+
+ gl_check_error(gl, mapper->log, "After uninitializing OpenGL interop");
+
+ mp_vdpau_mixer_destroy(p->mixer);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ p->gl = ra_gl_get(mapper->ra);
+ p->ctx = p_owner->ctx;
+
+ GL *gl = p->gl;
+ struct vdp_functions *vdp = &p->ctx->vdp;
+ VdpStatus vdp_st;
+
+ p->vdp_surface = VDP_INVALID_HANDLE;
+ p->mixer = mp_vdpau_mixer_create(p->ctx, mapper->log);
+ if (!p->mixer)
+ return -1;
+
+ mapper->dst_params = mapper->src_params;
+
+ if (mp_vdpau_handle_preemption(p->ctx, &p->preemption_counter) < 0)
+ return -1;
+
+ gl->VDPAUInitNV(BRAINDEATH(p->ctx->vdp_device), p->ctx->get_proc_address);
+
+ p->vdpgl_initialized = true;
+
+ gl->GenTextures(1, &p->gl_texture);
+
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_texture);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ vdp_st = vdp->output_surface_create(p->ctx->vdp_device,
+ VDP_RGBA_FORMAT_B8G8R8A8,
+ mapper->src_params.w,
+ mapper->src_params.h,
+ &p->vdp_surface);
+ CHECK_VDP_ERROR(mapper, "Error when calling vdp_output_surface_create");
+
+ p->vdpgl_surface = gl->VDPAURegisterOutputSurfaceNV(BRAINDEATH(p->vdp_surface),
+ GL_TEXTURE_2D,
+ 1, &p->gl_texture);
+ if (!p->vdpgl_surface)
+ return -1;
+
+ gl->VDPAUSurfaceAccessNV(p->vdpgl_surface, GL_READ_ONLY);
+
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+
+ gl_check_error(gl, mapper->log, "After initializing vdpau OpenGL interop");
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = p->gl;
+
+ int pe = mp_vdpau_handle_preemption(p->ctx, &p->preemption_counter);
+ if (pe < 1) {
+ mark_vdpau_objects_uninitialized(mapper);
+ if (pe < 0)
+ return -1;
+ mapper_uninit(mapper);
+ if (mapper_init(mapper) < 0)
+ return -1;
+ }
+
+ if (!p->vdpgl_surface)
+ return -1;
+
+ mp_vdpau_mixer_render(p->mixer, NULL, p->vdp_surface, NULL, mapper->src,
+ NULL);
+
+ gl->VDPAUMapSurfacesNV(1, &p->vdpgl_surface);
+
+ p->mapped = true;
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, 4),
+ .render_src = true,
+ .src_linear = true,
+ };
+
+ if (!params.format)
+ return -1;
+
+ mapper->tex[0] =
+ ra_create_wrapped_tex(mapper->ra, &params, p->gl_texture);
+ if (!mapper->tex[0])
+ return -1;
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_vdpau = {
+ .name = "vdpau-gl",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_VDPAU, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/opengl/libmpv_gl.c b/video/out/opengl/libmpv_gl.c
new file mode 100644
index 0000000..c297c13
--- /dev/null
+++ b/video/out/opengl/libmpv_gl.c
@@ -0,0 +1,114 @@
+#include "common.h"
+#include "context.h"
+#include "ra_gl.h"
+#include "options/m_config.h"
+#include "libmpv/render_gl.h"
+#include "video/out/gpu/libmpv_gpu.h"
+#include "video/out/gpu/ra.h"
+
+struct priv {
+ GL *gl;
+ struct ra_ctx *ra_ctx;
+};
+
+static int init(struct libmpv_gpu_context *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ mpv_opengl_init_params *init_params =
+ get_mpv_render_param(params, MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, NULL);
+ if (!init_params)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ p->gl = talloc_zero(p, GL);
+
+ mpgl_load_functions2(p->gl, init_params->get_proc_address,
+ init_params->get_proc_address_ctx,
+ NULL, ctx->log);
+ if (!p->gl->version && !p->gl->es) {
+ MP_FATAL(ctx, "OpenGL not initialized.\n");
+ return MPV_ERROR_UNSUPPORTED;
+ }
+
+ // initialize a blank ra_ctx to reuse ra_gl_ctx
+ p->ra_ctx = talloc_zero(p, struct ra_ctx);
+ p->ra_ctx->log = ctx->log;
+ p->ra_ctx->global = ctx->global;
+ p->ra_ctx->opts = (struct ra_ctx_opts) {
+ .allow_sw = true,
+ };
+
+ static const struct ra_swapchain_fns empty_swapchain_fns = {0};
+ struct ra_gl_ctx_params gl_params = {
+ // vo_libmpv is essentially like a gigantic external swapchain where
+ // the user is in charge of presentation / swapping etc. But we don't
+ // actually need to provide any of these functions, since we can just
+ // not call them to begin with - so just set it to an empty object to
+ // signal to ra_gl_p that we don't care about its latency emulation
+ // functionality
+ .external_swapchain = &empty_swapchain_fns
+ };
+
+ p->gl->SwapInterval = NULL; // we shouldn't randomly change this, so lock it
+ if (!ra_gl_ctx_init(p->ra_ctx, p->gl, gl_params))
+ return MPV_ERROR_UNSUPPORTED;
+
+ struct ra_ctx_opts *ctx_opts = mp_get_config_group(ctx, ctx->global, &ra_ctx_conf);
+ p->ra_ctx->opts.debug = ctx_opts->debug;
+ p->gl->debug_context = ctx_opts->debug;
+ ra_gl_set_debug(p->ra_ctx->ra, ctx_opts->debug);
+ talloc_free(ctx_opts);
+
+ ctx->ra_ctx = p->ra_ctx;
+
+ return 0;
+}
+
+static int wrap_fbo(struct libmpv_gpu_context *ctx, mpv_render_param *params,
+ struct ra_tex **out)
+{
+ struct priv *p = ctx->priv;
+
+ mpv_opengl_fbo *fbo =
+ get_mpv_render_param(params, MPV_RENDER_PARAM_OPENGL_FBO, NULL);
+ if (!fbo)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ if (fbo->fbo && !(p->gl->mpgl_caps & MPGL_CAP_FB)) {
+ MP_FATAL(ctx, "Rendering to FBO requested, but no FBO extension found!\n");
+ return MPV_ERROR_UNSUPPORTED;
+ }
+
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ struct ra_fbo target;
+ ra_gl_ctx_resize(sw, fbo->w, fbo->h, fbo->fbo);
+ ra_gl_ctx_start_frame(sw, &target);
+ *out = target.tex;
+ return 0;
+}
+
+static void done_frame(struct libmpv_gpu_context *ctx, bool ds)
+{
+ struct priv *p = ctx->priv;
+
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ struct vo_frame dummy = {.display_synced = ds};
+ ra_gl_ctx_submit_frame(sw, &dummy);
+}
+
+static void destroy(struct libmpv_gpu_context *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->ra_ctx)
+ ra_gl_ctx_uninit(p->ra_ctx);
+}
+
+const struct libmpv_gpu_context_fns libmpv_gpu_context_gl = {
+ .api_name = MPV_RENDER_API_TYPE_OPENGL,
+ .init = init,
+ .wrap_fbo = wrap_fbo,
+ .done_frame = done_frame,
+ .destroy = destroy,
+};
diff --git a/video/out/opengl/ra_gl.c b/video/out/opengl/ra_gl.c
new file mode 100644
index 0000000..f535f1f
--- /dev/null
+++ b/video/out/opengl/ra_gl.c
@@ -0,0 +1,1208 @@
+#include <libavutil/intreadwrite.h>
+
+#include "formats.h"
+#include "utils.h"
+#include "ra_gl.h"
+
+static struct ra_fns ra_fns_gl;
+
+// For ra.priv
+struct ra_gl {
+ GL *gl;
+ bool debug_enable;
+ bool timer_active; // hack for GL_TIME_ELAPSED limitations
+};
+
+// For ra_tex.priv
+struct ra_tex_gl {
+ struct ra_buf_pool pbo; // for ra.use_pbo
+ bool own_objects;
+ GLenum target;
+ GLuint texture; // 0 if no texture data associated
+ GLuint fbo; // 0 if no rendering requested, or it default framebuffer
+ // These 3 fields can be 0 if unknown.
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+};
+
+// For ra_buf.priv
+struct ra_buf_gl {
+ GLenum target;
+ GLuint buffer;
+ GLsync fence;
+};
+
+// For ra_renderpass.priv
+struct ra_renderpass_gl {
+ GLuint program;
+ // 1 entry for each ra_renderpass_params.inputs[] entry
+ GLint *uniform_loc;
+ int num_uniform_loc; // == ra_renderpass_params.num_inputs
+ struct gl_vao vao;
+};
+
+// (Init time only.)
+static void probe_real_size(GL *gl, struct ra_format *fmt)
+{
+ const struct gl_format *gl_fmt = fmt->priv;
+
+ if (!gl->GetTexLevelParameteriv)
+ return; // GLES
+
+ bool is_la = gl_fmt->format == GL_LUMINANCE ||
+ gl_fmt->format == GL_LUMINANCE_ALPHA;
+ if (is_la && gl->es)
+ return; // GLES doesn't provide GL_TEXTURE_LUMINANCE_SIZE.
+
+ GLuint tex;
+ gl->GenTextures(1, &tex);
+ gl->BindTexture(GL_TEXTURE_2D, tex);
+ gl->TexImage2D(GL_TEXTURE_2D, 0, gl_fmt->internal_format, 64, 64, 0,
+ gl_fmt->format, gl_fmt->type, NULL);
+ for (int i = 0; i < fmt->num_components; i++) {
+ const GLenum pnames[] = {
+ GL_TEXTURE_RED_SIZE,
+ GL_TEXTURE_GREEN_SIZE,
+ GL_TEXTURE_BLUE_SIZE,
+ GL_TEXTURE_ALPHA_SIZE,
+ GL_TEXTURE_LUMINANCE_SIZE,
+ GL_TEXTURE_ALPHA_SIZE,
+ };
+ int comp = is_la ? i + 4 : i;
+ assert(comp < MP_ARRAY_SIZE(pnames));
+ GLint param = -1;
+ gl->GetTexLevelParameteriv(GL_TEXTURE_2D, 0, pnames[comp], &param);
+ fmt->component_depth[i] = param > 0 ? param : 0;
+ }
+ gl->DeleteTextures(1, &tex);
+}
+
+static int ra_init_gl(struct ra *ra, GL *gl)
+{
+ if (gl->version < 210 && gl->es < 200) {
+ MP_ERR(ra, "At least OpenGL 2.1 or OpenGL ES 2.0 required.\n");
+ return -1;
+ }
+
+ struct ra_gl *p = ra->priv = talloc_zero(NULL, struct ra_gl);
+ p->gl = gl;
+
+ ra_gl_set_debug(ra, true);
+
+ ra->fns = &ra_fns_gl;
+ ra->glsl_version = gl->glsl_version;
+ ra->glsl_es = gl->es > 0;
+
+ static const int caps_map[][2] = {
+ {RA_CAP_DIRECT_UPLOAD, 0},
+ {RA_CAP_GLOBAL_UNIFORM, 0},
+ {RA_CAP_FRAGCOORD, 0},
+ {RA_CAP_TEX_1D, MPGL_CAP_1D_TEX},
+ {RA_CAP_TEX_3D, MPGL_CAP_3D_TEX},
+ {RA_CAP_COMPUTE, MPGL_CAP_COMPUTE_SHADER},
+ {RA_CAP_NUM_GROUPS, MPGL_CAP_COMPUTE_SHADER},
+ {RA_CAP_NESTED_ARRAY, MPGL_CAP_NESTED_ARRAY},
+ {RA_CAP_SLOW_DR, MPGL_CAP_SLOW_DR},
+ };
+
+ for (int i = 0; i < MP_ARRAY_SIZE(caps_map); i++) {
+ if ((gl->mpgl_caps & caps_map[i][1]) == caps_map[i][1])
+ ra->caps |= caps_map[i][0];
+ }
+
+ if (gl->BindBufferBase) {
+ if (gl->mpgl_caps & MPGL_CAP_UBO)
+ ra->caps |= RA_CAP_BUF_RO;
+ if (gl->mpgl_caps & MPGL_CAP_SSBO)
+ ra->caps |= RA_CAP_BUF_RW;
+ }
+
+ // textureGather is only supported in GLSL 400+ / ES 310+
+ if (ra->glsl_version >= (ra->glsl_es ? 310 : 400))
+ ra->caps |= RA_CAP_GATHER;
+
+ if (gl->BlitFramebuffer)
+ ra->caps |= RA_CAP_BLIT;
+
+ // Disable compute shaders for GLSL < 420. This work-around is needed since
+ // some buggy OpenGL drivers expose compute shaders for lower GLSL versions,
+ // despite the spec requiring 420+.
+ if (ra->glsl_version < (ra->glsl_es ? 310 : 420)) {
+ ra->caps &= ~RA_CAP_COMPUTE;
+ }
+
+ // While we can handle compute shaders on GLES the spec (intentionally)
+ // does not support binding textures for writing, which all uses inside mpv
+ // would require. So disable it unconditionally anyway.
+ if (ra->glsl_es)
+ ra->caps &= ~RA_CAP_COMPUTE;
+
+ int gl_fmt_features = gl_format_feature_flags(gl);
+
+ for (int n = 0; gl_formats[n].internal_format; n++) {
+ const struct gl_format *gl_fmt = &gl_formats[n];
+
+ if (!(gl_fmt->flags & gl_fmt_features))
+ continue;
+
+ struct ra_format *fmt = talloc_zero(ra, struct ra_format);
+ *fmt = (struct ra_format){
+ .name = gl_fmt->name,
+ .priv = (void *)gl_fmt,
+ .ctype = gl_format_type(gl_fmt),
+ .num_components = gl_format_components(gl_fmt->format),
+ .ordered = gl_fmt->format != GL_RGB_422_APPLE,
+ .pixel_size = gl_bytes_per_pixel(gl_fmt->format, gl_fmt->type),
+ .luminance_alpha = gl_fmt->format == GL_LUMINANCE_ALPHA,
+ .linear_filter = gl_fmt->flags & F_TF,
+ .renderable = (gl_fmt->flags & F_CR) &&
+ (gl->mpgl_caps & MPGL_CAP_FB),
+ // TODO: Check whether it's a storable format
+ // https://www.khronos.org/opengl/wiki/Image_Load_Store
+ .storable = true,
+ };
+
+ int csize = gl_component_size(gl_fmt->type) * 8;
+ int depth = csize;
+
+ if (gl_fmt->flags & F_F16) {
+ depth = 16;
+ csize = 32; // always upload as GL_FLOAT (simpler for us)
+ }
+
+ for (int i = 0; i < fmt->num_components; i++) {
+ fmt->component_size[i] = csize;
+ fmt->component_depth[i] = depth;
+ }
+
+ if (fmt->ctype == RA_CTYPE_UNORM && depth != 8)
+ probe_real_size(gl, fmt);
+
+ // Special formats for which OpenGL happens to have direct support.
+ if (strcmp(fmt->name, "rgb565") == 0) {
+ fmt->special_imgfmt = IMGFMT_RGB565;
+ struct ra_imgfmt_desc *desc = talloc_zero(fmt, struct ra_imgfmt_desc);
+ fmt->special_imgfmt_desc = desc;
+ desc->num_planes = 1;
+ desc->planes[0] = fmt;
+ for (int i = 0; i < 3; i++)
+ desc->components[0][i] = i + 1;
+ desc->chroma_w = desc->chroma_h = 1;
+ }
+ if (strcmp(fmt->name, "rgb10_a2") == 0) {
+ fmt->special_imgfmt = IMGFMT_RGB30;
+ struct ra_imgfmt_desc *desc = talloc_zero(fmt, struct ra_imgfmt_desc);
+ fmt->special_imgfmt_desc = desc;
+ desc->component_bits = 10;
+ desc->num_planes = 1;
+ desc->planes[0] = fmt;
+ for (int i = 0; i < 3; i++)
+ desc->components[0][i] = 3 - i;
+ desc->chroma_w = desc->chroma_h = 1;
+ }
+ if (strcmp(fmt->name, "appleyp") == 0) {
+ fmt->special_imgfmt = IMGFMT_UYVY;
+ struct ra_imgfmt_desc *desc = talloc_zero(fmt, struct ra_imgfmt_desc);
+ fmt->special_imgfmt_desc = desc;
+ desc->num_planes = 1;
+ desc->planes[0] = fmt;
+ desc->components[0][0] = 3;
+ desc->components[0][1] = 1;
+ desc->components[0][2] = 2;
+ desc->chroma_w = desc->chroma_h = 1;
+ }
+
+ fmt->glsl_format = ra_fmt_glsl_format(fmt);
+
+ MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, fmt);
+ }
+
+ GLint ival;
+ gl->GetIntegerv(GL_MAX_TEXTURE_SIZE, &ival);
+ ra->max_texture_wh = ival;
+
+ if (ra->caps & RA_CAP_COMPUTE) {
+ gl->GetIntegerv(GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, &ival);
+ ra->max_shmem = ival;
+ gl->GetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &ival);
+ ra->max_compute_group_threads = ival;
+ }
+
+ gl->Disable(GL_DITHER);
+
+ if (!ra_find_unorm_format(ra, 2, 1))
+ MP_VERBOSE(ra, "16 bit UNORM textures not available.\n");
+
+ return 0;
+}
+
+struct ra *ra_create_gl(GL *gl, struct mp_log *log)
+{
+ struct ra *ra = talloc_zero(NULL, struct ra);
+ ra->log = log;
+ if (ra_init_gl(ra, gl) < 0) {
+ talloc_free(ra);
+ return NULL;
+ }
+ return ra;
+}
+
+static void gl_destroy(struct ra *ra)
+{
+ talloc_free(ra->priv);
+}
+
+void ra_gl_set_debug(struct ra *ra, bool enable)
+{
+ struct ra_gl *p = ra->priv;
+ GL *gl = ra_gl_get(ra);
+
+ p->debug_enable = enable;
+ if (gl->debug_context)
+ gl_set_debug_logger(gl, enable ? ra->log : NULL);
+}
+
+static void gl_tex_destroy(struct ra *ra, struct ra_tex *tex)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_tex_gl *tex_gl = tex->priv;
+
+ ra_buf_pool_uninit(ra, &tex_gl->pbo);
+
+ if (tex_gl->own_objects) {
+ if (tex_gl->fbo)
+ gl->DeleteFramebuffers(1, &tex_gl->fbo);
+
+ gl->DeleteTextures(1, &tex_gl->texture);
+ }
+ talloc_free(tex_gl);
+ talloc_free(tex);
+}
+
+static struct ra_tex *gl_tex_create_blank(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ tex->params = *params;
+ tex->params.initial_data = NULL;
+ struct ra_tex_gl *tex_gl = tex->priv = talloc_zero(NULL, struct ra_tex_gl);
+
+ const struct gl_format *fmt = params->format->priv;
+ tex_gl->internal_format = fmt->internal_format;
+ tex_gl->format = fmt->format;
+ tex_gl->type = fmt->type;
+ switch (params->dimensions) {
+ case 1: tex_gl->target = GL_TEXTURE_1D; break;
+ case 2: tex_gl->target = GL_TEXTURE_2D; break;
+ case 3: tex_gl->target = GL_TEXTURE_3D; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+ if (params->non_normalized) {
+ assert(params->dimensions == 2);
+ tex_gl->target = GL_TEXTURE_RECTANGLE;
+ }
+ if (params->external_oes) {
+ assert(params->dimensions == 2 && !params->non_normalized);
+ tex_gl->target = GL_TEXTURE_EXTERNAL_OES;
+ }
+
+ if (params->downloadable && !(params->dimensions == 2 &&
+ params->format->renderable))
+ {
+ gl_tex_destroy(ra, tex);
+ return NULL;
+ }
+
+ return tex;
+}
+
+static struct ra_tex *gl_tex_create(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ assert(!params->format->dummy_format);
+
+ struct ra_tex *tex = gl_tex_create_blank(ra, params);
+ if (!tex)
+ return NULL;
+ struct ra_tex_gl *tex_gl = tex->priv;
+
+ tex_gl->own_objects = true;
+
+ gl->GenTextures(1, &tex_gl->texture);
+ gl->BindTexture(tex_gl->target, tex_gl->texture);
+
+ GLint filter = params->src_linear ? GL_LINEAR : GL_NEAREST;
+ GLint wrap = params->src_repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE;
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_MIN_FILTER, filter);
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_MAG_FILTER, filter);
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_WRAP_S, wrap);
+ if (params->dimensions > 1)
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_WRAP_T, wrap);
+ if (params->dimensions > 2)
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_WRAP_R, wrap);
+
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ switch (params->dimensions) {
+ case 1:
+ gl->TexImage1D(tex_gl->target, 0, tex_gl->internal_format, params->w,
+ 0, tex_gl->format, tex_gl->type, params->initial_data);
+ break;
+ case 2:
+ gl->TexImage2D(tex_gl->target, 0, tex_gl->internal_format, params->w,
+ params->h, 0, tex_gl->format, tex_gl->type,
+ params->initial_data);
+ break;
+ case 3:
+ gl->TexImage3D(tex_gl->target, 0, tex_gl->internal_format, params->w,
+ params->h, params->d, 0, tex_gl->format, tex_gl->type,
+ params->initial_data);
+ break;
+ }
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 4);
+
+ gl->BindTexture(tex_gl->target, 0);
+
+ gl_check_error(gl, ra->log, "after creating texture");
+
+ // Even blitting needs an FBO in OpenGL for strange reasons.
+ // Download is handled by reading from an FBO.
+ if (tex->params.render_dst || tex->params.blit_src ||
+ tex->params.blit_dst || tex->params.downloadable)
+ {
+ if (!tex->params.format->renderable) {
+ MP_ERR(ra, "Trying to create renderable texture with unsupported "
+ "format.\n");
+ ra_tex_free(ra, &tex);
+ return NULL;
+ }
+
+ assert(gl->mpgl_caps & MPGL_CAP_FB);
+
+ gl->GenFramebuffers(1, &tex_gl->fbo);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, tex_gl->fbo);
+ gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_TEXTURE_2D, tex_gl->texture, 0);
+ GLenum err = gl->CheckFramebufferStatus(GL_FRAMEBUFFER);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ if (err != GL_FRAMEBUFFER_COMPLETE) {
+ MP_ERR(ra, "Error: framebuffer completeness check failed (error=%d).\n",
+ (int)err);
+ ra_tex_free(ra, &tex);
+ return NULL;
+ }
+
+
+ gl_check_error(gl, ra->log, "after creating framebuffer");
+ }
+
+ return tex;
+}
+
+// Create a ra_tex that merely wraps an existing texture. The returned object
+// is freed with ra_tex_free(), but this will not delete the texture passed to
+// this function.
+// Some features are unsupported, e.g. setting params->initial_data or render_dst.
+struct ra_tex *ra_create_wrapped_tex(struct ra *ra,
+ const struct ra_tex_params *params,
+ GLuint gl_texture)
+{
+ struct ra_tex *tex = gl_tex_create_blank(ra, params);
+ if (!tex)
+ return NULL;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ tex_gl->texture = gl_texture;
+ return tex;
+}
+
+static const struct ra_format fbo_dummy_format = {
+ .name = "unknown_fbo",
+ .priv = (void *)&(const struct gl_format){
+ .name = "unknown",
+ .format = GL_RGBA,
+ .flags = F_CR,
+ },
+ .renderable = true,
+ .dummy_format = true,
+};
+
+// Create a ra_tex that merely wraps an existing framebuffer. gl_fbo can be 0
+// to wrap the default framebuffer.
+// The returned object is freed with ra_tex_free(), but this will not delete
+// the framebuffer object passed to this function.
+struct ra_tex *ra_create_wrapped_fb(struct ra *ra, GLuint gl_fbo, int w, int h)
+{
+ struct ra_tex *tex = talloc_zero(ra, struct ra_tex);
+ *tex = (struct ra_tex){
+ .params = {
+ .dimensions = 2,
+ .w = w, .h = h, .d = 1,
+ .format = &fbo_dummy_format,
+ .render_dst = true,
+ .blit_src = true,
+ .blit_dst = true,
+ },
+ };
+
+ struct ra_tex_gl *tex_gl = tex->priv = talloc_zero(NULL, struct ra_tex_gl);
+ *tex_gl = (struct ra_tex_gl){
+ .fbo = gl_fbo,
+ .internal_format = 0,
+ .format = GL_RGBA,
+ .type = 0,
+ };
+
+ return tex;
+}
+
+GL *ra_gl_get(struct ra *ra)
+{
+ struct ra_gl *p = ra->priv;
+ return p->gl;
+}
+
+// Return the associate glTexImage arguments for the given format. Sets all
+// fields to 0 on failure.
+void ra_gl_get_format(const struct ra_format *fmt, GLint *out_internal_format,
+ GLenum *out_format, GLenum *out_type)
+{
+ const struct gl_format *gl_format = fmt->priv;
+ *out_internal_format = gl_format->internal_format;
+ *out_format = gl_format->format;
+ *out_type = gl_format->type;
+}
+
+void ra_gl_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ GLuint *out_texture, GLenum *out_target)
+{
+ struct ra_tex_gl *tex_gl = tex->priv;
+ *out_texture = tex_gl->texture;
+ *out_target = tex_gl->target;
+}
+
+// Return whether the ra instance was created with ra_create_gl(). This is the
+// _only_ function that can be called on a ra instance of any type.
+bool ra_is_gl(struct ra *ra)
+{
+ return ra->fns == &ra_fns_gl;
+}
+
+static bool gl_tex_upload(struct ra *ra,
+ const struct ra_tex_upload_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_tex *tex = params->tex;
+ struct ra_buf *buf = params->buf;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ struct ra_buf_gl *buf_gl = buf ? buf->priv : NULL;
+ assert(tex->params.host_mutable);
+ assert(!params->buf || !params->src);
+
+ if (ra->use_pbo && !params->buf)
+ return ra_tex_upload_pbo(ra, &tex_gl->pbo, params);
+
+ const void *src = params->src;
+ if (buf) {
+ gl->BindBuffer(GL_PIXEL_UNPACK_BUFFER, buf_gl->buffer);
+ src = (void *)params->buf_offset;
+ }
+
+ gl->BindTexture(tex_gl->target, tex_gl->texture);
+ if (params->invalidate && gl->InvalidateTexImage)
+ gl->InvalidateTexImage(tex_gl->texture, 0);
+
+ switch (tex->params.dimensions) {
+ case 1:
+ gl->TexImage1D(tex_gl->target, 0, tex_gl->internal_format,
+ tex->params.w, 0, tex_gl->format, tex_gl->type, src);
+ break;
+ case 2: {
+ struct mp_rect rc = {0, 0, tex->params.w, tex->params.h};
+ if (params->rc)
+ rc = *params->rc;
+ gl_upload_tex(gl, tex_gl->target, tex_gl->format, tex_gl->type,
+ src, params->stride, rc.x0, rc.y0, rc.x1 - rc.x0,
+ rc.y1 - rc.y0);
+ break;
+ }
+ case 3:
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ gl->TexImage3D(GL_TEXTURE_3D, 0, tex_gl->internal_format, tex->params.w,
+ tex->params.h, tex->params.d, 0, tex_gl->format,
+ tex_gl->type, src);
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 4);
+ break;
+ }
+
+ gl->BindTexture(tex_gl->target, 0);
+
+ if (buf) {
+ gl->BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ if (buf->params.host_mapped) {
+ // Make sure the PBO is not reused until GL is done with it. If a
+ // previous operation is pending, "update" it by creating a new
+ // fence that will cover the previous operation as well.
+ gl->DeleteSync(buf_gl->fence);
+ buf_gl->fence = gl->FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ }
+ }
+
+ return true;
+}
+
+static bool gl_tex_download(struct ra *ra, struct ra_tex_download_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_tex *tex = params->tex;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ if (!tex_gl->fbo)
+ return false;
+ return gl_read_fbo_contents(gl, tex_gl->fbo, 1, tex_gl->format, tex_gl->type,
+ tex->params.w, tex->params.h, params->dst,
+ params->stride);
+}
+
+static void gl_buf_destroy(struct ra *ra, struct ra_buf *buf)
+{
+ if (!buf)
+ return;
+
+ GL *gl = ra_gl_get(ra);
+ struct ra_buf_gl *buf_gl = buf->priv;
+
+ if (buf_gl->fence)
+ gl->DeleteSync(buf_gl->fence);
+
+ if (buf->data) {
+ gl->BindBuffer(buf_gl->target, buf_gl->buffer);
+ gl->UnmapBuffer(buf_gl->target);
+ gl->BindBuffer(buf_gl->target, 0);
+ }
+ gl->DeleteBuffers(1, &buf_gl->buffer);
+
+ talloc_free(buf_gl);
+ talloc_free(buf);
+}
+
+static struct ra_buf *gl_buf_create(struct ra *ra,
+ const struct ra_buf_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+
+ if (params->host_mapped && !gl->BufferStorage)
+ return NULL;
+
+ struct ra_buf *buf = talloc_zero(NULL, struct ra_buf);
+ buf->params = *params;
+ buf->params.initial_data = NULL;
+
+ struct ra_buf_gl *buf_gl = buf->priv = talloc_zero(NULL, struct ra_buf_gl);
+ gl->GenBuffers(1, &buf_gl->buffer);
+
+ switch (params->type) {
+ case RA_BUF_TYPE_TEX_UPLOAD: buf_gl->target = GL_PIXEL_UNPACK_BUFFER; break;
+ case RA_BUF_TYPE_SHADER_STORAGE: buf_gl->target = GL_SHADER_STORAGE_BUFFER; break;
+ case RA_BUF_TYPE_UNIFORM: buf_gl->target = GL_UNIFORM_BUFFER; break;
+ default: abort();
+ };
+
+ gl->BindBuffer(buf_gl->target, buf_gl->buffer);
+
+ if (params->host_mapped) {
+ unsigned flags = GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT |
+ GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
+
+ unsigned storflags = flags;
+ if (params->type == RA_BUF_TYPE_TEX_UPLOAD)
+ storflags |= GL_CLIENT_STORAGE_BIT;
+
+ gl->BufferStorage(buf_gl->target, params->size, params->initial_data,
+ storflags);
+ buf->data = gl->MapBufferRange(buf_gl->target, 0, params->size, flags);
+ if (!buf->data) {
+ gl_check_error(gl, ra->log, "mapping buffer");
+ gl_buf_destroy(ra, buf);
+ buf = NULL;
+ }
+ } else {
+ GLenum hint;
+ switch (params->type) {
+ case RA_BUF_TYPE_TEX_UPLOAD: hint = GL_STREAM_DRAW; break;
+ case RA_BUF_TYPE_SHADER_STORAGE: hint = GL_STREAM_COPY; break;
+ case RA_BUF_TYPE_UNIFORM: hint = GL_STATIC_DRAW; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ gl->BufferData(buf_gl->target, params->size, params->initial_data, hint);
+ }
+
+ gl->BindBuffer(buf_gl->target, 0);
+ return buf;
+}
+
+static void gl_buf_update(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_buf_gl *buf_gl = buf->priv;
+ assert(buf->params.host_mutable);
+
+ gl->BindBuffer(buf_gl->target, buf_gl->buffer);
+ gl->BufferSubData(buf_gl->target, offset, size, data);
+ gl->BindBuffer(buf_gl->target, 0);
+}
+
+static bool gl_buf_poll(struct ra *ra, struct ra_buf *buf)
+{
+ // Non-persistently mapped buffers are always implicitly reusable in OpenGL,
+ // the implementation will create more buffers under the hood if needed.
+ if (!buf->data)
+ return true;
+
+ GL *gl = ra_gl_get(ra);
+ struct ra_buf_gl *buf_gl = buf->priv;
+
+ if (buf_gl->fence) {
+ GLenum res = gl->ClientWaitSync(buf_gl->fence, 0, 0); // non-blocking
+ if (res == GL_ALREADY_SIGNALED) {
+ gl->DeleteSync(buf_gl->fence);
+ buf_gl->fence = NULL;
+ }
+ }
+
+ return !buf_gl->fence;
+}
+
+static void gl_clear(struct ra *ra, struct ra_tex *dst, float color[4],
+ struct mp_rect *scissor)
+{
+ GL *gl = ra_gl_get(ra);
+
+ assert(dst->params.render_dst);
+ struct ra_tex_gl *dst_gl = dst->priv;
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, dst_gl->fbo);
+
+ gl->Scissor(scissor->x0, scissor->y0,
+ scissor->x1 - scissor->x0,
+ scissor->y1 - scissor->y0);
+
+ gl->Enable(GL_SCISSOR_TEST);
+ gl->ClearColor(color[0], color[1], color[2], color[3]);
+ gl->Clear(GL_COLOR_BUFFER_BIT);
+ gl->Disable(GL_SCISSOR_TEST);
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+static void gl_blit(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc)
+{
+ GL *gl = ra_gl_get(ra);
+
+ assert(src->params.blit_src);
+ assert(dst->params.blit_dst);
+
+ struct ra_tex_gl *src_gl = src->priv;
+ struct ra_tex_gl *dst_gl = dst->priv;
+
+ gl->BindFramebuffer(GL_READ_FRAMEBUFFER, src_gl->fbo);
+ gl->BindFramebuffer(GL_DRAW_FRAMEBUFFER, dst_gl->fbo);
+ gl->BlitFramebuffer(src_rc->x0, src_rc->y0, src_rc->x1, src_rc->y1,
+ dst_rc->x0, dst_rc->y0, dst_rc->x1, dst_rc->y1,
+ GL_COLOR_BUFFER_BIT, GL_NEAREST);
+ gl->BindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+ gl->BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+}
+
+static int gl_desc_namespace(struct ra *ra, enum ra_vartype type)
+{
+ return type;
+}
+
+static void gl_renderpass_destroy(struct ra *ra, struct ra_renderpass *pass)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_renderpass_gl *pass_gl = pass->priv;
+ gl->DeleteProgram(pass_gl->program);
+ gl_vao_uninit(&pass_gl->vao);
+
+ talloc_free(pass_gl);
+ talloc_free(pass);
+}
+
+static const char *shader_typestr(GLenum type)
+{
+ switch (type) {
+ case GL_VERTEX_SHADER: return "vertex";
+ case GL_FRAGMENT_SHADER: return "fragment";
+ case GL_COMPUTE_SHADER: return "compute";
+ default: MP_ASSERT_UNREACHABLE();
+ }
+}
+
+static void compile_attach_shader(struct ra *ra, GLuint program,
+ GLenum type, const char *source, bool *ok)
+{
+ GL *gl = ra_gl_get(ra);
+
+ GLuint shader = gl->CreateShader(type);
+ gl->ShaderSource(shader, 1, &source, NULL);
+ gl->CompileShader(shader);
+ GLint status = 0;
+ gl->GetShaderiv(shader, GL_COMPILE_STATUS, &status);
+ GLint log_length = 0;
+ gl->GetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
+
+ int pri = status ? (log_length > 1 ? MSGL_V : MSGL_DEBUG) : MSGL_ERR;
+ const char *typestr = shader_typestr(type);
+ if (mp_msg_test(ra->log, pri)) {
+ MP_MSG(ra, pri, "%s shader source:\n", typestr);
+ mp_log_source(ra->log, pri, source);
+ }
+ if (log_length > 1) {
+ GLchar *logstr = talloc_zero_size(NULL, log_length + 1);
+ gl->GetShaderInfoLog(shader, log_length, NULL, logstr);
+ MP_MSG(ra, pri, "%s shader compile log (status=%d):\n%s\n",
+ typestr, status, logstr);
+ talloc_free(logstr);
+ }
+ if (gl->GetTranslatedShaderSourceANGLE && mp_msg_test(ra->log, MSGL_DEBUG)) {
+ GLint len = 0;
+ gl->GetShaderiv(shader, GL_TRANSLATED_SHADER_SOURCE_LENGTH_ANGLE, &len);
+ if (len > 0) {
+ GLchar *sstr = talloc_zero_size(NULL, len + 1);
+ gl->GetTranslatedShaderSourceANGLE(shader, len, NULL, sstr);
+ MP_DBG(ra, "Translated shader:\n");
+ mp_log_source(ra->log, MSGL_DEBUG, sstr);
+ }
+ }
+
+ gl->AttachShader(program, shader);
+ gl->DeleteShader(shader);
+
+ *ok &= status;
+}
+
+static void link_shader(struct ra *ra, GLuint program, bool *ok)
+{
+ GL *gl = ra_gl_get(ra);
+
+ gl->LinkProgram(program);
+ GLint status = 0;
+ gl->GetProgramiv(program, GL_LINK_STATUS, &status);
+ GLint log_length = 0;
+ gl->GetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length);
+
+ int pri = status ? (log_length > 1 ? MSGL_V : MSGL_DEBUG) : MSGL_ERR;
+ if (mp_msg_test(ra->log, pri)) {
+ GLchar *logstr = talloc_zero_size(NULL, log_length + 1);
+ gl->GetProgramInfoLog(program, log_length, NULL, logstr);
+ MP_MSG(ra, pri, "shader link log (status=%d): %s\n", status, logstr);
+ talloc_free(logstr);
+ }
+
+ *ok &= status;
+}
+
+// either 'compute' or both 'vertex' and 'frag' are needed
+static GLuint compile_program(struct ra *ra, const struct ra_renderpass_params *p)
+{
+ GL *gl = ra_gl_get(ra);
+
+ GLuint prog = gl->CreateProgram();
+ bool ok = true;
+ if (p->type == RA_RENDERPASS_TYPE_COMPUTE)
+ compile_attach_shader(ra, prog, GL_COMPUTE_SHADER, p->compute_shader, &ok);
+ if (p->type == RA_RENDERPASS_TYPE_RASTER) {
+ compile_attach_shader(ra, prog, GL_VERTEX_SHADER, p->vertex_shader, &ok);
+ compile_attach_shader(ra, prog, GL_FRAGMENT_SHADER, p->frag_shader, &ok);
+ for (int n = 0; n < p->num_vertex_attribs; n++)
+ gl->BindAttribLocation(prog, n, p->vertex_attribs[n].name);
+ }
+ link_shader(ra, prog, &ok);
+ if (!ok) {
+ gl->DeleteProgram(prog);
+ prog = 0;
+ }
+ return prog;
+}
+
+static GLuint load_program(struct ra *ra, const struct ra_renderpass_params *p,
+ bstr *out_cached_data)
+{
+ GL *gl = ra_gl_get(ra);
+
+ GLuint prog = 0;
+
+ if (gl->ProgramBinary && p->cached_program.len > 4) {
+ GLenum format = AV_RL32(p->cached_program.start);
+ prog = gl->CreateProgram();
+ gl_check_error(gl, ra->log, "before loading program");
+ gl->ProgramBinary(prog, format, p->cached_program.start + 4,
+ p->cached_program.len - 4);
+ gl->GetError(); // discard potential useless error
+ GLint status = 0;
+ gl->GetProgramiv(prog, GL_LINK_STATUS, &status);
+ if (status) {
+ MP_DBG(ra, "Loading binary program succeeded.\n");
+ } else {
+ gl->DeleteProgram(prog);
+ prog = 0;
+ }
+ }
+
+ if (!prog) {
+ prog = compile_program(ra, p);
+
+ if (gl->GetProgramBinary && prog) {
+ GLint size = 0;
+ gl->GetProgramiv(prog, GL_PROGRAM_BINARY_LENGTH, &size);
+ uint8_t *buffer = talloc_size(NULL, size + 4);
+ GLsizei actual_size = 0;
+ GLenum binary_format = 0;
+ if (size > 0) {
+ gl->GetProgramBinary(prog, size, &actual_size, &binary_format,
+ buffer + 4);
+ }
+ AV_WL32(buffer, binary_format);
+ if (actual_size) {
+ *out_cached_data = (bstr){buffer, actual_size + 4};
+ } else {
+ talloc_free(buffer);
+ }
+ }
+ }
+
+ return prog;
+}
+
+static struct ra_renderpass *gl_renderpass_create(struct ra *ra,
+ const struct ra_renderpass_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+
+ struct ra_renderpass *pass = talloc_zero(NULL, struct ra_renderpass);
+ pass->params = *ra_renderpass_params_copy(pass, params);
+ pass->params.cached_program = (bstr){0};
+ struct ra_renderpass_gl *pass_gl = pass->priv =
+ talloc_zero(NULL, struct ra_renderpass_gl);
+
+ bstr cached = {0};
+ pass_gl->program = load_program(ra, params, &cached);
+ if (!pass_gl->program) {
+ gl_renderpass_destroy(ra, pass);
+ return NULL;
+ }
+
+ talloc_steal(pass, cached.start);
+ pass->params.cached_program = cached;
+
+ gl->UseProgram(pass_gl->program);
+ for (int n = 0; n < params->num_inputs; n++) {
+ GLint loc =
+ gl->GetUniformLocation(pass_gl->program, params->inputs[n].name);
+ MP_TARRAY_APPEND(pass_gl, pass_gl->uniform_loc, pass_gl->num_uniform_loc,
+ loc);
+
+ // For compatibility with older OpenGL, we need to explicitly update
+ // the texture/image unit bindings after creating the shader program,
+ // since specifying it directly requires GLSL 4.20+
+ switch (params->inputs[n].type) {
+ case RA_VARTYPE_TEX:
+ case RA_VARTYPE_IMG_W:
+ gl->Uniform1i(loc, params->inputs[n].binding);
+ break;
+ }
+ }
+ gl->UseProgram(0);
+
+ gl_vao_init(&pass_gl->vao, gl, pass->params.vertex_stride,
+ pass->params.vertex_attribs, pass->params.num_vertex_attribs);
+
+ return pass;
+}
+
+static GLenum map_blend(enum ra_blend blend)
+{
+ switch (blend) {
+ case RA_BLEND_ZERO: return GL_ZERO;
+ case RA_BLEND_ONE: return GL_ONE;
+ case RA_BLEND_SRC_ALPHA: return GL_SRC_ALPHA;
+ case RA_BLEND_ONE_MINUS_SRC_ALPHA: return GL_ONE_MINUS_SRC_ALPHA;
+ default: return 0;
+ }
+}
+
+// Assumes program is current (gl->UseProgram(program)).
+static void update_uniform(struct ra *ra, struct ra_renderpass *pass,
+ struct ra_renderpass_input_val *val)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_renderpass_gl *pass_gl = pass->priv;
+
+ struct ra_renderpass_input *input = &pass->params.inputs[val->index];
+ assert(val->index >= 0 && val->index < pass_gl->num_uniform_loc);
+ GLint loc = pass_gl->uniform_loc[val->index];
+
+ switch (input->type) {
+ case RA_VARTYPE_INT: {
+ assert(input->dim_v * input->dim_m == 1);
+ if (loc < 0)
+ break;
+ gl->Uniform1i(loc, *(int *)val->data);
+ break;
+ }
+ case RA_VARTYPE_FLOAT: {
+ float *f = val->data;
+ if (loc < 0)
+ break;
+ if (input->dim_m == 1) {
+ switch (input->dim_v) {
+ case 1: gl->Uniform1f(loc, f[0]); break;
+ case 2: gl->Uniform2f(loc, f[0], f[1]); break;
+ case 3: gl->Uniform3f(loc, f[0], f[1], f[2]); break;
+ case 4: gl->Uniform4f(loc, f[0], f[1], f[2], f[3]); break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+ } else if (input->dim_v == 2 && input->dim_m == 2) {
+ gl->UniformMatrix2fv(loc, 1, GL_FALSE, f);
+ } else if (input->dim_v == 3 && input->dim_m == 3) {
+ gl->UniformMatrix3fv(loc, 1, GL_FALSE, f);
+ } else {
+ MP_ASSERT_UNREACHABLE();
+ }
+ break;
+ }
+ case RA_VARTYPE_IMG_W: {
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ assert(tex->params.storage_dst);
+ gl->BindImageTexture(input->binding, tex_gl->texture, 0, GL_FALSE, 0,
+ GL_WRITE_ONLY, tex_gl->internal_format);
+ break;
+ }
+ case RA_VARTYPE_TEX: {
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ assert(tex->params.render_src);
+ gl->ActiveTexture(GL_TEXTURE0 + input->binding);
+ gl->BindTexture(tex_gl->target, tex_gl->texture);
+ break;
+ }
+ case RA_VARTYPE_BUF_RO: // fall through
+ case RA_VARTYPE_BUF_RW: {
+ struct ra_buf *buf = *(struct ra_buf **)val->data;
+ struct ra_buf_gl *buf_gl = buf->priv;
+ gl->BindBufferBase(buf_gl->target, input->binding, buf_gl->buffer);
+ // SSBOs are not implicitly coherent in OpengL
+ if (input->type == RA_VARTYPE_BUF_RW)
+ gl->MemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
+ break;
+ }
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+}
+
+static void disable_binding(struct ra *ra, struct ra_renderpass *pass,
+ struct ra_renderpass_input_val *val)
+{
+ GL *gl = ra_gl_get(ra);
+
+ struct ra_renderpass_input *input = &pass->params.inputs[val->index];
+
+ switch (input->type) {
+ case RA_VARTYPE_IMG_W: /* fall through */
+ case RA_VARTYPE_TEX: {
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ assert(tex->params.render_src);
+ if (input->type == RA_VARTYPE_TEX) {
+ gl->ActiveTexture(GL_TEXTURE0 + input->binding);
+ gl->BindTexture(tex_gl->target, 0);
+ } else {
+ gl->BindImageTexture(input->binding, 0, 0, GL_FALSE, 0,
+ GL_WRITE_ONLY, tex_gl->internal_format);
+ }
+ break;
+ }
+ case RA_VARTYPE_BUF_RW:
+ gl->BindBufferBase(GL_SHADER_STORAGE_BUFFER, input->binding, 0);
+ break;
+ }
+}
+
+static void gl_renderpass_run(struct ra *ra,
+ const struct ra_renderpass_run_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_renderpass *pass = params->pass;
+ struct ra_renderpass_gl *pass_gl = pass->priv;
+
+ gl->UseProgram(pass_gl->program);
+
+ for (int n = 0; n < params->num_values; n++)
+ update_uniform(ra, pass, &params->values[n]);
+ gl->ActiveTexture(GL_TEXTURE0);
+
+ switch (pass->params.type) {
+ case RA_RENDERPASS_TYPE_RASTER: {
+ struct ra_tex_gl *target_gl = params->target->priv;
+ assert(params->target->params.render_dst);
+ assert(params->target->params.format == pass->params.target_format);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, target_gl->fbo);
+ if (pass->params.invalidate_target && gl->InvalidateFramebuffer) {
+ GLenum fb = target_gl->fbo ? GL_COLOR_ATTACHMENT0 : GL_COLOR;
+ gl->InvalidateFramebuffer(GL_FRAMEBUFFER, 1, &fb);
+ }
+ gl->Viewport(params->viewport.x0, params->viewport.y0,
+ mp_rect_w(params->viewport),
+ mp_rect_h(params->viewport));
+ gl->Scissor(params->scissors.x0, params->scissors.y0,
+ mp_rect_w(params->scissors),
+ mp_rect_h(params->scissors));
+ gl->Enable(GL_SCISSOR_TEST);
+ if (pass->params.enable_blend) {
+ gl->BlendFuncSeparate(map_blend(pass->params.blend_src_rgb),
+ map_blend(pass->params.blend_dst_rgb),
+ map_blend(pass->params.blend_src_alpha),
+ map_blend(pass->params.blend_dst_alpha));
+ gl->Enable(GL_BLEND);
+ }
+ gl_vao_draw_data(&pass_gl->vao, GL_TRIANGLES, params->vertex_data,
+ params->vertex_count);
+ gl->Disable(GL_SCISSOR_TEST);
+ gl->Disable(GL_BLEND);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+ break;
+ }
+ case RA_RENDERPASS_TYPE_COMPUTE: {
+ gl->DispatchCompute(params->compute_groups[0],
+ params->compute_groups[1],
+ params->compute_groups[2]);
+
+ gl->MemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT);
+ break;
+ }
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ for (int n = 0; n < params->num_values; n++)
+ disable_binding(ra, pass, &params->values[n]);
+ gl->ActiveTexture(GL_TEXTURE0);
+
+ gl->UseProgram(0);
+}
+
+// Timers in GL use query objects, and are asynchronous. So pool a few of
+// these together. GL_QUERY_OBJECT_NUM should be large enough to avoid this
+// ever blocking. We can afford to throw query objects around, there's no
+// practical limit on them and their overhead is small.
+
+#define GL_QUERY_OBJECT_NUM 8
+
+struct gl_timer {
+ GLuint query[GL_QUERY_OBJECT_NUM];
+ int idx;
+ uint64_t result;
+ bool active;
+};
+
+static ra_timer *gl_timer_create(struct ra *ra)
+{
+ GL *gl = ra_gl_get(ra);
+
+ if (!gl->GenQueries)
+ return NULL;
+
+ struct gl_timer *timer = talloc_zero(NULL, struct gl_timer);
+ gl->GenQueries(GL_QUERY_OBJECT_NUM, timer->query);
+
+ return (ra_timer *)timer;
+}
+
+static void gl_timer_destroy(struct ra *ra, ra_timer *ratimer)
+{
+ if (!ratimer)
+ return;
+
+ GL *gl = ra_gl_get(ra);
+ struct gl_timer *timer = ratimer;
+
+ gl->DeleteQueries(GL_QUERY_OBJECT_NUM, timer->query);
+ talloc_free(timer);
+}
+
+static void gl_timer_start(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_gl *p = ra->priv;
+ GL *gl = p->gl;
+ struct gl_timer *timer = ratimer;
+
+ // GL_TIME_ELAPSED queries are not re-entrant, so just do nothing instead
+ // of crashing. Work-around for shitty GL limitations
+ if (p->timer_active)
+ return;
+
+ // If this query object already contains a result, we need to retrieve it
+ timer->result = 0;
+ if (gl->IsQuery(timer->query[timer->idx])) {
+ gl->GetQueryObjectui64v(timer->query[timer->idx], GL_QUERY_RESULT,
+ &timer->result);
+ }
+
+ gl->BeginQuery(GL_TIME_ELAPSED, timer->query[timer->idx++]);
+ timer->idx %= GL_QUERY_OBJECT_NUM;
+
+ p->timer_active = timer->active = true;
+}
+
+static uint64_t gl_timer_stop(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_gl *p = ra->priv;
+ GL *gl = p->gl;
+ struct gl_timer *timer = ratimer;
+
+ if (!timer->active)
+ return 0;
+
+ gl->EndQuery(GL_TIME_ELAPSED);
+ p->timer_active = timer->active = false;
+
+ return timer->result;
+}
+
+static void gl_debug_marker(struct ra *ra, const char *msg)
+{
+ struct ra_gl *p = ra->priv;
+
+ if (p->debug_enable)
+ gl_check_error(p->gl, ra->log, msg);
+}
+
+static struct ra_fns ra_fns_gl = {
+ .destroy = gl_destroy,
+ .tex_create = gl_tex_create,
+ .tex_destroy = gl_tex_destroy,
+ .tex_upload = gl_tex_upload,
+ .tex_download = gl_tex_download,
+ .buf_create = gl_buf_create,
+ .buf_destroy = gl_buf_destroy,
+ .buf_update = gl_buf_update,
+ .buf_poll = gl_buf_poll,
+ .clear = gl_clear,
+ .blit = gl_blit,
+ .uniform_layout = std140_layout,
+ .desc_namespace = gl_desc_namespace,
+ .renderpass_create = gl_renderpass_create,
+ .renderpass_destroy = gl_renderpass_destroy,
+ .renderpass_run = gl_renderpass_run,
+ .timer_create = gl_timer_create,
+ .timer_destroy = gl_timer_destroy,
+ .timer_start = gl_timer_start,
+ .timer_stop = gl_timer_stop,
+ .debug_marker = gl_debug_marker,
+};
diff --git a/video/out/opengl/ra_gl.h b/video/out/opengl/ra_gl.h
new file mode 100644
index 0000000..9844977
--- /dev/null
+++ b/video/out/opengl/ra_gl.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "common.h"
+#include "utils.h"
+
+struct ra *ra_create_gl(GL *gl, struct mp_log *log);
+struct ra_tex *ra_create_wrapped_tex(struct ra *ra,
+ const struct ra_tex_params *params,
+ GLuint gl_texture);
+struct ra_tex *ra_create_wrapped_fb(struct ra *ra, GLuint gl_fbo, int w, int h);
+GL *ra_gl_get(struct ra *ra);
+void ra_gl_set_debug(struct ra *ra, bool enable);
+void ra_gl_get_format(const struct ra_format *fmt, GLint *out_internal_format,
+ GLenum *out_format, GLenum *out_type);
+void ra_gl_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ GLuint *out_texture, GLenum *out_target);
+bool ra_is_gl(struct ra *ra);
diff --git a/video/out/opengl/utils.c b/video/out/opengl/utils.c
new file mode 100644
index 0000000..a551ce4
--- /dev/null
+++ b/video/out/opengl/utils.c
@@ -0,0 +1,282 @@
+/*
+ * This file is part of mpv.
+ * Parts based on MPlayer code by Reimar Döffinger.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include <libavutil/sha.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/mem.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "options/path.h"
+#include "stream/stream.h"
+#include "formats.h"
+#include "utils.h"
+
+// GLU has this as gluErrorString (we don't use GLU, as it is legacy-OpenGL)
+static const char *gl_error_to_string(GLenum error)
+{
+ switch (error) {
+ case GL_INVALID_ENUM: return "INVALID_ENUM";
+ case GL_INVALID_VALUE: return "INVALID_VALUE";
+ case GL_INVALID_OPERATION: return "INVALID_OPERATION";
+ case GL_INVALID_FRAMEBUFFER_OPERATION: return "INVALID_FRAMEBUFFER_OPERATION";
+ case GL_OUT_OF_MEMORY: return "OUT_OF_MEMORY";
+ default: return "unknown";
+ }
+}
+
+void gl_check_error(GL *gl, struct mp_log *log, const char *info)
+{
+ for (;;) {
+ GLenum error = gl->GetError();
+ if (error == GL_NO_ERROR)
+ break;
+ mp_msg(log, MSGL_ERR, "%s: OpenGL error %s.\n", info,
+ gl_error_to_string(error));
+ }
+}
+
+static int get_alignment(int stride)
+{
+ if (stride % 8 == 0)
+ return 8;
+ if (stride % 4 == 0)
+ return 4;
+ if (stride % 2 == 0)
+ return 2;
+ return 1;
+}
+
+// upload a texture, handling things like stride and slices
+// target: texture target, usually GL_TEXTURE_2D
+// format, type: texture parameters
+// dataptr, stride: image data
+// x, y, width, height: part of the image to upload
+void gl_upload_tex(GL *gl, GLenum target, GLenum format, GLenum type,
+ const void *dataptr, int stride,
+ int x, int y, int w, int h)
+{
+ int bpp = gl_bytes_per_pixel(format, type);
+ const uint8_t *data = dataptr;
+ int y_max = y + h;
+ if (w <= 0 || h <= 0 || !bpp)
+ return;
+ assert(stride > 0);
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, get_alignment(stride));
+ int slice = h;
+ if (gl->mpgl_caps & MPGL_CAP_ROW_LENGTH) {
+ // this is not always correct, but should work for MPlayer
+ gl->PixelStorei(GL_UNPACK_ROW_LENGTH, stride / bpp);
+ } else {
+ if (stride != bpp * w)
+ slice = 1; // very inefficient, but at least it works
+ }
+ for (; y + slice <= y_max; y += slice) {
+ gl->TexSubImage2D(target, 0, x, y, w, slice, format, type, data);
+ data += stride * slice;
+ }
+ if (y < y_max)
+ gl->TexSubImage2D(target, 0, x, y, w, y_max - y, format, type, data);
+ if (gl->mpgl_caps & MPGL_CAP_ROW_LENGTH)
+ gl->PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 4);
+}
+
+bool gl_read_fbo_contents(GL *gl, int fbo, int dir, GLenum format, GLenum type,
+ int w, int h, uint8_t *dst, int dst_stride)
+{
+ assert(dir == 1 || dir == -1);
+ if (fbo == 0 && gl->es)
+ return false; // ES can't read from front buffer
+ gl->BindFramebuffer(GL_FRAMEBUFFER, fbo);
+ GLenum obj = fbo ? GL_COLOR_ATTACHMENT0 : GL_FRONT;
+ gl->PixelStorei(GL_PACK_ALIGNMENT, 1);
+ gl->ReadBuffer(obj);
+ // reading by line allows flipping, and avoids stride-related trouble
+ int y1 = dir > 0 ? 0 : h;
+ for (int y = 0; y < h; y++)
+ gl->ReadPixels(0, y, w, 1, format, type, dst + (y1 + dir * y) * dst_stride);
+ gl->PixelStorei(GL_PACK_ALIGNMENT, 4);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+ return true;
+}
+
+static void gl_vao_enable_attribs(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+
+ for (int n = 0; n < vao->num_entries; n++) {
+ const struct ra_renderpass_input *e = &vao->entries[n];
+ GLenum type = 0;
+ bool normalized = false;
+ switch (e->type) {
+ case RA_VARTYPE_INT:
+ type = GL_INT;
+ break;
+ case RA_VARTYPE_FLOAT:
+ type = GL_FLOAT;
+ break;
+ case RA_VARTYPE_BYTE_UNORM:
+ type = GL_UNSIGNED_BYTE;
+ normalized = true;
+ break;
+ default:
+ abort();
+ }
+ assert(e->dim_m == 1);
+
+ gl->EnableVertexAttribArray(n);
+ gl->VertexAttribPointer(n, e->dim_v, type, normalized,
+ vao->stride, (void *)(intptr_t)e->offset);
+ }
+}
+
+void gl_vao_init(struct gl_vao *vao, GL *gl, int stride,
+ const struct ra_renderpass_input *entries,
+ int num_entries)
+{
+ assert(!vao->vao);
+ assert(!vao->buffer);
+
+ *vao = (struct gl_vao){
+ .gl = gl,
+ .stride = stride,
+ .entries = entries,
+ .num_entries = num_entries,
+ };
+
+ gl->GenBuffers(1, &vao->buffer);
+
+ if (gl->BindVertexArray) {
+ gl->BindBuffer(GL_ARRAY_BUFFER, vao->buffer);
+
+ gl->GenVertexArrays(1, &vao->vao);
+ gl->BindVertexArray(vao->vao);
+ gl_vao_enable_attribs(vao);
+ gl->BindVertexArray(0);
+
+ gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+}
+
+void gl_vao_uninit(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+ if (!gl)
+ return;
+
+ if (gl->DeleteVertexArrays)
+ gl->DeleteVertexArrays(1, &vao->vao);
+ gl->DeleteBuffers(1, &vao->buffer);
+
+ *vao = (struct gl_vao){0};
+}
+
+static void gl_vao_bind(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+
+ if (gl->BindVertexArray) {
+ gl->BindVertexArray(vao->vao);
+ } else {
+ gl->BindBuffer(GL_ARRAY_BUFFER, vao->buffer);
+ gl_vao_enable_attribs(vao);
+ gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+}
+
+static void gl_vao_unbind(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+
+ if (gl->BindVertexArray) {
+ gl->BindVertexArray(0);
+ } else {
+ for (int n = 0; n < vao->num_entries; n++)
+ gl->DisableVertexAttribArray(n);
+ }
+}
+
+// Draw the vertex data (as described by the gl_vao_entry entries) in ptr
+// to the screen. num is the number of vertexes. prim is usually GL_TRIANGLES.
+// If ptr is NULL, then skip the upload, and use the data uploaded with the
+// previous call.
+void gl_vao_draw_data(struct gl_vao *vao, GLenum prim, void *ptr, size_t num)
+{
+ GL *gl = vao->gl;
+
+ if (ptr) {
+ gl->BindBuffer(GL_ARRAY_BUFFER, vao->buffer);
+ gl->BufferData(GL_ARRAY_BUFFER, num * vao->stride, ptr, GL_STREAM_DRAW);
+ gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ gl_vao_bind(vao);
+
+ gl->DrawArrays(prim, 0, num);
+
+ gl_vao_unbind(vao);
+}
+
+static void GLAPIENTRY gl_debug_cb(GLenum source, GLenum type, GLuint id,
+ GLenum severity, GLsizei length,
+ const GLchar *message, const void *userParam)
+{
+ // keep in mind that the debug callback can be asynchronous
+ struct mp_log *log = (void *)userParam;
+ int level = MSGL_ERR;
+ switch (severity) {
+ case GL_DEBUG_SEVERITY_NOTIFICATION:level = MSGL_V; break;
+ case GL_DEBUG_SEVERITY_LOW: level = MSGL_INFO; break;
+ case GL_DEBUG_SEVERITY_MEDIUM: level = MSGL_WARN; break;
+ case GL_DEBUG_SEVERITY_HIGH: level = MSGL_ERR; break;
+ }
+ mp_msg(log, level, "GL: %s\n", message);
+}
+
+void gl_set_debug_logger(GL *gl, struct mp_log *log)
+{
+ if (gl->DebugMessageCallback)
+ gl->DebugMessageCallback(log ? gl_debug_cb : NULL, log);
+}
+
+// Given a GL combined extension string in extensions, find out whether ext
+// is included in it. Basically, a word search.
+bool gl_check_extension(const char *extensions, const char *ext)
+{
+ int len = strlen(ext);
+ const char *cur = extensions;
+ while (cur) {
+ cur = strstr(cur, ext);
+ if (!cur)
+ break;
+ if ((cur == extensions || cur[-1] == ' ') &&
+ (cur[len] == '\0' || cur[len] == ' '))
+ return true;
+ cur += len;
+ }
+ return false;
+}
diff --git a/video/out/opengl/utils.h b/video/out/opengl/utils.h
new file mode 100644
index 0000000..9bcadae
--- /dev/null
+++ b/video/out/opengl/utils.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of mpv.
+ * Parts based on MPlayer code by Reimar Döffinger.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_GL_UTILS_
+#define MP_GL_UTILS_
+
+#include <math.h>
+
+#include "video/out/gpu/utils.h"
+#include "common.h"
+
+struct mp_log;
+
+void gl_check_error(GL *gl, struct mp_log *log, const char *info);
+
+void gl_upload_tex(GL *gl, GLenum target, GLenum format, GLenum type,
+ const void *dataptr, int stride,
+ int x, int y, int w, int h);
+
+bool gl_read_fbo_contents(GL *gl, int fbo, int dir, GLenum format, GLenum type,
+ int w, int h, uint8_t *dst, int dst_stride);
+
+struct gl_vao {
+ GL *gl;
+ GLuint vao; // the VAO object, or 0 if unsupported by driver
+ GLuint buffer; // GL_ARRAY_BUFFER used for the data
+ int stride; // size of each element (interleaved elements are assumed)
+ const struct ra_renderpass_input *entries;
+ int num_entries;
+};
+
+void gl_vao_init(struct gl_vao *vao, GL *gl, int stride,
+ const struct ra_renderpass_input *entries,
+ int num_entries);
+void gl_vao_uninit(struct gl_vao *vao);
+void gl_vao_draw_data(struct gl_vao *vao, GLenum prim, void *ptr, size_t num);
+
+void gl_set_debug_logger(GL *gl, struct mp_log *log);
+
+bool gl_check_extension(const char *extensions, const char *ext);
+
+#endif
diff --git a/video/out/placebo/ra_pl.c b/video/out/placebo/ra_pl.c
new file mode 100644
index 0000000..6259651
--- /dev/null
+++ b/video/out/placebo/ra_pl.c
@@ -0,0 +1,677 @@
+#include "common/common.h"
+#include "common/msg.h"
+
+#include "ra_pl.h"
+#include "utils.h"
+
+struct ra_pl {
+ pl_gpu gpu;
+ struct ra_timer_pl *active_timer;
+};
+
+static inline pl_gpu get_gpu(const struct ra *ra)
+{
+ struct ra_pl *p = ra->priv;
+ return p->gpu;
+}
+
+static struct ra_fns ra_fns_pl;
+
+pl_gpu ra_pl_get(const struct ra *ra)
+{
+ return ra->fns == &ra_fns_pl ? get_gpu(ra) : NULL;
+}
+
+static pl_timer get_active_timer(const struct ra *ra);
+
+struct ra *ra_create_pl(pl_gpu gpu, struct mp_log *log)
+{
+ assert(gpu);
+
+ struct ra *ra = talloc_zero(NULL, struct ra);
+ ra->log = log;
+ ra->fns = &ra_fns_pl;
+
+ struct ra_pl *p = ra->priv = talloc_zero(ra, struct ra_pl);
+ p->gpu = gpu;
+
+ ra->glsl_version = gpu->glsl.version;
+ ra->glsl_vulkan = gpu->glsl.vulkan;
+ ra->glsl_es = gpu->glsl.gles;
+
+ ra->caps = RA_CAP_DIRECT_UPLOAD | RA_CAP_NESTED_ARRAY | RA_CAP_FRAGCOORD;
+
+ if (gpu->glsl.compute)
+ ra->caps |= RA_CAP_COMPUTE | RA_CAP_NUM_GROUPS;
+ if (gpu->limits.compute_queues > gpu->limits.fragment_queues)
+ ra->caps |= RA_CAP_PARALLEL_COMPUTE;
+ if (gpu->limits.max_variable_comps)
+ ra->caps |= RA_CAP_GLOBAL_UNIFORM;
+ if (!gpu->limits.host_cached)
+ ra->caps |= RA_CAP_SLOW_DR;
+
+ if (gpu->limits.max_tex_1d_dim)
+ ra->caps |= RA_CAP_TEX_1D;
+ if (gpu->limits.max_tex_3d_dim)
+ ra->caps |= RA_CAP_TEX_3D;
+ if (gpu->limits.max_ubo_size)
+ ra->caps |= RA_CAP_BUF_RO;
+ if (gpu->limits.max_ssbo_size)
+ ra->caps |= RA_CAP_BUF_RW;
+ if (gpu->glsl.min_gather_offset && gpu->glsl.max_gather_offset)
+ ra->caps |= RA_CAP_GATHER;
+
+ // Semi-hack: assume all textures are blittable if r8 is
+ pl_fmt r8 = pl_find_named_fmt(gpu, "r8");
+ if (r8->caps & PL_FMT_CAP_BLITTABLE)
+ ra->caps |= RA_CAP_BLIT;
+
+ ra->max_texture_wh = gpu->limits.max_tex_2d_dim;
+ ra->max_pushc_size = gpu->limits.max_pushc_size;
+ ra->max_compute_group_threads = gpu->glsl.max_group_threads;
+ ra->max_shmem = gpu->glsl.max_shmem_size;
+
+ // Set up format wrappers
+ for (int i = 0; i < gpu->num_formats; i++) {
+ pl_fmt plfmt = gpu->formats[i];
+ static const enum ra_ctype fmt_type_map[PL_FMT_TYPE_COUNT] = {
+ [PL_FMT_UNORM] = RA_CTYPE_UNORM,
+ [PL_FMT_UINT] = RA_CTYPE_UINT,
+ [PL_FMT_FLOAT] = RA_CTYPE_FLOAT,
+ };
+
+ enum ra_ctype type = fmt_type_map[plfmt->type];
+ if (!type || !(plfmt->caps & PL_FMT_CAP_SAMPLEABLE))
+ continue;
+
+ struct ra_format *rafmt = talloc_zero(ra, struct ra_format);
+ *rafmt = (struct ra_format) {
+ .name = plfmt->name,
+ .priv = (void *) plfmt,
+ .ctype = type,
+ .ordered = pl_fmt_is_ordered(plfmt),
+ .num_components = plfmt->num_components,
+ .pixel_size = plfmt->texel_size,
+ .linear_filter = plfmt->caps & PL_FMT_CAP_LINEAR,
+ .renderable = plfmt->caps & PL_FMT_CAP_RENDERABLE,
+ .storable = plfmt->caps & PL_FMT_CAP_STORABLE,
+ .glsl_format = plfmt->glsl_format,
+ };
+
+ for (int c = 0; c < plfmt->num_components; c++) {
+ rafmt->component_size[c] = plfmt->host_bits[c];
+ rafmt->component_depth[c] = plfmt->component_depth[c];
+ }
+
+ MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, rafmt);
+ }
+
+ return ra;
+}
+
+static void destroy_ra_pl(struct ra *ra)
+{
+ talloc_free(ra);
+}
+
+static struct ra_format *map_fmt(struct ra *ra, pl_fmt plfmt)
+{
+ for (int i = 0; i < ra->num_formats; i++) {
+ if (ra->formats[i]->priv == plfmt)
+ return ra->formats[i];
+ }
+
+ MP_ERR(ra, "Failed mapping pl_fmt '%s' to ra_fmt?\n", plfmt->name);
+ return NULL;
+}
+
+bool mppl_wrap_tex(struct ra *ra, pl_tex pltex, struct ra_tex *out_tex)
+{
+ if (!pltex)
+ return false;
+
+ *out_tex = (struct ra_tex) {
+ .params = {
+ .dimensions = pl_tex_params_dimension(pltex->params),
+ .w = pltex->params.w,
+ .h = pltex->params.h,
+ .d = pltex->params.d,
+ .format = map_fmt(ra, pltex->params.format),
+ .render_src = pltex->params.sampleable,
+ .render_dst = pltex->params.renderable,
+ .storage_dst = pltex->params.storable,
+ .blit_src = pltex->params.blit_src,
+ .blit_dst = pltex->params.blit_dst,
+ .host_mutable = pltex->params.host_writable,
+ .downloadable = pltex->params.host_readable,
+ // These don't exist upstream, so just pick something reasonable
+ .src_linear = pltex->params.format->caps & PL_FMT_CAP_LINEAR,
+ .src_repeat = false,
+ },
+ .priv = (void *) pltex,
+ };
+
+ return !!out_tex->params.format;
+}
+
+static struct ra_tex *tex_create_pl(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ pl_gpu gpu = get_gpu(ra);
+ pl_tex pltex = pl_tex_create(gpu, &(struct pl_tex_params) {
+ .w = params->w,
+ .h = params->dimensions >= 2 ? params->h : 0,
+ .d = params->dimensions >= 3 ? params->d : 0,
+ .format = params->format->priv,
+ .sampleable = params->render_src,
+ .renderable = params->render_dst,
+ .storable = params->storage_dst,
+ .blit_src = params->blit_src,
+ .blit_dst = params->blit_dst || params->render_dst,
+ .host_writable = params->host_mutable,
+ .host_readable = params->downloadable,
+ .initial_data = params->initial_data,
+ });
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ if (!mppl_wrap_tex(ra, pltex, ratex)) {
+ pl_tex_destroy(gpu, &pltex);
+ talloc_free(ratex);
+ return NULL;
+ }
+
+ // Keep track of these, so we can correctly bind them later
+ ratex->params.src_repeat = params->src_repeat;
+ ratex->params.src_linear = params->src_linear;
+
+ return ratex;
+}
+
+static void tex_destroy_pl(struct ra *ra, struct ra_tex *tex)
+{
+ if (!tex)
+ return;
+
+ pl_tex_destroy(get_gpu(ra), (pl_tex *) &tex->priv);
+ talloc_free(tex);
+}
+
+static bool tex_upload_pl(struct ra *ra, const struct ra_tex_upload_params *params)
+{
+ pl_gpu gpu = get_gpu(ra);
+ pl_tex tex = params->tex->priv;
+ struct pl_tex_transfer_params pl_params = {
+ .tex = tex,
+ .buf = params->buf ? params->buf->priv : NULL,
+ .buf_offset = params->buf_offset,
+ .ptr = (void *) params->src,
+ .timer = get_active_timer(ra),
+ };
+
+ pl_buf staging = NULL;
+ if (params->tex->params.dimensions == 2) {
+ if (params->rc) {
+ pl_params.rc = (struct pl_rect3d) {
+ .x0 = params->rc->x0, .x1 = params->rc->x1,
+ .y0 = params->rc->y0, .y1 = params->rc->y1,
+ };
+ }
+
+ pl_params.row_pitch = params->stride;
+ }
+
+ bool ok = pl_tex_upload(gpu, &pl_params);
+ pl_buf_destroy(gpu, &staging);
+ return ok;
+}
+
+static bool tex_download_pl(struct ra *ra, struct ra_tex_download_params *params)
+{
+ pl_tex tex = params->tex->priv;
+ struct pl_tex_transfer_params pl_params = {
+ .tex = tex,
+ .ptr = params->dst,
+ .timer = get_active_timer(ra),
+ .row_pitch = params->stride,
+ };
+
+ return pl_tex_download(get_gpu(ra), &pl_params);
+}
+
+static struct ra_buf *buf_create_pl(struct ra *ra,
+ const struct ra_buf_params *params)
+{
+ pl_buf plbuf = pl_buf_create(get_gpu(ra), &(struct pl_buf_params) {
+ .size = params->size,
+ .uniform = params->type == RA_BUF_TYPE_UNIFORM,
+ .storable = params->type == RA_BUF_TYPE_SHADER_STORAGE,
+ .host_mapped = params->host_mapped,
+ .host_writable = params->host_mutable,
+ .initial_data = params->initial_data,
+ });
+
+ if (!plbuf)
+ return NULL;
+
+ struct ra_buf *rabuf = talloc_ptrtype(NULL, rabuf);
+ *rabuf = (struct ra_buf) {
+ .params = *params,
+ .data = plbuf->data,
+ .priv = (void *) plbuf,
+ };
+
+ rabuf->params.initial_data = NULL;
+ return rabuf;
+}
+
+static void buf_destroy_pl(struct ra *ra, struct ra_buf *buf)
+{
+ if (!buf)
+ return;
+
+ pl_buf_destroy(get_gpu(ra), (pl_buf *) &buf->priv);
+ talloc_free(buf);
+}
+
+static void buf_update_pl(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size)
+{
+ pl_buf_write(get_gpu(ra), buf->priv, offset, data, size);
+}
+
+static bool buf_poll_pl(struct ra *ra, struct ra_buf *buf)
+{
+ return !pl_buf_poll(get_gpu(ra), buf->priv, 0);
+}
+
+static void clear_pl(struct ra *ra, struct ra_tex *dst, float color[4],
+ struct mp_rect *scissor)
+{
+ // TODO: implement scissor clearing by bltting a 1x1 tex instead
+ pl_tex_clear(get_gpu(ra), dst->priv, color);
+}
+
+static void blit_pl(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc)
+{
+ struct pl_rect3d plsrc = {0}, pldst = {0};
+ if (src_rc) {
+ plsrc.x0 = MPMIN(MPMAX(src_rc->x0, 0), src->params.w);
+ plsrc.y0 = MPMIN(MPMAX(src_rc->y0, 0), src->params.h);
+ plsrc.x1 = MPMIN(MPMAX(src_rc->x1, 0), src->params.w);
+ plsrc.y1 = MPMIN(MPMAX(src_rc->y1, 0), src->params.h);
+ }
+
+ if (dst_rc) {
+ pldst.x0 = MPMIN(MPMAX(dst_rc->x0, 0), dst->params.w);
+ pldst.y0 = MPMIN(MPMAX(dst_rc->y0, 0), dst->params.h);
+ pldst.x1 = MPMIN(MPMAX(dst_rc->x1, 0), dst->params.w);
+ pldst.y1 = MPMIN(MPMAX(dst_rc->y1, 0), dst->params.h);
+ }
+
+ pl_tex_blit(get_gpu(ra), &(struct pl_tex_blit_params) {
+ .src = src->priv,
+ .dst = dst->priv,
+ .src_rc = plsrc,
+ .dst_rc = pldst,
+ .sample_mode = src->params.src_linear ? PL_TEX_SAMPLE_LINEAR
+ : PL_TEX_SAMPLE_NEAREST,
+ });
+}
+
+static const enum pl_var_type var_type[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_INT] = PL_VAR_SINT,
+ [RA_VARTYPE_FLOAT] = PL_VAR_FLOAT,
+};
+
+static const enum pl_desc_type desc_type[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_TEX] = PL_DESC_SAMPLED_TEX,
+ [RA_VARTYPE_IMG_W] = PL_DESC_STORAGE_IMG,
+ [RA_VARTYPE_BUF_RO] = PL_DESC_BUF_UNIFORM,
+ [RA_VARTYPE_BUF_RW] = PL_DESC_BUF_STORAGE,
+};
+
+static const enum pl_fmt_type fmt_type[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_INT] = PL_FMT_SINT,
+ [RA_VARTYPE_FLOAT] = PL_FMT_FLOAT,
+ [RA_VARTYPE_BYTE_UNORM] = PL_FMT_UNORM,
+};
+
+static const size_t var_size[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_INT] = sizeof(int),
+ [RA_VARTYPE_FLOAT] = sizeof(float),
+ [RA_VARTYPE_BYTE_UNORM] = sizeof(uint8_t),
+};
+
+static struct ra_layout uniform_layout_pl(struct ra_renderpass_input *inp)
+{
+ // To get the alignment requirements, we try laying this out with
+ // an offset of 1 and then see where it ends up. This will always be
+ // the minimum alignment requirement.
+ struct pl_var_layout layout = pl_buf_uniform_layout(1, &(struct pl_var) {
+ .name = inp->name,
+ .type = var_type[inp->type],
+ .dim_v = inp->dim_v,
+ .dim_m = inp->dim_m,
+ .dim_a = 1,
+ });
+
+ return (struct ra_layout) {
+ .align = layout.offset,
+ .stride = layout.stride,
+ .size = layout.size,
+ };
+}
+
+static struct ra_layout push_constant_layout_pl(struct ra_renderpass_input *inp)
+{
+ struct pl_var_layout layout = pl_push_constant_layout(1, &(struct pl_var) {
+ .name = inp->name,
+ .type = var_type[inp->type],
+ .dim_v = inp->dim_v,
+ .dim_m = inp->dim_m,
+ .dim_a = 1,
+ });
+
+ return (struct ra_layout) {
+ .align = layout.offset,
+ .stride = layout.stride,
+ .size = layout.size,
+ };
+}
+
+static int desc_namespace_pl(struct ra *ra, enum ra_vartype type)
+{
+ return pl_desc_namespace(get_gpu(ra), desc_type[type]);
+}
+
+struct pass_priv {
+ pl_pass pass;
+ uint16_t *inp_index; // index translation map
+ // Space to hold the descriptor bindings and variable updates
+ struct pl_desc_binding *binds;
+ struct pl_var_update *varups;
+ int num_varups;
+};
+
+static struct ra_renderpass *renderpass_create_pl(struct ra *ra,
+ const struct ra_renderpass_params *params)
+{
+ void *tmp = talloc_new(NULL);
+ pl_gpu gpu = get_gpu(ra);
+ struct ra_renderpass *pass = NULL;
+
+ static const enum pl_pass_type pass_type[] = {
+ [RA_RENDERPASS_TYPE_RASTER] = PL_PASS_RASTER,
+ [RA_RENDERPASS_TYPE_COMPUTE] = PL_PASS_COMPUTE,
+ };
+
+ struct pl_var *vars = NULL;
+ struct pl_desc *descs = NULL;
+ int num_vars = 0, num_descs = 0;
+
+ struct pass_priv *priv = talloc_ptrtype(tmp, priv);
+ priv->inp_index = talloc_zero_array(priv, uint16_t, params->num_inputs);
+
+ for (int i = 0; i < params->num_inputs; i++) {
+ const struct ra_renderpass_input *inp = &params->inputs[i];
+ if (var_type[inp->type]) {
+ priv->inp_index[i] = num_vars;
+ MP_TARRAY_APPEND(tmp, vars, num_vars, (struct pl_var) {
+ .name = inp->name,
+ .type = var_type[inp->type],
+ .dim_v = inp->dim_v,
+ .dim_m = inp->dim_m,
+ .dim_a = 1,
+ });
+ } else if (desc_type[inp->type]) {
+ priv->inp_index[i] = num_descs;
+ MP_TARRAY_APPEND(tmp, descs, num_descs, (struct pl_desc) {
+ .name = inp->name,
+ .type = desc_type[inp->type],
+ .binding = inp->binding,
+ .access = inp->type == RA_VARTYPE_IMG_W ? PL_DESC_ACCESS_WRITEONLY
+ : inp->type == RA_VARTYPE_BUF_RW ? PL_DESC_ACCESS_READWRITE
+ : PL_DESC_ACCESS_READONLY,
+ });
+ }
+ }
+
+ // Allocate space to store the bindings map persistently
+ priv->binds = talloc_zero_array(priv, struct pl_desc_binding, num_descs);
+
+ struct pl_pass_params pl_params = {
+ .type = pass_type[params->type],
+ .variables = vars,
+ .num_variables = num_vars,
+ .descriptors = descs,
+ .num_descriptors = num_descs,
+ .push_constants_size = params->push_constants_size,
+ .glsl_shader = params->type == RA_RENDERPASS_TYPE_COMPUTE
+ ? params->compute_shader
+ : params->frag_shader,
+ };
+
+ struct pl_blend_params blend_params;
+
+ if (params->type == RA_RENDERPASS_TYPE_RASTER) {
+ pl_params.vertex_shader = params->vertex_shader;
+ pl_params.vertex_type = PL_PRIM_TRIANGLE_LIST;
+ pl_params.vertex_stride = params->vertex_stride;
+ pl_params.load_target = !params->invalidate_target;
+ pl_params.target_format = params->target_format->priv;
+
+ if (params->enable_blend) {
+ pl_params.blend_params = &blend_params;
+ blend_params = (struct pl_blend_params) {
+ // Same enum order as ra_blend
+ .src_rgb = (enum pl_blend_mode) params->blend_src_rgb,
+ .dst_rgb = (enum pl_blend_mode) params->blend_dst_rgb,
+ .src_alpha = (enum pl_blend_mode) params->blend_src_alpha,
+ .dst_alpha = (enum pl_blend_mode) params->blend_dst_alpha,
+ };
+ }
+
+ for (int i = 0; i < params->num_vertex_attribs; i++) {
+ const struct ra_renderpass_input *inp = &params->vertex_attribs[i];
+ struct pl_vertex_attrib attrib = {
+ .name = inp->name,
+ .offset = inp->offset,
+ .location = i,
+ .fmt = pl_find_fmt(gpu, fmt_type[inp->type], inp->dim_v, 0,
+ var_size[inp->type] * 8, PL_FMT_CAP_VERTEX),
+ };
+
+ if (!attrib.fmt) {
+ MP_ERR(ra, "Failed mapping vertex attrib '%s' to pl_fmt?\n",
+ inp->name);
+ goto error;
+ }
+
+ MP_TARRAY_APPEND(tmp, pl_params.vertex_attribs,
+ pl_params.num_vertex_attribs, attrib);
+ }
+ }
+
+ priv->pass = pl_pass_create(gpu, &pl_params);
+ if (!priv->pass)
+ goto error;
+
+ pass = talloc_ptrtype(NULL, pass);
+ *pass = (struct ra_renderpass) {
+ .params = *ra_renderpass_params_copy(pass, params),
+ .priv = talloc_steal(pass, priv),
+ };
+
+ // fall through
+error:
+ talloc_free(tmp);
+ return pass;
+}
+
+static void renderpass_destroy_pl(struct ra *ra, struct ra_renderpass *pass)
+{
+ if (!pass)
+ return;
+
+ struct pass_priv *priv = pass->priv;
+ pl_pass_destroy(get_gpu(ra), (pl_pass *) &priv->pass);
+ talloc_free(pass);
+}
+
+static void renderpass_run_pl(struct ra *ra,
+ const struct ra_renderpass_run_params *params)
+{
+ struct pass_priv *p = params->pass->priv;
+ p->num_varups = 0;
+
+ for (int i = 0; i < params->num_values; i++) {
+ const struct ra_renderpass_input_val *val = &params->values[i];
+ const struct ra_renderpass_input *inp = &params->pass->params.inputs[i];
+ if (var_type[inp->type]) {
+ MP_TARRAY_APPEND(p, p->varups, p->num_varups, (struct pl_var_update) {
+ .index = p->inp_index[val->index],
+ .data = val->data,
+ });
+ } else {
+ struct pl_desc_binding bind;
+ switch (inp->type) {
+ case RA_VARTYPE_TEX:
+ case RA_VARTYPE_IMG_W: {
+ struct ra_tex *tex = *((struct ra_tex **) val->data);
+ bind.object = tex->priv;
+ bind.sample_mode = tex->params.src_linear ? PL_TEX_SAMPLE_LINEAR
+ : PL_TEX_SAMPLE_NEAREST;
+ bind.address_mode = tex->params.src_repeat ? PL_TEX_ADDRESS_REPEAT
+ : PL_TEX_ADDRESS_CLAMP;
+ break;
+ }
+ case RA_VARTYPE_BUF_RO:
+ case RA_VARTYPE_BUF_RW:
+ bind.object = (* (struct ra_buf **) val->data)->priv;
+ break;
+ default: MP_ASSERT_UNREACHABLE();
+ };
+
+ p->binds[p->inp_index[val->index]] = bind;
+ };
+ }
+
+ struct pl_pass_run_params pl_params = {
+ .pass = p->pass,
+ .var_updates = p->varups,
+ .num_var_updates = p->num_varups,
+ .desc_bindings = p->binds,
+ .push_constants = params->push_constants,
+ .timer = get_active_timer(ra),
+ };
+
+ if (p->pass->params.type == PL_PASS_RASTER) {
+ pl_params.target = params->target->priv;
+ pl_params.viewport = mp_rect2d_to_pl(params->viewport);
+ pl_params.scissors = mp_rect2d_to_pl(params->scissors);
+ pl_params.vertex_data = params->vertex_data;
+ pl_params.vertex_count = params->vertex_count;
+ } else {
+ for (int i = 0; i < MP_ARRAY_SIZE(pl_params.compute_groups); i++)
+ pl_params.compute_groups[i] = params->compute_groups[i];
+ }
+
+ pl_pass_run(get_gpu(ra), &pl_params);
+}
+
+struct ra_timer_pl {
+ // Because libpplacebo only supports one operation per timer, we need
+ // to use multiple pl_timers to sum up multiple passes/transfers
+ pl_timer *timers;
+ int num_timers;
+ int idx_timers;
+};
+
+static ra_timer *timer_create_pl(struct ra *ra)
+{
+ struct ra_timer_pl *t = talloc_zero(ra, struct ra_timer_pl);
+ return t;
+}
+
+static void timer_destroy_pl(struct ra *ra, ra_timer *timer)
+{
+ pl_gpu gpu = get_gpu(ra);
+ struct ra_timer_pl *t = timer;
+
+ for (int i = 0; i < t->num_timers; i++)
+ pl_timer_destroy(gpu, &t->timers[i]);
+
+ talloc_free(t);
+}
+
+static void timer_start_pl(struct ra *ra, ra_timer *timer)
+{
+ struct ra_pl *p = ra->priv;
+ struct ra_timer_pl *t = timer;
+
+ // There's nothing easy we can do in this case, since libplacebo only
+ // supports one timer object per operation; so just ignore "inner" timers
+ // when the user is nesting different timer queries
+ if (p->active_timer)
+ return;
+
+ p->active_timer = t;
+ t->idx_timers = 0;
+}
+
+static uint64_t timer_stop_pl(struct ra *ra, ra_timer *timer)
+{
+ struct ra_pl *p = ra->priv;
+ struct ra_timer_pl *t = timer;
+
+ if (p->active_timer != t)
+ return 0;
+
+ p->active_timer = NULL;
+
+ // Sum up all of the active results
+ uint64_t res = 0;
+ for (int i = 0; i < t->idx_timers; i++)
+ res += pl_timer_query(p->gpu, t->timers[i]);
+
+ return res;
+}
+
+static pl_timer get_active_timer(const struct ra *ra)
+{
+ struct ra_pl *p = ra->priv;
+ if (!p->active_timer)
+ return NULL;
+
+ struct ra_timer_pl *t = p->active_timer;
+ if (t->idx_timers == t->num_timers)
+ MP_TARRAY_APPEND(t, t->timers, t->num_timers, pl_timer_create(p->gpu));
+
+ return t->timers[t->idx_timers++];
+}
+
+static struct ra_fns ra_fns_pl = {
+ .destroy = destroy_ra_pl,
+ .tex_create = tex_create_pl,
+ .tex_destroy = tex_destroy_pl,
+ .tex_upload = tex_upload_pl,
+ .tex_download = tex_download_pl,
+ .buf_create = buf_create_pl,
+ .buf_destroy = buf_destroy_pl,
+ .buf_update = buf_update_pl,
+ .buf_poll = buf_poll_pl,
+ .clear = clear_pl,
+ .blit = blit_pl,
+ .uniform_layout = uniform_layout_pl,
+ .push_constant_layout = push_constant_layout_pl,
+ .desc_namespace = desc_namespace_pl,
+ .renderpass_create = renderpass_create_pl,
+ .renderpass_destroy = renderpass_destroy_pl,
+ .renderpass_run = renderpass_run_pl,
+ .timer_create = timer_create_pl,
+ .timer_destroy = timer_destroy_pl,
+ .timer_start = timer_start_pl,
+ .timer_stop = timer_stop_pl,
+};
+
diff --git a/video/out/placebo/ra_pl.h b/video/out/placebo/ra_pl.h
new file mode 100644
index 0000000..1290c9c
--- /dev/null
+++ b/video/out/placebo/ra_pl.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "video/out/gpu/ra.h"
+#include <libplacebo/gpu.h>
+
+struct ra *ra_create_pl(pl_gpu gpu, struct mp_log *log);
+
+pl_gpu ra_pl_get(const struct ra *ra);
+
+static inline pl_fmt ra_pl_fmt_get(const struct ra_format *format)
+{
+ return format->priv;
+}
+
+// Wrap a pl_tex into a ra_tex struct, returns if successful
+bool mppl_wrap_tex(struct ra *ra, pl_tex pltex, struct ra_tex *out_tex);
diff --git a/video/out/placebo/utils.c b/video/out/placebo/utils.c
new file mode 100644
index 0000000..1209b72
--- /dev/null
+++ b/video/out/placebo/utils.c
@@ -0,0 +1,263 @@
+#include "common/common.h"
+#include "utils.h"
+
+#include <libplacebo/utils/dolbyvision.h>
+
+static const int pl_log_to_msg_lev[PL_LOG_ALL+1] = {
+ [PL_LOG_FATAL] = MSGL_FATAL,
+ [PL_LOG_ERR] = MSGL_ERR,
+ [PL_LOG_WARN] = MSGL_WARN,
+ [PL_LOG_INFO] = MSGL_V,
+ [PL_LOG_DEBUG] = MSGL_DEBUG,
+ [PL_LOG_TRACE] = MSGL_TRACE,
+};
+
+static const enum pl_log_level msg_lev_to_pl_log[MSGL_MAX+1] = {
+ [MSGL_FATAL] = PL_LOG_FATAL,
+ [MSGL_ERR] = PL_LOG_ERR,
+ [MSGL_WARN] = PL_LOG_WARN,
+ [MSGL_INFO] = PL_LOG_WARN,
+ [MSGL_STATUS] = PL_LOG_WARN,
+ [MSGL_V] = PL_LOG_INFO,
+ [MSGL_DEBUG] = PL_LOG_DEBUG,
+ [MSGL_TRACE] = PL_LOG_TRACE,
+ [MSGL_MAX] = PL_LOG_ALL,
+};
+
+// translates log levels while probing
+static const enum pl_log_level probing_map(enum pl_log_level level)
+{
+ switch (level) {
+ case PL_LOG_FATAL:
+ case PL_LOG_ERR:
+ case PL_LOG_WARN:
+ return PL_LOG_INFO;
+
+ default:
+ return level;
+ }
+}
+
+static void log_cb(void *priv, enum pl_log_level level, const char *msg)
+{
+ struct mp_log *log = priv;
+ mp_msg(log, pl_log_to_msg_lev[level], "%s\n", msg);
+}
+
+static void log_cb_probing(void *priv, enum pl_log_level level, const char *msg)
+{
+ struct mp_log *log = priv;
+ mp_msg(log, pl_log_to_msg_lev[probing_map(level)], "%s\n", msg);
+}
+
+pl_log mppl_log_create(void *tactx, struct mp_log *log)
+{
+ return pl_log_create(PL_API_VER, &(struct pl_log_params) {
+ .log_cb = log_cb,
+ .log_level = msg_lev_to_pl_log[mp_msg_level(log)],
+ .log_priv = mp_log_new(tactx, log, "libplacebo"),
+ });
+}
+
+void mppl_log_set_probing(pl_log log, bool probing)
+{
+ struct pl_log_params params = log->params;
+ params.log_cb = probing ? log_cb_probing : log_cb;
+ pl_log_update(log, &params);
+}
+
+enum pl_color_primaries mp_prim_to_pl(enum mp_csp_prim prim)
+{
+ switch (prim) {
+ case MP_CSP_PRIM_AUTO: return PL_COLOR_PRIM_UNKNOWN;
+ case MP_CSP_PRIM_BT_601_525: return PL_COLOR_PRIM_BT_601_525;
+ case MP_CSP_PRIM_BT_601_625: return PL_COLOR_PRIM_BT_601_625;
+ case MP_CSP_PRIM_BT_709: return PL_COLOR_PRIM_BT_709;
+ case MP_CSP_PRIM_BT_2020: return PL_COLOR_PRIM_BT_2020;
+ case MP_CSP_PRIM_BT_470M: return PL_COLOR_PRIM_BT_470M;
+ case MP_CSP_PRIM_APPLE: return PL_COLOR_PRIM_APPLE;
+ case MP_CSP_PRIM_ADOBE: return PL_COLOR_PRIM_ADOBE;
+ case MP_CSP_PRIM_PRO_PHOTO: return PL_COLOR_PRIM_PRO_PHOTO;
+ case MP_CSP_PRIM_CIE_1931: return PL_COLOR_PRIM_CIE_1931;
+ case MP_CSP_PRIM_DCI_P3: return PL_COLOR_PRIM_DCI_P3;
+ case MP_CSP_PRIM_DISPLAY_P3: return PL_COLOR_PRIM_DISPLAY_P3;
+ case MP_CSP_PRIM_V_GAMUT: return PL_COLOR_PRIM_V_GAMUT;
+ case MP_CSP_PRIM_S_GAMUT: return PL_COLOR_PRIM_S_GAMUT;
+ case MP_CSP_PRIM_EBU_3213: return PL_COLOR_PRIM_EBU_3213;
+ case MP_CSP_PRIM_FILM_C: return PL_COLOR_PRIM_FILM_C;
+ case MP_CSP_PRIM_ACES_AP0: return PL_COLOR_PRIM_ACES_AP0;
+ case MP_CSP_PRIM_ACES_AP1: return PL_COLOR_PRIM_ACES_AP1;
+ case MP_CSP_PRIM_COUNT: return PL_COLOR_PRIM_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum mp_csp_prim mp_prim_from_pl(enum pl_color_primaries prim)
+{
+ switch (prim){
+ case PL_COLOR_PRIM_UNKNOWN: return MP_CSP_PRIM_AUTO;
+ case PL_COLOR_PRIM_BT_601_525: return MP_CSP_PRIM_BT_601_525;
+ case PL_COLOR_PRIM_BT_601_625: return MP_CSP_PRIM_BT_601_625;
+ case PL_COLOR_PRIM_BT_709: return MP_CSP_PRIM_BT_709;
+ case PL_COLOR_PRIM_BT_2020: return MP_CSP_PRIM_BT_2020;
+ case PL_COLOR_PRIM_BT_470M: return MP_CSP_PRIM_BT_470M;
+ case PL_COLOR_PRIM_APPLE: return MP_CSP_PRIM_APPLE;
+ case PL_COLOR_PRIM_ADOBE: return MP_CSP_PRIM_ADOBE;
+ case PL_COLOR_PRIM_PRO_PHOTO: return MP_CSP_PRIM_PRO_PHOTO;
+ case PL_COLOR_PRIM_CIE_1931: return MP_CSP_PRIM_CIE_1931;
+ case PL_COLOR_PRIM_DCI_P3: return MP_CSP_PRIM_DCI_P3;
+ case PL_COLOR_PRIM_DISPLAY_P3: return MP_CSP_PRIM_DISPLAY_P3;
+ case PL_COLOR_PRIM_V_GAMUT: return MP_CSP_PRIM_V_GAMUT;
+ case PL_COLOR_PRIM_S_GAMUT: return MP_CSP_PRIM_S_GAMUT;
+ case PL_COLOR_PRIM_EBU_3213: return MP_CSP_PRIM_EBU_3213;
+ case PL_COLOR_PRIM_FILM_C: return MP_CSP_PRIM_FILM_C;
+ case PL_COLOR_PRIM_ACES_AP0: return MP_CSP_PRIM_ACES_AP0;
+ case PL_COLOR_PRIM_ACES_AP1: return MP_CSP_PRIM_ACES_AP1;
+ case PL_COLOR_PRIM_COUNT: return MP_CSP_PRIM_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_color_transfer mp_trc_to_pl(enum mp_csp_trc trc)
+{
+ switch (trc) {
+ case MP_CSP_TRC_AUTO: return PL_COLOR_TRC_UNKNOWN;
+ case MP_CSP_TRC_BT_1886: return PL_COLOR_TRC_BT_1886;
+ case MP_CSP_TRC_SRGB: return PL_COLOR_TRC_SRGB;
+ case MP_CSP_TRC_LINEAR: return PL_COLOR_TRC_LINEAR;
+ case MP_CSP_TRC_GAMMA18: return PL_COLOR_TRC_GAMMA18;
+ case MP_CSP_TRC_GAMMA20: return PL_COLOR_TRC_GAMMA20;
+ case MP_CSP_TRC_GAMMA22: return PL_COLOR_TRC_GAMMA22;
+ case MP_CSP_TRC_GAMMA24: return PL_COLOR_TRC_GAMMA24;
+ case MP_CSP_TRC_GAMMA26: return PL_COLOR_TRC_GAMMA26;
+ case MP_CSP_TRC_GAMMA28: return PL_COLOR_TRC_GAMMA28;
+ case MP_CSP_TRC_PRO_PHOTO: return PL_COLOR_TRC_PRO_PHOTO;
+ case MP_CSP_TRC_PQ: return PL_COLOR_TRC_PQ;
+ case MP_CSP_TRC_HLG: return PL_COLOR_TRC_HLG;
+ case MP_CSP_TRC_V_LOG: return PL_COLOR_TRC_V_LOG;
+ case MP_CSP_TRC_S_LOG1: return PL_COLOR_TRC_S_LOG1;
+ case MP_CSP_TRC_S_LOG2: return PL_COLOR_TRC_S_LOG2;
+ case MP_CSP_TRC_ST428: return PL_COLOR_TRC_ST428;
+ case MP_CSP_TRC_COUNT: return PL_COLOR_TRC_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum mp_csp_trc mp_trc_from_pl(enum pl_color_transfer trc)
+{
+ switch (trc){
+ case PL_COLOR_TRC_UNKNOWN: return MP_CSP_TRC_AUTO;
+ case PL_COLOR_TRC_BT_1886: return MP_CSP_TRC_BT_1886;
+ case PL_COLOR_TRC_SRGB: return MP_CSP_TRC_SRGB;
+ case PL_COLOR_TRC_LINEAR: return MP_CSP_TRC_LINEAR;
+ case PL_COLOR_TRC_GAMMA18: return MP_CSP_TRC_GAMMA18;
+ case PL_COLOR_TRC_GAMMA20: return MP_CSP_TRC_GAMMA20;
+ case PL_COLOR_TRC_GAMMA22: return MP_CSP_TRC_GAMMA22;
+ case PL_COLOR_TRC_GAMMA24: return MP_CSP_TRC_GAMMA24;
+ case PL_COLOR_TRC_GAMMA26: return MP_CSP_TRC_GAMMA26;
+ case PL_COLOR_TRC_GAMMA28: return MP_CSP_TRC_GAMMA28;
+ case PL_COLOR_TRC_PRO_PHOTO: return MP_CSP_TRC_PRO_PHOTO;
+ case PL_COLOR_TRC_PQ: return MP_CSP_TRC_PQ;
+ case PL_COLOR_TRC_HLG: return MP_CSP_TRC_HLG;
+ case PL_COLOR_TRC_V_LOG: return MP_CSP_TRC_V_LOG;
+ case PL_COLOR_TRC_S_LOG1: return MP_CSP_TRC_S_LOG1;
+ case PL_COLOR_TRC_S_LOG2: return MP_CSP_TRC_S_LOG2;
+ case PL_COLOR_TRC_ST428: return MP_CSP_TRC_ST428;
+ case PL_COLOR_TRC_COUNT: return MP_CSP_TRC_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_color_system mp_csp_to_pl(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_AUTO: return PL_COLOR_SYSTEM_UNKNOWN;
+ case MP_CSP_BT_601: return PL_COLOR_SYSTEM_BT_601;
+ case MP_CSP_BT_709: return PL_COLOR_SYSTEM_BT_709;
+ case MP_CSP_SMPTE_240M: return PL_COLOR_SYSTEM_SMPTE_240M;
+ case MP_CSP_BT_2020_NC: return PL_COLOR_SYSTEM_BT_2020_NC;
+ case MP_CSP_BT_2020_C: return PL_COLOR_SYSTEM_BT_2020_C;
+ case MP_CSP_RGB: return PL_COLOR_SYSTEM_RGB;
+ case MP_CSP_XYZ: return PL_COLOR_SYSTEM_XYZ;
+ case MP_CSP_YCGCO: return PL_COLOR_SYSTEM_YCGCO;
+ case MP_CSP_COUNT: return PL_COLOR_SYSTEM_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_color_levels mp_levels_to_pl(enum mp_csp_levels levels)
+{
+ switch (levels) {
+ case MP_CSP_LEVELS_AUTO: return PL_COLOR_LEVELS_UNKNOWN;
+ case MP_CSP_LEVELS_TV: return PL_COLOR_LEVELS_TV;
+ case MP_CSP_LEVELS_PC: return PL_COLOR_LEVELS_PC;
+ case MP_CSP_LEVELS_COUNT: return PL_COLOR_LEVELS_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum mp_csp_levels mp_levels_from_pl(enum pl_color_levels levels)
+{
+ switch (levels){
+ case PL_COLOR_LEVELS_UNKNOWN: return MP_CSP_LEVELS_AUTO;
+ case PL_COLOR_LEVELS_TV: return MP_CSP_LEVELS_TV;
+ case PL_COLOR_LEVELS_PC: return MP_CSP_LEVELS_PC;
+ case PL_COLOR_LEVELS_COUNT: return MP_CSP_LEVELS_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha)
+{
+ switch (alpha) {
+ case MP_ALPHA_AUTO: return PL_ALPHA_UNKNOWN;
+ case MP_ALPHA_STRAIGHT: return PL_ALPHA_INDEPENDENT;
+ case MP_ALPHA_PREMUL: return PL_ALPHA_PREMULTIPLIED;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma)
+{
+ switch (chroma) {
+ case MP_CHROMA_AUTO: return PL_CHROMA_UNKNOWN;
+ case MP_CHROMA_TOPLEFT: return PL_CHROMA_TOP_LEFT;
+ case MP_CHROMA_LEFT: return PL_CHROMA_LEFT;
+ case MP_CHROMA_CENTER: return PL_CHROMA_CENTER;
+ case MP_CHROMA_COUNT: return PL_CHROMA_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
+ struct pl_frame *frame)
+{
+#ifdef PL_HAVE_LAV_DOLBY_VISION
+ if (mpi->dovi) {
+ const AVDOVIMetadata *metadata = (AVDOVIMetadata *) mpi->dovi->data;
+ const AVDOVIRpuDataHeader *header = av_dovi_get_header(metadata);
+
+ if (header->disable_residual_flag) {
+ // Only automatically map DoVi RPUs that don't require an EL
+ struct pl_dovi_metadata *dovi = talloc_ptrtype(mpi, dovi);
+ pl_frame_map_avdovi_metadata(frame, dovi, metadata);
+ }
+ }
+
+#if defined(PL_HAVE_LIBDOVI)
+ if (mpi->dovi_buf)
+ pl_hdr_metadata_from_dovi_rpu(&frame->color.hdr, mpi->dovi_buf->data,
+ mpi->dovi_buf->size);
+#endif
+
+#endif // PL_HAVE_LAV_DOLBY_VISION
+}
diff --git a/video/out/placebo/utils.h b/video/out/placebo/utils.h
new file mode 100644
index 0000000..bf780a8
--- /dev/null
+++ b/video/out/placebo/utils.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "config.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+
+#include <libavutil/buffer.h>
+
+#include <libplacebo/common.h>
+#include <libplacebo/log.h>
+#include <libplacebo/colorspace.h>
+#include <libplacebo/renderer.h>
+#include <libplacebo/utils/libav.h>
+
+pl_log mppl_log_create(void *tactx, struct mp_log *log);
+void mppl_log_set_probing(pl_log log, bool probing);
+
+static inline struct pl_rect2d mp_rect2d_to_pl(struct mp_rect rc)
+{
+ return (struct pl_rect2d) {
+ .x0 = rc.x0,
+ .y0 = rc.y0,
+ .x1 = rc.x1,
+ .y1 = rc.y1,
+ };
+}
+
+enum pl_color_primaries mp_prim_to_pl(enum mp_csp_prim prim);
+enum mp_csp_prim mp_prim_from_pl(enum pl_color_primaries prim);
+enum pl_color_transfer mp_trc_to_pl(enum mp_csp_trc trc);
+enum mp_csp_trc mp_trc_from_pl(enum pl_color_transfer trc);
+enum pl_color_system mp_csp_to_pl(enum mp_csp csp);
+enum pl_color_levels mp_levels_to_pl(enum mp_csp_levels levels);
+enum mp_csp_levels mp_levels_from_pl(enum pl_color_levels levels);
+enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha);
+enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma);
+
+void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
+ struct pl_frame *frame);
diff --git a/video/out/present_sync.c b/video/out/present_sync.c
new file mode 100644
index 0000000..a3b1089
--- /dev/null
+++ b/video/out/present_sync.c
@@ -0,0 +1,126 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <time.h>
+
+#include "misc/linked_list.h"
+#include "mpv_talloc.h"
+#include "osdep/timer.h"
+#include "present_sync.h"
+
+/* General nonsense about this mechanism.
+ *
+ * This requires that that caller has access to two, related values:
+ * (ust, msc): clock time and incrementing counter of last vsync (this is
+ * increased continuously, even if we don't swap)
+ *
+ * Note that this concept originates from the GLX_OML_sync_control extension
+ * which includes another parameter: sbc (swap counter of frame that was
+ * last displayed). Both the xorg present extension and wayland's
+ * presentation-time protocol do not include sbc values so they are omitted
+ * from this mechanism. mpv does not need to keep track of sbc calls and can
+ * have reliable presentation without it.
+ */
+
+void present_sync_get_info(struct mp_present *present, struct vo_vsync_info *info)
+{
+ struct mp_present_entry *cur = present->head;
+ while (cur) {
+ if (cur->queue_display_time)
+ break;
+ cur = cur->list_node.next;
+ }
+ if (!cur)
+ return;
+
+ info->vsync_duration = cur->vsync_duration;
+ info->skipped_vsyncs = cur->skipped_vsyncs;
+ info->last_queue_display_time = cur->queue_display_time;
+
+ // Remove from the list, zero out everything, and append at the end
+ LL_REMOVE(list_node, present, cur);
+ *cur = (struct mp_present_entry){0};
+ LL_APPEND(list_node, present, cur);
+}
+
+struct mp_present *mp_present_initialize(void *talloc_ctx, struct mp_vo_opts *opts, int entries)
+{
+ struct mp_present *present = talloc_zero(talloc_ctx, struct mp_present);
+ for (int i = 0; i < entries; i++) {
+ struct mp_present_entry *entry = talloc_zero(present, struct mp_present_entry);
+ LL_APPEND(list_node, present, entry);
+ }
+ present->opts = opts;
+ return present;
+}
+
+void present_sync_swap(struct mp_present *present)
+{
+ struct mp_present_entry *cur = present->head;
+ while (cur) {
+ if (!cur->queue_display_time)
+ break;
+ cur = cur->list_node.next;
+ }
+ if (!cur)
+ return;
+
+ int64_t ust = cur->ust;
+ int64_t msc = cur->msc;
+ int64_t last_ust = cur->list_node.prev ? cur->list_node.prev->ust : 0;
+ int64_t last_msc = cur->list_node.prev ? cur->list_node.prev->msc : 0;
+
+ // Avoid attempting to use any presentation statistics if the ust is 0 or has
+ // not actually updated (i.e. the last_ust is equal to ust).
+ if (!ust || ust == last_ust) {
+ cur->skipped_vsyncs = -1;
+ cur->vsync_duration = -1;
+ cur->queue_display_time = -1;
+ return;
+ }
+
+ cur->skipped_vsyncs = 0;
+ int64_t ust_passed = ust ? ust - last_ust: 0;
+ int64_t msc_passed = msc ? msc - last_msc: 0;
+ if (msc_passed && ust_passed)
+ cur->vsync_duration = ust_passed / msc_passed;
+
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts))
+ return;
+
+ int64_t now_monotonic = MP_TIME_S_TO_NS(ts.tv_sec) + ts.tv_nsec;
+ int64_t ust_mp_time = mp_time_ns() - (now_monotonic - ust);
+ cur->queue_display_time = ust_mp_time;
+}
+
+void present_sync_update_values(struct mp_present *present, int64_t ust,
+ int64_t msc)
+{
+ struct mp_present_entry *cur = present->head;
+ int index = 0;
+ while (cur && ++index) {
+ if (!cur->ust || index == present->opts->swapchain_depth)
+ break;
+ cur = cur->list_node.next;
+ }
+ if (!cur)
+ return;
+
+ cur->ust = ust;
+ cur->msc = msc;
+}
diff --git a/video/out/present_sync.h b/video/out/present_sync.h
new file mode 100644
index 0000000..ba6d0b3
--- /dev/null
+++ b/video/out/present_sync.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_PRESENT_SYNC_H
+#define MP_PRESENT_SYNC_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include "vo.h"
+
+/* Generic helpers for obtaining presentation feedback from
+ * backend APIs. This requires ust/msc values. */
+
+struct mp_present_entry {
+ int64_t ust;
+ int64_t msc;
+ int64_t vsync_duration;
+ int64_t skipped_vsyncs;
+ int64_t queue_display_time;
+
+ struct {
+ struct mp_present_entry *next, *prev;
+ } list_node;
+};
+
+struct mp_present {
+ struct mp_present_entry *head, *tail;
+ struct mp_vo_opts *opts;
+};
+
+struct mp_present *mp_present_initialize(void *talloc_ctx, struct mp_vo_opts *opts, int entries);
+
+// Used during the get_vsync call to deliver the presentation statistics to the VO.
+void present_sync_get_info(struct mp_present *present, struct vo_vsync_info *info);
+
+// Called after every buffer swap to update presentation statistics.
+void present_sync_swap(struct mp_present *present);
+
+// Called anytime the backend delivers new ust/msc values.
+void present_sync_update_values(struct mp_present *present, int64_t ust,
+ int64_t msc);
+
+#endif /* MP_PRESENT_SYNC_H */
diff --git a/video/out/vo.c b/video/out/vo.c
new file mode 100644
index 0000000..50129fb
--- /dev/null
+++ b/video/out/vo.c
@@ -0,0 +1,1441 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+#include "misc/dispatch.h"
+#include "misc/rendezvous.h"
+#include "options/options.h"
+#include "misc/bstr.h"
+#include "vo.h"
+#include "aspect.h"
+#include "dr_helper.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "common/stats.h"
+#include "video/hwdec.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+
+extern const struct vo_driver video_out_mediacodec_embed;
+extern const struct vo_driver video_out_x11;
+extern const struct vo_driver video_out_vdpau;
+extern const struct vo_driver video_out_xv;
+extern const struct vo_driver video_out_gpu;
+extern const struct vo_driver video_out_gpu_next;
+extern const struct vo_driver video_out_libmpv;
+extern const struct vo_driver video_out_null;
+extern const struct vo_driver video_out_image;
+extern const struct vo_driver video_out_lavc;
+extern const struct vo_driver video_out_caca;
+extern const struct vo_driver video_out_drm;
+extern const struct vo_driver video_out_direct3d;
+extern const struct vo_driver video_out_sdl;
+extern const struct vo_driver video_out_vaapi;
+extern const struct vo_driver video_out_dmabuf_wayland;
+extern const struct vo_driver video_out_wlshm;
+extern const struct vo_driver video_out_rpi;
+extern const struct vo_driver video_out_tct;
+extern const struct vo_driver video_out_sixel;
+extern const struct vo_driver video_out_kitty;
+
+static const struct vo_driver *const video_out_drivers[] =
+{
+ &video_out_libmpv,
+#if HAVE_ANDROID
+ &video_out_mediacodec_embed,
+#endif
+ &video_out_gpu,
+ &video_out_gpu_next,
+#if HAVE_VDPAU
+ &video_out_vdpau,
+#endif
+#if HAVE_DIRECT3D
+ &video_out_direct3d,
+#endif
+#if HAVE_WAYLAND && HAVE_MEMFD_CREATE
+ &video_out_wlshm,
+#endif
+#if HAVE_XV
+ &video_out_xv,
+#endif
+#if HAVE_SDL2_VIDEO
+ &video_out_sdl,
+#endif
+#if HAVE_DMABUF_WAYLAND
+ &video_out_dmabuf_wayland,
+#endif
+#if HAVE_VAAPI_X11 && HAVE_GPL
+ &video_out_vaapi,
+#endif
+#if HAVE_X11
+ &video_out_x11,
+#endif
+ &video_out_null,
+ // should not be auto-selected
+ &video_out_image,
+ &video_out_tct,
+#if HAVE_CACA
+ &video_out_caca,
+#endif
+#if HAVE_DRM
+ &video_out_drm,
+#endif
+#if HAVE_RPI_MMAL
+ &video_out_rpi,
+#endif
+#if HAVE_SIXEL
+ &video_out_sixel,
+#endif
+ &video_out_kitty,
+ &video_out_lavc,
+};
+
+struct vo_internal {
+ mp_thread thread;
+ struct mp_dispatch_queue *dispatch;
+ struct dr_helper *dr_helper;
+
+ // --- The following fields are protected by lock
+ mp_mutex lock;
+ mp_cond wakeup;
+
+ bool need_wakeup;
+ bool terminate;
+
+ bool hasframe;
+ bool hasframe_rendered;
+ bool request_redraw; // redraw request from player to VO
+ bool want_redraw; // redraw request from VO to player
+ bool send_reset; // send VOCTRL_RESET
+ bool paused;
+ int queued_events; // event mask for the user
+ int internal_events; // event mask for us
+
+ double nominal_vsync_interval;
+
+ double vsync_interval;
+ int64_t *vsync_samples;
+ int num_vsync_samples;
+ int64_t num_total_vsync_samples;
+ int64_t prev_vsync;
+ double base_vsync;
+ int drop_point;
+ double estimated_vsync_interval;
+ double estimated_vsync_jitter;
+ bool expecting_vsync;
+ int64_t num_successive_vsyncs;
+
+ int64_t flip_queue_offset; // queue flip events at most this much in advance
+ int64_t timing_offset; // same (but from options; not VO configured)
+
+ int64_t delayed_count;
+ int64_t drop_count;
+ bool dropped_frame; // the previous frame was dropped
+
+ struct vo_frame *current_frame; // last frame queued to the VO
+
+ int64_t wakeup_pts; // time at which to pull frame from decoder
+
+ bool rendering; // true if an image is being rendered
+ struct vo_frame *frame_queued; // should be drawn next
+ int req_frames; // VO's requested value of num_frames
+ uint64_t current_frame_id;
+
+ double display_fps;
+ double reported_display_fps;
+
+ struct stats_ctx *stats;
+};
+
+extern const struct m_sub_options gl_video_conf;
+
+static void forget_frames(struct vo *vo);
+static MP_THREAD_VOID vo_thread(void *ptr);
+
+static bool get_desc(struct m_obj_desc *dst, int index)
+{
+ if (index >= MP_ARRAY_SIZE(video_out_drivers))
+ return false;
+ const struct vo_driver *vo = video_out_drivers[index];
+ *dst = (struct m_obj_desc) {
+ .name = vo->name,
+ .description = vo->description,
+ .priv_size = vo->priv_size,
+ .priv_defaults = vo->priv_defaults,
+ .options = vo->options,
+ .options_prefix = vo->options_prefix,
+ .global_opts = vo->global_opts,
+ .hidden = vo->encode,
+ .p = vo,
+ };
+ return true;
+}
+
+// For the vo option
+const struct m_obj_list vo_obj_list = {
+ .get_desc = get_desc,
+ .description = "video outputs",
+ .aliases = {
+ {"gl", "gpu"},
+ {"direct3d_shaders", "direct3d"},
+ {"opengl", "gpu"},
+ {"opengl-cb", "libmpv"},
+ {0}
+ },
+ .allow_trailer = true,
+ .disallow_positional_parameters = true,
+ .use_global_options = true,
+};
+
+static void dispatch_wakeup_cb(void *ptr)
+{
+ struct vo *vo = ptr;
+ vo_wakeup(vo);
+}
+
+// Initialize or update options from vo->opts
+static void read_opts(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_mutex_lock(&in->lock);
+ in->timing_offset = (uint64_t)(MP_TIME_S_TO_NS(vo->opts->timing_offset));
+ mp_mutex_unlock(&in->lock);
+}
+
+static void update_opts(void *p)
+{
+ struct vo *vo = p;
+
+ if (m_config_cache_update(vo->opts_cache)) {
+ read_opts(vo);
+
+ if (vo->driver->control) {
+ vo->driver->control(vo, VOCTRL_VO_OPTS_CHANGED, NULL);
+ // "Legacy" update of video position related options.
+ // Unlike VOCTRL_VO_OPTS_CHANGED, often not propagated to backends.
+ vo->driver->control(vo, VOCTRL_SET_PANSCAN, NULL);
+ }
+ }
+
+ if (vo->gl_opts_cache && m_config_cache_update(vo->gl_opts_cache)) {
+ // "Legacy" update of video GL renderer related options.
+ if (vo->driver->control)
+ vo->driver->control(vo, VOCTRL_UPDATE_RENDER_OPTS, NULL);
+ }
+
+ if (m_config_cache_update(vo->eq_opts_cache)) {
+ // "Legacy" update of video equalizer related options.
+ if (vo->driver->control)
+ vo->driver->control(vo, VOCTRL_SET_EQUALIZER, NULL);
+ }
+}
+
+// Does not include thread- and VO uninit.
+static void dealloc_vo(struct vo *vo)
+{
+ forget_frames(vo); // implicitly synchronized
+
+ // These must be free'd before vo->in->dispatch.
+ talloc_free(vo->opts_cache);
+ talloc_free(vo->gl_opts_cache);
+ talloc_free(vo->eq_opts_cache);
+
+ mp_mutex_destroy(&vo->in->lock);
+ mp_cond_destroy(&vo->in->wakeup);
+ talloc_free(vo);
+}
+
+static struct vo *vo_create(bool probing, struct mpv_global *global,
+ struct vo_extra *ex, char *name)
+{
+ assert(ex->wakeup_cb);
+
+ struct mp_log *log = mp_log_new(NULL, global->log, "vo");
+ struct m_obj_desc desc;
+ if (!m_obj_list_find(&desc, &vo_obj_list, bstr0(name))) {
+ mp_msg(log, MSGL_ERR, "Video output %s not found!\n", name);
+ talloc_free(log);
+ return NULL;
+ };
+ struct vo *vo = talloc_ptrtype(NULL, vo);
+ *vo = (struct vo) {
+ .log = mp_log_new(vo, log, name),
+ .driver = desc.p,
+ .global = global,
+ .encode_lavc_ctx = ex->encode_lavc_ctx,
+ .input_ctx = ex->input_ctx,
+ .osd = ex->osd,
+ .monitor_par = 1,
+ .extra = *ex,
+ .probing = probing,
+ .in = talloc(vo, struct vo_internal),
+ };
+ talloc_steal(vo, log);
+ *vo->in = (struct vo_internal) {
+ .dispatch = mp_dispatch_create(vo),
+ .req_frames = 1,
+ .estimated_vsync_jitter = -1,
+ .stats = stats_ctx_create(vo, global, "vo"),
+ };
+ mp_dispatch_set_wakeup_fn(vo->in->dispatch, dispatch_wakeup_cb, vo);
+ mp_mutex_init(&vo->in->lock);
+ mp_cond_init(&vo->in->wakeup);
+
+ vo->opts_cache = m_config_cache_alloc(NULL, global, &vo_sub_opts);
+ vo->opts = vo->opts_cache->opts;
+
+ m_config_cache_set_dispatch_change_cb(vo->opts_cache, vo->in->dispatch,
+ update_opts, vo);
+
+ vo->gl_opts_cache = m_config_cache_alloc(NULL, global, &gl_video_conf);
+ m_config_cache_set_dispatch_change_cb(vo->gl_opts_cache, vo->in->dispatch,
+ update_opts, vo);
+
+ vo->eq_opts_cache = m_config_cache_alloc(NULL, global, &mp_csp_equalizer_conf);
+ m_config_cache_set_dispatch_change_cb(vo->eq_opts_cache, vo->in->dispatch,
+ update_opts, vo);
+
+ mp_input_set_mouse_transform(vo->input_ctx, NULL, NULL);
+ if (vo->driver->encode != !!vo->encode_lavc_ctx)
+ goto error;
+ vo->priv = m_config_group_from_desc(vo, vo->log, global, &desc, name);
+ if (!vo->priv)
+ goto error;
+
+ if (mp_thread_create(&vo->in->thread, vo_thread, vo))
+ goto error;
+ if (mp_rendezvous(vo, 0) < 0) { // init barrier
+ mp_thread_join(vo->in->thread);
+ goto error;
+ }
+ return vo;
+
+error:
+ dealloc_vo(vo);
+ return NULL;
+}
+
+struct vo *init_best_video_out(struct mpv_global *global, struct vo_extra *ex)
+{
+ struct mp_vo_opts *opts = mp_get_config_group(NULL, global, &vo_sub_opts);
+ struct m_obj_settings *vo_list = opts->video_driver_list;
+ struct vo *vo = NULL;
+ // first try the preferred drivers, with their optional subdevice param:
+ if (vo_list && vo_list[0].name) {
+ for (int n = 0; vo_list[n].name; n++) {
+ // Something like "-vo name," allows fallback to autoprobing.
+ if (strlen(vo_list[n].name) == 0)
+ goto autoprobe;
+ bool p = !!vo_list[n + 1].name;
+ vo = vo_create(p, global, ex, vo_list[n].name);
+ if (vo)
+ goto done;
+ }
+ goto done;
+ }
+autoprobe:
+ // now try the rest...
+ for (int i = 0; i < MP_ARRAY_SIZE(video_out_drivers); i++) {
+ const struct vo_driver *driver = video_out_drivers[i];
+ if (driver == &video_out_null)
+ break;
+ vo = vo_create(true, global, ex, (char *)driver->name);
+ if (vo)
+ goto done;
+ }
+done:
+ talloc_free(opts);
+ return vo;
+}
+
+static void terminate_vo(void *p)
+{
+ struct vo *vo = p;
+ struct vo_internal *in = vo->in;
+ in->terminate = true;
+}
+
+void vo_destroy(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_dispatch_run(in->dispatch, terminate_vo, vo);
+ mp_thread_join(vo->in->thread);
+ dealloc_vo(vo);
+}
+
+// Wakeup the playloop to queue new video frames etc.
+static void wakeup_core(struct vo *vo)
+{
+ vo->extra.wakeup_cb(vo->extra.wakeup_ctx);
+}
+
+// Drop timing information on discontinuities like seeking.
+// Always called locked.
+static void reset_vsync_timings(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ in->drop_point = 0;
+ in->base_vsync = 0;
+ in->expecting_vsync = false;
+ in->num_successive_vsyncs = 0;
+}
+
+static double vsync_stddef(struct vo *vo, double ref_vsync)
+{
+ struct vo_internal *in = vo->in;
+ double jitter = 0;
+ for (int n = 0; n < in->num_vsync_samples; n++) {
+ double diff = in->vsync_samples[n] - ref_vsync;
+ jitter += diff * diff;
+ }
+ return sqrt(jitter / in->num_vsync_samples);
+}
+
+#define MAX_VSYNC_SAMPLES 1000
+#define DELAY_VSYNC_SAMPLES 10
+
+// Check if we should switch to measured average display FPS if it seems
+// "better" then the system-reported one. (Note that small differences are
+// handled as drift instead.)
+static void check_estimated_display_fps(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ bool use_estimated = false;
+ if (in->num_total_vsync_samples >= MAX_VSYNC_SAMPLES / 2 &&
+ in->estimated_vsync_interval <= 1e9 / 20.0 &&
+ in->estimated_vsync_interval >= 1e9 / 400.0)
+ {
+ for (int n = 0; n < in->num_vsync_samples; n++) {
+ if (fabs(in->vsync_samples[n] - in->estimated_vsync_interval)
+ >= in->estimated_vsync_interval / 4)
+ goto done;
+ }
+ double mjitter = vsync_stddef(vo, in->estimated_vsync_interval);
+ double njitter = vsync_stddef(vo, in->nominal_vsync_interval);
+ if (mjitter * 1.01 < njitter)
+ use_estimated = true;
+ done: ;
+ }
+ if (use_estimated == (fabs(in->vsync_interval - in->nominal_vsync_interval) < 1e9)) {
+ if (use_estimated) {
+ MP_TRACE(vo, "adjusting display FPS to a value closer to %.3f Hz\n",
+ 1e9 / in->estimated_vsync_interval);
+ } else {
+ MP_TRACE(vo, "switching back to assuming display fps = %.3f Hz\n",
+ 1e9 / in->nominal_vsync_interval);
+ }
+ }
+ in->vsync_interval = use_estimated ? in->estimated_vsync_interval
+ : in->nominal_vsync_interval;
+}
+
+// Attempt to detect vsyncs delayed/skipped by the driver. This tries to deal
+// with strong jitter too, because some drivers have crap vsync timing.
+static void vsync_skip_detection(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ int window = 4;
+ double t_r = in->prev_vsync, t_e = in->base_vsync, diff = 0.0, desync_early = 0.0;
+ for (int n = 0; n < in->drop_point; n++) {
+ diff += t_r - t_e;
+ t_r -= in->vsync_samples[n];
+ t_e -= in->vsync_interval;
+ if (n == window + 1)
+ desync_early = diff / window;
+ }
+ double desync = diff / in->num_vsync_samples;
+ if (in->drop_point > window * 2 &&
+ fabs(desync - desync_early) >= in->vsync_interval * 3 / 4)
+ {
+ // Assume a drop. An underflow can technically speaking not be a drop
+ // (it's up to the driver what this is supposed to mean), but no reason
+ // to treat it differently.
+ in->base_vsync = in->prev_vsync;
+ in->delayed_count += 1;
+ in->drop_point = 0;
+ MP_STATS(vo, "vo-delayed");
+ }
+ if (in->drop_point > 10)
+ in->base_vsync += desync / 10; // smooth out drift
+}
+
+// Always called locked.
+static void update_vsync_timing_after_swap(struct vo *vo,
+ struct vo_vsync_info *vsync)
+{
+ struct vo_internal *in = vo->in;
+
+ int64_t vsync_time = vsync->last_queue_display_time;
+ int64_t prev_vsync = in->prev_vsync;
+ in->prev_vsync = vsync_time;
+
+ if (!in->expecting_vsync) {
+ reset_vsync_timings(vo);
+ return;
+ }
+
+ in->num_successive_vsyncs++;
+ if (in->num_successive_vsyncs <= DELAY_VSYNC_SAMPLES)
+ return;
+
+ if (vsync_time <= 0 || vsync_time <= prev_vsync) {
+ in->prev_vsync = 0;
+ return;
+ }
+
+ if (prev_vsync <= 0)
+ return;
+
+ if (in->num_vsync_samples >= MAX_VSYNC_SAMPLES)
+ in->num_vsync_samples -= 1;
+ MP_TARRAY_INSERT_AT(in, in->vsync_samples, in->num_vsync_samples, 0,
+ vsync_time - prev_vsync);
+ in->drop_point = MPMIN(in->drop_point + 1, in->num_vsync_samples);
+ in->num_total_vsync_samples += 1;
+ if (in->base_vsync) {
+ in->base_vsync += in->vsync_interval;
+ } else {
+ in->base_vsync = vsync_time;
+ }
+
+ double avg = 0;
+ for (int n = 0; n < in->num_vsync_samples; n++) {
+ assert(in->vsync_samples[n] > 0);
+ avg += in->vsync_samples[n];
+ }
+ in->estimated_vsync_interval = avg / in->num_vsync_samples;
+ in->estimated_vsync_jitter =
+ vsync_stddef(vo, in->vsync_interval) / in->vsync_interval;
+
+ check_estimated_display_fps(vo);
+ vsync_skip_detection(vo);
+
+ MP_STATS(vo, "value %f jitter", in->estimated_vsync_jitter);
+ MP_STATS(vo, "value %f vsync-diff", MP_TIME_NS_TO_S(in->vsync_samples[0]));
+}
+
+// to be called from VO thread only
+static void update_display_fps(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if (in->internal_events & VO_EVENT_WIN_STATE) {
+ in->internal_events &= ~(unsigned)VO_EVENT_WIN_STATE;
+
+ mp_mutex_unlock(&in->lock);
+
+ double fps = 0;
+ vo->driver->control(vo, VOCTRL_GET_DISPLAY_FPS, &fps);
+
+ mp_mutex_lock(&in->lock);
+
+ in->reported_display_fps = fps;
+ }
+
+ double display_fps = vo->opts->display_fps_override;
+ if (display_fps <= 0)
+ display_fps = in->reported_display_fps;
+
+ if (in->display_fps != display_fps) {
+ in->nominal_vsync_interval = display_fps > 0 ? 1e9 / display_fps : 0;
+ in->vsync_interval = MPMAX(in->nominal_vsync_interval, 1);
+ in->display_fps = display_fps;
+
+ MP_VERBOSE(vo, "Assuming %f FPS for display sync.\n", display_fps);
+
+ // make sure to update the player
+ in->queued_events |= VO_EVENT_WIN_STATE;
+ wakeup_core(vo);
+ }
+
+ mp_mutex_unlock(&in->lock);
+}
+
+static void check_vo_caps(struct vo *vo)
+{
+ int rot = vo->params->rotate;
+ if (rot) {
+ bool ok = rot % 90 ? false : (vo->driver->caps & VO_CAP_ROTATE90);
+ if (!ok) {
+ MP_WARN(vo, "Video is flagged as rotated by %d degrees, but the "
+ "video output does not support this.\n", rot);
+ }
+ }
+}
+
+static void run_reconfig(void *p)
+{
+ void **pp = p;
+ struct vo *vo = pp[0];
+ struct mp_image *img = pp[1];
+ int *ret = pp[2];
+
+ struct mp_image_params *params = &img->params;
+
+ struct vo_internal *in = vo->in;
+
+ MP_VERBOSE(vo, "reconfig to %s\n", mp_image_params_to_str(params));
+
+ update_opts(vo);
+
+ mp_image_params_get_dsize(params, &vo->dwidth, &vo->dheight);
+
+ talloc_free(vo->params);
+ vo->params = talloc_dup(vo, params);
+
+ if (vo->driver->reconfig2) {
+ *ret = vo->driver->reconfig2(vo, img);
+ } else {
+ *ret = vo->driver->reconfig(vo, vo->params);
+ }
+ vo->config_ok = *ret >= 0;
+ if (vo->config_ok) {
+ check_vo_caps(vo);
+ } else {
+ talloc_free(vo->params);
+ vo->params = NULL;
+ }
+
+ mp_mutex_lock(&in->lock);
+ talloc_free(in->current_frame);
+ in->current_frame = NULL;
+ forget_frames(vo);
+ reset_vsync_timings(vo);
+ mp_mutex_unlock(&in->lock);
+
+ update_display_fps(vo);
+}
+
+int vo_reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ int ret;
+ struct mp_image dummy = {0};
+ mp_image_set_params(&dummy, params);
+ void *p[] = {vo, &dummy, &ret};
+ mp_dispatch_run(vo->in->dispatch, run_reconfig, p);
+ return ret;
+}
+
+int vo_reconfig2(struct vo *vo, struct mp_image *img)
+{
+ int ret;
+ void *p[] = {vo, img, &ret};
+ mp_dispatch_run(vo->in->dispatch, run_reconfig, p);
+ return ret;
+}
+
+static void run_control(void *p)
+{
+ void **pp = p;
+ struct vo *vo = pp[0];
+ int request = (intptr_t)pp[1];
+ void *data = pp[2];
+ update_opts(vo);
+ int ret = vo->driver->control(vo, request, data);
+ if (pp[3])
+ *(int *)pp[3] = ret;
+}
+
+int vo_control(struct vo *vo, int request, void *data)
+{
+ int ret;
+ void *p[] = {vo, (void *)(intptr_t)request, data, &ret};
+ mp_dispatch_run(vo->in->dispatch, run_control, p);
+ return ret;
+}
+
+// Run vo_control() without waiting for a reply.
+// (Only works for some VOCTRLs.)
+void vo_control_async(struct vo *vo, int request, void *data)
+{
+ void *p[4] = {vo, (void *)(intptr_t)request, NULL, NULL};
+ void **d = talloc_memdup(NULL, p, sizeof(p));
+
+ switch (request) {
+ case VOCTRL_UPDATE_PLAYBACK_STATE:
+ d[2] = talloc_dup(d, (struct voctrl_playback_state *)data);
+ break;
+ case VOCTRL_KILL_SCREENSAVER:
+ case VOCTRL_RESTORE_SCREENSAVER:
+ break;
+ default:
+ abort(); // requires explicit support
+ }
+
+ mp_dispatch_enqueue_autofree(vo->in->dispatch, run_control, d);
+}
+
+// must be called locked
+static void forget_frames(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ in->hasframe = false;
+ in->hasframe_rendered = false;
+ in->drop_count = 0;
+ in->delayed_count = 0;
+ talloc_free(in->frame_queued);
+ in->frame_queued = NULL;
+ in->current_frame_id += VO_MAX_REQ_FRAMES + 1;
+ // don't unref current_frame; we always want to be able to redraw it
+ if (in->current_frame) {
+ in->current_frame->num_vsyncs = 0; // but reset future repeats
+ in->current_frame->display_synced = false; // mark discontinuity
+ }
+}
+
+// VOs which have no special requirements on UI event loops etc. can set the
+// vo_driver.wait_events callback to this (and leave vo_driver.wakeup unset).
+// This function must not be used or called for other purposes.
+void vo_wait_default(struct vo *vo, int64_t until_time)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_mutex_lock(&in->lock);
+ if (!in->need_wakeup)
+ mp_cond_timedwait_until(&in->wakeup, &in->lock, until_time);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Called unlocked.
+static void wait_vo(struct vo *vo, int64_t until_time)
+{
+ struct vo_internal *in = vo->in;
+
+ if (vo->driver->wait_events) {
+ vo->driver->wait_events(vo, until_time);
+ } else {
+ vo_wait_default(vo, until_time);
+ }
+ mp_mutex_lock(&in->lock);
+ in->need_wakeup = false;
+ mp_mutex_unlock(&in->lock);
+}
+
+static void wakeup_locked(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_cond_broadcast(&in->wakeup);
+ if (vo->driver->wakeup)
+ vo->driver->wakeup(vo);
+ in->need_wakeup = true;
+}
+
+// Wakeup VO thread, and make it check for new events with VOCTRL_CHECK_EVENTS.
+// To be used by threaded VO backends.
+void vo_wakeup(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_mutex_lock(&in->lock);
+ wakeup_locked(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Whether vo_queue_frame() can be called. If the VO is not ready yet, the
+// function will return false, and the VO will call the wakeup callback once
+// it's ready.
+// next_pts is the exact time when the next frame should be displayed. If the
+// VO is ready, but the time is too "early", return false, and call the wakeup
+// callback once the time is right.
+// If next_pts is negative, disable any timing and draw the frame as fast as
+// possible.
+bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ bool blocked = vo->driver->initially_blocked &&
+ !(in->internal_events & VO_EVENT_INITIAL_UNBLOCK);
+ bool r = vo->config_ok && !in->frame_queued && !blocked &&
+ (!in->current_frame || in->current_frame->num_vsyncs < 1);
+ if (r && next_pts >= 0) {
+ // Don't show the frame too early - it would basically freeze the
+ // display by disallowing OSD redrawing or VO interaction.
+ // Actually render the frame at earliest the given offset before target
+ // time.
+ next_pts -= in->timing_offset;
+ next_pts -= in->flip_queue_offset;
+ int64_t now = mp_time_ns();
+ if (next_pts > now)
+ r = false;
+ if (!in->wakeup_pts || next_pts < in->wakeup_pts) {
+ in->wakeup_pts = next_pts;
+ // If we have to wait, update the vo thread's timer.
+ if (!r)
+ wakeup_locked(vo);
+ }
+ }
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+// Direct the VO thread to put the currently queued image on the screen.
+// vo_is_ready_for_frame() must have returned true before this call.
+// Ownership of frame is handed to the vo.
+void vo_queue_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ assert(vo->config_ok && !in->frame_queued &&
+ (!in->current_frame || in->current_frame->num_vsyncs < 1));
+ in->hasframe = true;
+ frame->frame_id = ++(in->current_frame_id);
+ in->frame_queued = frame;
+ in->wakeup_pts = frame->display_synced
+ ? 0 : frame->pts + MPMAX(frame->duration, 0);
+ wakeup_locked(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+// If a frame is currently being rendered (or queued), wait until it's done.
+// Otherwise, return immediately.
+void vo_wait_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ while (in->frame_queued || in->rendering)
+ mp_cond_wait(&in->wakeup, &in->lock);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Wait until realtime is >= ts
+// called without lock
+static void wait_until(struct vo *vo, int64_t target)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ while (target > mp_time_ns()) {
+ if (in->queued_events & VO_EVENT_LIVE_RESIZING)
+ break;
+ if (mp_cond_timedwait_until(&in->wakeup, &in->lock, target))
+ break;
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+static bool render_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ struct vo_frame *frame = NULL;
+ bool more_frames = false;
+
+ update_display_fps(vo);
+
+ mp_mutex_lock(&in->lock);
+
+ if (in->frame_queued) {
+ talloc_free(in->current_frame);
+ in->current_frame = in->frame_queued;
+ in->frame_queued = NULL;
+ } else if (in->paused || !in->current_frame || !in->hasframe ||
+ (in->current_frame->display_synced && in->current_frame->num_vsyncs < 1) ||
+ !in->current_frame->display_synced)
+ {
+ goto done;
+ }
+
+ frame = vo_frame_ref(in->current_frame);
+ assert(frame);
+
+ if (frame->display_synced) {
+ frame->pts = 0;
+ frame->duration = -1;
+ }
+
+ int64_t now = mp_time_ns();
+ int64_t pts = frame->pts;
+ int64_t duration = frame->duration;
+ int64_t end_time = pts + duration;
+
+ // Time at which we should flip_page on the VO.
+ int64_t target = frame->display_synced ? 0 : pts - in->flip_queue_offset;
+
+ // "normal" strict drop threshold.
+ in->dropped_frame = duration >= 0 && end_time < now;
+
+ in->dropped_frame &= !frame->display_synced;
+ in->dropped_frame &= !(vo->driver->caps & VO_CAP_FRAMEDROP);
+ in->dropped_frame &= frame->can_drop;
+ // Even if we're hopelessly behind, rather degrade to 10 FPS playback,
+ // instead of just freezing the display forever.
+ in->dropped_frame &= now - in->prev_vsync < MP_TIME_MS_TO_NS(100);
+ in->dropped_frame &= in->hasframe_rendered;
+
+ // Setup parameters for the next time this frame is drawn. ("frame" is the
+ // frame currently drawn, while in->current_frame is the potentially next.)
+ in->current_frame->repeat = true;
+ if (frame->display_synced) {
+ // Increment the offset only if it's not the last vsync. The current_frame
+ // can still be reused. This is mostly important for redraws that might
+ // overshoot the target vsync point.
+ if (in->current_frame->num_vsyncs > 1) {
+ in->current_frame->vsync_offset += in->current_frame->vsync_interval;
+ in->current_frame->ideal_frame_vsync += in->current_frame->ideal_frame_vsync_duration;
+ }
+ in->dropped_frame |= in->current_frame->num_vsyncs < 1;
+ }
+ if (in->current_frame->num_vsyncs > 0)
+ in->current_frame->num_vsyncs -= 1;
+
+ // Always render when paused (it's typically the last frame for a while).
+ in->dropped_frame &= !in->paused;
+
+ bool use_vsync = in->current_frame->display_synced && !in->paused;
+ if (use_vsync && !in->expecting_vsync) // first DS frame in a row
+ in->prev_vsync = now;
+ in->expecting_vsync = use_vsync;
+
+ // Store the initial value before we unlock.
+ bool request_redraw = in->request_redraw;
+
+ if (in->dropped_frame) {
+ in->drop_count += 1;
+ } else {
+ in->rendering = true;
+ in->hasframe_rendered = true;
+ int64_t prev_drop_count = vo->in->drop_count;
+ // Can the core queue new video now? Non-display-sync uses a separate
+ // timer instead, but possibly benefits from preparing a frame early.
+ bool can_queue = !in->frame_queued &&
+ (in->current_frame->num_vsyncs < 1 || !use_vsync);
+ mp_mutex_unlock(&in->lock);
+
+ if (can_queue)
+ wakeup_core(vo);
+
+ stats_time_start(in->stats, "video-draw");
+
+ vo->driver->draw_frame(vo, frame);
+
+ stats_time_end(in->stats, "video-draw");
+
+ wait_until(vo, target);
+
+ stats_time_start(in->stats, "video-flip");
+
+ vo->driver->flip_page(vo);
+
+ struct vo_vsync_info vsync = {
+ .last_queue_display_time = -1,
+ .skipped_vsyncs = -1,
+ };
+ if (vo->driver->get_vsync)
+ vo->driver->get_vsync(vo, &vsync);
+
+ // Make up some crap if presentation feedback is missing.
+ if (vsync.last_queue_display_time <= 0)
+ vsync.last_queue_display_time = mp_time_ns();
+
+ stats_time_end(in->stats, "video-flip");
+
+ mp_mutex_lock(&in->lock);
+ in->dropped_frame = prev_drop_count < vo->in->drop_count;
+ in->rendering = false;
+
+ update_vsync_timing_after_swap(vo, &vsync);
+ }
+
+ if (vo->driver->caps & VO_CAP_NORETAIN) {
+ talloc_free(in->current_frame);
+ in->current_frame = NULL;
+ }
+
+ if (in->dropped_frame) {
+ MP_STATS(vo, "drop-vo");
+ } else {
+ // If the initial redraw request was true or mpv is still playing,
+ // then we can clear it here since we just performed a redraw, or the
+ // next loop will draw what we need. However if there initially is
+ // no redraw request, then something can change this (i.e. the OSD)
+ // while the vo was unlocked. If we are paused, don't touch
+ // in->request_redraw in that case.
+ if (request_redraw || !in->paused)
+ in->request_redraw = false;
+ }
+
+ if (in->current_frame && in->current_frame->num_vsyncs &&
+ in->current_frame->display_synced)
+ more_frames = true;
+
+ if (in->frame_queued && in->frame_queued->display_synced)
+ more_frames = true;
+
+ mp_cond_broadcast(&in->wakeup); // for vo_wait_frame()
+ wakeup_core(vo);
+
+done:
+ if (!vo->driver->frame_owner)
+ talloc_free(frame);
+ mp_mutex_unlock(&in->lock);
+
+ return more_frames;
+}
+
+static void do_redraw(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ if (!vo->config_ok || (vo->driver->caps & VO_CAP_NORETAIN))
+ return;
+
+ mp_mutex_lock(&in->lock);
+ in->request_redraw = false;
+ bool full_redraw = in->dropped_frame;
+ struct vo_frame *frame = NULL;
+ if (!vo->driver->untimed)
+ frame = vo_frame_ref(in->current_frame);
+ if (frame)
+ in->dropped_frame = false;
+ struct vo_frame dummy = {0};
+ if (!frame)
+ frame = &dummy;
+ frame->redraw = !full_redraw; // unconditionally redraw if it was dropped
+ frame->repeat = false;
+ frame->still = true;
+ frame->pts = 0;
+ frame->duration = -1;
+ mp_mutex_unlock(&in->lock);
+
+ vo->driver->draw_frame(vo, frame);
+ vo->driver->flip_page(vo);
+
+ if (frame != &dummy && !vo->driver->frame_owner)
+ talloc_free(frame);
+}
+
+static struct mp_image *get_image_vo(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct vo *vo = ctx;
+ return vo->driver->get_image(vo, imgfmt, w, h, stride_align, flags);
+}
+
+static MP_THREAD_VOID vo_thread(void *ptr)
+{
+ struct vo *vo = ptr;
+ struct vo_internal *in = vo->in;
+ bool vo_paused = false;
+
+ mp_thread_set_name("vo");
+
+ if (vo->driver->get_image) {
+ in->dr_helper = dr_helper_create(in->dispatch, get_image_vo, vo);
+ dr_helper_acquire_thread(in->dr_helper);
+ }
+
+ int r = vo->driver->preinit(vo) ? -1 : 0;
+ mp_rendezvous(vo, r); // init barrier
+ if (r < 0)
+ goto done;
+
+ read_opts(vo);
+ update_display_fps(vo);
+ vo_event(vo, VO_EVENT_WIN_STATE);
+
+ while (1) {
+ mp_dispatch_queue_process(vo->in->dispatch, 0);
+ if (in->terminate)
+ break;
+ stats_event(in->stats, "iterations");
+ vo->driver->control(vo, VOCTRL_CHECK_EVENTS, NULL);
+ bool working = render_frame(vo);
+ int64_t now = mp_time_ns();
+ int64_t wait_until = now + MP_TIME_S_TO_NS(working ? 0 : 1000);
+
+ mp_mutex_lock(&in->lock);
+ if (in->wakeup_pts) {
+ if (in->wakeup_pts > now) {
+ wait_until = MPMIN(wait_until, in->wakeup_pts);
+ } else {
+ in->wakeup_pts = 0;
+ wakeup_core(vo);
+ }
+ }
+ if (vo->want_redraw && !in->want_redraw) {
+ in->want_redraw = true;
+ wakeup_core(vo);
+ }
+ vo->want_redraw = false;
+ bool redraw = in->request_redraw;
+ bool send_reset = in->send_reset;
+ in->send_reset = false;
+ bool send_pause = in->paused != vo_paused;
+ vo_paused = in->paused;
+ mp_mutex_unlock(&in->lock);
+
+ if (send_reset)
+ vo->driver->control(vo, VOCTRL_RESET, NULL);
+ if (send_pause)
+ vo->driver->control(vo, vo_paused ? VOCTRL_PAUSE : VOCTRL_RESUME, NULL);
+ if (wait_until > now && redraw) {
+ do_redraw(vo); // now is a good time
+ continue;
+ }
+ if (vo->want_redraw) // might have been set by VOCTRLs
+ wait_until = 0;
+
+ if (wait_until <= now)
+ continue;
+
+ wait_vo(vo, wait_until);
+ }
+ forget_frames(vo); // implicitly synchronized
+ talloc_free(in->current_frame);
+ in->current_frame = NULL;
+ vo->driver->uninit(vo);
+done:
+ TA_FREEP(&in->dr_helper);
+ MP_THREAD_RETURN();
+}
+
+void vo_set_paused(struct vo *vo, bool paused)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if (in->paused != paused) {
+ in->paused = paused;
+ if (in->paused && in->dropped_frame) {
+ in->request_redraw = true;
+ wakeup_core(vo);
+ }
+ reset_vsync_timings(vo);
+ wakeup_locked(vo);
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+int64_t vo_get_drop_count(struct vo *vo)
+{
+ mp_mutex_lock(&vo->in->lock);
+ int64_t r = vo->in->drop_count;
+ mp_mutex_unlock(&vo->in->lock);
+ return r;
+}
+
+void vo_increment_drop_count(struct vo *vo, int64_t n)
+{
+ mp_mutex_lock(&vo->in->lock);
+ vo->in->drop_count += n;
+ mp_mutex_unlock(&vo->in->lock);
+}
+
+// Make the VO redraw the OSD at some point in the future.
+void vo_redraw(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if (!in->request_redraw) {
+ in->request_redraw = true;
+ in->want_redraw = false;
+ wakeup_locked(vo);
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+bool vo_want_redraw(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ bool r = in->want_redraw;
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+void vo_seek_reset(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ forget_frames(vo);
+ reset_vsync_timings(vo);
+ in->send_reset = true;
+ wakeup_locked(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Return true if there is still a frame being displayed (or queued).
+// If this returns true, a wakeup some time in the future is guaranteed.
+bool vo_still_displaying(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ bool working = in->rendering || in->frame_queued;
+ mp_mutex_unlock(&in->lock);
+ return working && in->hasframe;
+}
+
+// Whether at least 1 frame was queued or rendered since last seek or reconfig.
+bool vo_has_frame(struct vo *vo)
+{
+ return vo->in->hasframe;
+}
+
+static void run_query_format(void *p)
+{
+ void **pp = p;
+ struct vo *vo = pp[0];
+ uint8_t *list = pp[1];
+ for (int format = IMGFMT_START; format < IMGFMT_END; format++)
+ list[format - IMGFMT_START] = vo->driver->query_format(vo, format);
+}
+
+// For each item in the list (allocated as uint8_t[IMGFMT_END - IMGFMT_START]),
+// set the supported format flags.
+void vo_query_formats(struct vo *vo, uint8_t *list)
+{
+ void *p[] = {vo, list};
+ mp_dispatch_run(vo->in->dispatch, run_query_format, p);
+}
+
+// Calculate the appropriate source and destination rectangle to
+// get a correctly scaled picture, including pan-scan.
+// out_src: visible part of the video
+// out_dst: area of screen covered by the video source rectangle
+// out_osd: OSD size, OSD margins, etc.
+// Must be called from the VO thread only.
+void vo_get_src_dst_rects(struct vo *vo, struct mp_rect *out_src,
+ struct mp_rect *out_dst, struct mp_osd_res *out_osd)
+{
+ if (!vo->params) {
+ *out_src = *out_dst = (struct mp_rect){0};
+ *out_osd = (struct mp_osd_res){0};
+ return;
+ }
+ mp_get_src_dst_rects(vo->log, vo->opts, vo->driver->caps, vo->params,
+ vo->dwidth, vo->dheight, vo->monitor_par,
+ out_src, out_dst, out_osd);
+}
+
+// flip_page[_timed] will be called offset_us nanoseconds too early.
+// (For vo_vdpau, which does its own timing.)
+// num_req_frames set the requested number of requested vo_frame.frames.
+// (For vo_gpu interpolation.)
+void vo_set_queue_params(struct vo *vo, int64_t offset_ns, int num_req_frames)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ in->flip_queue_offset = offset_ns;
+ in->req_frames = MPCLAMP(num_req_frames, 1, VO_MAX_REQ_FRAMES);
+ mp_mutex_unlock(&in->lock);
+}
+
+int vo_get_num_req_frames(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ int res = in->req_frames;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_vsync_interval(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = vo->in->vsync_interval > 1 ? vo->in->vsync_interval : -1;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_estimated_vsync_interval(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = in->estimated_vsync_interval;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_estimated_vsync_jitter(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = in->estimated_vsync_jitter;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+// Get the time in seconds at after which the currently rendering frame will
+// end. Returns positive values if the frame is yet to be finished, negative
+// values if it already finished.
+// This can only be called while no new frame is queued (after
+// vo_is_ready_for_frame). Returns 0 for non-display synced frames, or if the
+// deadline for continuous display was missed.
+double vo_get_delay(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ assert (!in->frame_queued);
+ int64_t res = 0;
+ if (in->base_vsync && in->vsync_interval > 1 && in->current_frame) {
+ res = in->base_vsync;
+ int extra = !!in->rendering;
+ res += (in->current_frame->num_vsyncs + extra) * in->vsync_interval;
+ if (!in->current_frame->display_synced)
+ res = 0;
+ }
+ mp_mutex_unlock(&in->lock);
+ return res ? MP_TIME_NS_TO_S(res - mp_time_ns()) : 0;
+}
+
+void vo_discard_timing_info(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ reset_vsync_timings(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+int64_t vo_get_delayed_count(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ int64_t res = vo->in->delayed_count;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_display_fps(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = vo->in->display_fps;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+// Set specific event flags, and wakeup the playback core if needed.
+// vo_query_and_reset_events() can retrieve the events again.
+void vo_event(struct vo *vo, int event)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if ((in->queued_events & event & VO_EVENTS_USER) != (event & VO_EVENTS_USER))
+ wakeup_core(vo);
+ if (event)
+ wakeup_locked(vo);
+ in->queued_events |= event;
+ in->internal_events |= event;
+ mp_mutex_unlock(&in->lock);
+}
+
+// Check event flags set with vo_event(). Return the mask of events that was
+// set and included in the events parameter. Clear the returned events.
+int vo_query_and_reset_events(struct vo *vo, int events)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ int r = in->queued_events & events;
+ in->queued_events &= ~(unsigned)r;
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct mp_image *vo_get_current_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ struct mp_image *r = NULL;
+ if (vo->in->current_frame)
+ r = mp_image_new_ref(vo->in->current_frame->current);
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct vo_frame *vo_get_current_vo_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ struct vo_frame *r = vo_frame_ref(vo->in->current_frame);
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct mp_image *vo_get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ if (vo->driver->get_image_ts)
+ return vo->driver->get_image_ts(vo, imgfmt, w, h, stride_align, flags);
+ if (vo->in->dr_helper)
+ return dr_helper_get_image(vo->in->dr_helper, imgfmt, w, h, stride_align, flags);
+ return NULL;
+}
+
+static void destroy_frame(void *p)
+{
+ struct vo_frame *frame = p;
+ for (int n = 0; n < frame->num_frames; n++)
+ talloc_free(frame->frames[n]);
+}
+
+// Return a new reference to the given frame. The image pointers are also new
+// references. Calling talloc_free() on the frame unrefs all currently set
+// image references. (Assuming current==frames[0].)
+struct vo_frame *vo_frame_ref(struct vo_frame *frame)
+{
+ if (!frame)
+ return NULL;
+
+ struct vo_frame *new = talloc_ptrtype(NULL, new);
+ talloc_set_destructor(new, destroy_frame);
+ *new = *frame;
+ for (int n = 0; n < frame->num_frames; n++)
+ new->frames[n] = mp_image_new_ref(frame->frames[n]);
+ new->current = new->num_frames ? new->frames[0] : NULL;
+ return new;
+}
+
+/*
+ * lookup an integer in a table, table must have 0 as the last key
+ * param: key key to search for
+ * returns translation corresponding to key or "to" value of last mapping
+ * if not found.
+ */
+int lookup_keymap_table(const struct mp_keymap *map, int key)
+{
+ while (map->from && map->from != key)
+ map++;
+ return map->to;
+}
+
+struct mp_image_params vo_get_current_params(struct vo *vo)
+{
+ struct mp_image_params p = {0};
+ mp_mutex_lock(&vo->in->lock);
+ if (vo->params)
+ p = *vo->params;
+ mp_mutex_unlock(&vo->in->lock);
+ return p;
+}
diff --git a/video/out/vo.h b/video/out/vo.h
new file mode 100644
index 0000000..e38dcf8
--- /dev/null
+++ b/video/out/vo.h
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) Aaron Holtzman - Aug 1999
+ *
+ * Strongly modified, most parts rewritten: A'rpi/ESP-team - 2000-2001
+ * (C) MPlayer developers
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_VIDEO_OUT_H
+#define MPLAYER_VIDEO_OUT_H
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "video/img_format.h"
+#include "common/common.h"
+#include "options/options.h"
+
+enum {
+ // VO needs to redraw
+ VO_EVENT_EXPOSE = 1 << 0,
+ // VO needs to update state to a new window size
+ VO_EVENT_RESIZE = 1 << 1,
+ // The ICC profile needs to be reloaded
+ VO_EVENT_ICC_PROFILE_CHANGED = 1 << 2,
+ // Some other window state changed (position, window state, fps)
+ VO_EVENT_WIN_STATE = 1 << 3,
+ // The ambient light conditions changed and need to be reloaded
+ VO_EVENT_AMBIENT_LIGHTING_CHANGED = 1 << 4,
+ // Special mechanism for making resizing with Cocoa react faster
+ VO_EVENT_LIVE_RESIZING = 1 << 5,
+ // For VOCTRL_GET_HIDPI_SCALE changes.
+ VO_EVENT_DPI = 1 << 6,
+ // Special thing for encode mode (vo_driver.initially_blocked).
+ // Part of VO_EVENTS_USER to make vo_is_ready_for_frame() work properly.
+ VO_EVENT_INITIAL_UNBLOCK = 1 << 7,
+ VO_EVENT_FOCUS = 1 << 8,
+
+ // Set of events the player core may be interested in.
+ VO_EVENTS_USER = VO_EVENT_RESIZE | VO_EVENT_WIN_STATE | VO_EVENT_DPI |
+ VO_EVENT_INITIAL_UNBLOCK | VO_EVENT_FOCUS,
+};
+
+enum mp_voctrl {
+ /* signal a device reset seek */
+ VOCTRL_RESET = 1,
+ /* Handle input and redraw events, called by vo_check_events() */
+ VOCTRL_CHECK_EVENTS,
+ /* signal a device pause */
+ VOCTRL_PAUSE,
+ /* start/resume playback */
+ VOCTRL_RESUME,
+
+ VOCTRL_SET_PANSCAN,
+ VOCTRL_SET_EQUALIZER,
+
+ // Triggered by any change to mp_vo_opts. This is for convenience. In theory,
+ // you could install your own listener.
+ VOCTRL_VO_OPTS_CHANGED,
+
+ /* private to vo_gpu */
+ VOCTRL_LOAD_HWDEC_API,
+
+ // Only used internally in vo_libmpv
+ VOCTRL_PREINIT,
+ VOCTRL_UNINIT,
+ VOCTRL_RECONFIG,
+
+ VOCTRL_UPDATE_WINDOW_TITLE, // char*
+ VOCTRL_UPDATE_PLAYBACK_STATE, // struct voctrl_playback_state*
+
+ VOCTRL_PERFORMANCE_DATA, // struct voctrl_performance_data*
+
+ VOCTRL_SET_CURSOR_VISIBILITY, // bool*
+
+ VOCTRL_CONTENT_TYPE, // enum mp_content_type*
+
+ VOCTRL_KILL_SCREENSAVER,
+ VOCTRL_RESTORE_SCREENSAVER,
+
+ // Return or set window size (not-fullscreen mode only - if fullscreened,
+ // these must access the not-fullscreened window size only).
+ VOCTRL_GET_UNFS_WINDOW_SIZE, // int[2] (w/h)
+ VOCTRL_SET_UNFS_WINDOW_SIZE, // int[2] (w/h)
+
+ VOCTRL_GET_FOCUSED, // bool*
+
+ // char *** (NULL terminated array compatible with CONF_TYPE_STRING_LIST)
+ // names for displays the window is on
+ VOCTRL_GET_DISPLAY_NAMES,
+
+ // Retrieve window contents. (Normal screenshots use vo_get_current_frame().)
+ // Deprecated for VOCTRL_SCREENSHOT with corresponding flags.
+ VOCTRL_SCREENSHOT_WIN, // struct mp_image**
+
+ // A normal screenshot - VOs can react to this if vo_get_current_frame() is
+ // not sufficient.
+ VOCTRL_SCREENSHOT, // struct voctrl_screenshot*
+
+ VOCTRL_UPDATE_RENDER_OPTS,
+
+ VOCTRL_GET_ICC_PROFILE, // bstr*
+ VOCTRL_GET_AMBIENT_LUX, // int*
+ VOCTRL_GET_DISPLAY_FPS, // double*
+ VOCTRL_GET_HIDPI_SCALE, // double*
+ VOCTRL_GET_DISPLAY_RES, // int[2]
+ VOCTRL_GET_WINDOW_ID, // int64_t*
+
+ /* private to vo_gpu and vo_gpu_next */
+ VOCTRL_EXTERNAL_RESIZE,
+};
+
+// Helper to expose what kind of content is currently playing to the VO.
+enum mp_content_type {
+ MP_CONTENT_NONE, // used for force-window
+ MP_CONTENT_IMAGE,
+ MP_CONTENT_VIDEO,
+};
+
+#define VO_TRUE true
+#define VO_FALSE false
+#define VO_ERROR -1
+#define VO_NOTAVAIL -2
+#define VO_NOTIMPL -3
+
+// VOCTRL_UPDATE_PLAYBACK_STATE
+struct voctrl_playback_state {
+ bool taskbar_progress;
+ bool playing;
+ bool paused;
+ int percent_pos;
+};
+
+// VOCTRL_PERFORMANCE_DATA
+#define VO_PERF_SAMPLE_COUNT 256
+
+struct mp_pass_perf {
+ // times are all in nanoseconds
+ uint64_t last, avg, peak;
+ uint64_t samples[VO_PERF_SAMPLE_COUNT];
+ uint64_t count;
+};
+
+#define VO_PASS_PERF_MAX 64
+#define VO_PASS_DESC_MAX_LEN 128
+
+struct mp_frame_perf {
+ int count;
+ struct mp_pass_perf perf[VO_PASS_PERF_MAX];
+ char desc[VO_PASS_PERF_MAX][VO_PASS_DESC_MAX_LEN];
+};
+
+struct voctrl_performance_data {
+ struct mp_frame_perf fresh, redraw;
+};
+
+struct voctrl_screenshot {
+ bool scaled, subs, osd, high_bit_depth, native_csp;
+ struct mp_image *res;
+};
+
+enum {
+ // VO does handle mp_image_params.rotate in 90 degree steps
+ VO_CAP_ROTATE90 = 1 << 0,
+ // VO does framedrop itself (vo_vdpau). Untimed/encoding VOs never drop.
+ VO_CAP_FRAMEDROP = 1 << 1,
+ // VO does not allow frames to be retained (vo_mediacodec_embed).
+ VO_CAP_NORETAIN = 1 << 2,
+ // VO supports applying film grain
+ VO_CAP_FILM_GRAIN = 1 << 3,
+};
+
+enum {
+ // Require DR buffers to be host-cached (i.e. fast readback)
+ VO_DR_FLAG_HOST_CACHED = 1 << 0,
+};
+
+#define VO_MAX_REQ_FRAMES 10
+#define VO_MAX_SWAPCHAIN_DEPTH 8
+
+struct vo;
+struct osd_state;
+struct mp_image;
+struct mp_image_params;
+
+struct vo_extra {
+ struct input_ctx *input_ctx;
+ struct osd_state *osd;
+ struct encode_lavc_context *encode_lavc_ctx;
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_ctx;
+};
+
+struct vo_frame {
+ // If > 0, realtime when frame should be shown, in mp_time_ns() units.
+ // If 0, present immediately.
+ int64_t pts;
+ // Approximate frame duration, in ns.
+ int duration;
+ // Realtime of estimated distance between 2 vsync events.
+ double vsync_interval;
+ // "ideal" display time within the vsync
+ double vsync_offset;
+ // "ideal" frame duration (can be different from num_vsyncs*vsync_interval
+ // up to a vsync) - valid for the entire frame, i.e. not changed for repeats
+ double ideal_frame_duration;
+ // "ideal" frame vsync point relative to the pts
+ double ideal_frame_vsync;
+ // "ideal" frame duration relative to the pts
+ double ideal_frame_vsync_duration;
+ // how often the frame will be repeated (does not include OSD redraws)
+ int num_vsyncs;
+ // Set if the current frame is repeated from the previous. It's guaranteed
+ // that the current is the same as the previous one, even if the image
+ // pointer is different.
+ // The repeat flag is set if exactly the same frame should be rendered
+ // again (and the OSD does not need to be redrawn).
+ // A repeat frame can be redrawn, in which case repeat==redraw==true, and
+ // OSD should be updated.
+ bool redraw, repeat;
+ // The frame is not in movement - e.g. redrawing while paused.
+ bool still;
+ // Frames are output as fast as possible, with implied vsync blocking.
+ bool display_synced;
+ // Dropping the frame is allowed if the VO is behind.
+ bool can_drop;
+ // The current frame to be drawn.
+ // Warning: When OSD should be redrawn in --force-window --idle mode, this
+ // can be NULL. The VO should draw a black background, OSD on top.
+ struct mp_image *current;
+ // List of future images, starting with the current one. This does not
+ // care about repeated frames - it simply contains the next real frames.
+ // vo_set_queue_params() sets how many future frames this should include.
+ // The actual number of frames delivered to the VO can be lower.
+ // frames[0] is current, frames[1] is the next frame.
+ // Note that some future frames may never be sent as current frame to the
+ // VO if frames are dropped.
+ int num_frames;
+ struct mp_image *frames[VO_MAX_REQ_FRAMES];
+ // Speed unadjusted, approximate frame duration inferred from past frames
+ double approx_duration;
+ // ID for frames[0] (== current). If current==NULL, the number is
+ // meaningless. Otherwise, it's an unique ID for the frame. The ID for
+ // a frame is guaranteed not to change (instant redraws will use the same
+ // ID). frames[n] has the ID frame_id+n, with the guarantee that frame
+ // drops or reconfigs will keep the guarantee.
+ // The ID is never 0 (unless num_frames==0). IDs are strictly monotonous.
+ uint64_t frame_id;
+};
+
+// Presentation feedback. See get_vsync() for how backends should fill this
+// struct.
+struct vo_vsync_info {
+ // mp_time_ns() timestamp at which the last queued frame will likely be
+ // displayed (this is in the future, unless the frame is instantly output).
+ // 0 or lower if unset or unsupported.
+ // This implies the latency of the output.
+ int64_t last_queue_display_time;
+
+ // Time between 2 vsync events in nanoseconds. The difference should be the
+ // from 2 times sampled from the same reference point (it should not be the
+ // difference between e.g. the end of scanout and the start of the next one;
+ // it must be continuous).
+ // -1 if unsupported.
+ // 0 if supported, but no value available yet. It is assumed that the value
+ // becomes available after enough swap_buffers() calls were done.
+ // >0 values are taken for granted. Very bad things will happen if it's
+ // inaccurate.
+ int64_t vsync_duration;
+
+ // Number of skipped physical vsyncs at some point in time. Typically, this
+ // value is some time in the past by an offset that equals to the latency.
+ // This value is reset and newly sampled at every swap_buffers() call.
+ // This can be used to detect delayed frames iff you try to call
+ // swap_buffers() for every physical vsync.
+ // -1 if unset or unsupported.
+ int64_t skipped_vsyncs;
+};
+
+struct vo_driver {
+ // Encoding functionality, which can be invoked via --o only.
+ bool encode;
+
+ // This requires waiting for a VO_EVENT_INITIAL_UNBLOCK event before the
+ // first frame can be sent. Doing vo_reconfig*() calls is allowed though.
+ // Encode mode uses this, the core uses vo_is_ready_for_frame() to
+ // implicitly check for this.
+ bool initially_blocked;
+
+ // VO_CAP_* bits
+ int caps;
+
+ // Disable video timing, push frames as quickly as possible, never redraw.
+ bool untimed;
+
+ // The VO is responsible for freeing frames.
+ bool frame_owner;
+
+ const char *name;
+ const char *description;
+
+ /*
+ * returns: zero on successful initialization, non-zero on error.
+ */
+ int (*preinit)(struct vo *vo);
+
+ /*
+ * Whether the given image format is supported and config() will succeed.
+ * format: one of IMGFMT_*
+ * returns: 0 on not supported, otherwise 1
+ */
+ int (*query_format)(struct vo *vo, int format);
+
+ /*
+ * Initialize or reconfigure the display driver.
+ * params: video parameters, like pixel format and frame size
+ * returns: < 0 on error, >= 0 on success
+ */
+ int (*reconfig)(struct vo *vo, struct mp_image_params *params);
+
+ /*
+ * Like reconfig(), but provides the whole mp_image for which the change is
+ * required. (The image doesn't have to have real data.)
+ */
+ int (*reconfig2)(struct vo *vo, struct mp_image *img);
+
+ /*
+ * Control interface
+ */
+ int (*control)(struct vo *vo, uint32_t request, void *data);
+
+ /*
+ * lavc callback for direct rendering
+ *
+ * Optional. To make implementation easier, the callback is always run on
+ * the VO thread. The returned mp_image's destructor callback is also called
+ * on the VO thread, even if it's actually unref'ed from another thread.
+ *
+ * It is guaranteed that the last reference to an image is destroyed before
+ * ->uninit is called (except it's not - libmpv screenshots can hold the
+ * reference longer, fuck).
+ *
+ * The allocated image - or a part of it, can be passed to draw_frame(). The
+ * point of this mechanism is that the decoder directly renders to GPU
+ * staging memory, to avoid a memcpy on frame upload. But this is not a
+ * guarantee. A filter could change the data pointers or return a newly
+ * allocated image. It's even possible that only 1 plane uses the buffer
+ * allocated by the get_image function. The VO has to check for this.
+ *
+ * stride_align is always a value >=1. The stride values of the returned
+ * image must be divisible by this value. This may be a non power of two.
+ *
+ * flags is a combination of VO_DR_FLAG_* flags.
+ *
+ * Currently, the returned image must have exactly 1 AVBufferRef set, for
+ * internal implementation simplicity.
+ *
+ * returns: an allocated, refcounted image; if NULL is returned, the caller
+ * will silently fallback to a default allocator
+ */
+ struct mp_image *(*get_image)(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+ /*
+ * Thread-safe variant of get_image. Set at most one of these callbacks.
+ * This excludes _all_ synchronization magic. The only guarantee is that
+ * vo_driver.uninit is not called before this function returns.
+ */
+ struct mp_image *(*get_image_ts)(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+ /* Render the given frame. Note that this is also called when repeating
+ * or redrawing frames.
+ *
+ * frame is freed by the caller if the callee did not assume ownership
+ * of the frames, but in any case the callee can still modify the
+ * contained data and references.
+ */
+ void (*draw_frame)(struct vo *vo, struct vo_frame *frame);
+
+ /*
+ * Blit/Flip buffer to the screen. Must be called after each frame!
+ */
+ void (*flip_page)(struct vo *vo);
+
+ /*
+ * Return presentation feedback. The implementation should not touch fields
+ * it doesn't support; the info fields are preinitialized to neutral values.
+ * Usually called once after flip_page(), but can be called any time.
+ * The values returned by this are always relative to the last flip_page()
+ * call.
+ */
+ void (*get_vsync)(struct vo *vo, struct vo_vsync_info *info);
+
+ /* These optional callbacks can be provided if the GUI framework used by
+ * the VO requires entering a message loop for receiving events and does
+ * not call vo_wakeup() from a separate thread when there are new events.
+ *
+ * wait_events() will wait for new events, until the timeout expires, or the
+ * function is interrupted. wakeup() is used to possibly interrupt the
+ * event loop (wakeup() itself must be thread-safe, and not call any other
+ * VO functions; it's the only vo_driver function with this requirement).
+ * wakeup() should behave like a binary semaphore; if wait_events() is not
+ * being called while wakeup() is, the next wait_events() call should exit
+ * immediately.
+ */
+ void (*wakeup)(struct vo *vo);
+ void (*wait_events)(struct vo *vo, int64_t until_time_ns);
+
+ /*
+ * Closes driver. Should restore the original state of the system.
+ */
+ void (*uninit)(struct vo *vo);
+
+ // Size of private struct for automatic allocation (0 doesn't allocate)
+ int priv_size;
+
+ // If not NULL, it's copied into the newly allocated private struct.
+ const void *priv_defaults;
+
+ // List of options to parse into priv struct (requires priv_size to be set)
+ // This will register them as global options (with options_prefix), and
+ // copy the current value at VO creation time to the priv struct.
+ const struct m_option *options;
+
+ // All options in the above array are prefixed with this string. (It's just
+ // for convenience and makes no difference in semantics.)
+ const char *options_prefix;
+
+ // Registers global options that go to a separate options struct.
+ const struct m_sub_options *global_opts;
+};
+
+struct vo {
+ const struct vo_driver *driver;
+ struct mp_log *log; // Using e.g. "[vo/vdpau]" as prefix
+ void *priv;
+ struct mpv_global *global;
+ struct vo_x11_state *x11;
+ struct vo_w32_state *w32;
+ struct vo_wayland_state *wl;
+ struct vo_android_state *android;
+ struct vo_drm_state *drm;
+ struct mp_hwdec_devices *hwdec_devs;
+ struct input_ctx *input_ctx;
+ struct osd_state *osd;
+ struct encode_lavc_context *encode_lavc_ctx;
+ struct vo_internal *in;
+ struct vo_extra extra;
+
+ // --- The following fields are generally only changed during initialization.
+
+ bool probing;
+
+ // --- The following fields are only changed with vo_reconfig(), and can
+ // be accessed unsynchronized (read-only).
+
+ int config_ok; // Last config call was successful?
+ struct mp_image_params *params; // Configured parameters (as in vo_reconfig)
+
+ // --- The following fields can be accessed only by the VO thread, or from
+ // anywhere _if_ the VO thread is suspended (use vo->dispatch).
+
+ struct m_config_cache *opts_cache; // cache for ->opts
+ struct mp_vo_opts *opts;
+ struct m_config_cache *gl_opts_cache;
+ struct m_config_cache *eq_opts_cache;
+
+ bool want_redraw; // redraw as soon as possible
+
+ // current window state
+ int dwidth;
+ int dheight;
+ float monitor_par;
+};
+
+struct mpv_global;
+struct vo *init_best_video_out(struct mpv_global *global, struct vo_extra *ex);
+int vo_reconfig(struct vo *vo, struct mp_image_params *p);
+int vo_reconfig2(struct vo *vo, struct mp_image *img);
+
+int vo_control(struct vo *vo, int request, void *data);
+void vo_control_async(struct vo *vo, int request, void *data);
+bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts);
+void vo_queue_frame(struct vo *vo, struct vo_frame *frame);
+void vo_wait_frame(struct vo *vo);
+bool vo_still_displaying(struct vo *vo);
+bool vo_has_frame(struct vo *vo);
+void vo_redraw(struct vo *vo);
+bool vo_want_redraw(struct vo *vo);
+void vo_seek_reset(struct vo *vo);
+void vo_destroy(struct vo *vo);
+void vo_set_paused(struct vo *vo, bool paused);
+int64_t vo_get_drop_count(struct vo *vo);
+void vo_increment_drop_count(struct vo *vo, int64_t n);
+int64_t vo_get_delayed_count(struct vo *vo);
+void vo_query_formats(struct vo *vo, uint8_t *list);
+void vo_event(struct vo *vo, int event);
+int vo_query_and_reset_events(struct vo *vo, int events);
+struct mp_image *vo_get_current_frame(struct vo *vo);
+void vo_set_queue_params(struct vo *vo, int64_t offset_ns, int num_req_frames);
+int vo_get_num_req_frames(struct vo *vo);
+double vo_get_vsync_interval(struct vo *vo);
+double vo_get_estimated_vsync_interval(struct vo *vo);
+double vo_get_estimated_vsync_jitter(struct vo *vo);
+double vo_get_display_fps(struct vo *vo);
+double vo_get_delay(struct vo *vo);
+void vo_discard_timing_info(struct vo *vo);
+struct vo_frame *vo_get_current_vo_frame(struct vo *vo);
+struct mp_image *vo_get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+void vo_wakeup(struct vo *vo);
+void vo_wait_default(struct vo *vo, int64_t until_time);
+
+struct mp_keymap {
+ int from;
+ int to;
+};
+int lookup_keymap_table(const struct mp_keymap *map, int key);
+
+struct mp_osd_res;
+void vo_get_src_dst_rects(struct vo *vo, struct mp_rect *out_src,
+ struct mp_rect *out_dst, struct mp_osd_res *out_osd);
+
+struct vo_frame *vo_frame_ref(struct vo_frame *frame);
+
+struct mp_image_params vo_get_current_params(struct vo *vo);
+
+#endif /* MPLAYER_VIDEO_OUT_H */
diff --git a/video/out/vo_caca.c b/video/out/vo_caca.c
new file mode 100644
index 0000000..0625de0
--- /dev/null
+++ b/video/out/vo_caca.c
@@ -0,0 +1,314 @@
+/*
+ * video output driver for libcaca
+ *
+ * by Pigeon <pigeon@pigeond.net>
+ *
+ * Some functions/codes/ideas are from x11 and aalib vo
+ *
+ * TODO: support draw_alpha?
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <assert.h>
+#include <caca.h>
+
+#include "config.h"
+#include "vo.h"
+#include "video/mp_image.h"
+
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "common/msg.h"
+#include "input/input.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+struct priv {
+ caca_canvas_t *canvas;
+ caca_display_t *display;
+ caca_dither_t *dither;
+ uint8_t *dither_buffer;
+ const char *dither_antialias;
+ const char *dither_charset;
+ const char *dither_color;
+ const char *dither_algo;
+
+ /* image infos */
+ int image_format;
+ int image_width;
+ int image_height;
+
+ int screen_w, screen_h;
+};
+
+/* We want 24bpp always for now */
+static const unsigned int bpp = 24;
+static const unsigned int depth = 3;
+static const unsigned int rmask = 0xff0000;
+static const unsigned int gmask = 0x00ff00;
+static const unsigned int bmask = 0x0000ff;
+static const unsigned int amask = 0;
+
+static int resize(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ priv->screen_w = caca_get_canvas_width(priv->canvas);
+ priv->screen_h = caca_get_canvas_height(priv->canvas);
+
+ caca_free_dither(priv->dither);
+ talloc_free(priv->dither_buffer);
+
+ priv->dither = caca_create_dither(bpp, priv->image_width, priv->image_height,
+ depth * priv->image_width,
+ rmask, gmask, bmask, amask);
+ if (priv->dither == NULL) {
+ MP_FATAL(vo, "caca_create_dither failed!\n");
+ return -1;
+ }
+ priv->dither_buffer =
+ talloc_array(NULL, uint8_t, depth * priv->image_width * priv->image_height);
+
+ /* Default libcaca features */
+ caca_set_dither_antialias(priv->dither, priv->dither_antialias);
+ caca_set_dither_charset(priv->dither, priv->dither_charset);
+ caca_set_dither_color(priv->dither, priv->dither_color);
+ caca_set_dither_algorithm(priv->dither, priv->dither_algo);
+
+ return 0;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *priv = vo->priv;
+ priv->image_height = params->h;
+ priv->image_width = params->w;
+ priv->image_format = params->imgfmt;
+
+ return resize(vo);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *priv = vo->priv;
+ struct mp_image *mpi = frame->current;
+ if (!mpi)
+ return;
+ memcpy_pic(priv->dither_buffer, mpi->planes[0], priv->image_width * depth, priv->image_height,
+ priv->image_width * depth, mpi->stride[0]);
+ caca_dither_bitmap(priv->canvas, 0, 0, priv->screen_w, priv->screen_h, priv->dither, priv->dither_buffer);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ caca_refresh_display(priv->display);
+}
+
+static void set_next_str(const char * const *list, const char **str,
+ const char **msg)
+{
+ int ind;
+ for (ind = 0; list[ind]; ind += 2) {
+ if (strcmp(list[ind], *str) == 0) {
+ if (list[ind + 2] == NULL)
+ ind = -2;
+ *str = list[ind + 2];
+ *msg = list[ind + 3];
+ return;
+ }
+ }
+
+ *str = list[0];
+ *msg = list[1];
+}
+
+static const struct mp_keymap keysym_map[] = {
+ {CACA_KEY_RETURN, MP_KEY_ENTER}, {CACA_KEY_ESCAPE, MP_KEY_ESC},
+ {CACA_KEY_UP, MP_KEY_UP}, {CACA_KEY_DOWN, MP_KEY_DOWN},
+ {CACA_KEY_LEFT, MP_KEY_LEFT}, {CACA_KEY_RIGHT, MP_KEY_RIGHT},
+ {CACA_KEY_PAGEUP, MP_KEY_PAGE_UP}, {CACA_KEY_PAGEDOWN, MP_KEY_PAGE_DOWN},
+ {CACA_KEY_HOME, MP_KEY_HOME}, {CACA_KEY_END, MP_KEY_END},
+ {CACA_KEY_INSERT, MP_KEY_INSERT}, {CACA_KEY_DELETE, MP_KEY_DELETE},
+ {CACA_KEY_BACKSPACE, MP_KEY_BACKSPACE}, {CACA_KEY_TAB, MP_KEY_TAB},
+ {CACA_KEY_PAUSE, MP_KEY_PAUSE},
+ {CACA_KEY_F1, MP_KEY_F+1}, {CACA_KEY_F2, MP_KEY_F+2},
+ {CACA_KEY_F3, MP_KEY_F+3}, {CACA_KEY_F4, MP_KEY_F+4},
+ {CACA_KEY_F5, MP_KEY_F+5}, {CACA_KEY_F6, MP_KEY_F+6},
+ {CACA_KEY_F7, MP_KEY_F+7}, {CACA_KEY_F8, MP_KEY_F+8},
+ {CACA_KEY_F9, MP_KEY_F+9}, {CACA_KEY_F10, MP_KEY_F+10},
+ {CACA_KEY_F11, MP_KEY_F+11}, {CACA_KEY_F12, MP_KEY_F+12},
+ {CACA_KEY_F13, MP_KEY_F+13}, {CACA_KEY_F14, MP_KEY_F+14},
+ {CACA_KEY_F15, MP_KEY_F+15},
+ {0, 0}
+};
+
+static void check_events(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+
+ caca_event_t cev;
+ while (caca_get_event(priv->display, CACA_EVENT_ANY, &cev, 0)) {
+
+ switch (cev.type) {
+ case CACA_EVENT_RESIZE:
+ caca_refresh_display(priv->display);
+ resize(vo);
+ break;
+ case CACA_EVENT_QUIT:
+ mp_input_put_key(vo->input_ctx, MP_KEY_CLOSE_WIN);
+ break;
+ case CACA_EVENT_MOUSE_MOTION:
+ mp_input_set_mouse_pos(vo->input_ctx, cev.data.mouse.x, cev.data.mouse.y);
+ break;
+ case CACA_EVENT_MOUSE_PRESS:
+ mp_input_put_key(vo->input_ctx,
+ (MP_MBTN_BASE + cev.data.mouse.button - 1) | MP_KEY_STATE_DOWN);
+ break;
+ case CACA_EVENT_MOUSE_RELEASE:
+ mp_input_put_key(vo->input_ctx,
+ (MP_MBTN_BASE + cev.data.mouse.button - 1) | MP_KEY_STATE_UP);
+ break;
+ case CACA_EVENT_KEY_PRESS:
+ {
+ int key = cev.data.key.ch;
+ int mpkey = lookup_keymap_table(keysym_map, key);
+ const char *msg_name;
+
+ if (mpkey)
+ mp_input_put_key(vo->input_ctx, mpkey);
+ else
+ switch (key) {
+ case 'd':
+ case 'D':
+ /* Toggle dithering algorithm */
+ set_next_str(caca_get_dither_algorithm_list(priv->dither),
+ &priv->dither_algo, &msg_name);
+ caca_set_dither_algorithm(priv->dither, priv->dither_algo);
+ break;
+
+ case 'a':
+ case 'A':
+ /* Toggle antialiasing method */
+ set_next_str(caca_get_dither_antialias_list(priv->dither),
+ &priv->dither_antialias, &msg_name);
+ caca_set_dither_antialias(priv->dither, priv->dither_antialias);
+ break;
+
+ case 'h':
+ case 'H':
+ /* Toggle charset method */
+ set_next_str(caca_get_dither_charset_list(priv->dither),
+ &priv->dither_charset, &msg_name);
+ caca_set_dither_charset(priv->dither, priv->dither_charset);
+ break;
+
+ case 'c':
+ case 'C':
+ /* Toggle color method */
+ set_next_str(caca_get_dither_color_list(priv->dither),
+ &priv->dither_color, &msg_name);
+ caca_set_dither_color(priv->dither, priv->dither_color);
+ break;
+
+ default:
+ if (key <= 255)
+ mp_input_put_key(vo->input_ctx, key);
+ break;
+ }
+ }
+ }
+ }
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ caca_free_dither(priv->dither);
+ priv->dither = NULL;
+ talloc_free(priv->dither_buffer);
+ priv->dither_buffer = NULL;
+ caca_free_display(priv->display);
+ caca_free_canvas(priv->canvas);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+
+ priv->dither_antialias = "default";
+ priv->dither_charset = "default";
+ priv->dither_color = "default";
+ priv->dither_algo = "none";
+
+ priv->canvas = caca_create_canvas(0, 0);
+ if (priv->canvas == NULL) {
+ MP_ERR(vo, "failed to create canvas\n");
+ return ENOSYS;
+ }
+
+ priv->display = caca_create_display(priv->canvas);
+
+ if (priv->display == NULL) {
+ MP_ERR(vo, "failed to create display\n");
+ caca_free_canvas(priv->canvas);
+ return ENOSYS;
+ }
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT_BGR24;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *priv = vo->priv;
+ switch (request) {
+ case VOCTRL_CHECK_EVENTS:
+ check_events(vo);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ caca_set_display_title(priv->display, (char *)data);
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+const struct vo_driver video_out_caca = {
+ .name = "caca",
+ .description = "libcaca",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_direct3d.c b/video/out/vo_direct3d.c
new file mode 100644
index 0000000..16936bb
--- /dev/null
+++ b/video/out/vo_direct3d.c
@@ -0,0 +1,1247 @@
+/*
+ * Copyright (c) 2008 Georgi Petrov (gogothebee) <gogothebee@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <d3d9.h>
+#include <inttypes.h>
+#include <limits.h>
+#include "config.h"
+#include "options/options.h"
+#include "options/m_option.h"
+#include "sub/draw_bmp.h"
+#include "mpv_talloc.h"
+#include "vo.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "video/img_format.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "w32_common.h"
+#include "sub/osd.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define DEVTYPE D3DDEVTYPE_HAL
+//#define DEVTYPE D3DDEVTYPE_REF
+
+#define D3DFVF_OSD_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)
+
+typedef struct {
+ float x, y, z;
+ float tu, tv;
+} vertex_osd;
+
+struct d3dtex {
+ // user-requested size
+ int w, h;
+ // allocated texture size
+ int tex_w, tex_h;
+ // D3DPOOL_SYSTEMMEM (or others) texture:
+ // - can be locked in order to write (and even read) data
+ // - can _not_ (probably) be used as texture for rendering
+ // This is always non-NULL if d3dtex_allocate succeeds.
+ IDirect3DTexture9 *system;
+ // D3DPOOL_DEFAULT texture:
+ // - can't be locked (Probably.)
+ // - must be used for rendering
+ // This can be NULL if the system one can be both locked and mapped.
+ IDirect3DTexture9 *device;
+};
+
+#define MAX_OSD_RECTS 64
+
+/* Global variables "priv" structure. I try to keep their count low.
+ */
+typedef struct d3d_priv {
+ struct mp_log *log;
+
+ bool opt_disable_texture_align;
+ // debugging
+ bool opt_force_power_of_2;
+ int opt_texture_memory;
+ bool opt_swap_discard;
+ bool opt_exact_backbuffer;
+
+ struct vo *vo;
+
+ bool have_image;
+ double osd_pts;
+
+ D3DLOCKED_RECT locked_rect; /**< The locked offscreen surface */
+ RECT fs_movie_rect; /**< Rect (upscaled) of the movie when displayed
+ in fullscreen */
+ RECT fs_panscan_rect; /**< PanScan source surface cropping in
+ fullscreen */
+ int src_width; /**< Source (movie) width */
+ int src_height; /**< Source (movie) height */
+ struct mp_osd_res osd_res;
+ int image_format; /**< mplayer image format */
+ struct mp_image_params params;
+
+ D3DFORMAT movie_src_fmt; /**< Movie colorspace format (depends on
+ the movie's codec) */
+ D3DFORMAT desktop_fmt; /**< Desktop (screen) colorspace format.
+ Usually XRGB */
+
+ HANDLE d3d9_dll; /**< d3d9 Library HANDLE */
+ IDirect3D9 * (WINAPI *pDirect3DCreate9)(UINT); /**< pointer to Direct3DCreate9 function */
+
+ LPDIRECT3D9 d3d_handle; /**< Direct3D Handle */
+ LPDIRECT3DDEVICE9 d3d_device; /**< The Direct3D Adapter */
+ bool d3d_in_scene; /**< BeginScene was called, EndScene not */
+ IDirect3DSurface9 *d3d_surface; /**< Offscreen Direct3D Surface. MPlayer
+ renders inside it. Uses colorspace
+ priv->movie_src_fmt */
+ IDirect3DSurface9 *d3d_backbuf; /**< Video card's back buffer (used to
+ display next frame) */
+ int cur_backbuf_width; /**< Current backbuffer width */
+ int cur_backbuf_height; /**< Current backbuffer height */
+ int device_caps_power2_only; /**< 1 = texture sizes have to be power 2
+ 0 = texture sizes can be anything */
+ int device_caps_square_only; /**< 1 = textures have to be square
+ 0 = textures do not have to be square */
+ int device_texture_sys; /**< 1 = device can texture from system memory
+ 0 = device requires shadow */
+ int max_texture_width; /**< from the device capabilities */
+ int max_texture_height; /**< from the device capabilities */
+
+ D3DMATRIX d3d_colormatrix;
+
+ struct mp_draw_sub_cache *osd_cache;
+ struct d3dtex osd_texture;
+ int osd_num_vertices;
+ vertex_osd osd_vertices[MAX_OSD_RECTS * 6];
+} d3d_priv;
+
+struct fmt_entry {
+ const unsigned int mplayer_fmt; /**< Given by MPlayer */
+ const D3DFORMAT fourcc; /**< Required by D3D's test function */
+};
+
+/* Map table from reported MPlayer format to the required
+ fourcc. This is needed to perform the format query. */
+
+static const struct fmt_entry fmt_table[] = {
+ // planar YUV
+ {IMGFMT_420P, MAKEFOURCC('Y','V','1','2')},
+ {IMGFMT_420P, MAKEFOURCC('I','4','2','0')},
+ {IMGFMT_420P, MAKEFOURCC('I','Y','U','V')},
+ {IMGFMT_NV12, MAKEFOURCC('N','V','1','2')},
+ // packed YUV
+ {IMGFMT_UYVY, D3DFMT_UYVY},
+ // packed RGB
+ {IMGFMT_BGR0, D3DFMT_X8R8G8B8},
+ {IMGFMT_RGB0, D3DFMT_X8B8G8R8},
+ {IMGFMT_BGR24, D3DFMT_R8G8B8}, //untested
+ {IMGFMT_RGB565, D3DFMT_R5G6B5},
+ {0},
+};
+
+
+static bool resize_d3d(d3d_priv *priv);
+static void uninit(struct vo *vo);
+static void flip_page(struct vo *vo);
+static mp_image_t *get_window_screenshot(d3d_priv *priv);
+static void draw_osd(struct vo *vo);
+static bool change_d3d_backbuffer(d3d_priv *priv);
+
+static void d3d_matrix_identity(D3DMATRIX *m)
+{
+ memset(m, 0, sizeof(D3DMATRIX));
+ m->_11 = m->_22 = m->_33 = m->_44 = 1.0f;
+}
+
+static void d3d_matrix_ortho(D3DMATRIX *m, float left, float right,
+ float bottom, float top)
+{
+ d3d_matrix_identity(m);
+ m->_11 = 2.0f / (right - left);
+ m->_22 = 2.0f / (top - bottom);
+ m->_33 = 1.0f;
+ m->_41 = -(right + left) / (right - left);
+ m->_42 = -(top + bottom) / (top - bottom);
+ m->_43 = 0;
+ m->_44 = 1.0f;
+}
+
+/****************************************************************************
+ * *
+ * *
+ * *
+ * Direct3D specific implementation functions *
+ * *
+ * *
+ * *
+ ****************************************************************************/
+
+static bool d3d_begin_scene(d3d_priv *priv)
+{
+ if (!priv->d3d_in_scene) {
+ if (FAILED(IDirect3DDevice9_BeginScene(priv->d3d_device))) {
+ MP_ERR(priv, "BeginScene failed.\n");
+ return false;
+ }
+ priv->d3d_in_scene = true;
+ }
+ return true;
+}
+
+/** @brief Calculate scaled fullscreen movie rectangle with
+ * preserved aspect ratio.
+ */
+static void calc_fs_rect(d3d_priv *priv)
+{
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ vo_get_src_dst_rects(priv->vo, &src_rect, &dst_rect, &priv->osd_res);
+
+ priv->fs_movie_rect.left = dst_rect.x0;
+ priv->fs_movie_rect.right = dst_rect.x1;
+ priv->fs_movie_rect.top = dst_rect.y0;
+ priv->fs_movie_rect.bottom = dst_rect.y1;
+ priv->fs_panscan_rect.left = src_rect.x0;
+ priv->fs_panscan_rect.right = src_rect.x1;
+ priv->fs_panscan_rect.top = src_rect.y0;
+ priv->fs_panscan_rect.bottom = src_rect.y1;
+}
+
+// Adjust the texture size *width/*height to fit the requirements of the D3D
+// device. The texture size is only increased.
+static void d3d_fix_texture_size(d3d_priv *priv, int *width, int *height)
+{
+ int tex_width = *width;
+ int tex_height = *height;
+
+ // avoid nasty special cases with 0-sized textures and texture sizes
+ tex_width = MPMAX(tex_width, 1);
+ tex_height = MPMAX(tex_height, 1);
+
+ if (priv->device_caps_power2_only) {
+ tex_width = 1;
+ tex_height = 1;
+ while (tex_width < *width) tex_width <<= 1;
+ while (tex_height < *height) tex_height <<= 1;
+ }
+ if (priv->device_caps_square_only)
+ /* device only supports square textures */
+ tex_width = tex_height = MPMAX(tex_width, tex_height);
+ // better round up to a multiple of 16
+ if (!priv->opt_disable_texture_align) {
+ tex_width = (tex_width + 15) & ~15;
+ tex_height = (tex_height + 15) & ~15;
+ }
+
+ *width = tex_width;
+ *height = tex_height;
+}
+
+static void d3dtex_release(d3d_priv *priv, struct d3dtex *tex)
+{
+ if (tex->system)
+ IDirect3DTexture9_Release(tex->system);
+ tex->system = NULL;
+
+ if (tex->device)
+ IDirect3DTexture9_Release(tex->device);
+ tex->device = NULL;
+
+ tex->tex_w = tex->tex_h = 0;
+}
+
+static bool d3dtex_allocate(d3d_priv *priv, struct d3dtex *tex, D3DFORMAT fmt,
+ int w, int h)
+{
+ d3dtex_release(priv, tex);
+
+ tex->w = w;
+ tex->h = h;
+
+ int tw = w, th = h;
+ d3d_fix_texture_size(priv, &tw, &th);
+
+ bool use_sh = !priv->device_texture_sys;
+ int memtype = D3DPOOL_SYSTEMMEM;
+ switch (priv->opt_texture_memory) {
+ case 1: memtype = D3DPOOL_MANAGED; use_sh = false; break;
+ case 2: memtype = D3DPOOL_DEFAULT; use_sh = false; break;
+ case 3: memtype = D3DPOOL_DEFAULT; use_sh = true; break;
+ case 4: memtype = D3DPOOL_SCRATCH; use_sh = true; break;
+ }
+
+ if (FAILED(IDirect3DDevice9_CreateTexture(priv->d3d_device, tw, th, 1,
+ D3DUSAGE_DYNAMIC, fmt, memtype, &tex->system, NULL)))
+ {
+ MP_ERR(priv, "Allocating %dx%d texture in system RAM failed.\n", w, h);
+ goto error_exit;
+ }
+
+ if (use_sh) {
+ if (FAILED(IDirect3DDevice9_CreateTexture(priv->d3d_device, tw, th, 1,
+ D3DUSAGE_DYNAMIC, fmt, D3DPOOL_DEFAULT, &tex->device, NULL)))
+ {
+ MP_ERR(priv, "Allocating %dx%d texture in video RAM failed.\n", w, h);
+ goto error_exit;
+ }
+ }
+
+ tex->tex_w = tw;
+ tex->tex_h = th;
+
+ return true;
+
+error_exit:
+ d3dtex_release(priv, tex);
+ return false;
+}
+
+static IDirect3DBaseTexture9 *d3dtex_get_render_texture(d3d_priv *priv,
+ struct d3dtex *tex)
+{
+ return (IDirect3DBaseTexture9 *)(tex->device ? tex->device : tex->system);
+}
+
+// Copy system texture contents to device texture.
+static bool d3dtex_update(d3d_priv *priv, struct d3dtex *tex)
+{
+ if (!tex->device)
+ return true;
+ return !FAILED(IDirect3DDevice9_UpdateTexture(priv->d3d_device,
+ (IDirect3DBaseTexture9 *)tex->system,
+ (IDirect3DBaseTexture9 *)tex->device));
+}
+
+static void d3d_unlock_video_objects(d3d_priv *priv)
+{
+ if (priv->locked_rect.pBits) {
+ if (FAILED(IDirect3DSurface9_UnlockRect(priv->d3d_surface)))
+ MP_VERBOSE(priv, "Unlocking video objects failed.\n");
+ }
+ priv->locked_rect.pBits = NULL;
+}
+
+// Free video surface/textures, etc.
+static void d3d_destroy_video_objects(d3d_priv *priv)
+{
+ d3d_unlock_video_objects(priv);
+
+ if (priv->d3d_surface)
+ IDirect3DSurface9_Release(priv->d3d_surface);
+ priv->d3d_surface = NULL;
+}
+
+/** @brief Destroy D3D Offscreen and Backbuffer surfaces.
+ */
+static void destroy_d3d_surfaces(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "destroy_d3d_surfaces called.\n");
+
+ d3d_destroy_video_objects(priv);
+ d3dtex_release(priv, &priv->osd_texture);
+
+ if (priv->d3d_backbuf)
+ IDirect3DSurface9_Release(priv->d3d_backbuf);
+ priv->d3d_backbuf = NULL;
+
+ priv->d3d_in_scene = false;
+}
+
+// Allocate video surface.
+static bool d3d_configure_video_objects(d3d_priv *priv)
+{
+ assert(priv->image_format != 0);
+
+ if (!priv->d3d_surface &&
+ FAILED(IDirect3DDevice9_CreateOffscreenPlainSurface(
+ priv->d3d_device, priv->src_width, priv->src_height,
+ priv->movie_src_fmt, D3DPOOL_DEFAULT, &priv->d3d_surface, NULL)))
+ {
+ MP_ERR(priv, "Allocating offscreen surface failed.\n");
+ return false;
+ }
+
+ return true;
+}
+
+// Recreate and initialize D3D objects if necessary. The amount of work that
+// needs to be done can be quite different: it could be that full initialization
+// is required, or that some objects need to be created, or that nothing is
+// done.
+static bool create_d3d_surfaces(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "create_d3d_surfaces called.\n");
+
+ if (!priv->d3d_backbuf &&
+ FAILED(IDirect3DDevice9_GetBackBuffer(priv->d3d_device, 0, 0,
+ D3DBACKBUFFER_TYPE_MONO,
+ &priv->d3d_backbuf))) {
+ MP_ERR(priv, "Allocating backbuffer failed.\n");
+ return 0;
+ }
+
+ if (!d3d_configure_video_objects(priv))
+ return 0;
+
+ /* setup default renderstate */
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHAFUNC, D3DCMP_GREATER);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHAREF, (DWORD)0x0);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_LIGHTING, FALSE);
+
+ // we use up to 3 samplers for up to 3 YUV planes
+ // TODO
+ /*
+ for (int n = 0; n < 3; n++) {
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_MINFILTER,
+ D3DTEXF_LINEAR);
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_MAGFILTER,
+ D3DTEXF_LINEAR);
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_ADDRESSU,
+ D3DTADDRESS_CLAMP);
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_ADDRESSV,
+ D3DTADDRESS_CLAMP);
+ }
+ */
+
+ return 1;
+}
+
+static bool init_d3d(d3d_priv *priv)
+{
+ D3DDISPLAYMODE disp_mode;
+ D3DCAPS9 disp_caps;
+ DWORD texture_caps;
+ DWORD dev_caps;
+
+ priv->d3d_handle = priv->pDirect3DCreate9(D3D_SDK_VERSION);
+ if (!priv->d3d_handle) {
+ MP_ERR(priv, "Initializing Direct3D failed.\n");
+ return false;
+ }
+
+ if (FAILED(IDirect3D9_GetAdapterDisplayMode(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ &disp_mode))) {
+ MP_ERR(priv, "Reading display mode failed.\n");
+ return false;
+ }
+
+ priv->desktop_fmt = disp_mode.Format;
+ priv->cur_backbuf_width = disp_mode.Width;
+ priv->cur_backbuf_height = disp_mode.Height;
+
+ MP_VERBOSE(priv, "Setting backbuffer dimensions to (%dx%d).\n",
+ disp_mode.Width, disp_mode.Height);
+
+ if (FAILED(IDirect3D9_GetDeviceCaps(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE,
+ &disp_caps)))
+ {
+ MP_ERR(priv, "Reading display capabilities failed.\n");
+ return false;
+ }
+
+ /* Store relevant information reguarding caps of device */
+ texture_caps = disp_caps.TextureCaps;
+ dev_caps = disp_caps.DevCaps;
+ priv->device_caps_power2_only = (texture_caps & D3DPTEXTURECAPS_POW2) &&
+ !(texture_caps & D3DPTEXTURECAPS_NONPOW2CONDITIONAL);
+ priv->device_caps_square_only = texture_caps & D3DPTEXTURECAPS_SQUAREONLY;
+ priv->device_texture_sys = dev_caps & D3DDEVCAPS_TEXTURESYSTEMMEMORY;
+ priv->max_texture_width = disp_caps.MaxTextureWidth;
+ priv->max_texture_height = disp_caps.MaxTextureHeight;
+
+ if (priv->opt_force_power_of_2)
+ priv->device_caps_power2_only = 1;
+
+ if (FAILED(IDirect3D9_CheckDeviceFormat(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE,
+ priv->desktop_fmt,
+ D3DUSAGE_DYNAMIC | D3DUSAGE_QUERY_FILTER,
+ D3DRTYPE_TEXTURE,
+ D3DFMT_A8R8G8B8)))
+ {
+ MP_ERR(priv, "OSD texture format not supported.\n");
+ return false;
+ }
+
+ if (!change_d3d_backbuffer(priv))
+ return false;
+
+ MP_VERBOSE(priv, "device_caps_power2_only %d, device_caps_square_only %d\n"
+ "device_texture_sys %d\n"
+ "max_texture_width %d, max_texture_height %d\n",
+ priv->device_caps_power2_only, priv->device_caps_square_only,
+ priv->device_texture_sys, priv->max_texture_width,
+ priv->max_texture_height);
+
+ return true;
+}
+
+/** @brief Fill D3D Presentation parameters
+ */
+static void fill_d3d_presentparams(d3d_priv *priv,
+ D3DPRESENT_PARAMETERS *present_params)
+{
+ /* Prepare Direct3D initialization parameters. */
+ memset(present_params, 0, sizeof(D3DPRESENT_PARAMETERS));
+ present_params->Windowed = TRUE;
+ present_params->SwapEffect =
+ priv->opt_swap_discard ? D3DSWAPEFFECT_DISCARD : D3DSWAPEFFECT_COPY;
+ present_params->Flags = D3DPRESENTFLAG_VIDEO;
+ present_params->hDeviceWindow = vo_w32_hwnd(priv->vo);
+ present_params->BackBufferWidth = priv->cur_backbuf_width;
+ present_params->BackBufferHeight = priv->cur_backbuf_height;
+ present_params->MultiSampleType = D3DMULTISAMPLE_NONE;
+ present_params->PresentationInterval = D3DPRESENT_INTERVAL_ONE;
+ present_params->BackBufferFormat = priv->desktop_fmt;
+ present_params->BackBufferCount = 1;
+ present_params->EnableAutoDepthStencil = FALSE;
+}
+
+
+// Create a new backbuffer. Create or Reset the D3D device.
+static bool change_d3d_backbuffer(d3d_priv *priv)
+{
+ int window_w = priv->vo->dwidth;
+ int window_h = priv->vo->dheight;
+
+ /* Grow the backbuffer in the required dimension. */
+ if (window_w > priv->cur_backbuf_width)
+ priv->cur_backbuf_width = window_w;
+
+ if (window_h > priv->cur_backbuf_height)
+ priv->cur_backbuf_height = window_h;
+
+ if (priv->opt_exact_backbuffer) {
+ priv->cur_backbuf_width = window_w;
+ priv->cur_backbuf_height = window_h;
+ }
+
+ /* The grown backbuffer dimensions are ready and fill_d3d_presentparams
+ * will use them, so we can reset the device.
+ */
+ D3DPRESENT_PARAMETERS present_params;
+ fill_d3d_presentparams(priv, &present_params);
+
+ if (!priv->d3d_device) {
+ if (FAILED(IDirect3D9_CreateDevice(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE, vo_w32_hwnd(priv->vo),
+ D3DCREATE_SOFTWARE_VERTEXPROCESSING
+ | D3DCREATE_FPU_PRESERVE
+ | D3DCREATE_MULTITHREADED,
+ &present_params, &priv->d3d_device)))
+ {
+ MP_VERBOSE(priv, "Creating Direct3D device failed.\n");
+ return 0;
+ }
+ } else {
+ if (FAILED(IDirect3DDevice9_Reset(priv->d3d_device, &present_params))) {
+ MP_ERR(priv, "Resetting Direct3D device failed.\n");
+ return 0;
+ }
+ }
+
+ MP_VERBOSE(priv, "New backbuffer (%dx%d), VO (%dx%d)\n",
+ present_params.BackBufferWidth, present_params.BackBufferHeight,
+ window_w, window_h);
+
+ return 1;
+}
+
+static void destroy_d3d(d3d_priv *priv)
+{
+ destroy_d3d_surfaces(priv);
+
+ if (priv->d3d_device)
+ IDirect3DDevice9_Release(priv->d3d_device);
+ priv->d3d_device = NULL;
+
+ if (priv->d3d_handle) {
+ MP_VERBOSE(priv, "Stopping Direct3D.\n");
+ IDirect3D9_Release(priv->d3d_handle);
+ }
+ priv->d3d_handle = NULL;
+}
+
+/** @brief Reconfigure the whole Direct3D. Called only
+ * when the video adapter becomes uncooperative. ("Lost" devices)
+ * @return 1 on success, 0 on failure
+ */
+static int reconfigure_d3d(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "reconfigure_d3d called.\n");
+
+ // Force complete destruction of the D3D state.
+ // Note: this step could be omitted. The resize_d3d call below would detect
+ // that d3d_device is NULL, and would properly recreate it. I'm not sure why
+ // the following code to release and recreate the d3d_handle exists.
+ destroy_d3d(priv);
+ if (!init_d3d(priv))
+ return 0;
+
+ // Proper re-initialization.
+ if (!resize_d3d(priv))
+ return 0;
+
+ return 1;
+}
+
+// Resize Direct3D context on window resize.
+// This function also is called when major initializations need to be done.
+static bool resize_d3d(d3d_priv *priv)
+{
+ D3DVIEWPORT9 vp = {0, 0, priv->vo->dwidth, priv->vo->dheight, 0, 1};
+
+ MP_VERBOSE(priv, "resize_d3d %dx%d called.\n",
+ priv->vo->dwidth, priv->vo->dheight);
+
+ /* Make sure that backbuffer is large enough to accommodate the new
+ viewport dimensions. Grow it if necessary. */
+
+ bool backbuf_resize = priv->vo->dwidth > priv->cur_backbuf_width ||
+ priv->vo->dheight > priv->cur_backbuf_height;
+
+ if (priv->opt_exact_backbuffer) {
+ backbuf_resize = priv->vo->dwidth != priv->cur_backbuf_width ||
+ priv->vo->dheight != priv->cur_backbuf_height;
+ }
+
+ if (backbuf_resize || !priv->d3d_device)
+ {
+ destroy_d3d_surfaces(priv);
+ if (!change_d3d_backbuffer(priv))
+ return 0;
+ }
+
+ if (!priv->d3d_device || !priv->image_format)
+ return 1;
+
+ if (!create_d3d_surfaces(priv))
+ return 0;
+
+ if (FAILED(IDirect3DDevice9_SetViewport(priv->d3d_device, &vp))) {
+ MP_ERR(priv, "Setting viewport failed.\n");
+ return 0;
+ }
+
+ // so that screen coordinates map to D3D ones
+ D3DMATRIX view;
+ d3d_matrix_ortho(&view, 0.5f, vp.Width + 0.5f, vp.Height + 0.5f, 0.5f);
+ IDirect3DDevice9_SetTransform(priv->d3d_device, D3DTS_VIEW, &view);
+
+ calc_fs_rect(priv);
+ priv->vo->want_redraw = true;
+
+ return 1;
+}
+
+/** @brief Uninitialize Direct3D and close the window.
+ */
+static void uninit_d3d(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "uninit_d3d called.\n");
+
+ destroy_d3d(priv);
+}
+
+static uint32_t d3d_draw_frame(d3d_priv *priv)
+{
+ if (!priv->d3d_device)
+ return VO_TRUE;
+
+ if (!d3d_begin_scene(priv))
+ return VO_ERROR;
+
+ IDirect3DDevice9_Clear(priv->d3d_device, 0, NULL, D3DCLEAR_TARGET, 0, 0, 0);
+
+ if (!priv->have_image)
+ goto render_osd;
+
+ RECT rm = priv->fs_movie_rect;
+ RECT rs = priv->fs_panscan_rect;
+
+ rs.left &= ~(ULONG)1;
+ rs.top &= ~(ULONG)1;
+ rs.right &= ~(ULONG)1;
+ rs.bottom &= ~(ULONG)1;
+ if (FAILED(IDirect3DDevice9_StretchRect(priv->d3d_device,
+ priv->d3d_surface,
+ &rs,
+ priv->d3d_backbuf,
+ &rm,
+ D3DTEXF_LINEAR))) {
+ MP_ERR(priv, "Copying frame to the backbuffer failed.\n");
+ return VO_ERROR;
+ }
+
+render_osd:
+
+ draw_osd(priv->vo);
+
+ return VO_TRUE;
+}
+
+static D3DFORMAT check_format(d3d_priv *priv, uint32_t movie_fmt)
+{
+ const struct fmt_entry *cur = &fmt_table[0];
+
+ while (cur->mplayer_fmt) {
+ if (cur->mplayer_fmt == movie_fmt) {
+ HRESULT res;
+ /* Test conversion from Movie colorspace to
+ * display's target colorspace. */
+ res = IDirect3D9_CheckDeviceFormatConversion(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE,
+ cur->fourcc,
+ priv->desktop_fmt);
+ if (FAILED(res)) {
+ MP_VERBOSE(priv, "Rejected image format: %s\n",
+ vo_format_name(cur->mplayer_fmt));
+ return 0;
+ }
+
+ MP_DBG(priv, "Accepted image format: %s\n",
+ vo_format_name(cur->mplayer_fmt));
+
+ return cur->fourcc;
+ }
+ cur++;
+ }
+
+ return 0;
+}
+
+// Return if the image format can be used. If it can, decide which rendering
+// and conversion mode to use.
+// If initialize is true, actually setup all variables to use the picked
+// rendering mode.
+static bool init_rendering_mode(d3d_priv *priv, uint32_t fmt, bool initialize)
+{
+ int blit_d3dfmt = check_format(priv, fmt);
+
+ if (!blit_d3dfmt)
+ return false;
+
+ MP_VERBOSE(priv, "Accepted rendering methods for "
+ "format='%s': StretchRect=%#x.\n",
+ vo_format_name(fmt), blit_d3dfmt);
+
+ if (!initialize)
+ return true;
+
+ // initialization doesn't fail beyond this point
+
+ priv->movie_src_fmt = 0;
+ priv->image_format = fmt;
+
+ priv->movie_src_fmt = blit_d3dfmt;
+
+ return true;
+}
+
+/** @brief Query if movie colorspace is supported by the HW.
+ * @return 0 on failure, device capabilities (not probed
+ * currently) on success.
+ */
+static int query_format(struct vo *vo, int movie_fmt)
+{
+ d3d_priv *priv = vo->priv;
+ if (!init_rendering_mode(priv, movie_fmt, false))
+ return 0;
+
+ return 1;
+}
+
+/****************************************************************************
+ * *
+ * *
+ * *
+ * libvo Control / Callback functions *
+ * *
+ * *
+ * *
+ ****************************************************************************/
+
+
+/** @brief libvo Callback: Preinitialize the video card.
+ * Preinit the hardware just enough to be queried about
+ * supported formats.
+ *
+ * @return 0 on success, -1 on failure
+ */
+
+static int preinit(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+ priv->vo = vo;
+ priv->log = vo->log;
+
+ priv->d3d9_dll = LoadLibraryA("d3d9.dll");
+ if (!priv->d3d9_dll) {
+ MP_ERR(priv, "Unable to dynamically load d3d9.dll\n");
+ goto err_out;
+ }
+
+ priv->pDirect3DCreate9 = (void *)GetProcAddress(priv->d3d9_dll,
+ "Direct3DCreate9");
+ if (!priv->pDirect3DCreate9) {
+ MP_ERR(priv, "Unable to find entry point of Direct3DCreate9\n");
+ goto err_out;
+ }
+
+ /* w32_common framework call. Configures window on the screen, gets
+ * fullscreen dimensions and does other useful stuff.
+ */
+ if (!vo_w32_init(vo)) {
+ MP_VERBOSE(priv, "Configuring onscreen window failed.\n");
+ goto err_out;
+ }
+
+ if (!init_d3d(priv))
+ goto err_out;
+
+ return 0;
+
+err_out:
+ uninit(vo);
+ return -1;
+}
+
+/** @brief libvo Callback: Handle control requests.
+ * @return VO_TRUE on success, VO_NOTIMPL when not implemented
+ */
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ d3d_priv *priv = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ calc_fs_rect(priv);
+ priv->vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)data = get_window_screenshot(priv);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int r = vo_w32_control(vo, &events, request, data);
+
+ if (events & VO_EVENT_RESIZE)
+ resize_d3d(priv);
+
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+
+ vo_event(vo, events);
+
+ return r;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ d3d_priv *priv = vo->priv;
+
+ priv->have_image = false;
+
+ vo_w32_config(vo);
+
+ if ((priv->image_format != params->imgfmt)
+ || (priv->src_width != params->w)
+ || (priv->src_height != params->h))
+ {
+ d3d_destroy_video_objects(priv);
+
+ priv->src_width = params->w;
+ priv->src_height = params->h;
+ priv->params = *params;
+ init_rendering_mode(priv, params->imgfmt, true);
+ }
+
+ if (!resize_d3d(priv))
+ return VO_ERROR;
+
+ return 0; /* Success */
+}
+
+/** @brief libvo Callback: Flip next already drawn frame on the
+ * screen.
+ */
+static void flip_page(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+
+ if (priv->d3d_device && priv->d3d_in_scene) {
+ if (FAILED(IDirect3DDevice9_EndScene(priv->d3d_device))) {
+ MP_ERR(priv, "EndScene failed.\n");
+ }
+ }
+ priv->d3d_in_scene = false;
+
+ RECT rect = {0, 0, vo->dwidth, vo->dheight};
+ if (!priv->d3d_device ||
+ FAILED(IDirect3DDevice9_Present(priv->d3d_device, &rect, 0, 0, 0))) {
+ MP_VERBOSE(priv, "Trying to reinitialize uncooperative video adapter.\n");
+ if (!reconfigure_d3d(priv)) {
+ MP_VERBOSE(priv, "Reinitialization failed.\n");
+ return;
+ } else {
+ MP_VERBOSE(priv, "Video adapter reinitialized.\n");
+ }
+ }
+}
+
+/** @brief libvo Callback: Uninitializes all pointers and closes
+ * all D3D related stuff,
+ */
+static void uninit(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+
+ MP_VERBOSE(priv, "uninit called.\n");
+
+ uninit_d3d(priv);
+ vo_w32_uninit(vo);
+ if (priv->d3d9_dll)
+ FreeLibrary(priv->d3d9_dll);
+ priv->d3d9_dll = NULL;
+}
+
+// Lock buffers and fill out to point to them.
+// Must call d3d_unlock_video_objects() to unlock the buffers again.
+static bool get_video_buffer(d3d_priv *priv, struct mp_image *out)
+{
+ *out = (struct mp_image) {0};
+ mp_image_set_size(out, priv->src_width, priv->src_height);
+ mp_image_setfmt(out, priv->image_format);
+
+ if (!priv->d3d_device)
+ return false;
+
+ if (!priv->locked_rect.pBits) {
+ if (FAILED(IDirect3DSurface9_LockRect(priv->d3d_surface,
+ &priv->locked_rect, NULL, 0)))
+ {
+ MP_ERR(priv, "Surface lock failed.\n");
+ return false;
+ }
+ }
+
+ uint8_t *base = priv->locked_rect.pBits;
+ size_t stride = priv->locked_rect.Pitch;
+
+ out->planes[0] = base;
+ out->stride[0] = stride;
+
+ if (out->num_planes == 2) {
+ // NV12, NV21
+ out->planes[1] = base + stride * out->h;
+ out->stride[1] = stride;
+ }
+
+ if (out->num_planes == 3) {
+ bool swap = priv->movie_src_fmt == MAKEFOURCC('Y','V','1','2');
+
+ size_t uv_stride = stride / 2;
+ uint8_t *u = base + out->h * stride;
+ uint8_t *v = u + (out->h / 2) * uv_stride;
+
+ out->planes[1] = swap ? v : u;
+ out->planes[2] = swap ? u : v;
+
+ out->stride[1] = out->stride[2] = uv_stride;
+ }
+
+ return true;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ d3d_priv *priv = vo->priv;
+ if (!priv->d3d_device)
+ return;
+
+ struct mp_image buffer;
+ if (!get_video_buffer(priv, &buffer))
+ return;
+
+ if (!frame->current)
+ return;
+
+ mp_image_copy(&buffer, frame->current);
+
+ d3d_unlock_video_objects(priv);
+
+ priv->have_image = true;
+ priv->osd_pts = frame->current->pts;
+
+ d3d_draw_frame(priv);
+}
+
+static mp_image_t *get_window_screenshot(d3d_priv *priv)
+{
+ D3DDISPLAYMODE mode;
+ mp_image_t *image = NULL;
+ RECT window_rc;
+ RECT screen_rc;
+ RECT visible;
+ POINT pt;
+ D3DLOCKED_RECT locked_rect;
+ int width, height;
+ IDirect3DSurface9 *surface = NULL;
+
+ if (FAILED(IDirect3DDevice9_GetDisplayMode(priv->d3d_device, 0, &mode))) {
+ MP_ERR(priv, "GetDisplayMode failed.\n");
+ goto error_exit;
+ }
+
+ if (FAILED(IDirect3DDevice9_CreateOffscreenPlainSurface(priv->d3d_device,
+ mode.Width, mode.Height, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &surface,
+ NULL)))
+ {
+ MP_ERR(priv, "Couldn't create surface.\n");
+ goto error_exit;
+ }
+
+ if (FAILED(IDirect3DDevice9_GetFrontBufferData(priv->d3d_device, 0,
+ surface)))
+ {
+ MP_ERR(priv, "Couldn't copy frontbuffer.\n");
+ goto error_exit;
+ }
+
+ GetClientRect(vo_w32_hwnd(priv->vo), &window_rc);
+ pt = (POINT) { 0, 0 };
+ ClientToScreen(vo_w32_hwnd(priv->vo), &pt);
+ window_rc.left = pt.x;
+ window_rc.top = pt.y;
+ window_rc.right += window_rc.left;
+ window_rc.bottom += window_rc.top;
+
+ screen_rc = (RECT) { 0, 0, mode.Width, mode.Height };
+
+ if (!IntersectRect(&visible, &screen_rc, &window_rc))
+ goto error_exit;
+ width = visible.right - visible.left;
+ height = visible.bottom - visible.top;
+ if (width < 1 || height < 1)
+ goto error_exit;
+
+ image = mp_image_alloc(IMGFMT_BGR0, width, height);
+ if (!image)
+ goto error_exit;
+
+ IDirect3DSurface9_LockRect(surface, &locked_rect, NULL, 0);
+
+ memcpy_pic(image->planes[0], (char*)locked_rect.pBits + visible.top *
+ locked_rect.Pitch + visible.left * 4, width * 4, height,
+ image->stride[0], locked_rect.Pitch);
+
+ IDirect3DSurface9_UnlockRect(surface);
+ IDirect3DSurface9_Release(surface);
+
+ return image;
+
+error_exit:
+ talloc_free(image);
+ if (surface)
+ IDirect3DSurface9_Release(surface);
+ return NULL;
+}
+
+static void update_osd(d3d_priv *priv)
+{
+ if (!priv->osd_cache)
+ priv->osd_cache = mp_draw_sub_alloc(priv, priv->vo->global);
+
+ struct sub_bitmap_list *sbs = osd_render(priv->vo->osd, priv->osd_res,
+ priv->osd_pts, 0, mp_draw_sub_formats);
+
+ struct mp_rect act_rc[MAX_OSD_RECTS], mod_rc[64];
+ int num_act_rc = 0, num_mod_rc = 0;
+
+ struct mp_image *osd = mp_draw_sub_overlay(priv->osd_cache, sbs,
+ act_rc, MP_ARRAY_SIZE(act_rc), &num_act_rc,
+ mod_rc, MP_ARRAY_SIZE(mod_rc), &num_mod_rc);
+
+ talloc_free(sbs);
+
+ if (!osd) {
+ MP_ERR(priv, "Failed to render OSD.\n");
+ return;
+ }
+
+ if (!num_mod_rc && priv->osd_texture.system)
+ return; // nothing changed
+
+ priv->osd_num_vertices = 0;
+
+ if (osd->w > priv->osd_texture.tex_w || osd->h > priv->osd_texture.tex_h) {
+ int new_w = osd->w;
+ int new_h = osd->h;
+ d3d_fix_texture_size(priv, &new_w, &new_h);
+
+ MP_DBG(priv, "reallocate OSD surface to %dx%d.\n", new_w, new_h);
+
+ d3dtex_release(priv, &priv->osd_texture);
+ if (!d3dtex_allocate(priv, &priv->osd_texture, D3DFMT_A8R8G8B8,
+ new_w, new_h))
+ return;
+ }
+
+ // Lazy; could/should use the bounding rect, or perform multiple lock calls.
+ // The previous approach (fully packed texture) was more efficient.
+ RECT dirty_rc = { 0, 0, priv->osd_texture.w, priv->osd_texture.h };
+
+ D3DLOCKED_RECT locked_rect;
+
+ if (FAILED(IDirect3DTexture9_LockRect(priv->osd_texture.system, 0, &locked_rect,
+ &dirty_rc, 0)))
+ {
+ MP_ERR(priv, "OSD texture lock failed.\n");
+ return;
+ }
+
+ for (int n = 0; n < num_mod_rc; n++) {
+ struct mp_rect rc = mod_rc[n];
+ int w = mp_rect_w(rc);
+ int h = mp_rect_h(rc);
+ void *src = mp_image_pixel_ptr(osd, 0, rc.x0, rc.y0);
+ void *dst = (char *)locked_rect.pBits + locked_rect.Pitch * rc.y0 +
+ rc.x0 * 4;
+ memcpy_pic(dst, src, w * 4, h, locked_rect.Pitch, osd->stride[0]);
+ }
+
+ if (FAILED(IDirect3DTexture9_UnlockRect(priv->osd_texture.system, 0))) {
+ MP_ERR(priv, "OSD texture unlock failed.\n");
+ return;
+ }
+
+ if (!d3dtex_update(priv, &priv->osd_texture))
+ return;
+
+ // We need 2 primitives per quad which makes 6 vertices.
+ priv->osd_num_vertices = num_act_rc * 6;
+
+ float tex_w = priv->osd_texture.tex_w;
+ float tex_h = priv->osd_texture.tex_h;
+
+ for (int n = 0; n < num_act_rc; n++) {
+ struct mp_rect rc = act_rc[n];
+
+ float tx0 = rc.x0 / tex_w;
+ float ty0 = rc.y0 / tex_h;
+ float tx1 = rc.x1 / tex_w;
+ float ty1 = rc.y1 / tex_h;
+
+ vertex_osd *v = &priv->osd_vertices[n * 6];
+ v[0] = (vertex_osd) { rc.x0, rc.y0, 0, tx0, ty0 };
+ v[1] = (vertex_osd) { rc.x1, rc.y0, 0, tx1, ty0 };
+ v[2] = (vertex_osd) { rc.x0, rc.y1, 0, tx0, ty1 };
+ v[3] = (vertex_osd) { rc.x1, rc.y1, 0, tx1, ty1 };
+ v[4] = v[2];
+ v[5] = v[1];
+ }
+}
+
+static void draw_osd(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+ if (!priv->d3d_device)
+ return;
+
+ update_osd(priv);
+
+ if (!priv->osd_num_vertices)
+ return;
+
+ d3d_begin_scene(priv);
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHABLENDENABLE, TRUE);
+
+ IDirect3DDevice9_SetTexture(priv->d3d_device, 0,
+ d3dtex_get_render_texture(priv, &priv->osd_texture));
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device, D3DRS_SRCBLEND,
+ D3DBLEND_ONE);
+
+ IDirect3DDevice9_SetFVF(priv->d3d_device, D3DFVF_OSD_VERTEX);
+ IDirect3DDevice9_DrawPrimitiveUP(priv->d3d_device, D3DPT_TRIANGLELIST,
+ priv->osd_num_vertices / 3,
+ priv->osd_vertices, sizeof(vertex_osd));
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
+
+ IDirect3DDevice9_SetTexture(priv->d3d_device, 0, NULL);
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHABLENDENABLE, FALSE);
+}
+
+#define OPT_BASE_STRUCT d3d_priv
+
+static const struct m_option opts[] = {
+ {"force-power-of-2", OPT_BOOL(opt_force_power_of_2)},
+ {"disable-texture-align", OPT_BOOL(opt_disable_texture_align)},
+ {"texture-memory", OPT_CHOICE(opt_texture_memory,
+ {"default", 0},
+ {"managed", 1},
+ {"default-pool", 2},
+ {"default-pool-shadow", 3},
+ {"scratch", 4})},
+ {"swap-discard", OPT_BOOL(opt_swap_discard)},
+ {"exact-backbuffer", OPT_BOOL(opt_exact_backbuffer)},
+ {0}
+};
+
+const struct vo_driver video_out_direct3d = {
+ .description = "Direct3D 9 Renderer",
+ .name = "direct3d",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(d3d_priv),
+ .options = opts,
+ .options_prefix = "vo-direct3d",
+};
diff --git a/video/out/vo_dmabuf_wayland.c b/video/out/vo_dmabuf_wayland.c
new file mode 100644
index 0000000..e04ff5d
--- /dev/null
+++ b/video/out/vo_dmabuf_wayland.c
@@ -0,0 +1,872 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavutil/hwcontext_drm.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include "config.h"
+
+#if HAVE_VAAPI
+#include <va/va_drmcommon.h>
+#endif
+
+#include "common/global.h"
+#include "gpu/hwdec.h"
+#include "gpu/video.h"
+#include "mpv_talloc.h"
+#include "present_sync.h"
+#include "sub/draw_bmp.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "vo.h"
+#include "wayland_common.h"
+#include "wldmabuf/ra_wldmabuf.h"
+
+#if HAVE_VAAPI
+#include "video/vaapi.h"
+#endif
+
+// Generated from wayland-protocols
+#include "linux-dmabuf-unstable-v1.h"
+#include "viewporter.h"
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+#include "single-pixel-buffer-v1.h"
+#endif
+
+// We need at least enough buffers to avoid a
+// flickering artifact in certain formats.
+#define WL_BUFFERS_WANTED 15
+
+enum hwdec_type {
+ HWDEC_NONE,
+ HWDEC_VAAPI,
+ HWDEC_DRMPRIME,
+};
+
+struct buffer {
+ struct vo *vo;
+ struct wl_buffer *buffer;
+ struct wl_list link;
+ struct vo_frame *frame;
+
+ uint32_t drm_format;
+ uintptr_t id;
+};
+
+struct osd_buffer {
+ struct vo *vo;
+ struct wl_buffer *buffer;
+ struct wl_list link;
+ struct mp_image image;
+ size_t size;
+};
+
+struct priv {
+ struct mp_log *log;
+ struct mp_rect src;
+ struct mpv_global *global;
+
+ struct ra_ctx *ctx;
+ struct ra_hwdec_ctx hwdec_ctx;
+
+ struct wl_shm_pool *solid_buffer_pool;
+ struct wl_buffer *solid_buffer;
+ struct wl_list buffer_list;
+ struct wl_list osd_buffer_list;
+
+ struct wl_shm_pool *osd_shm_pool;
+ uint8_t *osd_shm_data;
+ int osd_shm_width;
+ int osd_shm_stride;
+ int osd_shm_height;
+ bool osd_surface_is_mapped;
+ bool osd_surface_has_contents;
+
+ struct osd_buffer *osd_buffer;
+ struct mp_draw_sub_cache *osd_cache;
+ struct mp_osd_res screen_osd_res;
+
+ bool destroy_buffers;
+ bool force_window;
+ enum hwdec_type hwdec_type;
+ uint32_t drm_format;
+ uint64_t drm_modifier;
+};
+
+static void buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
+{
+ struct buffer *buf = data;
+ if (buf->frame) {
+ talloc_free(buf->frame);
+ buf->frame = NULL;
+ }
+}
+
+static const struct wl_buffer_listener buffer_listener = {
+ buffer_handle_release,
+};
+
+static void osd_buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
+{
+ struct osd_buffer *osd_buf = data;
+ wl_list_remove(&osd_buf->link);
+ if (osd_buf->buffer) {
+ wl_buffer_destroy(osd_buf->buffer);
+ osd_buf->buffer = NULL;
+ }
+ talloc_free(osd_buf);
+}
+
+static const struct wl_buffer_listener osd_buffer_listener = {
+ osd_buffer_handle_release,
+};
+
+#if HAVE_VAAPI
+static void close_file_descriptors(VADRMPRIMESurfaceDescriptor desc)
+{
+ for (int i = 0; i < desc.num_objects; i++)
+ close(desc.objects[i].fd);
+}
+#endif
+
+static uintptr_t vaapi_surface_id(struct mp_image *src)
+{
+ uintptr_t id = 0;
+#if HAVE_VAAPI
+ id = (uintptr_t)va_surface_id(src);
+#endif
+ return id;
+}
+
+static bool vaapi_drm_format(struct vo *vo, struct mp_image *src)
+{
+ bool format = false;
+#if HAVE_VAAPI
+ struct priv *p = vo->priv;
+ VADRMPRIMESurfaceDescriptor desc = {0};
+
+ uintptr_t id = vaapi_surface_id(src);
+ VADisplay display = ra_get_native_resource(p->ctx->ra, "VADisplay");
+ VAStatus status = vaExportSurfaceHandle(display, id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_COMPOSED_LAYERS | VA_EXPORT_SURFACE_READ_ONLY, &desc);
+
+ if (!CHECK_VA_STATUS(vo, "vaExportSurfaceHandle()")) {
+ /* invalid surface warning => composed layers not supported */
+ if (status == VA_STATUS_ERROR_INVALID_SURFACE)
+ MP_VERBOSE(vo, "vaExportSurfaceHandle: composed layers not supported.\n");
+ goto done;
+ }
+ p->drm_format = desc.layers[0].drm_format;
+ p->drm_modifier = desc.objects[0].drm_format_modifier;
+ format = true;
+done:
+ close_file_descriptors(desc);
+#endif
+ return format;
+}
+
+static void vaapi_dmabuf_importer(struct buffer *buf, struct mp_image *src,
+ struct zwp_linux_buffer_params_v1 *params)
+{
+#if HAVE_VAAPI
+ struct vo *vo = buf->vo;
+ struct priv *p = vo->priv;
+ VADRMPRIMESurfaceDescriptor desc = {0};
+ VADisplay display = ra_get_native_resource(p->ctx->ra, "VADisplay");
+
+ /* composed has single layer */
+ int layer_no = 0;
+ buf->id = vaapi_surface_id(src);
+ VAStatus status = vaExportSurfaceHandle(display, buf->id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_COMPOSED_LAYERS | VA_EXPORT_SURFACE_READ_ONLY, &desc);
+
+ if (!CHECK_VA_STATUS(vo, "vaExportSurfaceHandle()")) {
+ /* invalid surface warning => composed layers not supported */
+ if (status == VA_STATUS_ERROR_INVALID_SURFACE)
+ MP_VERBOSE(vo, "vaExportSurfaceHandle: composed layers not supported.\n");
+ goto done;
+ }
+ buf->drm_format = desc.layers[layer_no].drm_format;
+ if (!ra_compatible_format(p->ctx->ra, buf->drm_format, desc.objects[0].drm_format_modifier)) {
+ MP_VERBOSE(vo, "%s(%016lx) is not supported.\n",
+ mp_tag_str(buf->drm_format), desc.objects[0].drm_format_modifier);
+ buf->drm_format = 0;
+ goto done;
+ }
+ for (int plane_no = 0; plane_no < desc.layers[layer_no].num_planes; ++plane_no) {
+ int object = desc.layers[layer_no].object_index[plane_no];
+ uint64_t modifier = desc.objects[object].drm_format_modifier;
+ zwp_linux_buffer_params_v1_add(params, desc.objects[object].fd, plane_no, desc.layers[layer_no].offset[plane_no],
+ desc.layers[layer_no].pitch[plane_no], modifier >> 32, modifier & 0xffffffff);
+ }
+
+done:
+ close_file_descriptors(desc);
+#endif
+}
+
+static uintptr_t drmprime_surface_id(struct mp_image *src)
+{
+ uintptr_t id = 0;
+ struct AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)src->planes[0];
+
+ AVDRMObjectDescriptor object = desc->objects[0];
+ id = (uintptr_t)object.fd;
+ return id;
+}
+
+static bool drmprime_drm_format(struct vo *vo, struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ struct AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)src->planes[0];
+ if (!desc)
+ return false;
+
+ // Just check the very first layer/plane.
+ p->drm_format = desc->layers[0].format;
+ int object_index = desc->layers[0].planes[0].object_index;
+ p->drm_modifier = desc->objects[object_index].format_modifier;
+ return true;
+}
+
+static void drmprime_dmabuf_importer(struct buffer *buf, struct mp_image *src,
+ struct zwp_linux_buffer_params_v1 *params)
+{
+ int layer_no, plane_no;
+ int max_planes = 0;
+ const AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)src->planes[0];
+ if (!desc)
+ return;
+
+ buf->id = drmprime_surface_id(src);
+ for (layer_no = 0; layer_no < desc->nb_layers; layer_no++) {
+ AVDRMLayerDescriptor layer = desc->layers[layer_no];
+
+ buf->drm_format = layer.format;
+ max_planes = MPMAX(max_planes, layer.nb_planes);
+ for (plane_no = 0; plane_no < layer.nb_planes; ++plane_no) {
+ AVDRMPlaneDescriptor plane = layer.planes[plane_no];
+ int object_index = plane.object_index;
+ AVDRMObjectDescriptor object = desc->objects[object_index];
+ uint64_t modifier = object.format_modifier;
+
+ zwp_linux_buffer_params_v1_add(params, object.fd, plane_no, plane.offset,
+ plane.pitch, modifier >> 32, modifier & 0xffffffff);
+ }
+ }
+}
+
+static intptr_t surface_id(struct vo *vo, struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ switch(p->hwdec_type) {
+ case HWDEC_VAAPI:
+ return vaapi_surface_id(src);
+ case HWDEC_DRMPRIME:
+ return drmprime_surface_id(src);
+ default:
+ return 0;
+ }
+}
+
+static bool drm_format_check(struct vo *vo, struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ switch(p->hwdec_type) {
+ case HWDEC_VAAPI:
+ return vaapi_drm_format(vo, src);
+ case HWDEC_DRMPRIME:
+ return drmprime_drm_format(vo, src);
+ }
+ return false;
+}
+
+static struct buffer *buffer_check(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ /* Make more buffers if we're not at the desired amount yet. */
+ if (wl_list_length(&p->buffer_list) < WL_BUFFERS_WANTED)
+ goto done;
+
+ uintptr_t id = surface_id(vo, frame->current);
+ struct buffer *buf;
+ wl_list_for_each(buf, &p->buffer_list, link) {
+ if (buf->id == id) {
+ if (buf->frame)
+ talloc_free(buf->frame);
+ buf->frame = frame;
+ return buf;
+ }
+ }
+
+done:
+ return NULL;
+}
+
+static struct buffer *buffer_create(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct priv *p = vo->priv;
+
+ struct buffer *buf = talloc_zero(vo, struct buffer);
+ buf->vo = vo;
+ buf->frame = frame;
+
+ struct mp_image *image = buf->frame->current;
+ struct zwp_linux_buffer_params_v1 *params = zwp_linux_dmabuf_v1_create_params(wl->dmabuf);
+
+ switch(p->hwdec_type) {
+ case HWDEC_VAAPI:
+ vaapi_dmabuf_importer(buf, image, params);
+ break;
+ case HWDEC_DRMPRIME:
+ drmprime_dmabuf_importer(buf, image, params);
+ break;
+ }
+
+ if (!buf->drm_format) {
+ talloc_free(buf->frame);
+ talloc_free(buf);
+ zwp_linux_buffer_params_v1_destroy(params);
+ return NULL;
+ }
+
+ buf->buffer = zwp_linux_buffer_params_v1_create_immed(params, image->params.w, image->params.h,
+ buf->drm_format, 0);
+ zwp_linux_buffer_params_v1_destroy(params);
+ wl_buffer_add_listener(buf->buffer, &buffer_listener, buf);
+ wl_list_insert(&p->buffer_list, &buf->link);
+ return buf;
+}
+
+static struct buffer *buffer_get(struct vo *vo, struct vo_frame *frame)
+{
+ /* Reuse existing buffer if possible. */
+ struct buffer *buf = buffer_check(vo, frame);
+ if (buf) {
+ return buf;
+ } else {
+ return buffer_create(vo, frame);
+ }
+}
+
+static void destroy_buffers(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct buffer *buf, *tmp;
+ p->destroy_buffers = false;
+ wl_list_for_each_safe(buf, tmp, &p->buffer_list, link) {
+ wl_list_remove(&buf->link);
+ if (buf->frame) {
+ talloc_free(buf->frame);
+ buf->frame = NULL;
+ }
+ if (buf->buffer) {
+ wl_buffer_destroy(buf->buffer);
+ buf->buffer = NULL;
+ }
+ talloc_free(buf);
+ }
+}
+
+static void destroy_osd_buffers(struct vo *vo)
+{
+ if (!vo->wl)
+ return;
+
+ // Remove any existing buffer before we destroy them.
+ wl_surface_attach(vo->wl->osd_surface, NULL, 0, 0);
+ wl_surface_commit(vo->wl->osd_surface);
+
+ struct priv *p = vo->priv;
+ struct osd_buffer *osd_buf, *tmp;
+ wl_list_for_each_safe(osd_buf, tmp, &p->osd_buffer_list, link) {
+ wl_list_remove(&osd_buf->link);
+ munmap(osd_buf->image.planes[0], osd_buf->size);
+ if (osd_buf->buffer) {
+ wl_buffer_destroy(osd_buf->buffer);
+ osd_buf->buffer = NULL;
+ }
+ }
+}
+
+static struct osd_buffer *osd_buffer_check(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct osd_buffer *osd_buf;
+ wl_list_for_each(osd_buf, &p->osd_buffer_list, link) {
+ return osd_buf;
+ }
+ return NULL;
+}
+
+static struct osd_buffer *osd_buffer_create(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct osd_buffer *osd_buf = talloc_zero(vo, struct osd_buffer);
+
+ osd_buf->vo = vo;
+ osd_buf->size = p->osd_shm_height * p->osd_shm_stride;
+ mp_image_set_size(&osd_buf->image, p->osd_shm_width, p->osd_shm_height);
+ osd_buf->image.planes[0] = p->osd_shm_data;
+ osd_buf->image.stride[0] = p->osd_shm_stride;
+ osd_buf->buffer = wl_shm_pool_create_buffer(p->osd_shm_pool, 0,
+ p->osd_shm_width, p->osd_shm_height,
+ p->osd_shm_stride, WL_SHM_FORMAT_ARGB8888);
+
+ if (!osd_buf->buffer) {
+ talloc_free(osd_buf);
+ return NULL;
+ }
+
+ wl_list_insert(&p->osd_buffer_list, &osd_buf->link);
+ wl_buffer_add_listener(osd_buf->buffer, &osd_buffer_listener, osd_buf);
+ return osd_buf;
+}
+
+static struct osd_buffer *osd_buffer_get(struct vo *vo)
+{
+ struct osd_buffer *osd_buf = osd_buffer_check(vo);
+ if (osd_buf) {
+ return osd_buf;
+ } else {
+ return osd_buffer_create(vo);
+ }
+}
+
+static void create_shm_pool(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct priv *p = vo->priv;
+
+ int stride = MP_ALIGN_UP(vo->dwidth * 4, 16);
+ size_t size = vo->dheight * stride;
+ int fd = vo_wayland_allocate_memfd(vo, size);
+ if (fd < 0)
+ return;
+ uint8_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED)
+ goto error1;
+ struct wl_shm_pool *pool = wl_shm_create_pool(wl->shm, fd, size);
+ if (!pool)
+ goto error2;
+ close(fd);
+
+ destroy_osd_buffers(vo);
+
+ if (p->osd_shm_pool)
+ wl_shm_pool_destroy(p->osd_shm_pool);
+ p->osd_shm_pool = pool;
+ p->osd_shm_width = vo->dwidth;
+ p->osd_shm_height = vo->dheight;
+ p->osd_shm_stride = stride;
+ p->osd_shm_data = data;
+ return;
+
+error2:
+ munmap(data, size);
+error1:
+ close(fd);
+}
+
+static void set_viewport_source(struct vo *vo, struct mp_rect src)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+
+ if (p->force_window)
+ return;
+
+ if (wl->video_viewport && !mp_rect_equals(&p->src, &src)) {
+ wp_viewport_set_source(wl->video_viewport, src.x0 << 8,
+ src.y0 << 8, mp_rect_w(src) << 8,
+ mp_rect_h(src) << 8);
+ p->src = src;
+ }
+}
+
+static void resize(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct priv *p = vo->priv;
+
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_vo_opts *vo_opts = wl->vo_opts;
+
+ const int width = mp_rect_w(wl->geometry);
+ const int height = mp_rect_h(wl->geometry);
+
+ if (width == 0 || height == 0)
+ return;
+
+ vo_wayland_set_opaque_region(wl, false);
+ vo->dwidth = width;
+ vo->dheight = height;
+
+ create_shm_pool(vo);
+
+ // top level viewport is calculated with pan set to zero
+ vo->opts->pan_x = 0;
+ vo->opts->pan_y = 0;
+ vo_get_src_dst_rects(vo, &src, &dst, &p->screen_osd_res);
+ int window_w = p->screen_osd_res.ml + p->screen_osd_res.mr + mp_rect_w(dst);
+ int window_h = p->screen_osd_res.mt + p->screen_osd_res.mb + mp_rect_h(dst);
+ wp_viewport_set_destination(wl->viewport, window_w, window_h);
+
+ //now we restore pan for video viewport calculation
+ vo->opts->pan_x = vo_opts->pan_x;
+ vo->opts->pan_y = vo_opts->pan_y;
+ vo_get_src_dst_rects(vo, &src, &dst, &p->screen_osd_res);
+ wp_viewport_set_destination(wl->video_viewport, mp_rect_w(dst), mp_rect_h(dst));
+ wl_subsurface_set_position(wl->video_subsurface, dst.x0, dst.y0);
+ wp_viewport_set_destination(wl->osd_viewport, vo->dwidth, vo->dheight);
+ wl_subsurface_set_position(wl->osd_subsurface, 0 - dst.x0, 0 - dst.y0);
+ set_viewport_source(vo, src);
+}
+
+static bool draw_osd(struct vo *vo, struct mp_image *cur, double pts)
+{
+ struct priv *p = vo->priv;
+ struct mp_osd_res *res = &p->screen_osd_res;
+ bool draw = false;
+
+ struct sub_bitmap_list *sbs = osd_render(vo->osd, *res, pts, 0, mp_draw_sub_formats);
+
+ if (!sbs)
+ return draw;
+
+ struct mp_rect act_rc[1], mod_rc[64];
+ int num_act_rc = 0, num_mod_rc = 0;
+
+ if (!p->osd_cache)
+ p->osd_cache = mp_draw_sub_alloc(p, vo->global);
+
+ struct mp_image *osd = mp_draw_sub_overlay(p->osd_cache, sbs, act_rc,
+ MP_ARRAY_SIZE(act_rc), &num_act_rc,
+ mod_rc, MP_ARRAY_SIZE(mod_rc), &num_mod_rc);
+
+ p->osd_surface_has_contents = num_act_rc > 0;
+
+ if (!osd || !num_mod_rc)
+ goto done;
+
+ for (int n = 0; n < num_mod_rc; n++) {
+ struct mp_rect rc = mod_rc[n];
+
+ int rw = mp_rect_w(rc);
+ int rh = mp_rect_h(rc);
+
+ void *src = mp_image_pixel_ptr(osd, 0, rc.x0, rc.y0);
+ void *dst = cur->planes[0] + rc.x0 * 4 + rc.y0 * cur->stride[0];
+
+ memcpy_pic(dst, src, rw * 4, rh, cur->stride[0], osd->stride[0]);
+ }
+
+ draw = true;
+done:
+ talloc_free(sbs);
+ return draw;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ struct buffer *buf;
+ struct osd_buffer *osd_buf;
+ double pts;
+
+ if (!vo_wayland_check_visible(vo)) {
+ if (frame->current)
+ talloc_free(frame);
+ return;
+ }
+
+ if (p->destroy_buffers)
+ destroy_buffers(vo);
+
+ // Reuse the solid buffer so the osd can be visible
+ if (p->force_window) {
+ wl_surface_attach(wl->video_surface, p->solid_buffer, 0, 0);
+ wl_surface_damage_buffer(wl->video_surface, 0, 0, 1, 1);
+ }
+
+ pts = frame->current ? frame->current->pts : 0;
+ if (frame->current) {
+ buf = buffer_get(vo, frame);
+
+ if (buf && buf->frame) {
+ struct mp_image *image = buf->frame->current;
+ wl_surface_attach(wl->video_surface, buf->buffer, 0, 0);
+ wl_surface_damage_buffer(wl->video_surface, 0, 0, image->w,
+ image->h);
+
+ }
+ }
+
+ osd_buf = osd_buffer_get(vo);
+ if (osd_buf && osd_buf->buffer) {
+ if (draw_osd(vo, &osd_buf->image, pts) && p->osd_surface_has_contents) {
+ wl_surface_attach(wl->osd_surface, osd_buf->buffer, 0, 0);
+ wl_surface_damage_buffer(wl->osd_surface, 0, 0, osd_buf->image.w,
+ osd_buf->image.h);
+ p->osd_surface_is_mapped = true;
+ } else if (!p->osd_surface_has_contents && p->osd_surface_is_mapped) {
+ wl_surface_attach(wl->osd_surface, NULL, 0, 0);
+ p->osd_surface_is_mapped = false;
+ }
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ wl_surface_commit(wl->video_surface);
+ wl_surface_commit(wl->osd_surface);
+ wl_surface_commit(wl->surface);
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+static bool is_supported_fmt(int fmt)
+{
+ return (fmt == IMGFMT_DRMPRIME || fmt == IMGFMT_VAAPI);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return is_supported_fmt(format);
+}
+
+static int reconfig(struct vo *vo, struct mp_image *img)
+{
+ struct priv *p = vo->priv;
+
+ if (img->params.force_window) {
+ p->force_window = true;
+ goto done;
+ }
+
+ if (!drm_format_check(vo, img)) {
+ MP_ERR(vo, "Unable to get drm format from hardware decoding!\n");
+ return VO_ERROR;
+ }
+
+ if (!ra_compatible_format(p->ctx->ra, p->drm_format, p->drm_modifier)) {
+ MP_ERR(vo, "Format '%s' with modifier '(%016lx)' is not supported by"
+ " the compositor.\n", mp_tag_str(p->drm_format), p->drm_modifier);
+ return VO_ERROR;
+ }
+
+ p->force_window = false;
+done:
+ if (!vo_wayland_reconfig(vo))
+ return VO_ERROR;
+
+ // mpv rotates clockwise but the wayland spec has counter-clockwise rotations
+ // swap 1 and 3 to match mpv's direction
+ int transform = (360 - img->params.rotate) % 360 / 90;
+ wl_surface_set_buffer_transform(vo->wl->video_surface, transform);
+
+ // Immediately destroy all buffers if params change.
+ destroy_buffers(vo);
+ return 0;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+ int events = 0;
+ int ret;
+
+ switch (request) {
+ case VOCTRL_RESET:
+ p->destroy_buffers = true;
+ return VO_TRUE;
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ }
+
+ ret = vo_wayland_control(vo, &events, request, data);
+ if (events & VO_EVENT_RESIZE)
+ resize(vo);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+
+ return ret;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ destroy_buffers(vo);
+ destroy_osd_buffers(vo);
+ if (p->osd_shm_pool)
+ wl_shm_pool_destroy(p->osd_shm_pool);
+ if (p->solid_buffer_pool)
+ wl_shm_pool_destroy(p->solid_buffer_pool);
+ if (p->solid_buffer)
+ wl_buffer_destroy(p->solid_buffer);
+ ra_hwdec_ctx_uninit(&p->hwdec_ctx);
+ if (vo->hwdec_devs) {
+ hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+
+ vo_wayland_uninit(vo);
+ ra_ctx_destroy(&p->ctx);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->log = vo->log;
+ p->global = vo->global;
+ p->ctx = ra_ctx_create_by_name(vo, "wldmabuf");
+ wl_list_init(&p->buffer_list);
+ wl_list_init(&p->osd_buffer_list);
+ if (!p->ctx)
+ goto err;
+
+ assert(p->ctx->ra);
+
+ if (!vo->wl->dmabuf || !vo->wl->dmabuf_feedback) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s (ver. 4) protocol!\n",
+ zwp_linux_dmabuf_v1_interface.name);
+ goto err;
+ }
+
+ if (!vo->wl->shm) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wl_shm_interface.name);
+ goto err;
+ }
+
+ if (!vo->wl->video_subsurface) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wl_subcompositor_interface.name);
+ goto err;
+ }
+
+ if (!vo->wl->viewport) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wp_viewporter_interface.name);
+ goto err;
+ }
+
+ if (vo->wl->single_pixel_manager) {
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ p->solid_buffer = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(
+ vo->wl->single_pixel_manager, 0, 0, 0, UINT32_MAX); /* R, G, B, A */
+#endif
+ } else {
+ int width = 1;
+ int height = 1;
+ int stride = MP_ALIGN_UP(width * 4, 16);
+ int fd = vo_wayland_allocate_memfd(vo, stride);
+ if (fd < 0)
+ goto err;
+ p->solid_buffer_pool = wl_shm_create_pool(vo->wl->shm, fd, height * stride);
+ close(fd);
+ if (!p->solid_buffer_pool)
+ goto err;
+ p->solid_buffer = wl_shm_pool_create_buffer(
+ p->solid_buffer_pool, 0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
+ }
+ if (!p->solid_buffer)
+ goto err;
+
+ wl_surface_attach(vo->wl->surface, p->solid_buffer, 0, 0);
+
+ vo->hwdec_devs = hwdec_devices_create();
+ p->hwdec_ctx = (struct ra_hwdec_ctx) {
+ .log = p->log,
+ .global = p->global,
+ .ra_ctx = p->ctx,
+ };
+ ra_hwdec_ctx_init(&p->hwdec_ctx, vo->hwdec_devs, NULL, true);
+
+ // Loop through hardware accelerated formats and only request known
+ // supported formats.
+ for (int i = IMGFMT_VDPAU_OUTPUT; i < IMGFMT_AVPIXFMT_START; ++i) {
+ if (is_supported_fmt(i)) {
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = i,
+ .probing = false,
+ };
+ ra_hwdec_ctx_load_fmt(&p->hwdec_ctx, vo->hwdec_devs, &params);
+ }
+ }
+
+ for (int i = 0; i < p->hwdec_ctx.num_hwdecs; i++) {
+ struct ra_hwdec *hw = p->hwdec_ctx.hwdecs[i];
+ if (ra_get_native_resource(p->ctx->ra, "VADisplay")) {
+ p->hwdec_type = HWDEC_VAAPI;
+ } else if (strcmp(hw->driver->name, "drmprime") == 0) {
+ p->hwdec_type = HWDEC_DRMPRIME;
+ }
+ }
+
+ if (p->hwdec_type == HWDEC_NONE) {
+ MP_ERR(vo, "No valid hardware decoding driver could be loaded!\n");
+ goto err;
+ }
+
+ p->src = (struct mp_rect){0, 0, 0, 0};
+ return 0;
+
+err:
+ uninit(vo);
+ return -1;
+}
+
+const struct vo_driver video_out_dmabuf_wayland = {
+ .description = "Wayland dmabuf video output",
+ .name = "dmabuf-wayland",
+ .caps = VO_CAP_ROTATE90,
+ .frame_owner = true,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig2 = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_wayland_wakeup,
+ .wait_events = vo_wayland_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_drm.c b/video/out/vo_drm.c
new file mode 100644
index 0000000..aae73f7
--- /dev/null
+++ b/video/out/vo_drm.c
@@ -0,0 +1,458 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <sys/mman.h>
+#include <poll.h>
+#include <unistd.h>
+
+#include <drm_fourcc.h>
+#include <libswscale/swscale.h>
+
+#include "common/msg.h"
+#include "drm_atomic.h"
+#include "drm_common.h"
+#include "osdep/timer.h"
+#include "sub/osd.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "video/out/present_sync.h"
+#include "video/sws_utils.h"
+#include "vo.h"
+
+#define IMGFMT_XRGB8888 IMGFMT_BGR0
+#if BYTE_ORDER == BIG_ENDIAN
+#define IMGFMT_XRGB2101010 pixfmt2imgfmt(AV_PIX_FMT_GBRP10BE)
+#else
+#define IMGFMT_XRGB2101010 pixfmt2imgfmt(AV_PIX_FMT_GBRP10LE)
+#endif
+
+#define BYTES_PER_PIXEL 4
+#define BITS_PER_PIXEL 32
+
+struct drm_frame {
+ struct framebuffer *fb;
+};
+
+struct priv {
+ struct drm_frame **fb_queue;
+ unsigned int fb_queue_len;
+
+ uint32_t drm_format;
+ enum mp_imgfmt imgfmt;
+
+ struct mp_image *last_input;
+ struct mp_image *cur_frame;
+ struct mp_image *cur_frame_cropped;
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+ struct mp_sws_context *sws;
+
+ struct framebuffer **bufs;
+ int front_buf;
+ int buf_count;
+};
+
+static void destroy_framebuffer(int fd, struct framebuffer *fb)
+{
+ if (!fb)
+ return;
+
+ if (fb->map) {
+ munmap(fb->map, fb->size);
+ }
+ if (fb->id) {
+ drmModeRmFB(fd, fb->id);
+ }
+ if (fb->handle) {
+ struct drm_mode_destroy_dumb dreq = {
+ .handle = fb->handle,
+ };
+ drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
+ }
+}
+
+static struct framebuffer *setup_framebuffer(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+
+ struct framebuffer *fb = talloc_zero(drm, struct framebuffer);
+ fb->width = drm->mode.mode.hdisplay;
+ fb->height = drm->mode.mode.vdisplay;
+ fb->fd = drm->fd;
+ fb->handle = 0;
+
+ // create dumb buffer
+ struct drm_mode_create_dumb creq = {
+ .width = fb->width,
+ .height = fb->height,
+ .bpp = BITS_PER_PIXEL,
+ };
+
+ if (drmIoctl(drm->fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq) < 0) {
+ MP_ERR(vo, "Cannot create dumb buffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ fb->stride = creq.pitch;
+ fb->size = creq.size;
+ fb->handle = creq.handle;
+
+ // select format
+ if (drm->opts->drm_format == DRM_OPTS_FORMAT_XRGB2101010) {
+ p->drm_format = DRM_FORMAT_XRGB2101010;
+ p->imgfmt = IMGFMT_XRGB2101010;
+ } else {
+ p->drm_format = DRM_FORMAT_XRGB8888;;
+ p->imgfmt = IMGFMT_XRGB8888;
+ }
+
+ // create framebuffer object for the dumb-buffer
+ int ret = drmModeAddFB2(fb->fd, fb->width, fb->height,
+ p->drm_format,
+ (uint32_t[4]){fb->handle, 0, 0, 0},
+ (uint32_t[4]){fb->stride, 0, 0, 0},
+ (uint32_t[4]){0, 0, 0, 0},
+ &fb->id, 0);
+ if (ret) {
+ MP_ERR(vo, "Cannot create framebuffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ // prepare buffer for memory mapping
+ struct drm_mode_map_dumb mreq = {
+ .handle = fb->handle,
+ };
+ if (drmIoctl(drm->fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq)) {
+ MP_ERR(vo, "Cannot map dumb buffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ // perform actual memory mapping
+ fb->map = mmap(0, fb->size, PROT_READ | PROT_WRITE, MAP_SHARED,
+ drm->fd, mreq.offset);
+ if (fb->map == MAP_FAILED) {
+ MP_ERR(vo, "Cannot map dumb buffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ memset(fb->map, 0, fb->size);
+ return fb;
+
+err:
+ destroy_framebuffer(drm->fd, fb);
+ return NULL;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+
+ vo->dwidth =drm->fb->width;
+ vo->dheight = drm->fb->height;
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+
+ int w = p->dst.x1 - p->dst.x0;
+ int h = p->dst.y1 - p->dst.y0;
+
+ p->sws->src = *params;
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = p->imgfmt,
+ .w = w,
+ .h = h,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ talloc_free(p->cur_frame);
+ p->cur_frame = mp_image_alloc(p->imgfmt, drm->fb->width, drm->fb->height);
+ mp_image_params_guess_csp(&p->sws->dst);
+ mp_image_set_params(p->cur_frame, &p->sws->dst);
+ mp_image_set_size(p->cur_frame, drm->fb->width, drm->fb->height);
+
+ talloc_free(p->cur_frame_cropped);
+ p->cur_frame_cropped = mp_image_new_dummy_ref(p->cur_frame);
+ mp_image_crop_rc(p->cur_frame_cropped, p->dst);
+
+ talloc_free(p->last_input);
+ p->last_input = NULL;
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return -1;
+
+ vo->want_redraw = true;
+ return 0;
+}
+
+static struct framebuffer *get_new_fb(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->front_buf++;
+ p->front_buf %= p->buf_count;
+
+ return p->bufs[p->front_buf];
+}
+
+static void draw_image(struct vo *vo, mp_image_t *mpi, struct framebuffer *buf)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+
+ if (drm->active && buf != NULL) {
+ if (mpi) {
+ struct mp_image src = *mpi;
+ struct mp_rect src_rc = p->src;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
+ mp_image_crop_rc(&src, src_rc);
+
+ mp_image_clear(p->cur_frame, 0, 0, p->cur_frame->w, p->dst.y0);
+ mp_image_clear(p->cur_frame, 0, p->dst.y1, p->cur_frame->w, p->cur_frame->h);
+ mp_image_clear(p->cur_frame, 0, p->dst.y0, p->dst.x0, p->dst.y1);
+ mp_image_clear(p->cur_frame, p->dst.x1, p->dst.y0, p->cur_frame->w, p->dst.y1);
+
+ mp_sws_scale(p->sws, p->cur_frame_cropped, &src);
+ osd_draw_on_image(vo->osd, p->osd, src.pts, 0, p->cur_frame);
+ } else {
+ mp_image_clear(p->cur_frame, 0, 0, p->cur_frame->w, p->cur_frame->h);
+ osd_draw_on_image(vo->osd, p->osd, 0, 0, p->cur_frame);
+ }
+
+ if (p->drm_format == DRM_FORMAT_XRGB2101010) {
+ // Pack GBRP10 image into XRGB2101010 for DRM
+ const int w = p->cur_frame->w;
+ const int h = p->cur_frame->h;
+
+ const int g_padding = p->cur_frame->stride[0]/sizeof(uint16_t) - w;
+ const int b_padding = p->cur_frame->stride[1]/sizeof(uint16_t) - w;
+ const int r_padding = p->cur_frame->stride[2]/sizeof(uint16_t) - w;
+ const int fbuf_padding = buf->stride/sizeof(uint32_t) - w;
+
+ uint16_t *g_ptr = (uint16_t*)p->cur_frame->planes[0];
+ uint16_t *b_ptr = (uint16_t*)p->cur_frame->planes[1];
+ uint16_t *r_ptr = (uint16_t*)p->cur_frame->planes[2];
+ uint32_t *fbuf_ptr = (uint32_t*)buf->map;
+ for (unsigned y = 0; y < h; ++y) {
+ for (unsigned x = 0; x < w; ++x) {
+ *fbuf_ptr++ = (*r_ptr++ << 20) | (*g_ptr++ << 10) | (*b_ptr++);
+ }
+ g_ptr += g_padding;
+ b_ptr += b_padding;
+ r_ptr += r_padding;
+ fbuf_ptr += fbuf_padding;
+ }
+ } else { // p->drm_format == DRM_FORMAT_XRGB8888
+ memcpy_pic(buf->map, p->cur_frame->planes[0],
+ p->cur_frame->w * BYTES_PER_PIXEL, p->cur_frame->h,
+ buf->stride,
+ p->cur_frame->stride[0]);
+ }
+ }
+
+ if (mpi != p->last_input) {
+ talloc_free(p->last_input);
+ p->last_input = mpi;
+ }
+}
+
+static void enqueue_frame(struct vo *vo, struct framebuffer *fb)
+{
+ struct priv *p = vo->priv;
+
+ struct drm_frame *new_frame = talloc(p, struct drm_frame);
+ new_frame->fb = fb;
+ MP_TARRAY_APPEND(p, p->fb_queue, p->fb_queue_len, new_frame);
+}
+
+static void dequeue_frame(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ talloc_free(p->fb_queue[0]);
+ MP_TARRAY_REMOVE_AT(p->fb_queue, p->fb_queue_len, 0);
+}
+
+static void swapchain_step(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (p->fb_queue_len > 0) {
+ dequeue_frame(vo);
+ }
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_drm_state *drm = vo->drm;
+ struct priv *p = vo->priv;
+
+ if (!drm->active)
+ return;
+
+ drm->still = frame->still;
+
+ // we redraw the entire image when OSD needs to be redrawn
+ struct framebuffer *fb = p->bufs[p->front_buf];
+ const bool repeat = frame->repeat && !frame->redraw;
+ if (!repeat) {
+ fb = get_new_fb(vo);
+ draw_image(vo, mp_image_new_ref(frame->current), fb);
+ }
+
+ enqueue_frame(vo, fb);
+}
+
+static void queue_flip(struct vo *vo, struct drm_frame *frame)
+{
+ struct vo_drm_state *drm = vo->drm;
+
+ drm->fb = frame->fb;
+
+ int ret = drmModePageFlip(drm->fd, drm->crtc_id,
+ drm->fb->id, DRM_MODE_PAGE_FLIP_EVENT, drm);
+ if (ret)
+ MP_WARN(vo, "Failed to queue page flip: %s\n", mp_strerror(errno));
+ drm->waiting_for_flip = !ret;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+ const bool drain = drm->paused || drm->still;
+
+ if (!drm->active)
+ return;
+
+ while (drain || p->fb_queue_len > vo->opts->swapchain_depth) {
+ if (drm->waiting_for_flip) {
+ vo_drm_wait_on_flip(vo->drm);
+ swapchain_step(vo);
+ }
+ if (p->fb_queue_len <= 1)
+ break;
+ if (!p->fb_queue[1] || !p->fb_queue[1]->fb) {
+ MP_ERR(vo, "Hole in swapchain?\n");
+ swapchain_step(vo);
+ continue;
+ }
+ queue_flip(vo, p->fb_queue[1]);
+ }
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_drm_state *drm = vo->drm;
+ present_sync_get_info(drm->present, info);
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ vo_drm_uninit(vo);
+
+ while (p->fb_queue_len > 0) {
+ swapchain_step(vo);
+ }
+
+ talloc_free(p->last_input);
+ talloc_free(p->cur_frame);
+ talloc_free(p->cur_frame_cropped);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!vo_drm_init(vo))
+ goto err;
+
+ struct vo_drm_state *drm = vo->drm;
+ p->buf_count = vo->opts->swapchain_depth + 1;
+ p->bufs = talloc_zero_array(p, struct framebuffer *, p->buf_count);
+
+ p->front_buf = 0;
+ for (int i = 0; i < p->buf_count; i++) {
+ p->bufs[i] = setup_framebuffer(vo);
+ if (!p->bufs[i])
+ goto err;
+ }
+ drm->fb = p->bufs[0];
+
+ vo->drm->width = vo->drm->fb->width;
+ vo->drm->height = vo->drm->fb->height;
+
+ if (!vo_drm_acquire_crtc(vo->drm)) {
+ MP_ERR(vo, "Failed to set CRTC for connector %u: %s\n",
+ vo->drm->connector->connector_id, mp_strerror(errno));
+ goto err;
+ }
+
+ vo_drm_set_monitor_par(vo);
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+ return 0;
+
+err:
+ uninit(vo);
+ return -1;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return sws_isSupportedInput(imgfmt2pixfmt(format));
+}
+
+static int control(struct vo *vo, uint32_t request, void *arg)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ if (vo->config_ok)
+ reconfig(vo, vo->params);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int ret = vo_drm_control(vo, &events, request, arg);
+ vo_event(vo, events);
+ return ret;
+}
+
+const struct vo_driver video_out_drm = {
+ .name = "drm",
+ .description = "Direct Rendering Manager (software scaling)",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .uninit = uninit,
+ .wait_events = vo_drm_wait_events,
+ .wakeup = vo_drm_wakeup,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_gpu.c b/video/out/vo_gpu.c
new file mode 100644
index 0000000..c02e6e7
--- /dev/null
+++ b/video/out/vo_gpu.c
@@ -0,0 +1,336 @@
+/*
+ * Based on vo_gl.c by Reimar Doeffinger.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "options/m_config.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+
+#include "gpu/context.h"
+#include "gpu/hwdec.h"
+#include "gpu/video.h"
+
+struct gpu_priv {
+ struct mp_log *log;
+ struct ra_ctx *ctx;
+
+ char *context_name;
+ char *context_type;
+ struct gl_video *renderer;
+
+ int events;
+};
+static void resize(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+
+ MP_VERBOSE(vo, "Resize: %dx%d\n", vo->dwidth, vo->dheight);
+
+ struct mp_rect src, dst;
+ struct mp_osd_res osd;
+ vo_get_src_dst_rects(vo, &src, &dst, &osd);
+
+ gl_video_resize(p->renderer, &src, &dst, &osd);
+
+ int fb_depth = sw->fns->color_depth ? sw->fns->color_depth(sw) : 0;
+ if (fb_depth)
+ MP_VERBOSE(p, "Reported display depth: %d\n", fb_depth);
+ gl_video_set_fb_depth(p->renderer, fb_depth);
+
+ vo->want_redraw = true;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+
+ struct ra_fbo fbo;
+ if (!sw->fns->start_frame(sw, &fbo))
+ return;
+
+ gl_video_render_frame(p->renderer, frame, fbo, RENDER_FRAME_DEF);
+ if (!sw->fns->submit_frame(sw, frame)) {
+ MP_ERR(vo, "Failed presenting frame!\n");
+ return;
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+ sw->fns->swap_buffers(sw);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+ if (sw->fns->get_vsync)
+ sw->fns->get_vsync(sw, info);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct gpu_priv *p = vo->priv;
+ if (!gl_video_check_format(p->renderer, format))
+ return 0;
+ return 1;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct gpu_priv *p = vo->priv;
+
+ if (!p->ctx->fns->reconfig(p->ctx))
+ return -1;
+
+ resize(vo);
+ gl_video_config(p->renderer, params);
+
+ return 0;
+}
+
+static void request_hwdec_api(struct vo *vo, void *data)
+{
+ struct gpu_priv *p = vo->priv;
+ gl_video_load_hwdecs_for_img_fmt(p->renderer, vo->hwdec_devs, data);
+}
+
+static void call_request_hwdec_api(void *ctx,
+ struct hwdec_imgfmt_request *params)
+{
+ // Roundabout way to run hwdec loading on the VO thread.
+ // Redirects to request_hwdec_api().
+ vo_control(ctx, VOCTRL_LOAD_HWDEC_API, params);
+}
+
+static void get_and_update_icc_profile(struct gpu_priv *p)
+{
+ if (gl_video_icc_auto_enabled(p->renderer)) {
+ MP_VERBOSE(p, "Querying ICC profile...\n");
+ bstr icc = bstr0(NULL);
+ int r = p->ctx->fns->control(p->ctx, &p->events, VOCTRL_GET_ICC_PROFILE, &icc);
+
+ if (r != VO_NOTAVAIL) {
+ if (r == VO_FALSE) {
+ MP_WARN(p, "Could not retrieve an ICC profile.\n");
+ } else if (r == VO_NOTIMPL) {
+ MP_ERR(p, "icc-profile-auto not implemented on this platform.\n");
+ }
+
+ gl_video_set_icc_profile(p->renderer, icc);
+ }
+ }
+}
+
+static void get_and_update_ambient_lighting(struct gpu_priv *p)
+{
+ int lux;
+ int r = p->ctx->fns->control(p->ctx, &p->events, VOCTRL_GET_AMBIENT_LUX, &lux);
+ if (r == VO_TRUE) {
+ gl_video_set_ambient_lux(p->renderer, lux);
+ }
+ if (r != VO_TRUE && gl_video_gamma_auto_enabled(p->renderer)) {
+ MP_ERR(p, "gamma_auto option provided, but querying for ambient"
+ " lighting is not supported on this platform\n");
+ }
+}
+
+static void update_ra_ctx_options(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+
+ /* Only the alpha option has any runtime toggle ability. */
+ struct gl_video_opts *gl_opts = mp_get_config_group(p->ctx, vo->global, &gl_video_conf);
+ p->ctx->opts.want_alpha = gl_opts->alpha_mode == 1;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct gpu_priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT: {
+ struct vo_frame *frame = vo_get_current_vo_frame(vo);
+ if (frame)
+ gl_video_screenshot(p->renderer, frame, data);
+ talloc_free(frame);
+ return true;
+ }
+ case VOCTRL_LOAD_HWDEC_API:
+ request_hwdec_api(vo, data);
+ return true;
+ case VOCTRL_UPDATE_RENDER_OPTS: {
+ update_ra_ctx_options(vo);
+ gl_video_configure_queue(p->renderer, vo);
+ get_and_update_icc_profile(p);
+ if (p->ctx->fns->update_render_opts)
+ p->ctx->fns->update_render_opts(p->ctx);
+ vo->want_redraw = true;
+ return true;
+ }
+ case VOCTRL_RESET:
+ gl_video_reset(p->renderer);
+ return true;
+ case VOCTRL_PAUSE:
+ if (gl_video_showing_interpolated_frame(p->renderer))
+ vo->want_redraw = true;
+ return true;
+ case VOCTRL_PERFORMANCE_DATA:
+ gl_video_perfdata(p->renderer, (struct voctrl_performance_data *)data);
+ return true;
+ case VOCTRL_EXTERNAL_RESIZE:
+ p->ctx->fns->reconfig(p->ctx);
+ resize(vo);
+ return true;
+ }
+
+ int events = 0;
+ int r = p->ctx->fns->control(p->ctx, &events, request, data);
+ if (events & VO_EVENT_ICC_PROFILE_CHANGED) {
+ get_and_update_icc_profile(p);
+ vo->want_redraw = true;
+ }
+ if (events & VO_EVENT_AMBIENT_LIGHTING_CHANGED) {
+ get_and_update_ambient_lighting(p);
+ vo->want_redraw = true;
+ }
+ events |= p->events;
+ p->events = 0;
+ if (events & VO_EVENT_RESIZE)
+ resize(vo);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+
+ return r;
+}
+
+static void wakeup(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ if (p->ctx && p->ctx->fns->wakeup)
+ p->ctx->fns->wakeup(p->ctx);
+}
+
+static void wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct gpu_priv *p = vo->priv;
+ if (p->ctx && p->ctx->fns->wait_events) {
+ p->ctx->fns->wait_events(p->ctx, until_time_ns);
+ } else {
+ vo_wait_default(vo, until_time_ns);
+ }
+}
+
+static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct gpu_priv *p = vo->priv;
+
+ return gl_video_get_image(p->renderer, imgfmt, w, h, stride_align, flags);
+}
+
+static void uninit(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+
+ gl_video_uninit(p->renderer);
+ if (vo->hwdec_devs) {
+ hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+ ra_ctx_destroy(&p->ctx);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ p->log = vo->log;
+
+ struct ra_ctx_opts *ctx_opts = mp_get_config_group(vo, vo->global, &ra_ctx_conf);
+ struct gl_video_opts *gl_opts = mp_get_config_group(vo, vo->global, &gl_video_conf);
+ struct ra_ctx_opts opts = *ctx_opts;
+ opts.want_alpha = gl_opts->alpha_mode == 1;
+ p->ctx = ra_ctx_create(vo, opts);
+ talloc_free(ctx_opts);
+ talloc_free(gl_opts);
+ if (!p->ctx)
+ goto err_out;
+ assert(p->ctx->ra);
+ assert(p->ctx->swapchain);
+
+ p->renderer = gl_video_init(p->ctx->ra, vo->log, vo->global);
+ gl_video_set_osd_source(p->renderer, vo->osd);
+ gl_video_configure_queue(p->renderer, vo);
+
+ get_and_update_icc_profile(p);
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_set_loader(vo->hwdec_devs, call_request_hwdec_api, vo);
+
+ gl_video_init_hwdecs(p->renderer, p->ctx, vo->hwdec_devs, false);
+
+ return 0;
+
+err_out:
+ uninit(vo);
+ return -1;
+}
+
+const struct vo_driver video_out_gpu = {
+ .description = "Shader-based GPU Renderer",
+ .name = "gpu",
+ .caps = VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .get_image = get_image,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wait_events = wait_events,
+ .wakeup = wakeup,
+ .uninit = uninit,
+ .priv_size = sizeof(struct gpu_priv),
+};
diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
new file mode 100644
index 0000000..1dc1b18
--- /dev/null
+++ b/video/out/vo_gpu_next.c
@@ -0,0 +1,2104 @@
+/*
+ * Copyright (C) 2021 Niklas Haas
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <unistd.h>
+
+#include <libplacebo/colorspace.h>
+#include <libplacebo/options.h>
+#include <libplacebo/renderer.h>
+#include <libplacebo/shaders/lut.h>
+#include <libplacebo/shaders/icc.h>
+#include <libplacebo/utils/libav.h>
+#include <libplacebo/utils/frame_queue.h>
+
+#include "config.h"
+#include "common/common.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "stream/stream.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "video/out/placebo/ra_pl.h"
+#include "placebo/utils.h"
+#include "gpu/context.h"
+#include "gpu/hwdec.h"
+#include "gpu/video.h"
+#include "gpu/video_shaders.h"
+#include "sub/osd.h"
+#include "gpu_next/context.h"
+
+#if HAVE_GL && defined(PL_HAVE_OPENGL)
+#include <libplacebo/opengl.h>
+#include "video/out/opengl/ra_gl.h"
+#endif
+
+#if HAVE_D3D11 && defined(PL_HAVE_D3D11)
+#include <libplacebo/d3d11.h>
+#include "video/out/d3d11/ra_d3d11.h"
+#include "osdep/windows_utils.h"
+#endif
+
+
+struct osd_entry {
+ pl_tex tex;
+ struct pl_overlay_part *parts;
+ int num_parts;
+};
+
+struct osd_state {
+ struct osd_entry entries[MAX_OSD_PARTS];
+ struct pl_overlay overlays[MAX_OSD_PARTS];
+};
+
+struct scaler_params {
+ struct pl_filter_config config;
+};
+
+struct user_hook {
+ char *path;
+ const struct pl_hook *hook;
+};
+
+struct user_lut {
+ char *opt;
+ char *path;
+ int type;
+ struct pl_custom_lut *lut;
+};
+
+struct frame_info {
+ int count;
+ struct pl_dispatch_info info[VO_PASS_PERF_MAX];
+};
+
+struct cache {
+ char *path;
+ pl_cache cache;
+ uint64_t sig;
+};
+
+struct priv {
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct ra_ctx *ra_ctx;
+ struct gpu_ctx *context;
+ struct ra_hwdec_ctx hwdec_ctx;
+ struct ra_hwdec_mapper *hwdec_mapper;
+
+ // Allocated DR buffers
+ mp_mutex dr_lock;
+ pl_buf *dr_buffers;
+ int num_dr_buffers;
+
+ pl_log pllog;
+ pl_gpu gpu;
+ pl_renderer rr;
+ pl_queue queue;
+ pl_swapchain sw;
+ pl_fmt osd_fmt[SUBBITMAP_COUNT];
+ pl_tex *sub_tex;
+ int num_sub_tex;
+
+ struct mp_rect src, dst;
+ struct mp_osd_res osd_res;
+ struct osd_state osd_state;
+
+ uint64_t last_id;
+ uint64_t osd_sync;
+ double last_pts;
+ bool is_interpolated;
+ bool want_reset;
+ bool frame_pending;
+ bool redraw;
+
+ pl_options pars;
+ struct m_config_cache *opts_cache;
+ struct cache shader_cache, icc_cache;
+ struct mp_csp_equalizer_state *video_eq;
+ struct scaler_params scalers[SCALER_COUNT];
+ const struct pl_hook **hooks; // storage for `params.hooks`
+ enum mp_csp_levels output_levels;
+ char **raw_opts;
+
+ struct pl_icc_params icc_params;
+ char *icc_path;
+ pl_icc_object icc_profile;
+
+ struct user_lut image_lut;
+ struct user_lut target_lut;
+ struct user_lut lut;
+
+ // Cached shaders, preserved across options updates
+ struct user_hook *user_hooks;
+ int num_user_hooks;
+
+ // Performance data of last frame
+ struct frame_info perf_fresh;
+ struct frame_info perf_redraw;
+
+ bool delayed_peak;
+ bool inter_preserve;
+ bool target_hint;
+
+ float corner_rounding;
+};
+
+static void update_render_options(struct vo *vo);
+static void update_lut(struct priv *p, struct user_lut *lut);
+
+static pl_buf get_dr_buf(struct priv *p, const uint8_t *ptr)
+{
+ mp_mutex_lock(&p->dr_lock);
+
+ for (int i = 0; i < p->num_dr_buffers; i++) {
+ pl_buf buf = p->dr_buffers[i];
+ if (ptr >= buf->data && ptr < buf->data + buf->params.size) {
+ mp_mutex_unlock(&p->dr_lock);
+ return buf;
+ }
+ }
+
+ mp_mutex_unlock(&p->dr_lock);
+ return NULL;
+}
+
+static void free_dr_buf(void *opaque, uint8_t *data)
+{
+ struct priv *p = opaque;
+ mp_mutex_lock(&p->dr_lock);
+
+ for (int i = 0; i < p->num_dr_buffers; i++) {
+ if (p->dr_buffers[i]->data == data) {
+ pl_buf_destroy(p->gpu, &p->dr_buffers[i]);
+ MP_TARRAY_REMOVE_AT(p->dr_buffers, p->num_dr_buffers, i);
+ mp_mutex_unlock(&p->dr_lock);
+ return;
+ }
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct priv *p = vo->priv;
+ pl_gpu gpu = p->gpu;
+ if (!gpu->limits.thread_safe || !gpu->limits.max_mapped_size)
+ return NULL;
+
+ if ((flags & VO_DR_FLAG_HOST_CACHED) && !gpu->limits.host_cached)
+ return NULL;
+
+ stride_align = mp_lcm(stride_align, gpu->limits.align_tex_xfer_pitch);
+ stride_align = mp_lcm(stride_align, gpu->limits.align_tex_xfer_offset);
+ int size = mp_image_get_alloc_size(imgfmt, w, h, stride_align);
+ if (size < 0)
+ return NULL;
+
+ pl_buf buf = pl_buf_create(gpu, &(struct pl_buf_params) {
+ .memory_type = PL_BUF_MEM_HOST,
+ .host_mapped = true,
+ .size = size + stride_align,
+ });
+
+ if (!buf)
+ return NULL;
+
+ struct mp_image *mpi = mp_image_from_buffer(imgfmt, w, h, stride_align,
+ buf->data, buf->params.size,
+ p, free_dr_buf);
+ if (!mpi) {
+ pl_buf_destroy(gpu, &buf);
+ return NULL;
+ }
+
+ mp_mutex_lock(&p->dr_lock);
+ MP_TARRAY_APPEND(p, p->dr_buffers, p->num_dr_buffers, buf);
+ mp_mutex_unlock(&p->dr_lock);
+
+ return mpi;
+}
+
+static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi);
+
+static void update_overlays(struct vo *vo, struct mp_osd_res res,
+ int flags, enum pl_overlay_coords coords,
+ struct osd_state *state, struct pl_frame *frame,
+ struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ static const bool subfmt_all[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_LIBASS] = true,
+ [SUBBITMAP_BGRA] = true,
+ };
+
+ double pts = src ? src->pts : 0;
+ struct sub_bitmap_list *subs = osd_render(vo->osd, res, pts, flags, subfmt_all);
+
+ frame->overlays = state->overlays;
+ frame->num_overlays = 0;
+
+ for (int n = 0; n < subs->num_items; n++) {
+ const struct sub_bitmaps *item = subs->items[n];
+ if (!item->num_parts || !item->packed)
+ continue;
+ struct osd_entry *entry = &state->entries[item->render_index];
+ pl_fmt tex_fmt = p->osd_fmt[item->format];
+ if (!entry->tex)
+ MP_TARRAY_POP(p->sub_tex, p->num_sub_tex, &entry->tex);
+ bool ok = pl_tex_recreate(p->gpu, &entry->tex, &(struct pl_tex_params) {
+ .format = tex_fmt,
+ .w = MPMAX(item->packed_w, entry->tex ? entry->tex->params.w : 0),
+ .h = MPMAX(item->packed_h, entry->tex ? entry->tex->params.h : 0),
+ .host_writable = true,
+ .sampleable = true,
+ });
+ if (!ok) {
+ MP_ERR(vo, "Failed recreating OSD texture!\n");
+ break;
+ }
+ ok = pl_tex_upload(p->gpu, &(struct pl_tex_transfer_params) {
+ .tex = entry->tex,
+ .rc = { .x1 = item->packed_w, .y1 = item->packed_h, },
+ .row_pitch = item->packed->stride[0],
+ .ptr = item->packed->planes[0],
+ });
+ if (!ok) {
+ MP_ERR(vo, "Failed uploading OSD texture!\n");
+ break;
+ }
+
+ entry->num_parts = 0;
+ for (int i = 0; i < item->num_parts; i++) {
+ const struct sub_bitmap *b = &item->parts[i];
+ uint32_t c = b->libass.color;
+ struct pl_overlay_part part = {
+ .src = { b->src_x, b->src_y, b->src_x + b->w, b->src_y + b->h },
+ .dst = { b->x, b->y, b->x + b->dw, b->y + b->dh },
+ .color = {
+ (c >> 24) / 255.0,
+ ((c >> 16) & 0xFF) / 255.0,
+ ((c >> 8) & 0xFF) / 255.0,
+ 1.0 - (c & 0xFF) / 255.0,
+ }
+ };
+ MP_TARRAY_APPEND(p, entry->parts, entry->num_parts, part);
+ }
+
+ struct pl_overlay *ol = &state->overlays[frame->num_overlays++];
+ *ol = (struct pl_overlay) {
+ .tex = entry->tex,
+ .parts = entry->parts,
+ .num_parts = entry->num_parts,
+ .color = {
+ .primaries = PL_COLOR_PRIM_BT_709,
+ .transfer = PL_COLOR_TRC_SRGB,
+ },
+ .coords = coords,
+ };
+
+ switch (item->format) {
+ case SUBBITMAP_BGRA:
+ ol->mode = PL_OVERLAY_NORMAL;
+ ol->repr.alpha = PL_ALPHA_PREMULTIPLIED;
+ // Infer bitmap colorspace from source
+ if (src) {
+ ol->color = get_mpi_csp(vo, src);
+ // Seems like HDR subtitles are targeting SDR white
+ if (pl_color_transfer_is_hdr(ol->color.transfer)) {
+ ol->color.hdr = (struct pl_hdr_metadata) {
+ .max_luma = PL_COLOR_SDR_WHITE,
+ };
+ }
+ }
+ break;
+ case SUBBITMAP_LIBASS:
+ ol->mode = PL_OVERLAY_MONOCHROME;
+ ol->repr.alpha = PL_ALPHA_INDEPENDENT;
+ break;
+ }
+ }
+
+ talloc_free(subs);
+}
+
+struct frame_priv {
+ struct vo *vo;
+ struct osd_state subs;
+ uint64_t osd_sync;
+ struct ra_hwdec *hwdec;
+};
+
+static int plane_data_from_imgfmt(struct pl_plane_data out_data[4],
+ struct pl_bit_encoding *out_bits,
+ enum mp_imgfmt imgfmt)
+{
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(imgfmt);
+ if (!desc.num_planes || !(desc.flags & MP_IMGFLAG_HAS_COMPS))
+ return 0;
+
+ if (desc.flags & MP_IMGFLAG_HWACCEL)
+ return 0; // HW-accelerated frames need to be mapped differently
+
+ if (!(desc.flags & MP_IMGFLAG_NE))
+ return 0; // GPU endianness follows the host's
+
+ if (desc.flags & MP_IMGFLAG_PAL)
+ return 0; // Palette formats (currently) not supported in libplacebo
+
+ if ((desc.flags & MP_IMGFLAG_TYPE_FLOAT) && (desc.flags & MP_IMGFLAG_YUV))
+ return 0; // Floating-point YUV (currently) unsupported
+
+ bool has_bits = false;
+ bool any_padded = false;
+
+ for (int p = 0; p < desc.num_planes; p++) {
+ struct pl_plane_data *data = &out_data[p];
+ struct mp_imgfmt_comp_desc sorted[MP_NUM_COMPONENTS];
+ int num_comps = 0;
+ if (desc.bpp[p] % 8)
+ return 0; // Pixel size is not byte-aligned
+
+ for (int c = 0; c < mp_imgfmt_desc_get_num_comps(&desc); c++) {
+ if (desc.comps[c].plane != p)
+ continue;
+
+ data->component_map[num_comps] = c;
+ sorted[num_comps] = desc.comps[c];
+ num_comps++;
+
+ // Sort components by offset order, while keeping track of the
+ // semantic mapping in `data->component_map`
+ for (int i = num_comps - 1; i > 0; i--) {
+ if (sorted[i].offset >= sorted[i - 1].offset)
+ break;
+ MPSWAP(struct mp_imgfmt_comp_desc, sorted[i], sorted[i - 1]);
+ MPSWAP(int, data->component_map[i], data->component_map[i - 1]);
+ }
+ }
+
+ uint64_t total_bits = 0;
+
+ // Fill in the pl_plane_data fields for each component
+ memset(data->component_size, 0, sizeof(data->component_size));
+ for (int c = 0; c < num_comps; c++) {
+ data->component_size[c] = sorted[c].size;
+ data->component_pad[c] = sorted[c].offset - total_bits;
+ total_bits += data->component_pad[c] + data->component_size[c];
+ any_padded |= sorted[c].pad;
+
+ // Ignore bit encoding of alpha channel
+ if (!out_bits || data->component_map[c] == PL_CHANNEL_A)
+ continue;
+
+ struct pl_bit_encoding bits = {
+ .sample_depth = data->component_size[c],
+ .color_depth = sorted[c].size - abs(sorted[c].pad),
+ .bit_shift = MPMAX(sorted[c].pad, 0),
+ };
+
+ if (!has_bits) {
+ *out_bits = bits;
+ has_bits = true;
+ } else {
+ if (!pl_bit_encoding_equal(out_bits, &bits)) {
+ // Bit encoding differs between components/planes,
+ // cannot handle this
+ *out_bits = (struct pl_bit_encoding) {0};
+ out_bits = NULL;
+ }
+ }
+ }
+
+ data->pixel_stride = desc.bpp[p] / 8;
+ data->type = (desc.flags & MP_IMGFLAG_TYPE_FLOAT)
+ ? PL_FMT_FLOAT
+ : PL_FMT_UNORM;
+ }
+
+ if (any_padded && !out_bits)
+ return 0; // can't handle padded components without `pl_bit_encoding`
+
+ return desc.num_planes;
+}
+
+static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi)
+{
+ struct pl_color_space csp = {
+ .primaries = mp_prim_to_pl(mpi->params.color.primaries),
+ .transfer = mp_trc_to_pl(mpi->params.color.gamma),
+ .hdr = mpi->params.color.hdr,
+ };
+ return csp;
+}
+
+static bool hwdec_reconfig(struct priv *p, struct ra_hwdec *hwdec,
+ const struct mp_image_params *par)
+{
+ if (p->hwdec_mapper) {
+ if (mp_image_params_equal(par, &p->hwdec_mapper->src_params)) {
+ return p->hwdec_mapper;
+ } else {
+ ra_hwdec_mapper_free(&p->hwdec_mapper);
+ }
+ }
+
+ p->hwdec_mapper = ra_hwdec_mapper_create(hwdec, par);
+ if (!p->hwdec_mapper) {
+ MP_ERR(p, "Initializing texture for hardware decoding failed.\n");
+ return NULL;
+ }
+
+ return p->hwdec_mapper;
+}
+
+// For RAs not based on ra_pl, this creates a new pl_tex wrapper
+static pl_tex hwdec_get_tex(struct priv *p, int n)
+{
+ struct ra_tex *ratex = p->hwdec_mapper->tex[n];
+ struct ra *ra = p->hwdec_mapper->ra;
+ if (ra_pl_get(ra))
+ return (pl_tex) ratex->priv;
+
+#if HAVE_GL && defined(PL_HAVE_OPENGL)
+ if (ra_is_gl(ra) && pl_opengl_get(p->gpu)) {
+ struct pl_opengl_wrap_params par = {
+ .width = ratex->params.w,
+ .height = ratex->params.h,
+ };
+
+ ra_gl_get_format(ratex->params.format, &par.iformat,
+ &(GLenum){0}, &(GLenum){0});
+ ra_gl_get_raw_tex(ra, ratex, &par.texture, &par.target);
+ return pl_opengl_wrap(p->gpu, &par);
+ }
+#endif
+
+#if HAVE_D3D11 && defined(PL_HAVE_D3D11)
+ if (ra_is_d3d11(ra)) {
+ int array_slice = 0;
+ ID3D11Resource *res = ra_d3d11_get_raw_tex(ra, ratex, &array_slice);
+ pl_tex tex = pl_d3d11_wrap(p->gpu, pl_d3d11_wrap_params(
+ .tex = res,
+ .array_slice = array_slice,
+ .fmt = ra_d3d11_get_format(ratex->params.format),
+ .w = ratex->params.w,
+ .h = ratex->params.h,
+ ));
+ SAFE_RELEASE(res);
+ return tex;
+ }
+#endif
+
+ MP_ERR(p, "Failed mapping hwdec frame? Open a bug!\n");
+ return false;
+}
+
+static bool hwdec_acquire(pl_gpu gpu, struct pl_frame *frame)
+{
+ struct mp_image *mpi = frame->user_data;
+ struct frame_priv *fp = mpi->priv;
+ struct priv *p = fp->vo->priv;
+ if (!hwdec_reconfig(p, fp->hwdec, &mpi->params))
+ return false;
+
+ if (ra_hwdec_mapper_map(p->hwdec_mapper, mpi) < 0) {
+ MP_ERR(p, "Mapping hardware decoded surface failed.\n");
+ return false;
+ }
+
+ for (int n = 0; n < frame->num_planes; n++) {
+ if (!(frame->planes[n].texture = hwdec_get_tex(p, n)))
+ return false;
+ }
+
+ return true;
+}
+
+static void hwdec_release(pl_gpu gpu, struct pl_frame *frame)
+{
+ struct mp_image *mpi = frame->user_data;
+ struct frame_priv *fp = mpi->priv;
+ struct priv *p = fp->vo->priv;
+ if (!ra_pl_get(p->hwdec_mapper->ra)) {
+ for (int n = 0; n < frame->num_planes; n++)
+ pl_tex_destroy(p->gpu, &frame->planes[n].texture);
+ }
+
+ ra_hwdec_mapper_unmap(p->hwdec_mapper);
+}
+
+static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src,
+ struct pl_frame *frame)
+{
+ struct mp_image *mpi = src->frame_data;
+ const struct mp_image_params *par = &mpi->params;
+ struct frame_priv *fp = mpi->priv;
+ struct vo *vo = fp->vo;
+ struct priv *p = vo->priv;
+
+ fp->hwdec = ra_hwdec_get(&p->hwdec_ctx, mpi->imgfmt);
+ if (fp->hwdec) {
+ // Note: We don't actually need the mapper to map the frame yet, we
+ // only reconfig the mapper here (potentially creating it) to access
+ // `dst_params`. In practice, though, this should not matter unless the
+ // image format changes mid-stream.
+ if (!hwdec_reconfig(p, fp->hwdec, &mpi->params)) {
+ talloc_free(mpi);
+ return false;
+ }
+
+ par = &p->hwdec_mapper->dst_params;
+ }
+
+ *frame = (struct pl_frame) {
+ .color = get_mpi_csp(vo, mpi),
+ .repr = {
+ .sys = mp_csp_to_pl(par->color.space),
+ .levels = mp_levels_to_pl(par->color.levels),
+ .alpha = mp_alpha_to_pl(par->alpha),
+ },
+ .profile = {
+ .data = mpi->icc_profile ? mpi->icc_profile->data : NULL,
+ .len = mpi->icc_profile ? mpi->icc_profile->size : 0,
+ },
+ .rotation = par->rotate / 90,
+ .user_data = mpi,
+ };
+
+ // mp_image, like AVFrame, likes communicating RGB/XYZ/YCbCr status
+ // implicitly via the image format, rather than the actual tagging.
+ switch (mp_imgfmt_get_forced_csp(par->imgfmt)) {
+ case MP_CSP_RGB:
+ frame->repr.sys = PL_COLOR_SYSTEM_RGB;
+ frame->repr.levels = PL_COLOR_LEVELS_FULL;
+ break;
+ case MP_CSP_XYZ:
+ frame->repr.sys = PL_COLOR_SYSTEM_XYZ;
+ break;
+ case MP_CSP_AUTO:
+ if (!frame->repr.sys)
+ frame->repr.sys = pl_color_system_guess_ycbcr(par->w, par->h);
+ break;
+ default: break;
+ }
+
+ if (fp->hwdec) {
+
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(par->imgfmt);
+ frame->acquire = hwdec_acquire;
+ frame->release = hwdec_release;
+ frame->num_planes = desc.num_planes;
+ for (int n = 0; n < frame->num_planes; n++) {
+ struct pl_plane *plane = &frame->planes[n];
+ int *map = plane->component_mapping;
+ for (int c = 0; c < mp_imgfmt_desc_get_num_comps(&desc); c++) {
+ if (desc.comps[c].plane != n)
+ continue;
+
+ // Sort by component offset
+ uint8_t offset = desc.comps[c].offset;
+ int index = plane->components++;
+ while (index > 0 && desc.comps[map[index - 1]].offset > offset) {
+ map[index] = map[index - 1];
+ index--;
+ }
+ map[index] = c;
+ }
+ }
+
+ } else { // swdec
+
+ struct pl_plane_data data[4] = {0};
+ frame->num_planes = plane_data_from_imgfmt(data, &frame->repr.bits, mpi->imgfmt);
+ for (int n = 0; n < frame->num_planes; n++) {
+ struct pl_plane *plane = &frame->planes[n];
+ data[n].width = mp_image_plane_w(mpi, n);
+ data[n].height = mp_image_plane_h(mpi, n);
+ if (mpi->stride[n] < 0) {
+ data[n].pixels = mpi->planes[n] + (data[n].height - 1) * mpi->stride[n];
+ data[n].row_stride = -mpi->stride[n];
+ plane->flipped = true;
+ } else {
+ data[n].pixels = mpi->planes[n];
+ data[n].row_stride = mpi->stride[n];
+ }
+
+ pl_buf buf = get_dr_buf(p, data[n].pixels);
+ if (buf) {
+ data[n].buf = buf;
+ data[n].buf_offset = (uint8_t *) data[n].pixels - buf->data;
+ data[n].pixels = NULL;
+ } else if (gpu->limits.callbacks) {
+ data[n].callback = talloc_free;
+ data[n].priv = mp_image_new_ref(mpi);
+ }
+
+ if (!pl_upload_plane(gpu, plane, &tex[n], &data[n])) {
+ MP_ERR(vo, "Failed uploading frame!\n");
+ talloc_free(data[n].priv);
+ talloc_free(mpi);
+ return false;
+ }
+ }
+
+ }
+
+ // Update chroma location, must be done after initializing planes
+ pl_frame_set_chroma_location(frame, mp_chroma_to_pl(par->chroma_location));
+
+ // Set the frame DOVI metadata
+ mp_map_dovi_metadata_to_pl(mpi, frame);
+
+ if (mpi->film_grain)
+ pl_film_grain_from_av(&frame->film_grain, (AVFilmGrainParams *) mpi->film_grain->data);
+
+ // Compute a unique signature for any attached ICC profile. Wasteful in
+ // theory if the ICC profile is the same for multiple frames, but in
+ // practice ICC profiles are overwhelmingly going to be attached to
+ // still images so it shouldn't matter.
+ pl_icc_profile_compute_signature(&frame->profile);
+
+ // Update LUT attached to this frame
+ update_lut(p, &p->image_lut);
+ frame->lut = p->image_lut.lut;
+ frame->lut_type = p->image_lut.type;
+ return true;
+}
+
+static void unmap_frame(pl_gpu gpu, struct pl_frame *frame,
+ const struct pl_source_frame *src)
+{
+ struct mp_image *mpi = src->frame_data;
+ struct frame_priv *fp = mpi->priv;
+ struct priv *p = fp->vo->priv;
+ for (int i = 0; i < MP_ARRAY_SIZE(fp->subs.entries); i++) {
+ pl_tex tex = fp->subs.entries[i].tex;
+ if (tex)
+ MP_TARRAY_APPEND(p, p->sub_tex, p->num_sub_tex, tex);
+ }
+ talloc_free(mpi);
+}
+
+static void discard_frame(const struct pl_source_frame *src)
+{
+ struct mp_image *mpi = src->frame_data;
+ talloc_free(mpi);
+}
+
+static void info_callback(void *priv, const struct pl_render_info *info)
+{
+ struct vo *vo = priv;
+ struct priv *p = vo->priv;
+ if (info->index >= VO_PASS_PERF_MAX)
+ return; // silently ignore clipped passes, whatever
+
+ struct frame_info *frame;
+ switch (info->stage) {
+ case PL_RENDER_STAGE_FRAME: frame = &p->perf_fresh; break;
+ case PL_RENDER_STAGE_BLEND: frame = &p->perf_redraw; break;
+ default: abort();
+ }
+
+ frame->count = info->index + 1;
+ pl_dispatch_info_move(&frame->info[info->index], info->pass);
+}
+
+static void update_options(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ pl_options pars = p->pars;
+ if (m_config_cache_update(p->opts_cache))
+ update_render_options(vo);
+
+ update_lut(p, &p->lut);
+ pars->params.lut = p->lut.lut;
+ pars->params.lut_type = p->lut.type;
+
+ // Update equalizer state
+ struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
+ mp_csp_equalizer_state_get(p->video_eq, &cparams);
+ pars->color_adjustment.brightness = cparams.brightness;
+ pars->color_adjustment.contrast = cparams.contrast;
+ pars->color_adjustment.hue = cparams.hue;
+ pars->color_adjustment.saturation = cparams.saturation;
+ pars->color_adjustment.gamma = cparams.gamma;
+ p->output_levels = cparams.levels_out;
+
+ for (char **kv = p->raw_opts; kv && kv[0]; kv += 2)
+ pl_options_set_str(pars, kv[0], kv[1]);
+}
+
+static void apply_target_contrast(struct priv *p, struct pl_color_space *color)
+{
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+
+ // Auto mode, leave as is
+ if (!opts->target_contrast)
+ return;
+
+ // Infinite contrast
+ if (opts->target_contrast == -1) {
+ color->hdr.min_luma = 1e-7;
+ return;
+ }
+
+ // Infer max_luma for current pl_color_space
+ pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
+ .color = color,
+ // with HDR10 meta to respect value if already set
+ .metadata = PL_HDR_METADATA_HDR10,
+ .scaling = PL_HDR_NITS,
+ .out_max = &color->hdr.max_luma
+ ));
+
+ color->hdr.min_luma = color->hdr.max_luma / opts->target_contrast;
+}
+
+static void apply_target_options(struct priv *p, struct pl_frame *target)
+{
+ update_lut(p, &p->target_lut);
+ target->lut = p->target_lut.lut;
+ target->lut_type = p->target_lut.type;
+
+ // Colorspace overrides
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ if (p->output_levels)
+ target->repr.levels = mp_levels_to_pl(p->output_levels);
+ if (opts->target_prim)
+ target->color.primaries = mp_prim_to_pl(opts->target_prim);
+ if (opts->target_trc)
+ target->color.transfer = mp_trc_to_pl(opts->target_trc);
+ // If swapchain returned a value use this, override is used in hint
+ if (opts->target_peak && !target->color.hdr.max_luma)
+ target->color.hdr.max_luma = opts->target_peak;
+ if (!target->color.hdr.min_luma)
+ apply_target_contrast(p, &target->color);
+ if (opts->target_gamut) {
+ // Ensure resulting gamut still fits inside container
+ const struct pl_raw_primaries *gamut, *container;
+ gamut = pl_raw_primaries_get(mp_prim_to_pl(opts->target_gamut));
+ container = pl_raw_primaries_get(target->color.primaries);
+ target->color.hdr.prim = pl_primaries_clip(gamut, container);
+ }
+ if (opts->dither_depth > 0) {
+ struct pl_bit_encoding *tbits = &target->repr.bits;
+ tbits->color_depth += opts->dither_depth - tbits->sample_depth;
+ tbits->sample_depth = opts->dither_depth;
+ }
+
+ if (opts->icc_opts->icc_use_luma) {
+ p->icc_params.max_luma = 0.0f;
+ } else {
+ pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
+ .color = &target->color,
+ .metadata = PL_HDR_METADATA_HDR10, // use only static HDR nits
+ .scaling = PL_HDR_NITS,
+ .out_max = &p->icc_params.max_luma,
+ ));
+ }
+
+ pl_icc_update(p->pllog, &p->icc_profile, NULL, &p->icc_params);
+ target->icc = p->icc_profile;
+}
+
+static void apply_crop(struct pl_frame *frame, struct mp_rect crop,
+ int width, int height)
+{
+ frame->crop = (struct pl_rect2df) {
+ .x0 = crop.x0,
+ .y0 = crop.y0,
+ .x1 = crop.x1,
+ .y1 = crop.y1,
+ };
+
+ // mpv gives us rotated/flipped rects, libplacebo expects unrotated
+ pl_rect2df_rotate(&frame->crop, -frame->rotation);
+ if (frame->crop.x1 < frame->crop.x0) {
+ frame->crop.x0 = width - frame->crop.x0;
+ frame->crop.x1 = width - frame->crop.x1;
+ }
+
+ if (frame->crop.y1 < frame->crop.y0) {
+ frame->crop.y0 = height - frame->crop.y0;
+ frame->crop.y1 = height - frame->crop.y1;
+ }
+}
+
+static void update_tm_viz(struct pl_color_map_params *params,
+ const struct pl_frame *target)
+{
+ if (!params->visualize_lut)
+ return;
+
+ // Use right half of sceen for TM visualization, constrain to 1:1 AR
+ const float out_w = fabsf(pl_rect_w(target->crop));
+ const float out_h = fabsf(pl_rect_h(target->crop));
+ const float size = MPMIN(out_w / 2.0f, out_h);
+ params->visualize_rect = (pl_rect2df) {
+ .x0 = 1.0f - size / out_w,
+ .x1 = 1.0f,
+ .y0 = 0.0f,
+ .y1 = size / out_h,
+ };
+
+ // Visualize red-blue plane
+ params->visualize_hue = M_PI / 4.0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ pl_options pars = p->pars;
+ pl_gpu gpu = p->gpu;
+ update_options(vo);
+
+ struct pl_render_params params = pars->params;
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ bool will_redraw = frame->display_synced && frame->num_vsyncs > 1;
+ bool cache_frame = will_redraw || frame->still;
+ bool can_interpolate = opts->interpolation && frame->display_synced &&
+ !frame->still && frame->num_frames > 1;
+ double pts_offset = can_interpolate ? frame->ideal_frame_vsync : 0;
+ params.info_callback = info_callback;
+ params.info_priv = vo;
+ params.skip_caching_single_frame = !cache_frame;
+ params.preserve_mixing_cache = p->inter_preserve && !frame->still;
+ if (frame->still)
+ params.frame_mixer = NULL;
+
+ // pl_queue advances its internal virtual PTS and culls available frames
+ // based on this value and the VPS/FPS ratio. Requesting a non-monotonic PTS
+ // is an invalid use of pl_queue. Reset it if this happens in an attempt to
+ // recover as much as possible. Ideally, this should never occur, and if it
+ // does, it should be corrected. The ideal_frame_vsync may be negative if
+ // the last draw did not align perfectly with the vsync. In this case, we
+ // should have the previous frame available in pl_queue, or a reset is
+ // already requested. Clamp the check to 0, as we don't have the previous
+ // frame in vo_frame anyway.
+ struct pl_source_frame vpts;
+ if (frame->current && !p->want_reset) {
+ if (pl_queue_peek(p->queue, 0, &vpts) &&
+ frame->current->pts + MPMAX(0, pts_offset) < vpts.pts)
+ {
+ MP_VERBOSE(vo, "Forcing queue refill, PTS(%f + %f | %f) < VPTS(%f)\n",
+ frame->current->pts, pts_offset,
+ frame->ideal_frame_vsync_duration, vpts.pts);
+ p->want_reset = true;
+ }
+ }
+
+ // Push all incoming frames into the frame queue
+ for (int n = 0; n < frame->num_frames; n++) {
+ int id = frame->frame_id + n;
+
+ if (p->want_reset) {
+ pl_renderer_flush_cache(p->rr);
+ pl_queue_reset(p->queue);
+ p->last_pts = 0.0;
+ p->last_id = 0;
+ p->want_reset = false;
+ }
+
+ if (id <= p->last_id)
+ continue; // ignore already seen frames
+
+ struct mp_image *mpi = mp_image_new_ref(frame->frames[n]);
+ struct frame_priv *fp = talloc_zero(mpi, struct frame_priv);
+ mpi->priv = fp;
+ fp->vo = vo;
+
+ pl_queue_push(p->queue, &(struct pl_source_frame) {
+ .pts = mpi->pts,
+ .duration = can_interpolate ? frame->approx_duration : 0,
+ .frame_data = mpi,
+ .map = map_frame,
+ .unmap = unmap_frame,
+ .discard = discard_frame,
+ });
+
+ p->last_id = id;
+ }
+
+ if (p->target_hint && frame->current) {
+ struct pl_color_space hint = get_mpi_csp(vo, frame->current);
+ if (opts->target_prim)
+ hint.primaries = mp_prim_to_pl(opts->target_prim);
+ if (opts->target_trc)
+ hint.transfer = mp_trc_to_pl(opts->target_trc);
+ if (opts->target_peak)
+ hint.hdr.max_luma = opts->target_peak;
+ apply_target_contrast(p, &hint);
+ pl_swapchain_colorspace_hint(p->sw, &hint);
+ } else if (!p->target_hint) {
+ pl_swapchain_colorspace_hint(p->sw, NULL);
+ }
+
+ struct pl_swapchain_frame swframe;
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ bool should_draw = sw->fns->start_frame(sw, NULL); // for wayland logic
+ if (!should_draw || !pl_swapchain_start_frame(p->sw, &swframe)) {
+ if (frame->current) {
+ // Advance the queue state to the current PTS to discard unused frames
+ pl_queue_update(p->queue, NULL, pl_queue_params(
+ .pts = frame->current->pts + pts_offset,
+ .radius = pl_frame_mix_radius(&params),
+ .vsync_duration = can_interpolate ? frame->ideal_frame_vsync_duration : 0,
+#if PL_API_VER >= 340
+ .drift_compensation = 0,
+#endif
+ ));
+ }
+ return;
+ }
+
+ bool valid = false;
+ p->is_interpolated = false;
+
+ // Calculate target
+ struct pl_frame target;
+ pl_frame_from_swapchain(&target, &swframe);
+ apply_target_options(p, &target);
+ update_overlays(vo, p->osd_res,
+ (frame->current && opts->blend_subs) ? OSD_DRAW_OSD_ONLY : 0,
+ PL_OVERLAY_COORDS_DST_FRAME, &p->osd_state, &target, frame->current);
+ apply_crop(&target, p->dst, swframe.fbo->params.w, swframe.fbo->params.h);
+ update_tm_viz(&pars->color_map_params, &target);
+
+ struct pl_frame_mix mix = {0};
+ if (frame->current) {
+ // Update queue state
+ struct pl_queue_params qparams = *pl_queue_params(
+ .pts = frame->current->pts + pts_offset,
+ .radius = pl_frame_mix_radius(&params),
+ .vsync_duration = can_interpolate ? frame->ideal_frame_vsync_duration : 0,
+ .interpolation_threshold = opts->interpolation_threshold,
+#if PL_API_VER >= 340
+ .drift_compensation = 0,
+#endif
+ );
+
+ // Depending on the vsync ratio, we may be up to half of the vsync
+ // duration before the current frame time. This works fine because
+ // pl_queue will have this frame, unless it's after a reset event. In
+ // this case, start from the first available frame.
+ struct pl_source_frame first;
+ if (pl_queue_peek(p->queue, 0, &first) && qparams.pts < first.pts) {
+ if (first.pts != frame->current->pts)
+ MP_VERBOSE(vo, "Current PTS(%f) != VPTS(%f)\n", frame->current->pts, first.pts);
+ MP_VERBOSE(vo, "Clamping first frame PTS from %f to %f\n", qparams.pts, first.pts);
+ qparams.pts = first.pts;
+ }
+ p->last_pts = qparams.pts;
+
+ switch (pl_queue_update(p->queue, &mix, &qparams)) {
+ case PL_QUEUE_ERR:
+ MP_ERR(vo, "Failed updating frames!\n");
+ goto done;
+ case PL_QUEUE_EOF:
+ abort(); // we never signal EOF
+ case PL_QUEUE_MORE:
+ // This is expected to happen semi-frequently near the start and
+ // end of a file, so only log it at high verbosity and move on.
+ MP_DBG(vo, "Render queue underrun.\n");
+ break;
+ case PL_QUEUE_OK:
+ break;
+ }
+
+ // Update source crop and overlays on all existing frames. We
+ // technically own the `pl_frame` struct so this is kosher. This could
+ // be partially avoided by instead flushing the queue on resizes, but
+ // doing it this way avoids unnecessarily re-uploading frames.
+ for (int i = 0; i < mix.num_frames; i++) {
+ struct pl_frame *image = (struct pl_frame *) mix.frames[i];
+ struct mp_image *mpi = image->user_data;
+ struct frame_priv *fp = mpi->priv;
+ apply_crop(image, p->src, vo->params->w, vo->params->h);
+ if (opts->blend_subs) {
+ if (frame->redraw || fp->osd_sync < p->osd_sync) {
+ float rx = pl_rect_w(p->dst) / pl_rect_w(image->crop);
+ float ry = pl_rect_h(p->dst) / pl_rect_h(image->crop);
+ struct mp_osd_res res = {
+ .w = pl_rect_w(p->dst),
+ .h = pl_rect_h(p->dst),
+ .ml = -image->crop.x0 * rx,
+ .mr = (image->crop.x1 - vo->params->w) * rx,
+ .mt = -image->crop.y0 * ry,
+ .mb = (image->crop.y1 - vo->params->h) * ry,
+ .display_par = 1.0,
+ };
+ // TODO: fix this doing pointless updates
+ if (frame->redraw)
+ p->osd_sync++;
+ update_overlays(vo, res, OSD_DRAW_SUB_ONLY,
+ PL_OVERLAY_COORDS_DST_CROP,
+ &fp->subs, image, mpi);
+ fp->osd_sync = p->osd_sync;
+ }
+ } else {
+ // Disable overlays when blend_subs is disabled
+ image->num_overlays = 0;
+ fp->osd_sync = 0;
+ }
+
+ // Update the frame signature to include the current OSD sync
+ // value, in order to disambiguate between identical frames with
+ // modified OSD. Shift the OSD sync value by a lot to avoid
+ // collisions with low signature values.
+ //
+ // This is safe to do because `pl_frame_mix.signature` lives in
+ // temporary memory that is only valid for this `pl_queue_update`.
+ ((uint64_t *) mix.signatures)[i] ^= fp->osd_sync << 48;
+ }
+ }
+
+ // Render frame
+ if (!pl_render_image_mix(p->rr, &mix, &target, &params)) {
+ MP_ERR(vo, "Failed rendering frame!\n");
+ goto done;
+ }
+
+ const struct pl_frame *cur_frame = pl_frame_mix_nearest(&mix);
+ if (cur_frame && vo->params) {
+ vo->params->color.hdr = cur_frame->color.hdr;
+ // Augment metadata with peak detection max_pq_y / avg_pq_y
+ pl_renderer_get_hdr_metadata(p->rr, &vo->params->color.hdr);
+ }
+
+ p->is_interpolated = pts_offset != 0 && mix.num_frames > 1;
+ valid = true;
+ // fall through
+
+done:
+ if (!valid) // clear with purple to indicate error
+ pl_tex_clear(gpu, swframe.fbo, (float[4]){ 0.5, 0.0, 1.0, 1.0 });
+
+ pl_gpu_flush(gpu);
+ p->frame_pending = true;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+
+ if (p->frame_pending) {
+ if (!pl_swapchain_submit_frame(p->sw))
+ MP_ERR(vo, "Failed presenting frame!\n");
+ p->frame_pending = false;
+ }
+
+ sw->fns->swap_buffers(sw);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ if (sw->fns->get_vsync)
+ sw->fns->get_vsync(sw, info);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *p = vo->priv;
+ if (ra_hwdec_get(&p->hwdec_ctx, format))
+ return true;
+
+ struct pl_bit_encoding bits;
+ struct pl_plane_data data[4] = {0};
+ int planes = plane_data_from_imgfmt(data, &bits, format);
+ if (!planes)
+ return false;
+
+ for (int i = 0; i < planes; i++) {
+ if (!pl_plane_find_fmt(p->gpu, NULL, &data[i]))
+ return false;
+ }
+
+ return true;
+}
+
+static void resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct mp_rect src, dst;
+ struct mp_osd_res osd;
+ vo_get_src_dst_rects(vo, &src, &dst, &osd);
+ if (vo->dwidth && vo->dheight) {
+ gpu_ctx_resize(p->context, vo->dwidth, vo->dheight);
+ vo->want_redraw = true;
+ }
+
+ if (mp_rect_equals(&p->src, &src) &&
+ mp_rect_equals(&p->dst, &dst) &&
+ osd_res_equals(p->osd_res, osd))
+ return;
+
+ p->osd_sync++;
+ p->osd_res = osd;
+ p->src = src;
+ p->dst = dst;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+ if (!p->ra_ctx->fns->reconfig(p->ra_ctx))
+ return -1;
+
+ resize(vo);
+ return 0;
+}
+
+// Takes over ownership of `icc`. Can be used to unload profile (icc.len == 0)
+static bool update_icc(struct priv *p, struct bstr icc)
+{
+ struct pl_icc_profile profile = {
+ .data = icc.start,
+ .len = icc.len,
+ };
+
+ pl_icc_profile_compute_signature(&profile);
+
+ bool ok = pl_icc_update(p->pllog, &p->icc_profile, &profile, &p->icc_params);
+ talloc_free(icc.start);
+ return ok;
+}
+
+// Returns whether the ICC profile was updated (even on failure)
+static bool update_auto_profile(struct priv *p, int *events)
+{
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ if (!opts->icc_opts || !opts->icc_opts->profile_auto || p->icc_path)
+ return false;
+
+ MP_VERBOSE(p, "Querying ICC profile...\n");
+ bstr icc = {0};
+ int r = p->ra_ctx->fns->control(p->ra_ctx, events, VOCTRL_GET_ICC_PROFILE, &icc);
+
+ if (r != VO_NOTAVAIL) {
+ if (r == VO_FALSE) {
+ MP_WARN(p, "Could not retrieve an ICC profile.\n");
+ } else if (r == VO_NOTIMPL) {
+ MP_ERR(p, "icc-profile-auto not implemented on this platform.\n");
+ }
+
+ update_icc(p, icc);
+ return true;
+ }
+
+ return false;
+}
+
+static void video_screenshot(struct vo *vo, struct voctrl_screenshot *args)
+{
+ struct priv *p = vo->priv;
+ pl_options pars = p->pars;
+ pl_gpu gpu = p->gpu;
+ pl_tex fbo = NULL;
+ args->res = NULL;
+
+ update_options(vo);
+ struct pl_render_params params = pars->params;
+ params.info_callback = NULL;
+ params.skip_caching_single_frame = true;
+ params.preserve_mixing_cache = false;
+ params.frame_mixer = NULL;
+
+ struct pl_peak_detect_params peak_params;
+ if (params.peak_detect_params) {
+ peak_params = *params.peak_detect_params;
+ params.peak_detect_params = &peak_params;
+ peak_params.allow_delayed = false;
+ }
+
+ // Retrieve the current frame from the frame queue
+ struct pl_frame_mix mix;
+ enum pl_queue_status status;
+ status = pl_queue_update(p->queue, &mix, pl_queue_params(
+ .pts = p->last_pts,
+#if PL_API_VER >= 340
+ .drift_compensation = 0,
+#endif
+ ));
+ assert(status != PL_QUEUE_EOF);
+ if (status == PL_QUEUE_ERR) {
+ MP_ERR(vo, "Unknown error occurred while trying to take screenshot!\n");
+ return;
+ }
+ if (!mix.num_frames) {
+ MP_ERR(vo, "No frames available to take screenshot of, is a file loaded?\n");
+ return;
+ }
+
+ // Passing an interpolation radius of 0 guarantees that the first frame in
+ // the resulting mix is the correct frame for this PTS
+ struct pl_frame image = *(struct pl_frame *) mix.frames[0];
+ struct mp_image *mpi = image.user_data;
+ struct mp_rect src = p->src, dst = p->dst;
+ struct mp_osd_res osd = p->osd_res;
+ if (!args->scaled) {
+ int w, h;
+ mp_image_params_get_dsize(&mpi->params, &w, &h);
+ if (w < 1 || h < 1)
+ return;
+
+ int src_w = mpi->params.w;
+ int src_h = mpi->params.h;
+ src = (struct mp_rect) {0, 0, src_w, src_h};
+ dst = (struct mp_rect) {0, 0, w, h};
+
+ if (mp_image_crop_valid(&mpi->params))
+ src = mpi->params.crop;
+
+ if (mpi->params.rotate % 180 == 90) {
+ MPSWAP(int, w, h);
+ MPSWAP(int, src_w, src_h);
+ }
+ mp_rect_rotate(&src, src_w, src_h, mpi->params.rotate);
+ mp_rect_rotate(&dst, w, h, mpi->params.rotate);
+
+ osd = (struct mp_osd_res) {
+ .display_par = 1.0,
+ .w = mp_rect_w(dst),
+ .h = mp_rect_h(dst),
+ };
+ }
+
+ // Create target FBO, try high bit depth first
+ int mpfmt;
+ for (int depth = args->high_bit_depth ? 16 : 8; depth; depth -= 8) {
+ if (depth == 16) {
+ mpfmt = IMGFMT_RGBA64;
+ } else {
+ mpfmt = p->ra_ctx->opts.want_alpha ? IMGFMT_RGBA : IMGFMT_RGB0;
+ }
+ pl_fmt fmt = pl_find_fmt(gpu, PL_FMT_UNORM, 4, depth, depth,
+ PL_FMT_CAP_RENDERABLE | PL_FMT_CAP_HOST_READABLE);
+ if (!fmt)
+ continue;
+
+ fbo = pl_tex_create(gpu, pl_tex_params(
+ .w = osd.w,
+ .h = osd.h,
+ .format = fmt,
+ .blit_dst = true,
+ .renderable = true,
+ .host_readable = true,
+ .storable = fmt->caps & PL_FMT_CAP_STORABLE,
+ ));
+ if (fbo)
+ break;
+ }
+
+ if (!fbo) {
+ MP_ERR(vo, "Failed creating target FBO for screenshot!\n");
+ return;
+ }
+
+ struct pl_frame target = {
+ .repr = pl_color_repr_rgb,
+ .num_planes = 1,
+ .planes[0] = {
+ .texture = fbo,
+ .components = 4,
+ .component_mapping = {0, 1, 2, 3},
+ },
+ };
+
+ if (args->scaled) {
+ // Apply target LUT, ICC profile and CSP override only in window mode
+ apply_target_options(p, &target);
+ } else if (args->native_csp) {
+ target.color = image.color;
+ } else {
+ target.color = pl_color_space_srgb;
+ }
+
+ apply_crop(&image, src, mpi->params.w, mpi->params.h);
+ apply_crop(&target, dst, fbo->params.w, fbo->params.h);
+ update_tm_viz(&pars->color_map_params, &target);
+
+ int osd_flags = 0;
+ if (!args->subs)
+ osd_flags |= OSD_DRAW_OSD_ONLY;
+ if (!args->osd)
+ osd_flags |= OSD_DRAW_SUB_ONLY;
+
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ struct frame_priv *fp = mpi->priv;
+ if (opts->blend_subs) {
+ float rx = pl_rect_w(dst) / pl_rect_w(image.crop);
+ float ry = pl_rect_h(dst) / pl_rect_h(image.crop);
+ struct mp_osd_res res = {
+ .w = pl_rect_w(dst),
+ .h = pl_rect_h(dst),
+ .ml = -image.crop.x0 * rx,
+ .mr = (image.crop.x1 - vo->params->w) * rx,
+ .mt = -image.crop.y0 * ry,
+ .mb = (image.crop.y1 - vo->params->h) * ry,
+ .display_par = 1.0,
+ };
+ update_overlays(vo, res, osd_flags,
+ PL_OVERLAY_COORDS_DST_CROP,
+ &fp->subs, &image, mpi);
+ } else {
+ // Disable overlays when blend_subs is disabled
+ update_overlays(vo, osd, osd_flags, PL_OVERLAY_COORDS_DST_FRAME,
+ &p->osd_state, &target, mpi);
+ image.num_overlays = 0;
+ }
+
+ if (!pl_render_image(p->rr, &image, &target, &params)) {
+ MP_ERR(vo, "Failed rendering frame!\n");
+ goto done;
+ }
+
+ args->res = mp_image_alloc(mpfmt, fbo->params.w, fbo->params.h);
+ if (!args->res)
+ goto done;
+
+ args->res->params.color.primaries = mp_prim_from_pl(target.color.primaries);
+ args->res->params.color.gamma = mp_trc_from_pl(target.color.transfer);
+ args->res->params.color.levels = mp_levels_from_pl(target.repr.levels);
+ args->res->params.color.hdr = target.color.hdr;
+ if (args->scaled)
+ args->res->params.p_w = args->res->params.p_h = 1;
+
+ bool ok = pl_tex_download(gpu, pl_tex_transfer_params(
+ .tex = fbo,
+ .ptr = args->res->planes[0],
+ .row_pitch = args->res->stride[0],
+ ));
+
+ if (!ok)
+ TA_FREEP(&args->res);
+
+ // fall through
+done:
+ pl_tex_destroy(gpu, &fbo);
+}
+
+static inline void copy_frame_info_to_mp(struct frame_info *pl,
+ struct mp_frame_perf *mp) {
+ static_assert(MP_ARRAY_SIZE(pl->info) == MP_ARRAY_SIZE(mp->perf), "");
+ assert(pl->count <= VO_PASS_PERF_MAX);
+ mp->count = MPMIN(pl->count, VO_PASS_PERF_MAX);
+
+ for (int i = 0; i < mp->count; ++i) {
+ const struct pl_dispatch_info *pass = &pl->info[i];
+
+ static_assert(VO_PERF_SAMPLE_COUNT >= MP_ARRAY_SIZE(pass->samples), "");
+ assert(pass->num_samples <= MP_ARRAY_SIZE(pass->samples));
+
+ struct mp_pass_perf *perf = &mp->perf[i];
+ perf->count = MPMIN(pass->num_samples, VO_PERF_SAMPLE_COUNT);
+ memcpy(perf->samples, pass->samples, perf->count * sizeof(pass->samples[0]));
+ perf->last = pass->last;
+ perf->peak = pass->peak;
+ perf->avg = pass->average;
+
+ strncpy(mp->desc[i], pass->shader->description, sizeof(mp->desc[i]) - 1);
+ mp->desc[i][sizeof(mp->desc[i]) - 1] = '\0';
+ }
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ case VOCTRL_PAUSE:
+ if (p->is_interpolated)
+ vo->want_redraw = true;
+ return VO_TRUE;
+
+ case VOCTRL_UPDATE_RENDER_OPTS: {
+ m_config_cache_update(p->opts_cache);
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ p->ra_ctx->opts.want_alpha = opts->alpha_mode == ALPHA_YES;
+ if (p->ra_ctx->fns->update_render_opts)
+ p->ra_ctx->fns->update_render_opts(p->ra_ctx);
+ update_render_options(vo);
+ vo->want_redraw = true;
+
+ // Also re-query the auto profile, in case `update_render_options`
+ // unloaded a manually specified icc profile in favor of
+ // icc-profile-auto
+ int events = 0;
+ update_auto_profile(p, &events);
+ vo_event(vo, events);
+ return VO_TRUE;
+ }
+
+ case VOCTRL_RESET:
+ // Defer until the first new frame (unique ID) actually arrives
+ p->want_reset = true;
+ return VO_TRUE;
+
+ case VOCTRL_PERFORMANCE_DATA: {
+ struct voctrl_performance_data *perf = data;
+ copy_frame_info_to_mp(&p->perf_fresh, &perf->fresh);
+ copy_frame_info_to_mp(&p->perf_redraw, &perf->redraw);
+ return true;
+ }
+
+ case VOCTRL_SCREENSHOT:
+ video_screenshot(vo, data);
+ return true;
+
+ case VOCTRL_EXTERNAL_RESIZE:
+ reconfig(vo, NULL);
+ return true;
+
+ case VOCTRL_LOAD_HWDEC_API:
+ ra_hwdec_ctx_load_fmt(&p->hwdec_ctx, vo->hwdec_devs, data);
+ return true;
+ }
+
+ int events = 0;
+ int r = p->ra_ctx->fns->control(p->ra_ctx, &events, request, data);
+ if (events & VO_EVENT_ICC_PROFILE_CHANGED) {
+ if (update_auto_profile(p, &events))
+ vo->want_redraw = true;
+ }
+ if (events & VO_EVENT_RESIZE)
+ resize(vo);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+
+ return r;
+}
+
+static void wakeup(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (p->ra_ctx && p->ra_ctx->fns->wakeup)
+ p->ra_ctx->fns->wakeup(p->ra_ctx);
+}
+
+static void wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct priv *p = vo->priv;
+ if (p->ra_ctx && p->ra_ctx->fns->wait_events) {
+ p->ra_ctx->fns->wait_events(p->ra_ctx, until_time_ns);
+ } else {
+ vo_wait_default(vo, until_time_ns);
+ }
+}
+
+#if PL_API_VER < 342
+static inline void xor_hash(void *hash, pl_cache_obj obj)
+{
+ *((uint64_t *) hash) ^= obj.key;
+}
+
+static inline uint64_t pl_cache_signature(pl_cache cache)
+{
+ uint64_t hash = 0;
+ pl_cache_iterate(cache, xor_hash, &hash);
+ return hash;
+}
+#endif
+
+static void cache_init(struct vo *vo, struct cache *cache, size_t max_size,
+ const char *dir_opt)
+{
+ struct priv *p = vo->priv;
+ const char *name = cache == &p->shader_cache ? "shader.cache" : "icc.cache";
+
+ char *dir;
+ if (dir_opt && dir_opt[0]) {
+ dir = mp_get_user_path(NULL, p->global, dir_opt);
+ } else {
+ dir = mp_find_user_file(NULL, p->global, "cache", "");
+ }
+ if (!dir || !dir[0])
+ goto done;
+
+ mp_mkdirp(dir);
+ cache->path = mp_path_join(vo, dir, name);
+ cache->cache = pl_cache_create(pl_cache_params(
+ .log = p->pllog,
+ .max_total_size = max_size,
+ ));
+
+ FILE *file = fopen(cache->path, "rb");
+ if (file) {
+ int ret = pl_cache_load_file(cache->cache, file);
+ fclose(file);
+ if (ret < 0)
+ MP_WARN(p, "Failed loading cache from %s\n", cache->path);
+ }
+
+ cache->sig = pl_cache_signature(cache->cache);
+done:
+ talloc_free(dir);
+}
+
+static void cache_uninit(struct priv *p, struct cache *cache)
+{
+ if (!cache->cache)
+ goto done;
+ if (pl_cache_signature(cache->cache) == cache->sig)
+ goto done; // skip re-saving identical cache
+
+ assert(cache->path);
+ char *tmp = talloc_asprintf(cache->path, "%sXXXXXX", cache->path);
+ int fd = mkstemp(tmp);
+ if (fd < 0)
+ goto done;
+ FILE *file = fdopen(fd, "wb");
+ if (!file) {
+ close(fd);
+ unlink(tmp);
+ goto done;
+ }
+ int ret = pl_cache_save_file(cache->cache, file);
+ fclose(file);
+ if (ret >= 0)
+ ret = rename(tmp, cache->path);
+ if (ret < 0) {
+ MP_WARN(p, "Failed saving cache to %s\n", cache->path);
+ unlink(tmp);
+ }
+
+ // fall through
+done:
+ pl_cache_destroy(&cache->cache);
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ pl_queue_destroy(&p->queue); // destroy this first
+ for (int i = 0; i < MP_ARRAY_SIZE(p->osd_state.entries); i++)
+ pl_tex_destroy(p->gpu, &p->osd_state.entries[i].tex);
+ for (int i = 0; i < p->num_sub_tex; i++)
+ pl_tex_destroy(p->gpu, &p->sub_tex[i]);
+ for (int i = 0; i < p->num_user_hooks; i++)
+ pl_mpv_user_shader_destroy(&p->user_hooks[i].hook);
+
+ if (vo->hwdec_devs) {
+ ra_hwdec_mapper_free(&p->hwdec_mapper);
+ ra_hwdec_ctx_uninit(&p->hwdec_ctx);
+ hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+
+ assert(p->num_dr_buffers == 0);
+ mp_mutex_destroy(&p->dr_lock);
+
+ cache_uninit(p, &p->shader_cache);
+ cache_uninit(p, &p->icc_cache);
+
+ pl_icc_close(&p->icc_profile);
+ pl_renderer_destroy(&p->rr);
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; ++i) {
+ pl_shader_info_deref(&p->perf_fresh.info[i].shader);
+ pl_shader_info_deref(&p->perf_redraw.info[i].shader);
+ }
+
+ pl_options_free(&p->pars);
+
+ p->ra_ctx = NULL;
+ p->pllog = NULL;
+ p->gpu = NULL;
+ p->sw = NULL;
+ gpu_ctx_destroy(&p->context);
+}
+
+static void load_hwdec_api(void *ctx, struct hwdec_imgfmt_request *params)
+{
+ vo_control(ctx, VOCTRL_LOAD_HWDEC_API, params);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->opts_cache = m_config_cache_alloc(p, vo->global, &gl_video_conf);
+ p->video_eq = mp_csp_equalizer_create(p, vo->global);
+ p->global = vo->global;
+ p->log = vo->log;
+
+ struct gl_video_opts *gl_opts = p->opts_cache->opts;
+ p->context = gpu_ctx_create(vo, gl_opts);
+ if (!p->context)
+ goto err_out;
+ // For the time being
+ p->ra_ctx = p->context->ra_ctx;
+ p->pllog = p->context->pllog;
+ p->gpu = p->context->gpu;
+ p->sw = p->context->swapchain;
+ p->hwdec_ctx = (struct ra_hwdec_ctx) {
+ .log = p->log,
+ .global = p->global,
+ .ra_ctx = p->ra_ctx,
+ };
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_set_loader(vo->hwdec_devs, load_hwdec_api, vo);
+ ra_hwdec_ctx_init(&p->hwdec_ctx, vo->hwdec_devs, gl_opts->hwdec_interop, false);
+ mp_mutex_init(&p->dr_lock);
+
+ if (gl_opts->shader_cache)
+ cache_init(vo, &p->shader_cache, 10 << 20, gl_opts->shader_cache_dir);
+ if (gl_opts->icc_opts->cache)
+ cache_init(vo, &p->icc_cache, 20 << 20, gl_opts->icc_opts->cache_dir);
+
+ pl_gpu_set_cache(p->gpu, p->shader_cache.cache);
+ p->rr = pl_renderer_create(p->pllog, p->gpu);
+ p->queue = pl_queue_create(p->gpu);
+ p->osd_fmt[SUBBITMAP_LIBASS] = pl_find_named_fmt(p->gpu, "r8");
+ p->osd_fmt[SUBBITMAP_BGRA] = pl_find_named_fmt(p->gpu, "bgra8");
+ p->osd_sync = 1;
+
+ p->pars = pl_options_alloc(p->pllog);
+ update_render_options(vo);
+ return 0;
+
+err_out:
+ uninit(vo);
+ return -1;
+}
+
+static const struct pl_filter_config *map_scaler(struct priv *p,
+ enum scaler_unit unit)
+{
+ const struct pl_filter_preset fixed_scalers[] = {
+ { "bilinear", &pl_filter_bilinear },
+ { "bicubic_fast", &pl_filter_bicubic },
+ { "nearest", &pl_filter_nearest },
+ { "oversample", &pl_filter_oversample },
+ {0},
+ };
+
+ const struct pl_filter_preset fixed_frame_mixers[] = {
+ { "linear", &pl_filter_bilinear },
+ { "oversample", &pl_filter_oversample },
+ {0},
+ };
+
+ const struct pl_filter_preset *fixed_presets =
+ unit == SCALER_TSCALE ? fixed_frame_mixers : fixed_scalers;
+
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ const struct scaler_config *cfg = &opts->scaler[unit];
+ if (unit == SCALER_DSCALE && (!cfg->kernel.name || !cfg->kernel.name[0]))
+ cfg = &opts->scaler[SCALER_SCALE];
+ if (unit == SCALER_CSCALE && (!cfg->kernel.name || !cfg->kernel.name[0]))
+ cfg = &opts->scaler[SCALER_SCALE];
+
+ for (int i = 0; fixed_presets[i].name; i++) {
+ if (strcmp(cfg->kernel.name, fixed_presets[i].name) == 0)
+ return fixed_presets[i].filter;
+ }
+
+ // Attempt loading filter preset first, fall back to raw filter function
+ struct scaler_params *par = &p->scalers[unit];
+ const struct pl_filter_preset *preset;
+ const struct pl_filter_function_preset *fpreset;
+ if ((preset = pl_find_filter_preset(cfg->kernel.name))) {
+ par->config = *preset->filter;
+ } else if ((fpreset = pl_find_filter_function_preset(cfg->kernel.name))) {
+ par->config = (struct pl_filter_config) {
+ .kernel = fpreset->function,
+ .params[0] = fpreset->function->params[0],
+ .params[1] = fpreset->function->params[1],
+ };
+ } else {
+ MP_ERR(p, "Failed mapping filter function '%s', no libplacebo analog?\n",
+ cfg->kernel.name);
+ return &pl_filter_bilinear;
+ }
+
+ const struct pl_filter_function_preset *wpreset;
+ if ((wpreset = pl_find_filter_function_preset(cfg->window.name))) {
+ par->config.window = wpreset->function;
+ par->config.wparams[0] = wpreset->function->params[0];
+ par->config.wparams[1] = wpreset->function->params[1];
+ }
+
+ for (int i = 0; i < 2; i++) {
+ if (!isnan(cfg->kernel.params[i]))
+ par->config.params[i] = cfg->kernel.params[i];
+ if (!isnan(cfg->window.params[i]))
+ par->config.wparams[i] = cfg->window.params[i];
+ }
+
+ par->config.clamp = cfg->clamp;
+ if (cfg->kernel.blur > 0.0)
+ par->config.blur = cfg->kernel.blur;
+ if (cfg->kernel.taper > 0.0)
+ par->config.taper = cfg->kernel.taper;
+ if (cfg->radius > 0.0) {
+ if (par->config.kernel->resizable) {
+ par->config.radius = cfg->radius;
+ } else {
+ MP_WARN(p, "Filter radius specified but filter '%s' is not "
+ "resizable, ignoring\n", cfg->kernel.name);
+ }
+ }
+
+ return &par->config;
+}
+
+static const struct pl_hook *load_hook(struct priv *p, const char *path)
+{
+ if (!path || !path[0])
+ return NULL;
+
+ for (int i = 0; i < p->num_user_hooks; i++) {
+ if (strcmp(p->user_hooks[i].path, path) == 0)
+ return p->user_hooks[i].hook;
+ }
+
+ char *fname = mp_get_user_path(NULL, p->global, path);
+ bstr shader = stream_read_file(fname, p, p->global, 1000000000); // 1GB
+ talloc_free(fname);
+
+ const struct pl_hook *hook = NULL;
+ if (shader.len)
+ hook = pl_mpv_user_shader_parse(p->gpu, shader.start, shader.len);
+
+ MP_TARRAY_APPEND(p, p->user_hooks, p->num_user_hooks, (struct user_hook) {
+ .path = talloc_strdup(p, path),
+ .hook = hook,
+ });
+
+ return hook;
+}
+
+static void update_icc_opts(struct priv *p, const struct mp_icc_opts *opts)
+{
+ if (!opts)
+ return;
+
+ if (!opts->profile_auto && !p->icc_path) {
+ // Un-set any auto-loaded profiles if icc-profile-auto was disabled
+ update_icc(p, (bstr) {0});
+ }
+
+ int s_r = 0, s_g = 0, s_b = 0;
+ gl_parse_3dlut_size(opts->size_str, &s_r, &s_g, &s_b);
+ p->icc_params = pl_icc_default_params;
+ p->icc_params.intent = opts->intent;
+ p->icc_params.size_r = s_r;
+ p->icc_params.size_g = s_g;
+ p->icc_params.size_b = s_b;
+ p->icc_params.cache = p->icc_cache.cache;
+
+ if (!opts->profile || !opts->profile[0]) {
+ // No profile enabled, un-load any existing profiles
+ update_icc(p, (bstr) {0});
+ TA_FREEP(&p->icc_path);
+ return;
+ }
+
+ if (p->icc_path && strcmp(opts->profile, p->icc_path) == 0)
+ return; // ICC profile hasn't changed
+
+ char *fname = mp_get_user_path(NULL, p->global, opts->profile);
+ MP_VERBOSE(p, "Opening ICC profile '%s'\n", fname);
+ struct bstr icc = stream_read_file(fname, p, p->global, 100000000); // 100 MB
+ talloc_free(fname);
+ update_icc(p, icc);
+
+ // Update cached path
+ talloc_free(p->icc_path);
+ p->icc_path = talloc_strdup(p, opts->profile);
+}
+
+static void update_lut(struct priv *p, struct user_lut *lut)
+{
+ if (!lut->opt) {
+ pl_lut_free(&lut->lut);
+ TA_FREEP(&lut->path);
+ return;
+ }
+
+ if (lut->path && strcmp(lut->path, lut->opt) == 0)
+ return; // no change
+
+ // Update cached path
+ pl_lut_free(&lut->lut);
+ talloc_free(lut->path);
+ lut->path = talloc_strdup(p, lut->opt);
+
+ // Load LUT file
+ char *fname = mp_get_user_path(NULL, p->global, lut->path);
+ MP_VERBOSE(p, "Loading custom LUT '%s'\n", fname);
+ struct bstr lutdata = stream_read_file(fname, p, p->global, 100000000); // 100 MB
+ lut->lut = pl_lut_parse_cube(p->pllog, lutdata.start, lutdata.len);
+ talloc_free(lutdata.start);
+}
+
+static void update_hook_opts(struct priv *p, char **opts, const char *shaderpath,
+ const struct pl_hook *hook)
+{
+ if (!opts)
+ return;
+
+ const char *basename = mp_basename(shaderpath);
+ struct bstr shadername;
+ if (!mp_splitext(basename, &shadername))
+ shadername = bstr0(basename);
+
+ for (int n = 0; opts[n * 2]; n++) {
+ struct bstr k = bstr0(opts[n * 2 + 0]);
+ struct bstr v = bstr0(opts[n * 2 + 1]);
+ int pos;
+ if ((pos = bstrchr(k, '/')) >= 0) {
+ if (!bstr_equals(bstr_splice(k, 0, pos), shadername))
+ continue;
+ k = bstr_cut(k, pos + 1);
+ }
+
+ for (int i = 0; i < hook->num_parameters; i++) {
+ const struct pl_hook_par *hp = &hook->parameters[i];
+ if (!bstr_equals0(k, hp->name) != 0)
+ continue;
+
+ m_option_t opt = {
+ .name = hp->name,
+ };
+
+ if (hp->names) {
+ for (int j = hp->minimum.i; j <= hp->maximum.i; j++) {
+ if (bstr_equals0(v, hp->names[j])) {
+ hp->data->i = j;
+ goto next_hook;
+ }
+ }
+ }
+
+ switch (hp->type) {
+ case PL_VAR_FLOAT:
+ opt.type = &m_option_type_float;
+ opt.min = hp->minimum.f;
+ opt.max = hp->maximum.f;
+ break;
+ case PL_VAR_SINT:
+ opt.type = &m_option_type_int;
+ opt.min = hp->minimum.i;
+ opt.max = hp->maximum.i;
+ break;
+ case PL_VAR_UINT:
+ opt.type = &m_option_type_int;
+ opt.min = MPMIN(hp->minimum.u, INT_MAX);
+ opt.max = MPMIN(hp->maximum.u, INT_MAX);
+ break;
+ }
+
+ if (!opt.type)
+ goto next_hook;
+
+ opt.type->parse(p->log, &opt, k, v, hp->data);
+ goto next_hook;
+ }
+
+ next_hook:;
+ }
+}
+
+static void update_render_options(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ pl_options pars = p->pars;
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ pars->params.antiringing_strength = opts->scaler[0].antiring;
+ pars->params.background_color[0] = opts->background.r / 255.0;
+ pars->params.background_color[1] = opts->background.g / 255.0;
+ pars->params.background_color[2] = opts->background.b / 255.0;
+ pars->params.background_transparency = 1.0 - opts->background.a / 255.0;
+ pars->params.skip_anti_aliasing = !opts->correct_downscaling;
+ pars->params.disable_linear_scaling = !opts->linear_downscaling && !opts->linear_upscaling;
+ pars->params.disable_fbos = opts->dumb_mode == 1;
+ pars->params.blend_against_tiles = opts->alpha_mode == ALPHA_BLEND_TILES;
+ pars->params.corner_rounding = p->corner_rounding;
+ pars->params.correct_subpixel_offsets = !opts->scaler_resizes_only;
+
+ // Map scaler options as best we can
+ pars->params.upscaler = map_scaler(p, SCALER_SCALE);
+ pars->params.downscaler = map_scaler(p, SCALER_DSCALE);
+ pars->params.plane_upscaler = map_scaler(p, SCALER_CSCALE);
+ pars->params.frame_mixer = opts->interpolation ? map_scaler(p, SCALER_TSCALE) : NULL;
+
+ // Request as many frames as required from the decoder, depending on the
+ // speed VPS/FPS ratio libplacebo may need more frames. Request frames up to
+ // ratio of 1/2, but only if anti aliasing is enabled.
+ int req_frames = 2;
+ if (pars->params.frame_mixer) {
+ req_frames += ceilf(pars->params.frame_mixer->kernel->radius) *
+ (pars->params.skip_anti_aliasing ? 1 : 2);
+ }
+ vo_set_queue_params(vo, 0, MPMIN(VO_MAX_REQ_FRAMES, req_frames));
+
+ pars->params.deband_params = opts->deband ? &pars->deband_params : NULL;
+ pars->deband_params.iterations = opts->deband_opts->iterations;
+ pars->deband_params.radius = opts->deband_opts->range;
+ pars->deband_params.threshold = opts->deband_opts->threshold / 16.384;
+ pars->deband_params.grain = opts->deband_opts->grain / 8.192;
+
+ pars->params.sigmoid_params = opts->sigmoid_upscaling ? &pars->sigmoid_params : NULL;
+ pars->sigmoid_params.center = opts->sigmoid_center;
+ pars->sigmoid_params.slope = opts->sigmoid_slope;
+
+ pars->params.peak_detect_params = opts->tone_map.compute_peak >= 0 ? &pars->peak_detect_params : NULL;
+ pars->peak_detect_params.smoothing_period = opts->tone_map.decay_rate;
+ pars->peak_detect_params.scene_threshold_low = opts->tone_map.scene_threshold_low;
+ pars->peak_detect_params.scene_threshold_high = opts->tone_map.scene_threshold_high;
+ pars->peak_detect_params.percentile = opts->tone_map.peak_percentile;
+ pars->peak_detect_params.allow_delayed = p->delayed_peak;
+
+ const struct pl_tone_map_function * const tone_map_funs[] = {
+ [TONE_MAPPING_AUTO] = &pl_tone_map_auto,
+ [TONE_MAPPING_CLIP] = &pl_tone_map_clip,
+ [TONE_MAPPING_MOBIUS] = &pl_tone_map_mobius,
+ [TONE_MAPPING_REINHARD] = &pl_tone_map_reinhard,
+ [TONE_MAPPING_HABLE] = &pl_tone_map_hable,
+ [TONE_MAPPING_GAMMA] = &pl_tone_map_gamma,
+ [TONE_MAPPING_LINEAR] = &pl_tone_map_linear,
+ [TONE_MAPPING_SPLINE] = &pl_tone_map_spline,
+ [TONE_MAPPING_BT_2390] = &pl_tone_map_bt2390,
+ [TONE_MAPPING_BT_2446A] = &pl_tone_map_bt2446a,
+ [TONE_MAPPING_ST2094_40] = &pl_tone_map_st2094_40,
+ [TONE_MAPPING_ST2094_10] = &pl_tone_map_st2094_10,
+ };
+
+ const struct pl_gamut_map_function * const gamut_modes[] = {
+ [GAMUT_AUTO] = pl_color_map_default_params.gamut_mapping,
+ [GAMUT_CLIP] = &pl_gamut_map_clip,
+ [GAMUT_PERCEPTUAL] = &pl_gamut_map_perceptual,
+ [GAMUT_RELATIVE] = &pl_gamut_map_relative,
+ [GAMUT_SATURATION] = &pl_gamut_map_saturation,
+ [GAMUT_ABSOLUTE] = &pl_gamut_map_absolute,
+ [GAMUT_DESATURATE] = &pl_gamut_map_desaturate,
+ [GAMUT_DARKEN] = &pl_gamut_map_darken,
+ [GAMUT_WARN] = &pl_gamut_map_highlight,
+ [GAMUT_LINEAR] = &pl_gamut_map_linear,
+ };
+
+ pars->color_map_params.tone_mapping_function = tone_map_funs[opts->tone_map.curve];
+ pars->color_map_params.tone_mapping_param = opts->tone_map.curve_param;
+ if (isnan(pars->color_map_params.tone_mapping_param)) // vo_gpu compatibility
+ pars->color_map_params.tone_mapping_param = 0.0;
+ pars->color_map_params.inverse_tone_mapping = opts->tone_map.inverse;
+ pars->color_map_params.contrast_recovery = opts->tone_map.contrast_recovery;
+ pars->color_map_params.visualize_lut = opts->tone_map.visualize;
+ pars->color_map_params.contrast_smoothness = opts->tone_map.contrast_smoothness;
+ pars->color_map_params.gamut_mapping = gamut_modes[opts->tone_map.gamut_mode];
+
+ switch (opts->dither_algo) {
+ case DITHER_NONE:
+ pars->params.dither_params = NULL;
+ break;
+ case DITHER_ERROR_DIFFUSION:
+ pars->params.error_diffusion = pl_find_error_diffusion_kernel(opts->error_diffusion);
+ if (!pars->params.error_diffusion) {
+ MP_WARN(p, "Could not find error diffusion kernel '%s', falling "
+ "back to fruit.\n", opts->error_diffusion);
+ }
+ MP_FALLTHROUGH;
+ case DITHER_ORDERED:
+ case DITHER_FRUIT:
+ pars->params.dither_params = &pars->dither_params;
+ pars->dither_params.method = opts->dither_algo == DITHER_ORDERED
+ ? PL_DITHER_ORDERED_FIXED
+ : PL_DITHER_BLUE_NOISE;
+ pars->dither_params.lut_size = opts->dither_size;
+ pars->dither_params.temporal = opts->temporal_dither;
+ break;
+ }
+
+ if (opts->dither_depth < 0)
+ pars->params.dither_params = NULL;
+
+ update_icc_opts(p, opts->icc_opts);
+
+ pars->params.num_hooks = 0;
+ const struct pl_hook *hook;
+ for (int i = 0; opts->user_shaders && opts->user_shaders[i]; i++) {
+ if ((hook = load_hook(p, opts->user_shaders[i]))) {
+ MP_TARRAY_APPEND(p, p->hooks, pars->params.num_hooks, hook);
+ update_hook_opts(p, opts->user_shader_opts, opts->user_shaders[i], hook);
+ }
+ }
+
+ pars->params.hooks = p->hooks;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct m_opt_choice_alternatives lut_types[] = {
+ {"auto", PL_LUT_UNKNOWN},
+ {"native", PL_LUT_NATIVE},
+ {"normalized", PL_LUT_NORMALIZED},
+ {"conversion", PL_LUT_CONVERSION},
+ {0}
+};
+
+const struct vo_driver video_out_gpu_next = {
+ .description = "Video output based on libplacebo",
+ .name = "gpu-next",
+ .caps = VO_CAP_ROTATE90 |
+ VO_CAP_FILM_GRAIN |
+ 0x0,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .get_image_ts = get_image,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wait_events = wait_events,
+ .wakeup = wakeup,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .inter_preserve = true,
+ },
+
+ .options = (const struct m_option[]) {
+ {"allow-delayed-peak-detect", OPT_BOOL(delayed_peak)},
+ {"corner-rounding", OPT_FLOAT(corner_rounding), M_RANGE(0, 1)},
+ {"interpolation-preserve", OPT_BOOL(inter_preserve)},
+ {"lut", OPT_STRING(lut.opt), .flags = M_OPT_FILE},
+ {"lut-type", OPT_CHOICE_C(lut.type, lut_types)},
+ {"image-lut", OPT_STRING(image_lut.opt), .flags = M_OPT_FILE},
+ {"image-lut-type", OPT_CHOICE_C(image_lut.type, lut_types)},
+ {"target-lut", OPT_STRING(target_lut.opt), .flags = M_OPT_FILE},
+ {"target-colorspace-hint", OPT_BOOL(target_hint)},
+ // No `target-lut-type` because we don't support non-RGB targets
+ {"libplacebo-opts", OPT_KEYVALUELIST(raw_opts)},
+ {0}
+ },
+};
diff --git a/video/out/vo_image.c b/video/out/vo_image.c
new file mode 100644
index 0000000..cc48ab3
--- /dev/null
+++ b/video/out/vo_image.c
@@ -0,0 +1,165 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <sys/stat.h>
+
+#include <libswscale/swscale.h>
+
+#include "misc/bstr.h"
+#include "osdep/io.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/out/vo.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "video/fmt-conversion.h"
+#include "video/image_writer.h"
+#include "video/sws_utils.h"
+#include "sub/osd.h"
+#include "options/m_option.h"
+
+static const struct m_sub_options image_writer_conf = {
+ .opts = image_writer_opts,
+ .size = sizeof(struct image_writer_opts),
+ .defaults = &image_writer_opts_defaults,
+};
+
+struct vo_image_opts {
+ struct image_writer_opts *opts;
+ char *outdir;
+};
+
+#define OPT_BASE_STRUCT struct vo_image_opts
+
+static const struct m_sub_options vo_image_conf = {
+ .opts = (const struct m_option[]) {
+ {"vo-image", OPT_SUBSTRUCT(opts, image_writer_conf)},
+ {"vo-image-outdir", OPT_STRING(outdir), .flags = M_OPT_FILE},
+ {0},
+ },
+ .size = sizeof(struct vo_image_opts),
+};
+
+struct priv {
+ struct vo_image_opts *opts;
+
+ struct mp_image *current;
+ int frame;
+};
+
+static bool checked_mkdir(struct vo *vo, const char *buf)
+{
+ MP_INFO(vo, "Creating output directory '%s'...\n", buf);
+ if (mkdir(buf, 0755) < 0) {
+ char *errstr = mp_strerror(errno);
+ if (errno == EEXIST) {
+ struct stat stat_p;
+ if (stat(buf, &stat_p ) == 0 && S_ISDIR(stat_p.st_mode))
+ return true;
+ }
+ MP_ERR(vo, "Error creating output directory: %s\n", errstr);
+ return false;
+ }
+ return true;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ if (!frame->current)
+ return;
+
+ p->current = frame->current;
+
+ struct mp_osd_res dim = osd_res_from_image_params(vo->params);
+ osd_draw_on_image(vo->osd, dim, frame->current->pts, OSD_DRAW_SUB_ONLY, p->current);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (!p->current)
+ return;
+
+ (p->frame)++;
+
+ void *t = talloc_new(NULL);
+ char *filename = talloc_asprintf(t, "%08d.%s", p->frame,
+ image_writer_file_ext(p->opts->opts));
+
+ if (p->opts->outdir && strlen(p->opts->outdir))
+ filename = mp_path_join(t, p->opts->outdir, filename);
+
+ MP_INFO(vo, "Saving %s\n", filename);
+ write_image(p->current, p->opts->opts, filename, vo->global, vo->log);
+
+ talloc_free(t);
+}
+
+static int query_format(struct vo *vo, int fmt)
+{
+ if (mp_sws_supported_format(fmt))
+ return 1;
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->opts = mp_get_config_group(vo, vo->global, &vo_image_conf);
+ if (p->opts->outdir && !checked_mkdir(vo, p->opts->outdir))
+ return -1;
+ return 0;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+const struct vo_driver video_out_image =
+{
+ .description = "Write video frames to image files",
+ .name = "image",
+ .untimed = true,
+ .priv_size = sizeof(struct priv),
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .global_opts = &vo_image_conf,
+};
diff --git a/video/out/vo_kitty.c b/video/out/vo_kitty.c
new file mode 100644
index 0000000..7d548c7
--- /dev/null
+++ b/video/out/vo_kitty.c
@@ -0,0 +1,433 @@
+/*
+ * Video output device using the kitty terminal graphics protocol
+ * See https://sw.kovidgoyal.net/kitty/graphics-protocol/
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+
+#include "config.h"
+
+#if HAVE_POSIX
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#endif
+
+#include <libswscale/swscale.h>
+#include <libavutil/base64.h>
+
+#include "options/m_config.h"
+#include "osdep/terminal.h"
+#include "sub/osd.h"
+#include "vo.h"
+#include "video/sws_utils.h"
+#include "video/mp_image.h"
+
+#define IMGFMT IMGFMT_RGB24
+#define BYTES_PER_PX 3
+#define DEFAULT_WIDTH_PX 320
+#define DEFAULT_HEIGHT_PX 240
+#define DEFAULT_WIDTH 80
+#define DEFAULT_HEIGHT 25
+
+static inline void write_str(const char *s)
+{
+ // On POSIX platforms, write() is the fastest method. It also is the only
+ // one that allows atomic writes so mpv’s output will not be interrupted
+ // by other processes or threads that write to stdout, which would cause
+ // screen corruption. POSIX does not guarantee atomicity for writes
+ // exceeding PIPE_BUF, but at least Linux does seem to implement it that
+ // way.
+#if HAVE_POSIX
+ int remain = strlen(s);
+ while (remain > 0) {
+ ssize_t written = write(STDOUT_FILENO, s, remain);
+ if (written < 0)
+ return;
+ remain -= written;
+ s += written;
+ }
+#else
+ printf("%s", s);
+ fflush(stdout);
+#endif
+}
+
+#define KITTY_ESC_IMG "\033_Ga=T,f=24,s=%d,v=%d,C=1,q=2,m=1;"
+#define KITTY_ESC_IMG_SHM "\033_Ga=T,t=s,f=24,s=%d,v=%d,C=1,q=2,m=1;%s\033\\"
+#define KITTY_ESC_CONTINUE "\033_Gm=%d;"
+#define KITTY_ESC_END "\033\\"
+#define KITTY_ESC_DELETE_ALL "\033_Ga=d;\033\\"
+
+struct vo_kitty_opts {
+ int width, height, top, left, rows, cols;
+ bool config_clear, alt_screen;
+ bool use_shm;
+};
+
+struct priv {
+ struct vo_kitty_opts opts;
+
+ uint8_t *buffer;
+ char *output;
+ char *shm_path, *shm_path_b64;
+ int buffer_size, output_size;
+ int shm_fd;
+
+ int left, top, width, height, cols, rows;
+
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+ struct mp_image *frame;
+ struct mp_sws_context *sws;
+};
+
+#if HAVE_POSIX
+static struct sigaction saved_sigaction = {0};
+static bool resized;
+#endif
+
+static void close_shm(struct priv *p)
+{
+#if HAVE_POSIX_SHM
+ if (p->buffer != NULL) {
+ munmap(p->buffer, p->buffer_size);
+ p->buffer = NULL;
+ }
+ if (p->shm_fd != -1) {
+ close(p->shm_fd);
+ p->shm_fd = -1;
+ }
+#endif
+}
+
+static void free_bufs(struct vo* vo)
+{
+ struct priv* p = vo->priv;
+
+ talloc_free(p->frame);
+ talloc_free(p->output);
+
+ if (p->opts.use_shm) {
+ close_shm(p);
+ } else {
+ talloc_free(p->buffer);
+ }
+}
+
+static void get_win_size(struct vo *vo, int *out_rows, int *out_cols,
+ int *out_width, int *out_height)
+{
+ struct priv *p = vo->priv;
+ *out_rows = DEFAULT_HEIGHT;
+ *out_cols = DEFAULT_WIDTH;
+ *out_width = DEFAULT_WIDTH_PX;
+ *out_height = DEFAULT_HEIGHT_PX;
+
+ terminal_get_size2(out_rows, out_cols, out_width, out_height);
+
+ *out_rows = p->opts.rows > 0 ? p->opts.rows : *out_rows;
+ *out_cols = p->opts.cols > 0 ? p->opts.cols : *out_cols;
+ *out_width = p->opts.width > 0 ? p->opts.width : *out_width;
+ *out_height = p->opts.height > 0 ? p->opts.height : *out_height;
+}
+
+static void set_out_params(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+
+ p->width = p->dst.x1 - p->dst.x0;
+ p->height = p->dst.y1 - p->dst.y0;
+ p->top = p->opts.top > 0 ?
+ p->opts.top : p->rows * p->dst.y0 / vo->dheight;
+ p->left = p->opts.left > 0 ?
+ p->opts.left : p->cols * p->dst.x0 / vo->dwidth;
+
+ p->buffer_size = 3 * p->width * p->height;
+ p->output_size = AV_BASE64_SIZE(p->buffer_size);
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ vo->want_redraw = true;
+ write_str(KITTY_ESC_DELETE_ALL);
+ if (p->opts.config_clear)
+ write_str(TERM_ESC_CLEAR_SCREEN);
+
+ get_win_size(vo, &p->rows, &p->cols, &vo->dwidth, &vo->dheight);
+ set_out_params(vo);
+ free_bufs(vo);
+
+ p->sws->src = *params;
+ p->sws->src.w = mp_rect_w(p->src);
+ p->sws->src.h = mp_rect_h(p->src);
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = IMGFMT,
+ .w = p->width,
+ .h = p->height,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ p->frame = mp_image_alloc(IMGFMT, p->width, p->height);
+ if (!p->frame)
+ return -1;
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return -1;
+
+ if (!p->opts.use_shm) {
+ p->buffer = talloc_array(NULL, uint8_t, p->buffer_size);
+ p->output = talloc_array(NULL, char, p->output_size);
+ }
+
+ return 0;
+}
+
+static int create_shm(struct vo *vo)
+{
+#if HAVE_POSIX_SHM
+ struct priv *p = vo->priv;
+ p->shm_fd = shm_open(p->shm_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
+ if (p->shm_fd == -1) {
+ MP_ERR(vo, "Failed to create shared memory object");
+ return 0;
+ }
+
+ if (ftruncate(p->shm_fd, p->buffer_size) == -1) {
+ MP_ERR(vo, "Failed to truncate shared memory object");
+ shm_unlink(p->shm_path);
+ close(p->shm_fd);
+ return 0;
+ }
+
+ p->buffer = mmap(NULL, p->buffer_size,
+ PROT_READ | PROT_WRITE, MAP_SHARED, p->shm_fd, 0);
+
+ if (p->buffer == MAP_FAILED) {
+ MP_ERR(vo, "Failed to mmap shared memory object");
+ shm_unlink(p->shm_path);
+ close(p->shm_fd);
+ return 0;
+ }
+ return 1;
+#else
+ return 0;
+#endif
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ mp_image_t *mpi = NULL;
+
+#if !HAVE_POSIX
+ int prev_height = vo->dheight;
+ int prev_width = vo->dwidth;
+ get_win_size(vo, &p->rows, &p->cols, &vo->dwidth, &vo->dheight);
+ bool resized = (prev_width != vo->dwidth || prev_height != vo->dheight);
+#endif
+
+ if (resized)
+ reconfig(vo, vo->params);
+
+ resized = false;
+
+ if (frame->current) {
+ mpi = mp_image_new_ref(frame->current);
+ struct mp_rect src_rc = p->src;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
+ mp_image_crop_rc(mpi, src_rc);
+
+ mp_sws_scale(p->sws, p->frame, mpi);
+ } else {
+ mp_image_clear(p->frame, 0, 0, p->width, p->height);
+ }
+
+ struct mp_osd_res res = { .w = p->width, .h = p->height };
+ osd_draw_on_image(vo->osd, res, mpi ? mpi->pts : 0, 0, p->frame);
+
+
+ if (p->opts.use_shm && !create_shm(vo))
+ return;
+
+ memcpy_pic(p->buffer, p->frame->planes[0], p->width * BYTES_PER_PX,
+ p->height, p->width * BYTES_PER_PX, p->frame->stride[0]);
+
+ if (!p->opts.use_shm)
+ av_base64_encode(p->output, p->output_size, p->buffer, p->buffer_size);
+
+ talloc_free(mpi);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv* p = vo->priv;
+
+ if (p->buffer == NULL)
+ return;
+
+ char *cmd = talloc_asprintf(NULL, TERM_ESC_GOTO_YX, p->top, p->left);
+
+ if (p->opts.use_shm) {
+ cmd = talloc_asprintf_append(cmd, KITTY_ESC_IMG_SHM, p->width, p->height, p->shm_path_b64);
+ } else {
+ if (p->output == NULL) {
+ talloc_free(cmd);
+ return;
+ }
+
+ cmd = talloc_asprintf_append(cmd, KITTY_ESC_IMG, p->width, p->height);
+ for (int offset = 0, noffset;; offset += noffset) {
+ if (offset)
+ cmd = talloc_asprintf_append(cmd, KITTY_ESC_CONTINUE, offset < p->output_size);
+ noffset = MPMIN(4096, p->output_size - offset);
+ cmd = talloc_strndup_append(cmd, p->output + offset, noffset);
+ cmd = talloc_strdup_append(cmd, KITTY_ESC_END);
+
+ if (offset >= p->output_size)
+ break;
+ }
+ }
+
+ write_str(cmd);
+ talloc_free(cmd);
+
+#if HAVE_POSIX
+ if (p->opts.use_shm)
+ close_shm(p);
+#endif
+}
+
+#if HAVE_POSIX
+static void handle_winch(int sig) {
+ resized = true;
+ if (saved_sigaction.sa_handler)
+ saved_sigaction.sa_handler(sig);
+}
+#endif
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+#if HAVE_POSIX
+ struct sigaction sa;
+ sa.sa_handler = handle_winch;
+ sigaction(SIGWINCH, &sa, &saved_sigaction);
+#endif
+
+#if HAVE_POSIX_SHM
+ if (p->opts.use_shm) {
+ p->shm_path = talloc_asprintf(vo, "/mpv-kitty-%p", vo);
+ int p_size = strlen(p->shm_path) - 1;
+ int b64_size = AV_BASE64_SIZE(p_size);
+ p->shm_path_b64 = talloc_array(vo, char, b64_size);
+ av_base64_encode(p->shm_path_b64, b64_size, p->shm_path + 1, p_size);
+ }
+#else
+ if (p->opts.use_shm) {
+ MP_ERR(vo, "Shared memory support is not available on this platform.");
+ return -1;
+ }
+#endif
+
+ write_str(TERM_ESC_HIDE_CURSOR);
+ if (p->opts.alt_screen)
+ write_str(TERM_ESC_ALT_SCREEN);
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ if (request == VOCTRL_SET_PANSCAN)
+ return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE;
+ return VO_NOTIMPL;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+#if HAVE_POSIX
+ sigaction(SIGWINCH, &saved_sigaction, NULL);
+#endif
+
+ write_str(TERM_ESC_RESTORE_CURSOR);
+
+ if (p->opts.alt_screen) {
+ write_str(TERM_ESC_NORMAL_SCREEN);
+ } else {
+ char *cmd = talloc_asprintf(vo, TERM_ESC_GOTO_YX, p->cols, 0);
+ write_str(cmd);
+ }
+
+ free_bufs(vo);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_kitty = {
+ .name = "kitty",
+ .description = "Kitty terminal graphics protocol",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .shm_fd = -1,
+ .opts.config_clear = true,
+ .opts.alt_screen = true,
+ },
+ .options = (const m_option_t[]) {
+ {"width", OPT_INT(opts.width)},
+ {"height", OPT_INT(opts.height)},
+ {"top", OPT_INT(opts.top)},
+ {"left", OPT_INT(opts.left)},
+ {"rows", OPT_INT(opts.rows)},
+ {"cols", OPT_INT(opts.cols)},
+ {"config-clear", OPT_BOOL(opts.config_clear), },
+ {"alt-screen", OPT_BOOL(opts.alt_screen), },
+ {"use-shm", OPT_BOOL(opts.use_shm), },
+ {0}
+ },
+ .options_prefix = "vo-kitty",
+};
diff --git a/video/out/vo_lavc.c b/video/out/vo_lavc.c
new file mode 100644
index 0000000..7170c1d
--- /dev/null
+++ b/video/out/vo_lavc.c
@@ -0,0 +1,262 @@
+/*
+ * video encoding using libavformat
+ *
+ * Copyright (C) 2010 Nicolas George <george@nsup.org>
+ * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "common/common.h"
+#include "options/options.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "mpv_talloc.h"
+#include "vo.h"
+
+#include "common/encode_lavc.h"
+
+#include "sub/osd.h"
+
+struct priv {
+ struct encoder_context *enc;
+
+ bool shutdown;
+};
+
+static int preinit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ vc->enc = encoder_context_alloc(vo->encode_lavc_ctx, STREAM_VIDEO, vo->log);
+ if (!vc->enc)
+ return -1;
+ talloc_steal(vc, vc->enc);
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ struct encoder_context *enc = vc->enc;
+
+ if (!vc->shutdown)
+ encoder_encode(enc, NULL); // finish encoding
+}
+
+static void on_ready(void *ptr)
+{
+ struct vo *vo = ptr;
+
+ vo_event(vo, VO_EVENT_INITIAL_UNBLOCK);
+}
+
+static int reconfig2(struct vo *vo, struct mp_image *img)
+{
+ struct priv *vc = vo->priv;
+ AVCodecContext *encoder = vc->enc->encoder;
+
+ struct mp_image_params *params = &img->params;
+ enum AVPixelFormat pix_fmt = imgfmt2pixfmt(params->imgfmt);
+ AVRational aspect = {params->p_w, params->p_h};
+ int width = params->w;
+ int height = params->h;
+
+ if (vc->shutdown)
+ return -1;
+
+ if (avcodec_is_open(encoder)) {
+ if (width == encoder->width && height == encoder->height &&
+ pix_fmt == encoder->pix_fmt)
+ {
+ // consider these changes not critical
+ MP_ERR(vo, "Ignoring mid-stream parameter changes!\n");
+ return 0;
+ }
+
+ /* FIXME Is it possible with raw video? */
+ MP_ERR(vo, "resolution changes not supported.\n");
+ goto error;
+ }
+
+ // When we get here, this must be the first call to reconfigure(). Thus, we
+ // can rely on no existing data in vc having been allocated yet.
+ // Reason:
+ // - Second calls after reconfigure() already failed once fail (due to the
+ // vc->shutdown check above).
+ // - Second calls after reconfigure() already succeeded once return early
+ // (due to the avcodec_is_open() check above).
+
+ if (pix_fmt == AV_PIX_FMT_NONE) {
+ MP_FATAL(vo, "Format %s not supported by lavc.\n",
+ mp_imgfmt_to_name(params->imgfmt));
+ goto error;
+ }
+
+ encoder->sample_aspect_ratio = aspect;
+ encoder->width = width;
+ encoder->height = height;
+ encoder->pix_fmt = pix_fmt;
+ encoder->colorspace = mp_csp_to_avcol_spc(params->color.space);
+ encoder->color_range = mp_csp_levels_to_avcol_range(params->color.levels);
+
+ AVRational tb;
+
+ // we want to handle:
+ // 1/25
+ // 1001/24000
+ // 1001/30000
+ // for this we would need 120000fps...
+ // however, mpeg-4 only allows 16bit values
+ // so let's take 1001/30000 out
+ tb.num = 24000;
+ tb.den = 1;
+
+ const AVRational *rates = encoder->codec->supported_framerates;
+ if (rates && rates[0].den)
+ tb = rates[av_find_nearest_q_idx(tb, rates)];
+
+ encoder->time_base = av_inv_q(tb);
+
+ // Used for rate control, level selection, etc.
+ // Usually it's not too catastrophic if this isn't exactly correct,
+ // as long as it's not off by orders of magnitude.
+ // If we don't set anything, encoders will use the time base,
+ // and 24000 is so high that the output can end up extremely screwy (see #11215),
+ // so we default to 240 if we don't have a real value.
+ if (img->nominal_fps > 0)
+ encoder->framerate = av_d2q(img->nominal_fps, img->nominal_fps * 1001 + 2); // Hopefully give exact results for NTSC rates
+ else
+ encoder->framerate = (AVRational){ 240, 1 };
+
+ if (!encoder_init_codec_and_muxer(vc->enc, on_ready, vo))
+ goto error;
+
+ return 0;
+
+error:
+ vc->shutdown = true;
+ return -1;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *vc = vo->priv;
+
+ enum AVPixelFormat pix_fmt = imgfmt2pixfmt(format);
+ const enum AVPixelFormat *p = vc->enc->encoder->codec->pix_fmts;
+
+ if (!p)
+ return 1;
+
+ while (*p != AV_PIX_FMT_NONE) {
+ if (*p == pix_fmt)
+ return 1;
+ p++;
+ }
+
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *voframe)
+{
+ struct priv *vc = vo->priv;
+ struct encoder_context *enc = vc->enc;
+ struct encode_lavc_context *ectx = enc->encode_lavc_ctx;
+ AVCodecContext *avc = enc->encoder;
+
+ if (voframe->redraw || voframe->repeat || voframe->num_frames < 1)
+ return;
+
+ struct mp_image *mpi = voframe->frames[0];
+
+ struct mp_osd_res dim = osd_res_from_image_params(vo->params);
+ osd_draw_on_image(vo->osd, dim, mpi->pts, OSD_DRAW_SUB_ONLY, mpi);
+
+ if (vc->shutdown)
+ return;
+
+ // Lock for shared timestamp fields.
+ mp_mutex_lock(&ectx->lock);
+
+ double pts = mpi->pts;
+ double outpts = pts;
+ if (!enc->options->rawts) {
+ // fix the discontinuity pts offset
+ if (ectx->discontinuity_pts_offset == MP_NOPTS_VALUE) {
+ ectx->discontinuity_pts_offset = ectx->next_in_pts - pts;
+ } else if (fabs(pts + ectx->discontinuity_pts_offset -
+ ectx->next_in_pts) > 30)
+ {
+ MP_WARN(vo, "detected an unexpected discontinuity (pts jumped by "
+ "%f seconds)\n",
+ pts + ectx->discontinuity_pts_offset - ectx->next_in_pts);
+ ectx->discontinuity_pts_offset = ectx->next_in_pts - pts;
+ }
+
+ outpts = pts + ectx->discontinuity_pts_offset;
+ }
+
+ if (!enc->options->rawts) {
+ // calculate expected pts of next video frame
+ double timeunit = av_q2d(avc->time_base);
+ double expected_next_pts = pts + timeunit;
+ // set next allowed output pts value
+ double nextpts = expected_next_pts + ectx->discontinuity_pts_offset;
+ if (nextpts > ectx->next_in_pts)
+ ectx->next_in_pts = nextpts;
+ }
+
+ mp_mutex_unlock(&ectx->lock);
+
+ AVFrame *frame = mp_image_to_av_frame(mpi);
+ MP_HANDLE_OOM(frame);
+
+ frame->pts = rint(outpts * av_q2d(av_inv_q(avc->time_base)));
+ frame->pict_type = 0; // keep this at unknown/undefined
+ frame->quality = avc->global_quality;
+ encoder_encode(enc, frame);
+ av_frame_free(&frame);
+}
+
+static void flip_page(struct vo *vo)
+{
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+const struct vo_driver video_out_lavc = {
+ .encode = true,
+ .description = "video encoding using libavcodec",
+ .name = "lavc",
+ .initially_blocked = true,
+ .untimed = true,
+ .priv_size = sizeof(struct priv),
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig2 = reconfig2,
+ .control = control,
+ .uninit = uninit,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+};
+
+// vim: sw=4 ts=4 et tw=80
diff --git a/video/out/vo_libmpv.c b/video/out/vo_libmpv.c
new file mode 100644
index 0000000..972588e
--- /dev/null
+++ b/video/out/vo_libmpv.c
@@ -0,0 +1,748 @@
+#include <assert.h>
+#include <limits.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "misc/dispatch.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "aspect.h"
+#include "dr_helper.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "common/global.h"
+#include "player/client.h"
+
+#include "libmpv.h"
+
+/*
+ * mpv_render_context is managed by the host application - the host application
+ * can access it any time, even if the VO is destroyed (or not created yet).
+ *
+ * - the libmpv user can mix render API and normal API; thus render API
+ * functions can wait on the core, but not the reverse
+ * - the core does blocking calls into the VO thread, thus the VO functions
+ * can't wait on the user calling the API functions
+ * - to make video timing work like it should, the VO thread waits on the
+ * render API user anyway, and the (unlikely) deadlock is avoided with
+ * a timeout
+ *
+ * Locking: mpv core > VO > mpv_render_context.lock > mp_client_api.lock
+ * > mpv_render_context.update_lock
+ * And: render thread > VO (wait for present)
+ * VO > render thread (wait for present done, via timeout)
+ *
+ * Locking gets more complex with advanced_control enabled. Use
+ * mpv_render_context.dispatch with care; synchronous calls can add lock
+ * dependencies.
+ */
+
+struct vo_priv {
+ struct mpv_render_context *ctx; // immutable after init
+};
+
+struct mpv_render_context {
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct mp_client_api *client_api;
+
+ atomic_bool in_use;
+
+ // --- Immutable after init
+ struct mp_dispatch_queue *dispatch;
+ bool advanced_control;
+ struct dr_helper *dr; // NULL if advanced_control disabled
+
+ mp_mutex control_lock;
+ // --- Protected by control_lock
+ mp_render_cb_control_fn control_cb;
+ void *control_cb_ctx;
+
+ mp_mutex update_lock;
+ mp_cond update_cond; // paired with update_lock
+
+ // --- Protected by update_lock
+ mpv_render_update_fn update_cb;
+ void *update_cb_ctx;
+
+ mp_mutex lock;
+ mp_cond video_wait; // paired with lock
+
+ // --- Protected by lock
+ struct vo_frame *next_frame; // next frame to draw
+ int64_t present_count; // incremented when next frame can be shown
+ int64_t expected_flip_count; // next vsync event for next_frame
+ bool redrawing; // next_frame was a redraw request
+ int64_t flip_count;
+ struct vo_frame *cur_frame;
+ struct mp_image_params img_params;
+ int vp_w, vp_h;
+ bool flip;
+ bool imgfmt_supported[IMGFMT_END - IMGFMT_START];
+ bool need_reconfig;
+ bool need_resize;
+ bool need_reset;
+ bool need_update_external;
+ struct vo *vo;
+
+ // --- Mostly immutable after init.
+ struct mp_hwdec_devices *hwdec_devs;
+
+ // --- All of these can only be accessed from mpv_render_*() API, for
+ // which the user makes sure they're called synchronized.
+ struct render_backend *renderer;
+ struct m_config_cache *vo_opts_cache;
+ struct mp_vo_opts *vo_opts;
+};
+
+const struct render_backend_fns *render_backends[] = {
+ &render_backend_gpu,
+ &render_backend_sw,
+ NULL
+};
+
+static void update(struct mpv_render_context *ctx)
+{
+ mp_mutex_lock(&ctx->update_lock);
+ if (ctx->update_cb)
+ ctx->update_cb(ctx->update_cb_ctx);
+
+ mp_cond_broadcast(&ctx->update_cond);
+ mp_mutex_unlock(&ctx->update_lock);
+}
+
+void *get_mpv_render_param(mpv_render_param *params, mpv_render_param_type type,
+ void *def)
+{
+ for (int n = 0; params && params[n].type; n++) {
+ if (params[n].type == type)
+ return params[n].data;
+ }
+ return def;
+}
+
+static void forget_frames(struct mpv_render_context *ctx, bool all)
+{
+ mp_cond_broadcast(&ctx->video_wait);
+ if (all) {
+ talloc_free(ctx->cur_frame);
+ ctx->cur_frame = NULL;
+ }
+}
+
+static void dispatch_wakeup(void *ptr)
+{
+ struct mpv_render_context *ctx = ptr;
+
+ update(ctx);
+}
+
+static struct mp_image *render_get_image(void *ptr, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct mpv_render_context *ctx = ptr;
+
+ return ctx->renderer->fns->get_image(ctx->renderer, imgfmt, w, h, stride_align, flags);
+}
+
+int mpv_render_context_create(mpv_render_context **res, mpv_handle *mpv,
+ mpv_render_param *params)
+{
+ mpv_render_context *ctx = talloc_zero(NULL, mpv_render_context);
+ mp_mutex_init(&ctx->control_lock);
+ mp_mutex_init(&ctx->lock);
+ mp_mutex_init(&ctx->update_lock);
+ mp_cond_init(&ctx->update_cond);
+ mp_cond_init(&ctx->video_wait);
+
+ ctx->global = mp_client_get_global(mpv);
+ ctx->client_api = ctx->global->client_api;
+ ctx->log = mp_log_new(ctx, ctx->global->log, "libmpv_render");
+
+ ctx->vo_opts_cache = m_config_cache_alloc(ctx, ctx->global, &vo_sub_opts);
+ ctx->vo_opts = ctx->vo_opts_cache->opts;
+
+ ctx->dispatch = mp_dispatch_create(ctx);
+ mp_dispatch_set_wakeup_fn(ctx->dispatch, dispatch_wakeup, ctx);
+
+ if (GET_MPV_RENDER_PARAM(params, MPV_RENDER_PARAM_ADVANCED_CONTROL, int, 0))
+ ctx->advanced_control = true;
+
+ int err = MPV_ERROR_NOT_IMPLEMENTED;
+ for (int n = 0; render_backends[n]; n++) {
+ ctx->renderer = talloc_zero(NULL, struct render_backend);
+ *ctx->renderer = (struct render_backend){
+ .global = ctx->global,
+ .log = ctx->log,
+ .fns = render_backends[n],
+ };
+ err = ctx->renderer->fns->init(ctx->renderer, params);
+ if (err >= 0)
+ break;
+ ctx->renderer->fns->destroy(ctx->renderer);
+ talloc_free(ctx->renderer->priv);
+ TA_FREEP(&ctx->renderer);
+ if (err != MPV_ERROR_NOT_IMPLEMENTED)
+ break;
+ }
+
+ if (err < 0) {
+ mpv_render_context_free(ctx);
+ return err;
+ }
+
+ ctx->hwdec_devs = ctx->renderer->hwdec_devs;
+
+ for (int n = IMGFMT_START; n < IMGFMT_END; n++) {
+ ctx->imgfmt_supported[n - IMGFMT_START] =
+ ctx->renderer->fns->check_format(ctx->renderer, n);
+ }
+
+ if (ctx->renderer->fns->get_image && ctx->advanced_control)
+ ctx->dr = dr_helper_create(ctx->dispatch, render_get_image, ctx);
+
+ if (!mp_set_main_render_context(ctx->client_api, ctx, true)) {
+ MP_ERR(ctx, "There is already a mpv_render_context set.\n");
+ mpv_render_context_free(ctx);
+ return MPV_ERROR_GENERIC;
+ }
+
+ *res = ctx;
+ return 0;
+}
+
+void mpv_render_context_set_update_callback(mpv_render_context *ctx,
+ mpv_render_update_fn callback,
+ void *callback_ctx)
+{
+ mp_mutex_lock(&ctx->update_lock);
+ ctx->update_cb = callback;
+ ctx->update_cb_ctx = callback_ctx;
+ if (ctx->update_cb)
+ ctx->update_cb(ctx->update_cb_ctx);
+ mp_mutex_unlock(&ctx->update_lock);
+}
+
+void mp_render_context_set_control_callback(mpv_render_context *ctx,
+ mp_render_cb_control_fn callback,
+ void *callback_ctx)
+{
+ mp_mutex_lock(&ctx->control_lock);
+ ctx->control_cb = callback;
+ ctx->control_cb_ctx = callback_ctx;
+ mp_mutex_unlock(&ctx->control_lock);
+}
+
+void mpv_render_context_free(mpv_render_context *ctx)
+{
+ if (!ctx)
+ return;
+
+ // From here on, ctx becomes invisible and cannot be newly acquired. Only
+ // a VO could still hold a reference.
+ mp_set_main_render_context(ctx->client_api, ctx, false);
+
+ if (atomic_load(&ctx->in_use)) {
+ // Start destroy the VO, and also bring down the decoder etc., which
+ // still might be using the hwdec context or use DR images. The above
+ // mp_set_main_render_context() call guarantees it can't come back (so
+ // ctx->vo can't change to non-NULL).
+ // In theory, this races with vo_libmpv exiting and another VO being
+ // used, which is a harmless grotesque corner case.
+ kill_video_async(ctx->client_api);
+
+ while (atomic_load(&ctx->in_use)) {
+ // As a nasty detail, we need to wait until the VO is released, but
+ // also need to react to update() calls during it (the update calls
+ // are supposed to trigger processing ctx->dispatch). We solve this
+ // by making the VO uninit function call mp_dispatch_interrupt().
+ //
+ // Other than that, processing ctx->dispatch is needed to serve the
+ // video decoder, which might still not be fully destroyed, and e.g.
+ // performs calls to release DR images (or, as a grotesque corner
+ // case may even try to allocate new ones).
+ //
+ // Once the VO is released, ctx->dispatch becomes truly inactive.
+ // (The libmpv API user could call mpv_render_context_update() while
+ // mpv_render_context_free() is being called, but of course this is
+ // invalid.)
+ mp_dispatch_queue_process(ctx->dispatch, INFINITY);
+ }
+ }
+
+ mp_mutex_lock(&ctx->lock);
+ // Barrier - guarantee uninit() has left the lock region. It will access ctx
+ // until the lock has been released, so we must not proceed with destruction
+ // before we can acquire the lock. (The opposite, uninit() acquiring the
+ // lock, can not happen anymore at this point - we've waited for VO uninit,
+ // and prevented that new VOs can be created.)
+ mp_mutex_unlock(&ctx->lock);
+
+ assert(!atomic_load(&ctx->in_use));
+ assert(!ctx->vo);
+
+ // With the dispatch queue not being served anymore, allow frame free
+ // requests from this thread to be served directly.
+ if (ctx->dr)
+ dr_helper_acquire_thread(ctx->dr);
+
+ // Possibly remaining outstanding work.
+ mp_dispatch_queue_process(ctx->dispatch, 0);
+
+ forget_frames(ctx, true);
+
+ if (ctx->renderer) {
+ ctx->renderer->fns->destroy(ctx->renderer);
+ talloc_free(ctx->renderer->priv);
+ talloc_free(ctx->renderer);
+ }
+ talloc_free(ctx->dr);
+ talloc_free(ctx->dispatch);
+
+ mp_cond_destroy(&ctx->update_cond);
+ mp_cond_destroy(&ctx->video_wait);
+ mp_mutex_destroy(&ctx->update_lock);
+ mp_mutex_destroy(&ctx->lock);
+ mp_mutex_destroy(&ctx->control_lock);
+
+ talloc_free(ctx);
+}
+
+// Try to mark the context as "in exclusive use" (e.g. by a VO).
+// Note: the function must not acquire any locks, because it's called with an
+// external leaf lock held.
+bool mp_render_context_acquire(mpv_render_context *ctx)
+{
+ bool prev = false;
+ return atomic_compare_exchange_strong(&ctx->in_use, &prev, true);
+}
+
+int mpv_render_context_render(mpv_render_context *ctx, mpv_render_param *params)
+{
+ mp_mutex_lock(&ctx->lock);
+
+ int do_render =
+ !GET_MPV_RENDER_PARAM(params, MPV_RENDER_PARAM_SKIP_RENDERING, int, 0);
+
+ if (do_render) {
+ int vp_w, vp_h;
+ int err = ctx->renderer->fns->get_target_size(ctx->renderer, params,
+ &vp_w, &vp_h);
+ if (err < 0) {
+ mp_mutex_unlock(&ctx->lock);
+ return err;
+ }
+
+ if (ctx->vo && (ctx->vp_w != vp_w || ctx->vp_h != vp_h ||
+ ctx->need_resize))
+ {
+ ctx->vp_w = vp_w;
+ ctx->vp_h = vp_h;
+
+ m_config_cache_update(ctx->vo_opts_cache);
+
+ struct mp_rect src, dst;
+ struct mp_osd_res osd;
+ mp_get_src_dst_rects(ctx->log, ctx->vo_opts, ctx->vo->driver->caps,
+ &ctx->img_params, vp_w, abs(vp_h),
+ 1.0, &src, &dst, &osd);
+
+ ctx->renderer->fns->resize(ctx->renderer, &src, &dst, &osd);
+ }
+ ctx->need_resize = false;
+ }
+
+ if (ctx->need_reconfig)
+ ctx->renderer->fns->reconfig(ctx->renderer, &ctx->img_params);
+ ctx->need_reconfig = false;
+
+ if (ctx->need_update_external)
+ ctx->renderer->fns->update_external(ctx->renderer, ctx->vo);
+ ctx->need_update_external = false;
+
+ if (ctx->need_reset) {
+ ctx->renderer->fns->reset(ctx->renderer);
+ if (ctx->cur_frame)
+ ctx->cur_frame->still = true;
+ }
+ ctx->need_reset = false;
+
+ struct vo_frame *frame = ctx->next_frame;
+ int64_t wait_present_count = ctx->present_count;
+ if (frame) {
+ ctx->next_frame = NULL;
+ if (!(frame->redraw || !frame->current))
+ wait_present_count += 1;
+ mp_cond_broadcast(&ctx->video_wait);
+ talloc_free(ctx->cur_frame);
+ ctx->cur_frame = vo_frame_ref(frame);
+ } else {
+ frame = vo_frame_ref(ctx->cur_frame);
+ if (frame)
+ frame->redraw = true;
+ MP_STATS(ctx, "glcb-noframe");
+ }
+ struct vo_frame dummy = {0};
+ if (!frame)
+ frame = &dummy;
+
+ mp_mutex_unlock(&ctx->lock);
+
+ MP_STATS(ctx, "glcb-render");
+
+ int err = 0;
+
+ if (do_render)
+ err = ctx->renderer->fns->render(ctx->renderer, params, frame);
+
+ if (frame != &dummy)
+ talloc_free(frame);
+
+ if (GET_MPV_RENDER_PARAM(params, MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME,
+ int, 1))
+ {
+ mp_mutex_lock(&ctx->lock);
+ while (wait_present_count > ctx->present_count)
+ mp_cond_wait(&ctx->video_wait, &ctx->lock);
+ mp_mutex_unlock(&ctx->lock);
+ }
+
+ return err;
+}
+
+void mpv_render_context_report_swap(mpv_render_context *ctx)
+{
+ MP_STATS(ctx, "glcb-reportflip");
+
+ mp_mutex_lock(&ctx->lock);
+ ctx->flip_count += 1;
+ mp_cond_broadcast(&ctx->video_wait);
+ mp_mutex_unlock(&ctx->lock);
+}
+
+uint64_t mpv_render_context_update(mpv_render_context *ctx)
+{
+ uint64_t res = 0;
+
+ mp_dispatch_queue_process(ctx->dispatch, 0);
+
+ mp_mutex_lock(&ctx->lock);
+ if (ctx->next_frame)
+ res |= MPV_RENDER_UPDATE_FRAME;
+ mp_mutex_unlock(&ctx->lock);
+ return res;
+}
+
+int mpv_render_context_set_parameter(mpv_render_context *ctx,
+ mpv_render_param param)
+{
+ return ctx->renderer->fns->set_parameter(ctx->renderer, param);
+}
+
+int mpv_render_context_get_info(mpv_render_context *ctx,
+ mpv_render_param param)
+{
+ int res = MPV_ERROR_NOT_IMPLEMENTED;
+ mp_mutex_lock(&ctx->lock);
+
+ switch (param.type) {
+ case MPV_RENDER_PARAM_NEXT_FRAME_INFO: {
+ mpv_render_frame_info *info = param.data;
+ *info = (mpv_render_frame_info){0};
+ struct vo_frame *frame = ctx->next_frame;
+ if (frame) {
+ info->flags =
+ MPV_RENDER_FRAME_INFO_PRESENT |
+ (frame->redraw ? MPV_RENDER_FRAME_INFO_REDRAW : 0) |
+ (frame->repeat ? MPV_RENDER_FRAME_INFO_REPEAT : 0) |
+ (frame->display_synced && !frame->redraw ?
+ MPV_RENDER_FRAME_INFO_BLOCK_VSYNC : 0);
+ info->target_time = frame->pts;
+ }
+ res = 0;
+ break;
+ }
+ default:;
+ }
+
+ mp_mutex_unlock(&ctx->lock);
+ return res;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ mp_mutex_lock(&ctx->lock);
+ assert(!ctx->next_frame);
+ ctx->next_frame = vo_frame_ref(frame);
+ ctx->expected_flip_count = ctx->flip_count + 1;
+ ctx->redrawing = frame->redraw || !frame->current;
+ mp_mutex_unlock(&ctx->lock);
+
+ update(ctx);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+ int64_t until = mp_time_ns() + MP_TIME_MS_TO_NS(200);
+
+ mp_mutex_lock(&ctx->lock);
+
+ // Wait until frame was rendered
+ while (ctx->next_frame) {
+ if (mp_cond_timedwait_until(&ctx->video_wait, &ctx->lock, until)) {
+ if (ctx->next_frame) {
+ MP_VERBOSE(vo, "mpv_render_context_render() not being called "
+ "or stuck.\n");
+ goto done;
+ }
+ }
+ }
+
+ // Unblock mpv_render_context_render().
+ ctx->present_count += 1;
+ mp_cond_broadcast(&ctx->video_wait);
+
+ if (ctx->redrawing)
+ goto done; // do not block for redrawing
+
+ // Wait until frame was presented
+ while (ctx->expected_flip_count > ctx->flip_count) {
+ // mpv_render_report_swap() is declared as optional API.
+ // Assume the user calls it consistently _if_ it's called at all.
+ if (!ctx->flip_count)
+ break;
+ if (mp_cond_timedwait_until(&ctx->video_wait, &ctx->lock, until)) {
+ MP_VERBOSE(vo, "mpv_render_report_swap() not being called.\n");
+ goto done;
+ }
+ }
+
+done:
+
+ // Cleanup after the API user is not reacting, or is being unusually slow.
+ if (ctx->next_frame) {
+ talloc_free(ctx->cur_frame);
+ ctx->cur_frame = ctx->next_frame;
+ ctx->next_frame = NULL;
+ ctx->present_count += 2;
+ mp_cond_signal(&ctx->video_wait);
+ vo_increment_drop_count(vo, 1);
+ }
+
+ mp_mutex_unlock(&ctx->lock);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ bool ok = false;
+ mp_mutex_lock(&ctx->lock);
+ if (format >= IMGFMT_START && format < IMGFMT_END)
+ ok = ctx->imgfmt_supported[format - IMGFMT_START];
+ mp_mutex_unlock(&ctx->lock);
+ return ok;
+}
+
+static void run_control_on_render_thread(void *p)
+{
+ void **args = p;
+ struct mpv_render_context *ctx = args[0];
+ int request = (intptr_t)args[1];
+ void *data = args[2];
+ int ret = VO_NOTIMPL;
+
+ switch (request) {
+ case VOCTRL_SCREENSHOT: {
+ mp_mutex_lock(&ctx->lock);
+ struct vo_frame *frame = vo_frame_ref(ctx->cur_frame);
+ mp_mutex_unlock(&ctx->lock);
+ if (frame && ctx->renderer->fns->screenshot)
+ ctx->renderer->fns->screenshot(ctx->renderer, frame, data);
+ talloc_free(frame);
+ break;
+ }
+ case VOCTRL_PERFORMANCE_DATA: {
+ if (ctx->renderer->fns->perfdata) {
+ ctx->renderer->fns->perfdata(ctx->renderer, data);
+ ret = VO_TRUE;
+ }
+ break;
+ }
+ }
+
+ *(int *)args[3] = ret;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ switch (request) {
+ case VOCTRL_RESET:
+ mp_mutex_lock(&ctx->lock);
+ forget_frames(ctx, false);
+ ctx->need_reset = true;
+ mp_mutex_unlock(&ctx->lock);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_PAUSE:
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SET_PANSCAN:
+ mp_mutex_lock(&ctx->lock);
+ ctx->need_resize = true;
+ mp_mutex_unlock(&ctx->lock);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_UPDATE_RENDER_OPTS:
+ mp_mutex_lock(&ctx->lock);
+ ctx->need_update_external = true;
+ mp_mutex_unlock(&ctx->lock);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ }
+
+ // VOCTRLs to be run on the renderer thread (if possible at all).
+ if (ctx->advanced_control) {
+ switch (request) {
+ case VOCTRL_SCREENSHOT:
+ case VOCTRL_PERFORMANCE_DATA: {
+ int ret;
+ void *args[] = {ctx, (void *)(intptr_t)request, data, &ret};
+ mp_dispatch_run(ctx->dispatch, run_control_on_render_thread, args);
+ return ret;
+ }
+ }
+ }
+
+ int r = VO_NOTIMPL;
+ mp_mutex_lock(&ctx->control_lock);
+ if (ctx->control_cb) {
+ int events = 0;
+ r = p->ctx->control_cb(vo, p->ctx->control_cb_ctx,
+ &events, request, data);
+ vo_event(vo, events);
+ }
+ mp_mutex_unlock(&ctx->control_lock);
+
+ return r;
+}
+
+static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ if (ctx->dr)
+ return dr_helper_get_image(ctx->dr, imgfmt, w, h, stride_align, flags);
+
+ return NULL;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ mp_mutex_lock(&ctx->lock);
+ forget_frames(ctx, true);
+ ctx->img_params = *params;
+ ctx->need_reconfig = true;
+ ctx->need_resize = true;
+ mp_mutex_unlock(&ctx->lock);
+
+ control(vo, VOCTRL_RECONFIG, NULL);
+
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ control(vo, VOCTRL_UNINIT, NULL);
+
+ mp_mutex_lock(&ctx->lock);
+
+ forget_frames(ctx, true);
+ ctx->img_params = (struct mp_image_params){0};
+ ctx->need_reconfig = true;
+ ctx->need_resize = true;
+ ctx->need_update_external = true;
+ ctx->need_reset = true;
+ ctx->vo = NULL;
+
+ // The following do not normally need ctx->lock, however, ctx itself may
+ // become invalid once we release ctx->lock.
+ bool prev_in_use = atomic_exchange(&ctx->in_use, false);
+ assert(prev_in_use); // obviously must have been set
+ mp_dispatch_interrupt(ctx->dispatch);
+
+ mp_mutex_unlock(&ctx->lock);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct vo_priv *p = vo->priv;
+
+ struct mpv_render_context *ctx =
+ mp_client_api_acquire_render_context(vo->global->client_api);
+ p->ctx = ctx;
+
+ if (!ctx) {
+ if (!vo->probing)
+ MP_FATAL(vo, "No render context set.\n");
+ return -1;
+ }
+
+ mp_mutex_lock(&ctx->lock);
+ ctx->vo = vo;
+ ctx->need_resize = true;
+ ctx->need_update_external = true;
+ mp_mutex_unlock(&ctx->lock);
+
+ vo->hwdec_devs = ctx->hwdec_devs;
+ control(vo, VOCTRL_PREINIT, NULL);
+
+ return 0;
+}
+
+const struct vo_driver video_out_libmpv = {
+ .description = "render API for libmpv",
+ .name = "libmpv",
+ .caps = VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .get_image_ts = get_image,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct vo_priv),
+};
diff --git a/video/out/vo_mediacodec_embed.c b/video/out/vo_mediacodec_embed.c
new file mode 100644
index 0000000..08d3866
--- /dev/null
+++ b/video/out/vo_mediacodec_embed.c
@@ -0,0 +1,127 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libavcodec/mediacodec.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_mediacodec.h>
+
+#include "common/common.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "video/hwdec.h"
+
+struct priv {
+ struct mp_image *next_image;
+ struct mp_hwdec_ctx hwctx;
+};
+
+static AVBufferRef *create_mediacodec_device_ref(struct vo *vo)
+{
+ AVBufferRef *device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_MEDIACODEC);
+ if (!device_ref)
+ return NULL;
+
+ AVHWDeviceContext *ctx = (void *)device_ref->data;
+ AVMediaCodecDeviceContext *hwctx = ctx->hwctx;
+ assert(vo->opts->WinID != 0 && vo->opts->WinID != -1);
+ hwctx->surface = (void *)(intptr_t)(vo->opts->WinID);
+
+ if (av_hwdevice_ctx_init(device_ref) < 0)
+ av_buffer_unref(&device_ref);
+
+ return device_ref;
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ vo->hwdec_devs = hwdec_devices_create();
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = "mediacodec_embed",
+ .av_device_ref = create_mediacodec_device_ref(vo),
+ .hw_imgfmt = IMGFMT_MEDIACODEC,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(vo, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(vo->hwdec_devs, &p->hwctx);
+ return 0;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (!p->next_image)
+ return;
+
+ AVMediaCodecBuffer *buffer = (AVMediaCodecBuffer *)p->next_image->planes[3];
+ av_mediacodec_release_buffer(buffer, 1);
+ mp_image_unrefp(&p->next_image);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ mp_image_t *mpi = NULL;
+ if (!frame->redraw && !frame->repeat)
+ mpi = mp_image_new_ref(frame->current);
+
+ talloc_free(p->next_image);
+ p->next_image = mpi;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT_MEDIACODEC;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ mp_image_unrefp(&p->next_image);
+
+ hwdec_devices_remove(vo->hwdec_devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+const struct vo_driver video_out_mediacodec_embed = {
+ .description = "Android (Embedded MediaCodec Surface)",
+ .name = "mediacodec_embed",
+ .caps = VO_CAP_NORETAIN,
+ .preinit = preinit,
+ .query_format = query_format,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .reconfig = reconfig,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_null.c b/video/out/vo_null.c
new file mode 100644
index 0000000..0c49062
--- /dev/null
+++ b/video/out/vo_null.c
@@ -0,0 +1,104 @@
+/*
+ * based on video_out_null.c from mpeg2dec
+ *
+ * Copyright (C) Aaron Holtzman - June 2000
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include "common/msg.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "osdep/timer.h"
+#include "options/m_option.h"
+
+struct priv {
+ int64_t last_vsync;
+
+ double cfg_fps;
+};
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (p->cfg_fps) {
+ int64_t ft = 1e9 / p->cfg_fps;
+ int64_t prev_vsync = mp_time_ns() / ft;
+ int64_t target_time = (prev_vsync + 1) * ft;
+ for (;;) {
+ int64_t now = mp_time_ns();
+ if (now >= target_time)
+ break;
+ mp_sleep_ns(target_time - now);
+ }
+ }
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return 1;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+}
+
+static int preinit(struct vo *vo)
+{
+ return 0;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+ switch (request) {
+ case VOCTRL_GET_DISPLAY_FPS:
+ if (!p->cfg_fps)
+ break;
+ *(double *)data = p->cfg_fps;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+#define OPT_BASE_STRUCT struct priv
+const struct vo_driver video_out_null = {
+ .description = "Null video output",
+ .name = "null",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .options = (const struct m_option[]) {
+ {"fps", OPT_DOUBLE(cfg_fps), M_RANGE(0, 10000)},
+ {0},
+ },
+ .options_prefix = "vo-null",
+};
diff --git a/video/out/vo_rpi.c b/video/out/vo_rpi.c
new file mode 100644
index 0000000..55f1a68
--- /dev/null
+++ b/video/out/vo_rpi.c
@@ -0,0 +1,938 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <bcm_host.h>
+#include <interface/mmal/mmal.h>
+#include <interface/mmal/util/mmal_util.h>
+#include <interface/mmal/util/mmal_default_components.h>
+#include <interface/mmal/vc/mmal_vc_api.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include <libavutil/rational.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "opengl/common.h"
+#include "options/m_config.h"
+#include "osdep/timer.h"
+#include "vo.h"
+#include "win_state.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+
+#include "opengl/ra_gl.h"
+#include "gpu/video.h"
+
+struct mp_egl_rpi {
+ struct mp_log *log;
+ struct GL *gl;
+ struct ra *ra;
+ EGLDisplay egl_display;
+ EGLConfig egl_config;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+ // yep, the API keeps a pointer to it
+ EGL_DISPMANX_WINDOW_T egl_window;
+};
+
+struct priv {
+ DISPMANX_DISPLAY_HANDLE_T display;
+ DISPMANX_ELEMENT_HANDLE_T window;
+ DISPMANX_ELEMENT_HANDLE_T osd_overlay;
+ DISPMANX_UPDATE_HANDLE_T update;
+ uint32_t w, h;
+ uint32_t x, y;
+ double display_fps;
+
+ double osd_pts;
+ struct mp_osd_res osd_res;
+ struct m_config_cache *opts_cache;
+
+ struct mp_egl_rpi egl;
+ struct gl_video *gl_video;
+ struct mpgl_osd *osd;
+
+ MMAL_COMPONENT_T *renderer;
+ bool renderer_enabled;
+
+ bool display_synced, skip_osd;
+ struct mp_image *next_image;
+
+ // for RAM input
+ MMAL_POOL_T *swpool;
+
+ mp_mutex display_mutex;
+ mp_cond display_cond;
+ int64_t vsync_counter;
+ bool reload_display;
+
+ int background_layer;
+ int video_layer;
+ int osd_layer;
+
+ int display_nr;
+ int layer;
+ bool background;
+ bool enable_osd;
+};
+
+// Magic alignments (in pixels) expected by the MMAL internals.
+#define ALIGN_W 32
+#define ALIGN_H 16
+
+static void recreate_renderer(struct vo *vo);
+
+static void *get_proc_address(const GLubyte *name)
+{
+ void *p = eglGetProcAddress(name);
+ // EGL 1.4 (supported by the RPI firmware) does not necessarily return
+ // function pointers for core functions.
+ if (!p) {
+ void *h = dlopen("/opt/vc/lib/libbrcmGLESv2.so", RTLD_LAZY);
+ if (h) {
+ p = dlsym(h, name);
+ dlclose(h);
+ }
+ }
+ return p;
+}
+
+static EGLConfig select_fb_config_egl(struct mp_egl_rpi *p)
+{
+ EGLint attributes[] = {
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_DEPTH_SIZE, 0,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_NONE
+ };
+
+ EGLint config_count;
+ EGLConfig config;
+
+ eglChooseConfig(p->egl_display, attributes, &config, 1, &config_count);
+
+ if (!config_count) {
+ MP_FATAL(p, "Could find EGL configuration!\n");
+ return NULL;
+ }
+
+ return config;
+}
+
+static void mp_egl_rpi_destroy(struct mp_egl_rpi *p)
+{
+ if (p->egl_display) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ }
+ if (p->egl_surface)
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ if (p->egl_context)
+ eglDestroyContext(p->egl_display, p->egl_context);
+ p->egl_context = EGL_NO_CONTEXT;
+ eglReleaseThread();
+ p->egl_display = EGL_NO_DISPLAY;
+ talloc_free(p->gl);
+ p->gl = NULL;
+}
+
+static int mp_egl_rpi_init(struct mp_egl_rpi *p, DISPMANX_ELEMENT_HANDLE_T window,
+ int w, int h)
+{
+ p->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(p, "EGL failed to initialize.\n");
+ goto fail;
+ }
+
+ eglBindAPI(EGL_OPENGL_ES_API);
+
+ EGLConfig config = select_fb_config_egl(p);
+ if (!config)
+ goto fail;
+
+ p->egl_window = (EGL_DISPMANX_WINDOW_T){
+ .element = window,
+ .width = w,
+ .height = h,
+ };
+ p->egl_surface = eglCreateWindowSurface(p->egl_display, config,
+ &p->egl_window, NULL);
+
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(p, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ EGLint context_attributes[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_NONE
+ };
+ p->egl_context = eglCreateContext(p->egl_display, config,
+ EGL_NO_CONTEXT, context_attributes);
+
+ if (p->egl_context == EGL_NO_CONTEXT) {
+ MP_FATAL(p, "Could not create EGL context!\n");
+ goto fail;
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context);
+
+ p->gl = talloc_zero(NULL, struct GL);
+
+ const char *exts = eglQueryString(p->egl_display, EGL_EXTENSIONS);
+ mpgl_load_functions(p->gl, get_proc_address, exts, p->log);
+
+ if (!p->gl->version && !p->gl->es)
+ goto fail;
+
+ p->ra = ra_create_gl(p->gl, p->log);
+ if (!p->ra)
+ goto fail;
+
+ return 0;
+
+fail:
+ mp_egl_rpi_destroy(p);
+ return -1;
+}
+
+// Make mpi point to buffer, assuming MMAL_ENCODING_I420.
+// buffer can be NULL.
+// Return the required buffer space.
+static size_t layout_buffer(struct mp_image *mpi, MMAL_BUFFER_HEADER_T *buffer,
+ struct mp_image_params *params)
+{
+ assert(params->imgfmt == IMGFMT_420P);
+ mp_image_set_params(mpi, params);
+ int w = MP_ALIGN_UP(params->w, ALIGN_W);
+ int h = MP_ALIGN_UP(params->h, ALIGN_H);
+ uint8_t *cur = buffer ? buffer->data : NULL;
+ size_t size = 0;
+ for (int i = 0; i < 3; i++) {
+ int div = i ? 2 : 1;
+ mpi->planes[i] = cur;
+ mpi->stride[i] = w / div;
+ size_t plane_size = h / div * mpi->stride[i];
+ if (cur)
+ cur += plane_size;
+ size += plane_size;
+ }
+ return size;
+}
+
+static void update_osd(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (!p->enable_osd)
+ return;
+
+ if (!gl_video_check_osd_change(p->gl_video, &p->osd_res, p->osd_pts)) {
+ p->skip_osd = true;
+ return;
+ }
+
+ MP_STATS(vo, "start rpi_osd");
+
+ struct vo_frame frame = {0};
+ struct ra_fbo target = {
+ .tex = ra_create_wrapped_fb(p->egl.ra, 0, p->osd_res.w, p->osd_res.h),
+ .flip = true,
+ };
+ gl_video_set_osd_pts(p->gl_video, p->osd_pts);
+ gl_video_render_frame(p->gl_video, &frame, target, RENDER_FRAME_DEF);
+ ra_tex_free(p->egl.ra, &target.tex);
+
+ MP_STATS(vo, "stop rpi_osd");
+}
+
+static void resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+
+ struct mp_rect src, dst;
+
+ vo_get_src_dst_rects(vo, &src, &dst, &p->osd_res);
+
+ int rotate[] = {MMAL_DISPLAY_ROT0,
+ MMAL_DISPLAY_ROT90,
+ MMAL_DISPLAY_ROT180,
+ MMAL_DISPLAY_ROT270};
+
+
+ int src_w = src.x1 - src.x0, src_h = src.y1 - src.y0,
+ dst_w = dst.x1 - dst.x0, dst_h = dst.y1 - dst.y0;
+ int p_x, p_y;
+ av_reduce(&p_x, &p_y, dst_w * src_h, src_w * dst_h, 16000);
+ MMAL_DISPLAYREGION_T dr = {
+ .hdr = { .id = MMAL_PARAMETER_DISPLAYREGION,
+ .size = sizeof(MMAL_DISPLAYREGION_T), },
+ .src_rect = { .x = src.x0, .y = src.y0, .width = src_w, .height = src_h },
+ .dest_rect = { .x = dst.x0 + p->x, .y = dst.y0 + p->y,
+ .width = dst_w, .height = dst_h },
+ .layer = p->video_layer,
+ .display_num = p->display_nr,
+ .pixel_x = p_x,
+ .pixel_y = p_y,
+ .transform = rotate[vo->params ? vo->params->rotate / 90 : 0],
+ .fullscreen = vo->opts->fullscreen,
+ .set = MMAL_DISPLAY_SET_SRC_RECT | MMAL_DISPLAY_SET_DEST_RECT |
+ MMAL_DISPLAY_SET_LAYER | MMAL_DISPLAY_SET_NUM |
+ MMAL_DISPLAY_SET_PIXEL | MMAL_DISPLAY_SET_TRANSFORM |
+ MMAL_DISPLAY_SET_FULLSCREEN,
+ };
+
+ if (vo->params && (vo->params->rotate % 180) == 90) {
+ MPSWAP(int, dr.src_rect.x, dr.src_rect.y);
+ MPSWAP(int, dr.src_rect.width, dr.src_rect.height);
+ }
+
+ if (mmal_port_parameter_set(input, &dr.hdr))
+ MP_WARN(vo, "could not set video rectangle\n");
+
+ if (p->gl_video)
+ gl_video_resize(p->gl_video, &src, &dst, &p->osd_res);
+}
+
+static void destroy_overlays(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (p->window)
+ vc_dispmanx_element_remove(p->update, p->window);
+ p->window = 0;
+
+ gl_video_uninit(p->gl_video);
+ p->gl_video = NULL;
+ ra_free(&p->egl.ra);
+ mp_egl_rpi_destroy(&p->egl);
+
+ if (p->osd_overlay)
+ vc_dispmanx_element_remove(p->update, p->osd_overlay);
+ p->osd_overlay = 0;
+}
+
+static int update_display_size(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ uint32_t n_w = 0, n_h = 0;
+ if (graphics_get_display_size(0, &n_w, &n_h) < 0) {
+ MP_FATAL(vo, "Could not get display size.\n");
+ return -1;
+ }
+
+ if (p->w == n_w && p->h == n_h)
+ return 0;
+
+ p->w = n_w;
+ p->h = n_h;
+
+ MP_VERBOSE(vo, "Display size: %dx%d\n", p->w, p->h);
+
+ return 0;
+}
+
+static int create_overlays(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ destroy_overlays(vo);
+
+ if (!p->display)
+ return -1;
+
+ if (vo->opts->fullscreen && p->background) {
+ // Use the whole screen.
+ VC_RECT_T dst = {.width = p->w, .height = p->h};
+ VC_RECT_T src = {.width = 1 << 16, .height = 1 << 16};
+ VC_DISPMANX_ALPHA_T alpha = {
+ .flags = DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS,
+ .opacity = 0xFF,
+ };
+
+ p->window = vc_dispmanx_element_add(p->update, p->display,
+ p->background_layer,
+ &dst, 0, &src,
+ DISPMANX_PROTECTION_NONE,
+ &alpha, 0, 0);
+ if (!p->window) {
+ MP_FATAL(vo, "Could not add DISPMANX element.\n");
+ return -1;
+ }
+ }
+
+ if (p->enable_osd) {
+ VC_RECT_T dst = {.x = p->x, .y = p->y,
+ .width = p->osd_res.w, .height = p->osd_res.h};
+ VC_RECT_T src = {.width = p->osd_res.w << 16, .height = p->osd_res.h << 16};
+ VC_DISPMANX_ALPHA_T alpha = {
+ .flags = DISPMANX_FLAGS_ALPHA_FROM_SOURCE,
+ .opacity = 0xFF,
+ };
+ p->osd_overlay = vc_dispmanx_element_add(p->update, p->display,
+ p->osd_layer,
+ &dst, 0, &src,
+ DISPMANX_PROTECTION_NONE,
+ &alpha, 0, 0);
+ if (!p->osd_overlay) {
+ MP_FATAL(vo, "Could not add DISPMANX element.\n");
+ return -1;
+ }
+
+ if (mp_egl_rpi_init(&p->egl, p->osd_overlay,
+ p->osd_res.w, p->osd_res.h) < 0)
+ {
+ MP_FATAL(vo, "EGL/GLES initialization for OSD renderer failed.\n");
+ return -1;
+ }
+ p->gl_video = gl_video_init(p->egl.ra, vo->log, vo->global);
+ gl_video_set_clear_color(p->gl_video, (struct m_color){.a = 0});
+ gl_video_set_osd_source(p->gl_video, vo->osd);
+ }
+
+ p->display_fps = 0;
+ TV_GET_STATE_RESP_T tvstate;
+ TV_DISPLAY_STATE_T tvstate_disp;
+ if (!vc_tv_get_state(&tvstate) && !vc_tv_get_display_state(&tvstate_disp)) {
+ if (tvstate_disp.state & (VC_HDMI_HDMI | VC_HDMI_DVI)) {
+ p->display_fps = tvstate_disp.display.hdmi.frame_rate;
+
+ HDMI_PROPERTY_PARAM_T param = {
+ .property = HDMI_PROPERTY_PIXEL_CLOCK_TYPE,
+ };
+ if (!vc_tv_hdmi_get_property(&param) &&
+ param.param1 == HDMI_PIXEL_CLOCK_TYPE_NTSC)
+ p->display_fps = p->display_fps / 1.001;
+ } else {
+ p->display_fps = tvstate_disp.display.sdtv.frame_rate;
+ }
+ }
+
+ resize(vo);
+
+ vo_event(vo, VO_EVENT_WIN_STATE);
+
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = vc_dispmanx_update_start(10);
+
+ return 0;
+}
+
+static int set_geometry(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (vo->opts->fullscreen) {
+ vo->dwidth = p->w;
+ vo->dheight = p->h;
+ p->x = p->y = 0;
+ } else {
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc = {0, 0, p->w, p->h};
+
+ vo_calc_window_geometry(vo, &screenrc, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ p->x = geo.win.x0;
+ p->y = geo.win.y0;
+ }
+
+ resize(vo);
+
+ if (create_overlays(vo) < 0)
+ return -1;
+
+ return 0;
+}
+
+static void wait_next_vsync(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ mp_mutex_lock(&p->display_mutex);
+ int64_t end = mp_time_ns() + MP_TIME_MS_TO_NS(50);
+ int64_t old = p->vsync_counter;
+ while (old == p->vsync_counter && !p->reload_display) {
+ if (mp_cond_timedwait_until(&p->display_cond, &p->display_mutex, end))
+ break;
+ }
+ mp_mutex_unlock(&p->display_mutex);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!p->renderer_enabled)
+ return;
+
+ struct mp_image *mpi = p->next_image;
+ p->next_image = NULL;
+
+ // For OSD
+ if (!p->skip_osd && p->egl.gl)
+ eglSwapBuffers(p->egl.egl_display, p->egl.egl_surface);
+ p->skip_osd = false;
+
+ if (mpi) {
+ MMAL_PORT_T *input = p->renderer->input[0];
+ MMAL_BUFFER_HEADER_T *ref = (void *)mpi->planes[3];
+
+ // Assume this field is free for use by us.
+ ref->user_data = mpi;
+
+ if (mmal_port_send_buffer(input, ref)) {
+ MP_ERR(vo, "could not queue picture!\n");
+ talloc_free(mpi);
+ }
+ }
+
+ if (p->display_synced)
+ wait_next_vsync(vo);
+}
+
+static void free_mmal_buffer(void *arg)
+{
+ MMAL_BUFFER_HEADER_T *buffer = arg;
+ mmal_buffer_header_release(buffer);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ if (!p->renderer_enabled)
+ return;
+
+ mp_image_t *mpi = NULL;
+ if (!frame->redraw && !frame->repeat)
+ mpi = mp_image_new_ref(frame->current);
+
+ talloc_free(p->next_image);
+ p->next_image = NULL;
+
+ if (mpi)
+ p->osd_pts = mpi->pts;
+
+ // Redraw only if the OSD has meaningfully changed, which we assume it
+ // hasn't when a frame is merely repeated for display sync.
+ p->skip_osd = !frame->redraw && frame->repeat;
+
+ if (!p->skip_osd && p->egl.gl)
+ update_osd(vo);
+
+ p->display_synced = frame->display_synced;
+
+ if (mpi && mpi->imgfmt != IMGFMT_MMAL) {
+ MMAL_BUFFER_HEADER_T *buffer = mmal_queue_wait(p->swpool->queue);
+ if (!buffer) {
+ talloc_free(mpi);
+ MP_ERR(vo, "Can't allocate buffer.\n");
+ return;
+ }
+ mmal_buffer_header_reset(buffer);
+
+ struct mp_image *new_ref = mp_image_new_custom_ref(NULL, buffer,
+ free_mmal_buffer);
+ if (!new_ref) {
+ mmal_buffer_header_release(buffer);
+ talloc_free(mpi);
+ MP_ERR(vo, "Out of memory.\n");
+ return;
+ }
+
+ mp_image_setfmt(new_ref, IMGFMT_MMAL);
+ new_ref->planes[3] = (void *)buffer;
+
+ struct mp_image dmpi = {0};
+ buffer->length = layout_buffer(&dmpi, buffer, vo->params);
+ mp_image_copy(&dmpi, mpi);
+
+ talloc_free(mpi);
+ mpi = new_ref;
+ }
+
+ p->next_image = mpi;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT_MMAL || format == IMGFMT_420P;
+}
+
+static MMAL_FOURCC_T map_csp(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_BT_601: return MMAL_COLOR_SPACE_ITUR_BT601;
+ case MP_CSP_BT_709: return MMAL_COLOR_SPACE_ITUR_BT709;
+ case MP_CSP_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
+ default: return MMAL_COLOR_SPACE_UNKNOWN;
+ }
+}
+
+static void control_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ mmal_buffer_header_release(buffer);
+}
+
+static void input_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ struct mp_image *mpi = buffer->user_data;
+ talloc_free(mpi);
+}
+
+static void disable_renderer(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (p->renderer_enabled) {
+ mmal_port_disable(p->renderer->control);
+ mmal_port_disable(p->renderer->input[0]);
+
+ mmal_port_flush(p->renderer->control);
+ mmal_port_flush(p->renderer->input[0]);
+
+ mmal_component_disable(p->renderer);
+ }
+ mmal_pool_destroy(p->swpool);
+ p->swpool = NULL;
+ p->renderer_enabled = false;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+ bool opaque = params->imgfmt == IMGFMT_MMAL;
+
+ if (!p->display)
+ return -1;
+
+ disable_renderer(vo);
+
+ input->format->encoding = opaque ? MMAL_ENCODING_OPAQUE : MMAL_ENCODING_I420;
+ input->format->es->video.width = MP_ALIGN_UP(params->w, ALIGN_W);
+ input->format->es->video.height = MP_ALIGN_UP(params->h, ALIGN_H);
+ input->format->es->video.crop = (MMAL_RECT_T){0, 0, params->w, params->h};
+ input->format->es->video.par = (MMAL_RATIONAL_T){params->p_w, params->p_h};
+ input->format->es->video.color_space = map_csp(params->color.space);
+
+ if (mmal_port_format_commit(input))
+ return -1;
+
+ input->buffer_num = MPMAX(input->buffer_num_min,
+ input->buffer_num_recommended) + 3;
+ input->buffer_size = MPMAX(input->buffer_size_min,
+ input->buffer_size_recommended);
+
+ if (!opaque) {
+ size_t size = layout_buffer(&(struct mp_image){0}, NULL, params);
+ if (input->buffer_size != size) {
+ MP_FATAL(vo, "We disagree with MMAL about buffer sizes.\n");
+ return -1;
+ }
+
+ p->swpool = mmal_pool_create(input->buffer_num, input->buffer_size);
+ if (!p->swpool) {
+ MP_FATAL(vo, "Could not allocate buffer pool.\n");
+ return -1;
+ }
+ }
+
+ if (set_geometry(vo) < 0)
+ return -1;
+
+ p->renderer_enabled = true;
+
+ if (mmal_port_enable(p->renderer->control, control_port_cb))
+ return -1;
+
+ if (mmal_port_enable(input, input_port_cb))
+ return -1;
+
+ if (mmal_component_enable(p->renderer)) {
+ MP_FATAL(vo, "Failed to enable video renderer.\n");
+ return -1;
+ }
+
+ resize(vo);
+
+ return 0;
+}
+
+static struct mp_image *take_screenshot(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!p->display)
+ return NULL;
+
+ struct mp_image *img = mp_image_alloc(IMGFMT_BGR0, p->w, p->h);
+ if (!img)
+ return NULL;
+
+ DISPMANX_RESOURCE_HANDLE_T resource =
+ vc_dispmanx_resource_create(VC_IMAGE_ARGB8888,
+ img->w | ((img->w * 4) << 16), img->h,
+ &(int32_t){0});
+ if (!resource)
+ goto fail;
+
+ if (vc_dispmanx_snapshot(p->display, resource, 0))
+ goto fail;
+
+ VC_RECT_T rc = {.width = img->w, .height = img->h};
+ if (vc_dispmanx_resource_read_data(resource, &rc, img->planes[0], img->stride[0]))
+ goto fail;
+
+ vc_dispmanx_resource_delete(resource);
+ return img;
+
+fail:
+ vc_dispmanx_resource_delete(resource);
+ talloc_free(img);
+ return NULL;
+}
+
+static void set_fullscreen(struct vo *vo) {
+ struct priv *p = vo->priv;
+
+ if (p->renderer_enabled)
+ set_geometry(vo);
+ vo->want_redraw = true;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(p->opts_cache, &opt)) {
+ struct mp_vo_opts *opts = p->opts_cache->opts;
+ if (&opts->fullscreen == opt)
+ set_fullscreen(vo);
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_PANSCAN:
+ if (p->renderer_enabled)
+ resize(vo);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_REDRAW_FRAME:
+ update_osd(vo);
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)data = take_screenshot(vo);
+ return VO_TRUE;
+ case VOCTRL_CHECK_EVENTS: {
+ mp_mutex_lock(&p->display_mutex);
+ bool reload_required = p->reload_display;
+ p->reload_display = false;
+ mp_mutex_unlock(&p->display_mutex);
+ if (reload_required)
+ recreate_renderer(vo);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_FPS:
+ *(double *)data = p->display_fps;
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_RES:
+ ((int *)data)[0] = p->w;
+ ((int *)data)[1] = p->h;
+ return VO_TRUE;
+ }
+
+ return VO_NOTIMPL;
+}
+
+static void tv_callback(void *callback_data, uint32_t reason, uint32_t param1,
+ uint32_t param2)
+{
+ struct vo *vo = callback_data;
+ struct priv *p = vo->priv;
+ mp_mutex_lock(&p->display_mutex);
+ p->reload_display = true;
+ mp_cond_signal(&p->display_cond);
+ mp_mutex_unlock(&p->display_mutex);
+ vo_wakeup(vo);
+}
+
+static void vsync_callback(DISPMANX_UPDATE_HANDLE_T u, void *arg)
+{
+ struct vo *vo = arg;
+ struct priv *p = vo->priv;
+ mp_mutex_lock(&p->display_mutex);
+ p->vsync_counter += 1;
+ mp_cond_signal(&p->display_cond);
+ mp_mutex_unlock(&p->display_mutex);
+}
+
+static void destroy_dispmanx(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ disable_renderer(vo);
+ destroy_overlays(vo);
+
+ if (p->update)
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = 0;
+
+ if (p->display) {
+ vc_dispmanx_vsync_callback(p->display, NULL, NULL);
+ vc_dispmanx_display_close(p->display);
+ }
+ p->display = 0;
+}
+
+static int recreate_dispmanx(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->display = vc_dispmanx_display_open(p->display_nr);
+ p->update = vc_dispmanx_update_start(0);
+ if (!p->display || !p->update) {
+ MP_FATAL(vo, "Could not get DISPMANX objects.\n");
+ if (p->display)
+ vc_dispmanx_display_close(p->display);
+ p->display = 0;
+ p->update = 0;
+ return -1;
+ }
+
+ update_display_size(vo);
+
+ vc_dispmanx_vsync_callback(p->display, vsync_callback, vo);
+
+ return 0;
+}
+
+static void recreate_renderer(struct vo *vo)
+{
+ MP_WARN(vo, "Recreating renderer after display change.\n");
+
+ destroy_dispmanx(vo);
+ recreate_dispmanx(vo);
+
+ if (vo->params) {
+ if (reconfig(vo, vo->params) < 0)
+ MP_FATAL(vo, "Recreation failed.\n");
+ }
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ vc_tv_unregister_callback_full(tv_callback, vo);
+
+ talloc_free(p->next_image);
+
+ destroy_dispmanx(vo);
+
+ if (p->renderer)
+ mmal_component_release(p->renderer);
+
+ mmal_vc_deinit();
+
+ mp_cond_destroy(&p->display_cond);
+ mp_mutex_destroy(&p->display_mutex);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->background_layer = p->layer;
+ p->video_layer = p->layer + 1;
+ p->osd_layer = p->layer + 2;
+
+ p->egl.log = vo->log;
+
+ bcm_host_init();
+
+ if (mmal_vc_init()) {
+ MP_FATAL(vo, "Could not initialize MMAL.\n");
+ return -1;
+ }
+
+ mp_mutex_init(&p->display_mutex);
+ mp_cond_init(&p->display_cond);
+
+ p->opts_cache = m_config_cache_alloc(p, vo->global, &vo_sub_opts);
+
+ if (recreate_dispmanx(vo) < 0)
+ goto fail;
+
+ if (update_display_size(vo) < 0)
+ goto fail;
+
+ if (mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &p->renderer))
+ {
+ MP_FATAL(vo, "Could not create MMAL renderer.\n");
+ goto fail;
+ }
+
+ vc_tv_register_callback(tv_callback, vo);
+
+ return 0;
+
+fail:
+ uninit(vo);
+ return -1;
+}
+
+#define OPT_BASE_STRUCT struct priv
+static const struct m_option options[] = {
+ {"display", OPT_INT(display_nr)},
+ {"layer", OPT_INT(layer), OPTDEF_INT(-10)},
+ {"background", OPT_BOOL(background)},
+ {"osd", OPT_BOOL(enable_osd), OPTDEF_INT(1)},
+ {0},
+};
+
+const struct vo_driver video_out_rpi = {
+ .description = "Raspberry Pi (MMAL)",
+ .name = "rpi",
+ .caps = VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .options = options,
+ .options_prefix = "rpi",
+};
diff --git a/video/out/vo_sdl.c b/video/out/vo_sdl.c
new file mode 100644
index 0000000..5f4c027
--- /dev/null
+++ b/video/out/vo_sdl.c
@@ -0,0 +1,992 @@
+/*
+ * video output driver for SDL 2.0+
+ *
+ * Copyright (C) 2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <assert.h>
+
+#include <SDL.h>
+
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#include "osdep/timer.h"
+
+#include "sub/osd.h"
+
+#include "video/mp_image.h"
+
+#include "win_state.h"
+#include "vo.h"
+
+struct formatmap_entry {
+ Uint32 sdl;
+ unsigned int mpv;
+ int is_rgba;
+};
+const struct formatmap_entry formats[] = {
+ {SDL_PIXELFORMAT_YV12, IMGFMT_420P, 0},
+ {SDL_PIXELFORMAT_IYUV, IMGFMT_420P, 0},
+ {SDL_PIXELFORMAT_UYVY, IMGFMT_UYVY, 0},
+ //{SDL_PIXELFORMAT_YVYU, IMGFMT_YVYU, 0},
+#if BYTE_ORDER == BIG_ENDIAN
+ {SDL_PIXELFORMAT_RGB888, IMGFMT_0RGB, 0}, // RGB888 means XRGB8888
+ {SDL_PIXELFORMAT_RGBX8888, IMGFMT_RGB0, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_BGR888, IMGFMT_0BGR, 0}, // BGR888 means XBGR8888
+ {SDL_PIXELFORMAT_BGRX8888, IMGFMT_BGR0, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_ARGB8888, IMGFMT_ARGB, 1}, // matches SUBBITMAP_BGRA
+ {SDL_PIXELFORMAT_RGBA8888, IMGFMT_RGBA, 1},
+ {SDL_PIXELFORMAT_ABGR8888, IMGFMT_ABGR, 1},
+ {SDL_PIXELFORMAT_BGRA8888, IMGFMT_BGRA, 1},
+#else
+ {SDL_PIXELFORMAT_RGB888, IMGFMT_BGR0, 0}, // RGB888 means XRGB8888
+ {SDL_PIXELFORMAT_RGBX8888, IMGFMT_0BGR, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_BGR888, IMGFMT_RGB0, 0}, // BGR888 means XBGR8888
+ {SDL_PIXELFORMAT_BGRX8888, IMGFMT_0RGB, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_ARGB8888, IMGFMT_BGRA, 1}, // matches SUBBITMAP_BGRA
+ {SDL_PIXELFORMAT_RGBA8888, IMGFMT_ABGR, 1},
+ {SDL_PIXELFORMAT_ABGR8888, IMGFMT_RGBA, 1},
+ {SDL_PIXELFORMAT_BGRA8888, IMGFMT_ARGB, 1},
+#endif
+ {SDL_PIXELFORMAT_RGB24, IMGFMT_RGB24, 0},
+ {SDL_PIXELFORMAT_BGR24, IMGFMT_BGR24, 0},
+ {SDL_PIXELFORMAT_RGB565, IMGFMT_RGB565, 0},
+};
+
+struct keymap_entry {
+ SDL_Keycode sdl;
+ int mpv;
+};
+const struct keymap_entry keys[] = {
+ {SDLK_RETURN, MP_KEY_ENTER},
+ {SDLK_ESCAPE, MP_KEY_ESC},
+ {SDLK_BACKSPACE, MP_KEY_BACKSPACE},
+ {SDLK_TAB, MP_KEY_TAB},
+ {SDLK_PRINTSCREEN, MP_KEY_PRINT},
+ {SDLK_PAUSE, MP_KEY_PAUSE},
+ {SDLK_INSERT, MP_KEY_INSERT},
+ {SDLK_HOME, MP_KEY_HOME},
+ {SDLK_PAGEUP, MP_KEY_PAGE_UP},
+ {SDLK_DELETE, MP_KEY_DELETE},
+ {SDLK_END, MP_KEY_END},
+ {SDLK_PAGEDOWN, MP_KEY_PAGE_DOWN},
+ {SDLK_RIGHT, MP_KEY_RIGHT},
+ {SDLK_LEFT, MP_KEY_LEFT},
+ {SDLK_DOWN, MP_KEY_DOWN},
+ {SDLK_UP, MP_KEY_UP},
+ {SDLK_KP_ENTER, MP_KEY_KPENTER},
+ {SDLK_KP_1, MP_KEY_KP1},
+ {SDLK_KP_2, MP_KEY_KP2},
+ {SDLK_KP_3, MP_KEY_KP3},
+ {SDLK_KP_4, MP_KEY_KP4},
+ {SDLK_KP_5, MP_KEY_KP5},
+ {SDLK_KP_6, MP_KEY_KP6},
+ {SDLK_KP_7, MP_KEY_KP7},
+ {SDLK_KP_8, MP_KEY_KP8},
+ {SDLK_KP_9, MP_KEY_KP9},
+ {SDLK_KP_0, MP_KEY_KP0},
+ {SDLK_KP_PERIOD, MP_KEY_KPDEC},
+ {SDLK_POWER, MP_KEY_POWER},
+ {SDLK_MENU, MP_KEY_MENU},
+ {SDLK_STOP, MP_KEY_STOP},
+ {SDLK_MUTE, MP_KEY_MUTE},
+ {SDLK_VOLUMEUP, MP_KEY_VOLUME_UP},
+ {SDLK_VOLUMEDOWN, MP_KEY_VOLUME_DOWN},
+ {SDLK_KP_COMMA, MP_KEY_KPDEC},
+ {SDLK_AUDIONEXT, MP_KEY_NEXT},
+ {SDLK_AUDIOPREV, MP_KEY_PREV},
+ {SDLK_AUDIOSTOP, MP_KEY_STOP},
+ {SDLK_AUDIOPLAY, MP_KEY_PLAY},
+ {SDLK_AUDIOMUTE, MP_KEY_MUTE},
+ {SDLK_F1, MP_KEY_F + 1},
+ {SDLK_F2, MP_KEY_F + 2},
+ {SDLK_F3, MP_KEY_F + 3},
+ {SDLK_F4, MP_KEY_F + 4},
+ {SDLK_F5, MP_KEY_F + 5},
+ {SDLK_F6, MP_KEY_F + 6},
+ {SDLK_F7, MP_KEY_F + 7},
+ {SDLK_F8, MP_KEY_F + 8},
+ {SDLK_F9, MP_KEY_F + 9},
+ {SDLK_F10, MP_KEY_F + 10},
+ {SDLK_F11, MP_KEY_F + 11},
+ {SDLK_F12, MP_KEY_F + 12},
+ {SDLK_F13, MP_KEY_F + 13},
+ {SDLK_F14, MP_KEY_F + 14},
+ {SDLK_F15, MP_KEY_F + 15},
+ {SDLK_F16, MP_KEY_F + 16},
+ {SDLK_F17, MP_KEY_F + 17},
+ {SDLK_F18, MP_KEY_F + 18},
+ {SDLK_F19, MP_KEY_F + 19},
+ {SDLK_F20, MP_KEY_F + 20},
+ {SDLK_F21, MP_KEY_F + 21},
+ {SDLK_F22, MP_KEY_F + 22},
+ {SDLK_F23, MP_KEY_F + 23},
+ {SDLK_F24, MP_KEY_F + 24}
+};
+
+struct mousemap_entry {
+ Uint8 sdl;
+ int mpv;
+};
+const struct mousemap_entry mousebtns[] = {
+ {SDL_BUTTON_LEFT, MP_MBTN_LEFT},
+ {SDL_BUTTON_MIDDLE, MP_MBTN_MID},
+ {SDL_BUTTON_RIGHT, MP_MBTN_RIGHT},
+ {SDL_BUTTON_X1, MP_MBTN_BACK},
+ {SDL_BUTTON_X2, MP_MBTN_FORWARD},
+};
+
+struct priv {
+ SDL_Window *window;
+ SDL_Renderer *renderer;
+ int renderer_index;
+ SDL_RendererInfo renderer_info;
+ SDL_Texture *tex;
+ int tex_swapped;
+ struct mp_image_params params;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ struct mp_osd_res osd_res;
+ struct formatmap_entry osd_format;
+ struct osd_bitmap_surface {
+ int change_id;
+ struct osd_target {
+ SDL_Rect source;
+ SDL_Rect dest;
+ SDL_Texture *tex;
+ SDL_Texture *tex2;
+ } *targets;
+ int num_targets;
+ int targets_size;
+ } osd_surfaces[MAX_OSD_PARTS];
+ double osd_pts;
+ Uint32 wakeup_event;
+ bool screensaver_enabled;
+ struct m_config_cache *opts_cache;
+
+ // options
+ bool allow_sw;
+ bool switch_mode;
+ bool vsync;
+};
+
+static bool lock_texture(struct vo *vo, struct mp_image *texmpi)
+{
+ struct priv *vc = vo->priv;
+ *texmpi = (struct mp_image){0};
+ mp_image_set_size(texmpi, vc->params.w, vc->params.h);
+ mp_image_setfmt(texmpi, vc->params.imgfmt);
+ switch (texmpi->num_planes) {
+ case 1:
+ case 3:
+ break;
+ default:
+ MP_ERR(vo, "Invalid plane count\n");
+ return false;
+ }
+ void *pixels;
+ int pitch;
+ if (SDL_LockTexture(vc->tex, NULL, &pixels, &pitch)) {
+ MP_ERR(vo, "SDL_LockTexture failed\n");
+ return false;
+ }
+ texmpi->planes[0] = pixels;
+ texmpi->stride[0] = pitch;
+ if (texmpi->num_planes == 3) {
+ if (vc->tex_swapped) {
+ texmpi->planes[2] =
+ ((Uint8 *) texmpi->planes[0] + texmpi->h * pitch);
+ texmpi->stride[2] = pitch / 2;
+ texmpi->planes[1] =
+ ((Uint8 *) texmpi->planes[2] + (texmpi->h * pitch) / 4);
+ texmpi->stride[1] = pitch / 2;
+ } else {
+ texmpi->planes[1] =
+ ((Uint8 *) texmpi->planes[0] + texmpi->h * pitch);
+ texmpi->stride[1] = pitch / 2;
+ texmpi->planes[2] =
+ ((Uint8 *) texmpi->planes[1] + (texmpi->h * pitch) / 4);
+ texmpi->stride[2] = pitch / 2;
+ }
+ }
+ return true;
+}
+
+static bool is_good_renderer(SDL_RendererInfo *ri,
+ const char *driver_name_wanted, bool allow_sw,
+ struct formatmap_entry *osd_format)
+{
+ if (driver_name_wanted && driver_name_wanted[0])
+ if (strcmp(driver_name_wanted, ri->name))
+ return false;
+
+ if (!allow_sw &&
+ !(ri->flags & SDL_RENDERER_ACCELERATED))
+ return false;
+
+ int i, j;
+ for (i = 0; i < ri->num_texture_formats; ++i)
+ for (j = 0; j < sizeof(formats) / sizeof(formats[0]); ++j)
+ if (ri->texture_formats[i] == formats[j].sdl)
+ if (formats[j].is_rgba) {
+ if (osd_format)
+ *osd_format = formats[j];
+ return true;
+ }
+
+ return false;
+}
+
+static void destroy_renderer(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ // free ALL the textures
+ if (vc->tex) {
+ SDL_DestroyTexture(vc->tex);
+ vc->tex = NULL;
+ }
+
+ int i, j;
+ for (i = 0; i < MAX_OSD_PARTS; ++i) {
+ for (j = 0; j < vc->osd_surfaces[i].targets_size; ++j) {
+ if (vc->osd_surfaces[i].targets[j].tex) {
+ SDL_DestroyTexture(vc->osd_surfaces[i].targets[j].tex);
+ vc->osd_surfaces[i].targets[j].tex = NULL;
+ }
+ if (vc->osd_surfaces[i].targets[j].tex2) {
+ SDL_DestroyTexture(vc->osd_surfaces[i].targets[j].tex2);
+ vc->osd_surfaces[i].targets[j].tex2 = NULL;
+ }
+ }
+ }
+
+ if (vc->renderer) {
+ SDL_DestroyRenderer(vc->renderer);
+ vc->renderer = NULL;
+ }
+}
+
+static bool try_create_renderer(struct vo *vo, int i, const char *driver)
+{
+ struct priv *vc = vo->priv;
+
+ // first probe
+ SDL_RendererInfo ri;
+ if (SDL_GetRenderDriverInfo(i, &ri))
+ return false;
+ if (!is_good_renderer(&ri, driver, vc->allow_sw, NULL))
+ return false;
+
+ vc->renderer = SDL_CreateRenderer(vc->window, i, 0);
+ if (!vc->renderer) {
+ MP_ERR(vo, "SDL_CreateRenderer failed\n");
+ return false;
+ }
+
+ if (SDL_GetRendererInfo(vc->renderer, &vc->renderer_info)) {
+ MP_ERR(vo, "SDL_GetRendererInfo failed\n");
+ destroy_renderer(vo);
+ return false;
+ }
+
+ if (!is_good_renderer(&vc->renderer_info, NULL, vc->allow_sw,
+ &vc->osd_format)) {
+ MP_ERR(vo, "Renderer '%s' does not fulfill "
+ "requirements on this system\n",
+ vc->renderer_info.name);
+ destroy_renderer(vo);
+ return false;
+ }
+
+ if (vc->renderer_index != i) {
+ MP_INFO(vo, "Using %s\n", vc->renderer_info.name);
+ vc->renderer_index = i;
+ }
+
+ return true;
+}
+
+static int init_renderer(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ int n = SDL_GetNumRenderDrivers();
+ int i;
+
+ if (vc->renderer_index >= 0)
+ if (try_create_renderer(vo, vc->renderer_index, NULL))
+ return 0;
+
+ for (i = 0; i < n; ++i)
+ if (try_create_renderer(vo, i, SDL_GetHint(SDL_HINT_RENDER_DRIVER)))
+ return 0;
+
+ for (i = 0; i < n; ++i)
+ if (try_create_renderer(vo, i, NULL))
+ return 0;
+
+ MP_ERR(vo, "No supported renderer\n");
+ return -1;
+}
+
+static void resize(struct vo *vo, int w, int h)
+{
+ struct priv *vc = vo->priv;
+ vo->dwidth = w;
+ vo->dheight = h;
+ vo_get_src_dst_rects(vo, &vc->src_rect, &vc->dst_rect,
+ &vc->osd_res);
+ SDL_RenderSetLogicalSize(vc->renderer, w, h);
+ vo->want_redraw = true;
+ vo_wakeup(vo);
+}
+
+static void force_resize(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ int w, h;
+ SDL_GetWindowSize(vc->window, &w, &h);
+ resize(vo, w, h);
+}
+
+static void check_resize(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ int w, h;
+ SDL_GetWindowSize(vc->window, &w, &h);
+ if (vo->dwidth != w || vo->dheight != h)
+ resize(vo, w, h);
+}
+
+static inline void set_screensaver(bool enabled)
+{
+ if (!!enabled == !!SDL_IsScreenSaverEnabled())
+ return;
+
+ if (enabled)
+ SDL_EnableScreenSaver();
+ else
+ SDL_DisableScreenSaver();
+}
+
+static void set_fullscreen(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ struct mp_vo_opts *opts = vc->opts_cache->opts;
+ int fs = opts->fullscreen;
+ SDL_bool prev_screensaver_state = SDL_IsScreenSaverEnabled();
+
+ Uint32 fs_flag;
+ if (vc->switch_mode)
+ fs_flag = SDL_WINDOW_FULLSCREEN;
+ else
+ fs_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
+
+ Uint32 old_flags = SDL_GetWindowFlags(vc->window);
+ int prev_fs = !!(old_flags & fs_flag);
+ if (fs == prev_fs)
+ return;
+
+ Uint32 flags = 0;
+ if (fs)
+ flags |= fs_flag;
+
+ if (SDL_SetWindowFullscreen(vc->window, flags)) {
+ MP_ERR(vo, "SDL_SetWindowFullscreen failed\n");
+ return;
+ }
+
+ // toggling fullscreen might recreate the window, so better guard for this
+ set_screensaver(prev_screensaver_state);
+
+ force_resize(vo);
+}
+
+static void update_screeninfo(struct vo *vo, struct mp_rect *screenrc)
+{
+ struct priv *vc = vo->priv;
+ SDL_DisplayMode mode;
+ if (SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(vc->window),
+ &mode)) {
+ MP_ERR(vo, "SDL_GetCurrentDisplayMode failed\n");
+ return;
+ }
+ *screenrc = (struct mp_rect){0, 0, mode.w, mode.h};
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *vc = vo->priv;
+
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc;
+
+ update_screeninfo(vo, &screenrc);
+ vo_calc_window_geometry(vo, &screenrc, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ int win_w = vo->dwidth;
+ int win_h = vo->dheight;
+
+ SDL_SetWindowSize(vc->window, win_w, win_h);
+ if (geo.flags & VO_WIN_FORCE_POS)
+ SDL_SetWindowPosition(vc->window, geo.win.x0, geo.win.y0);
+
+ if (vc->tex)
+ SDL_DestroyTexture(vc->tex);
+ Uint32 texfmt = SDL_PIXELFORMAT_UNKNOWN;
+ int i, j;
+ for (i = 0; i < vc->renderer_info.num_texture_formats; ++i)
+ for (j = 0; j < sizeof(formats) / sizeof(formats[0]); ++j)
+ if (vc->renderer_info.texture_formats[i] == formats[j].sdl)
+ if (params->imgfmt == formats[j].mpv)
+ texfmt = formats[j].sdl;
+ if (texfmt == SDL_PIXELFORMAT_UNKNOWN) {
+ MP_ERR(vo, "Invalid pixel format\n");
+ return -1;
+ }
+
+ vc->tex_swapped = texfmt == SDL_PIXELFORMAT_YV12;
+ vc->tex = SDL_CreateTexture(vc->renderer, texfmt,
+ SDL_TEXTUREACCESS_STREAMING,
+ params->w, params->h);
+ if (!vc->tex) {
+ MP_ERR(vo, "Could not create a texture\n");
+ return -1;
+ }
+
+ vc->params = *params;
+
+ struct mp_image tmp;
+ if (!lock_texture(vo, &tmp)) {
+ SDL_DestroyTexture(vc->tex);
+ vc->tex = NULL;
+ return -1;
+ }
+ mp_image_clear(&tmp, 0, 0, tmp.w, tmp.h);
+ SDL_UnlockTexture(vc->tex);
+
+ resize(vo, win_w, win_h);
+
+ set_screensaver(vc->screensaver_enabled);
+ set_fullscreen(vo);
+
+ SDL_ShowWindow(vc->window);
+
+ check_resize(vo);
+
+ return 0;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ SDL_RenderPresent(vc->renderer);
+}
+
+static void wakeup(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ SDL_Event event = {.type = vc->wakeup_event};
+ // Note that there is no context - SDL is a singleton.
+ SDL_PushEvent(&event);
+}
+
+static void wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ // Round-up to 1ms for short timeouts (100us, 1000us]
+ if (wait_ns > MP_TIME_US_TO_NS(100))
+ wait_ns = MPMAX(wait_ns, MP_TIME_MS_TO_NS(1));
+ int timeout_ms = MPCLAMP(wait_ns / MP_TIME_MS_TO_NS(1), 0, 10000);
+ SDL_Event ev;
+
+ while (SDL_WaitEventTimeout(&ev, timeout_ms)) {
+ timeout_ms = 0;
+ switch (ev.type) {
+ case SDL_WINDOWEVENT:
+ switch (ev.window.event) {
+ case SDL_WINDOWEVENT_EXPOSED:
+ vo->want_redraw = true;
+ break;
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ check_resize(vo);
+ vo_event(vo, VO_EVENT_RESIZE);
+ break;
+ case SDL_WINDOWEVENT_ENTER:
+ mp_input_put_key(vo->input_ctx, MP_KEY_MOUSE_ENTER);
+ break;
+ case SDL_WINDOWEVENT_LEAVE:
+ mp_input_put_key(vo->input_ctx, MP_KEY_MOUSE_LEAVE);
+ break;
+ }
+ break;
+ case SDL_QUIT:
+ mp_input_put_key(vo->input_ctx, MP_KEY_CLOSE_WIN);
+ break;
+ case SDL_TEXTINPUT: {
+ int sdl_mod = SDL_GetModState();
+ int mpv_mod = 0;
+ // we ignore KMOD_LSHIFT, KMOD_RSHIFT and KMOD_RALT (if
+ // mp_input_use_alt_gr() is true) because these are already
+ // factored into ev.text.text
+ if (sdl_mod & (KMOD_LCTRL | KMOD_RCTRL))
+ mpv_mod |= MP_KEY_MODIFIER_CTRL;
+ if ((sdl_mod & KMOD_LALT) ||
+ ((sdl_mod & KMOD_RALT) && !mp_input_use_alt_gr(vo->input_ctx)))
+ mpv_mod |= MP_KEY_MODIFIER_ALT;
+ if (sdl_mod & (KMOD_LGUI | KMOD_RGUI))
+ mpv_mod |= MP_KEY_MODIFIER_META;
+ struct bstr t = {
+ ev.text.text, strlen(ev.text.text)
+ };
+ mp_input_put_key_utf8(vo->input_ctx, mpv_mod, t);
+ break;
+ }
+ case SDL_KEYDOWN: {
+ // Issue: we don't know in advance whether this keydown event
+ // will ALSO cause a SDL_TEXTINPUT event
+ // So we're conservative, and only map non printable keycodes
+ // (e.g. function keys, arrow keys, etc.)
+ // However, this does lose some keypresses at least on X11
+ // (e.g. Ctrl-A generates SDL_KEYDOWN only, but the key is
+ // 'a'... and 'a' is normally also handled by SDL_TEXTINPUT).
+ // The default config does not use Ctrl, so this is fine...
+ int keycode = 0;
+ int i;
+ for (i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i)
+ if (keys[i].sdl == ev.key.keysym.sym) {
+ keycode = keys[i].mpv;
+ break;
+ }
+ if (keycode) {
+ if (ev.key.keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT))
+ keycode |= MP_KEY_MODIFIER_SHIFT;
+ if (ev.key.keysym.mod & (KMOD_LCTRL | KMOD_RCTRL))
+ keycode |= MP_KEY_MODIFIER_CTRL;
+ if (ev.key.keysym.mod & (KMOD_LALT | KMOD_RALT))
+ keycode |= MP_KEY_MODIFIER_ALT;
+ if (ev.key.keysym.mod & (KMOD_LGUI | KMOD_RGUI))
+ keycode |= MP_KEY_MODIFIER_META;
+ mp_input_put_key(vo->input_ctx, keycode);
+ }
+ break;
+ }
+ case SDL_MOUSEMOTION:
+ mp_input_set_mouse_pos(vo->input_ctx, ev.motion.x, ev.motion.y);
+ break;
+ case SDL_MOUSEBUTTONDOWN: {
+ int i;
+ for (i = 0; i < sizeof(mousebtns) / sizeof(mousebtns[0]); ++i)
+ if (mousebtns[i].sdl == ev.button.button) {
+ mp_input_put_key(vo->input_ctx, mousebtns[i].mpv | MP_KEY_STATE_DOWN);
+ break;
+ }
+ break;
+ }
+ case SDL_MOUSEBUTTONUP: {
+ int i;
+ for (i = 0; i < sizeof(mousebtns) / sizeof(mousebtns[0]); ++i)
+ if (mousebtns[i].sdl == ev.button.button) {
+ mp_input_put_key(vo->input_ctx, mousebtns[i].mpv | MP_KEY_STATE_UP);
+ break;
+ }
+ break;
+ }
+ case SDL_MOUSEWHEEL: {
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+ double multiplier = ev.wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1;
+#else
+ double multiplier = 1;
+#endif
+ int y_code = ev.wheel.y > 0 ? MP_WHEEL_UP : MP_WHEEL_DOWN;
+ mp_input_put_wheel(vo->input_ctx, y_code, abs(ev.wheel.y) * multiplier);
+ int x_code = ev.wheel.x > 0 ? MP_WHEEL_RIGHT : MP_WHEEL_LEFT;
+ mp_input_put_wheel(vo->input_ctx, x_code, abs(ev.wheel.x) * multiplier);
+ break;
+ }
+ }
+ }
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ destroy_renderer(vo);
+ SDL_DestroyWindow(vc->window);
+ vc->window = NULL;
+ SDL_QuitSubSystem(SDL_INIT_VIDEO);
+ talloc_free(vc);
+}
+
+static inline void upload_to_texture(struct vo *vo, SDL_Texture *tex,
+ int w, int h, void *bitmap, int stride)
+{
+ struct priv *vc = vo->priv;
+
+ if (vc->osd_format.sdl == SDL_PIXELFORMAT_ARGB8888) {
+ // NOTE: this optimization is questionable, because SDL docs say
+ // that this way is slow.
+ // It did measure up faster, though...
+ SDL_UpdateTexture(tex, NULL, bitmap, stride);
+ return;
+ }
+
+ void *pixels;
+ int pitch;
+ if (SDL_LockTexture(tex, NULL, &pixels, &pitch)) {
+ MP_ERR(vo, "Could not lock texture\n");
+ } else {
+ SDL_ConvertPixels(w, h, SDL_PIXELFORMAT_ARGB8888,
+ bitmap, stride,
+ vc->osd_format.sdl,
+ pixels, pitch);
+ SDL_UnlockTexture(tex);
+ }
+}
+
+static inline void subbitmap_to_texture(struct vo *vo, SDL_Texture *tex,
+ struct sub_bitmap *bmp,
+ uint32_t ormask)
+{
+ if (ormask == 0) {
+ upload_to_texture(vo, tex, bmp->w, bmp->h,
+ bmp->bitmap, bmp->stride);
+ } else {
+ uint32_t *temppixels;
+ temppixels = talloc_array(vo, uint32_t, bmp->w * bmp->h);
+
+ int x, y;
+ for (y = 0; y < bmp->h; ++y) {
+ const uint32_t *src =
+ (const uint32_t *) ((const char *) bmp->bitmap + y * bmp->stride);
+ uint32_t *dst = temppixels + y * bmp->w;
+ for (x = 0; x < bmp->w; ++x)
+ dst[x] = src[x] | ormask;
+ }
+
+ upload_to_texture(vo, tex, bmp->w, bmp->h,
+ temppixels, sizeof(uint32_t) * bmp->w);
+
+ talloc_free(temppixels);
+ }
+}
+
+static void generate_osd_part(struct vo *vo, struct sub_bitmaps *imgs)
+{
+ struct priv *vc = vo->priv;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[imgs->render_index];
+
+ if (imgs->format == SUBBITMAP_EMPTY || imgs->num_parts == 0)
+ return;
+
+ if (imgs->change_id == sfc->change_id)
+ return;
+
+ if (imgs->num_parts > sfc->targets_size) {
+ sfc->targets = talloc_realloc(vc, sfc->targets,
+ struct osd_target, imgs->num_parts);
+ memset(&sfc->targets[sfc->targets_size], 0, sizeof(struct osd_target) *
+ (imgs->num_parts - sfc->targets_size));
+ sfc->targets_size = imgs->num_parts;
+ }
+ sfc->num_targets = imgs->num_parts;
+
+ for (int i = 0; i < imgs->num_parts; i++) {
+ struct osd_target *target = sfc->targets + i;
+ struct sub_bitmap *bmp = imgs->parts + i;
+
+ target->source = (SDL_Rect){
+ 0, 0, bmp->w, bmp->h
+ };
+ target->dest = (SDL_Rect){
+ bmp->x, bmp->y, bmp->dw, bmp->dh
+ };
+
+ // tex: alpha blended texture
+ if (target->tex) {
+ SDL_DestroyTexture(target->tex);
+ target->tex = NULL;
+ }
+ if (!target->tex)
+ target->tex = SDL_CreateTexture(vc->renderer,
+ vc->osd_format.sdl, SDL_TEXTUREACCESS_STREAMING,
+ bmp->w, bmp->h);
+ if (!target->tex) {
+ MP_ERR(vo, "Could not create texture\n");
+ }
+ if (target->tex) {
+ SDL_SetTextureBlendMode(target->tex,
+ SDL_BLENDMODE_BLEND);
+ SDL_SetTextureColorMod(target->tex, 0, 0, 0);
+ subbitmap_to_texture(vo, target->tex, bmp, 0); // RGBA -> 000A
+ }
+
+ // tex2: added texture
+ if (target->tex2) {
+ SDL_DestroyTexture(target->tex2);
+ target->tex2 = NULL;
+ }
+ if (!target->tex2)
+ target->tex2 = SDL_CreateTexture(vc->renderer,
+ vc->osd_format.sdl, SDL_TEXTUREACCESS_STREAMING,
+ bmp->w, bmp->h);
+ if (!target->tex2) {
+ MP_ERR(vo, "Could not create texture\n");
+ }
+ if (target->tex2) {
+ SDL_SetTextureBlendMode(target->tex2,
+ SDL_BLENDMODE_ADD);
+ subbitmap_to_texture(vo, target->tex2, bmp,
+ 0xFF000000); // RGBA -> RGB1
+ }
+ }
+
+ sfc->change_id = imgs->change_id;
+}
+
+static void draw_osd_part(struct vo *vo, int index)
+{
+ struct priv *vc = vo->priv;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[index];
+ int i;
+
+ for (i = 0; i < sfc->num_targets; i++) {
+ struct osd_target *target = sfc->targets + i;
+ if (target->tex)
+ SDL_RenderCopy(vc->renderer, target->tex,
+ &target->source, &target->dest);
+ if (target->tex2)
+ SDL_RenderCopy(vc->renderer, target->tex2,
+ &target->source, &target->dest);
+ }
+}
+
+static void draw_osd_cb(void *ctx, struct sub_bitmaps *imgs)
+{
+ struct vo *vo = ctx;
+ generate_osd_part(vo, imgs);
+ draw_osd_part(vo, imgs->render_index);
+}
+
+static void draw_osd(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ static const bool osdformats[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_BGRA] = true,
+ };
+
+ osd_draw(vo->osd, vc->osd_res, vc->osd_pts, 0, osdformats, draw_osd_cb, vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ if (SDL_WasInit(SDL_INIT_EVENTS)) {
+ MP_ERR(vo, "Another component is using SDL already.\n");
+ return -1;
+ }
+
+ vc->opts_cache = m_config_cache_alloc(vc, vo->global, &vo_sub_opts);
+
+ // predefine SDL defaults (SDL env vars shall override)
+ SDL_SetHintWithPriority(SDL_HINT_RENDER_SCALE_QUALITY, "1",
+ SDL_HINT_DEFAULT);
+ SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0",
+ SDL_HINT_DEFAULT);
+
+ // predefine MPV options (SDL env vars shall be overridden)
+ SDL_SetHintWithPriority(SDL_HINT_RENDER_VSYNC, vc->vsync ? "1" : "0",
+ SDL_HINT_OVERRIDE);
+
+ if (SDL_InitSubSystem(SDL_INIT_VIDEO)) {
+ MP_ERR(vo, "SDL_Init failed\n");
+ return -1;
+ }
+
+ // then actually try
+ vc->window = SDL_CreateWindow("MPV", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+ 640, 480, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN);
+ if (!vc->window) {
+ MP_ERR(vo, "SDL_CreateWindow failed\n");
+ return -1;
+ }
+
+ // try creating a renderer (this also gets the renderer_info data
+ // for query_format to use!)
+ if (init_renderer(vo) != 0) {
+ SDL_DestroyWindow(vc->window);
+ vc->window = NULL;
+ return -1;
+ }
+
+ vc->wakeup_event = SDL_RegisterEvents(1);
+ if (vc->wakeup_event == (Uint32)-1)
+ MP_ERR(vo, "SDL_RegisterEvents() failed.\n");
+
+ MP_WARN(vo, "Warning: this legacy VO has bad performance. Consider fixing "
+ "your graphics drivers, or not forcing the sdl VO.\n");
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *vc = vo->priv;
+ int i, j;
+ for (i = 0; i < vc->renderer_info.num_texture_formats; ++i)
+ for (j = 0; j < sizeof(formats) / sizeof(formats[0]); ++j)
+ if (vc->renderer_info.texture_formats[i] == formats[j].sdl)
+ if (format == formats[j].mpv)
+ return 1;
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *vc = vo->priv;
+
+ // typically this runs in parallel with the following mp_image_copy call
+ SDL_SetRenderDrawColor(vc->renderer, 0, 0, 0, 255);
+ SDL_RenderClear(vc->renderer);
+
+ SDL_SetTextureBlendMode(vc->tex, SDL_BLENDMODE_NONE);
+
+ if (frame->current) {
+ vc->osd_pts = frame->current->pts;
+
+ mp_image_t texmpi;
+ if (!lock_texture(vo, &texmpi))
+ return;
+
+ mp_image_copy(&texmpi, frame->current);
+
+ SDL_UnlockTexture(vc->tex);
+ }
+
+ SDL_Rect src, dst;
+ src.x = vc->src_rect.x0;
+ src.y = vc->src_rect.y0;
+ src.w = vc->src_rect.x1 - vc->src_rect.x0;
+ src.h = vc->src_rect.y1 - vc->src_rect.y0;
+ dst.x = vc->dst_rect.x0;
+ dst.y = vc->dst_rect.y0;
+ dst.w = vc->dst_rect.x1 - vc->dst_rect.x0;
+ dst.h = vc->dst_rect.y1 - vc->dst_rect.y0;
+
+ SDL_RenderCopy(vc->renderer, vc->tex, &src, &dst);
+
+ draw_osd(vo);
+}
+
+static struct mp_image *get_window_screenshot(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ struct mp_image *image = mp_image_alloc(vc->osd_format.mpv, vo->dwidth,
+ vo->dheight);
+ if (!image)
+ return NULL;
+ if (SDL_RenderReadPixels(vc->renderer, NULL, vc->osd_format.sdl,
+ image->planes[0], image->stride[0])) {
+ MP_ERR(vo, "SDL_RenderReadPixels failed\n");
+ talloc_free(image);
+ return NULL;
+ }
+ return image;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *vc = vo->priv;
+
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(vc->opts_cache, &opt)) {
+ struct mp_vo_opts *opts = vc->opts_cache->opts;
+ if (&opts->fullscreen == opt)
+ set_fullscreen(vo);
+ }
+ return 1;
+ }
+ case VOCTRL_SET_PANSCAN:
+ force_resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)data = get_window_screenshot(vo);
+ return true;
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ SDL_ShowCursor(*(bool *)data);
+ return true;
+ case VOCTRL_KILL_SCREENSAVER:
+ vc->screensaver_enabled = false;
+ set_screensaver(vc->screensaver_enabled);
+ return VO_TRUE;
+ case VOCTRL_RESTORE_SCREENSAVER:
+ vc->screensaver_enabled = true;
+ set_screensaver(vc->screensaver_enabled);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ SDL_SetWindowTitle(vc->window, (char *)data);
+ return true;
+ }
+ return VO_NOTIMPL;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_sdl = {
+ .description = "SDL 2.0 Renderer",
+ .name = "sdl",
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .renderer_index = -1,
+ .vsync = true,
+ },
+ .options = (const struct m_option []){
+ {"sw", OPT_BOOL(allow_sw)},
+ {"switch-mode", OPT_BOOL(switch_mode)},
+ {"vsync", OPT_BOOL(vsync)},
+ {NULL}
+ },
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .uninit = uninit,
+ .flip_page = flip_page,
+ .wait_events = wait_events,
+ .wakeup = wakeup,
+ .options_prefix = "sdl",
+};
diff --git a/video/out/vo_sixel.c b/video/out/vo_sixel.c
new file mode 100644
index 0000000..e05c455
--- /dev/null
+++ b/video/out/vo_sixel.c
@@ -0,0 +1,627 @@
+/*
+ * Sixel mpv output device implementation based on ffmpeg libavdevice implementation
+ * by Hayaki Saito
+ * https://github.com/saitoha/FFmpeg-SIXEL/blob/sixel/libavdevice/sixel.c
+ *
+ * Copyright (c) 2014 Hayaki Saito
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libswscale/swscale.h>
+#include <sixel.h>
+
+#include "config.h"
+#include "options/m_config.h"
+#include "osdep/terminal.h"
+#include "sub/osd.h"
+#include "vo.h"
+#include "video/sws_utils.h"
+#include "video/mp_image.h"
+
+#if HAVE_POSIX
+#include <unistd.h>
+#endif
+
+#define IMGFMT IMGFMT_RGB24
+
+#define TERM_ESC_USE_GLOBAL_COLOR_REG "\033[?1070l"
+
+#define TERMINAL_FALLBACK_COLS 80
+#define TERMINAL_FALLBACK_ROWS 25
+#define TERMINAL_FALLBACK_PX_WIDTH 320
+#define TERMINAL_FALLBACK_PX_HEIGHT 240
+
+struct vo_sixel_opts {
+ int diffuse;
+ int reqcolors;
+ bool fixedpal;
+ int threshold;
+ int width, height, top, left;
+ int pad_y, pad_x;
+ int rows, cols;
+ bool config_clear, alt_screen;
+ bool buffered;
+};
+
+struct priv {
+ // User specified options
+ struct vo_sixel_opts opts;
+
+ // Internal data
+ sixel_output_t *output;
+ sixel_dither_t *dither;
+ sixel_dither_t *testdither;
+ uint8_t *buffer;
+ char *sixel_output_buf;
+ bool skip_frame_draw;
+
+ int left, top; // image origin cell (1 based)
+ int width, height; // actual image px size - always reflects dst_rect.
+ int num_cols, num_rows; // terminal size in cells
+ int canvas_ok; // whether canvas vo->dwidth and vo->dheight are positive
+
+ int previous_histogram_colors;
+
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ struct mp_osd_res osd;
+ struct mp_image *frame;
+ struct mp_sws_context *sws;
+};
+
+static const unsigned int depth = 3;
+
+static int detect_scene_change(struct vo* vo)
+{
+ struct priv* priv = vo->priv;
+ int previous_histogram_colors = priv->previous_histogram_colors;
+ int histogram_colors = 0;
+
+ // If threshold is set negative, then every frame must be a scene change
+ if (priv->dither == NULL || priv->opts.threshold < 0)
+ return 1;
+
+ histogram_colors = sixel_dither_get_num_of_histogram_colors(priv->testdither);
+
+ int color_difference_count = previous_histogram_colors - histogram_colors;
+ color_difference_count = (color_difference_count > 0) ? // abs value
+ color_difference_count : -color_difference_count;
+
+ if (100 * color_difference_count >
+ priv->opts.threshold * previous_histogram_colors)
+ {
+ priv->previous_histogram_colors = histogram_colors; // update history
+ return 1;
+ } else {
+ return 0;
+ }
+
+}
+
+static void dealloc_dithers_and_buffers(struct vo* vo)
+{
+ struct priv* priv = vo->priv;
+
+ if (priv->buffer) {
+ talloc_free(priv->buffer);
+ priv->buffer = NULL;
+ }
+
+ if (priv->frame) {
+ talloc_free(priv->frame);
+ priv->frame = NULL;
+ }
+
+ if (priv->dither) {
+ sixel_dither_unref(priv->dither);
+ priv->dither = NULL;
+ }
+
+ if (priv->testdither) {
+ sixel_dither_unref(priv->testdither);
+ priv->testdither = NULL;
+ }
+}
+
+static SIXELSTATUS prepare_static_palette(struct vo* vo)
+{
+ struct priv* priv = vo->priv;
+
+ if (!priv->dither) {
+ priv->dither = sixel_dither_get(BUILTIN_XTERM256);
+ if (priv->dither == NULL)
+ return SIXEL_FALSE;
+
+ sixel_dither_set_diffusion_type(priv->dither, priv->opts.diffuse);
+ }
+
+ sixel_dither_set_body_only(priv->dither, 0);
+ return SIXEL_OK;
+}
+
+static SIXELSTATUS prepare_dynamic_palette(struct vo *vo)
+{
+ SIXELSTATUS status = SIXEL_FALSE;
+ struct priv *priv = vo->priv;
+
+ /* create histogram and construct color palette
+ * with median cut algorithm. */
+ status = sixel_dither_initialize(priv->testdither, priv->buffer,
+ priv->width, priv->height,
+ SIXEL_PIXELFORMAT_RGB888,
+ LARGE_NORM, REP_CENTER_BOX,
+ QUALITY_LOW);
+ if (SIXEL_FAILED(status))
+ return status;
+
+ if (detect_scene_change(vo)) {
+ if (priv->dither) {
+ sixel_dither_unref(priv->dither);
+ priv->dither = NULL;
+ }
+
+ priv->dither = priv->testdither;
+ status = sixel_dither_new(&priv->testdither, priv->opts.reqcolors, NULL);
+
+ if (SIXEL_FAILED(status))
+ return status;
+
+ sixel_dither_set_diffusion_type(priv->dither, priv->opts.diffuse);
+ } else {
+ if (priv->dither == NULL)
+ return SIXEL_FALSE;
+ }
+
+ sixel_dither_set_body_only(priv->dither, 0);
+ return status;
+}
+
+static void update_canvas_dimensions(struct vo *vo)
+{
+ // this function sets the vo canvas size in pixels vo->dwidth, vo->dheight,
+ // and the number of rows and columns available in priv->num_rows/cols
+ struct priv *priv = vo->priv;
+ int num_rows = TERMINAL_FALLBACK_ROWS;
+ int num_cols = TERMINAL_FALLBACK_COLS;
+ int total_px_width = 0;
+ int total_px_height = 0;
+
+ terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height);
+
+ // If the user has specified rows/cols use them for further calculations
+ num_rows = (priv->opts.rows > 0) ? priv->opts.rows : num_rows;
+ num_cols = (priv->opts.cols > 0) ? priv->opts.cols : num_cols;
+
+ // If the pad value is set in between 0 and width/2 - 1, then we
+ // subtract from the detected width. Otherwise, we assume that the width
+ // output must be a integer multiple of num_cols and accordingly set
+ // total_width to be an integer multiple of num_cols. So in case the padding
+ // added by terminal is less than the number of cells in that axis, then rounding
+ // down will take care of correcting the detected width and remove padding.
+ if (priv->opts.width > 0) {
+ // option - set by the user, hard truth
+ total_px_width = priv->opts.width;
+ } else {
+ if (total_px_width <= 0) {
+ // ioctl failed to read terminal width
+ total_px_width = TERMINAL_FALLBACK_PX_WIDTH;
+ } else {
+ if (priv->opts.pad_x >= 0 && priv->opts.pad_x < total_px_width / 2) {
+ // explicit padding set by the user
+ total_px_width -= (2 * priv->opts.pad_x);
+ } else {
+ // rounded "auto padding"
+ total_px_width = total_px_width / num_cols * num_cols;
+ }
+ }
+ }
+
+ if (priv->opts.height > 0) {
+ total_px_height = priv->opts.height;
+ } else {
+ if (total_px_height <= 0) {
+ total_px_height = TERMINAL_FALLBACK_PX_HEIGHT;
+ } else {
+ if (priv->opts.pad_y >= 0 && priv->opts.pad_y < total_px_height / 2) {
+ total_px_height -= (2 * priv->opts.pad_y);
+ } else {
+ total_px_height = total_px_height / num_rows * num_rows;
+ }
+ }
+ }
+
+ // use n-1 rows for height
+ // The last row can't be used for encoding image, because after sixel encode
+ // the terminal moves the cursor to next line below the image, causing the
+ // last line to be empty instead of displaying image data.
+ // TODO: Confirm if the output height must be a multiple of 6, if not, remove
+ // the / 6 * 6 part which is setting the height to be a multiple of 6.
+ vo->dheight = total_px_height * (num_rows - 1) / num_rows / 6 * 6;
+ vo->dwidth = total_px_width;
+
+ priv->num_rows = num_rows;
+ priv->num_cols = num_cols;
+
+ priv->canvas_ok = vo->dwidth > 0 && vo->dheight > 0;
+}
+
+static void set_sixel_output_parameters(struct vo *vo)
+{
+ // This function sets output scaled size in priv->width, priv->height
+ // and the scaling rectangles in pixels priv->src_rect, priv->dst_rect
+ // as well as image positioning in cells priv->top, priv->left.
+ struct priv *priv = vo->priv;
+
+ vo_get_src_dst_rects(vo, &priv->src_rect, &priv->dst_rect, &priv->osd);
+
+ // priv->width and priv->height are the width and height of dst_rect
+ // and they are not changed anywhere else outside this function.
+ // It is the sixel image output dimension which is output by libsixel.
+ priv->width = priv->dst_rect.x1 - priv->dst_rect.x0;
+ priv->height = priv->dst_rect.y1 - priv->dst_rect.y0;
+
+ // top/left values must be greater than 1. If it is set, then
+ // the image will be rendered from there and no further centering is done.
+ priv->top = (priv->opts.top > 0) ? priv->opts.top :
+ priv->num_rows * priv->dst_rect.y0 / vo->dheight + 1;
+ priv->left = (priv->opts.left > 0) ? priv->opts.left :
+ priv->num_cols * priv->dst_rect.x0 / vo->dwidth + 1;
+}
+
+static int update_sixel_swscaler(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *priv = vo->priv;
+
+ priv->sws->src = *params;
+ priv->sws->src.w = mp_rect_w(priv->src_rect);
+ priv->sws->src.h = mp_rect_h(priv->src_rect);
+ priv->sws->dst = (struct mp_image_params) {
+ .imgfmt = IMGFMT,
+ .w = priv->width,
+ .h = priv->height,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ dealloc_dithers_and_buffers(vo);
+
+ priv->frame = mp_image_alloc(IMGFMT, priv->width, priv->height);
+ if (!priv->frame)
+ return -1;
+
+ if (mp_sws_reinit(priv->sws) < 0)
+ return -1;
+
+ // create testdither only if dynamic palette mode is set
+ if (!priv->opts.fixedpal) {
+ SIXELSTATUS status = sixel_dither_new(&priv->testdither,
+ priv->opts.reqcolors, NULL);
+ if (SIXEL_FAILED(status)) {
+ MP_ERR(vo, "update_sixel_swscaler: Failed to create new dither: %s\n",
+ sixel_helper_format_error(status));
+ return -1;
+ }
+ }
+
+ priv->buffer =
+ talloc_array(NULL, uint8_t, depth * priv->width * priv->height);
+
+ return 0;
+}
+
+static inline int sixel_buffer(char *data, int size, void *priv) {
+ char **out = (char **)priv;
+ *out = talloc_strndup_append_buffer(*out, data, size);
+ return size;
+}
+
+static inline int sixel_write(char *data, int size, void *priv)
+{
+ FILE *p = (FILE *)priv;
+ // On POSIX platforms, write() is the fastest method. It also is the only
+ // one that allows atomic writes so mpv’s output will not be interrupted
+ // by other processes or threads that write to stdout, which would cause
+ // screen corruption. POSIX does not guarantee atomicity for writes
+ // exceeding PIPE_BUF, but at least Linux does seem to implement it that
+ // way.
+#if HAVE_POSIX
+ int remain = size;
+
+ while (remain > 0) {
+ ssize_t written = write(fileno(p), data, remain);
+ if (written < 0)
+ return written;
+ remain -= written;
+ data += written;
+ }
+
+ return size;
+#else
+ int ret = fwrite(data, 1, size, p);
+ fflush(p);
+ return ret;
+#endif
+}
+
+static inline void sixel_strwrite(char *s)
+{
+ sixel_write(s, strlen(s), stdout);
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *priv = vo->priv;
+ int ret = 0;
+ update_canvas_dimensions(vo);
+ if (priv->canvas_ok) { // if too small - succeed but skip the rendering
+ set_sixel_output_parameters(vo);
+ ret = update_sixel_swscaler(vo, params);
+ }
+
+ if (priv->opts.config_clear)
+ sixel_strwrite(TERM_ESC_CLEAR_SCREEN);
+ vo->want_redraw = true;
+
+ return ret;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *priv = vo->priv;
+ SIXELSTATUS status;
+ struct mp_image *mpi = NULL;
+
+ int prev_rows = priv->num_rows;
+ int prev_cols = priv->num_cols;
+ int prev_height = vo->dheight;
+ int prev_width = vo->dwidth;
+ bool resized = false;
+ update_canvas_dimensions(vo);
+ if (!priv->canvas_ok)
+ return;
+
+ if (prev_rows != priv->num_rows || prev_cols != priv->num_cols ||
+ prev_width != vo->dwidth || prev_height != vo->dheight)
+ {
+ set_sixel_output_parameters(vo);
+ // Not checking for vo->config_ok because draw_frame is never called
+ // with a failed reconfig.
+ update_sixel_swscaler(vo, vo->params);
+
+ if (priv->opts.config_clear)
+ sixel_strwrite(TERM_ESC_CLEAR_SCREEN);
+ resized = true;
+ }
+
+ if (frame->repeat && !frame->redraw && !resized) {
+ // Frame is repeated, and no need to update OSD either
+ priv->skip_frame_draw = true;
+ return;
+ } else {
+ // Either frame is new, or OSD has to be redrawn
+ priv->skip_frame_draw = false;
+ }
+
+ // Normal case where we have to draw the frame and the image is not NULL
+ if (frame->current) {
+ mpi = mp_image_new_ref(frame->current);
+ struct mp_rect src_rc = priv->src_rect;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
+ mp_image_crop_rc(mpi, src_rc);
+
+ // scale/pan to our dest rect
+ mp_sws_scale(priv->sws, priv->frame, mpi);
+ } else {
+ // Image is NULL, so need to clear image and draw OSD
+ mp_image_clear(priv->frame, 0, 0, priv->width, priv->height);
+ }
+
+ struct mp_osd_res dim = {
+ .w = priv->width,
+ .h = priv->height
+ };
+ osd_draw_on_image(vo->osd, dim, mpi ? mpi->pts : 0, 0, priv->frame);
+
+ // Copy from mpv to RGB format as required by libsixel
+ memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth,
+ priv->height, priv->width * depth, priv->frame->stride[0]);
+
+ // Even if either of these prepare palette functions fail, on re-running them
+ // they should try to re-initialize the dithers, so it shouldn't dereference
+ // any NULL pointers. flip_page also has a check to make sure dither is not
+ // NULL before drawing, so failure in these functions should still be okay.
+ if (priv->opts.fixedpal) {
+ status = prepare_static_palette(vo);
+ } else {
+ status = prepare_dynamic_palette(vo);
+ }
+
+ if (SIXEL_FAILED(status)) {
+ MP_WARN(vo, "draw_frame: prepare_palette returned error: %s\n",
+ sixel_helper_format_error(status));
+ }
+
+ if (mpi)
+ talloc_free(mpi);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv* priv = vo->priv;
+ if (!priv->canvas_ok)
+ return;
+
+ // If frame is repeated and no update required, then we skip encoding
+ if (priv->skip_frame_draw)
+ return;
+
+ // Make sure that image and dither are valid before drawing
+ if (priv->buffer == NULL || priv->dither == NULL)
+ return;
+
+ // Go to the offset row and column, then display the image
+ priv->sixel_output_buf = talloc_asprintf(NULL, TERM_ESC_GOTO_YX,
+ priv->top, priv->left);
+ if (!priv->opts.buffered)
+ sixel_strwrite(priv->sixel_output_buf);
+
+ sixel_encode(priv->buffer, priv->width, priv->height,
+ depth, priv->dither, priv->output);
+
+ if (priv->opts.buffered)
+ sixel_write(priv->sixel_output_buf,
+ ta_get_size(priv->sixel_output_buf), stdout);
+
+ talloc_free(priv->sixel_output_buf);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ SIXELSTATUS status = SIXEL_FALSE;
+
+ // Parse opts set by CLI or conf
+ priv->sws = mp_sws_alloc(vo);
+ priv->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(priv->sws, vo->global);
+
+ if (priv->opts.buffered)
+ status = sixel_output_new(&priv->output, sixel_buffer,
+ &priv->sixel_output_buf, NULL);
+ else
+ status = sixel_output_new(&priv->output, sixel_write, stdout, NULL);
+ if (SIXEL_FAILED(status)) {
+ MP_ERR(vo, "preinit: Failed to create output file: %s\n",
+ sixel_helper_format_error(status));
+ return -1;
+ }
+
+ sixel_output_set_encode_policy(priv->output, SIXEL_ENCODEPOLICY_FAST);
+
+ if (priv->opts.alt_screen)
+ sixel_strwrite(TERM_ESC_ALT_SCREEN);
+
+ sixel_strwrite(TERM_ESC_HIDE_CURSOR);
+
+ /* don't use private color registers for each frame. */
+ sixel_strwrite(TERM_ESC_USE_GLOBAL_COLOR_REG);
+
+ priv->dither = NULL;
+
+ // create testdither only if dynamic palette mode is set
+ if (!priv->opts.fixedpal) {
+ status = sixel_dither_new(&priv->testdither, priv->opts.reqcolors, NULL);
+ if (SIXEL_FAILED(status)) {
+ MP_ERR(vo, "preinit: Failed to create new dither: %s\n",
+ sixel_helper_format_error(status));
+ return -1;
+ }
+ }
+
+ priv->previous_histogram_colors = 0;
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ if (request == VOCTRL_SET_PANSCAN)
+ return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE;
+ return VO_NOTIMPL;
+}
+
+
+static void uninit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+
+ sixel_strwrite(TERM_ESC_RESTORE_CURSOR);
+
+ if (priv->opts.alt_screen)
+ sixel_strwrite(TERM_ESC_NORMAL_SCREEN);
+ fflush(stdout);
+
+ if (priv->output) {
+ sixel_output_unref(priv->output);
+ priv->output = NULL;
+ }
+
+ dealloc_dithers_and_buffers(vo);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_sixel = {
+ .name = "sixel",
+ .description = "terminal graphics using sixels",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .opts.diffuse = DIFFUSE_AUTO,
+ .opts.reqcolors = 256,
+ .opts.threshold = -1,
+ .opts.fixedpal = true,
+ .opts.pad_y = -1,
+ .opts.pad_x = -1,
+ .opts.config_clear = true,
+ .opts.alt_screen = true,
+ },
+ .options = (const m_option_t[]) {
+ {"dither", OPT_CHOICE(opts.diffuse,
+ {"auto", DIFFUSE_AUTO},
+ {"none", DIFFUSE_NONE},
+ {"atkinson", DIFFUSE_ATKINSON},
+ {"fs", DIFFUSE_FS},
+ {"jajuni", DIFFUSE_JAJUNI},
+ {"stucki", DIFFUSE_STUCKI},
+ {"burkes", DIFFUSE_BURKES},
+ {"arithmetic", DIFFUSE_A_DITHER},
+ {"xor", DIFFUSE_X_DITHER})},
+ {"width", OPT_INT(opts.width)},
+ {"height", OPT_INT(opts.height)},
+ {"reqcolors", OPT_INT(opts.reqcolors)},
+ {"fixedpalette", OPT_BOOL(opts.fixedpal)},
+ {"threshold", OPT_INT(opts.threshold)},
+ {"top", OPT_INT(opts.top)},
+ {"left", OPT_INT(opts.left)},
+ {"pad-y", OPT_INT(opts.pad_y)},
+ {"pad-x", OPT_INT(opts.pad_x)},
+ {"rows", OPT_INT(opts.rows)},
+ {"cols", OPT_INT(opts.cols)},
+ {"config-clear", OPT_BOOL(opts.config_clear), },
+ {"alt-screen", OPT_BOOL(opts.alt_screen), },
+ {"buffered", OPT_BOOL(opts.buffered), },
+ {"exit-clear", OPT_REPLACED("vo-sixel-alt-screen")},
+ {0}
+ },
+ .options_prefix = "vo-sixel",
+};
diff --git a/video/out/vo_tct.c b/video/out/vo_tct.c
new file mode 100644
index 0000000..8859095
--- /dev/null
+++ b/video/out/vo_tct.c
@@ -0,0 +1,347 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <config.h>
+
+#if HAVE_POSIX
+#include <sys/ioctl.h>
+#endif
+
+#include <libswscale/swscale.h>
+
+#include "options/m_config.h"
+#include "config.h"
+#include "osdep/terminal.h"
+#include "osdep/io.h"
+#include "vo.h"
+#include "sub/osd.h"
+#include "video/sws_utils.h"
+#include "video/mp_image.h"
+
+#define IMGFMT IMGFMT_BGR24
+
+#define ALGO_PLAIN 1
+#define ALGO_HALF_BLOCKS 2
+
+#define TERM_ESC_CLEAR_COLORS "\033[0m"
+#define TERM_ESC_COLOR256_BG "\033[48;5"
+#define TERM_ESC_COLOR256_FG "\033[38;5"
+#define TERM_ESC_COLOR24BIT_BG "\033[48;2"
+#define TERM_ESC_COLOR24BIT_FG "\033[38;2"
+
+#define DEFAULT_WIDTH 80
+#define DEFAULT_HEIGHT 25
+
+struct vo_tct_opts {
+ int algo;
+ int width; // 0 -> default
+ int height; // 0 -> default
+ bool term256; // 0 -> true color
+};
+
+struct lut_item {
+ char str[4];
+ int width;
+};
+
+struct priv {
+ struct vo_tct_opts opts;
+ size_t buffer_size;
+ int swidth;
+ int sheight;
+ struct mp_image *frame;
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_sws_context *sws;
+ struct lut_item lut[256];
+};
+
+// Convert RGB24 to xterm-256 8-bit value
+// For simplicity, assume RGB space is perceptually uniform.
+// There are 5 places where one of two outputs needs to be chosen when the
+// input is the exact middle:
+// - The r/g/b channels and the gray value: the higher value output is chosen.
+// - If the gray and color have same distance from the input - color is chosen.
+static int rgb_to_x256(uint8_t r, uint8_t g, uint8_t b)
+{
+ // Calculate the nearest 0-based color index at 16 .. 231
+# define v2ci(v) (v < 48 ? 0 : v < 115 ? 1 : (v - 35) / 40)
+ int ir = v2ci(r), ig = v2ci(g), ib = v2ci(b); // 0..5 each
+# define color_index() (36 * ir + 6 * ig + ib) /* 0..215, lazy evaluation */
+
+ // Calculate the nearest 0-based gray index at 232 .. 255
+ int average = (r + g + b) / 3;
+ int gray_index = average > 238 ? 23 : (average - 3) / 10; // 0..23
+
+ // Calculate the represented colors back from the index
+ static const int i2cv[6] = {0, 0x5f, 0x87, 0xaf, 0xd7, 0xff};
+ int cr = i2cv[ir], cg = i2cv[ig], cb = i2cv[ib]; // r/g/b, 0..255 each
+ int gv = 8 + 10 * gray_index; // same value for r/g/b, 0..255
+
+ // Return the one which is nearer to the original input rgb value
+# define dist_square(A,B,C, a,b,c) ((A-a)*(A-a) + (B-b)*(B-b) + (C-c)*(C-c))
+ int color_err = dist_square(cr, cg, cb, r, g, b);
+ int gray_err = dist_square(gv, gv, gv, r, g, b);
+ return color_err <= gray_err ? 16 + color_index() : 232 + gray_index;
+}
+
+static void print_seq3(struct lut_item *lut, const char* prefix,
+ uint8_t r, uint8_t g, uint8_t b)
+{
+// The fwrite implementation is about 25% faster than the printf code
+// (even if we use *.s with the lut values), however,
+// on windows we need to use printf in order to translate escape sequences and
+// UTF8 output for the console.
+#ifndef _WIN32
+ fputs(prefix, stdout);
+ fwrite(lut[r].str, lut[r].width, 1, stdout);
+ fwrite(lut[g].str, lut[g].width, 1, stdout);
+ fwrite(lut[b].str, lut[b].width, 1, stdout);
+ fputc('m', stdout);
+#else
+ printf("%s;%d;%d;%dm", prefix, (int)r, (int)g, (int)b);
+#endif
+}
+
+static void print_seq1(struct lut_item *lut, const char* prefix, uint8_t c)
+{
+#ifndef _WIN32
+ fputs(prefix, stdout);
+ fwrite(lut[c].str, lut[c].width, 1, stdout);
+ fputc('m', stdout);
+#else
+ printf("%s;%dm", prefix, (int)c);
+#endif
+}
+
+
+static void write_plain(
+ const int dwidth, const int dheight,
+ const int swidth, const int sheight,
+ const unsigned char *source, const int source_stride,
+ bool term256, struct lut_item *lut)
+{
+ assert(source);
+ const int tx = (dwidth - swidth) / 2;
+ const int ty = (dheight - sheight) / 2;
+ for (int y = 0; y < sheight; y++) {
+ const unsigned char *row = source + y * source_stride;
+ printf(TERM_ESC_GOTO_YX, ty + y, tx);
+ for (int x = 0; x < swidth; x++) {
+ unsigned char b = *row++;
+ unsigned char g = *row++;
+ unsigned char r = *row++;
+ if (term256) {
+ print_seq1(lut, TERM_ESC_COLOR256_BG, rgb_to_x256(r, g, b));
+ } else {
+ print_seq3(lut, TERM_ESC_COLOR24BIT_BG, r, g, b);
+ }
+ printf(" ");
+ }
+ printf(TERM_ESC_CLEAR_COLORS);
+ }
+ printf("\n");
+}
+
+static void write_half_blocks(
+ const int dwidth, const int dheight,
+ const int swidth, const int sheight,
+ unsigned char *source, int source_stride,
+ bool term256, struct lut_item *lut)
+{
+ assert(source);
+ const int tx = (dwidth - swidth) / 2;
+ const int ty = (dheight - sheight) / 2;
+ for (int y = 0; y < sheight * 2; y += 2) {
+ const unsigned char *row_up = source + y * source_stride;
+ const unsigned char *row_down = source + (y + 1) * source_stride;
+ printf(TERM_ESC_GOTO_YX, ty + y / 2, tx);
+ for (int x = 0; x < swidth; x++) {
+ unsigned char b_up = *row_up++;
+ unsigned char g_up = *row_up++;
+ unsigned char r_up = *row_up++;
+ unsigned char b_down = *row_down++;
+ unsigned char g_down = *row_down++;
+ unsigned char r_down = *row_down++;
+ if (term256) {
+ print_seq1(lut, TERM_ESC_COLOR256_BG, rgb_to_x256(r_up, g_up, b_up));
+ print_seq1(lut, TERM_ESC_COLOR256_FG, rgb_to_x256(r_down, g_down, b_down));
+ } else {
+ print_seq3(lut, TERM_ESC_COLOR24BIT_BG, r_up, g_up, b_up);
+ print_seq3(lut, TERM_ESC_COLOR24BIT_FG, r_down, g_down, b_down);
+ }
+ printf("\xe2\x96\x84"); // UTF8 bytes of U+2584 (lower half block)
+ }
+ printf(TERM_ESC_CLEAR_COLORS);
+ }
+ printf("\n");
+}
+
+static void get_win_size(struct vo *vo, int *out_width, int *out_height) {
+ struct priv *p = vo->priv;
+ *out_width = DEFAULT_WIDTH;
+ *out_height = DEFAULT_HEIGHT;
+
+ terminal_get_size(out_width, out_height);
+
+ if (p->opts.width > 0)
+ *out_width = p->opts.width;
+ if (p->opts.height > 0)
+ *out_height = p->opts.height;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ get_win_size(vo, &vo->dwidth, &vo->dheight);
+
+ struct mp_osd_res osd;
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &osd);
+ p->swidth = p->dst.x1 - p->dst.x0;
+ p->sheight = p->dst.y1 - p->dst.y0;
+
+ p->sws->src = *params;
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = IMGFMT,
+ .w = p->swidth,
+ .h = p->sheight,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ const int mul = (p->opts.algo == ALGO_PLAIN ? 1 : 2);
+ if (p->frame)
+ talloc_free(p->frame);
+ p->frame = mp_image_alloc(IMGFMT, p->swidth, p->sheight * mul);
+ if (!p->frame)
+ return -1;
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return -1;
+
+ printf(TERM_ESC_CLEAR_SCREEN);
+
+ vo->want_redraw = true;
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct mp_image *src = frame->current;
+ if (!src)
+ return;
+ // XXX: pan, crop etc.
+ mp_sws_scale(p->sws, p->frame, src);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ int width, height;
+ get_win_size(vo, &width, &height);
+
+ if (vo->dwidth != width || vo->dheight != height)
+ reconfig(vo, vo->params);
+
+ if (p->opts.algo == ALGO_PLAIN) {
+ write_plain(
+ vo->dwidth, vo->dheight, p->swidth, p->sheight,
+ p->frame->planes[0], p->frame->stride[0],
+ p->opts.term256, p->lut);
+ } else {
+ write_half_blocks(
+ vo->dwidth, vo->dheight, p->swidth, p->sheight,
+ p->frame->planes[0], p->frame->stride[0],
+ p->opts.term256, p->lut);
+ }
+ fflush(stdout);
+}
+
+static void uninit(struct vo *vo)
+{
+ printf(TERM_ESC_RESTORE_CURSOR);
+ printf(TERM_ESC_NORMAL_SCREEN);
+ struct priv *p = vo->priv;
+ if (p->frame)
+ talloc_free(p->frame);
+}
+
+static int preinit(struct vo *vo)
+{
+ // most terminal characters aren't 1:1, so we default to 2:1.
+ // if user passes their own value of choice, it'll be scaled accordingly.
+ vo->monitor_par = vo->opts->monitor_pixel_aspect * 2;
+
+ struct priv *p = vo->priv;
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+ for (int i = 0; i < 256; ++i) {
+ char buff[8];
+ p->lut[i].width = snprintf(buff, sizeof(buff), ";%d", i);
+ memcpy(p->lut[i].str, buff, 4); // some strings may not end on a null byte, but that's ok.
+ }
+
+ printf(TERM_ESC_HIDE_CURSOR);
+ printf(TERM_ESC_ALT_SCREEN);
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_tct = {
+ .name = "tct",
+ .description = "true-color terminals",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .opts.algo = ALGO_HALF_BLOCKS,
+ },
+ .options = (const m_option_t[]) {
+ {"algo", OPT_CHOICE(opts.algo,
+ {"plain", ALGO_PLAIN},
+ {"half-blocks", ALGO_HALF_BLOCKS})},
+ {"width", OPT_INT(opts.width)},
+ {"height", OPT_INT(opts.height)},
+ {"256", OPT_BOOL(opts.term256)},
+ {0}
+ },
+ .options_prefix = "vo-tct",
+};
diff --git a/video/out/vo_vaapi.c b/video/out/vo_vaapi.c
new file mode 100644
index 0000000..12888fe
--- /dev/null
+++ b/video/out/vo_vaapi.c
@@ -0,0 +1,877 @@
+/*
+ * VA API output module
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ * Gwenole Beauchesne <gbeauchesne@splitted-desktop.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdarg.h>
+#include <limits.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <va/va_x11.h>
+
+#include "common/msg.h"
+#include "video/out/vo.h"
+#include "video/mp_image_pool.h"
+#include "video/sws_utils.h"
+#include "sub/draw_bmp.h"
+#include "sub/img_convert.h"
+#include "sub/osd.h"
+#include "present_sync.h"
+#include "x11_common.h"
+
+#include "video/mp_image.h"
+#include "video/vaapi.h"
+#include "video/hwdec.h"
+
+struct vaapi_osd_image {
+ int w, h;
+ VAImage image;
+ VASubpictureID subpic_id;
+ bool is_used;
+};
+
+struct vaapi_subpic {
+ VASubpictureID id;
+ int src_x, src_y, src_w, src_h;
+ int dst_x, dst_y, dst_w, dst_h;
+};
+
+struct vaapi_osd_part {
+ bool active;
+ int change_id;
+ struct vaapi_osd_image image;
+ struct vaapi_subpic subpic;
+};
+
+#define MAX_OUTPUT_SURFACES 2
+
+struct priv {
+ struct mp_log *log;
+ struct vo *vo;
+ VADisplay display;
+ struct mp_vaapi_ctx *mpvaapi;
+
+ struct mp_image_params image_params;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ struct mp_osd_res screen_osd_res;
+
+ struct mp_image *output_surfaces[MAX_OUTPUT_SURFACES];
+ struct mp_image *swdec_surfaces[MAX_OUTPUT_SURFACES];
+
+ int output_surface;
+ int visible_surface;
+ int scaling;
+ bool force_scaled_osd;
+
+ VAImageFormat osd_format; // corresponds to OSD_VA_FORMAT
+ struct vaapi_osd_part osd_part;
+ bool osd_screen;
+ struct mp_draw_sub_cache *osd_cache;
+
+ struct mp_image_pool *pool;
+
+ struct mp_image *black_surface;
+
+ VAImageFormat *va_subpic_formats;
+ unsigned int *va_subpic_flags;
+ int va_num_subpic_formats;
+ VADisplayAttribute *va_display_attrs;
+ int *mp_display_attr;
+ int va_num_display_attrs;
+
+ struct va_image_formats *image_formats;
+};
+
+#define OSD_VA_FORMAT VA_FOURCC_BGRA
+
+static void draw_osd(struct vo *vo);
+
+
+struct fmtentry {
+ uint32_t va;
+ enum mp_imgfmt mp;
+};
+
+static const struct fmtentry va_to_imgfmt[] = {
+ {VA_FOURCC_NV12, IMGFMT_NV12},
+ {VA_FOURCC_YV12, IMGFMT_420P},
+ {VA_FOURCC_IYUV, IMGFMT_420P},
+ {VA_FOURCC_UYVY, IMGFMT_UYVY},
+ // Note: not sure about endian issues (the mp formats are byte-addressed)
+ {VA_FOURCC_RGBA, IMGFMT_RGBA},
+ {VA_FOURCC_RGBX, IMGFMT_RGBA},
+ {VA_FOURCC_BGRA, IMGFMT_BGRA},
+ {VA_FOURCC_BGRX, IMGFMT_BGRA},
+ {0 , IMGFMT_NONE}
+};
+
+static enum mp_imgfmt va_fourcc_to_imgfmt(uint32_t fourcc)
+{
+ for (const struct fmtentry *entry = va_to_imgfmt; entry->va; ++entry) {
+ if (entry->va == fourcc)
+ return entry->mp;
+ }
+ return IMGFMT_NONE;
+}
+
+static uint32_t va_fourcc_from_imgfmt(int imgfmt)
+{
+ for (const struct fmtentry *entry = va_to_imgfmt; entry->va; ++entry) {
+ if (entry->mp == imgfmt)
+ return entry->va;
+ }
+ return 0;
+}
+
+struct va_image_formats {
+ VAImageFormat *entries;
+ int num;
+};
+
+static void va_get_formats(struct priv *ctx)
+{
+ struct va_image_formats *formats = talloc_ptrtype(ctx, formats);
+ formats->num = vaMaxNumImageFormats(ctx->display);
+ formats->entries = talloc_array(formats, VAImageFormat, formats->num);
+ VAStatus status = vaQueryImageFormats(ctx->display, formats->entries,
+ &formats->num);
+ if (!CHECK_VA_STATUS(ctx, "vaQueryImageFormats()"))
+ return;
+ MP_VERBOSE(ctx, "%d image formats available:\n", formats->num);
+ for (int i = 0; i < formats->num; i++)
+ MP_VERBOSE(ctx, " %s\n", mp_tag_str(formats->entries[i].fourcc));
+ ctx->image_formats = formats;
+}
+
+static VAImageFormat *va_image_format_from_imgfmt(struct priv *ctx,
+ int imgfmt)
+{
+ struct va_image_formats *formats = ctx->image_formats;
+ const int fourcc = va_fourcc_from_imgfmt(imgfmt);
+ if (!formats || !formats->num || !fourcc)
+ return NULL;
+ for (int i = 0; i < formats->num; i++) {
+ if (formats->entries[i].fourcc == fourcc)
+ return &formats->entries[i];
+ }
+ return NULL;
+}
+
+struct va_surface {
+ struct mp_vaapi_ctx *ctx;
+ VADisplay display;
+
+ VASurfaceID id;
+ int rt_format;
+
+ // The actually allocated surface size (needed for cropping).
+ // mp_images can have a smaller size than this, which means they are
+ // cropped down to a smaller size by removing right/bottom pixels.
+ int w, h;
+
+ VAImage image; // used for software decoding case
+ bool is_derived; // is image derived by vaDeriveImage()?
+};
+
+static struct va_surface *va_surface_in_mp_image(struct mp_image *mpi)
+{
+ return mpi && mpi->imgfmt == IMGFMT_VAAPI ?
+ (struct va_surface*)mpi->planes[0] : NULL;
+}
+
+static void release_va_surface(void *arg)
+{
+ struct va_surface *surface = arg;
+
+ if (surface->id != VA_INVALID_ID) {
+ if (surface->image.image_id != VA_INVALID_ID)
+ vaDestroyImage(surface->display, surface->image.image_id);
+ vaDestroySurfaces(surface->display, &surface->id, 1);
+ }
+
+ talloc_free(surface);
+}
+
+static struct mp_image *alloc_surface(struct mp_vaapi_ctx *ctx, int rt_format,
+ int w, int h)
+{
+ VASurfaceID id = VA_INVALID_ID;
+ VAStatus status;
+ status = vaCreateSurfaces(ctx->display, rt_format, w, h, &id, 1, NULL, 0);
+ if (!CHECK_VA_STATUS(ctx, "vaCreateSurfaces()"))
+ return NULL;
+
+ struct va_surface *surface = talloc_ptrtype(NULL, surface);
+ if (!surface)
+ return NULL;
+
+ *surface = (struct va_surface){
+ .ctx = ctx,
+ .id = id,
+ .rt_format = rt_format,
+ .w = w,
+ .h = h,
+ .display = ctx->display,
+ .image = { .image_id = VA_INVALID_ID, .buf = VA_INVALID_ID },
+ };
+
+ struct mp_image img = {0};
+ mp_image_setfmt(&img, IMGFMT_VAAPI);
+ mp_image_set_size(&img, w, h);
+ img.planes[0] = (uint8_t*)surface;
+ img.planes[3] = (uint8_t*)(uintptr_t)surface->id;
+ return mp_image_new_custom_ref(&img, surface, release_va_surface);
+}
+
+static void va_surface_image_destroy(struct va_surface *surface)
+{
+ if (!surface || surface->image.image_id == VA_INVALID_ID)
+ return;
+ vaDestroyImage(surface->display, surface->image.image_id);
+ surface->image.image_id = VA_INVALID_ID;
+ surface->is_derived = false;
+}
+
+static int va_surface_image_alloc(struct va_surface *p, VAImageFormat *format)
+{
+ VADisplay *display = p->display;
+
+ if (p->image.image_id != VA_INVALID_ID &&
+ p->image.format.fourcc == format->fourcc)
+ return 0;
+
+ int r = 0;
+
+ va_surface_image_destroy(p);
+
+ VAStatus status = vaDeriveImage(display, p->id, &p->image);
+ if (status == VA_STATUS_SUCCESS) {
+ /* vaDeriveImage() is supported, check format */
+ if (p->image.format.fourcc == format->fourcc &&
+ p->image.width == p->w && p->image.height == p->h)
+ {
+ p->is_derived = true;
+ MP_TRACE(p->ctx, "Using vaDeriveImage()\n");
+ } else {
+ vaDestroyImage(p->display, p->image.image_id);
+ status = VA_STATUS_ERROR_OPERATION_FAILED;
+ }
+ }
+ if (status != VA_STATUS_SUCCESS) {
+ p->image.image_id = VA_INVALID_ID;
+ status = vaCreateImage(p->display, format, p->w, p->h, &p->image);
+ if (!CHECK_VA_STATUS(p->ctx, "vaCreateImage()")) {
+ p->image.image_id = VA_INVALID_ID;
+ r = -1;
+ }
+ }
+
+ return r;
+}
+
+// img must be a VAAPI surface; make sure its internal VAImage is allocated
+// to a format corresponding to imgfmt (or return an error).
+static int va_surface_alloc_imgfmt(struct priv *priv, struct mp_image *img,
+ int imgfmt)
+{
+ struct va_surface *p = va_surface_in_mp_image(img);
+ if (!p)
+ return -1;
+ // Multiple FourCCs can refer to the same imgfmt, so check by doing the
+ // surjective conversion first.
+ if (p->image.image_id != VA_INVALID_ID &&
+ va_fourcc_to_imgfmt(p->image.format.fourcc) == imgfmt)
+ return 0;
+ VAImageFormat *format = va_image_format_from_imgfmt(priv, imgfmt);
+ if (!format)
+ return -1;
+ if (va_surface_image_alloc(p, format) < 0)
+ return -1;
+ return 0;
+}
+
+static bool va_image_map(struct mp_vaapi_ctx *ctx, VAImage *image,
+ struct mp_image *mpi)
+{
+ int imgfmt = va_fourcc_to_imgfmt(image->format.fourcc);
+ if (imgfmt == IMGFMT_NONE)
+ return false;
+ void *data = NULL;
+ const VAStatus status = vaMapBuffer(ctx->display, image->buf, &data);
+ if (!CHECK_VA_STATUS(ctx, "vaMapBuffer()"))
+ return false;
+
+ *mpi = (struct mp_image) {0};
+ mp_image_setfmt(mpi, imgfmt);
+ mp_image_set_size(mpi, image->width, image->height);
+
+ for (int p = 0; p < image->num_planes; p++) {
+ mpi->stride[p] = image->pitches[p];
+ mpi->planes[p] = (uint8_t *)data + image->offsets[p];
+ }
+
+ if (image->format.fourcc == VA_FOURCC_YV12) {
+ MPSWAP(int, mpi->stride[1], mpi->stride[2]);
+ MPSWAP(uint8_t *, mpi->planes[1], mpi->planes[2]);
+ }
+
+ return true;
+}
+
+static bool va_image_unmap(struct mp_vaapi_ctx *ctx, VAImage *image)
+{
+ const VAStatus status = vaUnmapBuffer(ctx->display, image->buf);
+ return CHECK_VA_STATUS(ctx, "vaUnmapBuffer()");
+}
+
+// va_dst: copy destination, must be IMGFMT_VAAPI
+// sw_src: copy source, must be a software pixel format
+static int va_surface_upload(struct priv *priv, struct mp_image *va_dst,
+ struct mp_image *sw_src)
+{
+ struct va_surface *p = va_surface_in_mp_image(va_dst);
+ if (!p)
+ return -1;
+
+ if (va_surface_alloc_imgfmt(priv, va_dst, sw_src->imgfmt) < 0)
+ return -1;
+
+ struct mp_image img;
+ if (!va_image_map(p->ctx, &p->image, &img))
+ return -1;
+ assert(sw_src->w <= img.w && sw_src->h <= img.h);
+ mp_image_set_size(&img, sw_src->w, sw_src->h); // copy only visible part
+ mp_image_copy(&img, sw_src);
+ va_image_unmap(p->ctx, &p->image);
+
+ if (!p->is_derived) {
+ VAStatus status = vaPutImage(p->display, p->id,
+ p->image.image_id,
+ 0, 0, sw_src->w, sw_src->h,
+ 0, 0, sw_src->w, sw_src->h);
+ if (!CHECK_VA_STATUS(p->ctx, "vaPutImage()"))
+ return -1;
+ }
+
+ if (p->is_derived)
+ va_surface_image_destroy(p);
+ return 0;
+}
+
+struct pool_alloc_ctx {
+ struct mp_vaapi_ctx *vaapi;
+ int rt_format;
+};
+
+static struct mp_image *alloc_pool(void *pctx, int fmt, int w, int h)
+{
+ struct pool_alloc_ctx *alloc_ctx = pctx;
+ if (fmt != IMGFMT_VAAPI)
+ return NULL;
+
+ return alloc_surface(alloc_ctx->vaapi, alloc_ctx->rt_format, w, h);
+}
+
+// The allocator of the given image pool to allocate VAAPI surfaces, using
+// the given rt_format.
+static void va_pool_set_allocator(struct mp_image_pool *pool,
+ struct mp_vaapi_ctx *ctx, int rt_format)
+{
+ struct pool_alloc_ctx *alloc_ctx = talloc_ptrtype(pool, alloc_ctx);
+ *alloc_ctx = (struct pool_alloc_ctx){
+ .vaapi = ctx,
+ .rt_format = rt_format,
+ };
+ mp_image_pool_set_allocator(pool, alloc_pool, alloc_ctx);
+ mp_image_pool_set_lru(pool);
+}
+
+static void flush_output_surfaces(struct priv *p)
+{
+ for (int n = 0; n < MAX_OUTPUT_SURFACES; n++)
+ mp_image_unrefp(&p->output_surfaces[n]);
+ p->output_surface = 0;
+ p->visible_surface = 0;
+}
+
+// See flush_surfaces() remarks - the same applies.
+static void free_video_specific(struct priv *p)
+{
+ flush_output_surfaces(p);
+
+ mp_image_unrefp(&p->black_surface);
+
+ for (int n = 0; n < MAX_OUTPUT_SURFACES; n++)
+ mp_image_unrefp(&p->swdec_surfaces[n]);
+
+ if (p->pool)
+ mp_image_pool_clear(p->pool);
+}
+
+static bool alloc_swdec_surfaces(struct priv *p, int w, int h, int imgfmt)
+{
+ free_video_specific(p);
+ for (int i = 0; i < MAX_OUTPUT_SURFACES; i++) {
+ p->swdec_surfaces[i] = mp_image_pool_get(p->pool, IMGFMT_VAAPI, w, h);
+ if (va_surface_alloc_imgfmt(p, p->swdec_surfaces[i], imgfmt) < 0)
+ return false;
+ }
+ return true;
+}
+
+static void resize(struct priv *p)
+{
+ vo_get_src_dst_rects(p->vo, &p->src_rect, &p->dst_rect, &p->screen_osd_res);
+
+ // It's not clear whether this is needed; maybe not.
+ //vo_x11_clearwindow(p->vo, p->vo->x11->window);
+
+ p->vo->want_redraw = true;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ free_video_specific(p);
+
+ vo_x11_config_vo_window(vo);
+
+ if (params->imgfmt != IMGFMT_VAAPI) {
+ if (!alloc_swdec_surfaces(p, params->w, params->h, params->imgfmt))
+ return -1;
+ }
+
+ p->image_params = *params;
+ resize(p);
+ return 0;
+}
+
+static int query_format(struct vo *vo, int imgfmt)
+{
+ struct priv *p = vo->priv;
+ if (imgfmt == IMGFMT_VAAPI || va_image_format_from_imgfmt(p, imgfmt))
+ return 1;
+
+ return 0;
+}
+
+static bool render_to_screen(struct priv *p, struct mp_image *mpi)
+{
+ VAStatus status;
+
+ VASurfaceID surface = va_surface_id(mpi);
+ if (surface == VA_INVALID_ID) {
+ if (!p->black_surface) {
+ int w = p->image_params.w, h = p->image_params.h;
+ // 4:2:0 should work everywhere
+ int fmt = IMGFMT_420P;
+ p->black_surface = mp_image_pool_get(p->pool, IMGFMT_VAAPI, w, h);
+ if (p->black_surface) {
+ struct mp_image *img = mp_image_alloc(fmt, w, h);
+ if (img) {
+ mp_image_clear(img, 0, 0, w, h);
+ if (va_surface_upload(p, p->black_surface, img) < 0)
+ mp_image_unrefp(&p->black_surface);
+ talloc_free(img);
+ }
+ }
+ }
+ surface = va_surface_id(p->black_surface);
+ }
+
+ if (surface == VA_INVALID_ID)
+ return false;
+
+ struct vaapi_osd_part *part = &p->osd_part;
+ if (part->active) {
+ struct vaapi_subpic *sp = &part->subpic;
+ int flags = 0;
+ if (p->osd_screen)
+ flags |= VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD;
+ status = vaAssociateSubpicture(p->display,
+ sp->id, &surface, 1,
+ sp->src_x, sp->src_y,
+ sp->src_w, sp->src_h,
+ sp->dst_x, sp->dst_y,
+ sp->dst_w, sp->dst_h,
+ flags);
+ CHECK_VA_STATUS(p, "vaAssociateSubpicture()");
+ }
+
+ int flags = va_get_colorspace_flag(p->image_params.color.space) |
+ p->scaling | VA_FRAME_PICTURE;
+ status = vaPutSurface(p->display,
+ surface,
+ p->vo->x11->window,
+ p->src_rect.x0,
+ p->src_rect.y0,
+ p->src_rect.x1 - p->src_rect.x0,
+ p->src_rect.y1 - p->src_rect.y0,
+ p->dst_rect.x0,
+ p->dst_rect.y0,
+ p->dst_rect.x1 - p->dst_rect.x0,
+ p->dst_rect.y1 - p->dst_rect.y0,
+ NULL, 0,
+ flags);
+ CHECK_VA_STATUS(p, "vaPutSurface()");
+
+ if (part->active) {
+ struct vaapi_subpic *sp = &part->subpic;
+ status = vaDeassociateSubpicture(p->display, sp->id,
+ &surface, 1);
+ CHECK_VA_STATUS(p, "vaDeassociateSubpicture()");
+ }
+
+ return true;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->visible_surface = p->output_surface;
+ render_to_screen(p, p->output_surfaces[p->output_surface]);
+ p->output_surface = (p->output_surface + 1) % MAX_OUTPUT_SURFACES;
+ vo_x11_present(vo);
+ present_sync_swap(vo->x11->present);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ present_sync_get_info(x11->present, info);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct mp_image *mpi = frame->current;
+
+ if (mpi && mpi->imgfmt != IMGFMT_VAAPI) {
+ struct mp_image *dst = p->swdec_surfaces[p->output_surface];
+ if (!dst || va_surface_upload(p, dst, mpi) < 0) {
+ MP_WARN(vo, "Could not upload surface.\n");
+ talloc_free(mpi);
+ return;
+ }
+ mp_image_copy_attributes(dst, mpi);
+ mpi = mp_image_new_ref(dst);
+ }
+
+ talloc_free(p->output_surfaces[p->output_surface]);
+ p->output_surfaces[p->output_surface] = mpi;
+
+ draw_osd(vo);
+}
+
+static void free_subpicture(struct priv *p, struct vaapi_osd_image *img)
+{
+ if (img->image.image_id != VA_INVALID_ID)
+ vaDestroyImage(p->display, img->image.image_id);
+ if (img->subpic_id != VA_INVALID_ID)
+ vaDestroySubpicture(p->display, img->subpic_id);
+ img->image.image_id = VA_INVALID_ID;
+ img->subpic_id = VA_INVALID_ID;
+}
+
+static int new_subpicture(struct priv *p, int w, int h,
+ struct vaapi_osd_image *out)
+{
+ VAStatus status;
+
+ free_subpicture(p, out);
+
+ struct vaapi_osd_image m = {
+ .image = {.image_id = VA_INVALID_ID, .buf = VA_INVALID_ID},
+ .subpic_id = VA_INVALID_ID,
+ .w = w,
+ .h = h,
+ };
+
+ status = vaCreateImage(p->display, &p->osd_format, w, h, &m.image);
+ if (!CHECK_VA_STATUS(p, "vaCreateImage()"))
+ goto error;
+ status = vaCreateSubpicture(p->display, m.image.image_id, &m.subpic_id);
+ if (!CHECK_VA_STATUS(p, "vaCreateSubpicture()"))
+ goto error;
+
+ *out = m;
+ return 0;
+
+error:
+ free_subpicture(p, &m);
+ MP_ERR(p, "failed to allocate OSD sub-picture of size %dx%d.\n", w, h);
+ return -1;
+}
+
+static void draw_osd(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ struct mp_image *cur = p->output_surfaces[p->output_surface];
+ double pts = cur ? cur->pts : 0;
+
+ if (!p->osd_format.fourcc)
+ return;
+
+ struct mp_osd_res vid_res = osd_res_from_image_params(vo->params);
+
+ struct mp_osd_res *res;
+ if (p->osd_screen) {
+ res = &p->screen_osd_res;
+ } else {
+ res = &vid_res;
+ }
+
+ p->osd_part.active = false;
+
+ if (!p->osd_cache)
+ p->osd_cache = mp_draw_sub_alloc(p, vo->global);
+
+ struct sub_bitmap_list *sbs = osd_render(vo->osd, *res, pts, 0,
+ mp_draw_sub_formats);
+
+ struct mp_rect act_rc[1], mod_rc[64];
+ int num_act_rc = 0, num_mod_rc = 0;
+
+ struct mp_image *osd = mp_draw_sub_overlay(p->osd_cache, sbs,
+ act_rc, MP_ARRAY_SIZE(act_rc), &num_act_rc,
+ mod_rc, MP_ARRAY_SIZE(mod_rc), &num_mod_rc);
+
+ if (!osd)
+ goto error;
+
+ struct vaapi_osd_part *part = &p->osd_part;
+
+ part->active = false;
+
+ int w = res->w;
+ int h = res->h;
+ if (part->image.w != w || part->image.h != h) {
+ if (new_subpicture(p, w, h, &part->image) < 0)
+ goto error;
+ }
+
+ struct vaapi_osd_image *img = &part->image;
+ struct mp_image vaimg;
+ if (!va_image_map(p->mpvaapi, &img->image, &vaimg))
+ goto error;
+
+ for (int n = 0; n < num_mod_rc; n++) {
+ struct mp_rect *rc = &mod_rc[n];
+
+ int rw = mp_rect_w(*rc);
+ int rh = mp_rect_h(*rc);
+
+ void *src = mp_image_pixel_ptr(osd, 0, rc->x0, rc->y0);
+ void *dst = vaimg.planes[0] + rc->y0 * vaimg.stride[0] + rc->x0 * 4;
+
+ memcpy_pic(dst, src, rw * 4, rh, vaimg.stride[0], osd->stride[0]);
+ }
+
+ if (!va_image_unmap(p->mpvaapi, &img->image))
+ goto error;
+
+ if (num_act_rc) {
+ struct mp_rect rc = act_rc[0];
+ rc.x0 = rc.y0 = 0; // must be a Mesa bug
+ part->subpic = (struct vaapi_subpic) {
+ .id = img->subpic_id,
+ .src_x = rc.x0, .src_y = rc.y0,
+ .src_w = mp_rect_w(rc), .src_h = mp_rect_h(rc),
+ .dst_x = rc.x0, .dst_y = rc.y0,
+ .dst_w = mp_rect_w(rc), .dst_h = mp_rect_h(rc),
+ };
+ part->active = true;
+ }
+
+error:
+ talloc_free(sbs);
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(p);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+ if (events & VO_EVENT_RESIZE)
+ resize(p);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+ return r;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ free_video_specific(p);
+ talloc_free(p->pool);
+
+ struct vaapi_osd_part *part = &p->osd_part;
+ free_subpicture(p, &part->image);
+
+ if (vo->hwdec_devs) {
+ hwdec_devices_remove(vo->hwdec_devs, &p->mpvaapi->hwctx);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+
+ va_destroy(p->mpvaapi);
+
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->vo = vo;
+ p->log = vo->log;
+
+ VAStatus status;
+
+ if (!vo_x11_init(vo))
+ goto fail;
+
+ if (!vo_x11_create_vo_window(vo, NULL, "vaapi"))
+ goto fail;
+
+ p->display = vaGetDisplay(vo->x11->display);
+ if (!p->display)
+ goto fail;
+
+ p->mpvaapi = va_initialize(p->display, p->log, false);
+ if (!p->mpvaapi) {
+ vaTerminate(p->display);
+ p->display = NULL;
+ goto fail;
+ }
+
+ if (va_guess_if_emulated(p->mpvaapi)) {
+ MP_WARN(vo, "VA-API is most likely emulated via VDPAU.\n"
+ "It's better to use VDPAU directly with: --vo=vdpau\n");
+ }
+
+ va_get_formats(p);
+ if (!p->image_formats)
+ goto fail;
+
+ p->mpvaapi->hwctx.hw_imgfmt = IMGFMT_VAAPI;
+ p->pool = mp_image_pool_new(p);
+ va_pool_set_allocator(p->pool, p->mpvaapi, VA_RT_FORMAT_YUV420);
+
+ int max_subpic_formats = vaMaxNumSubpictureFormats(p->display);
+ p->va_subpic_formats = talloc_array(vo, VAImageFormat, max_subpic_formats);
+ p->va_subpic_flags = talloc_array(vo, unsigned int, max_subpic_formats);
+ status = vaQuerySubpictureFormats(p->display,
+ p->va_subpic_formats,
+ p->va_subpic_flags,
+ &p->va_num_subpic_formats);
+ if (!CHECK_VA_STATUS(p, "vaQuerySubpictureFormats()"))
+ p->va_num_subpic_formats = 0;
+ MP_VERBOSE(vo, "%d subpicture formats available:\n",
+ p->va_num_subpic_formats);
+
+ for (int i = 0; i < p->va_num_subpic_formats; i++) {
+ MP_VERBOSE(vo, " %s, flags 0x%x\n",
+ mp_tag_str(p->va_subpic_formats[i].fourcc),
+ p->va_subpic_flags[i]);
+ if (p->va_subpic_formats[i].fourcc == OSD_VA_FORMAT) {
+ p->osd_format = p->va_subpic_formats[i];
+ if (!p->force_scaled_osd) {
+ p->osd_screen =
+ p->va_subpic_flags[i] & VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD;
+ }
+ }
+ }
+
+ if (!p->osd_format.fourcc)
+ MP_ERR(vo, "OSD format not supported. Disabling OSD.\n");
+
+ struct vaapi_osd_part *part = &p->osd_part;
+ part->image.image.image_id = VA_INVALID_ID;
+ part->image.subpic_id = VA_INVALID_ID;
+
+ int max_display_attrs = vaMaxNumDisplayAttributes(p->display);
+ p->va_display_attrs = talloc_array(vo, VADisplayAttribute, max_display_attrs);
+ if (p->va_display_attrs) {
+ status = vaQueryDisplayAttributes(p->display, p->va_display_attrs,
+ &p->va_num_display_attrs);
+ if (!CHECK_VA_STATUS(p, "vaQueryDisplayAttributes()"))
+ p->va_num_display_attrs = 0;
+ p->mp_display_attr = talloc_zero_array(vo, int, p->va_num_display_attrs);
+ }
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_add(vo->hwdec_devs, &p->mpvaapi->hwctx);
+
+ MP_WARN(vo, "Warning: this compatibility VO is low quality and may "
+ "have issues with OSD, scaling, screenshots and more.\n"
+ "vo=gpu is the preferred choice in any case and "
+ "includes VA-API support via hwdec=vaapi or vaapi-copy.\n");
+
+ return 0;
+
+fail:
+ uninit(vo);
+ return -1;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_vaapi = {
+ .description = "VA API with X11",
+ .name = "vaapi",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .scaling = VA_FILTER_SCALING_DEFAULT,
+ },
+ .options = (const struct m_option[]) {
+ {"scaling", OPT_CHOICE(scaling,
+ {"default", VA_FILTER_SCALING_DEFAULT},
+ {"fast", VA_FILTER_SCALING_FAST},
+ {"hq", VA_FILTER_SCALING_HQ},
+ {"nla", VA_FILTER_SCALING_NL_ANAMORPHIC})},
+ {"scaled-osd", OPT_BOOL(force_scaled_osd)},
+ {0}
+ },
+ .options_prefix = "vo-vaapi",
+};
diff --git a/video/out/vo_vdpau.c b/video/out/vo_vdpau.c
new file mode 100644
index 0000000..d6b261f
--- /dev/null
+++ b/video/out/vo_vdpau.c
@@ -0,0 +1,1139 @@
+/*
+ * VDPAU video output driver
+ *
+ * Copyright (C) 2008 NVIDIA (Rajib Mahapatra <rmahapatra@nvidia.com>)
+ * Copyright (C) 2009 Uoti Urpala
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Actual decoding is done in video/decode/vdpau.c
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "video/vdpau.h"
+#include "video/vdpau_mixer.h"
+#include "video/hwdec.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "mpv_talloc.h"
+#include "vo.h"
+#include "x11_common.h"
+#include "video/csputils.h"
+#include "sub/osd.h"
+#include "options/m_option.h"
+#include "video/mp_image.h"
+#include "osdep/timer.h"
+
+// Returns x + a, but wrapped around to the range [0, m)
+// a must be within [-m, m], x within [0, m)
+#define WRAP_ADD(x, a, m) ((a) < 0 \
+ ? ((x)+(a)+(m) < (m) ? (x)+(a)+(m) : (x)+(a)) \
+ : ((x)+(a) < (m) ? (x)+(a) : (x)+(a)-(m)))
+
+
+/* number of video and output surfaces */
+#define MAX_OUTPUT_SURFACES 15
+
+/* Pixelformat used for output surfaces */
+#define OUTPUT_RGBA_FORMAT VDP_RGBA_FORMAT_B8G8R8A8
+
+/*
+ * Global variable declaration - VDPAU specific
+ */
+
+struct vdpctx {
+ struct mp_vdpau_ctx *mpvdp;
+ struct vdp_functions *vdp;
+ VdpDevice vdp_device;
+ uint64_t preemption_counter;
+
+ struct m_color colorkey;
+
+ VdpPresentationQueueTarget flip_target;
+ VdpPresentationQueue flip_queue;
+
+ VdpOutputSurface output_surfaces[MAX_OUTPUT_SURFACES];
+ int num_output_surfaces;
+ VdpOutputSurface black_pixel;
+ VdpOutputSurface rotation_surface;
+
+ struct mp_image *current_image;
+ int64_t current_pts;
+ int current_duration;
+
+ int output_surface_w, output_surface_h;
+ int rotation;
+
+ bool force_yuv;
+ struct mp_vdpau_mixer *video_mixer;
+ bool pullup;
+ float denoise;
+ float sharpen;
+ int hqscaling;
+ bool chroma_deint;
+ int flip_offset_window;
+ int flip_offset_fs;
+ int64_t flip_offset_us;
+
+ VdpRect src_rect_vid;
+ VdpRect out_rect_vid;
+ struct mp_osd_res osd_rect;
+ VdpBool supports_a8;
+
+ int surface_num; // indexes output_surfaces
+ int query_surface_num;
+ VdpTime recent_vsync_time;
+ float user_fps;
+ bool composite_detect;
+ int vsync_interval;
+ uint64_t last_queue_time;
+ uint64_t queue_time[MAX_OUTPUT_SURFACES];
+ uint64_t last_ideal_time;
+ bool dropped_frame;
+ uint64_t dropped_time;
+ uint32_t vid_width, vid_height;
+ uint32_t image_format;
+ VdpYCbCrFormat vdp_pixel_format;
+ bool rgb_mode;
+
+ // OSD
+ struct osd_bitmap_surface {
+ VdpRGBAFormat format;
+ VdpBitmapSurface surface;
+ uint32_t surface_w, surface_h;
+ // List of surfaces to be rendered
+ struct osd_target {
+ VdpRect source;
+ VdpRect dest;
+ VdpColor color;
+ } *targets;
+ int targets_size;
+ int render_count;
+ int change_id;
+ } osd_surfaces[MAX_OSD_PARTS];
+};
+
+static bool status_ok(struct vo *vo);
+
+static int video_to_output_surface(struct vo *vo, struct mp_image *mpi)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpTime dummy;
+ VdpStatus vdp_st;
+
+ VdpOutputSurface output_surface = vc->output_surfaces[vc->surface_num];
+ VdpRect *output_rect = &vc->out_rect_vid;
+ VdpRect *video_rect = &vc->src_rect_vid;
+
+ vdp_st = vdp->presentation_queue_block_until_surface_idle(vc->flip_queue,
+ output_surface,
+ &dummy);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_presentation_queue_block_until_surface_idle");
+
+ // Clear the borders between video and window (if there are any).
+ // For some reason, video_mixer_render doesn't need it for YUV.
+ // Also, if there is nothing to render, at least clear the screen.
+ if (vc->rgb_mode || !mpi || mpi->params.rotate != 0) {
+ int flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_0;
+ vdp_st = vdp->output_surface_render_output_surface(output_surface,
+ NULL, vc->black_pixel,
+ NULL, NULL, NULL,
+ flags);
+ CHECK_VDP_WARNING(vo, "Error clearing screen");
+ }
+
+ if (!mpi)
+ return -1;
+
+ struct mp_vdpau_mixer_frame *frame = mp_vdpau_mixed_frame_get(mpi);
+ struct mp_vdpau_mixer_opts opts = {0};
+ if (frame)
+ opts = frame->opts;
+
+ // Apply custom vo_vdpau suboptions.
+ opts.chroma_deint |= vc->chroma_deint;
+ opts.pullup |= vc->pullup;
+ opts.denoise = MPCLAMP(opts.denoise + vc->denoise, 0, 1);
+ opts.sharpen = MPCLAMP(opts.sharpen + vc->sharpen, -1, 1);
+ if (vc->hqscaling)
+ opts.hqscaling = vc->hqscaling;
+
+ if (mpi->params.rotate != 0) {
+ int flags;
+ VdpRect r_rect;
+ switch (mpi->params.rotate) {
+ case 90:
+ r_rect.y0 = output_rect->x0;
+ r_rect.y1 = output_rect->x1;
+ r_rect.x0 = output_rect->y0;
+ r_rect.x1 = output_rect->y1;
+ flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_90;
+ break;
+ case 180:
+ r_rect.x0 = output_rect->x0;
+ r_rect.x1 = output_rect->x1;
+ r_rect.y0 = output_rect->y0;
+ r_rect.y1 = output_rect->y1;
+ flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_180;
+ break;
+ case 270:
+ r_rect.y0 = output_rect->x0;
+ r_rect.y1 = output_rect->x1;
+ r_rect.x0 = output_rect->y0;
+ r_rect.x1 = output_rect->y1;
+ flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_270;
+ break;
+ default:
+ MP_ERR(vo, "Unsupported rotation angle: %u\n", mpi->params.rotate);
+ return -1;
+ }
+
+ mp_vdpau_mixer_render(vc->video_mixer, &opts, vc->rotation_surface,
+ &r_rect, mpi, video_rect);
+ vdp_st = vdp->output_surface_render_output_surface(output_surface,
+ output_rect,
+ vc->rotation_surface,
+ &r_rect,
+ NULL,
+ NULL,
+ flags);
+ CHECK_VDP_WARNING(vo, "Error rendering rotated frame");
+ } else {
+ mp_vdpau_mixer_render(vc->video_mixer, &opts, output_surface,
+ output_rect, mpi, video_rect);
+ }
+ return 0;
+}
+
+static void forget_frames(struct vo *vo, bool seek_reset)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (!seek_reset)
+ mp_image_unrefp(&vc->current_image);
+
+ vc->dropped_frame = false;
+}
+
+static int s_size(int max, int s, int disp)
+{
+ disp = MPMAX(1, disp);
+ return MPMIN(max, MPMAX(s, disp));
+}
+
+static void resize(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ vo_get_src_dst_rects(vo, &src_rect, &dst_rect, &vc->osd_rect);
+ vc->out_rect_vid.x0 = dst_rect.x0;
+ vc->out_rect_vid.x1 = dst_rect.x1;
+ vc->out_rect_vid.y0 = dst_rect.y0;
+ vc->out_rect_vid.y1 = dst_rect.y1;
+ if (vo->params->rotate == 90 || vo->params->rotate == 270) {
+ vc->src_rect_vid.y0 = src_rect.x0;
+ vc->src_rect_vid.y1 = src_rect.x1;
+ vc->src_rect_vid.x0 = src_rect.y0;
+ vc->src_rect_vid.x1 = src_rect.y1;
+ } else {
+ vc->src_rect_vid.x0 = src_rect.x0;
+ vc->src_rect_vid.x1 = src_rect.x1;
+ vc->src_rect_vid.y0 = src_rect.y0;
+ vc->src_rect_vid.y1 = src_rect.y1;
+ }
+
+ VdpBool ok;
+ uint32_t max_w, max_h;
+ vdp_st = vdp->output_surface_query_capabilities(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ &ok, &max_w, &max_h);
+ if (vdp_st != VDP_STATUS_OK || !ok)
+ return;
+
+ vc->flip_offset_us = vo->opts->fullscreen ?
+ 1000LL * vc->flip_offset_fs :
+ 1000LL * vc->flip_offset_window;
+ vo_set_queue_params(vo, vc->flip_offset_us * 1000, 1);
+
+ if (vc->output_surface_w < vo->dwidth || vc->output_surface_h < vo->dheight ||
+ vc->rotation != vo->params->rotate)
+ {
+ vc->output_surface_w = s_size(max_w, vc->output_surface_w, vo->dwidth);
+ vc->output_surface_h = s_size(max_h, vc->output_surface_h, vo->dheight);
+ // Creation of output_surfaces
+ for (int i = 0; i < vc->num_output_surfaces; i++)
+ if (vc->output_surfaces[i] != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->output_surfaces[i]);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_output_surface_destroy");
+ }
+ for (int i = 0; i < vc->num_output_surfaces; i++) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ vc->output_surface_w,
+ vc->output_surface_h,
+ &vc->output_surfaces[i]);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_create");
+ MP_DBG(vo, "vdpau out create: %u\n",
+ vc->output_surfaces[i]);
+ }
+ if (vc->rotation_surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->rotation_surface);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_output_surface_destroy");
+ vc->rotation_surface = VDP_INVALID_HANDLE;
+ }
+ if (vo->params->rotate == 90 || vo->params->rotate == 270) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ vc->output_surface_h,
+ vc->output_surface_w,
+ &vc->rotation_surface);
+ } else if (vo->params->rotate == 180) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ vc->output_surface_w,
+ vc->output_surface_h,
+ &vc->rotation_surface);
+ }
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_create");
+ MP_DBG(vo, "vdpau rotation surface create: %u\n",
+ vc->rotation_surface);
+ }
+ vc->rotation = vo->params->rotate;
+ vo->want_redraw = true;
+}
+
+static int win_x11_init_vdpau_flip_queue(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ struct vo_x11_state *x11 = vo->x11;
+ VdpStatus vdp_st;
+
+ if (vc->flip_target == VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_target_create_x11(vc->vdp_device,
+ x11->window,
+ &vc->flip_target);
+ CHECK_VDP_ERROR(vo, "Error when calling "
+ "vdp_presentation_queue_target_create_x11");
+ }
+
+ /* Empirically this seems to be the first call which fails when we
+ * try to reinit after preemption while the user is still switched
+ * from X to a virtual terminal (creating the vdp_device initially
+ * succeeds, as does creating the flip_target above). This is
+ * probably not guaranteed behavior.
+ */
+ if (vc->flip_queue == VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_create(vc->vdp_device, vc->flip_target,
+ &vc->flip_queue);
+ CHECK_VDP_ERROR(vo, "Error when calling vdp_presentation_queue_create");
+ }
+
+ if (vc->colorkey.a > 0) {
+ VdpColor color = {
+ .red = vc->colorkey.r / 255.0,
+ .green = vc->colorkey.g / 255.0,
+ .blue = vc->colorkey.b / 255.0,
+ .alpha = 0,
+ };
+ vdp_st = vdp->presentation_queue_set_background_color(vc->flip_queue,
+ &color);
+ CHECK_VDP_WARNING(vo, "Error setting colorkey");
+ }
+
+ if (vc->composite_detect && vo_x11_screen_is_composited(vo)) {
+ MP_INFO(vo, "Compositing window manager detected. Assuming timing info "
+ "is inaccurate.\n");
+ vc->user_fps = -1;
+ }
+
+ return 0;
+}
+
+// Free everything specific to a certain video file
+static void free_video_specific(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ forget_frames(vo, false);
+
+ if (vc->black_pixel != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->black_pixel);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_destroy");
+ }
+ vc->black_pixel = VDP_INVALID_HANDLE;
+}
+
+static int initialize_vdpau_objects(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ mp_vdpau_get_format(vc->image_format, NULL, &vc->vdp_pixel_format);
+
+ vc->video_mixer->initialized = false;
+
+ if (win_x11_init_vdpau_flip_queue(vo) < 0)
+ return -1;
+
+ if (vc->black_pixel == VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device, OUTPUT_RGBA_FORMAT,
+ 1, 1, &vc->black_pixel);
+ CHECK_VDP_ERROR(vo, "Allocating clearing surface");
+ const char data[4] = {0};
+ vdp_st = vdp->output_surface_put_bits_native(vc->black_pixel,
+ (const void*[]){data},
+ (uint32_t[]){4}, NULL);
+ CHECK_VDP_ERROR(vo, "Initializing clearing surface");
+ }
+
+ forget_frames(vo, false);
+ resize(vo);
+ return 0;
+}
+
+static void mark_vdpau_objects_uninitialized(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ forget_frames(vo, false);
+ vc->black_pixel = VDP_INVALID_HANDLE;
+ vc->flip_queue = VDP_INVALID_HANDLE;
+ vc->flip_target = VDP_INVALID_HANDLE;
+ for (int i = 0; i < MAX_OUTPUT_SURFACES; i++)
+ vc->output_surfaces[i] = VDP_INVALID_HANDLE;
+ vc->rotation_surface = VDP_INVALID_HANDLE;
+ vc->vdp_device = VDP_INVALID_HANDLE;
+ for (int i = 0; i < MAX_OSD_PARTS; i++) {
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[i];
+ sfc->change_id = 0;
+ *sfc = (struct osd_bitmap_surface){
+ .surface = VDP_INVALID_HANDLE,
+ };
+ }
+ vc->output_surface_w = vc->output_surface_h = -1;
+}
+
+static bool check_preemption(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ int r = mp_vdpau_handle_preemption(vc->mpvdp, &vc->preemption_counter);
+ if (r < 1) {
+ mark_vdpau_objects_uninitialized(vo);
+ if (r < 0)
+ return false;
+ vc->vdp_device = vc->mpvdp->vdp_device;
+ if (initialize_vdpau_objects(vo) < 0)
+ return false;
+ }
+ return true;
+}
+
+static bool status_ok(struct vo *vo)
+{
+ return vo->config_ok && check_preemption(vo);
+}
+
+/*
+ * connect to X server, create and map window, initialize all
+ * VDPAU objects, create different surfaces etc.
+ */
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ if (!check_preemption(vo))
+ {
+ /*
+ * When prempted, leave the reconfig() immediately
+ * without reconfiguring the vo_window and without
+ * initializing the vdpau objects. When recovered
+ * from preemption, if there is a difference between
+ * the VD thread parameters and the VO thread parameters
+ * the reconfig() is triggered again.
+ */
+ return 0;
+ }
+
+ VdpChromaType chroma_type = VDP_CHROMA_TYPE_420;
+ mp_vdpau_get_format(params->imgfmt, &chroma_type, NULL);
+
+ VdpBool ok;
+ uint32_t max_w, max_h;
+ vdp_st = vdp->video_surface_query_capabilities(vc->vdp_device, chroma_type,
+ &ok, &max_w, &max_h);
+ CHECK_VDP_ERROR(vo, "Error when calling vdp_video_surface_query_capabilities");
+
+ if (!ok)
+ return -1;
+ if (params->w > max_w || params->h > max_h) {
+ if (ok)
+ MP_ERR(vo, "Video too large for vdpau.\n");
+ return -1;
+ }
+
+ vc->image_format = params->imgfmt;
+ vc->vid_width = params->w;
+ vc->vid_height = params->h;
+
+ vc->rgb_mode = mp_vdpau_get_rgb_format(params->imgfmt, NULL);
+
+ free_video_specific(vo);
+
+ vo_x11_config_vo_window(vo);
+
+ if (initialize_vdpau_objects(vo) < 0)
+ return -1;
+
+ return 0;
+}
+
+static void draw_osd_part(struct vo *vo, int index)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[index];
+ VdpOutputSurface output_surface = vc->output_surfaces[vc->surface_num];
+ int i;
+
+ VdpOutputSurfaceRenderBlendState blend_state = {
+ .struct_version = VDP_OUTPUT_SURFACE_RENDER_BLEND_STATE_VERSION,
+ .blend_factor_source_color =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_SRC_ALPHA,
+ .blend_factor_source_alpha =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ZERO,
+ .blend_factor_destination_color =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
+ .blend_factor_destination_alpha =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ZERO,
+ .blend_equation_color = VDP_OUTPUT_SURFACE_RENDER_BLEND_EQUATION_ADD,
+ .blend_equation_alpha = VDP_OUTPUT_SURFACE_RENDER_BLEND_EQUATION_ADD,
+ };
+
+ VdpOutputSurfaceRenderBlendState blend_state_premultiplied = blend_state;
+ blend_state_premultiplied.blend_factor_source_color =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ONE;
+
+ for (i = 0; i < sfc->render_count; i++) {
+ VdpOutputSurfaceRenderBlendState *blend = &blend_state;
+ if (sfc->format == VDP_RGBA_FORMAT_B8G8R8A8)
+ blend = &blend_state_premultiplied;
+ vdp_st = vdp->
+ output_surface_render_bitmap_surface(output_surface,
+ &sfc->targets[i].dest,
+ sfc->surface,
+ &sfc->targets[i].source,
+ &sfc->targets[i].color,
+ blend,
+ VDP_OUTPUT_SURFACE_RENDER_ROTATE_0);
+ CHECK_VDP_WARNING(vo, "OSD: Error when rendering");
+ }
+}
+
+static int next_pow2(int v)
+{
+ for (int x = 0; x < 30; x++) {
+ if ((1 << x) >= v)
+ return 1 << x;
+ }
+ return INT_MAX;
+}
+
+static void generate_osd_part(struct vo *vo, struct sub_bitmaps *imgs)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[imgs->render_index];
+
+ if (imgs->change_id == sfc->change_id)
+ return; // Nothing changed and we still have the old data
+
+ sfc->change_id = imgs->change_id;
+ sfc->render_count = 0;
+
+ if (imgs->format == SUBBITMAP_EMPTY || imgs->num_parts == 0)
+ return;
+
+ VdpRGBAFormat format;
+ switch (imgs->format) {
+ case SUBBITMAP_LIBASS:
+ format = VDP_RGBA_FORMAT_A8;
+ break;
+ case SUBBITMAP_BGRA:
+ format = VDP_RGBA_FORMAT_B8G8R8A8;
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ };
+
+ assert(imgs->packed);
+
+ int r_w = next_pow2(imgs->packed_w);
+ int r_h = next_pow2(imgs->packed_h);
+
+ if (sfc->format != format || sfc->surface == VDP_INVALID_HANDLE ||
+ sfc->surface_w < r_w || sfc->surface_h < r_h)
+ {
+ MP_VERBOSE(vo, "Allocating a %dx%d surface for OSD bitmaps.\n", r_w, r_h);
+
+ uint32_t m_w = 0, m_h = 0;
+ vdp_st = vdp->bitmap_surface_query_capabilities(vc->vdp_device, format,
+ &(VdpBool){0}, &m_w, &m_h);
+ CHECK_VDP_WARNING(vo, "Query to get max OSD surface size failed");
+
+ if (r_w > m_w || r_h > m_h) {
+ MP_ERR(vo, "OSD bitmaps do not fit on a surface with the maximum "
+ "supported size\n");
+ return;
+ }
+
+ if (sfc->surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->bitmap_surface_destroy(sfc->surface);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_bitmap_surface_destroy");
+ }
+
+ VdpBitmapSurface surface;
+ vdp_st = vdp->bitmap_surface_create(vc->vdp_device, format,
+ r_w, r_h, true, &surface);
+ CHECK_VDP_WARNING(vo, "OSD: error when creating surface");
+ if (vdp_st != VDP_STATUS_OK)
+ return;
+
+ sfc->surface = surface;
+ sfc->surface_w = r_w;
+ sfc->surface_h = r_h;
+ sfc->format = format;
+ }
+
+ void *data = imgs->packed->planes[0];
+ int stride = imgs->packed->stride[0];
+ VdpRect rc = {0, 0, imgs->packed_w, imgs->packed_h};
+ vdp_st = vdp->bitmap_surface_put_bits_native(sfc->surface,
+ &(const void *){data},
+ &(uint32_t){stride},
+ &rc);
+ CHECK_VDP_WARNING(vo, "OSD: putbits failed");
+
+ MP_TARRAY_GROW(vc, sfc->targets, imgs->num_parts);
+ sfc->render_count = imgs->num_parts;
+
+ for (int i = 0; i < imgs->num_parts; i++) {
+ struct sub_bitmap *b = &imgs->parts[i];
+ struct osd_target *target = &sfc->targets[i];
+ target->source = (VdpRect){b->src_x, b->src_y,
+ b->src_x + b->w, b->src_y + b->h};
+ target->dest = (VdpRect){b->x, b->y, b->x + b->dw, b->y + b->dh};
+ target->color = (VdpColor){1, 1, 1, 1};
+ if (imgs->format == SUBBITMAP_LIBASS) {
+ uint32_t color = b->libass.color;
+ target->color.alpha = 1.0 - ((color >> 0) & 0xff) / 255.0;
+ target->color.blue = ((color >> 8) & 0xff) / 255.0;
+ target->color.green = ((color >> 16) & 0xff) / 255.0;
+ target->color.red = ((color >> 24) & 0xff) / 255.0;
+ }
+ }
+}
+
+static void draw_osd_cb(void *ctx, struct sub_bitmaps *imgs)
+{
+ struct vo *vo = ctx;
+ generate_osd_part(vo, imgs);
+ draw_osd_part(vo, imgs->render_index);
+}
+
+static void draw_osd(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (!status_ok(vo))
+ return;
+
+ bool formats[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_LIBASS] = vc->supports_a8,
+ [SUBBITMAP_BGRA] = true,
+ };
+
+ double pts = vc->current_image ? vc->current_image->pts : 0;
+ osd_draw(vo->osd, vc->osd_rect, pts, 0, formats, draw_osd_cb, vo);
+}
+
+static int update_presentation_queue_status(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ while (vc->query_surface_num != vc->surface_num) {
+ VdpTime vtime;
+ VdpPresentationQueueStatus status;
+ VdpOutputSurface surface = vc->output_surfaces[vc->query_surface_num];
+ vdp_st = vdp->presentation_queue_query_surface_status(vc->flip_queue,
+ surface,
+ &status, &vtime);
+ CHECK_VDP_WARNING(vo, "Error calling "
+ "presentation_queue_query_surface_status");
+ if (mp_msg_test(vo->log, MSGL_TRACE)) {
+ VdpTime current;
+ vdp_st = vdp->presentation_queue_get_time(vc->flip_queue, &current);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_presentation_queue_get_time");
+ MP_TRACE(vo, "Vdpau time: %"PRIu64"\n", (uint64_t)current);
+ MP_TRACE(vo, "Surface %d status: %d time: %"PRIu64"\n",
+ (int)surface, (int)status, (uint64_t)vtime);
+ }
+ if (status == VDP_PRESENTATION_QUEUE_STATUS_QUEUED)
+ break;
+ if (vc->vsync_interval > 1) {
+ uint64_t qtime = vc->queue_time[vc->query_surface_num];
+ int diff = ((int64_t)vtime - (int64_t)qtime) / 1e6;
+ MP_TRACE(vo, "Queue time difference: %d ms\n", diff);
+ if (vtime < qtime + vc->vsync_interval / 2)
+ MP_VERBOSE(vo, "Frame shown too early (%d ms)\n", diff);
+ if (vtime > qtime + vc->vsync_interval)
+ MP_VERBOSE(vo, "Frame shown late (%d ms)\n", diff);
+ }
+ vc->query_surface_num = WRAP_ADD(vc->query_surface_num, 1,
+ vc->num_output_surfaces);
+ vc->recent_vsync_time = vtime;
+ }
+ int num_queued = WRAP_ADD(vc->surface_num, -vc->query_surface_num,
+ vc->num_output_surfaces);
+ MP_DBG(vo, "Queued surface count (before add): %d\n", num_queued);
+ return num_queued;
+}
+
+// Return the timestamp of the vsync that must have happened before ts.
+static inline uint64_t prev_vsync(struct vdpctx *vc, uint64_t ts)
+{
+ int64_t diff = (int64_t)(ts - vc->recent_vsync_time);
+ int64_t offset = diff % vc->vsync_interval;
+ if (offset < 0)
+ offset += vc->vsync_interval;
+ return ts - offset;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ int64_t pts_us = vc->current_pts;
+ int duration = vc->current_duration;
+
+ vc->dropped_frame = true; // changed at end if false
+
+ if (!check_preemption(vo))
+ goto drop;
+
+ vc->vsync_interval = 1;
+ if (vc->user_fps > 0) {
+ vc->vsync_interval = 1e9 / vc->user_fps;
+ } else if (vc->user_fps == 0) {
+ vc->vsync_interval = vo_get_vsync_interval(vo);
+ }
+ vc->vsync_interval = MPMAX(vc->vsync_interval, 1);
+
+ if (duration > INT_MAX / 1000)
+ duration = -1;
+ else
+ duration *= 1000;
+
+ if (vc->vsync_interval == 1)
+ duration = -1; // Make sure drop logic is disabled
+
+ VdpTime vdp_time = 0;
+ vdp_st = vdp->presentation_queue_get_time(vc->flip_queue, &vdp_time);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_presentation_queue_get_time");
+
+ int64_t rel_pts_ns = (pts_us * 1000) - mp_time_ns();
+ if (!pts_us || rel_pts_ns < 0)
+ rel_pts_ns = 0;
+
+ uint64_t now = vdp_time;
+ uint64_t pts = now + rel_pts_ns;
+ uint64_t ideal_pts = pts;
+ uint64_t npts = duration >= 0 ? pts + duration : UINT64_MAX;
+
+ /* This should normally never happen.
+ * - The last queued frame can't have a PTS that goes more than 50ms in the
+ * future. This is guaranteed by vo.c, which currently actually queues
+ * ahead by roughly the flip queue offset. Just to be sure
+ * give some additional room by doubling the time.
+ * - The last vsync can never be in the future.
+ */
+ int64_t max_pts_ahead = vc->flip_offset_us * 1000 * 2;
+ if (vc->last_queue_time > now + max_pts_ahead ||
+ vc->recent_vsync_time > now)
+ {
+ vc->last_queue_time = 0;
+ vc->recent_vsync_time = 0;
+ MP_WARN(vo, "Inconsistent timing detected.\n");
+ }
+
+#define PREV_VSYNC(ts) prev_vsync(vc, ts)
+
+ /* We hope to be here at least one vsync before the frame should be shown.
+ * If we are running late then don't drop the frame unless there is
+ * already one queued for the next vsync; even if we _hope_ to show the
+ * next frame soon enough to mean this one should be dropped we might
+ * not make the target time in reality. Without this check we could drop
+ * every frame, freezing the display completely if video lags behind.
+ */
+ if (now > PREV_VSYNC(MPMAX(pts, vc->last_queue_time + vc->vsync_interval)))
+ npts = UINT64_MAX;
+
+ /* Allow flipping a frame at a vsync if its presentation time is a
+ * bit after that vsync and the change makes the flip time delta
+ * from previous frame better match the target timestamp delta.
+ * This avoids instability with frame timestamps falling near vsyncs.
+ * For example if the frame timestamps were (with vsyncs at
+ * integer values) 0.01, 1.99, 4.01, 5.99, 8.01, ... then
+ * straightforward timing at next vsync would flip the frames at
+ * 1, 2, 5, 6, 9; this changes it to 1, 2, 4, 6, 8 and so on with
+ * regular 2-vsync intervals.
+ *
+ * Also allow moving the frame forward if it looks like we dropped
+ * the previous frame incorrectly (now that we know better after
+ * having final exact timestamp information for this frame) and
+ * there would unnecessarily be a vsync without a frame change.
+ */
+ uint64_t vsync = PREV_VSYNC(pts);
+ if (pts < vsync + vc->vsync_interval / 4
+ && (vsync - PREV_VSYNC(vc->last_queue_time)
+ > pts - vc->last_ideal_time + vc->vsync_interval / 2
+ || (vc->dropped_frame && vsync > vc->dropped_time)))
+ pts -= vc->vsync_interval / 2;
+
+ vc->dropped_time = ideal_pts;
+
+ pts = MPMAX(pts, vc->last_queue_time + vc->vsync_interval);
+ pts = MPMAX(pts, now);
+ if (npts < PREV_VSYNC(pts) + vc->vsync_interval)
+ goto drop;
+
+ int num_flips = update_presentation_queue_status(vo);
+ vsync = vc->recent_vsync_time + num_flips * vc->vsync_interval;
+ pts = MPMAX(pts, now);
+ pts = MPMAX(pts, vsync + (vc->vsync_interval >> 2));
+ vsync = PREV_VSYNC(pts);
+ if (npts < vsync + vc->vsync_interval)
+ goto drop;
+ pts = vsync + (vc->vsync_interval >> 2);
+ VdpOutputSurface frame = vc->output_surfaces[vc->surface_num];
+ vdp_st = vdp->presentation_queue_display(vc->flip_queue, frame,
+ vo->dwidth, vo->dheight, pts);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_presentation_queue_display");
+
+ MP_TRACE(vo, "Queue new surface %d: Vdpau time: %"PRIu64" "
+ "pts: %"PRIu64"\n", (int)frame, now, pts);
+
+ vc->last_queue_time = pts;
+ vc->queue_time[vc->surface_num] = pts;
+ vc->last_ideal_time = ideal_pts;
+ vc->dropped_frame = false;
+ vc->surface_num = WRAP_ADD(vc->surface_num, 1, vc->num_output_surfaces);
+ return;
+
+drop:
+ vo_increment_drop_count(vo, 1);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vdpctx *vc = vo->priv;
+
+ check_preemption(vo);
+
+ if (frame->current && !frame->redraw) {
+ struct mp_image *vdp_mpi =
+ mp_vdpau_upload_video_surface(vc->mpvdp, frame->current);
+ if (!vdp_mpi)
+ MP_ERR(vo, "Could not upload image.\n");
+
+ talloc_free(vc->current_image);
+ vc->current_image = vdp_mpi;
+ }
+
+ vc->current_pts = frame->pts;
+ vc->current_duration = frame->duration;
+
+ if (status_ok(vo)) {
+ video_to_output_surface(vo, vc->current_image);
+ draw_osd(vo);
+ }
+}
+
+// warning: the size and pixel format of surface must match that of the
+// surfaces in vc->output_surfaces
+static struct mp_image *read_output_surface(struct vo *vo,
+ VdpOutputSurface surface)
+{
+ struct vdpctx *vc = vo->priv;
+ VdpStatus vdp_st;
+ struct vdp_functions *vdp = vc->vdp;
+ if (!vo->params)
+ return NULL;
+
+ VdpRGBAFormat fmt;
+ uint32_t w, h;
+ vdp_st = vdp->output_surface_get_parameters(surface, &fmt, &w, &h);
+ if (vdp_st != VDP_STATUS_OK)
+ return NULL;
+
+ assert(fmt == OUTPUT_RGBA_FORMAT);
+
+ struct mp_image *image = mp_image_alloc(IMGFMT_BGR0, w, h);
+ if (!image)
+ return NULL;
+
+ void *dst_planes[] = { image->planes[0] };
+ uint32_t dst_pitches[] = { image->stride[0] };
+ vdp_st = vdp->output_surface_get_bits_native(surface, NULL, dst_planes,
+ dst_pitches);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_get_bits_native");
+
+ return image;
+}
+
+static struct mp_image *get_window_screenshot(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ int last_surface = WRAP_ADD(vc->surface_num, -1, vc->num_output_surfaces);
+ VdpOutputSurface screen = vc->output_surfaces[last_surface];
+ struct mp_image *image = read_output_surface(vo, screen);
+ if (image && image->w >= vo->dwidth && image->h >= vo->dheight)
+ mp_image_set_size(image, vo->dwidth, vo->dheight);
+ return image;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (mp_vdpau_get_format(format, NULL, NULL))
+ return 1;
+ if (!vc->force_yuv && mp_vdpau_get_rgb_format(format, NULL))
+ return 1;
+ return 0;
+}
+
+static void destroy_vdpau_objects(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+
+ VdpStatus vdp_st;
+
+ free_video_specific(vo);
+
+ if (vc->flip_queue != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_destroy(vc->flip_queue);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_presentation_queue_destroy");
+ }
+
+ if (vc->flip_target != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_target_destroy(vc->flip_target);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_presentation_queue_target_destroy");
+ }
+
+ for (int i = 0; i < vc->num_output_surfaces; i++) {
+ if (vc->output_surfaces[i] == VDP_INVALID_HANDLE)
+ continue;
+ vdp_st = vdp->output_surface_destroy(vc->output_surfaces[i]);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_destroy");
+ }
+ if (vc->rotation_surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->rotation_surface);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_destroy");
+ }
+
+ for (int i = 0; i < MAX_OSD_PARTS; i++) {
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[i];
+ if (sfc->surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->bitmap_surface_destroy(sfc->surface);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_bitmap_surface_destroy");
+ }
+ }
+
+ mp_vdpau_destroy(vc->mpvdp);
+ vc->mpvdp = NULL;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ hwdec_devices_remove(vo->hwdec_devs, &vc->mpvdp->hwctx);
+ hwdec_devices_destroy(vo->hwdec_devs);
+
+ /* Destroy all vdpau objects */
+ mp_vdpau_mixer_destroy(vc->video_mixer);
+ destroy_vdpau_objects(vo);
+
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (!vo_x11_init(vo))
+ return -1;
+
+ if (!vo_x11_create_vo_window(vo, NULL, "vdpau")) {
+ vo_x11_uninit(vo);
+ return -1;
+ }
+
+ vc->mpvdp = mp_vdpau_create_device_x11(vo->log, vo->x11->display, false);
+ if (!vc->mpvdp) {
+ vo_x11_uninit(vo);
+ return -1;
+ }
+ vc->mpvdp->hwctx.hw_imgfmt = IMGFMT_VDPAU;
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_add(vo->hwdec_devs, &vc->mpvdp->hwctx);
+
+ vc->video_mixer = mp_vdpau_mixer_create(vc->mpvdp, vo->log);
+ vc->video_mixer->video_eq = mp_csp_equalizer_create(vo, vo->global);
+
+ if (mp_vdpau_guess_if_emulated(vc->mpvdp)) {
+ MP_WARN(vo, "VDPAU is most likely emulated via VA-API.\n"
+ "This is inefficient. Use --vo=gpu instead.\n");
+ }
+
+ // Mark everything as invalid first so uninit() can tell what has been
+ // allocated
+ mark_vdpau_objects_uninitialized(vo);
+
+ mp_vdpau_handle_preemption(vc->mpvdp, &vc->preemption_counter);
+
+ vc->vdp_device = vc->mpvdp->vdp_device;
+ vc->vdp = &vc->mpvdp->vdp;
+
+ vc->vdp->bitmap_surface_query_capabilities(vc->vdp_device, VDP_RGBA_FORMAT_A8,
+ &vc->supports_a8, &(uint32_t){0}, &(uint32_t){0});
+
+ MP_WARN(vo, "Warning: this compatibility VO is low quality and may "
+ "have issues with OSD, scaling, screenshots and more.\n"
+ "vo=gpu is the preferred choice in any case and "
+ "includes VDPAU support via hwdec=vdpau or vdpau-copy.\n");
+
+ return 0;
+}
+
+static void checked_resize(struct vo *vo)
+{
+ if (!status_ok(vo))
+ return;
+ resize(vo);
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ check_preemption(vo);
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ checked_resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ vo->want_redraw = true;
+ return true;
+ case VOCTRL_RESET:
+ forget_frames(vo, true);
+ return true;
+ case VOCTRL_SCREENSHOT_WIN:
+ if (!status_ok(vo))
+ return false;
+ *(struct mp_image **)data = get_window_screenshot(vo);
+ return true;
+ }
+
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+
+ if (events & VO_EVENT_RESIZE) {
+ checked_resize(vo);
+ } else if (events & VO_EVENT_EXPOSE) {
+ vo->want_redraw = true;
+ }
+ vo_event(vo, events);
+
+ return r;
+}
+
+#define OPT_BASE_STRUCT struct vdpctx
+
+const struct vo_driver video_out_vdpau = {
+ .description = "VDPAU with X11",
+ .name = "vdpau",
+ .caps = VO_CAP_FRAMEDROP | VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct vdpctx),
+ .options = (const struct m_option []){
+ {"chroma-deint", OPT_BOOL(chroma_deint), OPTDEF_INT(1)},
+ {"pullup", OPT_BOOL(pullup)},
+ {"denoise", OPT_FLOAT(denoise), M_RANGE(0, 1)},
+ {"sharpen", OPT_FLOAT(sharpen), M_RANGE(-1, 1)},
+ {"hqscaling", OPT_INT(hqscaling), M_RANGE(0, 9)},
+ {"fps", OPT_FLOAT(user_fps)},
+ {"composite-detect", OPT_BOOL(composite_detect), OPTDEF_INT(1)},
+ {"queuetime-windowed", OPT_INT(flip_offset_window), OPTDEF_INT(50)},
+ {"queuetime-fs", OPT_INT(flip_offset_fs), OPTDEF_INT(50)},
+ {"output-surfaces", OPT_INT(num_output_surfaces),
+ M_RANGE(2, MAX_OUTPUT_SURFACES), OPTDEF_INT(3)},
+ {"colorkey", OPT_COLOR(colorkey),
+ .defval = &(const struct m_color){.r = 2, .g = 5, .b = 7, .a = 255}},
+ {"force-yuv", OPT_BOOL(force_yuv)},
+ {NULL},
+ },
+ .options_prefix = "vo-vdpau",
+};
diff --git a/video/out/vo_wlshm.c b/video/out/vo_wlshm.c
new file mode 100644
index 0000000..1e5e009
--- /dev/null
+++ b/video/out/vo_wlshm.c
@@ -0,0 +1,324 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <libswscale/swscale.h>
+
+#include "osdep/endian.h"
+#include "present_sync.h"
+#include "sub/osd.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+#include "vo.h"
+#include "wayland_common.h"
+
+struct buffer {
+ struct vo *vo;
+ size_t size;
+ struct wl_shm_pool *pool;
+ struct wl_buffer *buffer;
+ struct mp_image mpi;
+ struct buffer *next;
+};
+
+struct priv {
+ struct mp_sws_context *sws;
+ struct buffer *free_buffers;
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+};
+
+static void buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
+{
+ struct buffer *buf = data;
+ struct vo *vo = buf->vo;
+ struct priv *p = vo->priv;
+
+ if (buf->mpi.w == vo->dwidth && buf->mpi.h == vo->dheight) {
+ buf->next = p->free_buffers;
+ p->free_buffers = buf;
+ } else {
+ talloc_free(buf);
+ }
+}
+
+static const struct wl_buffer_listener buffer_listener = {
+ buffer_handle_release,
+};
+
+static void buffer_destroy(void *p)
+{
+ struct buffer *buf = p;
+ wl_buffer_destroy(buf->buffer);
+ wl_shm_pool_destroy(buf->pool);
+ munmap(buf->mpi.planes[0], buf->size);
+}
+
+static struct buffer *buffer_create(struct vo *vo, int width, int height)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ int fd;
+ int stride;
+ size_t size;
+ uint8_t *data;
+ struct buffer *buf;
+
+ stride = MP_ALIGN_UP(width * 4, 16);
+ size = height * stride;
+ fd = vo_wayland_allocate_memfd(vo, size);
+ if (fd < 0)
+ goto error0;
+ data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED)
+ goto error1;
+ buf = talloc_zero(NULL, struct buffer);
+ if (!buf)
+ goto error2;
+ buf->vo = vo;
+ buf->size = size;
+ mp_image_set_params(&buf->mpi, &p->sws->dst);
+ mp_image_set_size(&buf->mpi, width, height);
+ buf->mpi.planes[0] = data;
+ buf->mpi.stride[0] = stride;
+ buf->pool = wl_shm_create_pool(wl->shm, fd, size);
+ if (!buf->pool)
+ goto error3;
+ buf->buffer = wl_shm_pool_create_buffer(buf->pool, 0, width, height,
+ stride, WL_SHM_FORMAT_XRGB8888);
+ if (!buf->buffer)
+ goto error4;
+ wl_buffer_add_listener(buf->buffer, &buffer_listener, buf);
+
+ close(fd);
+ talloc_set_destructor(buf, buffer_destroy);
+
+ return buf;
+
+error4:
+ wl_shm_pool_destroy(buf->pool);
+error3:
+ talloc_free(buf);
+error2:
+ munmap(data, size);
+error1:
+ close(fd);
+error0:
+ return NULL;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct buffer *buf;
+
+ while (p->free_buffers) {
+ buf = p->free_buffers;
+ p->free_buffers = buf->next;
+ talloc_free(buf);
+ }
+ vo_wayland_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!vo_wayland_init(vo))
+ goto err;
+ if (!vo->wl->shm) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wl_shm_interface.name);
+ goto err;
+ }
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+ return 0;
+err:
+ uninit(vo);
+ return -1;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return sws_isSupportedInput(imgfmt2pixfmt(format));
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ if (!vo_wayland_reconfig(vo))
+ return -1;
+ p->sws->src = *params;
+
+ return 0;
+}
+
+static int resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+
+ if (width == 0 || height == 0)
+ return 1;
+
+ struct buffer *buf;
+
+ vo_wayland_set_opaque_region(wl, false);
+ vo->want_redraw = true;
+ vo->dwidth = width;
+ vo->dheight = height;
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = MP_SELECT_LE_BE(IMGFMT_BGR0, IMGFMT_0RGB),
+ .w = width,
+ .h = height,
+ .p_w = 1,
+ .p_h = 1,
+ };
+ mp_image_params_guess_csp(&p->sws->dst);
+ while (p->free_buffers) {
+ buf = p->free_buffers;
+ p->free_buffers = buf->next;
+ talloc_free(buf);
+ }
+
+ vo_wayland_handle_fractional_scale(wl);
+
+ return mp_sws_reinit(p->sws);
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int ret = vo_wayland_control(vo, &events, request, data);
+
+ if (events & VO_EVENT_RESIZE)
+ ret = resize(vo);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+ return ret;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ struct mp_image *src = frame->current;
+ struct buffer *buf;
+
+ bool render = vo_wayland_check_visible(vo);
+ if (!render)
+ return;
+
+ buf = p->free_buffers;
+ if (buf) {
+ p->free_buffers = buf->next;
+ } else {
+ buf = buffer_create(vo, vo->dwidth, vo->dheight);
+ if (!buf) {
+ wl_surface_attach(wl->surface, NULL, 0, 0);
+ return;
+ }
+ }
+ if (src) {
+ struct mp_image dst = buf->mpi;
+ struct mp_rect src_rc;
+ struct mp_rect dst_rc;
+ src_rc.x0 = MP_ALIGN_DOWN(p->src.x0, MPMAX(src->fmt.align_x, 4));
+ src_rc.y0 = MP_ALIGN_DOWN(p->src.y0, MPMAX(src->fmt.align_y, 4));
+ src_rc.x1 = p->src.x1 - (p->src.x0 - src_rc.x0);
+ src_rc.y1 = p->src.y1 - (p->src.y0 - src_rc.y0);
+ dst_rc.x0 = MP_ALIGN_DOWN(p->dst.x0, MPMAX(dst.fmt.align_x, 4));
+ dst_rc.y0 = MP_ALIGN_DOWN(p->dst.y0, MPMAX(dst.fmt.align_y, 4));
+ dst_rc.x1 = p->dst.x1 - (p->dst.x0 - dst_rc.x0);
+ dst_rc.y1 = p->dst.y1 - (p->dst.y0 - dst_rc.y0);
+ mp_image_crop_rc(src, src_rc);
+ mp_image_crop_rc(&dst, dst_rc);
+ mp_sws_scale(p->sws, &dst, src);
+ if (dst_rc.y0 > 0)
+ mp_image_clear(&buf->mpi, 0, 0, buf->mpi.w, dst_rc.y0);
+ if (buf->mpi.h > dst_rc.y1)
+ mp_image_clear(&buf->mpi, 0, dst_rc.y1, buf->mpi.w, buf->mpi.h);
+ if (dst_rc.x0 > 0)
+ mp_image_clear(&buf->mpi, 0, dst_rc.y0, dst_rc.x0, dst_rc.y1);
+ if (buf->mpi.w > dst_rc.x1)
+ mp_image_clear(&buf->mpi, dst_rc.x1, dst_rc.y0, buf->mpi.w, dst_rc.y1);
+ osd_draw_on_image(vo->osd, p->osd, src->pts, 0, &buf->mpi);
+ } else {
+ mp_image_clear(&buf->mpi, 0, 0, buf->mpi.w, buf->mpi.h);
+ osd_draw_on_image(vo->osd, p->osd, 0, 0, &buf->mpi);
+ }
+ wl_surface_attach(wl->surface, buf->buffer, 0, 0);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ wl_surface_damage_buffer(wl->surface, 0, 0, vo->dwidth,
+ vo->dheight);
+ wl_surface_commit(wl->surface);
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+const struct vo_driver video_out_wlshm = {
+ .description = "Wayland SHM video output (software scaling)",
+ .name = "wlshm",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_wayland_wakeup,
+ .wait_events = vo_wayland_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_x11.c b/video/out/vo_x11.c
new file mode 100644
index 0000000..fa93157
--- /dev/null
+++ b/video/out/vo_x11.c
@@ -0,0 +1,447 @@
+/*
+ * Original author: Aaron Holtzman <aholtzma@ess.engr.uvic.ca>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include <libswscale/swscale.h>
+
+#include "vo.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include <errno.h>
+
+#include "present_sync.h"
+#include "x11_common.h"
+
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <X11/extensions/XShm.h>
+
+#include "sub/osd.h"
+#include "sub/draw_bmp.h"
+
+#include "video/sws_utils.h"
+#include "video/fmt-conversion.h"
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/options.h"
+#include "osdep/timer.h"
+
+struct priv {
+ struct vo *vo;
+
+ struct mp_image *original_image;
+
+ XImage *myximage[2];
+ struct mp_image mp_ximages[2];
+ int depth;
+ GC gc;
+
+ uint32_t image_width;
+ uint32_t image_height;
+
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+
+ struct mp_sws_context *sws;
+
+ XVisualInfo vinfo;
+
+ int current_buf;
+
+ int Shmem_Flag;
+ XShmSegmentInfo Shminfo[2];
+ int Shm_Warned_Slow;
+};
+
+static bool resize(struct vo *vo);
+
+static bool getMyXImage(struct priv *p, int foo)
+{
+ struct vo *vo = p->vo;
+ if (vo->x11->display_is_local && XShmQueryExtension(vo->x11->display)) {
+ p->Shmem_Flag = 1;
+ vo->x11->ShmCompletionEvent = XShmGetEventBase(vo->x11->display)
+ + ShmCompletion;
+ } else {
+ p->Shmem_Flag = 0;
+ MP_WARN(vo, "Shared memory not supported\nReverting to normal Xlib\n");
+ }
+
+ if (p->Shmem_Flag) {
+ p->myximage[foo] =
+ XShmCreateImage(vo->x11->display, p->vinfo.visual, p->depth,
+ ZPixmap, NULL, &p->Shminfo[foo], p->image_width,
+ p->image_height);
+ if (p->myximage[foo] == NULL) {
+ MP_WARN(vo, "Shared memory error,disabling ( Ximage error )\n");
+ goto shmemerror;
+ }
+ p->Shminfo[foo].shmid = shmget(IPC_PRIVATE,
+ p->myximage[foo]->bytes_per_line *
+ p->myximage[foo]->height,
+ IPC_CREAT | 0777);
+ if (p->Shminfo[foo].shmid < 0) {
+ XDestroyImage(p->myximage[foo]);
+ MP_WARN(vo, "Shared memory error,disabling ( seg id error )\n");
+ goto shmemerror;
+ }
+ p->Shminfo[foo].shmaddr = (char *) shmat(p->Shminfo[foo].shmid, 0, 0);
+
+ if (p->Shminfo[foo].shmaddr == ((char *) -1)) {
+ XDestroyImage(p->myximage[foo]);
+ MP_WARN(vo, "Shared memory error,disabling ( address error )\n");
+ goto shmemerror;
+ }
+ p->myximage[foo]->data = p->Shminfo[foo].shmaddr;
+ p->Shminfo[foo].readOnly = False;
+ XShmAttach(vo->x11->display, &p->Shminfo[foo]);
+
+ XSync(vo->x11->display, False);
+
+ shmctl(p->Shminfo[foo].shmid, IPC_RMID, 0);
+ } else {
+shmemerror:
+ p->Shmem_Flag = 0;
+
+ MP_VERBOSE(vo, "Not using SHM.\n");
+ p->myximage[foo] =
+ XCreateImage(vo->x11->display, p->vinfo.visual, p->depth, ZPixmap,
+ 0, NULL, p->image_width, p->image_height, 8, 0);
+ if (p->myximage[foo]) {
+ p->myximage[foo]->data =
+ calloc(1, p->myximage[foo]->bytes_per_line * p->image_height + 32);
+ }
+ if (!p->myximage[foo] || !p->myximage[foo]->data) {
+ MP_WARN(vo, "could not allocate image");
+ return false;
+ }
+ }
+ return true;
+}
+
+static void freeMyXImage(struct priv *p, int foo)
+{
+ struct vo *vo = p->vo;
+ if (p->Shmem_Flag) {
+ XShmDetach(vo->x11->display, &p->Shminfo[foo]);
+ XDestroyImage(p->myximage[foo]);
+ shmdt(p->Shminfo[foo].shmaddr);
+ } else {
+ if (p->myximage[foo]) {
+ // XDestroyImage() would free the data too since XFree() just calls
+ // free(), but do it ourselves for portability reasons
+ free(p->myximage[foo]->data);
+ p->myximage[foo]->data = NULL;
+ XDestroyImage(p->myximage[foo]);
+ }
+ }
+ p->myximage[foo] = NULL;
+}
+
+#define MAKE_MASK(comp) (((1ul << (comp).size) - 1) << (comp).offset)
+
+static int reconfig(struct vo *vo, struct mp_image_params *fmt)
+{
+ vo_x11_config_vo_window(vo);
+
+ if (!resize(vo))
+ return -1;
+
+ return 0;
+}
+
+static bool resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ // Attempt to align. We don't know the size in bytes yet (????), so just
+ // assume worst case (1 byte per pixel).
+ int nw = MPMAX(1, MP_ALIGN_UP(vo->dwidth, MP_IMAGE_BYTE_ALIGN));
+ int nh = MPMAX(1, vo->dheight);
+
+ if (nw > p->image_width || nh > p->image_height) {
+ for (int i = 0; i < 2; i++)
+ freeMyXImage(p, i);
+
+ p->image_width = nw;
+ p->image_height = nh;
+
+ for (int i = 0; i < 2; i++) {
+ if (!getMyXImage(p, i)) {
+ p->image_width = 0;
+ p->image_height = 0;
+ return false;
+ }
+ }
+ }
+
+ int mpfmt = 0;
+ for (int fmt = IMGFMT_START; fmt < IMGFMT_END; fmt++) {
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(fmt);
+ if ((desc.flags & MP_IMGFLAG_HAS_COMPS) && desc.num_planes == 1 &&
+ (desc.flags & MP_IMGFLAG_COLOR_MASK) == MP_IMGFLAG_COLOR_RGB &&
+ (desc.flags & MP_IMGFLAG_TYPE_MASK) == MP_IMGFLAG_TYPE_UINT &&
+ (desc.flags & MP_IMGFLAG_NE) && !(desc.flags & MP_IMGFLAG_ALPHA) &&
+ desc.bpp[0] <= 8 * sizeof(unsigned long) &&
+ p->myximage[0]->bits_per_pixel == desc.bpp[0] &&
+ p->myximage[0]->byte_order == MP_SELECT_LE_BE(LSBFirst, MSBFirst))
+ {
+ // desc.comps[] uses little endian bit offsets, so "swap" the
+ // offsets here.
+ if (MP_SELECT_LE_BE(0, 1)) {
+ // Except for formats that use byte swapping; for these, the
+ // offsets are in native endian. There is no way to distinguish
+ // which one a given format is (could even be both), and using
+ // mp_find_other_endian() is just a guess.
+ if (!mp_find_other_endian(fmt)) {
+ for (int c = 0; c < 3; c++) {
+ desc.comps[c].offset =
+ desc.bpp[0] - desc.comps[c].size -desc.comps[c].offset;
+ }
+ }
+ }
+ if (p->myximage[0]->red_mask == MAKE_MASK(desc.comps[0]) &&
+ p->myximage[0]->green_mask == MAKE_MASK(desc.comps[1]) &&
+ p->myximage[0]->blue_mask == MAKE_MASK(desc.comps[2]))
+ {
+ mpfmt = fmt;
+ break;
+ }
+ }
+ }
+
+ if (!mpfmt) {
+ MP_ERR(vo, "X server image format not supported, use another VO.\n");
+ return false;
+ }
+ MP_VERBOSE(vo, "Using mp format: %s\n", mp_imgfmt_to_name(mpfmt));
+
+ for (int i = 0; i < 2; i++) {
+ struct mp_image *img = &p->mp_ximages[i];
+ *img = (struct mp_image){0};
+ mp_image_setfmt(img, mpfmt);
+ mp_image_set_size(img, p->image_width, p->image_height);
+ img->planes[0] = p->myximage[i]->data;
+ img->stride[0] = p->myximage[i]->bytes_per_line;
+
+ mp_image_params_guess_csp(&img->params);
+ }
+
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+
+ if (vo->params) {
+ p->sws->src = *vo->params;
+ p->sws->src.w = mp_rect_w(p->src);
+ p->sws->src.h = mp_rect_h(p->src);
+
+ p->sws->dst = p->mp_ximages[0].params;
+ p->sws->dst.w = mp_rect_w(p->dst);
+ p->sws->dst.h = mp_rect_h(p->dst);
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return false;
+ }
+
+ vo->want_redraw = true;
+ return true;
+}
+
+static void Display_Image(struct priv *p, XImage *myximage)
+{
+ struct vo *vo = p->vo;
+
+ XImage *x_image = p->myximage[p->current_buf];
+
+ if (p->Shmem_Flag) {
+ XShmPutImage(vo->x11->display, vo->x11->window, p->gc, x_image,
+ 0, 0, 0, 0, vo->dwidth, vo->dheight, True);
+ vo->x11->ShmCompletionWaitCount++;
+ } else {
+ XPutImage(vo->x11->display, vo->x11->window, p->gc, x_image,
+ 0, 0, 0, 0, vo->dwidth, vo->dheight);
+ }
+}
+
+static void wait_for_completion(struct vo *vo, int max_outstanding)
+{
+ struct priv *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ if (ctx->Shmem_Flag) {
+ while (x11->ShmCompletionWaitCount > max_outstanding) {
+ if (!ctx->Shm_Warned_Slow) {
+ MP_WARN(vo, "can't keep up! Waiting"
+ " for XShm completion events...\n");
+ ctx->Shm_Warned_Slow = 1;
+ }
+ mp_sleep_ns(MP_TIME_MS_TO_NS(1));
+ vo_x11_check_events(vo);
+ }
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ Display_Image(p, p->myximage[p->current_buf]);
+ p->current_buf = (p->current_buf + 1) % 2;
+ if (vo->x11->use_present) {
+ vo_x11_present(vo);
+ present_sync_swap(vo->x11->present);
+ }
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ wait_for_completion(vo, 1);
+ bool render = vo_x11_check_visible(vo);
+ if (!render)
+ return;
+
+ struct mp_image *img = &p->mp_ximages[p->current_buf];
+
+ if (frame->current) {
+ mp_image_clear_rc_inv(img, p->dst);
+
+ struct mp_image *src = frame->current;
+ struct mp_rect src_rc = p->src;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, src->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, src->fmt.align_y);
+ mp_image_crop_rc(src, src_rc);
+
+ struct mp_image dst = *img;
+ mp_image_crop_rc(&dst, p->dst);
+
+ mp_sws_scale(p->sws, &dst, src);
+ } else {
+ mp_image_clear(img, 0, 0, img->w, img->h);
+ }
+
+ osd_draw_on_image(vo->osd, p->osd, frame->current ? frame->current->pts : 0, 0, img);
+
+ if (frame->current != p->original_image)
+ p->original_image = frame->current;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *p = vo->priv;
+ if (mp_sws_supports_formats(p->sws, IMGFMT_RGB0, format))
+ return 1;
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (p->myximage[0])
+ freeMyXImage(p, 0);
+ if (p->myximage[1])
+ freeMyXImage(p, 1);
+ if (p->gc)
+ XFreeGC(vo->x11->display, p->gc);
+
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->vo = vo;
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+ if (!vo_x11_init(vo))
+ goto error;
+ struct vo_x11_state *x11 = vo->x11;
+
+ XWindowAttributes attribs;
+ XGetWindowAttributes(x11->display, x11->rootwin, &attribs);
+ p->depth = attribs.depth;
+
+ if (!XMatchVisualInfo(x11->display, x11->screen, p->depth,
+ TrueColor, &p->vinfo))
+ goto error;
+
+ MP_VERBOSE(vo, "selected visual: %d\n", (int)p->vinfo.visualid);
+
+ if (!vo_x11_create_vo_window(vo, &p->vinfo, "x11"))
+ goto error;
+
+ p->gc = XCreateGC(x11->display, x11->window, 0, NULL);
+ MP_WARN(vo, "Warning: this legacy VO has bad performance. Consider fixing "
+ "your graphics drivers, or not forcing the x11 VO.\n");
+ return 0;
+
+error:
+ uninit(vo);
+ return -1;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ if (vo->config_ok)
+ resize(vo);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+ if (vo->config_ok && (events & (VO_EVENT_EXPOSE | VO_EVENT_RESIZE)))
+ resize(vo);
+ vo_event(vo, events);
+ return r;
+}
+
+const struct vo_driver video_out_x11 = {
+ .description = "X11 (software scaling)",
+ .name = "x11",
+ .priv_size = sizeof(struct priv),
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+};
diff --git a/video/out/vo_xv.c b/video/out/vo_xv.c
new file mode 100644
index 0000000..6c776c5
--- /dev/null
+++ b/video/out/vo_xv.c
@@ -0,0 +1,921 @@
+/*
+ * X11 Xv interface
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include <libavutil/common.h>
+
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <X11/extensions/XShm.h>
+
+// Note: depends on the inclusion of X11/extensions/XShm.h
+#include <X11/extensions/Xv.h>
+#include <X11/extensions/Xvlib.h>
+
+#include "options/options.h"
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "present_sync.h"
+#include "x11_common.h"
+#include "sub/osd.h"
+#include "sub/draw_bmp.h"
+#include "video/csputils.h"
+#include "options/m_option.h"
+#include "input/input.h"
+#include "osdep/timer.h"
+
+#define CK_METHOD_NONE 0 // no colorkey drawing
+#define CK_METHOD_BACKGROUND 1 // set colorkey as window background
+#define CK_METHOD_AUTOPAINT 2 // let xv draw the colorkey
+#define CK_METHOD_MANUALFILL 3 // manually draw the colorkey
+#define CK_SRC_USE 0 // use specified / default colorkey
+#define CK_SRC_SET 1 // use and set specified / default colorkey
+#define CK_SRC_CUR 2 // use current colorkey (get it from xv)
+
+#define MAX_BUFFERS 10
+
+struct xvctx {
+ struct xv_ck_info_s {
+ int method; // CK_METHOD_* constants
+ int source; // CK_SRC_* constants
+ } xv_ck_info;
+ int colorkey;
+ unsigned long xv_colorkey;
+ int xv_port;
+ int cfg_xv_adaptor;
+ int cfg_buffers;
+ XvAdaptorInfo *ai;
+ XvImageFormatValues *fo;
+ unsigned int formats, adaptors, xv_format;
+ int current_buf;
+ int current_ip_buf;
+ int num_buffers;
+ XvImage *xvimage[MAX_BUFFERS];
+ struct mp_image *original_image;
+ uint32_t image_width;
+ uint32_t image_height;
+ uint32_t image_format;
+ int cached_csp;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ uint32_t max_width, max_height; // zero means: not set
+ GC f_gc; // used to paint background
+ GC vo_gc; // used to paint video
+ int Shmem_Flag;
+ XShmSegmentInfo Shminfo[MAX_BUFFERS];
+ int Shm_Warned_Slow;
+};
+
+#define MP_FOURCC(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((unsigned)(d)<<24))
+
+#define MP_FOURCC_YV12 MP_FOURCC('Y', 'V', '1', '2')
+#define MP_FOURCC_I420 MP_FOURCC('I', '4', '2', '0')
+#define MP_FOURCC_IYUV MP_FOURCC('I', 'Y', 'U', 'V')
+#define MP_FOURCC_UYVY MP_FOURCC('U', 'Y', 'V', 'Y')
+
+struct fmt_entry {
+ int imgfmt;
+ int fourcc;
+};
+static const struct fmt_entry fmt_table[] = {
+ {IMGFMT_420P, MP_FOURCC_YV12},
+ {IMGFMT_420P, MP_FOURCC_I420},
+ {IMGFMT_UYVY, MP_FOURCC_UYVY},
+ {0}
+};
+
+static bool allocate_xvimage(struct vo *, int);
+static void deallocate_xvimage(struct vo *vo, int foo);
+static struct mp_image get_xv_buffer(struct vo *vo, int buf_index);
+
+static int find_xv_format(int imgfmt)
+{
+ for (int n = 0; fmt_table[n].imgfmt; n++) {
+ if (fmt_table[n].imgfmt == imgfmt)
+ return fmt_table[n].fourcc;
+ }
+ return 0;
+}
+
+static int xv_find_atom(struct vo *vo, uint32_t xv_port, const char *name,
+ bool get, int *min, int *max)
+{
+ Atom atom = None;
+ int howmany = 0;
+ XvAttribute *attributes = XvQueryPortAttributes(vo->x11->display, xv_port,
+ &howmany);
+ for (int i = 0; i < howmany && attributes; i++) {
+ int flag = get ? XvGettable : XvSettable;
+ if (attributes[i].flags & flag) {
+ atom = XInternAtom(vo->x11->display, attributes[i].name, True);
+ *min = attributes[i].min_value;
+ *max = attributes[i].max_value;
+/* since we have SET_DEFAULTS first in our list, we can check if it's available
+ then trigger it if it's ok so that the other values are at default upon query */
+ if (atom != None) {
+ if (!strcmp(attributes[i].name, "XV_BRIGHTNESS") &&
+ (!strcmp(name, "brightness")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_CONTRAST") &&
+ (!strcmp(name, "contrast")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_SATURATION") &&
+ (!strcmp(name, "saturation")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_HUE") &&
+ (!strcmp(name, "hue")))
+ break;
+ if (!strcmp(attributes[i].name, "XV_RED_INTENSITY") &&
+ (!strcmp(name, "red_intensity")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_GREEN_INTENSITY")
+ && (!strcmp(name, "green_intensity")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_BLUE_INTENSITY")
+ && (!strcmp(name, "blue_intensity")))
+ break;
+ else if ((!strcmp(attributes[i].name, "XV_ITURBT_709") //NVIDIA
+ || !strcmp(attributes[i].name, "XV_COLORSPACE")) //ATI
+ && (!strcmp(name, "bt_709")))
+ break;
+ atom = None;
+ continue;
+ }
+ }
+ }
+ XFree(attributes);
+ return atom;
+}
+
+static int xv_set_eq(struct vo *vo, uint32_t xv_port, const char *name,
+ int value)
+{
+ MP_VERBOSE(vo, "xv_set_eq called! (%s, %d)\n", name, value);
+
+ int min, max;
+ int atom = xv_find_atom(vo, xv_port, name, false, &min, &max);
+ if (atom != None) {
+ // -100 -> min
+ // 0 -> (max+min)/2
+ // +100 -> max
+ int port_value = (value + 100) * (max - min) / 200 + min;
+ XvSetPortAttribute(vo->x11->display, xv_port, atom, port_value);
+ return VO_TRUE;
+ }
+ return VO_FALSE;
+}
+
+static int xv_get_eq(struct vo *vo, uint32_t xv_port, const char *name,
+ int *value)
+{
+ int min, max;
+ int atom = xv_find_atom(vo, xv_port, name, true, &min, &max);
+ if (atom != None) {
+ int port_value = 0;
+ XvGetPortAttribute(vo->x11->display, xv_port, atom, &port_value);
+
+ *value = (port_value - min) * 200 / (max - min) - 100;
+ MP_VERBOSE(vo, "xv_get_eq called! (%s, %d)\n", name, *value);
+ return VO_TRUE;
+ }
+ return VO_FALSE;
+}
+
+static Atom xv_intern_atom_if_exists(struct vo *vo, char const *atom_name)
+{
+ struct xvctx *ctx = vo->priv;
+ XvAttribute *attributes;
+ int attrib_count, i;
+ Atom xv_atom = None;
+
+ attributes = XvQueryPortAttributes(vo->x11->display, ctx->xv_port,
+ &attrib_count);
+ if (attributes != NULL) {
+ for (i = 0; i < attrib_count; ++i) {
+ if (strcmp(attributes[i].name, atom_name) == 0) {
+ xv_atom = XInternAtom(vo->x11->display, atom_name, False);
+ break;
+ }
+ }
+ XFree(attributes);
+ }
+
+ return xv_atom;
+}
+
+// Try to enable vsync for xv.
+// Returns -1 if not available, 0 on failure and 1 on success.
+static int xv_enable_vsync(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ Atom xv_atom = xv_intern_atom_if_exists(vo, "XV_SYNC_TO_VBLANK");
+ if (xv_atom == None)
+ return -1;
+ return XvSetPortAttribute(vo->x11->display, ctx->xv_port, xv_atom, 1)
+ == Success;
+}
+
+// Get maximum supported source image dimensions.
+// If querying the dimensions fails, don't change *width and *height.
+static void xv_get_max_img_dim(struct vo *vo, uint32_t *width, uint32_t *height)
+{
+ struct xvctx *ctx = vo->priv;
+ XvEncodingInfo *encodings;
+ unsigned int num_encodings, idx;
+
+ XvQueryEncodings(vo->x11->display, ctx->xv_port, &num_encodings, &encodings);
+
+ if (encodings) {
+ for (idx = 0; idx < num_encodings; ++idx) {
+ if (strcmp(encodings[idx].name, "XV_IMAGE") == 0) {
+ *width = encodings[idx].width;
+ *height = encodings[idx].height;
+ break;
+ }
+ }
+ }
+
+ MP_VERBOSE(vo, "Maximum source image dimensions: %ux%u\n", *width, *height);
+
+ XvFreeEncodingInfo(encodings);
+}
+
+static void xv_print_ck_info(struct vo *vo)
+{
+ struct xvctx *xv = vo->priv;
+
+ switch (xv->xv_ck_info.method) {
+ case CK_METHOD_NONE:
+ MP_VERBOSE(vo, "Drawing no colorkey.\n");
+ return;
+ case CK_METHOD_AUTOPAINT:
+ MP_VERBOSE(vo, "Colorkey is drawn by Xv.\n");
+ break;
+ case CK_METHOD_MANUALFILL:
+ MP_VERBOSE(vo, "Drawing colorkey manually.\n");
+ break;
+ case CK_METHOD_BACKGROUND:
+ MP_VERBOSE(vo, "Colorkey is drawn as window background.\n");
+ break;
+ }
+
+ switch (xv->xv_ck_info.source) {
+ case CK_SRC_CUR:
+ MP_VERBOSE(vo, "Using colorkey from Xv (0x%06lx).\n", xv->xv_colorkey);
+ break;
+ case CK_SRC_USE:
+ if (xv->xv_ck_info.method == CK_METHOD_AUTOPAINT) {
+ MP_VERBOSE(vo, "Ignoring colorkey from mpv (0x%06lx).\n",
+ xv->xv_colorkey);
+ } else {
+ MP_VERBOSE(vo, "Using colorkey from mpv (0x%06lx). Use -colorkey to change.\n",
+ xv->xv_colorkey);
+ }
+ break;
+ case CK_SRC_SET:
+ MP_VERBOSE(vo, "Setting and using colorkey from mpv (0x%06lx)."
+ " Use -colorkey to change.\n", xv->xv_colorkey);
+ break;
+ }
+}
+
+/* NOTE: If vo.colorkey has bits set after the first 3 low order bytes
+ * we don't draw anything as this means it was forced to off. */
+static int xv_init_colorkey(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ Display *display = vo->x11->display;
+ Atom xv_atom;
+ int rez;
+
+ /* check if colorkeying is needed */
+ xv_atom = xv_intern_atom_if_exists(vo, "XV_COLORKEY");
+ if (xv_atom != None && ctx->xv_ck_info.method != CK_METHOD_NONE) {
+ if (ctx->xv_ck_info.source == CK_SRC_CUR) {
+ int colorkey_ret;
+
+ rez = XvGetPortAttribute(display, ctx->xv_port, xv_atom,
+ &colorkey_ret);
+ if (rez == Success)
+ ctx->xv_colorkey = colorkey_ret;
+ else {
+ MP_FATAL(vo, "Couldn't get colorkey! "
+ "Maybe the selected Xv port has no overlay.\n");
+ return 0; // error getting colorkey
+ }
+ } else {
+ ctx->xv_colorkey = ctx->colorkey;
+
+ /* check if we have to set the colorkey too */
+ if (ctx->xv_ck_info.source == CK_SRC_SET) {
+ xv_atom = XInternAtom(display, "XV_COLORKEY", False);
+
+ rez = XvSetPortAttribute(display, ctx->xv_port, xv_atom,
+ ctx->colorkey);
+ if (rez != Success) {
+ MP_FATAL(vo, "Couldn't set colorkey!\n");
+ return 0; // error setting colorkey
+ }
+ }
+ }
+
+ xv_atom = xv_intern_atom_if_exists(vo, "XV_AUTOPAINT_COLORKEY");
+
+ /* should we draw the colorkey ourselves or activate autopainting? */
+ if (ctx->xv_ck_info.method == CK_METHOD_AUTOPAINT) {
+ rez = !Success;
+
+ if (xv_atom != None) // autopaint is supported
+ rez = XvSetPortAttribute(display, ctx->xv_port, xv_atom, 1);
+
+ if (rez != Success)
+ ctx->xv_ck_info.method = CK_METHOD_MANUALFILL;
+ } else {
+ // disable colorkey autopainting if supported
+ if (xv_atom != None)
+ XvSetPortAttribute(display, ctx->xv_port, xv_atom, 0);
+ }
+ } else { // do no colorkey drawing at all
+ ctx->xv_ck_info.method = CK_METHOD_NONE;
+ ctx->colorkey = 0xFF000000;
+ }
+
+ xv_print_ck_info(vo);
+
+ return 1;
+}
+
+/* Draw the colorkey on the video window.
+ *
+ * Draws the colorkey depending on the set method ( colorkey_handling ).
+ *
+ * Also draws the black bars ( when the video doesn't fit the display in
+ * fullscreen ) separately, so they don't overlap with the video area. */
+static void xv_draw_colorkey(struct vo *vo, const struct mp_rect *rc)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ if (ctx->xv_ck_info.method == CK_METHOD_MANUALFILL ||
+ ctx->xv_ck_info.method == CK_METHOD_BACKGROUND)
+ {
+ if (!ctx->vo_gc)
+ return;
+ //less tearing than XClearWindow()
+ XSetForeground(x11->display, ctx->vo_gc, ctx->xv_colorkey);
+ XFillRectangle(x11->display, x11->window, ctx->vo_gc, rc->x0, rc->y0,
+ rc->x1 - rc->x0, rc->y1 - rc->y0);
+ }
+}
+
+static void read_xv_csp(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ ctx->cached_csp = 0;
+ int bt709_enabled;
+ if (xv_get_eq(vo, ctx->xv_port, "bt_709", &bt709_enabled))
+ ctx->cached_csp = bt709_enabled == 100 ? MP_CSP_BT_709 : MP_CSP_BT_601;
+}
+
+
+static void fill_rect(struct vo *vo, GC gc, int x0, int y0, int x1, int y1)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ x0 = MPMAX(x0, 0);
+ y0 = MPMAX(y0, 0);
+ x1 = MPMIN(x1, vo->dwidth);
+ y1 = MPMIN(y1, vo->dheight);
+
+ if (x11->window && gc && x1 > x0 && y1 > y0)
+ XFillRectangle(x11->display, x11->window, gc, x0, y0, x1 - x0, y1 - y0);
+}
+
+// Clear everything outside of rc with the background color
+static void vo_x11_clear_background(struct vo *vo, const struct mp_rect *rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct xvctx *ctx = vo->priv;
+ GC gc = ctx->f_gc;
+
+ int w = vo->dwidth;
+ int h = vo->dheight;
+
+ fill_rect(vo, gc, 0, 0, w, rc->y0); // top
+ fill_rect(vo, gc, 0, rc->y1, w, h); // bottom
+ fill_rect(vo, gc, 0, rc->y0, rc->x0, rc->y1); // left
+ fill_rect(vo, gc, rc->x1, rc->y0, w, rc->y1); // right
+
+ XFlush(x11->display);
+}
+
+static void resize(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+
+ // Can't be used, because the function calculates screen-space coordinates,
+ // while we need video-space.
+ struct mp_osd_res unused;
+
+ vo_get_src_dst_rects(vo, &ctx->src_rect, &ctx->dst_rect, &unused);
+
+ vo_x11_clear_background(vo, &ctx->dst_rect);
+ xv_draw_colorkey(vo, &ctx->dst_rect);
+ read_xv_csp(vo);
+
+ mp_input_set_mouse_transform(vo->input_ctx, &ctx->dst_rect, &ctx->src_rect);
+
+ vo->want_redraw = true;
+}
+
+/*
+ * create and map window,
+ * allocate colors and (shared) memory
+ */
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct xvctx *ctx = vo->priv;
+ int i;
+
+ ctx->image_height = params->h;
+ ctx->image_width = params->w;
+ ctx->image_format = params->imgfmt;
+
+ if ((ctx->max_width != 0 && ctx->max_height != 0)
+ && (ctx->image_width > ctx->max_width
+ || ctx->image_height > ctx->max_height)) {
+ MP_ERR(vo, "Source image dimensions are too high: %ux%u (maximum is %ux%u)\n",
+ ctx->image_width, ctx->image_height, ctx->max_width,
+ ctx->max_height);
+ return -1;
+ }
+
+ /* check image formats */
+ ctx->xv_format = 0;
+ for (i = 0; i < ctx->formats; i++) {
+ MP_VERBOSE(vo, "Xvideo image format: 0x%x (%4.4s) %s\n",
+ ctx->fo[i].id, (char *) &ctx->fo[i].id,
+ (ctx->fo[i].format == XvPacked) ? "packed" : "planar");
+ if (ctx->fo[i].id == find_xv_format(ctx->image_format))
+ ctx->xv_format = ctx->fo[i].id;
+ }
+ if (!ctx->xv_format)
+ return -1;
+
+ vo_x11_config_vo_window(vo);
+
+ if (!ctx->f_gc && !ctx->vo_gc) {
+ ctx->f_gc = XCreateGC(x11->display, x11->window, 0, 0);
+ ctx->vo_gc = XCreateGC(x11->display, x11->window, 0, NULL);
+ XSetForeground(x11->display, ctx->f_gc, 0);
+ }
+
+ if (ctx->xv_ck_info.method == CK_METHOD_BACKGROUND)
+ XSetWindowBackground(x11->display, x11->window, ctx->xv_colorkey);
+
+ MP_VERBOSE(vo, "using Xvideo port %d for hw scaling\n", ctx->xv_port);
+
+ // In case config has been called before
+ for (i = 0; i < ctx->num_buffers; i++)
+ deallocate_xvimage(vo, i);
+
+ ctx->num_buffers = ctx->cfg_buffers;
+
+ for (i = 0; i < ctx->num_buffers; i++) {
+ if (!allocate_xvimage(vo, i)) {
+ MP_FATAL(vo, "could not allocate Xv image data\n");
+ return -1;
+ }
+ }
+
+ ctx->current_buf = 0;
+ ctx->current_ip_buf = 0;
+
+ int is_709 = params->color.space == MP_CSP_BT_709;
+ xv_set_eq(vo, ctx->xv_port, "bt_709", is_709 * 200 - 100);
+ read_xv_csp(vo);
+
+ resize(vo);
+
+ return 0;
+}
+
+static bool allocate_xvimage(struct vo *vo, int foo)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ // align it for faster OSD rendering (draw_bmp.c swscale usage)
+ int aligned_w = FFALIGN(ctx->image_width, 32);
+ // round up the height to next chroma boundary too
+ int aligned_h = FFALIGN(ctx->image_height, 2);
+ if (x11->display_is_local && XShmQueryExtension(x11->display)) {
+ ctx->Shmem_Flag = 1;
+ x11->ShmCompletionEvent = XShmGetEventBase(x11->display)
+ + ShmCompletion;
+ } else {
+ ctx->Shmem_Flag = 0;
+ MP_INFO(vo, "Shared memory not supported\nReverting to normal Xv.\n");
+ }
+ if (ctx->Shmem_Flag) {
+ ctx->xvimage[foo] =
+ (XvImage *) XvShmCreateImage(x11->display, ctx->xv_port,
+ ctx->xv_format, NULL,
+ aligned_w, aligned_h,
+ &ctx->Shminfo[foo]);
+ if (!ctx->xvimage[foo])
+ return false;
+
+ ctx->Shminfo[foo].shmid = shmget(IPC_PRIVATE,
+ ctx->xvimage[foo]->data_size,
+ IPC_CREAT | 0777);
+ ctx->Shminfo[foo].shmaddr = shmat(ctx->Shminfo[foo].shmid, 0, 0);
+ if (ctx->Shminfo[foo].shmaddr == (void *)-1)
+ return false;
+ ctx->Shminfo[foo].readOnly = False;
+
+ ctx->xvimage[foo]->data = ctx->Shminfo[foo].shmaddr;
+ XShmAttach(x11->display, &ctx->Shminfo[foo]);
+ XSync(x11->display, False);
+ shmctl(ctx->Shminfo[foo].shmid, IPC_RMID, 0);
+ } else {
+ ctx->xvimage[foo] =
+ (XvImage *) XvCreateImage(x11->display, ctx->xv_port,
+ ctx->xv_format, NULL, aligned_w,
+ aligned_h);
+ if (!ctx->xvimage[foo])
+ return false;
+ ctx->xvimage[foo]->data = av_malloc(ctx->xvimage[foo]->data_size);
+ if (!ctx->xvimage[foo]->data)
+ return false;
+ XSync(x11->display, False);
+ }
+
+ if ((ctx->xvimage[foo]->width < aligned_w) ||
+ (ctx->xvimage[foo]->height < aligned_h)) {
+ MP_ERR(vo, "Got XvImage with too small size: %ux%u (expected %ux%u)\n",
+ ctx->xvimage[foo]->width, ctx->xvimage[foo]->height,
+ aligned_w, ctx->image_height);
+ return false;
+ }
+
+ struct mp_image img = get_xv_buffer(vo, foo);
+ mp_image_set_size(&img, aligned_w, aligned_h);
+ mp_image_clear(&img, 0, 0, img.w, img.h);
+ return true;
+}
+
+static void deallocate_xvimage(struct vo *vo, int foo)
+{
+ struct xvctx *ctx = vo->priv;
+ if (ctx->Shmem_Flag) {
+ XShmDetach(vo->x11->display, &ctx->Shminfo[foo]);
+ shmdt(ctx->Shminfo[foo].shmaddr);
+ } else {
+ av_free(ctx->xvimage[foo]->data);
+ }
+ if (ctx->xvimage[foo])
+ XFree(ctx->xvimage[foo]);
+
+ ctx->xvimage[foo] = NULL;
+ ctx->Shminfo[foo] = (XShmSegmentInfo){0};
+
+ XSync(vo->x11->display, False);
+ return;
+}
+
+static inline void put_xvimage(struct vo *vo, XvImage *xvi)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_rect *src = &ctx->src_rect;
+ struct mp_rect *dst = &ctx->dst_rect;
+ int dw = dst->x1 - dst->x0, dh = dst->y1 - dst->y0;
+ int sw = src->x1 - src->x0, sh = src->y1 - src->y0;
+
+ if (ctx->Shmem_Flag) {
+ XvShmPutImage(x11->display, ctx->xv_port, x11->window, ctx->vo_gc, xvi,
+ src->x0, src->y0, sw, sh,
+ dst->x0, dst->y0, dw, dh,
+ True);
+ x11->ShmCompletionWaitCount++;
+ } else {
+ XvPutImage(x11->display, ctx->xv_port, x11->window, ctx->vo_gc, xvi,
+ src->x0, src->y0, sw, sh,
+ dst->x0, dst->y0, dw, dh);
+ }
+}
+
+static struct mp_image get_xv_buffer(struct vo *vo, int buf_index)
+{
+ struct xvctx *ctx = vo->priv;
+ XvImage *xv_image = ctx->xvimage[buf_index];
+
+ struct mp_image img = {0};
+ mp_image_set_size(&img, ctx->image_width, ctx->image_height);
+ mp_image_setfmt(&img, ctx->image_format);
+
+ bool swapuv = ctx->xv_format == MP_FOURCC_YV12;
+ for (int n = 0; n < img.num_planes; n++) {
+ int sn = n > 0 && swapuv ? (n == 1 ? 2 : 1) : n;
+ img.planes[n] = xv_image->data + xv_image->offsets[sn];
+ img.stride[n] = xv_image->pitches[sn];
+ }
+
+ if (vo->params) {
+ struct mp_image_params params = *vo->params;
+ if (ctx->cached_csp)
+ params.color.space = ctx->cached_csp;
+ mp_image_set_attributes(&img, &params);
+ }
+
+ return img;
+}
+
+static void wait_for_completion(struct vo *vo, int max_outstanding)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ if (ctx->Shmem_Flag) {
+ while (x11->ShmCompletionWaitCount > max_outstanding) {
+ if (!ctx->Shm_Warned_Slow) {
+ MP_WARN(vo, "X11 can't keep up! Waiting"
+ " for XShm completion events...\n");
+ ctx->Shm_Warned_Slow = 1;
+ }
+ mp_sleep_ns(MP_TIME_MS_TO_NS(1));
+ vo_x11_check_events(vo);
+ }
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ put_xvimage(vo, ctx->xvimage[ctx->current_buf]);
+
+ /* remember the currently visible buffer */
+ ctx->current_buf = (ctx->current_buf + 1) % ctx->num_buffers;
+
+ if (!ctx->Shmem_Flag)
+ XSync(vo->x11->display, False);
+
+ if (vo->x11->use_present) {
+ vo_x11_present(vo);
+ present_sync_swap(vo->x11->present);
+ }
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct xvctx *ctx = vo->priv;
+
+ wait_for_completion(vo, ctx->num_buffers - 1);
+ bool render = vo_x11_check_visible(vo);
+ if (!render)
+ return;
+
+ struct mp_image xv_buffer = get_xv_buffer(vo, ctx->current_buf);
+ if (frame->current) {
+ mp_image_copy(&xv_buffer, frame->current);
+ } else {
+ mp_image_clear(&xv_buffer, 0, 0, xv_buffer.w, xv_buffer.h);
+ }
+
+ struct mp_osd_res res = osd_res_from_image_params(vo->params);
+ osd_draw_on_image(vo->osd, res, frame->current ? frame->current->pts : 0, 0, &xv_buffer);
+
+ if (frame->current != ctx->original_image)
+ ctx->original_image = frame->current;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct xvctx *ctx = vo->priv;
+ uint32_t i;
+
+ int fourcc = find_xv_format(format);
+ if (fourcc) {
+ for (i = 0; i < ctx->formats; i++) {
+ if (ctx->fo[i].id == fourcc)
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ int i;
+
+ if (ctx->ai)
+ XvFreeAdaptorInfo(ctx->ai);
+ ctx->ai = NULL;
+ if (ctx->fo) {
+ XFree(ctx->fo);
+ ctx->fo = NULL;
+ }
+ for (i = 0; i < ctx->num_buffers; i++)
+ deallocate_xvimage(vo, i);
+ if (ctx->f_gc != None)
+ XFreeGC(vo->x11->display, ctx->f_gc);
+ if (ctx->vo_gc != None)
+ XFreeGC(vo->x11->display, ctx->vo_gc);
+ // uninit() shouldn't get called unless initialization went past vo_init()
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ XvPortID xv_p;
+ int busy_ports = 0;
+ unsigned int i;
+ struct xvctx *ctx = vo->priv;
+ int xv_adaptor = ctx->cfg_xv_adaptor;
+
+ if (!vo_x11_init(vo))
+ return -1;
+
+ if (!vo_x11_create_vo_window(vo, NULL, "xv"))
+ goto error;
+
+ struct vo_x11_state *x11 = vo->x11;
+
+ /* check for Xvideo extension */
+ unsigned int ver, rel, req, ev, err;
+ if (Success != XvQueryExtension(x11->display, &ver, &rel, &req, &ev, &err)) {
+ MP_ERR(vo, "Xv not supported by this X11 version/driver\n");
+ goto error;
+ }
+
+ /* check for Xvideo support */
+ if (Success !=
+ XvQueryAdaptors(x11->display, DefaultRootWindow(x11->display),
+ &ctx->adaptors, &ctx->ai)) {
+ MP_ERR(vo, "XvQueryAdaptors failed.\n");
+ goto error;
+ }
+
+ /* check adaptors */
+ if (ctx->xv_port) {
+ int port_found;
+
+ for (port_found = 0, i = 0; !port_found && i < ctx->adaptors; i++) {
+ if ((ctx->ai[i].type & XvInputMask)
+ && (ctx->ai[i].type & XvImageMask)) {
+ for (xv_p = ctx->ai[i].base_id;
+ xv_p < ctx->ai[i].base_id + ctx->ai[i].num_ports;
+ ++xv_p) {
+ if (xv_p == ctx->xv_port) {
+ port_found = 1;
+ break;
+ }
+ }
+ }
+ }
+ if (port_found) {
+ if (XvGrabPort(x11->display, ctx->xv_port, CurrentTime))
+ ctx->xv_port = 0;
+ } else {
+ MP_WARN(vo, "Invalid port parameter, overriding with port 0.\n");
+ ctx->xv_port = 0;
+ }
+ }
+
+ for (i = 0; i < ctx->adaptors && ctx->xv_port == 0; i++) {
+ /* check if adaptor number has been specified */
+ if (xv_adaptor != -1 && xv_adaptor != i)
+ continue;
+
+ if ((ctx->ai[i].type & XvInputMask) && (ctx->ai[i].type & XvImageMask)) {
+ for (xv_p = ctx->ai[i].base_id;
+ xv_p < ctx->ai[i].base_id + ctx->ai[i].num_ports; ++xv_p)
+ if (!XvGrabPort(x11->display, xv_p, CurrentTime)) {
+ ctx->xv_port = xv_p;
+ MP_VERBOSE(vo, "Using Xv Adapter #%d (%s)\n",
+ i, ctx->ai[i].name);
+ break;
+ } else {
+ MP_WARN(vo, "Could not grab port %i.\n", (int) xv_p);
+ ++busy_ports;
+ }
+ }
+ }
+ if (!ctx->xv_port) {
+ if (busy_ports)
+ MP_ERR(vo, "Xvideo ports busy.\n");
+ else
+ MP_ERR(vo, "No Xvideo support found.\n");
+ goto error;
+ }
+
+ if (!xv_init_colorkey(vo)) {
+ goto error; // bail out, colorkey setup failed
+ }
+ xv_enable_vsync(vo);
+ xv_get_max_img_dim(vo, &ctx->max_width, &ctx->max_height);
+
+ ctx->fo = XvListImageFormats(x11->display, ctx->xv_port,
+ (int *) &ctx->formats);
+
+ MP_WARN(vo, "Warning: this legacy VO has bad quality and performance, "
+ "and will in particular result in blurry OSD and subtitles. "
+ "You should fix your graphics drivers, or not force the xv VO.\n");
+ return 0;
+
+ error:
+ uninit(vo); // free resources
+ return -1;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ }
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+ if (events & (VO_EVENT_EXPOSE | VO_EVENT_RESIZE))
+ resize(vo);
+ vo_event(vo, events);
+ return r;
+}
+
+#define OPT_BASE_STRUCT struct xvctx
+
+const struct vo_driver video_out_xv = {
+ .description = "X11/Xv",
+ .name = "xv",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct xvctx),
+ .priv_defaults = &(const struct xvctx) {
+ .cfg_xv_adaptor = -1,
+ .xv_ck_info = {CK_METHOD_MANUALFILL, CK_SRC_CUR},
+ .colorkey = 0x0000ff00, // default colorkey is green
+ // (0xff000000 means that colorkey has been disabled)
+ .cfg_buffers = 2,
+ },
+ .options = (const struct m_option[]) {
+ {"port", OPT_INT(xv_port), M_RANGE(0, DBL_MAX)},
+ {"adaptor", OPT_INT(cfg_xv_adaptor), M_RANGE(-1, DBL_MAX)},
+ {"ck", OPT_CHOICE(xv_ck_info.source,
+ {"use", CK_SRC_USE},
+ {"set", CK_SRC_SET},
+ {"cur", CK_SRC_CUR})},
+ {"ck-method", OPT_CHOICE(xv_ck_info.method,
+ {"none", CK_METHOD_NONE},
+ {"bg", CK_METHOD_BACKGROUND},
+ {"man", CK_METHOD_MANUALFILL},
+ {"auto", CK_METHOD_AUTOPAINT})},
+ {"colorkey", OPT_INT(colorkey)},
+ {"buffers", OPT_INT(cfg_buffers), M_RANGE(1, MAX_BUFFERS)},
+ {0}
+ },
+ .options_prefix = "xv",
+};
diff --git a/video/out/vulkan/common.h b/video/out/vulkan/common.h
new file mode 100644
index 0000000..d006942
--- /dev/null
+++ b/video/out/vulkan/common.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+
+// We need to define all platforms we want to support. Since we have
+// our own mechanism for checking this, we re-define the right symbols
+#if HAVE_WAYLAND
+#define VK_USE_PLATFORM_WAYLAND_KHR
+#endif
+#if HAVE_X11
+#define VK_USE_PLATFORM_XLIB_KHR
+#endif
+#if HAVE_WIN32_DESKTOP
+#define VK_USE_PLATFORM_WIN32_KHR
+#endif
+#if HAVE_COCOA
+#define VK_USE_PLATFORM_MACOS_MVK
+#define VK_USE_PLATFORM_METAL_EXT
+#endif
+
+#include <libplacebo/vulkan.h>
+
+// Shared struct used to hold vulkan context information
+struct mpvk_ctx {
+ pl_log pllog;
+ pl_vk_inst vkinst;
+ pl_vulkan vulkan;
+ pl_gpu gpu; // points to vulkan->gpu for convenience
+ pl_swapchain swapchain;
+ VkSurfaceKHR surface;
+};
diff --git a/video/out/vulkan/context.c b/video/out/vulkan/context.c
new file mode 100644
index 0000000..5087403
--- /dev/null
+++ b/video/out/vulkan/context.c
@@ -0,0 +1,372 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#if HAVE_LAVU_UUID
+#include <libavutil/uuid.h>
+#else
+#include "misc/uuid.h"
+#endif
+
+#include "options/m_config.h"
+#include "video/out/placebo/ra_pl.h"
+
+#include "context.h"
+#include "utils.h"
+
+struct vulkan_opts {
+ char *device; // force a specific GPU
+ int swap_mode;
+ int queue_count;
+ bool async_transfer;
+ bool async_compute;
+};
+
+static int vk_validate_dev(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ int ret = M_OPT_INVALID;
+ VkResult res;
+
+ // Create a dummy instance to validate/list the devices
+ VkInstanceCreateInfo info = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .pApplicationInfo = &(VkApplicationInfo) {
+ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ .apiVersion = VK_API_VERSION_1_1,
+ }
+ };
+
+ VkInstance inst;
+ VkPhysicalDevice *devices = NULL;
+ uint32_t num = 0;
+
+ res = vkCreateInstance(&info, NULL, &inst);
+ if (res != VK_SUCCESS)
+ goto done;
+
+ res = vkEnumeratePhysicalDevices(inst, &num, NULL);
+ if (res != VK_SUCCESS)
+ goto done;
+
+ devices = talloc_array(NULL, VkPhysicalDevice, num);
+ res = vkEnumeratePhysicalDevices(inst, &num, devices);
+ if (res != VK_SUCCESS)
+ goto done;
+
+ bool help = bstr_equals0(param, "help");
+ if (help) {
+ mp_info(log, "Available vulkan devices:\n");
+ ret = M_OPT_EXIT;
+ }
+
+ AVUUID param_uuid;
+ bool is_uuid = av_uuid_parse(*value, param_uuid) == 0;
+
+ for (int i = 0; i < num; i++) {
+ VkPhysicalDeviceIDPropertiesKHR id_prop = { 0 };
+ id_prop.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES_KHR;
+
+ VkPhysicalDeviceProperties2KHR prop2 = { 0 };
+ prop2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2_KHR;
+ prop2.pNext = &id_prop;
+
+ vkGetPhysicalDeviceProperties2(devices[i], &prop2);
+
+ const VkPhysicalDeviceProperties *prop = &prop2.properties;
+
+ if (help) {
+ char device_uuid[37];
+ av_uuid_unparse(id_prop.deviceUUID, device_uuid);
+ mp_info(log, " '%s' (GPU %d, PCI ID %x:%x, UUID %s)\n",
+ prop->deviceName, i, (unsigned)prop->vendorID,
+ (unsigned)prop->deviceID, device_uuid);
+ } else if (bstr_equals0(param, prop->deviceName)) {
+ ret = 0;
+ goto done;
+ } else if (is_uuid && av_uuid_equal(param_uuid, id_prop.deviceUUID)) {
+ ret = 0;
+ goto done;
+ }
+ }
+
+ if (!help)
+ mp_err(log, "No device with %s '%.*s'!\n", is_uuid ? "UUID" : "name",
+ BSTR_P(param));
+
+done:
+ talloc_free(devices);
+ return ret;
+}
+
+#define OPT_BASE_STRUCT struct vulkan_opts
+const struct m_sub_options vulkan_conf = {
+ .opts = (const struct m_option[]) {
+ {"vulkan-device", OPT_STRING_VALIDATE(device, vk_validate_dev)},
+ {"vulkan-swap-mode", OPT_CHOICE(swap_mode,
+ {"auto", -1},
+ {"fifo", VK_PRESENT_MODE_FIFO_KHR},
+ {"fifo-relaxed", VK_PRESENT_MODE_FIFO_RELAXED_KHR},
+ {"mailbox", VK_PRESENT_MODE_MAILBOX_KHR},
+ {"immediate", VK_PRESENT_MODE_IMMEDIATE_KHR})},
+ {"vulkan-queue-count", OPT_INT(queue_count), M_RANGE(1, 8)},
+ {"vulkan-async-transfer", OPT_BOOL(async_transfer)},
+ {"vulkan-async-compute", OPT_BOOL(async_compute)},
+ {"vulkan-disable-events", OPT_REMOVED("Unused")},
+ {0}
+ },
+ .size = sizeof(struct vulkan_opts),
+ .defaults = &(struct vulkan_opts) {
+ .swap_mode = -1,
+ .queue_count = 1,
+ .async_transfer = true,
+ .async_compute = true,
+ },
+};
+
+struct priv {
+ struct mpvk_ctx *vk;
+ struct vulkan_opts *opts;
+ struct ra_vk_ctx_params params;
+ struct ra_tex proxy_tex;
+};
+
+static const struct ra_swapchain_fns vulkan_swapchain;
+
+struct mpvk_ctx *ra_vk_ctx_get(struct ra_ctx *ctx)
+{
+ if (!ctx->swapchain || ctx->swapchain->fns != &vulkan_swapchain)
+ return NULL;
+
+ struct priv *p = ctx->swapchain->priv;
+ return p->vk;
+}
+
+void ra_vk_ctx_uninit(struct ra_ctx *ctx)
+{
+ if (!ctx->swapchain)
+ return;
+
+ struct priv *p = ctx->swapchain->priv;
+ struct mpvk_ctx *vk = p->vk;
+
+ if (ctx->ra) {
+ pl_gpu_finish(vk->gpu);
+ pl_swapchain_destroy(&vk->swapchain);
+ ctx->ra->fns->destroy(ctx->ra);
+ ctx->ra = NULL;
+ }
+
+ vk->gpu = NULL;
+ pl_vulkan_destroy(&vk->vulkan);
+ TA_FREEP(&ctx->swapchain);
+}
+
+bool ra_vk_ctx_init(struct ra_ctx *ctx, struct mpvk_ctx *vk,
+ struct ra_vk_ctx_params params,
+ VkPresentModeKHR preferred_mode)
+{
+ struct ra_swapchain *sw = ctx->swapchain = talloc_zero(NULL, struct ra_swapchain);
+ sw->ctx = ctx;
+ sw->fns = &vulkan_swapchain;
+
+ struct priv *p = sw->priv = talloc_zero(sw, struct priv);
+ p->vk = vk;
+ p->params = params;
+ p->opts = mp_get_config_group(p, ctx->global, &vulkan_conf);
+
+ VkPhysicalDeviceFeatures2 features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
+ };
+
+#if HAVE_VULKAN_INTEROP
+ /*
+ * Request the additional extensions and features required to make full use
+ * of the ffmpeg Vulkan hwcontext and video decoding capability.
+ */
+ const char *opt_extensions[] = {
+ VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME,
+ VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME,
+ VK_KHR_VIDEO_DECODE_QUEUE_EXTENSION_NAME,
+ VK_KHR_VIDEO_DECODE_H264_EXTENSION_NAME,
+ VK_KHR_VIDEO_DECODE_H265_EXTENSION_NAME,
+ VK_KHR_VIDEO_QUEUE_EXTENSION_NAME,
+ // This is a literal string as it's not in the official headers yet.
+ "VK_MESA_video_decode_av1",
+ };
+
+ VkPhysicalDeviceDescriptorBufferFeaturesEXT descriptor_buffer_feature = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT,
+ .pNext = NULL,
+ .descriptorBuffer = true,
+ .descriptorBufferPushDescriptors = true,
+ };
+
+ VkPhysicalDeviceShaderAtomicFloatFeaturesEXT atomic_float_feature = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_FLOAT_FEATURES_EXT,
+ .pNext = &descriptor_buffer_feature,
+ .shaderBufferFloat32Atomics = true,
+ .shaderBufferFloat32AtomicAdd = true,
+ };
+
+ features.pNext = &atomic_float_feature;
+#endif
+
+ AVUUID param_uuid = { 0 };
+ bool is_uuid = p->opts->device &&
+ av_uuid_parse(p->opts->device, param_uuid) == 0;
+
+ assert(vk->pllog);
+ assert(vk->vkinst);
+ struct pl_vulkan_params device_params = {
+ .instance = vk->vkinst->instance,
+ .get_proc_addr = vk->vkinst->get_proc_addr,
+ .surface = vk->surface,
+ .async_transfer = p->opts->async_transfer,
+ .async_compute = p->opts->async_compute,
+ .queue_count = p->opts->queue_count,
+#if HAVE_VULKAN_INTEROP
+ .extra_queues = VK_QUEUE_VIDEO_DECODE_BIT_KHR,
+ .opt_extensions = opt_extensions,
+ .num_opt_extensions = MP_ARRAY_SIZE(opt_extensions),
+#endif
+ .features = &features,
+ .device_name = is_uuid ? NULL : p->opts->device,
+ };
+ if (is_uuid)
+ av_uuid_copy(device_params.device_uuid, param_uuid);
+
+ vk->vulkan = pl_vulkan_create(vk->pllog, &device_params);
+ if (!vk->vulkan)
+ goto error;
+
+ vk->gpu = vk->vulkan->gpu;
+ ctx->ra = ra_create_pl(vk->gpu, ctx->log);
+ if (!ctx->ra)
+ goto error;
+
+ // Create the swapchain
+ struct pl_vulkan_swapchain_params pl_params = {
+ .surface = vk->surface,
+ .present_mode = preferred_mode,
+ .swapchain_depth = ctx->vo->opts->swapchain_depth,
+ // mpv already handles resize events, so gracefully allow suboptimal
+ // swapchains to exist in order to make resizing even smoother
+ .allow_suboptimal = true,
+ };
+
+ if (p->opts->swap_mode >= 0) // user override
+ pl_params.present_mode = p->opts->swap_mode;
+
+ vk->swapchain = pl_vulkan_create_swapchain(vk->vulkan, &pl_params);
+ if (!vk->swapchain)
+ goto error;
+
+ return true;
+
+error:
+ ra_vk_ctx_uninit(ctx);
+ return false;
+}
+
+bool ra_vk_ctx_resize(struct ra_ctx *ctx, int width, int height)
+{
+ struct priv *p = ctx->swapchain->priv;
+
+ bool ok = pl_swapchain_resize(p->vk->swapchain, &width, &height);
+ ctx->vo->dwidth = width;
+ ctx->vo->dheight = height;
+
+ return ok;
+}
+
+char *ra_vk_ctx_get_device_name(struct ra_ctx *ctx)
+{
+ /*
+ * This implementation is a bit odd because it has to work even if the
+ * ctx hasn't been initialised yet. A context implementation may need access
+ * to the device name before it can fully initialise the ctx.
+ */
+ struct vulkan_opts *opts = mp_get_config_group(NULL, ctx->global,
+ &vulkan_conf);
+ char *device_name = talloc_strdup(NULL, opts->device);
+ talloc_free(opts);
+ return device_name;
+}
+
+static int color_depth(struct ra_swapchain *sw)
+{
+ return 0; // TODO: implement this somehow?
+}
+
+static bool start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct priv *p = sw->priv;
+ struct pl_swapchain_frame frame;
+
+ bool visible = true;
+ if (p->params.check_visible)
+ visible = p->params.check_visible(sw->ctx);
+
+ // If out_fbo is NULL, this was called from vo_gpu_next. Bail out.
+ if (out_fbo == NULL || !visible)
+ return visible;
+
+ if (!pl_swapchain_start_frame(p->vk->swapchain, &frame))
+ return false;
+ if (!mppl_wrap_tex(sw->ctx->ra, frame.fbo, &p->proxy_tex))
+ return false;
+
+ *out_fbo = (struct ra_fbo) {
+ .tex = &p->proxy_tex,
+ .flip = frame.flipped,
+ };
+
+ return true;
+}
+
+static bool submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame)
+{
+ struct priv *p = sw->priv;
+ return pl_swapchain_submit_frame(p->vk->swapchain);
+}
+
+static void swap_buffers(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ pl_swapchain_swap_buffers(p->vk->swapchain);
+ if (p->params.swap_buffers)
+ p->params.swap_buffers(sw->ctx);
+}
+
+static void get_vsync(struct ra_swapchain *sw,
+ struct vo_vsync_info *info)
+{
+ struct priv *p = sw->priv;
+ if (p->params.get_vsync)
+ p->params.get_vsync(sw->ctx, info);
+}
+
+static const struct ra_swapchain_fns vulkan_swapchain = {
+ .color_depth = color_depth,
+ .start_frame = start_frame,
+ .submit_frame = submit_frame,
+ .swap_buffers = swap_buffers,
+ .get_vsync = get_vsync,
+};
diff --git a/video/out/vulkan/context.h b/video/out/vulkan/context.h
new file mode 100644
index 0000000..c846942
--- /dev/null
+++ b/video/out/vulkan/context.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "video/out/gpu/context.h"
+#include "common.h"
+
+struct ra_vk_ctx_params {
+ // See ra_swapchain_fns.get_vsync.
+ void (*get_vsync)(struct ra_ctx *ctx, struct vo_vsync_info *info);
+
+ // For special contexts (i.e. wayland) that want to check visibility
+ // before drawing a frame.
+ bool (*check_visible)(struct ra_ctx *ctx);
+
+ // In case something special needs to be done on the buffer swap.
+ void (*swap_buffers)(struct ra_ctx *ctx);
+};
+
+// Helpers for ra_ctx based on ra_vk. These initialize ctx->ra and ctx->swchain.
+void ra_vk_ctx_uninit(struct ra_ctx *ctx);
+bool ra_vk_ctx_init(struct ra_ctx *ctx, struct mpvk_ctx *vk,
+ struct ra_vk_ctx_params params,
+ VkPresentModeKHR preferred_mode);
+
+// Handles a resize request, and updates ctx->vo->dwidth/dheight
+bool ra_vk_ctx_resize(struct ra_ctx *ctx, int width, int height);
+
+// May be called on a ra_ctx of any type.
+struct mpvk_ctx *ra_vk_ctx_get(struct ra_ctx *ctx);
+
+// Get the user requested Vulkan device name.
+char *ra_vk_ctx_get_device_name(struct ra_ctx *ctx);
diff --git a/video/out/vulkan/context_android.c b/video/out/vulkan/context_android.c
new file mode 100644
index 0000000..ddab391
--- /dev/null
+++ b/video/out/vulkan/context_android.c
@@ -0,0 +1,96 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <vulkan/vulkan.h>
+#include <vulkan/vulkan_android.h>
+
+#include "video/out/android_common.h"
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static void android_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+
+ vo_android_uninit(ctx->vo);
+}
+
+static bool android_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!vo_android_init(ctx->vo))
+ goto fail;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME))
+ goto fail;
+
+ VkAndroidSurfaceCreateInfoKHR info = {
+ .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
+ .window = vo_android_native_window(ctx->vo)
+ };
+
+ struct ra_vk_ctx_params params = {0};
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateAndroidSurfaceKHR(inst, &info, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Android surface\n");
+ goto fail;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto fail;
+
+ return true;
+fail:
+ android_uninit(ctx);
+ return false;
+}
+
+static bool android_reconfig(struct ra_ctx *ctx)
+{
+ int w, h;
+ if (!vo_android_surface_size(ctx->vo, &w, &h))
+ return false;
+
+ ra_vk_ctx_resize(ctx, w, h);
+ return true;
+}
+
+static int android_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ return VO_NOTIMPL;
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_android = {
+ .type = "vulkan",
+ .name = "androidvk",
+ .reconfig = android_reconfig,
+ .control = android_control,
+ .init = android_init,
+ .uninit = android_uninit,
+};
diff --git a/video/out/vulkan/context_display.c b/video/out/vulkan/context_display.c
new file mode 100644
index 0000000..84cef1e
--- /dev/null
+++ b/video/out/vulkan/context_display.c
@@ -0,0 +1,491 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "context.h"
+#include "options/m_config.h"
+#include "utils.h"
+
+#if HAVE_DRM
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "libmpv/render_gl.h"
+#include "video/out/drm_common.h"
+#endif
+
+struct vulkan_display_opts {
+ int display;
+ int mode;
+ int plane;
+};
+
+struct mode_selector {
+ // Indexes of selected display/mode/plane.
+ int display_idx;
+ int mode_idx;
+ int plane_idx;
+
+ // Must be freed with talloc_free
+ VkDisplayModePropertiesKHR *out_mode_props;
+};
+
+/**
+ * If a selector is passed, verify that it is valid and return the matching
+ * mode properties. If null is passed, walk all modes and print them out.
+ */
+static bool walk_display_properties(struct mp_log *log,
+ int msgl_err,
+ VkPhysicalDevice device,
+ struct mode_selector *selector) {
+ bool ret = false;
+ VkResult res;
+
+ int msgl_info = selector ? MSGL_TRACE : MSGL_INFO;
+
+ // Use a dummy as parent for all other allocations.
+ void *tmp = talloc_new(NULL);
+
+ VkPhysicalDeviceProperties prop;
+ vkGetPhysicalDeviceProperties(device, &prop);
+ mp_msg(log, msgl_info, " '%s' (GPU ID %x:%x)\n", prop.deviceName,
+ (unsigned)prop.vendorID, (unsigned)prop.deviceID);
+
+ // Count displays. This must be done before enumerating planes with the
+ // Intel driver, or it will not enumerate any planes. WTF.
+ int num_displays = 0;
+ vkGetPhysicalDeviceDisplayPropertiesKHR(device, &num_displays, NULL);
+ if (!num_displays) {
+ mp_msg(log, msgl_info, " No available displays for device.\n");
+ goto done;
+ }
+ if (selector && selector->display_idx + 1 > num_displays) {
+ mp_msg(log, msgl_err, "Selected display (%d) not present.\n",
+ selector->display_idx);
+ goto done;
+ }
+
+ // Enumerate Planes
+ int num_planes = 0;
+ vkGetPhysicalDeviceDisplayPlanePropertiesKHR(device, &num_planes, NULL);
+ if (!num_planes) {
+ mp_msg(log, msgl_info, " No available planes for device.\n");
+ goto done;
+ }
+ if (selector && selector->plane_idx + 1 > num_planes) {
+ mp_msg(log, msgl_err, "Selected plane (%d) not present.\n",
+ selector->plane_idx);
+ goto done;
+ }
+
+ VkDisplayPlanePropertiesKHR *planes =
+ talloc_array(tmp, VkDisplayPlanePropertiesKHR, num_planes);
+ res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(device, &num_planes,
+ planes);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating planes\n");
+ goto done;
+ }
+
+ // Allocate zeroed arrays so that planes with no displays have a null entry.
+ VkDisplayKHR **planes_to_displays =
+ talloc_zero_array(tmp, VkDisplayKHR *, num_planes);
+ for (int j = 0; j < num_planes; j++) {
+ int num_displays_for_plane = 0;
+ vkGetDisplayPlaneSupportedDisplaysKHR(device, j,
+ &num_displays_for_plane, NULL);
+ if (!num_displays_for_plane)
+ continue;
+
+ // Null terminated array
+ VkDisplayKHR *displays =
+ talloc_zero_array(planes_to_displays, VkDisplayKHR,
+ num_displays_for_plane + 1);
+ res = vkGetDisplayPlaneSupportedDisplaysKHR(device, j,
+ &num_displays_for_plane,
+ displays);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating plane displays\n");
+ continue;
+ }
+ planes_to_displays[j] = displays;
+ }
+
+ // Enumerate Displays and Modes
+ VkDisplayPropertiesKHR *props =
+ talloc_array(tmp, VkDisplayPropertiesKHR, num_displays);
+ res = vkGetPhysicalDeviceDisplayPropertiesKHR(device, &num_displays, props);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating display properties\n");
+ goto done;
+ }
+
+ for (int j = 0; j < num_displays; j++) {
+ if (selector && selector->display_idx != j)
+ continue;
+
+ mp_msg(log, msgl_info, " Display %d: '%s' (%dx%d)\n",
+ j,
+ props[j].displayName,
+ props[j].physicalResolution.width,
+ props[j].physicalResolution.height);
+
+ VkDisplayKHR display = props[j].display;
+
+ mp_msg(log, msgl_info, " Modes:\n");
+
+ int num_modes = 0;
+ vkGetDisplayModePropertiesKHR(device, display, &num_modes, NULL);
+ if (!num_modes) {
+ mp_msg(log, msgl_info, " No available modes for display.\n");
+ continue;
+ }
+ if (selector && selector->mode_idx + 1 > num_modes) {
+ mp_msg(log, msgl_err, "Selected mode (%d) not present.\n",
+ selector->mode_idx);
+ goto done;
+ }
+
+ VkDisplayModePropertiesKHR *modes =
+ talloc_array(tmp, VkDisplayModePropertiesKHR, num_modes);
+ res = vkGetDisplayModePropertiesKHR(device, display, &num_modes, modes);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating display modes\n");
+ continue;
+ }
+
+ for (int k = 0; k < num_modes; k++) {
+ if (selector && selector->mode_idx != k)
+ continue;
+
+ mp_msg(log, msgl_info, " Mode %02d: %dx%d (%02d.%03d Hz)\n", k,
+ modes[k].parameters.visibleRegion.width,
+ modes[k].parameters.visibleRegion.height,
+ modes[k].parameters.refreshRate / 1000,
+ modes[k].parameters.refreshRate % 1000);
+
+ if (selector)
+ selector->out_mode_props = talloc_dup(NULL, &modes[k]);
+ }
+
+ int found_plane = -1;
+ mp_msg(log, msgl_info, " Planes:\n");
+ for (int k = 0; k < num_planes; k++) {
+ VkDisplayKHR *displays = planes_to_displays[k];
+ if (!displays) {
+ // This plane is not connected to any displays.
+ continue;
+ }
+ for (int d = 0; displays[d]; d++) {
+ if (displays[d] == display) {
+ if (selector && selector->plane_idx != k)
+ continue;
+
+ mp_msg(log, msgl_info, " Plane: %d\n", k);
+ found_plane = k;
+ }
+ }
+ }
+ if (selector && selector->plane_idx != found_plane) {
+ mp_msg(log, msgl_err,
+ "Selected plane (%d) not available on selected display.\n",
+ selector->plane_idx);
+ goto done;
+ }
+ }
+ ret = true;
+done:
+ talloc_free(tmp);
+ return ret;
+}
+
+static int print_display_info(struct mp_log *log, const struct m_option *opt,
+ struct bstr name) {
+ VkResult res;
+ VkPhysicalDevice *devices = NULL;
+
+ // Create a dummy instance to list the resources
+ VkInstanceCreateInfo info = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .enabledExtensionCount = 1,
+ .ppEnabledExtensionNames = (const char*[]) {
+ VK_KHR_DISPLAY_EXTENSION_NAME
+ },
+ };
+
+ VkInstance inst = NULL;
+ res = vkCreateInstance(&info, NULL, &inst);
+ if (res != VK_SUCCESS) {
+ mp_warn(log, "Unable to create Vulkan instance.\n");
+ goto done;
+ }
+
+ uint32_t num_devices = 0;
+ vkEnumeratePhysicalDevices(inst, &num_devices, NULL);
+ if (!num_devices) {
+ mp_info(log, "No Vulkan devices detected.\n");
+ goto done;
+ }
+
+ devices = talloc_array(NULL, VkPhysicalDevice, num_devices);
+ vkEnumeratePhysicalDevices(inst, &num_devices, devices);
+ if (res != VK_SUCCESS) {
+ mp_warn(log, "Failed enumerating physical devices.\n");
+ goto done;
+ }
+
+ mp_info(log, "Vulkan Devices:\n");
+ for (int i = 0; i < num_devices; i++) {
+ walk_display_properties(log, MSGL_WARN, devices[i], NULL);
+ }
+
+done:
+ talloc_free(devices);
+ vkDestroyInstance(inst, NULL);
+ return M_OPT_EXIT;
+}
+
+#define OPT_BASE_STRUCT struct vulkan_display_opts
+const struct m_sub_options vulkan_display_conf = {
+ .opts = (const struct m_option[]) {
+ {"vulkan-display-display", OPT_INT(display),
+ .help = print_display_info,
+ },
+ {"vulkan-display-mode", OPT_INT(mode),
+ .help = print_display_info,
+ },
+ {"vulkan-display-plane", OPT_INT(plane),
+ .help = print_display_info,
+ },
+ {0}
+ },
+ .size = sizeof(struct vulkan_display_opts),
+ .defaults = &(struct vulkan_display_opts) {0},
+};
+
+struct priv {
+ struct mpvk_ctx vk;
+ struct vulkan_display_opts *opts;
+ uint32_t width;
+ uint32_t height;
+
+#if HAVE_DRM
+ struct mpv_opengl_drm_params_v2 drm_params;
+#endif
+};
+
+#if HAVE_DRM
+static void open_render_fd(struct ra_ctx *ctx, const char *render_path)
+{
+ struct priv *p = ctx->priv;
+ p->drm_params.fd = -1;
+ p->drm_params.render_fd = open(render_path, O_RDWR | O_CLOEXEC);
+ if (p->drm_params.render_fd == -1) {
+ MP_WARN(ctx, "Failed to open render node: %s\n",
+ strerror(errno));
+ }
+}
+
+static bool drm_setup(struct ra_ctx *ctx, int display_idx,
+ VkPhysicalDevicePCIBusInfoPropertiesEXT *pci_props)
+{
+ drmDevice *devs[32] = {};
+ int count = drmGetDevices2(0, devs, MP_ARRAY_SIZE(devs));
+ for (int i = 0; i < count; i++) {
+ drmDevice *dev = devs[i];
+
+ if (dev->bustype != DRM_BUS_PCI ||
+ dev->businfo.pci->domain != pci_props->pciDomain ||
+ dev->businfo.pci->bus != pci_props->pciBus ||
+ dev->businfo.pci->dev != pci_props->pciDevice ||
+ dev->businfo.pci->func != pci_props->pciFunction)
+ {
+ continue;
+ }
+
+ // Found our matching device.
+ MP_DBG(ctx, "DRM device found for Vulkan device at %04X:%02X:%02X:%02X\n",
+ pci_props->pciDomain, pci_props->pciBus,
+ pci_props->pciDevice, pci_props->pciFunction);
+
+ if (!(dev->available_nodes & 1 << DRM_NODE_RENDER)) {
+ MP_DBG(ctx, "Card does not have a render node.\n");
+ continue;
+ }
+
+ open_render_fd(ctx, dev->nodes[DRM_NODE_RENDER]);
+
+ break;
+ }
+ drmFreeDevices(devs, MP_ARRAY_SIZE(devs));
+
+ struct priv *p = ctx->priv;
+ if (p->drm_params.render_fd == -1) {
+ MP_WARN(ctx, "Couldn't open DRM render node for Vulkan device "
+ "at: %04X:%02X:%02X:%02X\n",
+ pci_props->pciDomain, pci_props->pciBus,
+ pci_props->pciDevice, pci_props->pciFunction);
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+static void display_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+
+#if HAVE_DRM
+ if (p->drm_params.render_fd != -1) {
+ close(p->drm_params.render_fd);
+ p->drm_params.render_fd = -1;
+ }
+#endif
+}
+
+static bool display_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+ VkResult res;
+ bool ret = false;
+
+ VkDisplayModePropertiesKHR *mode = NULL;
+
+ p->opts = mp_get_config_group(p, ctx->global, &vulkan_display_conf);
+ int display_idx = p->opts->display;
+ int mode_idx = p->opts->mode;
+ int plane_idx = p->opts->plane;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_DISPLAY_EXTENSION_NAME))
+ goto error;
+
+ char *device_name = ra_vk_ctx_get_device_name(ctx);
+ struct pl_vulkan_device_params vulkan_params = {
+ .instance = vk->vkinst->instance,
+ .device_name = device_name,
+ };
+ VkPhysicalDevice device = pl_vulkan_choose_device(vk->pllog, &vulkan_params);
+ talloc_free(device_name);
+ if (!device) {
+ MP_MSG(ctx, msgl, "Failed to open physical device.\n");
+ goto error;
+ }
+
+#if HAVE_DRM
+ VkPhysicalDevicePCIBusInfoPropertiesEXT pci_props = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PCI_BUS_INFO_PROPERTIES_EXT,
+ };
+ VkPhysicalDeviceProperties2KHR props = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2_KHR,
+ .pNext = &pci_props,
+ };
+ vkGetPhysicalDeviceProperties2(device, &props);
+
+ if (!drm_setup(ctx, display_idx, &pci_props))
+ MP_WARN(ctx, "Failed to set up DRM.\n");
+#endif
+
+ struct mode_selector selector = {
+ .display_idx = display_idx,
+ .mode_idx = mode_idx,
+ .plane_idx = plane_idx,
+
+ };
+ if (!walk_display_properties(ctx->log, msgl, device, &selector))
+ goto error;
+ mode = selector.out_mode_props;
+
+ VkDisplaySurfaceCreateInfoKHR xinfo = {
+ .sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR,
+ .displayMode = mode->displayMode,
+ .imageExtent = mode->parameters.visibleRegion,
+ .planeIndex = plane_idx,
+ .transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
+ .alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR,
+ };
+
+ res = vkCreateDisplayPlaneSurfaceKHR(vk->vkinst->instance, &xinfo, NULL,
+ &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Display surface\n");
+ goto error;
+ }
+
+ p->width = mode->parameters.visibleRegion.width;
+ p->height = mode->parameters.visibleRegion.height;
+
+ struct ra_vk_ctx_params params = {0};
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+#if HAVE_DRM
+ if (p->drm_params.render_fd > -1) {
+ ra_add_native_resource(ctx->ra, "drm_params_v2", &p->drm_params);
+ } else {
+ MP_WARN(ctx,
+ "No DRM render fd available. VAAPI hwaccel will not be usable.\n");
+ }
+#endif
+
+ ret = true;
+
+done:
+ talloc_free(mode);
+ return ret;
+
+error:
+ display_uninit(ctx);
+ goto done;
+}
+
+static bool display_reconfig(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ return ra_vk_ctx_resize(ctx, p->width, p->height);
+}
+
+static int display_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ return VO_NOTIMPL;
+}
+
+static void display_wakeup(struct ra_ctx *ctx)
+{
+ // TODO
+}
+
+static void display_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ // TODO
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_display = {
+ .type = "vulkan",
+ .name = "displayvk",
+ .reconfig = display_reconfig,
+ .control = display_control,
+ .wakeup = display_wakeup,
+ .wait_events = display_wait_events,
+ .init = display_init,
+ .uninit = display_uninit,
+};
diff --git a/video/out/vulkan/context_mac.m b/video/out/vulkan/context_mac.m
new file mode 100644
index 0000000..8ac6e16
--- /dev/null
+++ b/video/out/vulkan/context_mac.m
@@ -0,0 +1,119 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "video/out/gpu/context.h"
+#include "osdep/macOS_swift.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+ MacCommon *vo_mac;
+};
+
+static void mac_vk_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ [p->vo_mac uninit:ctx->vo];
+}
+
+static void mac_vk_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ [p->vo_mac swapBuffer];
+}
+
+static bool mac_vk_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_EXT_METAL_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ p->vo_mac = [[MacCommon alloc] init:ctx->vo];
+ if (!p->vo_mac)
+ goto error;
+
+ VkMetalSurfaceCreateInfoEXT mac_info = {
+ .sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK,
+ .pNext = NULL,
+ .flags = 0,
+ .pLayer = p->vo_mac.layer,
+ };
+
+ struct ra_vk_ctx_params params = {
+ .swap_buffers = mac_vk_swap_buffers,
+ };
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateMetalSurfaceEXT(inst, &mac_info, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating metal surface\n");
+ goto error;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+ return true;
+error:
+ if (p->vo_mac)
+ [p->vo_mac uninit:ctx->vo];
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight);
+}
+
+static bool mac_vk_reconfig(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (![p->vo_mac config:ctx->vo])
+ return false;
+ return true;
+}
+
+static int mac_vk_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ struct priv *p = ctx->priv;
+ int ret = [p->vo_mac control:ctx->vo events:events request:request data:arg];
+
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_mac = {
+ .type = "vulkan",
+ .name = "macvk",
+ .reconfig = mac_vk_reconfig,
+ .control = mac_vk_control,
+ .init = mac_vk_init,
+ .uninit = mac_vk_uninit,
+};
diff --git a/video/out/vulkan/context_wayland.c b/video/out/vulkan/context_wayland.c
new file mode 100644
index 0000000..761ff5b
--- /dev/null
+++ b/video/out/vulkan/context_wayland.c
@@ -0,0 +1,167 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "video/out/gpu/context.h"
+#include "video/out/present_sync.h"
+#include "video/out/wayland_common.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static bool wayland_vk_check_visible(struct ra_ctx *ctx)
+{
+ return vo_wayland_check_visible(ctx->vo);
+}
+
+static void wayland_vk_swap_buffers(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void wayland_vk_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+static void wayland_vk_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ vo_wayland_uninit(ctx->vo);
+}
+
+static bool wayland_vk_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ if (!vo_wayland_init(ctx->vo))
+ goto error;
+
+ VkWaylandSurfaceCreateInfoKHR wlinfo = {
+ .sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR,
+ .display = ctx->vo->wl->display,
+ .surface = ctx->vo->wl->surface,
+ };
+
+ struct ra_vk_ctx_params params = {
+ .check_visible = wayland_vk_check_visible,
+ .swap_buffers = wayland_vk_swap_buffers,
+ .get_vsync = wayland_vk_get_vsync,
+ };
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateWaylandSurfaceKHR(inst, &wlinfo, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Wayland surface\n");
+ goto error;
+ }
+
+ /* Because in Wayland clients render whenever they receive a callback from
+ * the compositor, and the fact that the compositor usually stops sending
+ * callbacks once the surface is no longer visible, using FIFO here would
+ * mean the entire player would block on acquiring swapchain images. Hence,
+ * use MAILBOX to guarantee that there'll always be a swapchain image and
+ * the player won't block waiting on those */
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_MAILBOX_KHR))
+ goto error;
+
+ ra_add_native_resource(ctx->ra, "wl", ctx->vo->wl->display);
+
+ return true;
+
+error:
+ wayland_vk_uninit(ctx);
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ MP_VERBOSE(wl, "Handling resize on the vk side\n");
+
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ vo_wayland_handle_fractional_scale(wl);
+ return ra_vk_ctx_resize(ctx, width, height);
+}
+
+static bool wayland_vk_reconfig(struct ra_ctx *ctx)
+{
+ return vo_wayland_reconfig(ctx->vo);
+}
+
+static int wayland_vk_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_wayland_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+static void wayland_vk_wakeup(struct ra_ctx *ctx)
+{
+ vo_wayland_wakeup(ctx->vo);
+}
+
+static void wayland_vk_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_wayland_wait_events(ctx->vo, until_time_ns);
+}
+
+static void wayland_vk_update_render_opts(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ wl_surface_commit(wl->surface);
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_wayland = {
+ .type = "vulkan",
+ .name = "waylandvk",
+ .reconfig = wayland_vk_reconfig,
+ .control = wayland_vk_control,
+ .wakeup = wayland_vk_wakeup,
+ .wait_events = wayland_vk_wait_events,
+ .update_render_opts = wayland_vk_update_render_opts,
+ .init = wayland_vk_init,
+ .uninit = wayland_vk_uninit,
+};
diff --git a/video/out/vulkan/context_win.c b/video/out/vulkan/context_win.c
new file mode 100644
index 0000000..a89c644
--- /dev/null
+++ b/video/out/vulkan/context_win.c
@@ -0,0 +1,106 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "video/out/gpu/context.h"
+#include "video/out/w32_common.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static void win_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ vo_w32_uninit(ctx->vo);
+}
+
+static bool win_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_WIN32_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ if (!vo_w32_init(ctx->vo))
+ goto error;
+
+ VkWin32SurfaceCreateInfoKHR wininfo = {
+ .sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
+ .hinstance = HINST_THISCOMPONENT,
+ .hwnd = vo_w32_hwnd(ctx->vo),
+ };
+
+ struct ra_vk_ctx_params params = {0};
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateWin32SurfaceKHR(inst, &wininfo, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Windows surface\n");
+ goto error;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+ return true;
+
+error:
+ win_uninit(ctx);
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight);
+}
+
+static bool win_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ return resize(ctx);
+}
+
+static int win_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_win = {
+ .type = "vulkan",
+ .name = "winvk",
+ .reconfig = win_reconfig,
+ .control = win_control,
+ .init = win_init,
+ .uninit = win_uninit,
+};
diff --git a/video/out/vulkan/context_xlib.c b/video/out/vulkan/context_xlib.c
new file mode 100644
index 0000000..673dc31
--- /dev/null
+++ b/video/out/vulkan/context_xlib.c
@@ -0,0 +1,143 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "video/out/gpu/context.h"
+#include "video/out/present_sync.h"
+#include "video/out/x11_common.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static bool xlib_check_visible(struct ra_ctx *ctx)
+{
+ return vo_x11_check_visible(ctx->vo);
+}
+
+static void xlib_vk_swap_buffers(struct ra_ctx *ctx)
+{
+ if (ctx->vo->x11->use_present)
+ present_sync_swap(ctx->vo->x11->present);
+}
+
+static void xlib_vk_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = ctx->vo->x11;
+ if (ctx->vo->x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static void xlib_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ vo_x11_uninit(ctx->vo);
+}
+
+static bool xlib_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_XLIB_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ if (!vo_x11_init(ctx->vo))
+ goto error;
+
+ if (!vo_x11_create_vo_window(ctx->vo, NULL, "mpvk"))
+ goto error;
+
+ VkXlibSurfaceCreateInfoKHR xinfo = {
+ .sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR,
+ .dpy = ctx->vo->x11->display,
+ .window = ctx->vo->x11->window,
+ };
+
+ struct ra_vk_ctx_params params = {
+ .check_visible = xlib_check_visible,
+ .swap_buffers = xlib_vk_swap_buffers,
+ .get_vsync = xlib_vk_get_vsync,
+ };
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateXlibSurfaceKHR(inst, &xinfo, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Xlib surface\n");
+ goto error;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+ ra_add_native_resource(ctx->ra, "x11", ctx->vo->x11->display);
+
+ return true;
+
+error:
+ xlib_uninit(ctx);
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight);
+}
+
+static bool xlib_reconfig(struct ra_ctx *ctx)
+{
+ vo_x11_config_vo_window(ctx->vo);
+ return resize(ctx);
+}
+
+static int xlib_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_x11_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+static void xlib_wakeup(struct ra_ctx *ctx)
+{
+ vo_x11_wakeup(ctx->vo);
+}
+
+static void xlib_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_x11_wait_events(ctx->vo, until_time_ns);
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_xlib = {
+ .type = "vulkan",
+ .name = "x11vk",
+ .reconfig = xlib_reconfig,
+ .control = xlib_control,
+ .wakeup = xlib_wakeup,
+ .wait_events = xlib_wait_events,
+ .init = xlib_init,
+ .uninit = xlib_uninit,
+};
diff --git a/video/out/vulkan/utils.c b/video/out/vulkan/utils.c
new file mode 100644
index 0000000..57a3664
--- /dev/null
+++ b/video/out/vulkan/utils.c
@@ -0,0 +1,42 @@
+#include "video/out/placebo/utils.h"
+#include "utils.h"
+
+bool mpvk_init(struct mpvk_ctx *vk, struct ra_ctx *ctx, const char *surface_ext)
+{
+ vk->pllog = mppl_log_create(ctx, ctx->vo->log);
+ if (!vk->pllog)
+ goto error;
+
+ const char *exts[] = {
+ VK_KHR_SURFACE_EXTENSION_NAME,
+ surface_ext,
+ };
+
+ mppl_log_set_probing(vk->pllog, true);
+ vk->vkinst = pl_vk_inst_create(vk->pllog, &(struct pl_vk_inst_params) {
+ .debug = ctx->opts.debug,
+ .extensions = exts,
+ .num_extensions = MP_ARRAY_SIZE(exts),
+ });
+ mppl_log_set_probing(vk->pllog, false);
+ if (!vk->vkinst)
+ goto error;
+
+ return true;
+
+error:
+ mpvk_uninit(vk);
+ return false;
+}
+
+void mpvk_uninit(struct mpvk_ctx *vk)
+{
+ if (vk->surface) {
+ assert(vk->vkinst);
+ vkDestroySurfaceKHR(vk->vkinst->instance, vk->surface, NULL);
+ vk->surface = VK_NULL_HANDLE;
+ }
+
+ pl_vk_inst_destroy(&vk->vkinst);
+ pl_log_destroy(&vk->pllog);
+}
diff --git a/video/out/vulkan/utils.h b/video/out/vulkan/utils.h
new file mode 100644
index 0000000..a98e147
--- /dev/null
+++ b/video/out/vulkan/utils.h
@@ -0,0 +1,6 @@
+#pragma once
+#include "common.h"
+#include "video/out/gpu/context.h"
+
+bool mpvk_init(struct mpvk_ctx *vk, struct ra_ctx *ctx, const char *surface_ext);
+void mpvk_uninit(struct mpvk_ctx *vk);
diff --git a/video/out/w32_common.c b/video/out/w32_common.c
new file mode 100644
index 0000000..e6a4670
--- /dev/null
+++ b/video/out/w32_common.c
@@ -0,0 +1,2144 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <limits.h>
+#include <stdatomic.h>
+#include <stdio.h>
+
+#include <windows.h>
+#include <windowsx.h>
+#include <dwmapi.h>
+#include <ole2.h>
+#include <shobjidl.h>
+#include <avrt.h>
+
+#include "options/m_config.h"
+#include "options/options.h"
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "input/event.h"
+#include "stream/stream.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "vo.h"
+#include "win_state.h"
+#include "w32_common.h"
+#include "win32/displayconfig.h"
+#include "win32/droptarget.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/w32_keyboard.h"
+#include "misc/dispatch.h"
+#include "misc/rendezvous.h"
+#include "mpv_talloc.h"
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
+
+#ifndef WM_DPICHANGED
+#define WM_DPICHANGED (0x02E0)
+#endif
+
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+
+//Older MinGW compatibility
+#define DWMWA_WINDOW_CORNER_PREFERENCE 33
+#define DWMWA_SYSTEMBACKDROP_TYPE 38
+
+#ifndef DPI_ENUMS_DECLARED
+typedef enum MONITOR_DPI_TYPE {
+ MDT_EFFECTIVE_DPI = 0,
+ MDT_ANGULAR_DPI = 1,
+ MDT_RAW_DPI = 2,
+ MDT_DEFAULT = MDT_EFFECTIVE_DPI
+} MONITOR_DPI_TYPE;
+#endif
+
+#define rect_w(r) ((r).right - (r).left)
+#define rect_h(r) ((r).bottom - (r).top)
+
+struct w32_api {
+ HRESULT (WINAPI *pGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*);
+ BOOL (WINAPI *pImmDisableIME)(DWORD);
+ BOOL (WINAPI *pAdjustWindowRectExForDpi)(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi);
+ BOOLEAN (WINAPI *pShouldAppsUseDarkMode)(void);
+ DWORD (WINAPI *pSetPreferredAppMode)(DWORD mode);
+};
+
+struct vo_w32_state {
+ struct mp_log *log;
+ struct vo *vo;
+ struct mp_vo_opts *opts;
+ struct m_config_cache *opts_cache;
+ struct input_ctx *input_ctx;
+
+ mp_thread thread;
+ bool terminate;
+ struct mp_dispatch_queue *dispatch; // used to run stuff on the GUI thread
+ bool in_dispatch;
+
+ struct w32_api api; // stores functions from dynamically loaded DLLs
+
+ HWND window;
+ HWND parent; // 0 normally, set in embedding mode
+ HHOOK parent_win_hook;
+ HWINEVENTHOOK parent_evt_hook;
+
+ HMONITOR monitor; // Handle of the current screen
+ char *color_profile; // Path of the current screen's color profile
+
+ // Has the window seen a WM_DESTROY? If so, don't call DestroyWindow again.
+ bool destroyed;
+
+ bool focused;
+
+ // whether the window position and size were initialized
+ bool window_bounds_initialized;
+
+ bool current_fs;
+ bool toggle_fs; // whether the current fullscreen state needs to be switched
+
+ // Note: maximized state doesn't involve nor modify windowrc
+ RECT windowrc; // currently known normal/fullscreen window client rect
+ RECT prev_windowrc; // saved normal window client rect while in fullscreen
+
+ // video size
+ uint32_t o_dwidth;
+ uint32_t o_dheight;
+
+ int dpi;
+ double dpi_scale;
+
+ bool disable_screensaver;
+ bool cursor_visible;
+ atomic_uint event_flags;
+
+ BOOL tracking;
+ TRACKMOUSEEVENT trackEvent;
+
+ int mouse_x;
+ int mouse_y;
+
+ // Should SetCursor be called when handling VOCTRL_SET_CURSOR_VISIBILITY?
+ bool can_set_cursor;
+
+ // UTF-16 decoding state for WM_CHAR and VK_PACKET
+ int high_surrogate;
+
+ // Fit the window to one monitor working area next time it's not fullscreen
+ // and not maximized. Used once after every new "untrusted" size comes from
+ // mpv, else we assume that the last known size is valid and don't fit.
+ // FIXME: on a multi-monitor setup one bit is not enough, because the first
+ // fit (autofit etc) should be to one monitor, but later size changes from
+ // mpv like window-scale (VOCTRL_SET_UNFS_WINDOW_SIZE) should allow the
+ // entire virtual desktop area - but we still limit to one monitor size.
+ bool fit_on_screen;
+
+ bool win_force_pos;
+
+ ITaskbarList2 *taskbar_list;
+ ITaskbarList3 *taskbar_list3;
+ UINT tbtnCreatedMsg;
+ bool tbtnCreated;
+
+ struct voctrl_playback_state current_pstate;
+
+ // updates on move/resize/displaychange
+ double display_fps;
+
+ bool moving;
+
+ union {
+ uint8_t snapped;
+ struct {
+ uint8_t snapped_left : 1;
+ uint8_t snapped_right : 1;
+ uint8_t snapped_top : 1;
+ uint8_t snapped_bottom : 1;
+ };
+ };
+ int snap_dx;
+ int snap_dy;
+
+ HANDLE avrt_handle;
+
+ bool cleared;
+};
+
+static void adjust_window_rect(struct vo_w32_state *w32, HWND hwnd, RECT *rc)
+{
+ if (!w32->opts->border)
+ return;
+
+ if (w32->api.pAdjustWindowRectExForDpi) {
+ w32->api.pAdjustWindowRectExForDpi(rc,
+ GetWindowLongPtrW(hwnd, GWL_STYLE), 0,
+ GetWindowLongPtrW(hwnd, GWL_EXSTYLE), w32->dpi);
+ } else {
+ AdjustWindowRect(rc, GetWindowLongPtrW(hwnd, GWL_STYLE), 0);
+ }
+}
+
+static void add_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc)
+{
+ RECT win = *rc;
+ adjust_window_rect(w32, hwnd, rc);
+ // Adjust for title bar height that will be hidden in WM_NCCALCSIZE
+ if (w32->opts->border && !w32->opts->title_bar && !w32->current_fs)
+ rc->top -= rc->top - win.top;
+}
+
+// basically a reverse AdjustWindowRect (win32 doesn't appear to have this)
+static void subtract_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc)
+{
+ RECT b = { 0, 0, 0, 0 };
+ add_window_borders(w32, hwnd, &b);
+ rc->left -= b.left;
+ rc->top -= b.top;
+ rc->right -= b.right;
+ rc->bottom -= b.bottom;
+}
+
+static LRESULT borderless_nchittest(struct vo_w32_state *w32, int x, int y)
+{
+ if (IsMaximized(w32->window))
+ return HTCLIENT;
+
+ RECT rc;
+ if (!GetWindowRect(w32->window, &rc))
+ return HTNOWHERE;
+
+ POINT frame = {GetSystemMetrics(SM_CXSIZEFRAME),
+ GetSystemMetrics(SM_CYSIZEFRAME)};
+ if (w32->opts->border) {
+ frame.x += GetSystemMetrics(SM_CXPADDEDBORDER);
+ frame.y += GetSystemMetrics(SM_CXPADDEDBORDER);
+ if (!w32->opts->title_bar)
+ rc.top -= GetSystemMetrics(SM_CXPADDEDBORDER);
+ }
+ InflateRect(&rc, -frame.x, -frame.y);
+
+ // Hit-test top border
+ if (y < rc.top) {
+ if (x < rc.left)
+ return HTTOPLEFT;
+ if (x > rc.right)
+ return HTTOPRIGHT;
+ return HTTOP;
+ }
+
+ // Hit-test bottom border
+ if (y > rc.bottom) {
+ if (x < rc.left)
+ return HTBOTTOMLEFT;
+ if (x > rc.right)
+ return HTBOTTOMRIGHT;
+ return HTBOTTOM;
+ }
+
+ // Hit-test side borders
+ if (x < rc.left)
+ return HTLEFT;
+ if (x > rc.right)
+ return HTRIGHT;
+ return HTCLIENT;
+}
+
+// turn a WMSZ_* input value in v into the border that should be resized
+// take into consideration which borders are snapped to avoid detaching
+// returns: 0=left, 1=top, 2=right, 3=bottom, -1=undefined
+static int get_resize_border(struct vo_w32_state *w32, int v)
+{
+ switch (v) {
+ case WMSZ_LEFT:
+ case WMSZ_RIGHT:
+ return w32->snapped_bottom ? 1 : 3;
+ case WMSZ_TOP:
+ case WMSZ_BOTTOM:
+ return w32->snapped_right ? 0 : 2;
+ case WMSZ_TOPLEFT: return 1;
+ case WMSZ_TOPRIGHT: return 1;
+ case WMSZ_BOTTOMLEFT: return 3;
+ case WMSZ_BOTTOMRIGHT: return 3;
+ default: return -1;
+ }
+}
+
+static bool key_state(int vk)
+{
+ return GetKeyState(vk) & 0x8000;
+}
+
+static int mod_state(struct vo_w32_state *w32)
+{
+ int res = 0;
+
+ // AltGr is represented as LCONTROL+RMENU on Windows
+ bool alt_gr = mp_input_use_alt_gr(w32->input_ctx) &&
+ key_state(VK_RMENU) && key_state(VK_LCONTROL);
+
+ if (key_state(VK_RCONTROL) || (key_state(VK_LCONTROL) && !alt_gr))
+ res |= MP_KEY_MODIFIER_CTRL;
+ if (key_state(VK_SHIFT))
+ res |= MP_KEY_MODIFIER_SHIFT;
+ if (key_state(VK_LMENU) || (key_state(VK_RMENU) && !alt_gr))
+ res |= MP_KEY_MODIFIER_ALT;
+ return res;
+}
+
+static int decode_surrogate_pair(wchar_t lead, wchar_t trail)
+{
+ return 0x10000 + (((lead & 0x3ff) << 10) | (trail & 0x3ff));
+}
+
+static int decode_utf16(struct vo_w32_state *w32, wchar_t c)
+{
+ // Decode UTF-16, keeping state in w32->high_surrogate
+ if (IS_HIGH_SURROGATE(c)) {
+ w32->high_surrogate = c;
+ return 0;
+ }
+ if (IS_LOW_SURROGATE(c)) {
+ if (!w32->high_surrogate) {
+ MP_ERR(w32, "Invalid UTF-16 input\n");
+ return 0;
+ }
+ int codepoint = decode_surrogate_pair(w32->high_surrogate, c);
+ w32->high_surrogate = 0;
+ return codepoint;
+ }
+ if (w32->high_surrogate != 0) {
+ w32->high_surrogate = 0;
+ MP_ERR(w32, "Invalid UTF-16 input\n");
+ return 0;
+ }
+
+ return c;
+}
+
+static void clear_keyboard_buffer(void)
+{
+ static const UINT vkey = VK_DECIMAL;
+ static const BYTE keys[256] = { 0 };
+ UINT scancode = MapVirtualKey(vkey, MAPVK_VK_TO_VSC);
+ wchar_t buf[10];
+ int ret = 0;
+
+ // Use the method suggested by Michael Kaplan to clear any pending dead
+ // keys from the current keyboard layout. See:
+ // https://web.archive.org/web/20101004154432/http://blogs.msdn.com/b/michkap/archive/2006/04/06/569632.aspx
+ // https://web.archive.org/web/20100820152419/http://blogs.msdn.com/b/michkap/archive/2007/10/27/5717859.aspx
+ do {
+ ret = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0);
+ } while (ret < 0);
+}
+
+static int to_unicode(UINT vkey, UINT scancode, const BYTE keys[256])
+{
+ // This wraps ToUnicode to be stateless and to return only one character
+
+ // Make the buffer 10 code units long to be safe, same as here:
+ // https://web.archive.org/web/20101013215215/http://blogs.msdn.com/b/michkap/archive/2006/03/24/559169.aspx
+ wchar_t buf[10] = { 0 };
+
+ // Dead keys aren't useful for key shortcuts, so clear the keyboard state
+ clear_keyboard_buffer();
+
+ int len = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0);
+
+ // Return the last complete UTF-16 code point. A negative return value
+ // indicates a dead key, however there should still be a non-combining
+ // version of the key in the buffer.
+ if (len < 0)
+ len = -len;
+ if (len >= 2 && IS_SURROGATE_PAIR(buf[len - 2], buf[len - 1]))
+ return decode_surrogate_pair(buf[len - 2], buf[len - 1]);
+ if (len >= 1)
+ return buf[len - 1];
+
+ return 0;
+}
+
+static int decode_key(struct vo_w32_state *w32, UINT vkey, UINT scancode)
+{
+ BYTE keys[256];
+ GetKeyboardState(keys);
+
+ // If mp_input_use_alt_gr is false, detect and remove AltGr so normal
+ // characters are generated. Note that AltGr is represented as
+ // LCONTROL+RMENU on Windows.
+ if ((keys[VK_RMENU] & 0x80) && (keys[VK_LCONTROL] & 0x80) &&
+ !mp_input_use_alt_gr(w32->input_ctx))
+ {
+ keys[VK_RMENU] = keys[VK_LCONTROL] = 0;
+ keys[VK_MENU] = keys[VK_LMENU];
+ keys[VK_CONTROL] = keys[VK_RCONTROL];
+ }
+
+ int c = to_unicode(vkey, scancode, keys);
+
+ // Some shift states prevent ToUnicode from working or cause it to produce
+ // control characters. If this is detected, remove modifiers until it
+ // starts producing normal characters.
+ if (c < 0x20 && (keys[VK_MENU] & 0x80)) {
+ keys[VK_LMENU] = keys[VK_RMENU] = keys[VK_MENU] = 0;
+ c = to_unicode(vkey, scancode, keys);
+ }
+ if (c < 0x20 && (keys[VK_CONTROL] & 0x80)) {
+ keys[VK_LCONTROL] = keys[VK_RCONTROL] = keys[VK_CONTROL] = 0;
+ c = to_unicode(vkey, scancode, keys);
+ }
+ if (c < 0x20)
+ return 0;
+
+ // Decode lone UTF-16 surrogates (VK_PACKET can generate these)
+ if (c < 0x10000)
+ return decode_utf16(w32, c);
+ return c;
+}
+
+static bool handle_appcommand(struct vo_w32_state *w32, UINT cmd)
+{
+ if (!mp_input_use_media_keys(w32->input_ctx))
+ return false;
+ int mpkey = mp_w32_appcmd_to_mpkey(cmd);
+ if (!mpkey)
+ return false;
+ mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32));
+ return true;
+}
+
+static void handle_key_down(struct vo_w32_state *w32, UINT vkey, UINT scancode)
+{
+ // Ignore key repeat
+ if (scancode & KF_REPEAT)
+ return;
+
+ int mpkey = mp_w32_vkey_to_mpkey(vkey, scancode & KF_EXTENDED);
+ if (!mpkey) {
+ mpkey = decode_key(w32, vkey, scancode & (0xff | KF_EXTENDED));
+ if (!mpkey)
+ return;
+ }
+
+ mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32) | MP_KEY_STATE_DOWN);
+}
+
+static void handle_key_up(struct vo_w32_state *w32, UINT vkey, UINT scancode)
+{
+ switch (vkey) {
+ case VK_MENU:
+ case VK_CONTROL:
+ case VK_SHIFT:
+ break;
+ default:
+ // Releasing all keys on key-up is simpler and ensures no keys can be
+ // get "stuck." This matches the behaviour of other VOs.
+ mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL);
+ }
+}
+
+static bool handle_char(struct vo_w32_state *w32, wchar_t wc)
+{
+ int c = decode_utf16(w32, wc);
+
+ if (c == 0)
+ return true;
+ if (c < 0x20)
+ return false;
+
+ mp_input_put_key(w32->input_ctx, c | mod_state(w32));
+ return true;
+}
+
+static bool handle_mouse_down(struct vo_w32_state *w32, int btn, int x, int y)
+{
+ btn |= mod_state(w32);
+ mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_DOWN);
+
+ if (btn == MP_MBTN_LEFT && !w32->current_fs &&
+ !mp_input_test_dragging(w32->input_ctx, x, y))
+ {
+ // Window dragging hack
+ ReleaseCapture();
+ SendMessage(w32->window, WM_NCLBUTTONDOWN, HTCAPTION, 0);
+ mp_input_put_key(w32->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP);
+
+ // Indicate the message was handled, so DefWindowProc won't be called
+ return true;
+ }
+
+ SetCapture(w32->window);
+ return false;
+}
+
+static void handle_mouse_up(struct vo_w32_state *w32, int btn)
+{
+ btn |= mod_state(w32);
+ mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_UP);
+
+ ReleaseCapture();
+}
+
+static void handle_mouse_wheel(struct vo_w32_state *w32, bool horiz, int val)
+{
+ int code;
+ if (horiz)
+ code = val > 0 ? MP_WHEEL_RIGHT : MP_WHEEL_LEFT;
+ else
+ code = val > 0 ? MP_WHEEL_UP : MP_WHEEL_DOWN;
+ mp_input_put_wheel(w32->input_ctx, code | mod_state(w32), abs(val) / 120.);
+}
+
+static void signal_events(struct vo_w32_state *w32, int events)
+{
+ atomic_fetch_or(&w32->event_flags, events);
+ vo_wakeup(w32->vo);
+}
+
+static void wakeup_gui_thread(void *ctx)
+{
+ struct vo_w32_state *w32 = ctx;
+ // Wake up the window procedure (which processes the dispatch queue)
+ if (GetWindowThreadProcessId(w32->window, NULL) == GetCurrentThreadId()) {
+ PostMessageW(w32->window, WM_NULL, 0, 0);
+ } else {
+ // Use a sent message when cross-thread, since the queue of sent
+ // messages is processed in some cases when posted messages are blocked
+ SendNotifyMessageW(w32->window, WM_NULL, 0, 0);
+ }
+}
+
+static double get_refresh_rate_from_gdi(const wchar_t *device)
+{
+ DEVMODEW dm = { .dmSize = sizeof dm };
+ if (!EnumDisplaySettingsW(device, ENUM_CURRENT_SETTINGS, &dm))
+ return 0.0;
+
+ // May return 0 or 1 which "represent the display hardware's default refresh rate"
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd183565%28v=vs.85%29.aspx
+ // mpv validates this value with a threshold of 1, so don't return exactly 1
+ if (dm.dmDisplayFrequency == 1)
+ return 0.0;
+
+ // dm.dmDisplayFrequency is an integer which is rounded down, so it's
+ // highly likely that 23 represents 24/1.001, 59 represents 60/1.001, etc.
+ // A caller can always reproduce the original value by using floor.
+ double rv = dm.dmDisplayFrequency;
+ switch (dm.dmDisplayFrequency) {
+ case 23:
+ case 29:
+ case 47:
+ case 59:
+ case 71:
+ case 89:
+ case 95:
+ case 119:
+ case 143:
+ case 164:
+ case 239:
+ case 359:
+ case 479:
+ rv = (rv + 1) / 1.001;
+ }
+
+ return rv;
+}
+
+static char *get_color_profile(void *ctx, const wchar_t *device)
+{
+ char *name = NULL;
+
+ HDC ic = CreateICW(device, NULL, NULL, NULL);
+ if (!ic)
+ goto done;
+ wchar_t wname[MAX_PATH + 1];
+ if (!GetICMProfileW(ic, &(DWORD){ MAX_PATH }, wname))
+ goto done;
+
+ name = mp_to_utf8(ctx, wname);
+done:
+ if (ic)
+ DeleteDC(ic);
+ return name;
+}
+
+static void update_dpi(struct vo_w32_state *w32)
+{
+ UINT dpiX, dpiY;
+ HDC hdc = NULL;
+ int dpi = 0;
+
+ if (w32->api.pGetDpiForMonitor && w32->api.pGetDpiForMonitor(w32->monitor,
+ MDT_EFFECTIVE_DPI, &dpiX, &dpiY) == S_OK) {
+ dpi = (int)dpiX;
+ MP_VERBOSE(w32, "DPI detected from the new API: %d\n", dpi);
+ } else if ((hdc = GetDC(NULL))) {
+ dpi = GetDeviceCaps(hdc, LOGPIXELSX);
+ ReleaseDC(NULL, hdc);
+ MP_VERBOSE(w32, "DPI detected from the old API: %d\n", dpi);
+ }
+
+ if (dpi <= 0) {
+ dpi = 96;
+ MP_VERBOSE(w32, "Couldn't determine DPI, falling back to %d\n", dpi);
+ }
+
+ w32->dpi = dpi;
+ w32->dpi_scale = w32->opts->hidpi_window_scale ? w32->dpi / 96.0 : 1.0;
+ signal_events(w32, VO_EVENT_DPI);
+}
+
+static void update_display_info(struct vo_w32_state *w32)
+{
+ HMONITOR monitor = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
+ if (w32->monitor == monitor)
+ return;
+ w32->monitor = monitor;
+
+ update_dpi(w32);
+
+ MONITORINFOEXW mi = { .cbSize = sizeof mi };
+ GetMonitorInfoW(monitor, (MONITORINFO*)&mi);
+
+ // Try to get the monitor refresh rate.
+ double freq = 0.0;
+
+ if (freq == 0.0)
+ freq = mp_w32_displayconfig_get_refresh_rate(mi.szDevice);
+ if (freq == 0.0)
+ freq = get_refresh_rate_from_gdi(mi.szDevice);
+
+ if (freq != w32->display_fps) {
+ MP_VERBOSE(w32, "display-fps: %f\n", freq);
+ if (freq == 0.0)
+ MP_WARN(w32, "Couldn't determine monitor refresh rate\n");
+ w32->display_fps = freq;
+ signal_events(w32, VO_EVENT_WIN_STATE);
+ }
+
+ char *color_profile = get_color_profile(w32, mi.szDevice);
+ if ((color_profile == NULL) != (w32->color_profile == NULL) ||
+ (color_profile && strcmp(color_profile, w32->color_profile)))
+ {
+ if (color_profile)
+ MP_VERBOSE(w32, "color-profile: %s\n", color_profile);
+ talloc_free(w32->color_profile);
+ w32->color_profile = color_profile;
+ color_profile = NULL;
+ signal_events(w32, VO_EVENT_ICC_PROFILE_CHANGED);
+ }
+
+ talloc_free(color_profile);
+}
+
+static void force_update_display_info(struct vo_w32_state *w32)
+{
+ w32->monitor = 0;
+ update_display_info(w32);
+}
+
+static void update_playback_state(struct vo_w32_state *w32)
+{
+ struct voctrl_playback_state *pstate = &w32->current_pstate;
+
+ if (!w32->taskbar_list3 || !w32->tbtnCreated)
+ return;
+
+ if (!pstate->playing || !pstate->taskbar_progress) {
+ ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window,
+ TBPF_NOPROGRESS);
+ return;
+ }
+
+ ITaskbarList3_SetProgressValue(w32->taskbar_list3, w32->window,
+ pstate->percent_pos, 100);
+ ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window,
+ pstate->paused ? TBPF_PAUSED :
+ TBPF_NORMAL);
+}
+
+struct get_monitor_data {
+ int i;
+ int target;
+ HMONITOR mon;
+};
+
+static BOOL CALLBACK get_monitor_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p)
+{
+ struct get_monitor_data *data = (struct get_monitor_data*)p;
+
+ if (data->i == data->target) {
+ data->mon = mon;
+ return FALSE;
+ }
+ data->i++;
+ return TRUE;
+}
+
+static HMONITOR get_monitor(int id)
+{
+ struct get_monitor_data data = { .target = id };
+ EnumDisplayMonitors(NULL, NULL, get_monitor_proc, (LPARAM)&data);
+ return data.mon;
+}
+
+static HMONITOR get_default_monitor(struct vo_w32_state *w32)
+{
+ const int id = w32->current_fs ? w32->opts->fsscreen_id :
+ w32->opts->screen_id;
+
+ // Handle --fs-screen=<all|default> and --screen=default
+ if (id < 0) {
+ if (w32->win_force_pos && !w32->current_fs) {
+ // Get window from forced position
+ return MonitorFromRect(&w32->windowrc, MONITOR_DEFAULTTOPRIMARY);
+ } else {
+ // Let compositor decide
+ return MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
+ }
+ }
+
+ HMONITOR mon = get_monitor(id);
+ if (mon)
+ return mon;
+ MP_VERBOSE(w32, "Screen %d does not exist, falling back to primary\n", id);
+ return MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
+}
+
+static MONITORINFO get_monitor_info(struct vo_w32_state *w32)
+{
+ HMONITOR mon;
+ if (IsWindowVisible(w32->window) && !w32->current_fs) {
+ mon = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
+ } else {
+ // The window is not visible during initialization, so get the
+ // monitor by --screen or --fs-screen id, or fallback to primary.
+ mon = get_default_monitor(w32);
+ }
+ MONITORINFO mi = { .cbSize = sizeof(mi) };
+ GetMonitorInfoW(mon, &mi);
+ return mi;
+}
+
+static RECT get_screen_area(struct vo_w32_state *w32)
+{
+ // Handle --fs-screen=all
+ if (w32->current_fs && w32->opts->fsscreen_id == -2) {
+ const int x = GetSystemMetrics(SM_XVIRTUALSCREEN);
+ const int y = GetSystemMetrics(SM_YVIRTUALSCREEN);
+ return (RECT) { x, y, x + GetSystemMetrics(SM_CXVIRTUALSCREEN),
+ y + GetSystemMetrics(SM_CYVIRTUALSCREEN) };
+ }
+ return get_monitor_info(w32).rcMonitor;
+}
+
+static RECT get_working_area(struct vo_w32_state *w32)
+{
+ return w32->current_fs ? get_screen_area(w32) :
+ get_monitor_info(w32).rcWork;
+}
+
+// Adjust working area boundaries to compensate for invisible borders.
+static void adjust_working_area_for_extended_frame(RECT *wa_rect, RECT *wnd_rect, HWND wnd)
+{
+ RECT frame = {0};
+
+ if (DwmGetWindowAttribute(wnd, DWMWA_EXTENDED_FRAME_BOUNDS,
+ &frame, sizeof(RECT)) == S_OK) {
+ wa_rect->left -= frame.left - wnd_rect->left;
+ wa_rect->top -= frame.top - wnd_rect->top;
+ wa_rect->right += wnd_rect->right - frame.right;
+ wa_rect->bottom += wnd_rect->bottom - frame.bottom;
+ }
+}
+
+static bool snap_to_screen_edges(struct vo_w32_state *w32, RECT *rc)
+{
+ if (w32->parent || w32->current_fs || IsMaximized(w32->window))
+ return false;
+
+ if (!w32->opts->snap_window) {
+ w32->snapped = 0;
+ return false;
+ }
+
+ RECT rect;
+ POINT cursor;
+ if (!GetWindowRect(w32->window, &rect) || !GetCursorPos(&cursor))
+ return false;
+ // Check if window is going to be aero-snapped
+ if (rect_w(*rc) != rect_w(rect) || rect_h(*rc) != rect_h(rect))
+ return false;
+
+ // Check if window has already been aero-snapped
+ WINDOWPLACEMENT wp = {0};
+ wp.length = sizeof(wp);
+ if (!GetWindowPlacement(w32->window, &wp))
+ return false;
+ RECT wr = wp.rcNormalPosition;
+ if (rect_w(*rc) != rect_w(wr) || rect_h(*rc) != rect_h(wr))
+ return false;
+
+ // Get the work area to let the window snap to taskbar
+ wr = get_working_area(w32);
+
+ adjust_working_area_for_extended_frame(&wr, &rect, w32->window);
+
+ // Let the window to unsnap by changing its position,
+ // otherwise it will stick to the screen edges forever
+ rect = *rc;
+ if (w32->snapped) {
+ OffsetRect(&rect, cursor.x - rect.left - w32->snap_dx,
+ cursor.y - rect.top - w32->snap_dy);
+ }
+
+ int threshold = (w32->dpi * 16) / 96;
+ bool was_snapped = !!w32->snapped;
+ w32->snapped = 0;
+ // Adjust X position
+ // snapped_left & snapped_right are mutually exclusive
+ if (abs(rect.left - wr.left) < threshold) {
+ w32->snapped_left = 1;
+ OffsetRect(&rect, wr.left - rect.left, 0);
+ } else if (abs(rect.right - wr.right) < threshold) {
+ w32->snapped_right = 1;
+ OffsetRect(&rect, wr.right - rect.right, 0);
+ }
+ // Adjust Y position
+ // snapped_top & snapped_bottom are mutually exclusive
+ if (abs(rect.top - wr.top) < threshold) {
+ w32->snapped_top = 1;
+ OffsetRect(&rect, 0, wr.top - rect.top);
+ } else if (abs(rect.bottom - wr.bottom) < threshold) {
+ w32->snapped_bottom = 1;
+ OffsetRect(&rect, 0, wr.bottom - rect.bottom);
+ }
+
+ if (!was_snapped && w32->snapped != 0) {
+ w32->snap_dx = cursor.x - rc->left;
+ w32->snap_dy = cursor.y - rc->top;
+ }
+
+ *rc = rect;
+ return true;
+}
+
+static DWORD update_style(struct vo_w32_state *w32, DWORD style)
+{
+ const DWORD NO_FRAME = WS_OVERLAPPED | WS_MINIMIZEBOX | WS_THICKFRAME;
+ const DWORD FRAME = WS_OVERLAPPEDWINDOW;
+ const DWORD FULLSCREEN = NO_FRAME & ~WS_THICKFRAME;
+ style &= ~(NO_FRAME | FRAME | FULLSCREEN);
+ style |= WS_SYSMENU;
+ if (w32->current_fs) {
+ style |= FULLSCREEN;
+ } else {
+ style |= w32->opts->border ? FRAME : NO_FRAME;
+ }
+ return style;
+}
+
+static LONG get_title_bar_height(struct vo_w32_state *w32)
+{
+ RECT rc = {0};
+ adjust_window_rect(w32, w32->window, &rc);
+ return -rc.top;
+}
+
+static void update_window_style(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ // SetWindowLongPtr can trigger a WM_SIZE event, so window rect
+ // has to be saved now and restored after setting the new style.
+ const RECT wr = w32->windowrc;
+ const DWORD style = GetWindowLongPtrW(w32->window, GWL_STYLE);
+ SetWindowLongPtrW(w32->window, GWL_STYLE, update_style(w32, style));
+ w32->windowrc = wr;
+}
+
+// Resize window rect to width = w and height = h. If window is snapped,
+// don't let it detach from snapped borders. Otherwise resize around the center.
+static void resize_and_move_rect(struct vo_w32_state *w32, RECT *rc, int w, int h)
+{
+ int x, y;
+
+ if (w32->snapped_left)
+ x = rc->left;
+ else if (w32->snapped_right)
+ x = rc->right - w;
+ else
+ x = rc->left + rect_w(*rc) / 2 - w / 2;
+
+ if (w32->snapped_top)
+ y = rc->top;
+ else if (w32->snapped_bottom)
+ y = rc->bottom - h;
+ else
+ y = rc->top + rect_h(*rc) / 2 - h / 2;
+
+ SetRect(rc, x, y, x + w, y + h);
+}
+
+// If rc is wider/taller than n_w/n_h, shrink rc size while keeping the center.
+// returns true if the rectangle was modified.
+static bool fit_rect_size(struct vo_w32_state *w32, RECT *rc, long n_w, long n_h)
+{
+ // nothing to do if we already fit.
+ int o_w = rect_w(*rc), o_h = rect_h(*rc);
+ if (o_w <= n_w && o_h <= n_h)
+ return false;
+
+ // Apply letterboxing
+ const float o_asp = o_w / (float)MPMAX(o_h, 1);
+ const float n_asp = n_w / (float)MPMAX(n_h, 1);
+ if (o_asp > n_asp) {
+ n_h = n_w / o_asp;
+ } else {
+ n_w = n_h * o_asp;
+ }
+
+ resize_and_move_rect(w32, rc, n_w, n_h);
+
+ return true;
+}
+
+// If the window is bigger than the desktop, shrink to fit with same center.
+// Also, if the top edge is above the working area, move down to align.
+static void fit_window_on_screen(struct vo_w32_state *w32)
+{
+ RECT screen = get_working_area(w32);
+ if (w32->opts->border)
+ subtract_window_borders(w32, w32->window, &screen);
+
+ RECT window_rect;
+ if (GetWindowRect(w32->window, &window_rect))
+ adjust_working_area_for_extended_frame(&screen, &window_rect, w32->window);
+
+ bool adjusted = fit_rect_size(w32, &w32->windowrc, rect_w(screen), rect_h(screen));
+
+ if (w32->windowrc.top < screen.top) {
+ // if the top-edge of client area is above the target area (mainly
+ // because the client-area is centered but the title bar is taller
+ // than the bottom border), then move it down to align the edges.
+ // Windows itself applies the same constraint during manual move.
+ w32->windowrc.bottom += screen.top - w32->windowrc.top;
+ w32->windowrc.top = screen.top;
+ adjusted = true;
+ }
+
+ if (adjusted) {
+ MP_VERBOSE(w32, "adjusted window bounds: %d:%d:%d:%d\n",
+ (int)w32->windowrc.left, (int)w32->windowrc.top,
+ (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
+ }
+}
+
+// Calculate new fullscreen state and change window size and position.
+static void update_fullscreen_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ bool new_fs = w32->opts->fullscreen;
+ if (w32->toggle_fs) {
+ new_fs = !w32->current_fs;
+ w32->toggle_fs = false;
+ }
+
+ bool toggle_fs = w32->current_fs != new_fs;
+ w32->opts->fullscreen = w32->current_fs = new_fs;
+ m_config_cache_write_opt(w32->opts_cache,
+ &w32->opts->fullscreen);
+
+ if (toggle_fs) {
+ if (w32->current_fs) {
+ // Save window rect when switching to fullscreen.
+ w32->prev_windowrc = w32->windowrc;
+ MP_VERBOSE(w32, "save window bounds: %d:%d:%d:%d\n",
+ (int)w32->windowrc.left, (int)w32->windowrc.top,
+ (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
+ } else {
+ // Restore window rect when switching from fullscreen.
+ w32->windowrc = w32->prev_windowrc;
+ }
+ }
+
+ if (w32->current_fs)
+ w32->windowrc = get_screen_area(w32);
+
+ MP_VERBOSE(w32, "reset window bounds: %d:%d:%d:%d\n",
+ (int)w32->windowrc.left, (int)w32->windowrc.top,
+ (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
+}
+
+static void update_minimized_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ if (!!IsMinimized(w32->window) != w32->opts->window_minimized) {
+ if (w32->opts->window_minimized) {
+ ShowWindow(w32->window, SW_SHOWMINNOACTIVE);
+ } else {
+ ShowWindow(w32->window, SW_RESTORE);
+ }
+ }
+}
+
+static void update_maximized_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ // Don't change the maximized state in fullscreen for now. In future, this
+ // should be made to apply the maximized state on leaving fullscreen.
+ if (w32->current_fs)
+ return;
+
+ WINDOWPLACEMENT wp = { .length = sizeof wp };
+ GetWindowPlacement(w32->window, &wp);
+
+ if (wp.showCmd == SW_SHOWMINIMIZED) {
+ // When the window is minimized, setting this property just changes
+ // whether it will be maximized when it's restored
+ if (w32->opts->window_maximized) {
+ wp.flags |= WPF_RESTORETOMAXIMIZED;
+ } else {
+ wp.flags &= ~WPF_RESTORETOMAXIMIZED;
+ }
+ SetWindowPlacement(w32->window, &wp);
+ } else if ((wp.showCmd == SW_SHOWMAXIMIZED) != w32->opts->window_maximized) {
+ if (w32->opts->window_maximized) {
+ ShowWindow(w32->window, SW_SHOWMAXIMIZED);
+ } else {
+ ShowWindow(w32->window, SW_SHOWNOACTIVATE);
+ }
+ }
+}
+
+static bool is_visible(HWND window)
+{
+ // Unlike IsWindowVisible, this doesn't check the window's parents
+ return GetWindowLongPtrW(window, GWL_STYLE) & WS_VISIBLE;
+}
+
+//Set the mpv window's affinity.
+//This will affect how it's displayed on the desktop and in system-level operations like taking screenshots.
+static void update_affinity(struct vo_w32_state *w32)
+{
+ if (!w32 || w32->parent) {
+ return;
+ }
+ SetWindowDisplayAffinity(w32->window, w32->opts->window_affinity);
+}
+
+static void update_window_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ RECT wr = w32->windowrc;
+ add_window_borders(w32, w32->window, &wr);
+
+ SetWindowPos(w32->window, w32->opts->ontop ? HWND_TOPMOST : HWND_NOTOPMOST,
+ wr.left, wr.top, rect_w(wr), rect_h(wr),
+ SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
+
+ // Show the window if it's not yet visible
+ if (!is_visible(w32->window)) {
+ if (w32->opts->window_minimized) {
+ ShowWindow(w32->window, SW_SHOWMINNOACTIVE);
+ update_maximized_state(w32); // Set the WPF_RESTORETOMAXIMIZED flag
+ } else if (w32->opts->window_maximized) {
+ ShowWindow(w32->window, SW_SHOWMAXIMIZED);
+ } else {
+ ShowWindow(w32->window, SW_SHOW);
+ }
+ }
+
+ // Notify the taskbar about the fullscreen state only after the window
+ // is visible, to make sure the taskbar item has already been created
+ if (w32->taskbar_list) {
+ ITaskbarList2_MarkFullscreenWindow(w32->taskbar_list,
+ w32->window, w32->current_fs);
+ }
+
+ // Update snapping status if needed
+ if (w32->opts->snap_window && !w32->parent &&
+ !w32->current_fs && !IsMaximized(w32->window)) {
+ RECT wa = get_working_area(w32);
+
+ adjust_working_area_for_extended_frame(&wa, &wr, w32->window);
+
+ // snapped_left & snapped_right are mutually exclusive
+ if (wa.left == wr.left && wa.right == wr.right) {
+ // Leave as is.
+ } else if (wa.left == wr.left) {
+ w32->snapped_left = 1;
+ w32->snapped_right = 0;
+ } else if (wa.right == wr.right) {
+ w32->snapped_right = 1;
+ w32->snapped_left = 0;
+ } else {
+ w32->snapped_left = w32->snapped_right = 0;
+ }
+
+ // snapped_top & snapped_bottom are mutually exclusive
+ if (wa.top == wr.top && wa.bottom == wr.bottom) {
+ // Leave as is.
+ } else if (wa.top == wr.top) {
+ w32->snapped_top = 1;
+ w32->snapped_bottom = 0;
+ } else if (wa.bottom == wr.bottom) {
+ w32->snapped_bottom = 1;
+ w32->snapped_top = 0;
+ } else {
+ w32->snapped_top = w32->snapped_bottom = 0;
+ }
+ }
+
+ signal_events(w32, VO_EVENT_RESIZE);
+}
+
+static void update_corners_pref(const struct vo_w32_state *w32) {
+ if (w32->parent)
+ return;
+
+ int pref = w32->current_fs ? 0 : w32->opts->window_corners;
+ DwmSetWindowAttribute(w32->window, DWMWA_WINDOW_CORNER_PREFERENCE,
+ &pref, sizeof(pref));
+}
+
+static void reinit_window_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ // The order matters: fs state should be updated prior to changing styles
+ update_fullscreen_state(w32);
+ update_corners_pref(w32);
+ update_window_style(w32);
+
+ // fit_on_screen is applied at most once when/if applicable (normal win).
+ if (w32->fit_on_screen && !w32->current_fs && !IsMaximized(w32->window)) {
+ fit_window_on_screen(w32);
+ w32->fit_on_screen = false;
+ }
+
+ // Show and activate the window after all window state parameters were set
+ update_window_state(w32);
+}
+
+// Follow Windows settings and update dark mode state
+// Microsoft documented how to enable dark mode for title bar:
+// https://learn.microsoft.com/windows/apps/desktop/modernize/apply-windows-themes
+// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+// Documentation says to set the DWMWA_USE_IMMERSIVE_DARK_MODE attribute to
+// TRUE to honor dark mode for the window, FALSE to always use light mode. While
+// in fact setting it to TRUE causes dark mode to be always enabled, regardless
+// of the settings. Since it is quite unlikely that it will be fixed, just use
+// UxTheme API to check if dark mode should be applied and while at it enable it
+// fully. Ideally this function should only call the DwmSetWindowAttribute(),
+// but it just doesn't work as documented.
+static void update_dark_mode(const struct vo_w32_state *w32)
+{
+ if (w32->api.pSetPreferredAppMode)
+ w32->api.pSetPreferredAppMode(1); // allow dark mode
+
+ HIGHCONTRAST hc = {sizeof(hc)};
+ SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0);
+ bool high_contrast = hc.dwFlags & HCF_HIGHCONTRASTON;
+
+ // if pShouldAppsUseDarkMode is not available, just assume it to be true
+ const BOOL use_dark_mode = !high_contrast && (!w32->api.pShouldAppsUseDarkMode ||
+ w32->api.pShouldAppsUseDarkMode());
+
+ SetWindowTheme(w32->window, use_dark_mode ? L"DarkMode_Explorer" : L"", NULL);
+
+ DwmSetWindowAttribute(w32->window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &use_dark_mode, sizeof(use_dark_mode));
+}
+
+static void update_backdrop(const struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ int backdropType = w32->opts->backdrop_type;
+ DwmSetWindowAttribute(w32->window, DWMWA_SYSTEMBACKDROP_TYPE,
+ &backdropType, sizeof(backdropType));
+}
+
+static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
+ LPARAM lParam)
+{
+ struct vo_w32_state *w32 = (void*)GetWindowLongPtrW(hWnd, GWLP_USERDATA);
+ if (!w32) {
+ // WM_NCCREATE is supposed to be the first message that a window
+ // receives. It allows struct vo_w32_state to be passed from
+ // CreateWindow's lpParam to the window procedure. However, as a
+ // longstanding Windows bug, overlapped top-level windows will get a
+ // WM_GETMINMAXINFO before WM_NCCREATE. This can be ignored.
+ if (message != WM_NCCREATE)
+ return DefWindowProcW(hWnd, message, wParam, lParam);
+
+ CREATESTRUCTW *cs = (CREATESTRUCTW *)lParam;
+ w32 = cs->lpCreateParams;
+ w32->window = hWnd;
+ SetWindowLongPtrW(hWnd, GWLP_USERDATA, (LONG_PTR)w32);
+ }
+
+ // The dispatch queue should be processed as soon as possible to prevent
+ // playback glitches, since it is likely blocking the VO thread
+ if (!w32->in_dispatch) {
+ w32->in_dispatch = true;
+ mp_dispatch_queue_process(w32->dispatch, 0);
+ w32->in_dispatch = false;
+ }
+
+ switch (message) {
+ case WM_ERASEBKGND:
+ if (w32->cleared || !w32->opts->border || w32->current_fs)
+ return TRUE;
+ break;
+ case WM_PAINT:
+ w32->cleared = true;
+ signal_events(w32, VO_EVENT_EXPOSE);
+ break;
+ case WM_MOVE: {
+ w32->moving = false;
+ const int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
+ OffsetRect(&w32->windowrc, x - w32->windowrc.left,
+ y - w32->windowrc.top);
+
+ // Window may intersect with new monitors (see VOCTRL_GET_DISPLAY_NAMES)
+ signal_events(w32, VO_EVENT_WIN_STATE);
+
+ update_display_info(w32); // if we moved between monitors
+ break;
+ }
+ case WM_MOVING: {
+ w32->moving = true;
+ RECT *rc = (RECT*)lParam;
+ if (snap_to_screen_edges(w32, rc))
+ return TRUE;
+ break;
+ }
+ case WM_ENTERSIZEMOVE:
+ w32->moving = true;
+ if (w32->snapped != 0) {
+ // Save the cursor offset from the window borders,
+ // so the player window can be unsnapped later
+ RECT rc;
+ POINT cursor;
+ if (GetWindowRect(w32->window, &rc) && GetCursorPos(&cursor)) {
+ w32->snap_dx = cursor.x - rc.left;
+ w32->snap_dy = cursor.y - rc.top;
+ }
+ }
+ break;
+ case WM_EXITSIZEMOVE:
+ w32->moving = false;
+ break;
+ case WM_SIZE: {
+ const int w = LOWORD(lParam), h = HIWORD(lParam);
+ if (w > 0 && h > 0) {
+ w32->windowrc.right = w32->windowrc.left + w;
+ w32->windowrc.bottom = w32->windowrc.top + h;
+ signal_events(w32, VO_EVENT_RESIZE);
+ MP_VERBOSE(w32, "resize window: %d:%d\n", w, h);
+ }
+
+ // Window may have been minimized, maximized or restored
+ if (is_visible(w32->window)) {
+ WINDOWPLACEMENT wp = { .length = sizeof wp };
+ GetWindowPlacement(w32->window, &wp);
+
+ bool is_minimized = wp.showCmd == SW_SHOWMINIMIZED;
+ if (w32->opts->window_minimized != is_minimized) {
+ w32->opts->window_minimized = is_minimized;
+ m_config_cache_write_opt(w32->opts_cache,
+ &w32->opts->window_minimized);
+ }
+
+ bool is_maximized = wp.showCmd == SW_SHOWMAXIMIZED ||
+ (wp.showCmd == SW_SHOWMINIMIZED &&
+ (wp.flags & WPF_RESTORETOMAXIMIZED));
+ if (w32->opts->window_maximized != is_maximized) {
+ w32->opts->window_maximized = is_maximized;
+ m_config_cache_write_opt(w32->opts_cache,
+ &w32->opts->window_maximized);
+ }
+ }
+
+ signal_events(w32, VO_EVENT_WIN_STATE);
+
+ update_display_info(w32);
+ break;
+ }
+ case WM_SIZING:
+ if (w32->opts->keepaspect && w32->opts->keepaspect_window &&
+ !w32->current_fs && !w32->parent)
+ {
+ RECT *rc = (RECT*)lParam;
+ // get client area of the windows if it had the rect rc
+ // (subtracting the window borders)
+ RECT r = *rc;
+ subtract_window_borders(w32, w32->window, &r);
+ int c_w = rect_w(r), c_h = rect_h(r);
+ float aspect = w32->o_dwidth / (float) MPMAX(w32->o_dheight, 1);
+ int d_w = c_h * aspect - c_w;
+ int d_h = c_w / aspect - c_h;
+ int d_corners[4] = { d_w, d_h, -d_w, -d_h };
+ int corners[4] = { rc->left, rc->top, rc->right, rc->bottom };
+ int corner = get_resize_border(w32, wParam);
+ if (corner >= 0)
+ corners[corner] -= d_corners[corner];
+ *rc = (RECT) { corners[0], corners[1], corners[2], corners[3] };
+ return TRUE;
+ }
+ break;
+ case WM_DPICHANGED:
+ update_display_info(w32);
+
+ RECT *rc = (RECT*)lParam;
+ w32->windowrc = *rc;
+ subtract_window_borders(w32, w32->window, &w32->windowrc);
+ update_window_state(w32);
+ break;
+ case WM_CLOSE:
+ // Don't destroy the window yet to not lose wakeup events.
+ mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN);
+ return 0;
+ case WM_NCDESTROY: // Sometimes only WM_NCDESTROY is received in --wid mode
+ case WM_DESTROY:
+ if (w32->destroyed)
+ break;
+ // If terminate is not set, something else destroyed the window. This
+ // can also happen in --wid mode when the parent window is destroyed.
+ if (!w32->terminate)
+ mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN);
+ RevokeDragDrop(w32->window);
+ w32->destroyed = true;
+ w32->window = NULL;
+ PostQuitMessage(0);
+ break;
+ case WM_SYSCOMMAND:
+ switch (wParam & 0xFFF0) {
+ case SC_SCREENSAVE:
+ case SC_MONITORPOWER:
+ if (w32->disable_screensaver) {
+ MP_VERBOSE(w32, "killing screensaver\n");
+ return 0;
+ }
+ break;
+ case SC_RESTORE:
+ if (IsMaximized(w32->window) && w32->current_fs) {
+ w32->toggle_fs = true;
+ reinit_window_state(w32);
+
+ return 0;
+ }
+ break;
+ }
+ break;
+ case WM_NCACTIVATE:
+ // Cosmetic to remove blinking window border when initializing window
+ if (!w32->opts->border)
+ lParam = -1;
+ break;
+ case WM_NCHITTEST:
+ // Provide sizing handles for borderless windows
+ if ((!w32->opts->border || !w32->opts->title_bar) && !w32->current_fs) {
+ return borderless_nchittest(w32, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ }
+ break;
+ case WM_APPCOMMAND:
+ if (handle_appcommand(w32, GET_APPCOMMAND_LPARAM(lParam)))
+ return TRUE;
+ break;
+ case WM_SYSKEYDOWN:
+ // Open the window menu on Alt+Space. Normally DefWindowProc opens the
+ // window menu in response to WM_SYSCHAR, but since mpv translates its
+ // own keyboard input, WM_SYSCHAR isn't generated, so the window menu
+ // must be opened manually.
+ if (wParam == VK_SPACE) {
+ SendMessage(w32->window, WM_SYSCOMMAND, SC_KEYMENU, ' ');
+ return 0;
+ }
+
+ handle_key_down(w32, wParam, HIWORD(lParam));
+ if (wParam == VK_F10)
+ return 0;
+ break;
+ case WM_KEYDOWN:
+ handle_key_down(w32, wParam, HIWORD(lParam));
+ break;
+ case WM_SYSKEYUP:
+ case WM_KEYUP:
+ handle_key_up(w32, wParam, HIWORD(lParam));
+ if (wParam == VK_F10)
+ return 0;
+ break;
+ case WM_CHAR:
+ case WM_SYSCHAR:
+ if (handle_char(w32, wParam))
+ return 0;
+ break;
+ case WM_KILLFOCUS:
+ mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL);
+ w32->focused = false;
+ signal_events(w32, VO_EVENT_FOCUS);
+ return 0;
+ case WM_SETFOCUS:
+ w32->focused = true;
+ signal_events(w32, VO_EVENT_FOCUS);
+ return 0;
+ case WM_SETCURSOR:
+ // The cursor should only be hidden if the mouse is in the client area
+ // and if the window isn't in menu mode (HIWORD(lParam) is non-zero)
+ w32->can_set_cursor = LOWORD(lParam) == HTCLIENT && HIWORD(lParam);
+ if (w32->can_set_cursor && !w32->cursor_visible) {
+ SetCursor(NULL);
+ return TRUE;
+ }
+ break;
+ case WM_MOUSELEAVE:
+ w32->tracking = FALSE;
+ mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_LEAVE);
+ break;
+ case WM_MOUSEMOVE: {
+ if (!w32->tracking) {
+ w32->tracking = TrackMouseEvent(&w32->trackEvent);
+ mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_ENTER);
+ }
+ // Windows can send spurious mouse events, which would make the mpv
+ // core unhide the mouse cursor on completely unrelated events. See:
+ // https://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx
+ int x = GET_X_LPARAM(lParam);
+ int y = GET_Y_LPARAM(lParam);
+ if (x != w32->mouse_x || y != w32->mouse_y) {
+ w32->mouse_x = x;
+ w32->mouse_y = y;
+ mp_input_set_mouse_pos(w32->input_ctx, x, y);
+ }
+ break;
+ }
+ case WM_LBUTTONDOWN:
+ if (handle_mouse_down(w32, MP_MBTN_LEFT, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam)))
+ return 0;
+ break;
+ case WM_LBUTTONUP:
+ handle_mouse_up(w32, MP_MBTN_LEFT);
+ break;
+ case WM_MBUTTONDOWN:
+ handle_mouse_down(w32, MP_MBTN_MID, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ break;
+ case WM_MBUTTONUP:
+ handle_mouse_up(w32, MP_MBTN_MID);
+ break;
+ case WM_RBUTTONDOWN:
+ handle_mouse_down(w32, MP_MBTN_RIGHT, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ break;
+ case WM_RBUTTONUP:
+ handle_mouse_up(w32, MP_MBTN_RIGHT);
+ break;
+ case WM_MOUSEWHEEL:
+ handle_mouse_wheel(w32, false, GET_WHEEL_DELTA_WPARAM(wParam));
+ return 0;
+ case WM_MOUSEHWHEEL:
+ handle_mouse_wheel(w32, true, GET_WHEEL_DELTA_WPARAM(wParam));
+ // Some buggy mouse drivers (SetPoint) stop delivering WM_MOUSEHWHEEL
+ // events when the message loop doesn't return TRUE (even on Windows 7)
+ return TRUE;
+ case WM_XBUTTONDOWN:
+ handle_mouse_down(w32,
+ HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD,
+ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ break;
+ case WM_XBUTTONUP:
+ handle_mouse_up(w32,
+ HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD);
+ break;
+ case WM_DISPLAYCHANGE:
+ force_update_display_info(w32);
+ break;
+ case WM_SETTINGCHANGE:
+ update_dark_mode(w32);
+ break;
+ case WM_NCCALCSIZE:
+ if (!w32->opts->border)
+ return 0;
+ // Apparently removing WS_CAPTION disables some window animation, instead
+ // just reduce non-client size to remove title bar.
+ if (wParam && lParam && w32->opts->border && !w32->opts->title_bar &&
+ !w32->current_fs && !w32->parent)
+ {
+ ((LPNCCALCSIZE_PARAMS) lParam)->rgrc[0].top -= get_title_bar_height(w32);
+ }
+ break;
+ }
+
+ if (message == w32->tbtnCreatedMsg) {
+ w32->tbtnCreated = true;
+ update_playback_state(w32);
+ return 0;
+ }
+
+ return DefWindowProcW(hWnd, message, wParam, lParam);
+}
+
+static mp_once window_class_init_once = MP_STATIC_ONCE_INITIALIZER;
+static ATOM window_class;
+static void register_window_class(void)
+{
+ window_class = RegisterClassExW(&(WNDCLASSEXW) {
+ .cbSize = sizeof(WNDCLASSEXW),
+ .style = CS_HREDRAW | CS_VREDRAW,
+ .lpfnWndProc = WndProc,
+ .hInstance = HINST_THISCOMPONENT,
+ .hIcon = LoadIconW(HINST_THISCOMPONENT, L"IDI_ICON1"),
+ .hCursor = LoadCursor(NULL, IDC_ARROW),
+ .hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH),
+ .lpszClassName = L"mpv",
+ });
+}
+
+static ATOM get_window_class(void)
+{
+ mp_exec_once(&window_class_init_once, register_window_class);
+ return window_class;
+}
+
+static void resize_child_win(HWND parent)
+{
+ // Check if an mpv window is a child of this window. This will not
+ // necessarily be the case because the hook functions will run for all
+ // windows on the parent window's thread.
+ ATOM cls = get_window_class();
+ HWND child = FindWindowExW(parent, NULL, (LPWSTR)MAKEINTATOM(cls), NULL);
+ if (!child)
+ return;
+ // Make sure the window was created by this instance
+ if (GetWindowLongPtrW(child, GWLP_HINSTANCE) != (LONG_PTR)HINST_THISCOMPONENT)
+ return;
+
+ // Resize the mpv window to match its parent window's size
+ RECT rm, rp;
+ if (!GetClientRect(child, &rm))
+ return;
+ if (!GetClientRect(parent, &rp))
+ return;
+ if (EqualRect(&rm, &rp))
+ return;
+ SetWindowPos(child, NULL, 0, 0, rp.right, rp.bottom, SWP_ASYNCWINDOWPOS |
+ SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING);
+}
+
+static LRESULT CALLBACK parent_win_hook(int nCode, WPARAM wParam, LPARAM lParam)
+{
+ if (nCode != HC_ACTION)
+ goto done;
+ CWPSTRUCT *cwp = (CWPSTRUCT*)lParam;
+ if (cwp->message != WM_WINDOWPOSCHANGED)
+ goto done;
+ resize_child_win(cwp->hwnd);
+done:
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+}
+
+static void CALLBACK parent_evt_hook(HWINEVENTHOOK hWinEventHook, DWORD event,
+ HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread,
+ DWORD dwmsEventTime)
+{
+ if (event != EVENT_OBJECT_LOCATIONCHANGE)
+ return;
+ if (!hwnd || idObject != OBJID_WINDOW || idChild != CHILDID_SELF)
+ return;
+ resize_child_win(hwnd);
+}
+
+static void install_parent_hook(struct vo_w32_state *w32)
+{
+ DWORD pid;
+ DWORD tid = GetWindowThreadProcessId(w32->parent, &pid);
+
+ // If the parent lives inside the current process, install a Windows hook
+ if (pid == GetCurrentProcessId()) {
+ w32->parent_win_hook = SetWindowsHookExW(WH_CALLWNDPROC,
+ parent_win_hook, NULL, tid);
+ } else {
+ // Otherwise, use a WinEvent hook. These don't seem to be as smooth as
+ // Windows hooks, but they can be delivered across process boundaries.
+ w32->parent_evt_hook = SetWinEventHook(
+ EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE,
+ NULL, parent_evt_hook, pid, tid, WINEVENT_OUTOFCONTEXT);
+ }
+}
+
+static void remove_parent_hook(struct vo_w32_state *w32)
+{
+ if (w32->parent_win_hook)
+ UnhookWindowsHookEx(w32->parent_win_hook);
+ if (w32->parent_evt_hook)
+ UnhookWinEvent(w32->parent_evt_hook);
+}
+
+// Dispatch incoming window events and handle them.
+// This returns only when the thread is asked to terminate.
+static void run_message_loop(struct vo_w32_state *w32)
+{
+ MSG msg;
+ while (GetMessageW(&msg, 0, 0, 0) > 0)
+ DispatchMessageW(&msg);
+
+ // Even if the message loop somehow exits, we still have to respond to
+ // external requests until termination is requested.
+ while (!w32->terminate)
+ mp_dispatch_queue_process(w32->dispatch, 1000);
+}
+
+static void gui_thread_reconfig(void *ptr)
+{
+ struct vo_w32_state *w32 = ptr;
+ struct vo *vo = w32->vo;
+
+ RECT r = get_working_area(w32);
+ // for normal window which is auto-positioned (centered), center the window
+ // rather than the content (by subtracting the borders from the work area)
+ if (!w32->current_fs && !IsMaximized(w32->window) && w32->opts->border &&
+ !w32->opts->geometry.xy_valid /* specific position not requested */)
+ {
+ subtract_window_borders(w32, w32->window, &r);
+ }
+ struct mp_rect screen = { r.left, r.top, r.right, r.bottom };
+ struct vo_win_geometry geo;
+
+ RECT monrc = get_monitor_info(w32).rcMonitor;
+ struct mp_rect mon = { monrc.left, monrc.top, monrc.right, monrc.bottom };
+
+ if (w32->dpi_scale == 0)
+ force_update_display_info(w32);
+
+ vo_calc_window_geometry3(vo, &screen, &mon, w32->dpi_scale, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ bool reset_size = (w32->o_dwidth != vo->dwidth ||
+ w32->o_dheight != vo->dheight) &&
+ w32->opts->auto_window_resize;
+
+ w32->o_dwidth = vo->dwidth;
+ w32->o_dheight = vo->dheight;
+
+ if (!w32->parent && !w32->window_bounds_initialized) {
+ SetRect(&w32->windowrc, geo.win.x0, geo.win.y0,
+ geo.win.x0 + vo->dwidth, geo.win.y0 + vo->dheight);
+ w32->prev_windowrc = w32->windowrc;
+ w32->window_bounds_initialized = true;
+ w32->win_force_pos = geo.flags & VO_WIN_FORCE_POS;
+ w32->fit_on_screen = !w32->win_force_pos;
+ goto finish;
+ }
+
+ // The rect which size is going to be modified.
+ RECT *rc = &w32->windowrc;
+
+ // The desired size always matches the window size in wid mode.
+ if (!reset_size || w32->parent) {
+ GetClientRect(w32->window, &r);
+ // Restore vo_dwidth and vo_dheight, which were reset in vo_config()
+ vo->dwidth = r.right;
+ vo->dheight = r.bottom;
+ } else {
+ if (w32->current_fs)
+ rc = &w32->prev_windowrc;
+ w32->fit_on_screen = true;
+ }
+
+ resize_and_move_rect(w32, rc, vo->dwidth, vo->dheight);
+
+finish:
+ reinit_window_state(w32);
+}
+
+// Resize the window. On the first call, it's also made visible.
+void vo_w32_config(struct vo *vo)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ mp_dispatch_run(w32->dispatch, gui_thread_reconfig, w32);
+}
+
+static void w32_api_load(struct vo_w32_state *w32)
+{
+ HMODULE shcore_dll = LoadLibraryW(L"shcore.dll");
+ // Available since Win8.1
+ w32->api.pGetDpiForMonitor = !shcore_dll ? NULL :
+ (void *)GetProcAddress(shcore_dll, "GetDpiForMonitor");
+
+ HMODULE user32_dll = LoadLibraryW(L"user32.dll");
+ // Available since Win10
+ w32->api.pAdjustWindowRectExForDpi = !user32_dll ? NULL :
+ (void *)GetProcAddress(user32_dll, "AdjustWindowRectExForDpi");
+
+ // imm32.dll must be loaded dynamically
+ // to account for machines without East Asian language support
+ HMODULE imm32_dll = LoadLibraryW(L"imm32.dll");
+ w32->api.pImmDisableIME = !imm32_dll ? NULL :
+ (void *)GetProcAddress(imm32_dll, "ImmDisableIME");
+
+ // Dark mode related functions, available since the 1809 Windows 10 update
+ // Check the Windows build version as on previous versions used ordinals
+ // may point to unexpected code/data. Alternatively could check uxtheme.dll
+ // version directly, but it is little bit more boilerplate code, and build
+ // number is good enough check.
+ void (WINAPI *pRtlGetNtVersionNumbers)(LPDWORD, LPDWORD, LPDWORD) =
+ (void *)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers");
+
+ DWORD major, build;
+ pRtlGetNtVersionNumbers(&major, NULL, &build);
+ build &= ~0xF0000000;
+
+ HMODULE uxtheme_dll = (major < 10 || build < 17763) ? NULL :
+ GetModuleHandle(L"uxtheme.dll");
+ w32->api.pShouldAppsUseDarkMode = !uxtheme_dll ? NULL :
+ (void *)GetProcAddress(uxtheme_dll, MAKEINTRESOURCEA(132));
+ w32->api.pSetPreferredAppMode = !uxtheme_dll ? NULL :
+ (void *)GetProcAddress(uxtheme_dll, MAKEINTRESOURCEA(135));
+}
+
+static MP_THREAD_VOID gui_thread(void *ptr)
+{
+ struct vo_w32_state *w32 = ptr;
+ bool ole_ok = false;
+ int res = 0;
+
+ mp_thread_set_name("window");
+
+ w32_api_load(w32);
+
+ // Disables the IME for windows on this thread
+ if (w32->api.pImmDisableIME)
+ w32->api.pImmDisableIME(0);
+
+ if (w32->opts->WinID >= 0)
+ w32->parent = (HWND)(intptr_t)(w32->opts->WinID);
+
+ ATOM cls = get_window_class();
+ if (w32->parent) {
+ RECT r;
+ GetClientRect(w32->parent, &r);
+ CreateWindowExW(WS_EX_NOPARENTNOTIFY, (LPWSTR)MAKEINTATOM(cls), L"mpv",
+ WS_CHILD | WS_VISIBLE, 0, 0, r.right, r.bottom,
+ w32->parent, 0, HINST_THISCOMPONENT, w32);
+
+ // Install a hook to get notifications when the parent changes size
+ if (w32->window)
+ install_parent_hook(w32);
+ } else {
+ CreateWindowExW(0, (LPWSTR)MAKEINTATOM(cls), L"mpv",
+ update_style(w32, 0), CW_USEDEFAULT, SW_HIDE, 100, 100,
+ 0, 0, HINST_THISCOMPONENT, w32);
+ }
+
+ if (!w32->window) {
+ MP_ERR(w32, "unable to create window!\n");
+ goto done;
+ }
+
+ update_dark_mode(w32);
+ update_corners_pref(w32);
+ if (w32->opts->window_affinity)
+ update_affinity(w32);
+ if (w32->opts->backdrop_type)
+ update_backdrop(w32);
+
+ if (SUCCEEDED(OleInitialize(NULL))) {
+ ole_ok = true;
+
+ IDropTarget *dt = mp_w32_droptarget_create(w32->log, w32->opts, w32->input_ctx);
+ RegisterDragDrop(w32->window, dt);
+
+ // ITaskbarList2 has the MarkFullscreenWindow method, which is used to
+ // make sure the taskbar is hidden when mpv goes fullscreen
+ if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL,
+ CLSCTX_INPROC_SERVER, &IID_ITaskbarList2,
+ (void**)&w32->taskbar_list)))
+ {
+ if (FAILED(ITaskbarList2_HrInit(w32->taskbar_list))) {
+ ITaskbarList2_Release(w32->taskbar_list);
+ w32->taskbar_list = NULL;
+ }
+ }
+
+ // ITaskbarList3 has methods for status indication on taskbar buttons,
+ // however that interface is only available on Win7/2008 R2 or newer
+ if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL,
+ CLSCTX_INPROC_SERVER, &IID_ITaskbarList3,
+ (void**)&w32->taskbar_list3)))
+ {
+ if (FAILED(ITaskbarList3_HrInit(w32->taskbar_list3))) {
+ ITaskbarList3_Release(w32->taskbar_list3);
+ w32->taskbar_list3 = NULL;
+ } else {
+ w32->tbtnCreatedMsg = RegisterWindowMessage(L"TaskbarButtonCreated");
+ }
+ }
+ } else {
+ MP_ERR(w32, "Failed to initialize OLE/COM\n");
+ }
+
+ w32->tracking = FALSE;
+ w32->trackEvent = (TRACKMOUSEEVENT){
+ .cbSize = sizeof(TRACKMOUSEEVENT),
+ .dwFlags = TME_LEAVE,
+ .hwndTrack = w32->window,
+ };
+
+ if (w32->parent)
+ EnableWindow(w32->window, 0);
+
+ w32->cursor_visible = true;
+ w32->moving = false;
+ w32->snapped = 0;
+ w32->snap_dx = w32->snap_dy = 0;
+
+ mp_dispatch_set_wakeup_fn(w32->dispatch, wakeup_gui_thread, w32);
+
+ res = 1;
+done:
+
+ mp_rendezvous(w32, res); // init barrier
+
+ // This blocks until the GUI thread is to be exited.
+ if (res)
+ run_message_loop(w32);
+
+ MP_VERBOSE(w32, "uninit\n");
+
+ remove_parent_hook(w32);
+ if (w32->window && !w32->destroyed)
+ DestroyWindow(w32->window);
+ if (w32->taskbar_list)
+ ITaskbarList2_Release(w32->taskbar_list);
+ if (w32->taskbar_list3)
+ ITaskbarList3_Release(w32->taskbar_list3);
+ if (ole_ok)
+ OleUninitialize();
+ SetThreadExecutionState(ES_CONTINUOUS);
+ MP_THREAD_RETURN();
+}
+
+bool vo_w32_init(struct vo *vo)
+{
+ assert(!vo->w32);
+
+ struct vo_w32_state *w32 = talloc_ptrtype(vo, w32);
+ *w32 = (struct vo_w32_state){
+ .log = mp_log_new(w32, vo->log, "win32"),
+ .vo = vo,
+ .opts_cache = m_config_cache_alloc(w32, vo->global, &vo_sub_opts),
+ .input_ctx = vo->input_ctx,
+ .dispatch = mp_dispatch_create(w32),
+ };
+ w32->opts = w32->opts_cache->opts;
+ vo->w32 = w32;
+
+ if (mp_thread_create(&w32->thread, gui_thread, w32))
+ goto fail;
+
+ if (!mp_rendezvous(w32, 0)) { // init barrier
+ mp_thread_join(w32->thread);
+ goto fail;
+ }
+
+ // While the UI runs in its own thread, the thread in which this function
+ // runs in will be the renderer thread. Apply magic MMCSS cargo-cult,
+ // which might stop Windows from throttling clock rate and so on.
+ if (vo->opts->mmcss_profile[0]) {
+ wchar_t *profile = mp_from_utf8(NULL, vo->opts->mmcss_profile);
+ w32->avrt_handle = AvSetMmThreadCharacteristicsW(profile, &(DWORD){0});
+ talloc_free(profile);
+ }
+
+ return true;
+fail:
+ talloc_free(w32);
+ vo->w32 = NULL;
+ return false;
+}
+
+struct disp_names_data {
+ HMONITOR assoc;
+ int count;
+ char **names;
+};
+
+static BOOL CALLBACK disp_names_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p)
+{
+ struct disp_names_data *data = (struct disp_names_data*)p;
+
+ // get_disp_names() adds data->assoc to the list, so skip it here
+ if (mon == data->assoc)
+ return TRUE;
+
+ MONITORINFOEXW mi = { .cbSize = sizeof mi };
+ if (GetMonitorInfoW(mon, (MONITORINFO*)&mi)) {
+ MP_TARRAY_APPEND(NULL, data->names, data->count,
+ mp_to_utf8(NULL, mi.szDevice));
+ }
+ return TRUE;
+}
+
+static char **get_disp_names(struct vo_w32_state *w32)
+{
+ // Get the client area of the window in screen space
+ RECT rect = { 0 };
+ GetClientRect(w32->window, &rect);
+ MapWindowPoints(w32->window, NULL, (POINT*)&rect, 2);
+
+ struct disp_names_data data = { .assoc = w32->monitor };
+
+ // Make sure the monitor that Windows considers to be associated with the
+ // window is first in the list
+ MONITORINFOEXW mi = { .cbSize = sizeof mi };
+ if (GetMonitorInfoW(data.assoc, (MONITORINFO*)&mi)) {
+ MP_TARRAY_APPEND(NULL, data.names, data.count,
+ mp_to_utf8(NULL, mi.szDevice));
+ }
+
+ // Get the names of the other monitors that intersect the client rect
+ EnumDisplayMonitors(NULL, &rect, disp_names_proc, (LPARAM)&data);
+ MP_TARRAY_APPEND(NULL, data.names, data.count, NULL);
+ return data.names;
+}
+
+static int gui_thread_control(struct vo_w32_state *w32, int request, void *arg)
+{
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *changed_option;
+
+ while (m_config_cache_get_next_changed(w32->opts_cache,
+ &changed_option))
+ {
+ struct mp_vo_opts *vo_opts = w32->opts_cache->opts;
+
+ if (changed_option == &vo_opts->fullscreen) {
+ reinit_window_state(w32);
+ } else if (changed_option == &vo_opts->window_affinity) {
+ update_affinity(w32);
+ } else if (changed_option == &vo_opts->ontop) {
+ update_window_state(w32);
+ } else if (changed_option == &vo_opts->backdrop_type) {
+ update_backdrop(w32);
+ } else if (changed_option == &vo_opts->border ||
+ changed_option == &vo_opts->title_bar)
+ {
+ update_window_style(w32);
+ update_window_state(w32);
+ } else if (changed_option == &vo_opts->window_minimized) {
+ update_minimized_state(w32);
+ } else if (changed_option == &vo_opts->window_maximized) {
+ update_maximized_state(w32);
+ } else if (changed_option == &vo_opts->window_corners) {
+ update_corners_pref(w32);
+ }
+ }
+
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_WINDOW_ID: {
+ if (!w32->window)
+ return VO_NOTAVAIL;
+ *(int64_t *)arg = (intptr_t)w32->window;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_HIDPI_SCALE: {
+ *(double *)arg = w32->dpi_scale;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+
+ if (!w32->window_bounds_initialized)
+ return VO_FALSE;
+
+ RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc;
+ s[0] = rect_w(*rc) / w32->dpi_scale;
+ s[1] = rect_h(*rc) / w32->dpi_scale;
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+
+ if (!w32->window_bounds_initialized)
+ return VO_FALSE;
+
+ s[0] *= w32->dpi_scale;
+ s[1] *= w32->dpi_scale;
+
+ RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc;
+ resize_and_move_rect(w32, rc, s[0], s[1]);
+
+ w32->fit_on_screen = true;
+ reinit_window_state(w32);
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ w32->cursor_visible = *(bool *)arg;
+
+ if (w32->can_set_cursor && w32->tracking) {
+ if (w32->cursor_visible)
+ SetCursor(LoadCursor(NULL, IDC_ARROW));
+ else
+ SetCursor(NULL);
+ }
+ return VO_TRUE;
+ case VOCTRL_KILL_SCREENSAVER:
+ w32->disable_screensaver = true;
+ SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED |
+ ES_DISPLAY_REQUIRED);
+ return VO_TRUE;
+ case VOCTRL_RESTORE_SCREENSAVER:
+ w32->disable_screensaver = false;
+ SetThreadExecutionState(ES_CONTINUOUS);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE: {
+ wchar_t *title = mp_from_utf8(NULL, (char *)arg);
+ SetWindowTextW(w32->window, title);
+ talloc_free(title);
+ return VO_TRUE;
+ }
+ case VOCTRL_UPDATE_PLAYBACK_STATE: {
+ w32->current_pstate = *(struct voctrl_playback_state *)arg;
+
+ update_playback_state(w32);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_FPS:
+ update_display_info(w32);
+ *(double*) arg = w32->display_fps;
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_RES: ;
+ RECT monrc = get_monitor_info(w32).rcMonitor;
+ ((int *)arg)[0] = monrc.right - monrc.left;
+ ((int *)arg)[1] = monrc.bottom - monrc.top;
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_NAMES:
+ *(char ***)arg = get_disp_names(w32);
+ return VO_TRUE;
+ case VOCTRL_GET_ICC_PROFILE:
+ update_display_info(w32);
+ if (w32->color_profile) {
+ bstr *p = arg;
+ *p = stream_read_file(w32->color_profile, NULL,
+ w32->vo->global, 100000000); // 100 MB
+ return p->len ? VO_TRUE : VO_FALSE;
+ }
+ return VO_FALSE;
+ case VOCTRL_GET_FOCUSED:
+ *(bool *)arg = w32->focused;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+static void do_control(void *ptr)
+{
+ void **p = ptr;
+ struct vo_w32_state *w32 = p[0];
+ int *events = p[1];
+ int request = *(int *)p[2];
+ void *arg = p[3];
+ int *ret = p[4];
+ *ret = gui_thread_control(w32, request, arg);
+ *events |= atomic_fetch_and(&w32->event_flags, 0);
+ // Safe access, since caller (owner of vo) is blocked.
+ if (*events & VO_EVENT_RESIZE) {
+ w32->vo->dwidth = rect_w(w32->windowrc);
+ w32->vo->dheight = rect_h(w32->windowrc);
+ }
+}
+
+int vo_w32_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ if (request == VOCTRL_CHECK_EVENTS) {
+ *events |= atomic_fetch_and(&w32->event_flags, 0);
+ if (*events & VO_EVENT_RESIZE) {
+ mp_dispatch_lock(w32->dispatch);
+ vo->dwidth = rect_w(w32->windowrc);
+ vo->dheight = rect_h(w32->windowrc);
+ mp_dispatch_unlock(w32->dispatch);
+ }
+ return VO_TRUE;
+ } else {
+ int r;
+ void *p[] = {w32, events, &request, arg, &r};
+ mp_dispatch_run(w32->dispatch, do_control, p);
+ return r;
+ }
+}
+
+static void do_terminate(void *ptr)
+{
+ struct vo_w32_state *w32 = ptr;
+ w32->terminate = true;
+
+ if (!w32->destroyed)
+ DestroyWindow(w32->window);
+
+ mp_dispatch_interrupt(w32->dispatch);
+}
+
+void vo_w32_uninit(struct vo *vo)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ if (!w32)
+ return;
+
+ mp_dispatch_run(w32->dispatch, do_terminate, w32);
+ mp_thread_join(w32->thread);
+
+ AvRevertMmThreadCharacteristics(w32->avrt_handle);
+
+ talloc_free(w32);
+ vo->w32 = NULL;
+}
+
+HWND vo_w32_hwnd(struct vo *vo)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ return w32->window; // immutable, so no synchronization needed
+}
+
+void vo_w32_run_on_thread(struct vo *vo, void (*cb)(void *ctx), void *ctx)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ mp_dispatch_run(w32->dispatch, cb, ctx);
+}
diff --git a/video/out/w32_common.h b/video/out/w32_common.h
new file mode 100644
index 0000000..528b216
--- /dev/null
+++ b/video/out/w32_common.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_W32_COMMON_H
+#define MPLAYER_W32_COMMON_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <windows.h>
+
+#include "common/common.h"
+
+struct vo;
+
+bool vo_w32_init(struct vo *vo);
+void vo_w32_uninit(struct vo *vo);
+int vo_w32_control(struct vo *vo, int *events, int request, void *arg);
+void vo_w32_config(struct vo *vo);
+HWND vo_w32_hwnd(struct vo *vo);
+void vo_w32_run_on_thread(struct vo *vo, void (*cb)(void *ctx), void *ctx);
+
+#endif /* MPLAYER_W32_COMMON_H */
diff --git a/video/out/wayland_common.c b/video/out/wayland_common.c
new file mode 100644
index 0000000..589135f
--- /dev/null
+++ b/video/out/wayland_common.c
@@ -0,0 +1,2629 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <linux/input-event-codes.h>
+#include <poll.h>
+#include <time.h>
+#include <unistd.h>
+#include <wayland-cursor.h>
+#include <xkbcommon/xkbcommon.h>
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "options/m_config.h"
+#include "osdep/io.h"
+#include "osdep/poll_wrapper.h"
+#include "osdep/timer.h"
+#include "present_sync.h"
+#include "wayland_common.h"
+#include "win_state.h"
+
+// Generated from wayland-protocols
+#include "idle-inhibit-unstable-v1.h"
+#include "linux-dmabuf-unstable-v1.h"
+#include "presentation-time.h"
+#include "xdg-decoration-unstable-v1.h"
+#include "xdg-shell.h"
+#include "viewporter.h"
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+#include "content-type-v1.h"
+#include "single-pixel-buffer-v1.h"
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+#include "fractional-scale-v1.h"
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+#include "cursor-shape-v1.h"
+#endif
+
+#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 22
+#define HAVE_WAYLAND_1_22
+#endif
+
+#ifndef CLOCK_MONOTONIC_RAW
+#define CLOCK_MONOTONIC_RAW 4
+#endif
+
+#ifndef XDG_TOPLEVEL_STATE_SUSPENDED
+#define XDG_TOPLEVEL_STATE_SUSPENDED 9
+#endif
+
+
+static const struct mp_keymap keymap[] = {
+ /* Special keys */
+ {XKB_KEY_Pause, MP_KEY_PAUSE}, {XKB_KEY_Escape, MP_KEY_ESC},
+ {XKB_KEY_BackSpace, MP_KEY_BS}, {XKB_KEY_Tab, MP_KEY_TAB},
+ {XKB_KEY_Return, MP_KEY_ENTER}, {XKB_KEY_Menu, MP_KEY_MENU},
+ {XKB_KEY_Print, MP_KEY_PRINT}, {XKB_KEY_ISO_Left_Tab, MP_KEY_TAB},
+
+ /* Cursor keys */
+ {XKB_KEY_Left, MP_KEY_LEFT}, {XKB_KEY_Right, MP_KEY_RIGHT},
+ {XKB_KEY_Up, MP_KEY_UP}, {XKB_KEY_Down, MP_KEY_DOWN},
+
+ /* Navigation keys */
+ {XKB_KEY_Insert, MP_KEY_INSERT}, {XKB_KEY_Delete, MP_KEY_DELETE},
+ {XKB_KEY_Home, MP_KEY_HOME}, {XKB_KEY_End, MP_KEY_END},
+ {XKB_KEY_Page_Up, MP_KEY_PAGE_UP}, {XKB_KEY_Page_Down, MP_KEY_PAGE_DOWN},
+
+ /* F-keys */
+ {XKB_KEY_F1, MP_KEY_F + 1}, {XKB_KEY_F2, MP_KEY_F + 2},
+ {XKB_KEY_F3, MP_KEY_F + 3}, {XKB_KEY_F4, MP_KEY_F + 4},
+ {XKB_KEY_F5, MP_KEY_F + 5}, {XKB_KEY_F6, MP_KEY_F + 6},
+ {XKB_KEY_F7, MP_KEY_F + 7}, {XKB_KEY_F8, MP_KEY_F + 8},
+ {XKB_KEY_F9, MP_KEY_F + 9}, {XKB_KEY_F10, MP_KEY_F +10},
+ {XKB_KEY_F11, MP_KEY_F +11}, {XKB_KEY_F12, MP_KEY_F +12},
+ {XKB_KEY_F13, MP_KEY_F +13}, {XKB_KEY_F14, MP_KEY_F +14},
+ {XKB_KEY_F15, MP_KEY_F +15}, {XKB_KEY_F16, MP_KEY_F +16},
+ {XKB_KEY_F17, MP_KEY_F +17}, {XKB_KEY_F18, MP_KEY_F +18},
+ {XKB_KEY_F19, MP_KEY_F +19}, {XKB_KEY_F20, MP_KEY_F +20},
+ {XKB_KEY_F21, MP_KEY_F +21}, {XKB_KEY_F22, MP_KEY_F +22},
+ {XKB_KEY_F23, MP_KEY_F +23}, {XKB_KEY_F24, MP_KEY_F +24},
+
+ /* Numpad independent of numlock */
+ {XKB_KEY_KP_Subtract, '-'}, {XKB_KEY_KP_Add, '+'},
+ {XKB_KEY_KP_Multiply, '*'}, {XKB_KEY_KP_Divide, '/'},
+ {XKB_KEY_KP_Enter, MP_KEY_KPENTER},
+
+ /* Numpad with numlock */
+ {XKB_KEY_KP_0, MP_KEY_KP0}, {XKB_KEY_KP_1, MP_KEY_KP1},
+ {XKB_KEY_KP_2, MP_KEY_KP2}, {XKB_KEY_KP_3, MP_KEY_KP3},
+ {XKB_KEY_KP_4, MP_KEY_KP4}, {XKB_KEY_KP_5, MP_KEY_KP5},
+ {XKB_KEY_KP_6, MP_KEY_KP6}, {XKB_KEY_KP_7, MP_KEY_KP7},
+ {XKB_KEY_KP_8, MP_KEY_KP8}, {XKB_KEY_KP_9, MP_KEY_KP9},
+ {XKB_KEY_KP_Decimal, MP_KEY_KPDEC}, {XKB_KEY_KP_Separator, MP_KEY_KPDEC},
+
+ /* Numpad without numlock */
+ {XKB_KEY_KP_Insert, MP_KEY_KPINS}, {XKB_KEY_KP_End, MP_KEY_KPEND},
+ {XKB_KEY_KP_Down, MP_KEY_KPDOWN}, {XKB_KEY_KP_Page_Down, MP_KEY_KPPGDOWN},
+ {XKB_KEY_KP_Left, MP_KEY_KPLEFT}, {XKB_KEY_KP_Begin, MP_KEY_KP5},
+ {XKB_KEY_KP_Right, MP_KEY_KPRIGHT}, {XKB_KEY_KP_Home, MP_KEY_KPHOME},
+ {XKB_KEY_KP_Up, MP_KEY_KPUP}, {XKB_KEY_KP_Page_Up, MP_KEY_KPPGUP},
+ {XKB_KEY_KP_Delete, MP_KEY_KPDEL},
+
+ /* Multimedia keys */
+ {XKB_KEY_XF86MenuKB, MP_KEY_MENU},
+ {XKB_KEY_XF86AudioPlay, MP_KEY_PLAY}, {XKB_KEY_XF86AudioPause, MP_KEY_PAUSE},
+ {XKB_KEY_XF86AudioStop, MP_KEY_STOP},
+ {XKB_KEY_XF86AudioPrev, MP_KEY_PREV}, {XKB_KEY_XF86AudioNext, MP_KEY_NEXT},
+ {XKB_KEY_XF86AudioRewind, MP_KEY_REWIND},
+ {XKB_KEY_XF86AudioForward, MP_KEY_FORWARD},
+ {XKB_KEY_XF86AudioMute, MP_KEY_MUTE},
+ {XKB_KEY_XF86AudioLowerVolume, MP_KEY_VOLUME_DOWN},
+ {XKB_KEY_XF86AudioRaiseVolume, MP_KEY_VOLUME_UP},
+ {XKB_KEY_XF86HomePage, MP_KEY_HOMEPAGE}, {XKB_KEY_XF86WWW, MP_KEY_WWW},
+ {XKB_KEY_XF86Mail, MP_KEY_MAIL}, {XKB_KEY_XF86Favorites, MP_KEY_FAVORITES},
+ {XKB_KEY_XF86Search, MP_KEY_SEARCH}, {XKB_KEY_XF86Sleep, MP_KEY_SLEEP},
+ {XKB_KEY_XF86Back, MP_KEY_BACK}, {XKB_KEY_XF86Tools, MP_KEY_TOOLS},
+ {XKB_KEY_XF86ZoomIn, MP_KEY_ZOOMIN}, {XKB_KEY_XF86ZoomOut, MP_KEY_ZOOMOUT},
+
+ {0, 0}
+};
+
+#define OPT_BASE_STRUCT struct wayland_opts
+const struct m_sub_options wayland_conf = {
+ .opts = (const struct m_option[]) {
+ {"wayland-configure-bounds", OPT_CHOICE(configure_bounds,
+ {"auto", -1}, {"no", 0}, {"yes", 1})},
+ {"wayland-disable-vsync", OPT_BOOL(disable_vsync)},
+ {"wayland-edge-pixels-pointer", OPT_INT(edge_pixels_pointer),
+ M_RANGE(0, INT_MAX)},
+ {"wayland-edge-pixels-touch", OPT_INT(edge_pixels_touch),
+ M_RANGE(0, INT_MAX)},
+ {0},
+ },
+ .size = sizeof(struct wayland_opts),
+ .defaults = &(struct wayland_opts) {
+ .configure_bounds = -1,
+ .edge_pixels_pointer = 16,
+ .edge_pixels_touch = 32,
+ },
+};
+
+struct vo_wayland_feedback_pool {
+ struct wp_presentation_feedback **fback;
+ struct vo_wayland_state *wl;
+ int len;
+};
+
+struct vo_wayland_output {
+ struct vo_wayland_state *wl;
+ struct wl_output *output;
+ struct mp_rect geometry;
+ bool has_surface;
+ uint32_t id;
+ uint32_t flags;
+ int phys_width;
+ int phys_height;
+ int scale;
+ double refresh_rate;
+ char *make;
+ char *model;
+ char *name;
+ struct wl_list link;
+};
+
+static int check_for_resize(struct vo_wayland_state *wl, int edge_pixels,
+ enum xdg_toplevel_resize_edge *edge);
+static int get_mods(struct vo_wayland_state *wl);
+static int lookupkey(int key);
+static int set_cursor_visibility(struct vo_wayland_state *wl, bool on);
+static int spawn_cursor(struct vo_wayland_state *wl);
+
+static void add_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback);
+static void get_shape_device(struct vo_wayland_state *wl);
+static int greatest_common_divisor(int a, int b);
+static void guess_focus(struct vo_wayland_state *wl);
+static void prepare_resize(struct vo_wayland_state *wl, int width, int height);
+static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback);
+static void remove_output(struct vo_wayland_output *out);
+static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode);
+static void rescale_geometry(struct vo_wayland_state *wl, double old_scale);
+static void set_geometry(struct vo_wayland_state *wl, bool resize);
+static void set_surface_scaling(struct vo_wayland_state *wl);
+static void window_move(struct vo_wayland_state *wl, uint32_t serial);
+
+/* Wayland listener boilerplate */
+static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
+ uint32_t serial, struct wl_surface *surface,
+ wl_fixed_t sx, wl_fixed_t sy)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->pointer = pointer;
+ wl->pointer_id = serial;
+
+ set_cursor_visibility(wl, wl->cursor_visible);
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_ENTER);
+}
+
+static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
+ uint32_t serial, struct wl_surface *surface)
+{
+ struct vo_wayland_state *wl = data;
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_LEAVE);
+}
+
+static void pointer_handle_motion(void *data, struct wl_pointer *pointer,
+ uint32_t time, wl_fixed_t sx, wl_fixed_t sy)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->mouse_x = wl_fixed_to_int(sx) * wl->scaling;
+ wl->mouse_y = wl_fixed_to_int(sy) * wl->scaling;
+
+ if (!wl->toplevel_configured)
+ mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y);
+ wl->toplevel_configured = false;
+}
+
+static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer,
+ uint32_t serial, uint32_t time, uint32_t button,
+ uint32_t state)
+{
+ struct vo_wayland_state *wl = data;
+ state = state == WL_POINTER_BUTTON_STATE_PRESSED ? MP_KEY_STATE_DOWN
+ : MP_KEY_STATE_UP;
+
+ if (button >= BTN_MOUSE && button < BTN_JOYSTICK) {
+ switch (button) {
+ case BTN_LEFT:
+ button = MP_MBTN_LEFT;
+ break;
+ case BTN_MIDDLE:
+ button = MP_MBTN_MID;
+ break;
+ case BTN_RIGHT:
+ button = MP_MBTN_RIGHT;
+ break;
+ case BTN_SIDE:
+ button = MP_MBTN_BACK;
+ break;
+ case BTN_EXTRA:
+ button = MP_MBTN_FORWARD;
+ break;
+ default:
+ button += MP_MBTN9 - BTN_FORWARD;
+ break;
+ }
+ } else {
+ button = 0;
+ }
+
+ if (button)
+ mp_input_put_key(wl->vo->input_ctx, button | state | wl->mpmod);
+
+ if (!mp_input_test_dragging(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y) &&
+ !wl->locked_size && (button == MP_MBTN_LEFT) && (state == MP_KEY_STATE_DOWN))
+ {
+ uint32_t edges;
+ // Implement an edge resize zone if there are no decorations
+ if (!wl->vo_opts->border && check_for_resize(wl, wl->opts->edge_pixels_pointer, &edges)) {
+ xdg_toplevel_resize(wl->xdg_toplevel, wl->seat, serial, edges);
+ } else {
+ window_move(wl, serial);
+ }
+ // Explicitly send an UP event after the client finishes a move/resize
+ mp_input_put_key(wl->vo->input_ctx, button | MP_KEY_STATE_UP);
+ }
+}
+
+static void pointer_handle_axis(void *data, struct wl_pointer *wl_pointer,
+ uint32_t time, uint32_t axis, wl_fixed_t value)
+{
+ struct vo_wayland_state *wl = data;
+
+ double val = wl_fixed_to_double(value) < 0 ? -1 : 1;
+ switch (axis) {
+ case WL_POINTER_AXIS_VERTICAL_SCROLL:
+ if (value > 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_DOWN | wl->mpmod, +val);
+ if (value < 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_UP | wl->mpmod, -val);
+ break;
+ case WL_POINTER_AXIS_HORIZONTAL_SCROLL:
+ if (value > 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_RIGHT | wl->mpmod, +val);
+ if (value < 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_LEFT | wl->mpmod, -val);
+ break;
+ }
+}
+
+static const struct wl_pointer_listener pointer_listener = {
+ pointer_handle_enter,
+ pointer_handle_leave,
+ pointer_handle_motion,
+ pointer_handle_button,
+ pointer_handle_axis,
+};
+
+static void touch_handle_down(void *data, struct wl_touch *wl_touch,
+ uint32_t serial, uint32_t time, struct wl_surface *surface,
+ int32_t id, wl_fixed_t x_w, wl_fixed_t y_w)
+{
+ struct vo_wayland_state *wl = data;
+ wl->mouse_x = wl_fixed_to_int(x_w) * wl->scaling;
+ wl->mouse_y = wl_fixed_to_int(y_w) * wl->scaling;
+
+ enum xdg_toplevel_resize_edge edge;
+ if (!mp_input_test_dragging(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y)) {
+ if (check_for_resize(wl, wl->opts->edge_pixels_touch, &edge)) {
+ xdg_toplevel_resize(wl->xdg_toplevel, wl->seat, serial, edge);
+ } else {
+ xdg_toplevel_move(wl->xdg_toplevel, wl->seat, serial);
+ }
+ }
+
+ mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y);
+ mp_input_put_key(wl->vo->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_DOWN);
+}
+
+static void touch_handle_up(void *data, struct wl_touch *wl_touch,
+ uint32_t serial, uint32_t time, int32_t id)
+{
+ struct vo_wayland_state *wl = data;
+ mp_input_put_key(wl->vo->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP);
+}
+
+static void touch_handle_motion(void *data, struct wl_touch *wl_touch,
+ uint32_t time, int32_t id, wl_fixed_t x_w, wl_fixed_t y_w)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->mouse_x = wl_fixed_to_int(x_w) * wl->scaling;
+ wl->mouse_y = wl_fixed_to_int(y_w) * wl->scaling;
+
+ mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y);
+}
+
+static void touch_handle_frame(void *data, struct wl_touch *wl_touch)
+{
+}
+
+static void touch_handle_cancel(void *data, struct wl_touch *wl_touch)
+{
+}
+
+static const struct wl_touch_listener touch_listener = {
+ touch_handle_down,
+ touch_handle_up,
+ touch_handle_motion,
+ touch_handle_frame,
+ touch_handle_cancel,
+};
+
+static void keyboard_handle_keymap(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t format, int32_t fd, uint32_t size)
+{
+ struct vo_wayland_state *wl = data;
+ char *map_str;
+
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
+ close(fd);
+ return;
+ }
+
+ map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map_str == MAP_FAILED) {
+ close(fd);
+ return;
+ }
+
+ wl->xkb_keymap = xkb_keymap_new_from_buffer(wl->xkb_context, map_str,
+ strnlen(map_str, size),
+ XKB_KEYMAP_FORMAT_TEXT_V1, 0);
+
+ munmap(map_str, size);
+ close(fd);
+
+ if (!wl->xkb_keymap) {
+ MP_ERR(wl, "failed to compile keymap\n");
+ return;
+ }
+
+ wl->xkb_state = xkb_state_new(wl->xkb_keymap);
+ if (!wl->xkb_state) {
+ MP_ERR(wl, "failed to create XKB state\n");
+ xkb_keymap_unref(wl->xkb_keymap);
+ wl->xkb_keymap = NULL;
+ return;
+ }
+}
+
+static void keyboard_handle_enter(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, struct wl_surface *surface,
+ struct wl_array *keys)
+{
+ struct vo_wayland_state *wl = data;
+ wl->has_keyboard_input = true;
+ guess_focus(wl);
+}
+
+static void keyboard_handle_leave(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, struct wl_surface *surface)
+{
+ struct vo_wayland_state *wl = data;
+ wl->has_keyboard_input = false;
+ wl->keyboard_code = 0;
+ wl->mpkey = 0;
+ wl->mpmod = 0;
+ mp_input_put_key(wl->vo->input_ctx, MP_INPUT_RELEASE_ALL);
+ guess_focus(wl);
+}
+
+static void keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, uint32_t time, uint32_t key,
+ uint32_t state)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->keyboard_code = key + 8;
+ xkb_keysym_t sym = xkb_state_key_get_one_sym(wl->xkb_state, wl->keyboard_code);
+ int mpkey = lookupkey(sym);
+
+ state = state == WL_KEYBOARD_KEY_STATE_PRESSED ? MP_KEY_STATE_DOWN
+ : MP_KEY_STATE_UP;
+
+ if (mpkey) {
+ mp_input_put_key(wl->vo->input_ctx, mpkey | state | wl->mpmod);
+ } else {
+ char s[128];
+ if (xkb_keysym_to_utf8(sym, s, sizeof(s)) > 0) {
+ mp_input_put_key_utf8(wl->vo->input_ctx, state | wl->mpmod, bstr0(s));
+ } else {
+ // Assume a modifier was pressed and handle it in the mod event instead.
+ return;
+ }
+ }
+ if (state == MP_KEY_STATE_DOWN)
+ wl->mpkey = mpkey;
+ if (mpkey && state == MP_KEY_STATE_UP)
+ wl->mpkey = 0;
+}
+
+static void keyboard_handle_modifiers(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, uint32_t mods_depressed,
+ uint32_t mods_latched, uint32_t mods_locked,
+ uint32_t group)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (wl->xkb_state) {
+ xkb_state_update_mask(wl->xkb_state, mods_depressed, mods_latched,
+ mods_locked, 0, 0, group);
+ wl->mpmod = get_mods(wl);
+ if (wl->mpkey)
+ mp_input_put_key(wl->vo->input_ctx, wl->mpkey | MP_KEY_STATE_DOWN | wl->mpmod);
+ }
+}
+
+static void keyboard_handle_repeat_info(void *data, struct wl_keyboard *wl_keyboard,
+ int32_t rate, int32_t delay)
+{
+ struct vo_wayland_state *wl = data;
+ if (wl->vo_opts->native_keyrepeat)
+ mp_input_set_repeat_info(wl->vo->input_ctx, rate, delay);
+}
+
+static const struct wl_keyboard_listener keyboard_listener = {
+ keyboard_handle_keymap,
+ keyboard_handle_enter,
+ keyboard_handle_leave,
+ keyboard_handle_key,
+ keyboard_handle_modifiers,
+ keyboard_handle_repeat_info,
+};
+
+static void seat_handle_caps(void *data, struct wl_seat *seat,
+ enum wl_seat_capability caps)
+{
+ struct vo_wayland_state *wl = data;
+
+ if ((caps & WL_SEAT_CAPABILITY_POINTER) && !wl->pointer) {
+ wl->pointer = wl_seat_get_pointer(seat);
+ get_shape_device(wl);
+ wl_pointer_add_listener(wl->pointer, &pointer_listener, wl);
+ } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && wl->pointer) {
+ wl_pointer_destroy(wl->pointer);
+ wl->pointer = NULL;
+ }
+
+ if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !wl->keyboard) {
+ wl->keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener(wl->keyboard, &keyboard_listener, wl);
+ } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && wl->keyboard) {
+ wl_keyboard_destroy(wl->keyboard);
+ wl->keyboard = NULL;
+ }
+
+ if ((caps & WL_SEAT_CAPABILITY_TOUCH) && !wl->touch) {
+ wl->touch = wl_seat_get_touch(seat);
+ wl_touch_set_user_data(wl->touch, wl);
+ wl_touch_add_listener(wl->touch, &touch_listener, wl);
+ } else if (!(caps & WL_SEAT_CAPABILITY_TOUCH) && wl->touch) {
+ wl_touch_destroy(wl->touch);
+ wl->touch = NULL;
+ }
+}
+
+static const struct wl_seat_listener seat_listener = {
+ seat_handle_caps,
+};
+
+static void data_offer_handle_offer(void *data, struct wl_data_offer *offer,
+ const char *mime_type)
+{
+ struct vo_wayland_state *wl = data;
+ int score = mp_event_get_mime_type_score(wl->vo->input_ctx, mime_type);
+ if (score > wl->dnd_mime_score && wl->vo_opts->drag_and_drop != -2) {
+ wl->dnd_mime_score = score;
+ if (wl->dnd_mime_type)
+ talloc_free(wl->dnd_mime_type);
+ wl->dnd_mime_type = talloc_strdup(wl, mime_type);
+ MP_VERBOSE(wl, "Given DND offer with mime type %s\n", wl->dnd_mime_type);
+ }
+}
+
+static void data_offer_source_actions(void *data, struct wl_data_offer *offer, uint32_t source_actions)
+{
+}
+
+static void data_offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action)
+{
+ struct vo_wayland_state *wl = data;
+ if (dnd_action && wl->vo_opts->drag_and_drop != -2) {
+ if (wl->vo_opts->drag_and_drop >= 0) {
+ wl->dnd_action = wl->vo_opts->drag_and_drop;
+ } else {
+ wl->dnd_action = dnd_action & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY ?
+ DND_REPLACE : DND_APPEND;
+ }
+ MP_VERBOSE(wl, "DND action is %s\n",
+ wl->dnd_action == DND_REPLACE ? "DND_REPLACE" : "DND_APPEND");
+ }
+}
+
+static const struct wl_data_offer_listener data_offer_listener = {
+ data_offer_handle_offer,
+ data_offer_source_actions,
+ data_offer_action,
+};
+
+static void data_device_handle_data_offer(void *data, struct wl_data_device *wl_ddev,
+ struct wl_data_offer *id)
+{
+ struct vo_wayland_state *wl = data;
+ if (wl->dnd_offer)
+ wl_data_offer_destroy(wl->dnd_offer);
+
+ wl->dnd_offer = id;
+ wl_data_offer_add_listener(id, &data_offer_listener, wl);
+}
+
+static void data_device_handle_enter(void *data, struct wl_data_device *wl_ddev,
+ uint32_t serial, struct wl_surface *surface,
+ wl_fixed_t x, wl_fixed_t y,
+ struct wl_data_offer *id)
+{
+ struct vo_wayland_state *wl = data;
+ if (wl->dnd_offer != id) {
+ MP_FATAL(wl, "DND offer ID mismatch!\n");
+ return;
+ }
+
+ if (wl->vo_opts->drag_and_drop != -2) {
+ wl_data_offer_set_actions(id, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY |
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE,
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
+ wl_data_offer_accept(id, serial, wl->dnd_mime_type);
+ MP_VERBOSE(wl, "Accepting DND offer with mime type %s\n", wl->dnd_mime_type);
+ }
+
+}
+
+static void data_device_handle_leave(void *data, struct wl_data_device *wl_ddev)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (wl->dnd_offer) {
+ if (wl->dnd_fd != -1)
+ return;
+ wl_data_offer_destroy(wl->dnd_offer);
+ wl->dnd_offer = NULL;
+ }
+
+ if (wl->vo_opts->drag_and_drop != -2) {
+ MP_VERBOSE(wl, "Releasing DND offer with mime type %s\n", wl->dnd_mime_type);
+ if (wl->dnd_mime_type)
+ TA_FREEP(&wl->dnd_mime_type);
+ wl->dnd_mime_score = 0;
+ }
+}
+
+static void data_device_handle_motion(void *data, struct wl_data_device *wl_ddev,
+ uint32_t time, wl_fixed_t x, wl_fixed_t y)
+{
+ struct vo_wayland_state *wl = data;
+ wl_data_offer_accept(wl->dnd_offer, time, wl->dnd_mime_type);
+}
+
+static void data_device_handle_drop(void *data, struct wl_data_device *wl_ddev)
+{
+ struct vo_wayland_state *wl = data;
+
+ int pipefd[2];
+
+ if (pipe2(pipefd, O_CLOEXEC) == -1) {
+ MP_ERR(wl, "Failed to create dnd pipe!\n");
+ return;
+ }
+
+ if (wl->vo_opts->drag_and_drop != -2) {
+ MP_VERBOSE(wl, "Receiving DND offer with mime %s\n", wl->dnd_mime_type);
+ wl_data_offer_receive(wl->dnd_offer, wl->dnd_mime_type, pipefd[1]);
+ }
+
+ close(pipefd[1]);
+ wl->dnd_fd = pipefd[0];
+}
+
+static void data_device_handle_selection(void *data, struct wl_data_device *wl_ddev,
+ struct wl_data_offer *id)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (wl->dnd_offer) {
+ wl_data_offer_destroy(wl->dnd_offer);
+ wl->dnd_offer = NULL;
+ MP_VERBOSE(wl, "Received a new DND offer. Releasing the previous offer.\n");
+ }
+
+}
+
+static const struct wl_data_device_listener data_device_listener = {
+ data_device_handle_data_offer,
+ data_device_handle_enter,
+ data_device_handle_leave,
+ data_device_handle_motion,
+ data_device_handle_drop,
+ data_device_handle_selection,
+};
+
+static void output_handle_geometry(void *data, struct wl_output *wl_output,
+ int32_t x, int32_t y, int32_t phys_width,
+ int32_t phys_height, int32_t subpixel,
+ const char *make, const char *model,
+ int32_t transform)
+{
+ struct vo_wayland_output *output = data;
+ output->make = talloc_strdup(output->wl, make);
+ output->model = talloc_strdup(output->wl, model);
+ output->geometry.x0 = x;
+ output->geometry.y0 = y;
+ output->phys_width = phys_width;
+ output->phys_height = phys_height;
+}
+
+static void output_handle_mode(void *data, struct wl_output *wl_output,
+ uint32_t flags, int32_t width,
+ int32_t height, int32_t refresh)
+{
+ struct vo_wayland_output *output = data;
+
+ /* Only save current mode */
+ if (!(flags & WL_OUTPUT_MODE_CURRENT))
+ return;
+
+ output->geometry.x1 = width;
+ output->geometry.y1 = height;
+ output->flags = flags;
+ output->refresh_rate = (double)refresh * 0.001;
+}
+
+static void output_handle_done(void *data, struct wl_output *wl_output)
+{
+ struct vo_wayland_output *o = data;
+ struct vo_wayland_state *wl = o->wl;
+
+ o->geometry.x1 += o->geometry.x0;
+ o->geometry.y1 += o->geometry.y0;
+
+ MP_VERBOSE(o->wl, "Registered output %s %s (0x%x):\n"
+ "\tx: %dpx, y: %dpx\n"
+ "\tw: %dpx (%dmm), h: %dpx (%dmm)\n"
+ "\tscale: %d\n"
+ "\tHz: %f\n", o->make, o->model, o->id, o->geometry.x0,
+ o->geometry.y0, mp_rect_w(o->geometry), o->phys_width,
+ mp_rect_h(o->geometry), o->phys_height, o->scale, o->refresh_rate);
+
+ /* If we satisfy this conditional, something about the current
+ * output must have changed (resolution, scale, etc). All window
+ * geometry and scaling should be recalculated. */
+ if (wl->current_output && wl->current_output->output == wl_output) {
+ set_surface_scaling(wl);
+ spawn_cursor(wl);
+ set_geometry(wl, false);
+ prepare_resize(wl, 0, 0);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ }
+
+ wl->pending_vo_events |= VO_EVENT_WIN_STATE;
+}
+
+static void output_handle_scale(void *data, struct wl_output *wl_output,
+ int32_t factor)
+{
+ struct vo_wayland_output *output = data;
+ if (!factor) {
+ MP_ERR(output->wl, "Invalid output scale given by the compositor!\n");
+ return;
+ }
+ output->scale = factor;
+}
+
+static void output_handle_name(void *data, struct wl_output *wl_output,
+ const char *name)
+{
+ struct vo_wayland_output *output = data;
+ output->name = talloc_strdup(output->wl, name);
+}
+
+static void output_handle_description(void *data, struct wl_output *wl_output,
+ const char *description)
+{
+}
+
+static const struct wl_output_listener output_listener = {
+ output_handle_geometry,
+ output_handle_mode,
+ output_handle_done,
+ output_handle_scale,
+ output_handle_name,
+ output_handle_description,
+};
+
+static void surface_handle_enter(void *data, struct wl_surface *wl_surface,
+ struct wl_output *output)
+{
+ struct vo_wayland_state *wl = data;
+ if (!wl->current_output)
+ return;
+
+ struct mp_rect old_output_geometry = wl->current_output->geometry;
+ struct mp_rect old_geometry = wl->geometry;
+ wl->current_output = NULL;
+
+ struct vo_wayland_output *o;
+ wl_list_for_each(o, &wl->output_list, link) {
+ if (o->output == output) {
+ wl->current_output = o;
+ break;
+ }
+ }
+
+ wl->current_output->has_surface = true;
+ bool force_resize = false;
+
+ if (!wl->fractional_scale_manager && wl_surface_get_version(wl_surface) < 6 &&
+ wl->scaling != wl->current_output->scale)
+ {
+ set_surface_scaling(wl);
+ spawn_cursor(wl);
+ force_resize = true;
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ }
+
+ if (!mp_rect_equals(&old_output_geometry, &wl->current_output->geometry)) {
+ set_geometry(wl, false);
+ force_resize = true;
+ }
+
+ if (!mp_rect_equals(&old_geometry, &wl->geometry) || force_resize)
+ prepare_resize(wl, 0, 0);
+
+ MP_VERBOSE(wl, "Surface entered output %s %s (0x%x), scale = %f, refresh rate = %f Hz\n",
+ o->make, o->model, o->id, wl->scaling, o->refresh_rate);
+
+ wl->pending_vo_events |= VO_EVENT_WIN_STATE;
+}
+
+static void surface_handle_leave(void *data, struct wl_surface *wl_surface,
+ struct wl_output *output)
+{
+ struct vo_wayland_state *wl = data;
+
+ struct vo_wayland_output *o;
+ wl_list_for_each(o, &wl->output_list, link) {
+ if (o->output == output) {
+ o->has_surface = false;
+ wl->pending_vo_events |= VO_EVENT_WIN_STATE;
+ return;
+ }
+ }
+}
+
+#ifdef HAVE_WAYLAND_1_22
+static void surface_handle_preferred_buffer_scale(void *data,
+ struct wl_surface *wl_surface,
+ int32_t scale)
+{
+ struct vo_wayland_state *wl = data;
+ double old_scale = wl->scaling;
+
+ if (wl->fractional_scale_manager)
+ return;
+
+ // dmabuf_wayland is always wl->scaling = 1
+ wl->scaling = !wl->using_dmabuf_wayland ? scale : 1;
+ MP_VERBOSE(wl, "Obtained preferred scale, %f, from the compositor.\n",
+ wl->scaling);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ if (wl->current_output) {
+ rescale_geometry(wl, old_scale);
+ set_geometry(wl, false);
+ prepare_resize(wl, 0, 0);
+ }
+}
+
+static void surface_handle_preferred_buffer_transform(void *data,
+ struct wl_surface *wl_surface,
+ uint32_t transform)
+{
+}
+#endif
+
+static const struct wl_surface_listener surface_listener = {
+ surface_handle_enter,
+ surface_handle_leave,
+#ifdef HAVE_WAYLAND_1_22
+ surface_handle_preferred_buffer_scale,
+ surface_handle_preferred_buffer_transform,
+#endif
+};
+
+static void xdg_wm_base_ping(void *data, struct xdg_wm_base *wm_base, uint32_t serial)
+{
+ xdg_wm_base_pong(wm_base, serial);
+}
+
+static const struct xdg_wm_base_listener xdg_wm_base_listener = {
+ xdg_wm_base_ping,
+};
+
+static void handle_surface_config(void *data, struct xdg_surface *surface,
+ uint32_t serial)
+{
+ xdg_surface_ack_configure(surface, serial);
+}
+
+static const struct xdg_surface_listener xdg_surface_listener = {
+ handle_surface_config,
+};
+
+static void handle_toplevel_config(void *data, struct xdg_toplevel *toplevel,
+ int32_t width, int32_t height, struct wl_array *states)
+{
+ struct vo_wayland_state *wl = data;
+ struct mp_vo_opts *vo_opts = wl->vo_opts;
+ struct mp_rect old_geometry = wl->geometry;
+
+ int old_toplevel_width = wl->toplevel_width;
+ int old_toplevel_height = wl->toplevel_height;
+ wl->toplevel_width = width;
+ wl->toplevel_height = height;
+
+ if (!wl->configured) {
+ /* Save initial window size if the compositor gives us a hint here. */
+ bool autofit_or_geometry = vo_opts->geometry.wh_valid || vo_opts->autofit.wh_valid ||
+ vo_opts->autofit_larger.wh_valid || vo_opts->autofit_smaller.wh_valid;
+ if (width && height && !autofit_or_geometry) {
+ wl->initial_size_hint = true;
+ wl->window_size = (struct mp_rect){0, 0, width, height};
+ wl->geometry = wl->window_size;
+ }
+ return;
+ }
+
+ bool is_maximized = false;
+ bool is_fullscreen = false;
+ bool is_activated = false;
+ bool is_suspended = false;
+ bool is_tiled = false;
+ enum xdg_toplevel_state *state;
+ wl_array_for_each(state, states) {
+ switch (*state) {
+ case XDG_TOPLEVEL_STATE_FULLSCREEN:
+ is_fullscreen = true;
+ break;
+ case XDG_TOPLEVEL_STATE_RESIZING:
+ break;
+ case XDG_TOPLEVEL_STATE_ACTIVATED:
+ is_activated = true;
+ /*
+ * If we get an ACTIVATED state, we know it cannot be
+ * minimized, but it may not have been minimized
+ * previously, so we can't detect the exact state.
+ */
+ vo_opts->window_minimized = false;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &vo_opts->window_minimized);
+ break;
+ case XDG_TOPLEVEL_STATE_TILED_TOP:
+ case XDG_TOPLEVEL_STATE_TILED_LEFT:
+ case XDG_TOPLEVEL_STATE_TILED_RIGHT:
+ case XDG_TOPLEVEL_STATE_TILED_BOTTOM:
+ is_tiled = true;
+ break;
+ case XDG_TOPLEVEL_STATE_MAXIMIZED:
+ is_maximized = true;
+ break;
+ case XDG_TOPLEVEL_STATE_SUSPENDED:
+ is_suspended = true;
+ break;
+ }
+ }
+
+ if (wl->hidden != is_suspended)
+ wl->hidden = is_suspended;
+
+ if (vo_opts->fullscreen != is_fullscreen) {
+ wl->state_change = true;
+ vo_opts->fullscreen = is_fullscreen;
+ m_config_cache_write_opt(wl->vo_opts_cache, &vo_opts->fullscreen);
+ }
+
+ if (vo_opts->window_maximized != is_maximized) {
+ wl->state_change = true;
+ vo_opts->window_maximized = is_maximized;
+ m_config_cache_write_opt(wl->vo_opts_cache, &vo_opts->window_maximized);
+ }
+
+ wl->tiled = is_tiled;
+
+ wl->locked_size = is_fullscreen || is_maximized || is_tiled;
+
+ if (wl->requested_decoration)
+ request_decoration_mode(wl, wl->requested_decoration);
+
+ if (wl->activated != is_activated) {
+ wl->activated = is_activated;
+ guess_focus(wl);
+ /* Just force a redraw to be on the safe side. */
+ if (wl->activated) {
+ wl->hidden = false;
+ wl->pending_vo_events |= VO_EVENT_EXPOSE;
+ }
+ }
+
+ if (wl->state_change) {
+ if (!wl->locked_size) {
+ wl->geometry = wl->window_size;
+ wl->state_change = false;
+ goto resize;
+ }
+ }
+
+ /* Reuse old size if either of these are 0. */
+ if (width == 0 || height == 0) {
+ if (!wl->locked_size) {
+ wl->geometry = wl->window_size;
+ }
+ goto resize;
+ }
+
+ if (old_toplevel_width == wl->toplevel_width &&
+ old_toplevel_height == wl->toplevel_height)
+ return;
+
+ if (!wl->locked_size) {
+ if (vo_opts->keepaspect) {
+ double scale_factor = (double)width / wl->reduced_width;
+ width = ceil(wl->reduced_width * scale_factor);
+ if (vo_opts->keepaspect_window)
+ height = ceil(wl->reduced_height * scale_factor);
+ }
+ wl->window_size.x0 = 0;
+ wl->window_size.y0 = 0;
+ wl->window_size.x1 = round(width * wl->scaling);
+ wl->window_size.y1 = round(height * wl->scaling);
+ }
+ wl->geometry.x0 = 0;
+ wl->geometry.y0 = 0;
+ wl->geometry.x1 = round(width * wl->scaling);
+ wl->geometry.y1 = round(height * wl->scaling);
+
+ if (mp_rect_equals(&old_geometry, &wl->geometry))
+ return;
+
+resize:
+ MP_VERBOSE(wl, "Resizing due to xdg from %ix%i to %ix%i\n",
+ mp_rect_w(old_geometry), mp_rect_h(old_geometry),
+ mp_rect_w(wl->geometry), mp_rect_h(wl->geometry));
+
+ prepare_resize(wl, width, height);
+ wl->toplevel_configured = true;
+}
+
+static void handle_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel)
+{
+ struct vo_wayland_state *wl = data;
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_CLOSE_WIN);
+}
+
+static void handle_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel,
+ int32_t width, int32_t height)
+{
+ struct vo_wayland_state *wl = data;
+ wl->bounded_width = width * wl->scaling;
+ wl->bounded_height = height * wl->scaling;
+}
+
+#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION
+static void handle_wm_capabilities(void *data, struct xdg_toplevel *xdg_toplevel,
+ struct wl_array *capabilities)
+{
+}
+#endif
+
+static const struct xdg_toplevel_listener xdg_toplevel_listener = {
+ handle_toplevel_config,
+ handle_toplevel_close,
+ handle_configure_bounds,
+#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION
+ handle_wm_capabilities,
+#endif
+};
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+static void preferred_scale(void *data,
+ struct wp_fractional_scale_v1 *fractional_scale,
+ uint32_t scale)
+{
+ struct vo_wayland_state *wl = data;
+ double old_scale = wl->scaling;
+
+ // dmabuf_wayland is always wl->scaling = 1
+ wl->scaling = !wl->using_dmabuf_wayland ? (double)scale / 120 : 1;
+ MP_VERBOSE(wl, "Obtained preferred scale, %f, from the compositor.\n",
+ wl->scaling);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ if (wl->current_output) {
+ rescale_geometry(wl, old_scale);
+ set_geometry(wl, false);
+ prepare_resize(wl, 0, 0);
+ }
+}
+
+static const struct wp_fractional_scale_v1_listener fractional_scale_listener = {
+ preferred_scale,
+};
+#endif
+
+static const char *zxdg_decoration_mode_to_str(const uint32_t mode)
+{
+ switch (mode) {
+ case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE:
+ return "server-side";
+ case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE:
+ return "client-side";
+ default:
+ return "<unknown>";
+ }
+}
+
+static void configure_decorations(void *data,
+ struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration,
+ uint32_t mode)
+{
+ struct vo_wayland_state *wl = data;
+ struct mp_vo_opts *opts = wl->vo_opts;
+
+ if (wl->requested_decoration && mode != wl->requested_decoration) {
+ MP_DBG(wl,
+ "Requested %s decorations but compositor responded with %s. "
+ "It is likely that compositor wants us to stay in a given mode.\n",
+ zxdg_decoration_mode_to_str(wl->requested_decoration),
+ zxdg_decoration_mode_to_str(mode));
+ }
+
+ wl->requested_decoration = 0;
+
+ if (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) {
+ MP_VERBOSE(wl, "Enabling server decorations\n");
+ } else {
+ MP_VERBOSE(wl, "Disabling server decorations\n");
+ }
+ opts->border = mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
+ m_config_cache_write_opt(wl->vo_opts_cache, &opts->border);
+}
+
+static const struct zxdg_toplevel_decoration_v1_listener decoration_listener = {
+ configure_decorations,
+};
+
+static void pres_set_clockid(void *data, struct wp_presentation *pres,
+ uint32_t clockid)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (clockid == CLOCK_MONOTONIC || clockid == CLOCK_MONOTONIC_RAW)
+ wl->use_present = true;
+}
+
+static const struct wp_presentation_listener pres_listener = {
+ pres_set_clockid,
+};
+
+static void feedback_sync_output(void *data, struct wp_presentation_feedback *fback,
+ struct wl_output *output)
+{
+}
+
+static void feedback_presented(void *data, struct wp_presentation_feedback *fback,
+ uint32_t tv_sec_hi, uint32_t tv_sec_lo,
+ uint32_t tv_nsec, uint32_t refresh_nsec,
+ uint32_t seq_hi, uint32_t seq_lo,
+ uint32_t flags)
+{
+ struct vo_wayland_feedback_pool *fback_pool = data;
+ struct vo_wayland_state *wl = fback_pool->wl;
+
+ if (fback)
+ remove_feedback(fback_pool, fback);
+
+ wl->refresh_interval = (int64_t)refresh_nsec;
+
+ // Very similar to oml_sync_control, in this case we assume that every
+ // time the compositor receives feedback, a buffer swap has been already
+ // been performed.
+ //
+ // Notes:
+ // - tv_sec_lo + tv_sec_hi is the equivalent of oml's ust
+ // - seq_lo + seq_hi is the equivalent of oml's msc
+ // - these values are updated every time the compositor receives feedback.
+
+ int64_t sec = (uint64_t) tv_sec_lo + ((uint64_t) tv_sec_hi << 32);
+ int64_t ust = MP_TIME_S_TO_NS(sec) + (uint64_t) tv_nsec;
+ int64_t msc = (uint64_t) seq_lo + ((uint64_t) seq_hi << 32);
+ present_sync_update_values(wl->present, ust, msc);
+}
+
+static void feedback_discarded(void *data, struct wp_presentation_feedback *fback)
+{
+ struct vo_wayland_feedback_pool *fback_pool = data;
+ if (fback)
+ remove_feedback(fback_pool, fback);
+}
+
+static const struct wp_presentation_feedback_listener feedback_listener = {
+ feedback_sync_output,
+ feedback_presented,
+ feedback_discarded,
+};
+
+static const struct wl_callback_listener frame_listener;
+
+static void frame_callback(void *data, struct wl_callback *callback, uint32_t time)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (callback)
+ wl_callback_destroy(callback);
+
+ wl->frame_callback = wl_surface_frame(wl->callback_surface);
+ wl_callback_add_listener(wl->frame_callback, &frame_listener, wl);
+
+ if (wl->use_present) {
+ struct wp_presentation_feedback *fback = wp_presentation_feedback(wl->presentation, wl->callback_surface);
+ add_feedback(wl->fback_pool, fback);
+ wp_presentation_feedback_add_listener(fback, &feedback_listener, wl->fback_pool);
+ }
+
+ wl->frame_wait = false;
+ wl->hidden = false;
+}
+
+static const struct wl_callback_listener frame_listener = {
+ frame_callback,
+};
+
+static void done(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1)
+{
+}
+
+static void format_table(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ int32_t fd,
+ uint32_t size)
+{
+ struct vo_wayland_state *wl = data;
+
+ void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ close(fd);
+
+ if (map != MAP_FAILED) {
+ wl->format_map = map;
+ wl->format_size = size;
+ }
+}
+
+static void main_device(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ struct wl_array *device)
+{
+}
+
+static void tranche_done(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1)
+{
+}
+
+static void tranche_target_device(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ struct wl_array *device)
+{
+}
+
+static void tranche_formats(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ struct wl_array *indices)
+{
+}
+
+static void tranche_flags(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ uint32_t flags)
+{
+}
+
+static const struct zwp_linux_dmabuf_feedback_v1_listener dmabuf_feedback_listener = {
+ done,
+ format_table,
+ main_device,
+ tranche_done,
+ tranche_target_device,
+ tranche_formats,
+ tranche_flags,
+};
+
+static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id,
+ const char *interface, uint32_t ver)
+{
+ int found = 1;
+ struct vo_wayland_state *wl = data;
+
+ if (!strcmp(interface, wl_compositor_interface.name) && (ver >= 4) && found++) {
+#ifdef HAVE_WAYLAND_1_22
+ ver = MPMIN(ver, 6); /* Cap at 6 in case new events are added later. */
+#else
+ ver = 4;
+#endif
+ wl->compositor = wl_registry_bind(reg, id, &wl_compositor_interface, ver);
+ wl->surface = wl_compositor_create_surface(wl->compositor);
+ wl->video_surface = wl_compositor_create_surface(wl->compositor);
+ wl->osd_surface = wl_compositor_create_surface(wl->compositor);
+
+ /* never accept input events on anything besides the main surface */
+ struct wl_region *region = wl_compositor_create_region(wl->compositor);
+ wl_surface_set_input_region(wl->osd_surface, region);
+ wl_surface_set_input_region(wl->video_surface, region);
+ wl_region_destroy(region);
+
+ wl->cursor_surface = wl_compositor_create_surface(wl->compositor);
+ wl_surface_add_listener(wl->surface, &surface_listener, wl);
+ }
+
+ if (!strcmp(interface, wl_subcompositor_interface.name) && (ver >= 1) && found++) {
+ wl->subcompositor = wl_registry_bind(reg, id, &wl_subcompositor_interface, 1);
+ }
+
+ if (!strcmp (interface, zwp_linux_dmabuf_v1_interface.name) && (ver >= 4) && found++) {
+ wl->dmabuf = wl_registry_bind(reg, id, &zwp_linux_dmabuf_v1_interface, 4);
+ wl->dmabuf_feedback = zwp_linux_dmabuf_v1_get_default_feedback(wl->dmabuf);
+ zwp_linux_dmabuf_feedback_v1_add_listener(wl->dmabuf_feedback, &dmabuf_feedback_listener, wl);
+ }
+
+ if (!strcmp (interface, wp_viewporter_interface.name) && (ver >= 1) && found++) {
+ wl->viewporter = wl_registry_bind (reg, id, &wp_viewporter_interface, 1);
+ }
+
+ if (!strcmp(interface, wl_data_device_manager_interface.name) && (ver >= 3) && found++) {
+ wl->dnd_devman = wl_registry_bind(reg, id, &wl_data_device_manager_interface, 3);
+ }
+
+ if (!strcmp(interface, wl_output_interface.name) && (ver >= 2) && found++) {
+ struct vo_wayland_output *output = talloc_zero(wl, struct vo_wayland_output);
+
+ output->wl = wl;
+ output->id = id;
+ output->scale = 1;
+ output->name = "";
+
+ ver = MPMIN(ver, 4); /* Cap at 4 in case new events are added later. */
+ output->output = wl_registry_bind(reg, id, &wl_output_interface, ver);
+ wl_output_add_listener(output->output, &output_listener, output);
+ wl_list_insert(&wl->output_list, &output->link);
+ }
+
+ if (!strcmp(interface, wl_seat_interface.name) && found++) {
+ wl->seat = wl_registry_bind(reg, id, &wl_seat_interface, 1);
+ wl_seat_add_listener(wl->seat, &seat_listener, wl);
+ }
+
+ if (!strcmp(interface, wl_shm_interface.name) && found++) {
+ wl->shm = wl_registry_bind(reg, id, &wl_shm_interface, 1);
+ }
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (!strcmp(interface, wp_content_type_manager_v1_interface.name) && found++) {
+ wl->content_type_manager = wl_registry_bind(reg, id, &wp_content_type_manager_v1_interface, 1);
+ }
+
+ if (!strcmp(interface, wp_single_pixel_buffer_manager_v1_interface.name) && found++) {
+ wl->single_pixel_manager = wl_registry_bind(reg, id, &wp_single_pixel_buffer_manager_v1_interface, 1);
+ }
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+ if (!strcmp(interface, wp_fractional_scale_manager_v1_interface.name) && found++) {
+ wl->fractional_scale_manager = wl_registry_bind(reg, id, &wp_fractional_scale_manager_v1_interface, 1);
+ }
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ if (!strcmp(interface, wp_cursor_shape_manager_v1_interface.name) && found++) {
+ wl->cursor_shape_manager = wl_registry_bind(reg, id, &wp_cursor_shape_manager_v1_interface, 1);
+ }
+#endif
+
+ if (!strcmp(interface, wp_presentation_interface.name) && found++) {
+ wl->presentation = wl_registry_bind(reg, id, &wp_presentation_interface, 1);
+ wp_presentation_add_listener(wl->presentation, &pres_listener, wl);
+ }
+
+ if (!strcmp(interface, xdg_wm_base_interface.name) && found++) {
+ ver = MPMIN(ver, 6); /* Cap at 6 in case new events are added later. */
+ wl->wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, ver);
+ xdg_wm_base_add_listener(wl->wm_base, &xdg_wm_base_listener, wl);
+ }
+
+ if (!strcmp(interface, zxdg_decoration_manager_v1_interface.name) && found++) {
+ wl->xdg_decoration_manager = wl_registry_bind(reg, id, &zxdg_decoration_manager_v1_interface, 1);
+ }
+
+ if (!strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) && found++) {
+ wl->idle_inhibit_manager = wl_registry_bind(reg, id, &zwp_idle_inhibit_manager_v1_interface, 1);
+ }
+
+ if (found > 1)
+ MP_VERBOSE(wl, "Registered for protocol %s\n", interface);
+}
+
+static void registry_handle_remove(void *data, struct wl_registry *reg, uint32_t id)
+{
+ struct vo_wayland_state *wl = data;
+ struct vo_wayland_output *output, *tmp;
+ wl_list_for_each_safe(output, tmp, &wl->output_list, link) {
+ if (output->id == id) {
+ remove_output(output);
+ return;
+ }
+ }
+}
+
+static const struct wl_registry_listener registry_listener = {
+ registry_handle_add,
+ registry_handle_remove,
+};
+
+/* Static functions */
+static void check_dnd_fd(struct vo_wayland_state *wl)
+{
+ if (wl->dnd_fd == -1)
+ return;
+
+ struct pollfd fdp = { wl->dnd_fd, POLLIN | POLLHUP, 0 };
+ if (poll(&fdp, 1, 0) <= 0)
+ return;
+
+ if (fdp.revents & POLLIN) {
+ ptrdiff_t offset = 0;
+ size_t data_read = 0;
+ const size_t chunk_size = 1;
+ uint8_t *buffer = ta_zalloc_size(wl, chunk_size);
+ if (!buffer)
+ goto end;
+
+ while ((data_read = read(wl->dnd_fd, buffer + offset, chunk_size)) > 0) {
+ offset += data_read;
+ buffer = ta_realloc_size(wl, buffer, offset + chunk_size);
+ memset(buffer + offset, 0, chunk_size);
+ if (!buffer)
+ goto end;
+ }
+
+ MP_VERBOSE(wl, "Read %td bytes from the DND fd\n", offset);
+
+ struct bstr file_list = bstr0(buffer);
+ mp_event_drop_mime_data(wl->vo->input_ctx, wl->dnd_mime_type,
+ file_list, wl->dnd_action);
+ talloc_free(buffer);
+end:
+ if (wl->dnd_mime_type)
+ talloc_free(wl->dnd_mime_type);
+
+ if (wl->dnd_action >= 0 && wl->dnd_offer)
+ wl_data_offer_finish(wl->dnd_offer);
+
+ wl->dnd_action = -1;
+ wl->dnd_mime_type = NULL;
+ wl->dnd_mime_score = 0;
+ }
+
+ if (fdp.revents & (POLLIN | POLLERR | POLLHUP)) {
+ close(wl->dnd_fd);
+ wl->dnd_fd = -1;
+ }
+}
+
+static int check_for_resize(struct vo_wayland_state *wl, int edge_pixels,
+ enum xdg_toplevel_resize_edge *edge)
+{
+ if (wl->vo_opts->fullscreen || wl->vo_opts->window_maximized)
+ return 0;
+
+ int pos[2] = { wl->mouse_x, wl->mouse_y };
+ int left_edge = pos[0] < edge_pixels;
+ int top_edge = pos[1] < edge_pixels;
+ int right_edge = pos[0] > (mp_rect_w(wl->geometry) - edge_pixels);
+ int bottom_edge = pos[1] > (mp_rect_h(wl->geometry) - edge_pixels);
+
+ if (left_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
+ if (top_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT;
+ else if (bottom_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
+ } else if (right_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
+ if (top_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT;
+ else if (bottom_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
+ } else if (top_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP;
+ } else if (bottom_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
+ } else {
+ *edge = 0;
+ return 0;
+ }
+
+ return 1;
+}
+
+static bool create_input(struct vo_wayland_state *wl)
+{
+ wl->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+
+ if (!wl->xkb_context) {
+ MP_ERR(wl, "failed to initialize input: check xkbcommon\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+static int create_viewports(struct vo_wayland_state *wl)
+{
+ if (wl->viewporter) {
+ wl->viewport = wp_viewporter_get_viewport(wl->viewporter, wl->surface);
+ wl->osd_viewport = wp_viewporter_get_viewport(wl->viewporter, wl->osd_surface);
+ wl->video_viewport = wp_viewporter_get_viewport(wl->viewporter, wl->video_surface);
+ }
+
+ if (wl->viewporter && (!wl->viewport || !wl->osd_viewport || !wl->video_viewport)) {
+ MP_ERR(wl, "failed to create viewport interfaces!\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int create_xdg_surface(struct vo_wayland_state *wl)
+{
+ wl->xdg_surface = xdg_wm_base_get_xdg_surface(wl->wm_base, wl->surface);
+ xdg_surface_add_listener(wl->xdg_surface, &xdg_surface_listener, wl);
+
+ wl->xdg_toplevel = xdg_surface_get_toplevel(wl->xdg_surface);
+ xdg_toplevel_add_listener(wl->xdg_toplevel, &xdg_toplevel_listener, wl);
+
+ if (!wl->xdg_surface || !wl->xdg_toplevel) {
+ MP_ERR(wl, "failed to create xdg_surface and xdg_toplevel!\n");
+ return 1;
+ }
+ return 0;
+}
+
+static void add_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback)
+{
+ for (int i = 0; i < fback_pool->len; ++i) {
+ if (!fback_pool->fback[i]) {
+ fback_pool->fback[i] = fback;
+ break;
+ } else if (i == fback_pool->len - 1) {
+ // Shouldn't happen in practice.
+ wp_presentation_feedback_destroy(fback_pool->fback[i]);
+ fback_pool->fback[i] = fback;
+ }
+ }
+}
+
+static void do_minimize(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ if (wl->vo_opts->window_minimized)
+ xdg_toplevel_set_minimized(wl->xdg_toplevel);
+}
+
+static char **get_displays_spanned(struct vo_wayland_state *wl)
+{
+ char **names = NULL;
+ int displays_spanned = 0;
+ struct vo_wayland_output *output;
+ wl_list_for_each(output, &wl->output_list, link) {
+ if (output->has_surface) {
+ char *name = output->name ? output->name : output->model;
+ MP_TARRAY_APPEND(NULL, names, displays_spanned,
+ talloc_strdup(NULL, name));
+ }
+ }
+ MP_TARRAY_APPEND(NULL, names, displays_spanned, NULL);
+ return names;
+}
+
+static int get_mods(struct vo_wayland_state *wl)
+{
+ static char* const mod_names[] = {
+ XKB_MOD_NAME_SHIFT,
+ XKB_MOD_NAME_CTRL,
+ XKB_MOD_NAME_ALT,
+ XKB_MOD_NAME_LOGO,
+ };
+
+ static const int mods[] = {
+ MP_KEY_MODIFIER_SHIFT,
+ MP_KEY_MODIFIER_CTRL,
+ MP_KEY_MODIFIER_ALT,
+ MP_KEY_MODIFIER_META,
+ };
+
+ int modifiers = 0;
+
+ for (int n = 0; n < MP_ARRAY_SIZE(mods); n++) {
+ xkb_mod_index_t index = xkb_keymap_mod_get_index(wl->xkb_keymap, mod_names[n]);
+ if (!xkb_state_mod_index_is_consumed(wl->xkb_state, wl->keyboard_code, index)
+ && xkb_state_mod_index_is_active(wl->xkb_state, index,
+ XKB_STATE_MODS_DEPRESSED))
+ modifiers |= mods[n];
+ }
+ return modifiers;
+}
+
+static void get_shape_device(struct vo_wayland_state *wl)
+{
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ if (!wl->cursor_shape_device && wl->cursor_shape_manager) {
+ wl->cursor_shape_device = wp_cursor_shape_manager_v1_get_pointer(wl->cursor_shape_manager,
+ wl->pointer);
+ }
+#endif
+}
+
+static int greatest_common_divisor(int a, int b)
+{
+ int rem = a % b;
+ if (rem == 0)
+ return b;
+ return greatest_common_divisor(b, rem);
+}
+
+static void guess_focus(struct vo_wayland_state *wl)
+{
+ // We can't actually know if the window is focused or not in wayland,
+ // so just guess it with some common sense. Obviously won't work if
+ // the user has no keyboard.
+ if ((!wl->focused && wl->activated && wl->has_keyboard_input) ||
+ (wl->focused && !wl->activated))
+ {
+ wl->focused = !wl->focused;
+ wl->pending_vo_events |= VO_EVENT_FOCUS;
+ }
+}
+
+static struct vo_wayland_output *find_output(struct vo_wayland_state *wl)
+{
+ int index = 0;
+ struct mp_vo_opts *opts = wl->vo_opts;
+ int screen_id = opts->fullscreen ? opts->fsscreen_id : opts->screen_id;
+ char *screen_name = opts->fullscreen ? opts->fsscreen_name : opts->screen_name;
+ struct vo_wayland_output *output = NULL;
+ struct vo_wayland_output *fallback_output = NULL;
+ wl_list_for_each(output, &wl->output_list, link) {
+ if (index == 0)
+ fallback_output = output;
+ if (screen_id == -1 && !screen_name)
+ return output;
+ if (screen_id == -1 && screen_name && !strcmp(screen_name, output->name))
+ return output;
+ if (screen_id == -1 && screen_name && !strcmp(screen_name, output->model))
+ return output;
+ if (screen_id == index++)
+ return output;
+ }
+ if (!fallback_output) {
+ MP_ERR(wl, "No screens could be found!\n");
+ return NULL;
+ } else if (screen_id >= 0) {
+ MP_WARN(wl, "Screen index %i not found/unavailable! Falling back to screen 0!\n", screen_id);
+ } else if (screen_name && screen_name[0]) {
+ MP_WARN(wl, "Screen name %s not found/unavailable! Falling back to screen 0!\n", screen_name);
+ }
+ return fallback_output;
+}
+
+static int lookupkey(int key)
+{
+ const char *passthrough_keys = " -+*/<>`~!@#$%^&()_{}:;\"\',.?\\|=[]";
+
+ int mpkey = 0;
+ if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') ||
+ (key >= '0' && key <= '9') ||
+ (key > 0 && key < 256 && strchr(passthrough_keys, key)))
+ mpkey = key;
+
+ if (!mpkey)
+ mpkey = lookup_keymap_table(keymap, key);
+
+ return mpkey;
+}
+
+static void prepare_resize(struct vo_wayland_state *wl, int width, int height)
+{
+ if (!width)
+ width = mp_rect_w(wl->geometry) / wl->scaling;
+ if (!height)
+ height = mp_rect_h(wl->geometry) / wl->scaling;
+ xdg_surface_set_window_geometry(wl->xdg_surface, 0, 0, width, height);
+ wl->pending_vo_events |= VO_EVENT_RESIZE;
+}
+
+static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode)
+{
+ wl->requested_decoration = mode;
+ zxdg_toplevel_decoration_v1_set_mode(wl->xdg_toplevel_decoration, mode);
+}
+
+static void rescale_geometry(struct vo_wayland_state *wl, double old_scale)
+{
+ double factor = old_scale / wl->scaling;
+ wl->window_size.x1 /= factor;
+ wl->window_size.y1 /= factor;
+ wl->geometry.x1 /= factor;
+ wl->geometry.y1 /= factor;
+}
+
+static void clean_feedback_pool(struct vo_wayland_feedback_pool *fback_pool)
+{
+ for (int i = 0; i < fback_pool->len; ++i) {
+ if (fback_pool->fback[i]) {
+ wp_presentation_feedback_destroy(fback_pool->fback[i]);
+ fback_pool->fback[i] = NULL;
+ }
+ }
+}
+
+static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback)
+{
+ for (int i = 0; i < fback_pool->len; ++i) {
+ if (fback_pool->fback[i] == fback) {
+ wp_presentation_feedback_destroy(fback);
+ fback_pool->fback[i] = NULL;
+ break;
+ }
+ }
+}
+
+static void remove_output(struct vo_wayland_output *out)
+{
+ if (!out)
+ return;
+
+ MP_VERBOSE(out->wl, "Deregistering output %s %s (0x%x)\n", out->make,
+ out->model, out->id);
+ wl_list_remove(&out->link);
+ wl_output_destroy(out->output);
+ talloc_free(out->make);
+ talloc_free(out->model);
+ talloc_free(out);
+ return;
+}
+
+static void set_content_type(struct vo_wayland_state *wl)
+{
+ if (!wl->content_type_manager)
+ return;
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ // handle auto;
+ if (wl->vo_opts->content_type == -1) {
+ wp_content_type_v1_set_content_type(wl->content_type, wl->current_content_type);
+ } else {
+ wp_content_type_v1_set_content_type(wl->content_type, wl->vo_opts->content_type);
+ }
+#endif
+}
+
+static void set_cursor_shape(struct vo_wayland_state *wl)
+{
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ wp_cursor_shape_device_v1_set_shape(wl->cursor_shape_device, wl->pointer_id,
+ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
+#endif
+}
+
+static int set_cursor_visibility(struct vo_wayland_state *wl, bool on)
+{
+ wl->cursor_visible = on;
+ if (on) {
+ if (wl->cursor_shape_device) {
+ set_cursor_shape(wl);
+ } else {
+ if (spawn_cursor(wl))
+ return VO_FALSE;
+ struct wl_cursor_image *img = wl->default_cursor->images[0];
+ struct wl_buffer *buffer = wl_cursor_image_get_buffer(img);
+ if (!buffer)
+ return VO_FALSE;
+ int scale = MPMAX(wl->scaling, 1);
+ wl_pointer_set_cursor(wl->pointer, wl->pointer_id, wl->cursor_surface,
+ img->hotspot_x / scale, img->hotspot_y / scale);
+ wl_surface_set_buffer_scale(wl->cursor_surface, scale);
+ wl_surface_attach(wl->cursor_surface, buffer, 0, 0);
+ wl_surface_damage_buffer(wl->cursor_surface, 0, 0, img->width, img->height);
+ }
+ wl_surface_commit(wl->cursor_surface);
+ } else {
+ wl_pointer_set_cursor(wl->pointer, wl->pointer_id, NULL, 0, 0);
+ }
+ return VO_TRUE;
+}
+
+static void set_geometry(struct vo_wayland_state *wl, bool resize)
+{
+ struct vo *vo = wl->vo;
+ if (!wl->current_output)
+ return;
+
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc = wl->current_output->geometry;
+ vo_calc_window_geometry2(vo, &screenrc, wl->scaling, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ int gcd = greatest_common_divisor(vo->dwidth, vo->dheight);
+ wl->reduced_width = vo->dwidth / gcd;
+ wl->reduced_height = vo->dheight / gcd;
+
+ if (!wl->initial_size_hint)
+ wl->window_size = (struct mp_rect){0, 0, vo->dwidth, vo->dheight};
+ wl->initial_size_hint = false;
+
+ if (resize) {
+ if (!wl->locked_size)
+ wl->geometry = wl->window_size;
+ prepare_resize(wl, 0, 0);
+ }
+}
+
+static void set_input_region(struct vo_wayland_state *wl, bool passthrough)
+{
+ if (passthrough) {
+ struct wl_region *region = wl_compositor_create_region(wl->compositor);
+ wl_surface_set_input_region(wl->surface, region);
+ wl_region_destroy(region);
+ } else {
+ wl_surface_set_input_region(wl->surface, NULL);
+ }
+}
+
+static int set_screensaver_inhibitor(struct vo_wayland_state *wl, int state)
+{
+ if (!wl->idle_inhibit_manager)
+ return VO_NOTIMPL;
+ if (state == (!!wl->idle_inhibitor))
+ return VO_TRUE;
+ if (state) {
+ MP_VERBOSE(wl, "Enabling idle inhibitor\n");
+ struct zwp_idle_inhibit_manager_v1 *mgr = wl->idle_inhibit_manager;
+ wl->idle_inhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor(mgr, wl->surface);
+ } else {
+ MP_VERBOSE(wl, "Disabling the idle inhibitor\n");
+ zwp_idle_inhibitor_v1_destroy(wl->idle_inhibitor);
+ wl->idle_inhibitor = NULL;
+ }
+ return VO_TRUE;
+}
+
+static void set_surface_scaling(struct vo_wayland_state *wl)
+{
+ if (wl->fractional_scale_manager)
+ return;
+
+ // dmabuf_wayland is always wl->scaling = 1
+ double old_scale = wl->scaling;
+ wl->scaling = !wl->using_dmabuf_wayland ? wl->current_output->scale : 1;
+
+ rescale_geometry(wl, old_scale);
+ wl_surface_set_buffer_scale(wl->surface, wl->scaling);
+}
+
+static void set_window_bounds(struct vo_wayland_state *wl)
+{
+ // If the user has set geometry/autofit and the option is auto,
+ // don't use these.
+ if (wl->opts->configure_bounds == -1 && (wl->vo_opts->geometry.wh_valid ||
+ wl->vo_opts->autofit.wh_valid || wl->vo_opts->autofit_larger.wh_valid ||
+ wl->vo_opts->autofit_smaller.wh_valid))
+ {
+ return;
+ }
+
+ if (wl->bounded_width && wl->bounded_width < wl->window_size.x1)
+ wl->window_size.x1 = wl->bounded_width;
+ if (wl->bounded_height && wl->bounded_height < wl->window_size.y1)
+ wl->window_size.y1 = wl->bounded_height;
+}
+
+static int spawn_cursor(struct vo_wayland_state *wl)
+{
+ /* Don't use this if we have cursor-shape. */
+ if (wl->cursor_shape_device)
+ return 0;
+ /* Reuse if size is identical */
+ if (!wl->pointer || wl->allocated_cursor_scale == wl->scaling)
+ return 0;
+ else if (wl->cursor_theme)
+ wl_cursor_theme_destroy(wl->cursor_theme);
+
+ const char *xcursor_theme = getenv("XCURSOR_THEME");
+ const char *size_str = getenv("XCURSOR_SIZE");
+ int size = 24;
+ if (size_str != NULL) {
+ errno = 0;
+ char *end;
+ long size_long = strtol(size_str, &end, 10);
+ if (!*end && !errno && size_long > 0 && size_long <= INT_MAX)
+ size = (int)size_long;
+ }
+
+ wl->cursor_theme = wl_cursor_theme_load(xcursor_theme, size*wl->scaling, wl->shm);
+ if (!wl->cursor_theme) {
+ MP_ERR(wl, "Unable to load cursor theme!\n");
+ return 1;
+ }
+
+ wl->default_cursor = wl_cursor_theme_get_cursor(wl->cursor_theme, "left_ptr");
+ if (!wl->default_cursor) {
+ MP_ERR(wl, "Unable to load cursor theme!\n");
+ return 1;
+ }
+
+ wl->allocated_cursor_scale = wl->scaling;
+
+ return 0;
+}
+
+static void toggle_fullscreen(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ wl->state_change = true;
+ bool specific_screen = wl->vo_opts->fsscreen_id >= 0 || wl->vo_opts->fsscreen_name;
+ if (wl->vo_opts->fullscreen && !specific_screen) {
+ xdg_toplevel_set_fullscreen(wl->xdg_toplevel, NULL);
+ } else if (wl->vo_opts->fullscreen && specific_screen) {
+ struct vo_wayland_output *output = find_output(wl);
+ xdg_toplevel_set_fullscreen(wl->xdg_toplevel, output->output);
+ } else {
+ xdg_toplevel_unset_fullscreen(wl->xdg_toplevel);
+ }
+}
+
+static void toggle_maximized(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ wl->state_change = true;
+ if (wl->vo_opts->window_maximized) {
+ xdg_toplevel_set_maximized(wl->xdg_toplevel);
+ } else {
+ xdg_toplevel_unset_maximized(wl->xdg_toplevel);
+ }
+}
+
+static void update_app_id(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ xdg_toplevel_set_app_id(wl->xdg_toplevel, wl->vo_opts->appid);
+}
+
+static int update_window_title(struct vo_wayland_state *wl, const char *title)
+{
+ if (!wl->xdg_toplevel)
+ return VO_NOTAVAIL;
+ /* The xdg-shell protocol requires that the title is UTF-8. */
+ void *tmp = talloc_new(NULL);
+ struct bstr b_title = bstr_sanitize_utf8_latin1(tmp, bstr0(title));
+ xdg_toplevel_set_title(wl->xdg_toplevel, bstrto0(tmp, b_title));
+ talloc_free(tmp);
+ return VO_TRUE;
+}
+
+static void window_move(struct vo_wayland_state *wl, uint32_t serial)
+{
+ if (wl->xdg_toplevel)
+ xdg_toplevel_move(wl->xdg_toplevel, wl->seat, serial);
+}
+
+static void wayland_dispatch_events(struct vo_wayland_state *wl, int nfds, int64_t timeout_ns)
+{
+ if (wl->display_fd == -1)
+ return;
+
+ struct pollfd fds[2] = {
+ {.fd = wl->display_fd, .events = POLLIN },
+ {.fd = wl->wakeup_pipe[0], .events = POLLIN },
+ };
+
+ while (wl_display_prepare_read(wl->display) != 0)
+ wl_display_dispatch_pending(wl->display);
+ wl_display_flush(wl->display);
+
+ mp_poll(fds, nfds, timeout_ns);
+
+ if (fds[0].revents & POLLIN) {
+ wl_display_read_events(wl->display);
+ } else {
+ wl_display_cancel_read(wl->display);
+ }
+
+ if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
+ MP_FATAL(wl, "Error occurred on the display fd\n");
+ wl->display_fd = -1;
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_CLOSE_WIN);
+ }
+
+ if (fds[1].revents & POLLIN)
+ mp_flush_wakeup_pipe(wl->wakeup_pipe[0]);
+
+ wl_display_dispatch_pending(wl->display);
+}
+
+/* Non-static */
+int vo_wayland_allocate_memfd(struct vo *vo, size_t size)
+{
+#if !HAVE_MEMFD_CREATE
+ return VO_ERROR;
+#else
+ int fd = memfd_create("mpv", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ if (fd < 0) {
+ MP_ERR(vo, "Failed to allocate memfd: %s\n", mp_strerror(errno));
+ return VO_ERROR;
+ }
+
+ fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL);
+
+ if (posix_fallocate(fd, 0, size) == 0)
+ return fd;
+
+ close(fd);
+ MP_ERR(vo, "Failed to allocate memfd: %s\n", mp_strerror(errno));
+
+ return VO_ERROR;
+#endif
+}
+
+bool vo_wayland_check_visible(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ bool render = !wl->hidden || wl->vo_opts->force_render;
+ wl->frame_wait = true;
+ return render;
+}
+
+int vo_wayland_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct mp_vo_opts *opts = wl->vo_opts;
+ wl_display_dispatch_pending(wl->display);
+
+ switch (request) {
+ case VOCTRL_CHECK_EVENTS: {
+ check_dnd_fd(wl);
+ *events |= wl->pending_vo_events;
+ if (*events & VO_EVENT_RESIZE) {
+ *events |= VO_EVENT_EXPOSE;
+ wl->frame_wait = false;
+ wl->timeout_count = 0;
+ wl->hidden = false;
+ }
+ wl->pending_vo_events = 0;
+ return VO_TRUE;
+ }
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(wl->vo_opts_cache, &opt)) {
+ if (opt == &opts->appid)
+ update_app_id(wl);
+ if (opt == &opts->border)
+ {
+ // This is stupid but the value of border shouldn't be written
+ // unless we get a configure event. Change it back to its old
+ // value and let configure_decorations handle it after the request.
+ if (wl->xdg_toplevel_decoration) {
+ int requested_border_mode = opts->border;
+ opts->border = !opts->border;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &opts->border);
+ request_decoration_mode(
+ wl, requested_border_mode ?
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE :
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
+ } else {
+ opts->border = false;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &wl->vo_opts->border);
+ }
+ }
+ if (opt == &opts->content_type)
+ set_content_type(wl);
+ if (opt == &opts->cursor_passthrough)
+ set_input_region(wl, opts->cursor_passthrough);
+ if (opt == &opts->fullscreen)
+ toggle_fullscreen(wl);
+ if (opt == &opts->hidpi_window_scale)
+ set_geometry(wl, true);
+ if (opt == &opts->window_maximized)
+ toggle_maximized(wl);
+ if (opt == &opts->window_minimized)
+ do_minimize(wl);
+ if (opt == &opts->geometry || opt == &opts->autofit ||
+ opt == &opts->autofit_smaller || opt == &opts->autofit_larger)
+ {
+ set_geometry(wl, true);
+ }
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_CONTENT_TYPE: {
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ wl->current_content_type = *(enum mp_content_type *)arg;
+ set_content_type(wl);
+#endif
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_FOCUSED: {
+ *(bool *)arg = wl->focused;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_NAMES: {
+ *(char ***)arg = get_displays_spanned(wl);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ if (wl->vo_opts->window_maximized || wl->tiled) {
+ s[0] = mp_rect_w(wl->geometry);
+ s[1] = mp_rect_h(wl->geometry);
+ } else {
+ s[0] = mp_rect_w(wl->window_size);
+ s[1] = mp_rect_h(wl->window_size);
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ wl->window_size.x0 = 0;
+ wl->window_size.y0 = 0;
+ wl->window_size.x1 = s[0];
+ wl->window_size.y1 = s[1];
+ if (!wl->vo_opts->fullscreen && !wl->tiled) {
+ if (wl->vo_opts->window_maximized) {
+ xdg_toplevel_unset_maximized(wl->xdg_toplevel);
+ wl_display_dispatch_pending(wl->display);
+ /* Make sure the compositor let us unmaximize */
+ if (wl->vo_opts->window_maximized)
+ return VO_TRUE;
+ }
+ wl->geometry = wl->window_size;
+ prepare_resize(wl, 0, 0);
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_FPS: {
+ struct vo_wayland_output *out;
+ if (wl->current_output) {
+ out = wl->current_output;
+ } else {
+ out = find_output(wl);
+ }
+ if (!out)
+ return VO_NOTAVAIL;
+ *(double *)arg = out->refresh_rate;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_RES: {
+ struct vo_wayland_output *out;
+ if (wl->current_output) {
+ out = wl->current_output;
+ } else {
+ out = find_output(wl);
+ }
+ if (!out)
+ return VO_NOTAVAIL;
+ ((int *)arg)[0] = out->geometry.x1;
+ ((int *)arg)[1] = out->geometry.y1;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_HIDPI_SCALE: {
+ if (!wl->scaling)
+ return VO_NOTAVAIL;
+ *(double *)arg = wl->scaling;
+ return VO_TRUE;
+ }
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ return update_window_title(wl, (const char *)arg);
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ if (!wl->pointer)
+ return VO_NOTAVAIL;
+ return set_cursor_visibility(wl, *(bool *)arg);
+ case VOCTRL_KILL_SCREENSAVER:
+ return set_screensaver_inhibitor(wl, true);
+ case VOCTRL_RESTORE_SCREENSAVER:
+ return set_screensaver_inhibitor(wl, false);
+ }
+
+ return VO_NOTIMPL;
+}
+
+void vo_wayland_handle_fractional_scale(struct vo_wayland_state *wl)
+{
+ if (wl->fractional_scale_manager && wl->viewport)
+ wp_viewport_set_destination(wl->viewport,
+ round(mp_rect_w(wl->geometry) / wl->scaling),
+ round(mp_rect_h(wl->geometry) / wl->scaling));
+}
+
+bool vo_wayland_init(struct vo *vo)
+{
+ vo->wl = talloc_zero(NULL, struct vo_wayland_state);
+ struct vo_wayland_state *wl = vo->wl;
+
+ *wl = (struct vo_wayland_state) {
+ .display = wl_display_connect(NULL),
+ .vo = vo,
+ .log = mp_log_new(wl, vo->log, "wayland"),
+ .bounded_width = 0,
+ .bounded_height = 0,
+ .refresh_interval = 0,
+ .scaling = 1,
+ .wakeup_pipe = {-1, -1},
+ .display_fd = -1,
+ .dnd_fd = -1,
+ .cursor_visible = true,
+ .vo_opts_cache = m_config_cache_alloc(wl, vo->global, &vo_sub_opts),
+ };
+ wl->vo_opts = wl->vo_opts_cache->opts;
+ wl->using_dmabuf_wayland = !strcmp(wl->vo->driver->name, "dmabuf-wayland");
+
+ wl_list_init(&wl->output_list);
+
+ if (!wl->display)
+ goto err;
+
+ if (create_input(wl))
+ goto err;
+
+ wl->registry = wl_display_get_registry(wl->display);
+ wl_registry_add_listener(wl->registry, &registry_listener, wl);
+
+ /* Do a roundtrip to run the registry */
+ wl_display_roundtrip(wl->display);
+
+ if (!wl->surface) {
+ MP_FATAL(wl, "Compositor doesn't support %s (ver. 4)\n",
+ wl_compositor_interface.name);
+ goto err;
+ }
+
+ if (!wl->wm_base) {
+ MP_FATAL(wl, "Compositor doesn't support the required %s protocol!\n",
+ xdg_wm_base_interface.name);
+ goto err;
+ }
+
+ if (!wl_list_length(&wl->output_list)) {
+ MP_FATAL(wl, "No outputs found or compositor doesn't support %s (ver. 2)\n",
+ wl_output_interface.name);
+ goto err;
+ }
+
+ /* Can't be initialized during registry due to multi-protocol dependence */
+ if (create_viewports(wl))
+ goto err;
+
+ if (create_xdg_surface(wl))
+ goto err;
+
+ if (wl->subcompositor) {
+ wl->osd_subsurface = wl_subcompositor_get_subsurface(wl->subcompositor, wl->osd_surface, wl->video_surface);
+ wl->video_subsurface = wl_subcompositor_get_subsurface(wl->subcompositor, wl->video_surface, wl->surface);
+ }
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (wl->content_type_manager) {
+ wl->content_type = wp_content_type_manager_v1_get_surface_content_type(wl->content_type_manager, wl->surface);
+ } else {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_content_type_manager_v1_interface.name);
+ }
+
+ if (!wl->single_pixel_manager) {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_single_pixel_buffer_manager_v1_interface.name);
+ }
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+ if (wl->fractional_scale_manager) {
+ wl->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(wl->fractional_scale_manager, wl->surface);
+ wp_fractional_scale_v1_add_listener(wl->fractional_scale, &fractional_scale_listener, wl);
+ } else {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_fractional_scale_manager_v1_interface.name);
+ }
+#endif
+
+ if (wl->dnd_devman && wl->seat) {
+ wl->dnd_ddev = wl_data_device_manager_get_data_device(wl->dnd_devman, wl->seat);
+ wl_data_device_add_listener(wl->dnd_ddev, &data_device_listener, wl);
+ } else if (!wl->dnd_devman) {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s (ver. 3) protocol!\n",
+ wl_data_device_manager_interface.name);
+ }
+
+ if (wl->presentation) {
+ wl->fback_pool = talloc_zero(wl, struct vo_wayland_feedback_pool);
+ wl->fback_pool->wl = wl;
+ wl->fback_pool->len = VO_MAX_SWAPCHAIN_DEPTH;
+ wl->fback_pool->fback = talloc_zero_array(wl->fback_pool, struct wp_presentation_feedback *,
+ wl->fback_pool->len);
+ wl->present = mp_present_initialize(wl, wl->vo_opts, VO_MAX_SWAPCHAIN_DEPTH);
+ } else {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_presentation_interface.name);
+ }
+
+ if (wl->xdg_decoration_manager) {
+ wl->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(wl->xdg_decoration_manager, wl->xdg_toplevel);
+ zxdg_toplevel_decoration_v1_add_listener(wl->xdg_toplevel_decoration, &decoration_listener, wl);
+ request_decoration_mode(
+ wl, wl->vo_opts->border ?
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE :
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
+ } else {
+ wl->vo_opts->border = false;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &wl->vo_opts->border);
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ zxdg_decoration_manager_v1_interface.name);
+ }
+
+ if (!wl->idle_inhibit_manager) {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ zwp_idle_inhibit_manager_v1_interface.name);
+ }
+
+ wl->opts = mp_get_config_group(wl, wl->vo->global, &wayland_conf);
+ wl->display_fd = wl_display_get_fd(wl->display);
+
+ update_app_id(wl);
+ mp_make_wakeup_pipe(wl->wakeup_pipe);
+
+ wl->callback_surface = wl->using_dmabuf_wayland ? wl->video_surface : wl->surface;
+ wl->frame_callback = wl_surface_frame(wl->callback_surface);
+ wl_callback_add_listener(wl->frame_callback, &frame_listener, wl);
+ wl_surface_commit(wl->surface);
+
+ /* Do another roundtrip to ensure all of the above is initialized
+ * before mpv does anything else. */
+ wl_display_roundtrip(wl->display);
+
+ return true;
+
+err:
+ vo_wayland_uninit(vo);
+ return false;
+}
+
+bool vo_wayland_reconfig(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ MP_VERBOSE(wl, "Reconfiguring!\n");
+
+ if (!wl->current_output) {
+ wl->current_output = find_output(wl);
+ if (!wl->current_output)
+ return false;
+ set_surface_scaling(wl);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ }
+
+ if (wl->vo_opts->auto_window_resize || !wl->configured)
+ set_geometry(wl, false);
+
+ if (wl->opts->configure_bounds)
+ set_window_bounds(wl);
+
+ if (!wl->configured || !wl->locked_size) {
+ wl->geometry = wl->window_size;
+ wl->configured = true;
+ }
+
+ if (wl->vo_opts->cursor_passthrough)
+ set_input_region(wl, true);
+
+ if (wl->vo_opts->fullscreen)
+ toggle_fullscreen(wl);
+
+ if (wl->vo_opts->window_maximized)
+ toggle_maximized(wl);
+
+ if (wl->vo_opts->window_minimized)
+ do_minimize(wl);
+
+ prepare_resize(wl, 0, 0);
+
+ return true;
+}
+
+void vo_wayland_set_opaque_region(struct vo_wayland_state *wl, bool alpha)
+{
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+ if (!alpha) {
+ struct wl_region *region = wl_compositor_create_region(wl->compositor);
+ wl_region_add(region, 0, 0, width, height);
+ wl_surface_set_opaque_region(wl->surface, region);
+ wl_region_destroy(region);
+ } else {
+ wl_surface_set_opaque_region(wl->surface, NULL);
+ }
+}
+
+void vo_wayland_uninit(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ if (!wl)
+ return;
+
+ mp_input_put_key(wl->vo->input_ctx, MP_INPUT_RELEASE_ALL);
+
+ if (wl->compositor)
+ wl_compositor_destroy(wl->compositor);
+
+ if (wl->subcompositor)
+ wl_subcompositor_destroy(wl->subcompositor);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ if (wl->cursor_shape_device)
+ wp_cursor_shape_device_v1_destroy(wl->cursor_shape_device);
+
+ if (wl->cursor_shape_manager)
+ wp_cursor_shape_manager_v1_destroy(wl->cursor_shape_manager);
+#endif
+
+ if (wl->cursor_surface)
+ wl_surface_destroy(wl->cursor_surface);
+
+ if (wl->cursor_theme)
+ wl_cursor_theme_destroy(wl->cursor_theme);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (wl->content_type)
+ wp_content_type_v1_destroy(wl->content_type);
+
+ if (wl->content_type_manager)
+ wp_content_type_manager_v1_destroy(wl->content_type_manager);
+#endif
+
+ if (wl->dnd_ddev)
+ wl_data_device_destroy(wl->dnd_ddev);
+
+ if (wl->dnd_devman)
+ wl_data_device_manager_destroy(wl->dnd_devman);
+
+ if (wl->dnd_offer)
+ wl_data_offer_destroy(wl->dnd_offer);
+
+ if (wl->fback_pool)
+ clean_feedback_pool(wl->fback_pool);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+ if (wl->fractional_scale)
+ wp_fractional_scale_v1_destroy(wl->fractional_scale);
+
+ if (wl->fractional_scale_manager)
+ wp_fractional_scale_manager_v1_destroy(wl->fractional_scale_manager);
+#endif
+
+ if (wl->frame_callback)
+ wl_callback_destroy(wl->frame_callback);
+
+ if (wl->idle_inhibitor)
+ zwp_idle_inhibitor_v1_destroy(wl->idle_inhibitor);
+
+ if (wl->idle_inhibit_manager)
+ zwp_idle_inhibit_manager_v1_destroy(wl->idle_inhibit_manager);
+
+ if (wl->keyboard)
+ wl_keyboard_destroy(wl->keyboard);
+
+ if (wl->pointer)
+ wl_pointer_destroy(wl->pointer);
+
+ if (wl->presentation)
+ wp_presentation_destroy(wl->presentation);
+
+ if (wl->registry)
+ wl_registry_destroy(wl->registry);
+
+ if (wl->viewporter)
+ wp_viewporter_destroy(wl->viewporter);
+
+ if (wl->viewport)
+ wp_viewport_destroy(wl->viewport);
+
+ if (wl->osd_viewport)
+ wp_viewport_destroy(wl->osd_viewport);
+
+ if (wl->video_viewport)
+ wp_viewport_destroy(wl->video_viewport);
+
+ if (wl->dmabuf)
+ zwp_linux_dmabuf_v1_destroy(wl->dmabuf);
+
+ if (wl->dmabuf_feedback)
+ zwp_linux_dmabuf_feedback_v1_destroy(wl->dmabuf_feedback);
+
+ if (wl->seat)
+ wl_seat_destroy(wl->seat);
+
+ if (wl->shm)
+ wl_shm_destroy(wl->shm);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (wl->single_pixel_manager)
+ wp_single_pixel_buffer_manager_v1_destroy(wl->single_pixel_manager);
+#endif
+
+ if (wl->surface)
+ wl_surface_destroy(wl->surface);
+
+ if (wl->osd_surface)
+ wl_surface_destroy(wl->osd_surface);
+
+ if (wl->osd_subsurface)
+ wl_subsurface_destroy(wl->osd_subsurface);
+
+ if (wl->video_surface)
+ wl_surface_destroy(wl->video_surface);
+
+ if (wl->video_subsurface)
+ wl_subsurface_destroy(wl->video_subsurface);
+
+ if (wl->wm_base)
+ xdg_wm_base_destroy(wl->wm_base);
+
+ if (wl->xdg_decoration_manager)
+ zxdg_decoration_manager_v1_destroy(wl->xdg_decoration_manager);
+
+ if (wl->xdg_toplevel)
+ xdg_toplevel_destroy(wl->xdg_toplevel);
+
+ if (wl->xdg_toplevel_decoration)
+ zxdg_toplevel_decoration_v1_destroy(wl->xdg_toplevel_decoration);
+
+ if (wl->xdg_surface)
+ xdg_surface_destroy(wl->xdg_surface);
+
+ if (wl->xkb_context)
+ xkb_context_unref(wl->xkb_context);
+
+ if (wl->xkb_keymap)
+ xkb_keymap_unref(wl->xkb_keymap);
+
+ if (wl->xkb_state)
+ xkb_state_unref(wl->xkb_state);
+
+ struct vo_wayland_output *output, *tmp;
+ wl_list_for_each_safe(output, tmp, &wl->output_list, link)
+ remove_output(output);
+
+ if (wl->display)
+ wl_display_disconnect(wl->display);
+
+ munmap(wl->format_map, wl->format_size);
+
+ for (int n = 0; n < 2; n++)
+ close(wl->wakeup_pipe[n]);
+ talloc_free(wl);
+ vo->wl = NULL;
+}
+
+void vo_wayland_wait_frame(struct vo_wayland_state *wl)
+{
+ int64_t vblank_time = 0;
+ /* We need some vblank interval to use for the timeout in
+ * this function. The order of preference of values to use is:
+ * 1. vsync duration from presentation time
+ * 2. refresh interval reported by presentation time
+ * 3. refresh rate of the output reported by the compositor
+ * 4. make up crap if vblank_time is still <= 0 (better than nothing) */
+
+ if (wl->use_present && wl->present->head)
+ vblank_time = wl->present->head->vsync_duration;
+
+ if (vblank_time <= 0 && wl->refresh_interval > 0)
+ vblank_time = wl->refresh_interval;
+
+ if (vblank_time <= 0 && wl->current_output->refresh_rate > 0)
+ vblank_time = 1e9 / wl->current_output->refresh_rate;
+
+ // Ideally you should never reach this point.
+ if (vblank_time <= 0)
+ vblank_time = 1e9 / 60;
+
+ // Completely arbitrary amount of additional time to wait.
+ vblank_time += 0.05 * vblank_time;
+ int64_t finish_time = mp_time_ns() + vblank_time;
+
+ while (wl->frame_wait && finish_time > mp_time_ns()) {
+ int64_t poll_time = finish_time - mp_time_ns();
+ if (poll_time < 0) {
+ poll_time = 0;
+ }
+ wayland_dispatch_events(wl, 1, poll_time);
+ }
+
+ /* If the compositor does not have presentation time, we cannot be sure
+ * that this wait is accurate. Do a hacky block with wl_display_roundtrip. */
+ if (!wl->use_present && !wl_display_get_error(wl->display))
+ wl_display_roundtrip(wl->display);
+
+ /* Only use this heuristic if the compositor doesn't support the suspended state. */
+ if (wl->frame_wait && xdg_toplevel_get_version(wl->xdg_toplevel) < 6) {
+ // Only consider consecutive missed callbacks.
+ if (wl->timeout_count > 1) {
+ wl->hidden = true;
+ return;
+ } else {
+ wl->timeout_count += 1;
+ return;
+ }
+ }
+
+ wl->timeout_count = 0;
+}
+
+void vo_wayland_wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10));
+
+ wayland_dispatch_events(wl, 2, timeout_ns);
+}
+
+void vo_wayland_wakeup(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ (void)write(wl->wakeup_pipe[1], &(char){0}, 1);
+}
diff --git a/video/out/wayland_common.h b/video/out/wayland_common.h
new file mode 100644
index 0000000..adbcca6
--- /dev/null
+++ b/video/out/wayland_common.h
@@ -0,0 +1,189 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_WAYLAND_COMMON_H
+#define MPLAYER_WAYLAND_COMMON_H
+
+#include <wayland-client.h>
+#include "input/event.h"
+#include "vo.h"
+
+typedef struct {
+ uint32_t format;
+ uint32_t padding;
+ uint64_t modifier;
+} wayland_format;
+
+struct wayland_opts {
+ int configure_bounds;
+ int content_type;
+ bool disable_vsync;
+ int edge_pixels_pointer;
+ int edge_pixels_touch;
+};
+
+struct vo_wayland_state {
+ struct m_config_cache *vo_opts_cache;
+ struct mp_log *log;
+ struct mp_vo_opts *vo_opts;
+ struct vo *vo;
+ struct wayland_opts *opts;
+ struct wl_callback *frame_callback;
+ struct wl_compositor *compositor;
+ struct wl_subcompositor *subcompositor;
+ struct wl_display *display;
+ struct wl_registry *registry;
+ struct wl_shm *shm;
+ struct wl_surface *surface;
+ struct wl_surface *osd_surface;
+ struct wl_subsurface *osd_subsurface;
+ struct wl_surface *video_surface;
+ struct wl_surface *callback_surface;
+ struct wl_subsurface *video_subsurface;
+
+ /* Geometry */
+ struct mp_rect geometry;
+ struct mp_rect window_size;
+ struct wl_list output_list;
+ struct vo_wayland_output *current_output;
+ int bounded_height;
+ int bounded_width;
+ int reduced_height;
+ int reduced_width;
+ int toplevel_width;
+ int toplevel_height;
+
+ /* State */
+ bool activated;
+ bool configured;
+ bool focused;
+ bool frame_wait;
+ bool has_keyboard_input;
+ bool hidden;
+ bool initial_size_hint;
+ bool locked_size;
+ bool state_change;
+ bool tiled;
+ bool toplevel_configured;
+ int display_fd;
+ int mouse_x;
+ int mouse_y;
+ int pending_vo_events;
+ double scaling;
+ int timeout_count;
+ int wakeup_pipe[2];
+
+ /* content-type */
+ /* TODO: unvoid these if required wayland protocols is bumped to 1.27+ */
+ void *content_type_manager;
+ void *content_type;
+ int current_content_type;
+
+ /* cursor-shape */
+ /* TODO: unvoid these if required wayland protocols is bumped to 1.32+ */
+ void *cursor_shape_manager;
+ void *cursor_shape_device;
+
+ /* fractional-scale */
+ /* TODO: unvoid these if required wayland protocols is bumped to 1.31+ */
+ void *fractional_scale_manager;
+ void *fractional_scale;
+
+ /* idle-inhibit */
+ struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager;
+ struct zwp_idle_inhibitor_v1 *idle_inhibitor;
+
+ /* linux-dmabuf */
+ struct zwp_linux_dmabuf_v1 *dmabuf;
+ struct zwp_linux_dmabuf_feedback_v1 *dmabuf_feedback;
+ wayland_format *format_map;
+ uint32_t format_size;
+ bool using_dmabuf_wayland;
+
+ /* presentation-time */
+ struct wp_presentation *presentation;
+ struct vo_wayland_feedback_pool *fback_pool;
+ struct mp_present *present;
+ int64_t refresh_interval;
+ bool use_present;
+
+ /* single-pixel-buffer */
+ /* TODO: unvoid this if required wayland-protocols is bumped to 1.27+ */
+ void *single_pixel_manager;
+
+ /* xdg-decoration */
+ struct zxdg_decoration_manager_v1 *xdg_decoration_manager;
+ struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration;
+ int requested_decoration;
+
+ /* xdg-shell */
+ struct xdg_wm_base *wm_base;
+ struct xdg_surface *xdg_surface;
+ struct xdg_toplevel *xdg_toplevel;
+
+ /* viewporter */
+ struct wp_viewporter *viewporter;
+ struct wp_viewport *viewport;
+ struct wp_viewport *osd_viewport;
+ struct wp_viewport *video_viewport;
+
+ /* Input */
+ struct wl_keyboard *keyboard;
+ struct wl_pointer *pointer;
+ struct wl_seat *seat;
+ struct wl_touch *touch;
+ struct xkb_context *xkb_context;
+ struct xkb_keymap *xkb_keymap;
+ struct xkb_state *xkb_state;
+ uint32_t keyboard_code;
+ int mpkey;
+ int mpmod;
+
+ /* DND */
+ struct wl_data_device *dnd_ddev;
+ struct wl_data_device_manager *dnd_devman;
+ struct wl_data_offer *dnd_offer;
+ enum mp_dnd_action dnd_action;
+ char *dnd_mime_type;
+ int dnd_fd;
+ int dnd_mime_score;
+
+ /* Cursor */
+ struct wl_cursor_theme *cursor_theme;
+ struct wl_cursor *default_cursor;
+ struct wl_surface *cursor_surface;
+ bool cursor_visible;
+ int allocated_cursor_scale;
+ uint32_t pointer_id;
+};
+
+bool vo_wayland_check_visible(struct vo *vo);
+bool vo_wayland_init(struct vo *vo);
+bool vo_wayland_reconfig(struct vo *vo);
+
+int vo_wayland_allocate_memfd(struct vo *vo, size_t size);
+int vo_wayland_control(struct vo *vo, int *events, int request, void *arg);
+
+void vo_wayland_handle_fractional_scale(struct vo_wayland_state *wl);
+void vo_wayland_set_opaque_region(struct vo_wayland_state *wl, bool alpha);
+void vo_wayland_sync_swap(struct vo_wayland_state *wl);
+void vo_wayland_uninit(struct vo *vo);
+void vo_wayland_wait_events(struct vo *vo, int64_t until_time_ns);
+void vo_wayland_wait_frame(struct vo_wayland_state *wl);
+void vo_wayland_wakeup(struct vo *vo);
+
+#endif /* MPLAYER_WAYLAND_COMMON_H */
diff --git a/video/out/win32/displayconfig.c b/video/out/win32/displayconfig.c
new file mode 100644
index 0000000..9844afd
--- /dev/null
+++ b/video/out/win32/displayconfig.c
@@ -0,0 +1,140 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include "displayconfig.h"
+
+#include "mpv_talloc.h"
+
+static bool is_valid_refresh_rate(DISPLAYCONFIG_RATIONAL rr)
+{
+ // DisplayConfig sometimes reports a rate of 1 when the rate is not known
+ return rr.Denominator != 0 && rr.Numerator / rr.Denominator > 1;
+}
+
+static int get_config(void *ctx,
+ UINT32 *num_paths, DISPLAYCONFIG_PATH_INFO** paths,
+ UINT32 *num_modes, DISPLAYCONFIG_MODE_INFO** modes)
+{
+ LONG res;
+ *paths = NULL;
+ *modes = NULL;
+
+ // The display configuration could change between the call to
+ // GetDisplayConfigBufferSizes and the call to QueryDisplayConfig, so call
+ // them in a loop until the correct buffer size is chosen
+ do {
+ res = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, num_paths,
+ num_modes);
+ if (res != ERROR_SUCCESS)
+ goto fail;
+
+ // Free old buffers if they exist and allocate new ones
+ talloc_free(*paths);
+ talloc_free(*modes);
+ *paths = talloc_array(ctx, DISPLAYCONFIG_PATH_INFO, *num_paths);
+ *modes = talloc_array(ctx, DISPLAYCONFIG_MODE_INFO, *num_modes);
+
+ res = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, num_paths, *paths,
+ num_modes, *modes, NULL);
+ } while (res == ERROR_INSUFFICIENT_BUFFER);
+ if (res != ERROR_SUCCESS)
+ goto fail;
+
+ return 0;
+fail:
+ talloc_free(*paths);
+ talloc_free(*modes);
+ return -1;
+}
+
+static DISPLAYCONFIG_PATH_INFO *get_path(UINT32 num_paths,
+ DISPLAYCONFIG_PATH_INFO* paths,
+ const wchar_t *device)
+{
+ // Search for a path with a matching device name
+ for (UINT32 i = 0; i < num_paths; i++) {
+ // Send a GET_SOURCE_NAME request
+ DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {
+ .header = {
+ .size = sizeof source,
+ .type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
+ .adapterId = paths[i].sourceInfo.adapterId,
+ .id = paths[i].sourceInfo.id,
+ }
+ };
+ if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS)
+ return NULL;
+
+ // Check if the device name matches
+ if (!wcscmp(device, source.viewGdiDeviceName))
+ return &paths[i];
+ }
+
+ return NULL;
+}
+
+static double get_refresh_rate_from_mode(DISPLAYCONFIG_MODE_INFO *mode)
+{
+ if (mode->infoType != DISPLAYCONFIG_MODE_INFO_TYPE_TARGET)
+ return 0.0;
+
+ DISPLAYCONFIG_VIDEO_SIGNAL_INFO *info =
+ &mode->targetMode.targetVideoSignalInfo;
+ if (!is_valid_refresh_rate(info->vSyncFreq))
+ return 0.0;
+
+ return ((double)info->vSyncFreq.Numerator) /
+ ((double)info->vSyncFreq.Denominator);
+}
+
+double mp_w32_displayconfig_get_refresh_rate(const wchar_t *device)
+{
+ void *ctx = talloc_new(NULL);
+ double freq = 0.0;
+
+ // Get the current display configuration
+ UINT32 num_paths;
+ DISPLAYCONFIG_PATH_INFO* paths;
+ UINT32 num_modes;
+ DISPLAYCONFIG_MODE_INFO* modes;
+ if (get_config(ctx, &num_paths, &paths, &num_modes, &modes))
+ goto end;
+
+ // Get the path for the specified monitor
+ DISPLAYCONFIG_PATH_INFO* path;
+ if (!(path = get_path(num_paths, paths, device)))
+ goto end;
+
+ // Try getting the refresh rate from the mode first. The value in the mode
+ // overrides the value in the path.
+ if (path->targetInfo.modeInfoIdx != DISPLAYCONFIG_PATH_MODE_IDX_INVALID)
+ freq = get_refresh_rate_from_mode(&modes[path->targetInfo.modeInfoIdx]);
+
+ // If the mode didn't contain a valid refresh rate, try the path
+ if (freq == 0.0 && is_valid_refresh_rate(path->targetInfo.refreshRate)) {
+ freq = ((double)path->targetInfo.refreshRate.Numerator) /
+ ((double)path->targetInfo.refreshRate.Denominator);
+ }
+
+end:
+ talloc_free(ctx);
+ return freq;
+}
diff --git a/video/out/win32/displayconfig.h b/video/out/win32/displayconfig.h
new file mode 100644
index 0000000..ee6cd03
--- /dev/null
+++ b/video/out/win32/displayconfig.h
@@ -0,0 +1,27 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_WIN32_DISPLAYCONFIG_H_
+#define MP_WIN32_DISPLAYCONFIG_H_
+
+#include <wchar.h>
+
+// Given a GDI monitor device name, get the precise refresh rate using the
+// Windows 7 DisplayConfig API. Returns 0.0 on failure.
+double mp_w32_displayconfig_get_refresh_rate(const wchar_t *device);
+
+#endif
diff --git a/video/out/win32/droptarget.c b/video/out/win32/droptarget.c
new file mode 100644
index 0000000..8a33522
--- /dev/null
+++ b/video/out/win32/droptarget.c
@@ -0,0 +1,227 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <stdatomic.h>
+
+#include <windows.h>
+#include <ole2.h>
+#include <shobjidl.h>
+
+#include "common/msg.h"
+#include "common/common.h"
+#include "input/input.h"
+#include "input/event.h"
+#include "osdep/io.h"
+#include "osdep/windows_utils.h"
+#include "mpv_talloc.h"
+
+#include "droptarget.h"
+
+struct droptarget {
+ IDropTarget iface;
+ atomic_int ref_cnt;
+ struct mp_log *log;
+ struct input_ctx *input_ctx;
+ struct mp_vo_opts *opts;
+ DWORD last_effect;
+ IDataObject *data_obj;
+};
+
+static FORMATETC fmtetc_file = {
+ .cfFormat = CF_HDROP,
+ .dwAspect = DVASPECT_CONTENT,
+ .lindex = -1,
+ .tymed = TYMED_HGLOBAL,
+};
+
+static FORMATETC fmtetc_url = {
+ .dwAspect = DVASPECT_CONTENT,
+ .lindex = -1,
+ .tymed = TYMED_HGLOBAL,
+};
+
+static void DropTarget_Destroy(struct droptarget *t)
+{
+ SAFE_RELEASE(t->data_obj);
+ talloc_free(t);
+}
+
+static STDMETHODIMP DropTarget_QueryInterface(IDropTarget *self, REFIID riid,
+ void **ppvObject)
+{
+ if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IDropTarget)) {
+ *ppvObject = self;
+ IDropTarget_AddRef(self);
+ return S_OK;
+ }
+
+ *ppvObject = NULL;
+ return E_NOINTERFACE;
+}
+
+static STDMETHODIMP_(ULONG) DropTarget_AddRef(IDropTarget *self)
+{
+ struct droptarget *t = (struct droptarget *)self;
+ return atomic_fetch_add(&t->ref_cnt, 1) + 1;
+}
+
+static STDMETHODIMP_(ULONG) DropTarget_Release(IDropTarget *self)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ ULONG ref_cnt = atomic_fetch_add(&t->ref_cnt, -1) - 1;
+ if (ref_cnt == 0)
+ DropTarget_Destroy(t);
+ return ref_cnt;
+}
+
+static STDMETHODIMP DropTarget_DragEnter(IDropTarget *self,
+ IDataObject *pDataObj,
+ DWORD grfKeyState, POINTL pt,
+ DWORD *pdwEffect)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ IDataObject_AddRef(pDataObj);
+ if (FAILED(IDataObject_QueryGetData(pDataObj, &fmtetc_file)) &&
+ FAILED(IDataObject_QueryGetData(pDataObj, &fmtetc_url)))
+ {
+ *pdwEffect = DROPEFFECT_NONE;
+ }
+
+ SAFE_RELEASE(t->data_obj);
+ t->data_obj = pDataObj;
+ t->last_effect = *pdwEffect;
+ return S_OK;
+}
+
+static STDMETHODIMP DropTarget_DragOver(IDropTarget *self, DWORD grfKeyState,
+ POINTL pt, DWORD *pdwEffect)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ *pdwEffect = t->last_effect;
+ return S_OK;
+}
+
+static STDMETHODIMP DropTarget_DragLeave(IDropTarget *self)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ SAFE_RELEASE(t->data_obj);
+ return S_OK;
+}
+
+static STDMETHODIMP DropTarget_Drop(IDropTarget *self, IDataObject *pDataObj,
+ DWORD grfKeyState, POINTL pt,
+ DWORD *pdwEffect)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ enum mp_dnd_action action;
+ if (t->opts->drag_and_drop >= 0) {
+ action = t->opts->drag_and_drop;
+ } else {
+ action = (grfKeyState & MK_SHIFT) ? DND_APPEND : DND_REPLACE;
+ }
+
+ SAFE_RELEASE(t->data_obj);
+
+ STGMEDIUM medium;
+ if (t->opts->drag_and_drop == -2) {
+ t->last_effect = DROPEFFECT_NONE;
+ } else if (SUCCEEDED(IDataObject_GetData(pDataObj, &fmtetc_file, &medium))) {
+ if (GlobalLock(medium.hGlobal)) {
+ HDROP drop = medium.hGlobal;
+
+ UINT files_num = DragQueryFileW(drop, 0xFFFFFFFF, NULL, 0);
+ char **files = talloc_zero_array(NULL, char*, files_num);
+
+ UINT recvd_files = 0;
+ for (UINT i = 0; i < files_num; i++) {
+ UINT len = DragQueryFileW(drop, i, NULL, 0);
+ wchar_t *buf = talloc_array(NULL, wchar_t, len + 1);
+
+ if (DragQueryFileW(drop, i, buf, len + 1) == len) {
+ char *fname = mp_to_utf8(files, buf);
+ files[recvd_files++] = fname;
+
+ MP_VERBOSE(t, "received dropped file: %s\n", fname);
+ } else {
+ MP_ERR(t, "error getting dropped file name\n");
+ }
+
+ talloc_free(buf);
+ }
+
+ GlobalUnlock(medium.hGlobal);
+ mp_event_drop_files(t->input_ctx, recvd_files, files, action);
+ talloc_free(files);
+ }
+
+ ReleaseStgMedium(&medium);
+ } else if (SUCCEEDED(IDataObject_GetData(pDataObj, &fmtetc_url, &medium))) {
+ wchar_t *wurl = GlobalLock(medium.hGlobal);
+ if (wurl) {
+ char *url = mp_to_utf8(NULL, wurl);
+ if (mp_event_drop_mime_data(t->input_ctx, "text/uri-list",
+ bstr0(url), action) > 0)
+ {
+ MP_VERBOSE(t, "received dropped URL: %s\n", url);
+ } else {
+ MP_ERR(t, "error getting dropped URL\n");
+ }
+
+ talloc_free(url);
+ GlobalUnlock(medium.hGlobal);
+ }
+
+ ReleaseStgMedium(&medium);
+ } else {
+ t->last_effect = DROPEFFECT_NONE;
+ }
+
+ *pdwEffect = t->last_effect;
+ return S_OK;
+}
+
+static IDropTargetVtbl idroptarget_vtbl = {
+ .QueryInterface = DropTarget_QueryInterface,
+ .AddRef = DropTarget_AddRef,
+ .Release = DropTarget_Release,
+ .DragEnter = DropTarget_DragEnter,
+ .DragOver = DropTarget_DragOver,
+ .DragLeave = DropTarget_DragLeave,
+ .Drop = DropTarget_Drop,
+};
+
+IDropTarget *mp_w32_droptarget_create(struct mp_log *log,
+ struct mp_vo_opts *opts,
+ struct input_ctx *input_ctx)
+{
+ fmtetc_url.cfFormat = RegisterClipboardFormatW(L"UniformResourceLocatorW");
+
+ struct droptarget *dt = talloc(NULL, struct droptarget);
+ dt->iface.lpVtbl = &idroptarget_vtbl;
+ atomic_store(&dt->ref_cnt, 0);
+ dt->last_effect = 0;
+ dt->data_obj = NULL;
+ dt->log = mp_log_new(dt, log, "droptarget");
+ dt->opts = opts;
+ dt->input_ctx = input_ctx;
+
+ return &dt->iface;
+}
diff --git a/video/out/win32/droptarget.h b/video/out/win32/droptarget.h
new file mode 100644
index 0000000..1c18c06
--- /dev/null
+++ b/video/out/win32/droptarget.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MP_WIN32_DROPTARGET_H_
+#define MP_WIN32_DROPTARGET_H_
+
+#include <windows.h>
+#include <ole2.h>
+#include <shobjidl.h>
+
+#include "input/input.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "options/options.h"
+
+// Create a IDropTarget implementation that sends dropped files to input_ctx
+IDropTarget *mp_w32_droptarget_create(struct mp_log *log,
+ struct mp_vo_opts *opts,
+ struct input_ctx *input_ctx);
+
+#endif
diff --git a/video/out/win_state.c b/video/out/win_state.c
new file mode 100644
index 0000000..b4bc9fd
--- /dev/null
+++ b/video/out/win_state.c
@@ -0,0 +1,155 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "win_state.h"
+#include "vo.h"
+
+#include "video/mp_image.h"
+
+static void calc_monitor_aspect(struct mp_vo_opts *opts, int scr_w, int scr_h,
+ double *pixelaspect, int *w, int *h)
+{
+ *pixelaspect = 1.0 / opts->monitor_pixel_aspect;
+
+ if (scr_w > 0 && scr_h > 0 && opts->force_monitor_aspect)
+ *pixelaspect = 1.0 / (opts->force_monitor_aspect * scr_h / scr_w);
+
+ if (*pixelaspect < 1) {
+ *h /= *pixelaspect;
+ } else {
+ *w *= *pixelaspect;
+ }
+}
+
+// Fit *w/*h into the size specified by geo.
+static void apply_autofit(int *w, int *h, int scr_w, int scr_h,
+ struct m_geometry *geo, bool allow_up, bool allow_down)
+{
+ if (!geo->wh_valid)
+ return;
+
+ int dummy = 0;
+ int n_w = *w, n_h = *h;
+ m_geometry_apply(&dummy, &dummy, &n_w, &n_h, scr_w, scr_h, geo);
+
+ if (!allow_up && *w <= n_w && *h <= n_h)
+ return;
+ if (!allow_down && *w >= n_w && *h >= n_h)
+ return;
+
+ // If aspect mismatches, always make the window smaller than the fit box
+ // (Or larger, if allow_down==false.)
+ double asp = (double)*w / *h;
+ double n_asp = (double)n_w / n_h;
+ if ((n_asp <= asp) == allow_down) {
+ *w = n_w;
+ *h = n_w / asp;
+ } else {
+ *w = n_h * asp;
+ *h = n_h;
+ }
+}
+
+// Compute the "suggested" window size and position and return it in *out_geo.
+// screen is the bounding box of the current screen within the virtual desktop.
+// Does not change *vo.
+// screen: position of the area on virtual desktop on which the video-content
+// should be placed (maybe after excluding decorations, taskbars, etc)
+// monitor: position of the monitor on virtual desktop (used for pixelaspect).
+// dpi_scale: the DPI multiplier to get from virtual to real coordinates
+// (>1 for "hidpi")
+// Use vo_apply_window_geometry() to copy the result into the vo.
+// NOTE: currently, all windowing backends do their own handling of window
+// geometry additional to this code. This is to deal with initial window
+// placement, fullscreen handling, avoiding resize on reconfig() with no
+// size change, multi-monitor stuff, and possibly more.
+void vo_calc_window_geometry3(struct vo *vo, const struct mp_rect *screen,
+ const struct mp_rect *monitor,
+ double dpi_scale, struct vo_win_geometry *out_geo)
+{
+ struct mp_vo_opts *opts = vo->opts;
+
+ *out_geo = (struct vo_win_geometry){0};
+
+ // The case of calling this function even though no video was configured
+ // yet (i.e. vo->params==NULL) happens when vo_gpu creates a hidden window
+ // in order to create a rendering context.
+ struct mp_image_params params = { .w = 320, .h = 200 };
+ if (vo->params)
+ params = *vo->params;
+
+ if (!opts->hidpi_window_scale)
+ dpi_scale = 1;
+
+ int d_w, d_h;
+ mp_image_params_get_dsize(&params, &d_w, &d_h);
+ if ((vo->driver->caps & VO_CAP_ROTATE90) && params.rotate % 180 == 90)
+ MPSWAP(int, d_w, d_h);
+ d_w = MPCLAMP(d_w * opts->window_scale * dpi_scale, 1, 16000);
+ d_h = MPCLAMP(d_h * opts->window_scale * dpi_scale, 1, 16000);
+
+ int scr_w = screen->x1 - screen->x0;
+ int scr_h = screen->y1 - screen->y0;
+
+ int mon_w = monitor->x1 - monitor->x0;
+ int mon_h = monitor->y1 - monitor->y0;
+
+ MP_DBG(vo, "max content size: %dx%d\n", scr_w, scr_h);
+ MP_DBG(vo, "monitor size: %dx%d\n", mon_w, mon_h);
+
+ calc_monitor_aspect(opts, mon_w, mon_h, &out_geo->monitor_par, &d_w, &d_h);
+
+ apply_autofit(&d_w, &d_h, scr_w, scr_h, &opts->autofit, true, true);
+ apply_autofit(&d_w, &d_h, scr_w, scr_h, &opts->autofit_smaller, true, false);
+ apply_autofit(&d_w, &d_h, scr_w, scr_h, &opts->autofit_larger, false, true);
+
+ out_geo->win.x0 = (int)(scr_w - d_w) / 2;
+ out_geo->win.y0 = (int)(scr_h - d_h) / 2;
+ m_geometry_apply(&out_geo->win.x0, &out_geo->win.y0, &d_w, &d_h,
+ scr_w, scr_h, &opts->geometry);
+
+ out_geo->win.x0 += screen->x0;
+ out_geo->win.y0 += screen->y0;
+ out_geo->win.x1 = out_geo->win.x0 + d_w;
+ out_geo->win.y1 = out_geo->win.y0 + d_h;
+
+ if (opts->geometry.xy_valid || opts->force_window_position)
+ out_geo->flags |= VO_WIN_FORCE_POS;
+}
+
+// same as vo_calc_window_geometry3 with monitor assumed same as screen
+void vo_calc_window_geometry2(struct vo *vo, const struct mp_rect *screen,
+ double dpi_scale, struct vo_win_geometry *out_geo)
+{
+ vo_calc_window_geometry3(vo, screen, screen, dpi_scale, out_geo);
+}
+
+void vo_calc_window_geometry(struct vo *vo, const struct mp_rect *screen,
+ struct vo_win_geometry *out_geo)
+{
+ vo_calc_window_geometry2(vo, screen, 1.0, out_geo);
+}
+
+// Copy the parameters in *geo to the vo fields.
+// (Doesn't do anything else - windowing backends should trigger VO_EVENT_RESIZE
+// to ensure that the VO reinitializes rendering properly.)
+void vo_apply_window_geometry(struct vo *vo, const struct vo_win_geometry *geo)
+{
+ vo->dwidth = geo->win.x1 - geo->win.x0;
+ vo->dheight = geo->win.y1 - geo->win.y0;
+ vo->monitor_par = geo->monitor_par;
+}
diff --git a/video/out/win_state.h b/video/out/win_state.h
new file mode 100644
index 0000000..a253efa
--- /dev/null
+++ b/video/out/win_state.h
@@ -0,0 +1,35 @@
+#ifndef MP_WIN_STATE_H_
+#define MP_WIN_STATE_H_
+
+#include "common/common.h"
+
+struct vo;
+
+enum {
+ // By user settings, the window manager's chosen window position should
+ // be overridden.
+ VO_WIN_FORCE_POS = (1 << 0),
+};
+
+struct vo_win_geometry {
+ // Bitfield of VO_WIN_* flags
+ int flags;
+ // Position & size of the window. In xinerama coordinates, i.e. they're
+ // relative to the virtual desktop encompassing all screens, not the
+ // current screen.
+ struct mp_rect win;
+ // Aspect ratio of the current monitor.
+ // (calculated from screen size and options.)
+ double monitor_par;
+};
+
+void vo_calc_window_geometry(struct vo *vo, const struct mp_rect *screen,
+ struct vo_win_geometry *out_geo);
+void vo_calc_window_geometry2(struct vo *vo, const struct mp_rect *screen,
+ double dpi_scale, struct vo_win_geometry *out_geo);
+void vo_calc_window_geometry3(struct vo *vo, const struct mp_rect *screen,
+ const struct mp_rect *monitor,
+ double dpi_scale, struct vo_win_geometry *out_geo);
+void vo_apply_window_geometry(struct vo *vo, const struct vo_win_geometry *geo);
+
+#endif
diff --git a/video/out/wldmabuf/context_wldmabuf.c b/video/out/wldmabuf/context_wldmabuf.c
new file mode 100644
index 0000000..c494575
--- /dev/null
+++ b/video/out/wldmabuf/context_wldmabuf.c
@@ -0,0 +1,43 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "video/out/wayland_common.h"
+#include "video/out/opengl/context.h"
+#include "ra_wldmabuf.h"
+
+static void uninit(struct ra_ctx *ctx)
+{
+ ra_free(&ctx->ra);
+ vo_wayland_uninit(ctx->vo);
+}
+
+static bool init(struct ra_ctx *ctx)
+{
+ if (!vo_wayland_init(ctx->vo))
+ return false;
+ ctx->ra = ra_create_wayland(ctx->log, ctx->vo);
+
+ return true;
+}
+
+const struct ra_ctx_fns ra_ctx_wldmabuf = {
+ .type = "none",
+ .name = "wldmabuf",
+ .hidden = true,
+ .init = init,
+ .uninit = uninit,
+};
diff --git a/video/out/wldmabuf/ra_wldmabuf.c b/video/out/wldmabuf/ra_wldmabuf.c
new file mode 100644
index 0000000..3f27314
--- /dev/null
+++ b/video/out/wldmabuf/ra_wldmabuf.c
@@ -0,0 +1,66 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "video/out/wayland_common.h"
+#include "video/out/gpu/ra.h"
+#include "ra_wldmabuf.h"
+
+struct priv {
+ struct vo *vo;
+};
+
+static void destroy(struct ra *ra)
+{
+ talloc_free(ra->priv);
+}
+
+bool ra_compatible_format(struct ra* ra, uint32_t drm_format, uint64_t modifier)
+{
+ struct priv* p = ra->priv;
+ struct vo_wayland_state *wl = p->vo->wl;
+ const wayland_format *formats = wl->format_map;
+
+ for (int i = 0; i < wl->format_size / sizeof(wayland_format); i++) {
+ if (drm_format == formats[i].format && modifier == formats[i].modifier)
+ return true;
+ }
+
+ return false;
+}
+
+static struct ra_fns ra_fns_wldmabuf = {
+ .destroy = destroy,
+};
+
+struct ra *ra_create_wayland(struct mp_log *log, struct vo* vo)
+{
+ struct ra *ra = talloc_zero(NULL, struct ra);
+
+ ra->fns = &ra_fns_wldmabuf;
+ ra->log = log;
+ ra_add_native_resource(ra, "wl", vo->wl->display);
+ ra->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ra->priv;
+ p->vo = vo;
+
+ return ra;
+}
+
+bool ra_is_wldmabuf(struct ra *ra)
+{
+ return (ra->fns == &ra_fns_wldmabuf);
+}
diff --git a/video/out/wldmabuf/ra_wldmabuf.h b/video/out/wldmabuf/ra_wldmabuf.h
new file mode 100644
index 0000000..8e20173
--- /dev/null
+++ b/video/out/wldmabuf/ra_wldmabuf.h
@@ -0,0 +1,23 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "video/out/wayland_common.h"
+
+struct ra *ra_create_wayland(struct mp_log *log, struct vo *vo);
+bool ra_compatible_format(struct ra* ra, uint32_t drm_format, uint64_t modifier);
+bool ra_is_wldmabuf(struct ra *ra);
diff --git a/video/out/x11_common.c b/video/out/x11_common.c
new file mode 100644
index 0000000..b4605bf
--- /dev/null
+++ b/video/out/x11_common.c
@@ -0,0 +1,2291 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <unistd.h>
+#include <poll.h>
+#include <string.h>
+#include <assert.h>
+
+#include <X11/Xmd.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <X11/keysym.h>
+#include <X11/XKBlib.h>
+#include <X11/XF86keysym.h>
+
+#include <X11/extensions/scrnsaver.h>
+#include <X11/extensions/dpms.h>
+#include <X11/extensions/shape.h>
+#include <X11/extensions/Xpresent.h>
+#include <X11/extensions/Xrandr.h>
+
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "input/event.h"
+#include "video/image_loader.h"
+#include "video/mp_image.h"
+#include "present_sync.h"
+#include "x11_common.h"
+#include "mpv_talloc.h"
+
+#include "vo.h"
+#include "win_state.h"
+#include "osdep/io.h"
+#include "osdep/poll_wrapper.h"
+#include "osdep/timer.h"
+#include "osdep/subprocess.h"
+
+#include "input/input.h"
+#include "input/keycodes.h"
+
+#define vo_wm_LAYER 1
+#define vo_wm_FULLSCREEN 2
+#define vo_wm_STAYS_ON_TOP 4
+#define vo_wm_ABOVE 8
+#define vo_wm_BELOW 16
+#define vo_wm_STICKY 32
+
+/* EWMH state actions, see
+ http://freedesktop.org/Standards/wm-spec/index.html#id2768769 */
+#define NET_WM_STATE_REMOVE 0 /* remove/unset property */
+#define NET_WM_STATE_ADD 1 /* add/set property */
+#define NET_WM_STATE_TOGGLE 2 /* toggle property */
+
+#define WIN_LAYER_ONBOTTOM 2
+#define WIN_LAYER_NORMAL 4
+#define WIN_LAYER_ONTOP 6
+#define WIN_LAYER_ABOVE_DOCK 10
+
+#define DND_VERSION 5
+
+#define XEMBED_VERSION 0
+#define XEMBED_MAPPED (1 << 0)
+#define XEMBED_EMBEDDED_NOTIFY 0
+#define XEMBED_REQUEST_FOCUS 3
+
+// ----- Motif header: -------
+
+#define MWM_HINTS_FUNCTIONS (1L << 0)
+#define MWM_HINTS_DECORATIONS (1L << 1)
+
+#define MWM_FUNC_RESIZE (1L << 1)
+#define MWM_FUNC_MOVE (1L << 2)
+#define MWM_FUNC_MINIMIZE (1L << 3)
+#define MWM_FUNC_MAXIMIZE (1L << 4)
+#define MWM_FUNC_CLOSE (1L << 5)
+
+#define MWM_DECOR_ALL (1L << 0)
+
+typedef struct
+{
+ long flags;
+ long functions;
+ long decorations;
+ long input_mode;
+ long state;
+} MotifWmHints;
+
+static const char x11_icon_16[] =
+#include "etc/mpv-icon-8bit-16x16.png.inc"
+;
+
+static const char x11_icon_32[] =
+#include "etc/mpv-icon-8bit-32x32.png.inc"
+;
+
+static const char x11_icon_64[] =
+#include "etc/mpv-icon-8bit-64x64.png.inc"
+;
+
+static const char x11_icon_128[] =
+#include "etc/mpv-icon-8bit-128x128.png.inc"
+;
+
+#define ICON_ENTRY(var) { (char *)var, sizeof(var) }
+static const struct bstr x11_icons[] = {
+ ICON_ENTRY(x11_icon_16),
+ ICON_ENTRY(x11_icon_32),
+ ICON_ENTRY(x11_icon_64),
+ ICON_ENTRY(x11_icon_128),
+ {0}
+};
+
+static struct mp_log *x11_error_output;
+static atomic_int x11_error_silence;
+
+static bool rc_overlaps(struct mp_rect rc1, struct mp_rect rc2);
+static void vo_x11_update_geometry(struct vo *vo);
+static void vo_x11_fullscreen(struct vo *vo);
+static void xscreensaver_heartbeat(struct vo_x11_state *x11);
+static void set_screensaver(struct vo_x11_state *x11, bool enabled);
+static void vo_x11_selectinput_witherr(struct vo *vo, Display *display,
+ Window w, long event_mask);
+static void vo_x11_setlayer(struct vo *vo, bool ontop);
+static void vo_x11_xembed_handle_message(struct vo *vo, XClientMessageEvent *ce);
+static void vo_x11_xembed_send_message(struct vo_x11_state *x11, long m[4]);
+static void vo_x11_move_resize(struct vo *vo, bool move, bool resize,
+ struct mp_rect rc);
+static void vo_x11_maximize(struct vo *vo);
+static void vo_x11_minimize(struct vo *vo);
+static void vo_x11_set_input_region(struct vo *vo, bool passthrough);
+static void vo_x11_sticky(struct vo *vo, bool sticky);
+
+#define XA(x11, s) (XInternAtom((x11)->display, # s, False))
+#define XAs(x11, s) XInternAtom((x11)->display, s, False)
+
+#define RC_W(rc) ((rc).x1 - (rc).x0)
+#define RC_H(rc) ((rc).y1 - (rc).y0)
+
+static char *x11_atom_name_buf(struct vo_x11_state *x11, Atom atom,
+ char *buf, size_t buf_size)
+{
+ buf[0] = '\0';
+
+ char *new_name = XGetAtomName(x11->display, atom);
+ if (new_name) {
+ snprintf(buf, buf_size, "%s", new_name);
+ XFree(new_name);
+ }
+
+ return buf;
+}
+
+#define x11_atom_name(x11, atom) x11_atom_name_buf(x11, atom, (char[80]){0}, 80)
+
+// format = 8 (unsigned char), 16 (short), 32 (long, even on LP64 systems)
+// *out_nitems = returned number of items of requested format
+static void *x11_get_property(struct vo_x11_state *x11, Window w, Atom property,
+ Atom type, int format, int *out_nitems)
+{
+ assert(format == 8 || format == 16 || format == 32);
+ *out_nitems = 0;
+ if (!w)
+ return NULL;
+ long max_len = 128 * 1024 * 1024; // static maximum limit
+ Atom ret_type = 0;
+ int ret_format = 0;
+ unsigned long ret_nitems = 0;
+ unsigned long ret_bytesleft = 0;
+ unsigned char *ret_prop = NULL;
+ if (XGetWindowProperty(x11->display, w, property, 0, max_len, False, type,
+ &ret_type, &ret_format, &ret_nitems, &ret_bytesleft,
+ &ret_prop) != Success)
+ return NULL;
+ if (ret_format != format || ret_nitems < 1 || ret_bytesleft) {
+ XFree(ret_prop);
+ ret_prop = NULL;
+ ret_nitems = 0;
+ }
+ *out_nitems = ret_nitems;
+ return ret_prop;
+}
+
+static bool x11_get_property_copy(struct vo_x11_state *x11, Window w,
+ Atom property, Atom type, int format,
+ void *dst, size_t dst_size)
+{
+ bool ret = false;
+ int len;
+ void *ptr = x11_get_property(x11, w, property, type, format, &len);
+ if (ptr) {
+ size_t ib = format == 32 ? sizeof(long) : format / 8;
+ if (dst_size <= len * ib) {
+ memcpy(dst, ptr, dst_size);
+ ret = true;
+ }
+ XFree(ptr);
+ }
+ return ret;
+}
+
+static void x11_send_ewmh_msg(struct vo_x11_state *x11, char *message_type,
+ long params[5])
+{
+ if (!x11->window)
+ return;
+
+ XEvent xev = {
+ .xclient = {
+ .type = ClientMessage,
+ .send_event = True,
+ .message_type = XInternAtom(x11->display, message_type, False),
+ .window = x11->window,
+ .format = 32,
+ },
+ };
+ for (int n = 0; n < 5; n++)
+ xev.xclient.data.l[n] = params[n];
+
+ if (!XSendEvent(x11->display, x11->rootwin, False,
+ SubstructureRedirectMask | SubstructureNotifyMask,
+ &xev))
+ MP_ERR(x11, "Couldn't send EWMH %s message!\n", message_type);
+}
+
+// change the _NET_WM_STATE hint. Remove or add the state according to "set".
+static void x11_set_ewmh_state(struct vo_x11_state *x11, char *state, bool set)
+{
+ long params[5] = {
+ set ? NET_WM_STATE_ADD : NET_WM_STATE_REMOVE,
+ XInternAtom(x11->display, state, False),
+ 0, // No second state
+ 1, // source indication: normal
+ };
+ x11_send_ewmh_msg(x11, "_NET_WM_STATE", params);
+}
+
+static void vo_update_cursor(struct vo *vo)
+{
+ Cursor no_ptr;
+ Pixmap bm_no;
+ XColor black, dummy;
+ Colormap colormap;
+ const char bm_no_data[] = {0, 0, 0, 0, 0, 0, 0, 0};
+ struct vo_x11_state *x11 = vo->x11;
+ Display *disp = x11->display;
+ Window win = x11->window;
+ bool should_hide = x11->has_focus && !x11->mouse_cursor_visible;
+
+ if (should_hide == x11->mouse_cursor_set)
+ return;
+
+ x11->mouse_cursor_set = should_hide;
+
+ if (x11->parent == x11->rootwin || !win)
+ return; // do not hide if playing on the root window
+
+ if (x11->mouse_cursor_set) {
+ colormap = DefaultColormap(disp, DefaultScreen(disp));
+ if (!XAllocNamedColor(disp, colormap, "black", &black, &dummy))
+ return; // color alloc failed, give up
+ bm_no = XCreateBitmapFromData(disp, win, bm_no_data, 8, 8);
+ no_ptr = XCreatePixmapCursor(disp, bm_no, bm_no, &black, &black, 0, 0);
+ XDefineCursor(disp, win, no_ptr);
+ XFreeCursor(disp, no_ptr);
+ if (bm_no != None)
+ XFreePixmap(disp, bm_no);
+ XFreeColors(disp, colormap, &black.pixel, 1, 0);
+ } else {
+ XDefineCursor(x11->display, x11->window, 0);
+ }
+}
+
+static int x11_errorhandler(Display *display, XErrorEvent *event)
+{
+ struct mp_log *log = x11_error_output;
+ if (!log)
+ return 0;
+
+ char msg[60];
+ XGetErrorText(display, event->error_code, (char *) &msg, sizeof(msg));
+
+ int lev = atomic_load(&x11_error_silence) ? MSGL_V : MSGL_ERR;
+ mp_msg(log, lev, "X11 error: %s\n", msg);
+ mp_msg(log, lev, "Type: %x, display: %p, resourceid: %lx, serial: %lx\n",
+ event->type, event->display, event->resourceid, event->serial);
+ mp_msg(log, lev, "Error code: %x, request code: %x, minor code: %x\n",
+ event->error_code, event->request_code, event->minor_code);
+
+ return 0;
+}
+
+void vo_x11_silence_xlib(int dir)
+{
+ atomic_fetch_add(&x11_error_silence, dir);
+}
+
+static int net_wm_support_state_test(struct vo_x11_state *x11, Atom atom)
+{
+#define NET_WM_STATE_TEST(x) { \
+ if (atom == XA(x11, _NET_WM_STATE_##x)) { \
+ MP_DBG(x11, "Detected wm supports " #x " state.\n" ); \
+ return vo_wm_##x; \
+ } \
+}
+
+ NET_WM_STATE_TEST(FULLSCREEN);
+ NET_WM_STATE_TEST(ABOVE);
+ NET_WM_STATE_TEST(STAYS_ON_TOP);
+ NET_WM_STATE_TEST(BELOW);
+ NET_WM_STATE_TEST(STICKY);
+ return 0;
+}
+
+static int vo_wm_detect(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ int i;
+ int wm = 0;
+ int nitems;
+ Atom *args = NULL;
+ Window win = x11->rootwin;
+
+ if (x11->parent)
+ return 0;
+
+// -- supports layers
+ args = x11_get_property(x11, win, XA(x11, _WIN_PROTOCOLS), XA_ATOM, 32,
+ &nitems);
+ if (args) {
+ for (i = 0; i < nitems; i++) {
+ if (args[i] == XA(x11, _WIN_LAYER)) {
+ MP_DBG(x11, "Detected wm supports layers.\n");
+ wm |= vo_wm_LAYER;
+ }
+ }
+ XFree(args);
+ }
+// --- netwm
+ args = x11_get_property(x11, win, XA(x11, _NET_SUPPORTED), XA_ATOM, 32,
+ &nitems);
+ if (args) {
+ MP_DBG(x11, "Detected wm supports NetWM.\n");
+ if (x11->opts->x11_netwm >= 0) {
+ for (i = 0; i < nitems; i++)
+ wm |= net_wm_support_state_test(vo->x11, args[i]);
+ } else {
+ MP_DBG(x11, "NetWM usage disabled by user.\n");
+ }
+ XFree(args);
+ }
+
+ if (wm == 0)
+ MP_DBG(x11, "Unknown wm type...\n");
+ if (x11->opts->x11_netwm > 0 && !(wm & vo_wm_FULLSCREEN)) {
+ MP_WARN(x11, "Forcing NetWM FULLSCREEN support.\n");
+ wm |= vo_wm_FULLSCREEN;
+ }
+ return wm;
+}
+
+static void xpresent_set(struct vo_x11_state *x11)
+{
+ int present = x11->opts->x11_present;
+ x11->use_present = x11->present_code &&
+ ((x11->has_mesa && !x11->has_nvidia && present) ||
+ present == 2);
+ if (x11->use_present) {
+ MP_VERBOSE(x11, "XPresent enabled.\n");
+ } else {
+ MP_VERBOSE(x11, "XPresent disabled.\n");
+ }
+}
+
+static void xrandr_read(struct vo_x11_state *x11)
+{
+ for(int i = 0; i < x11->num_displays; i++)
+ talloc_free(x11->displays[i].name);
+
+ x11->num_displays = 0;
+
+ if (x11->xrandr_event < 0) {
+ int event_base, error_base;
+ if (!XRRQueryExtension(x11->display, &event_base, &error_base)) {
+ MP_VERBOSE(x11, "Couldn't init Xrandr.\n");
+ return;
+ }
+ x11->xrandr_event = event_base + RRNotify;
+ XRRSelectInput(x11->display, x11->rootwin, RRScreenChangeNotifyMask |
+ RRCrtcChangeNotifyMask | RROutputChangeNotifyMask);
+ }
+
+ XRRScreenResources *r = XRRGetScreenResourcesCurrent(x11->display, x11->rootwin);
+ if (!r) {
+ MP_VERBOSE(x11, "Xrandr doesn't work.\n");
+ return;
+ }
+
+ /* Look at the available providers on the current screen and try to determine
+ * the driver. If amd/intel/radeon, assume this is mesa. If nvidia is found,
+ * assume nvidia. Because the same screen can have multiple providers (e.g.
+ * a laptop with switchable graphics), we need to know both of these things.
+ * In practice, this is used for determining whether or not to use XPresent
+ * (i.e. needs to be Mesa and not Nvidia). Requires Randr 1.4. */
+ XRRProviderResources *pr = XRRGetProviderResources(x11->display, x11->rootwin);
+ for (int i = 0; i < pr->nproviders; i++) {
+ XRRProviderInfo *info = XRRGetProviderInfo(x11->display, r, pr->providers[i]);
+ struct bstr provider_name = bstrdup(x11, bstr0(info->name));
+ bstr_lower(provider_name);
+ int amd = bstr_find0(provider_name, "amd");
+ int intel = bstr_find0(provider_name, "intel");
+ int modesetting = bstr_find0(provider_name, "modesetting");
+ int nouveau = bstr_find0(provider_name, "nouveau");
+ int nvidia = bstr_find0(provider_name, "nvidia");
+ int radeon = bstr_find0(provider_name, "radeon");
+ x11->has_mesa = x11->has_mesa || amd >= 0 || intel >= 0 ||
+ modesetting >= 0 || nouveau >= 0 || radeon >= 0;
+ x11->has_nvidia = x11->has_nvidia || nvidia >= 0;
+ XRRFreeProviderInfo(info);
+ }
+ if (x11->present_code)
+ xpresent_set(x11);
+ XRRFreeProviderResources(pr);
+
+ int primary_id = -1;
+ RROutput primary = XRRGetOutputPrimary(x11->display, x11->rootwin);
+ for (int o = 0; o < r->noutput; o++) {
+ RROutput output = r->outputs[o];
+ XRRCrtcInfo *crtc = NULL;
+ XRROutputInfo *out = XRRGetOutputInfo(x11->display, r, output);
+ if (!out || !out->crtc)
+ goto next;
+ crtc = XRRGetCrtcInfo(x11->display, r, out->crtc);
+ if (!crtc)
+ goto next;
+ for (int om = 0; om < out->nmode; om++) {
+ RRMode xm = out->modes[om];
+ for (int n = 0; n < r->nmode; n++) {
+ XRRModeInfo m = r->modes[n];
+ if (m.id != xm || crtc->mode != xm)
+ continue;
+ if (x11->num_displays >= MAX_DISPLAYS)
+ continue;
+ double vTotal = m.vTotal;
+ if (m.modeFlags & RR_DoubleScan)
+ vTotal *= 2;
+ if (m.modeFlags & RR_Interlace)
+ vTotal /= 2;
+ struct xrandr_display d = {
+ .rc = { crtc->x, crtc->y,
+ crtc->x + crtc->width, crtc->y + crtc->height },
+ .fps = m.dotClock / (m.hTotal * vTotal),
+ .name = talloc_strdup(x11, out->name),
+ };
+ int num = x11->num_displays++;
+ MP_VERBOSE(x11, "Display %d (%s): [%d, %d, %d, %d] @ %f FPS\n",
+ num, d.name, d.rc.x0, d.rc.y0, d.rc.x1, d.rc.y1, d.fps);
+ x11->displays[num] = d;
+ if (output == primary)
+ primary_id = num;
+ }
+ }
+ next:
+ if (crtc)
+ XRRFreeCrtcInfo(crtc);
+ if (out)
+ XRRFreeOutputInfo(out);
+ }
+
+ for (int i = 0; i < x11->num_displays; i++) {
+ struct xrandr_display *d = &(x11->displays[i]);
+ d->screen = i;
+
+ if (i == primary_id) {
+ d->atom_id = 0;
+ continue;
+ }
+ if (primary_id > 0 && i < primary_id) {
+ d->atom_id = i+1;
+ continue;
+ }
+ d->atom_id = i;
+ }
+
+ XRRFreeScreenResources(r);
+}
+
+static int vo_x11_select_screen(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+ int screen = -2; // all displays
+ if (!opts->fullscreen || opts->fsscreen_id != -2) {
+ screen = opts->fullscreen ? opts->fsscreen_id : opts->screen_id;
+ if (opts->fullscreen && opts->fsscreen_id == -1)
+ screen = opts->screen_id;
+
+ if (screen == -1 && (opts->fsscreen_name || opts->screen_name)) {
+ char *screen_name = opts->fullscreen ? opts->fsscreen_name : opts->screen_name;
+ if (screen_name) {
+ bool screen_found = false;
+ for (int n = 0; n < x11->num_displays; n++) {
+ char *display_name = x11->displays[n].name;
+ if (!strcmp(display_name, screen_name)) {
+ screen = n;
+ screen_found = true;
+ break;
+ }
+ }
+ if (!screen_found)
+ MP_WARN(x11, "Screen name %s not found!\n", screen_name);
+ }
+ }
+
+ if (screen >= x11->num_displays)
+ screen = x11->num_displays - 1;
+ }
+ return screen;
+}
+
+static void vo_x11_update_screeninfo(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ x11->screenrc = (struct mp_rect){.x1 = x11->ws_width, .y1 = x11->ws_height};
+ int screen = vo_x11_select_screen(vo);
+ if (screen >= -1) {
+ if (screen == -1) {
+ int x = x11->winrc.x0 + RC_W(x11->winrc) / 2;
+ int y = x11->winrc.y0 + RC_H(x11->winrc) / 2;
+ for (screen = x11->num_displays - 1; screen > 0; screen--) {
+ struct xrandr_display *disp = &x11->displays[screen];
+ int left = disp->rc.x0;
+ int right = disp->rc.x1;
+ int top = disp->rc.y0;
+ int bottom = disp->rc.y1;
+ if (left <= x && x <= right && top <= y && y <= bottom)
+ break;
+ }
+ }
+
+ if (screen < 0)
+ screen = 0;
+ x11->screenrc = (struct mp_rect){
+ .x0 = x11->displays[screen].rc.x0,
+ .y0 = x11->displays[screen].rc.y0,
+ .x1 = x11->displays[screen].rc.x1,
+ .y1 = x11->displays[screen].rc.y1,
+ };
+ }
+}
+
+static struct xrandr_display *get_current_display(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct xrandr_display *selected_disp = NULL;
+ for (int n = 0; n < x11->num_displays; n++) {
+ struct xrandr_display *disp = &x11->displays[n];
+ disp->overlaps = rc_overlaps(disp->rc, x11->winrc);
+ if (disp->overlaps && (!selected_disp || disp->fps < selected_disp->fps))
+ selected_disp = disp;
+ }
+ return selected_disp;
+}
+
+// Get the monitors for the 4 edges of the rectangle spanning all screens.
+static void vo_x11_get_bounding_monitors(struct vo_x11_state *x11, long b[4])
+{
+ //top bottom left right
+ b[0] = b[1] = b[2] = b[3] = 0;
+ for (int n = 0; n < x11->num_displays; n++) {
+ struct xrandr_display *d = &x11->displays[n];
+ if (d->rc.y0 < x11->displays[b[0]].rc.y0)
+ b[0] = n;
+ if (d->rc.y1 < x11->displays[b[1]].rc.y1)
+ b[1] = n;
+ if (d->rc.x0 < x11->displays[b[2]].rc.x0)
+ b[2] = n;
+ if (d->rc.x1 < x11->displays[b[3]].rc.x1)
+ b[3] = n;
+ }
+}
+
+bool vo_x11_init(struct vo *vo)
+{
+ char *dispName;
+
+ assert(!vo->x11);
+
+ XInitThreads();
+
+ struct vo_x11_state *x11 = talloc_ptrtype(NULL, x11);
+ *x11 = (struct vo_x11_state){
+ .log = mp_log_new(x11, vo->log, "x11"),
+ .input_ctx = vo->input_ctx,
+ .screensaver_enabled = true,
+ .xrandr_event = -1,
+ .wakeup_pipe = {-1, -1},
+ .dpi_scale = 1,
+ .opts_cache = m_config_cache_alloc(x11, vo->global, &vo_sub_opts),
+ };
+ x11->opts = x11->opts_cache->opts;
+ vo->x11 = x11;
+
+ x11_error_output = x11->log;
+ XSetErrorHandler(x11_errorhandler);
+ x11->present = mp_present_initialize(x11, x11->opts, VO_MAX_SWAPCHAIN_DEPTH);
+
+ dispName = XDisplayName(NULL);
+
+ MP_VERBOSE(x11, "X11 opening display: %s\n", dispName);
+
+ x11->display = XOpenDisplay(dispName);
+ if (!x11->display) {
+ MP_MSG(x11, vo->probing ? MSGL_V : MSGL_ERR,
+ "couldn't open the X11 display (%s)!\n", dispName);
+ goto error;
+ }
+ x11->screen = DefaultScreen(x11->display); // screen ID
+ x11->rootwin = RootWindow(x11->display, x11->screen); // root window ID
+
+ if (x11->opts->WinID >= 0)
+ x11->parent = x11->opts->WinID ? x11->opts->WinID : x11->rootwin;
+
+ if (!x11->opts->native_keyrepeat) {
+ Bool ok = False;
+ XkbSetDetectableAutoRepeat(x11->display, True, &ok);
+ x11->no_autorepeat = ok;
+ }
+
+ x11->xim = XOpenIM(x11->display, NULL, NULL, NULL);
+ if (!x11->xim)
+ MP_WARN(x11, "XOpenIM() failed. Unicode input will not work.\n");
+
+ x11->ws_width = DisplayWidth(x11->display, x11->screen);
+ x11->ws_height = DisplayHeight(x11->display, x11->screen);
+
+ if (strncmp(dispName, "unix:", 5) == 0)
+ dispName += 4;
+ else if (strncmp(dispName, "localhost:", 10) == 0)
+ dispName += 9;
+ x11->display_is_local = dispName[0] == ':' &&
+ strtoul(dispName + 1, NULL, 10) < 10;
+ MP_DBG(x11, "X11 running at %dx%d (\"%s\" => %s display)\n",
+ x11->ws_width, x11->ws_height, dispName,
+ x11->display_is_local ? "local" : "remote");
+
+ int w_mm = DisplayWidthMM(x11->display, x11->screen);
+ int h_mm = DisplayHeightMM(x11->display, x11->screen);
+ double dpi_x = x11->ws_width * 25.4 / w_mm;
+ double dpi_y = x11->ws_height * 25.4 / h_mm;
+ double base_dpi = 96;
+ if (isfinite(dpi_x) && isfinite(dpi_y) && x11->opts->hidpi_window_scale) {
+ int s_x = lrint(MPCLAMP(dpi_x / base_dpi, 0, 10));
+ int s_y = lrint(MPCLAMP(dpi_y / base_dpi, 0, 10));
+ if (s_x == s_y && s_x > 1 && s_x < 10) {
+ x11->dpi_scale = s_x;
+ MP_VERBOSE(x11, "Assuming DPI scale %d for prescaling. This can "
+ "be disabled with --hidpi-window-scale=no.\n",
+ x11->dpi_scale);
+ }
+ }
+
+ x11->wm_type = vo_wm_detect(vo);
+
+ x11->event_fd = ConnectionNumber(x11->display);
+ mp_make_wakeup_pipe(x11->wakeup_pipe);
+
+ xrandr_read(x11);
+
+ vo_x11_update_geometry(vo);
+
+ return true;
+
+error:
+ vo_x11_uninit(vo);
+ return false;
+}
+
+static const struct mp_keymap keymap[] = {
+ // special keys
+ {XK_Pause, MP_KEY_PAUSE}, {XK_Escape, MP_KEY_ESC},
+ {XK_BackSpace, MP_KEY_BS}, {XK_Tab, MP_KEY_TAB}, {XK_Return, MP_KEY_ENTER},
+ {XK_Menu, MP_KEY_MENU}, {XK_Print, MP_KEY_PRINT},
+ {XK_Cancel, MP_KEY_CANCEL}, {XK_ISO_Left_Tab, MP_KEY_TAB},
+
+ // cursor keys
+ {XK_Left, MP_KEY_LEFT}, {XK_Right, MP_KEY_RIGHT}, {XK_Up, MP_KEY_UP},
+ {XK_Down, MP_KEY_DOWN},
+
+ // navigation block
+ {XK_Insert, MP_KEY_INSERT}, {XK_Delete, MP_KEY_DELETE},
+ {XK_Home, MP_KEY_HOME}, {XK_End, MP_KEY_END}, {XK_Page_Up, MP_KEY_PAGE_UP},
+ {XK_Page_Down, MP_KEY_PAGE_DOWN},
+
+ // F-keys
+ {XK_F1, MP_KEY_F+1}, {XK_F2, MP_KEY_F+2}, {XK_F3, MP_KEY_F+3},
+ {XK_F4, MP_KEY_F+4}, {XK_F5, MP_KEY_F+5}, {XK_F6, MP_KEY_F+6},
+ {XK_F7, MP_KEY_F+7}, {XK_F8, MP_KEY_F+8}, {XK_F9, MP_KEY_F+9},
+ {XK_F10, MP_KEY_F+10}, {XK_F11, MP_KEY_F+11}, {XK_F12, MP_KEY_F+12},
+ {XK_F13, MP_KEY_F+13}, {XK_F14, MP_KEY_F+14}, {XK_F15, MP_KEY_F+15},
+ {XK_F16, MP_KEY_F+16}, {XK_F17, MP_KEY_F+17}, {XK_F18, MP_KEY_F+18},
+ {XK_F19, MP_KEY_F+19}, {XK_F20, MP_KEY_F+20}, {XK_F21, MP_KEY_F+21},
+ {XK_F22, MP_KEY_F+22}, {XK_F23, MP_KEY_F+23}, {XK_F24, MP_KEY_F+24},
+
+ // numpad independent of numlock
+ {XK_KP_Subtract, '-'}, {XK_KP_Add, '+'}, {XK_KP_Multiply, '*'},
+ {XK_KP_Divide, '/'}, {XK_KP_Enter, MP_KEY_KPENTER},
+
+ // numpad with numlock
+ {XK_KP_0, MP_KEY_KP0}, {XK_KP_1, MP_KEY_KP1}, {XK_KP_2, MP_KEY_KP2},
+ {XK_KP_3, MP_KEY_KP3}, {XK_KP_4, MP_KEY_KP4}, {XK_KP_5, MP_KEY_KP5},
+ {XK_KP_6, MP_KEY_KP6}, {XK_KP_7, MP_KEY_KP7}, {XK_KP_8, MP_KEY_KP8},
+ {XK_KP_9, MP_KEY_KP9}, {XK_KP_Decimal, MP_KEY_KPDEC},
+ {XK_KP_Separator, MP_KEY_KPDEC},
+
+ // numpad without numlock
+ {XK_KP_Insert, MP_KEY_KPINS}, {XK_KP_End, MP_KEY_KPEND},
+ {XK_KP_Down, MP_KEY_KPDOWN}, {XK_KP_Page_Down, MP_KEY_KPPGDOWN},
+ {XK_KP_Left, MP_KEY_KPLEFT}, {XK_KP_Begin, MP_KEY_KP5},
+ {XK_KP_Right, MP_KEY_KPRIGHT}, {XK_KP_Home, MP_KEY_KPHOME}, {XK_KP_Up, MP_KEY_KPUP},
+ {XK_KP_Page_Up, MP_KEY_KPPGUP}, {XK_KP_Delete, MP_KEY_KPDEL},
+
+ {XF86XK_MenuKB, MP_KEY_MENU},
+ {XF86XK_AudioPlay, MP_KEY_PLAY}, {XF86XK_AudioPause, MP_KEY_PAUSE},
+ {XF86XK_AudioStop, MP_KEY_STOP},
+ {XF86XK_AudioPrev, MP_KEY_PREV}, {XF86XK_AudioNext, MP_KEY_NEXT},
+ {XF86XK_AudioRewind, MP_KEY_REWIND}, {XF86XK_AudioForward, MP_KEY_FORWARD},
+ {XF86XK_AudioMute, MP_KEY_MUTE},
+ {XF86XK_AudioLowerVolume, MP_KEY_VOLUME_DOWN},
+ {XF86XK_AudioRaiseVolume, MP_KEY_VOLUME_UP},
+ {XF86XK_HomePage, MP_KEY_HOMEPAGE}, {XF86XK_WWW, MP_KEY_WWW},
+ {XF86XK_Mail, MP_KEY_MAIL}, {XF86XK_Favorites, MP_KEY_FAVORITES},
+ {XF86XK_Search, MP_KEY_SEARCH}, {XF86XK_Sleep, MP_KEY_SLEEP},
+ {XF86XK_Back, MP_KEY_BACK}, {XF86XK_Tools, MP_KEY_TOOLS},
+ {XF86XK_ZoomIn, MP_KEY_ZOOMIN}, {XF86XK_ZoomOut, MP_KEY_ZOOMOUT},
+
+ {0, 0}
+};
+
+static int vo_x11_lookupkey(int key)
+{
+ const char *passthrough_keys = " -+*/<>`~!@#$%^&()_{}:;\"\',.?\\|=[]";
+ int mpkey = 0;
+ if ((key >= 'a' && key <= 'z') ||
+ (key >= 'A' && key <= 'Z') ||
+ (key >= '0' && key <= '9') ||
+ (key > 0 && key < 256 && strchr(passthrough_keys, key)))
+ mpkey = key;
+
+ if (!mpkey)
+ mpkey = lookup_keymap_table(keymap, key);
+
+ // XFree86 keysym range; typically contains obscure "extra" keys
+ if (!mpkey && key >= 0x10080001 && key <= 0x1008FFFF) {
+ mpkey = MP_KEY_UNKNOWN_RESERVED_START + (key - 0x10080000);
+ if (mpkey > MP_KEY_UNKNOWN_RESERVED_LAST)
+ mpkey = 0;
+ }
+
+ return mpkey;
+}
+
+static void vo_x11_decoration(struct vo *vo, bool d)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->parent || !x11->window)
+ return;
+
+ Atom motif_hints = XA(x11, _MOTIF_WM_HINTS);
+ MotifWmHints mhints = {0};
+ bool got = x11_get_property_copy(x11, x11->window, motif_hints,
+ motif_hints, 32, &mhints, sizeof(mhints));
+ // hints weren't set, and decorations requested -> assume WM displays them
+ if (!got && d)
+ return;
+ if (!got) {
+ mhints.flags = MWM_HINTS_FUNCTIONS;
+ mhints.functions = MWM_FUNC_MOVE | MWM_FUNC_CLOSE | MWM_FUNC_MINIMIZE |
+ MWM_FUNC_MAXIMIZE | MWM_FUNC_RESIZE;
+ }
+ mhints.flags |= MWM_HINTS_DECORATIONS;
+ mhints.decorations = d ? MWM_DECOR_ALL : 0;
+ XChangeProperty(x11->display, x11->window, motif_hints, motif_hints, 32,
+ PropModeReplace, (unsigned char *) &mhints, 5);
+}
+
+static void vo_x11_wm_hints(struct vo *vo, Window window)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ XWMHints hints = {0};
+ hints.flags = InputHint | StateHint;
+ hints.input = 1;
+ hints.initial_state = NormalState;
+ XSetWMHints(x11->display, window, &hints);
+}
+
+static void vo_x11_classhint(struct vo *vo, Window window, const char *name)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+ XClassHint wmClass;
+ long pid = getpid();
+
+ wmClass.res_name = opts->winname ? opts->winname : (char *)name;
+ wmClass.res_class = "mpv";
+ XSetClassHint(x11->display, window, &wmClass);
+ XChangeProperty(x11->display, window, XA(x11, _NET_WM_PID), XA_CARDINAL,
+ 32, PropModeReplace, (unsigned char *) &pid, 1);
+}
+
+void vo_x11_uninit(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (!x11)
+ return;
+
+ mp_input_put_key(x11->input_ctx, MP_INPUT_RELEASE_ALL);
+
+ set_screensaver(x11, true);
+
+ if (x11->window != None && x11->window != x11->rootwin)
+ XDestroyWindow(x11->display, x11->window);
+ if (x11->xic)
+ XDestroyIC(x11->xic);
+ if (x11->colormap != None)
+ XFreeColormap(vo->x11->display, x11->colormap);
+
+ MP_DBG(x11, "uninit ...\n");
+ if (x11->xim)
+ XCloseIM(x11->xim);
+ if (x11->display) {
+ XSetErrorHandler(NULL);
+ x11_error_output = NULL;
+ XCloseDisplay(x11->display);
+ }
+
+ if (x11->wakeup_pipe[0] >= 0) {
+ close(x11->wakeup_pipe[0]);
+ close(x11->wakeup_pipe[1]);
+ }
+
+ talloc_free(x11);
+ vo->x11 = NULL;
+}
+
+#define DND_PROPERTY "mpv_dnd_selection"
+
+static void vo_x11_dnd_init_window(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ Atom version = DND_VERSION;
+ XChangeProperty(x11->display, x11->window, XA(x11, XdndAware), XA_ATOM,
+ 32, PropModeReplace, (unsigned char *)&version, 1);
+}
+
+// The Atom does not always map to a mime type, but often.
+static char *x11_dnd_mime_type_buf(struct vo_x11_state *x11, Atom atom,
+ char *buf, size_t buf_size)
+{
+ if (atom == XInternAtom(x11->display, "UTF8_STRING", False))
+ return "text";
+ return x11_atom_name_buf(x11, atom, buf, buf_size);
+}
+
+#define x11_dnd_mime_type(x11, atom) \
+ x11_dnd_mime_type_buf(x11, atom, (char[80]){0}, 80)
+
+static bool dnd_format_is_better(struct vo_x11_state *x11, Atom cur, Atom new)
+{
+ int new_score = mp_event_get_mime_type_score(x11->input_ctx,
+ x11_dnd_mime_type(x11, new));
+ int cur_score = -1;
+ if (cur) {
+ cur_score = mp_event_get_mime_type_score(x11->input_ctx,
+ x11_dnd_mime_type(x11, cur));
+ }
+ return new_score >= 0 && new_score > cur_score;
+}
+
+static void dnd_select_format(struct vo_x11_state *x11, Atom *args, int items)
+{
+ x11->dnd_requested_format = 0;
+
+ for (int n = 0; n < items; n++) {
+ MP_VERBOSE(x11, "DnD type: '%s'\n", x11_atom_name(x11, args[n]));
+ // There are other types; possibly not worth supporting.
+ if (dnd_format_is_better(x11, x11->dnd_requested_format, args[n]))
+ x11->dnd_requested_format = args[n];
+ }
+
+ MP_VERBOSE(x11, "Selected DnD type: %s\n", x11->dnd_requested_format ?
+ x11_atom_name(x11, x11->dnd_requested_format) : "(none)");
+}
+
+static void dnd_reset(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ x11->dnd_src_window = 0;
+ x11->dnd_requested_format = 0;
+}
+
+static void vo_x11_dnd_handle_message(struct vo *vo, XClientMessageEvent *ce)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window)
+ return;
+
+ if (ce->message_type == XA(x11, XdndEnter)) {
+ x11->dnd_requested_format = 0;
+
+ Window src = ce->data.l[0];
+ if (ce->data.l[1] & 1) {
+ int nitems;
+ Atom *args = x11_get_property(x11, src, XA(x11, XdndTypeList),
+ XA_ATOM, 32, &nitems);
+ if (args) {
+ dnd_select_format(x11, args, nitems);
+ XFree(args);
+ }
+ } else {
+ Atom args[3];
+ for (int n = 2; n <= 4; n++)
+ args[n - 2] = ce->data.l[n];
+ dnd_select_format(x11, args, 3);
+ }
+ } else if (ce->message_type == XA(x11, XdndPosition)) {
+ x11->dnd_requested_action = ce->data.l[4];
+
+ Window src = ce->data.l[0];
+ XEvent xev;
+
+ xev.xclient.type = ClientMessage;
+ xev.xclient.serial = 0;
+ xev.xclient.send_event = True;
+ xev.xclient.message_type = XA(x11, XdndStatus);
+ xev.xclient.window = src;
+ xev.xclient.format = 32;
+ xev.xclient.data.l[0] = x11->window;
+ xev.xclient.data.l[1] = x11->dnd_requested_format ? 1 : 0;
+ xev.xclient.data.l[2] = 0;
+ xev.xclient.data.l[3] = 0;
+ xev.xclient.data.l[4] = XA(x11, XdndActionCopy);
+
+ XSendEvent(x11->display, src, False, 0, &xev);
+ } else if (ce->message_type == XA(x11, XdndDrop)) {
+ x11->dnd_src_window = ce->data.l[0];
+ XConvertSelection(x11->display, XA(x11, XdndSelection),
+ x11->dnd_requested_format, XAs(x11, DND_PROPERTY),
+ x11->window, ce->data.l[2]);
+ } else if (ce->message_type == XA(x11, XdndLeave)) {
+ dnd_reset(vo);
+ }
+}
+
+static void vo_x11_dnd_handle_selection(struct vo *vo, XSelectionEvent *se)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window || !x11->dnd_src_window)
+ return;
+
+ bool success = false;
+
+ if (se->selection == XA(x11, XdndSelection) &&
+ se->property == XAs(x11, DND_PROPERTY) &&
+ se->target == x11->dnd_requested_format &&
+ x11->opts->drag_and_drop != -2)
+ {
+ int nitems;
+ void *prop = x11_get_property(x11, x11->window, XAs(x11, DND_PROPERTY),
+ x11->dnd_requested_format, 8, &nitems);
+ if (prop) {
+ enum mp_dnd_action action;
+ if (x11->opts->drag_and_drop >= 0) {
+ action = x11->opts->drag_and_drop;
+ } else {
+ action = x11->dnd_requested_action == XA(x11, XdndActionCopy) ?
+ DND_REPLACE : DND_APPEND;
+ }
+
+ char *mime_type = x11_dnd_mime_type(x11, x11->dnd_requested_format);
+ MP_VERBOSE(x11, "Dropping type: %s (%s)\n",
+ x11_atom_name(x11, x11->dnd_requested_format), mime_type);
+
+ // No idea if this is guaranteed to be \0-padded, so use bstr.
+ success = mp_event_drop_mime_data(x11->input_ctx, mime_type,
+ (bstr){prop, nitems}, action) > 0;
+ XFree(prop);
+ }
+ }
+
+ XEvent xev;
+
+ xev.xclient.type = ClientMessage;
+ xev.xclient.serial = 0;
+ xev.xclient.send_event = True;
+ xev.xclient.message_type = XA(x11, XdndFinished);
+ xev.xclient.window = x11->dnd_src_window;
+ xev.xclient.format = 32;
+ xev.xclient.data.l[0] = x11->window;
+ xev.xclient.data.l[1] = success ? 1 : 0;
+ xev.xclient.data.l[2] = success ? XA(x11, XdndActionCopy) : 0;
+ xev.xclient.data.l[3] = 0;
+ xev.xclient.data.l[4] = 0;
+
+ XSendEvent(x11->display, x11->dnd_src_window, False, 0, &xev);
+
+ dnd_reset(vo);
+}
+
+static void update_vo_size(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (RC_W(x11->winrc) != vo->dwidth || RC_H(x11->winrc) != vo->dheight) {
+ vo->dwidth = RC_W(x11->winrc);
+ vo->dheight = RC_H(x11->winrc);
+ x11->pending_vo_events |= VO_EVENT_RESIZE;
+ }
+}
+
+static int get_mods(unsigned int state)
+{
+ int modifiers = 0;
+ if (state & ShiftMask)
+ modifiers |= MP_KEY_MODIFIER_SHIFT;
+ if (state & ControlMask)
+ modifiers |= MP_KEY_MODIFIER_CTRL;
+ if (state & Mod1Mask)
+ modifiers |= MP_KEY_MODIFIER_ALT;
+ if (state & Mod4Mask)
+ modifiers |= MP_KEY_MODIFIER_META;
+ return modifiers;
+}
+
+static void vo_x11_update_composition_hint(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ long hint = 0;
+ switch (x11->opts->x11_bypass_compositor) {
+ case 0: hint = 0; break; // leave default
+ case 1: hint = 1; break; // always bypass
+ case 2: hint = x11->fs ? 1 : 0; break; // bypass in FS
+ case 3: hint = 2; break; // always enable
+ }
+
+ XChangeProperty(x11->display, x11->window, XA(x11,_NET_WM_BYPASS_COMPOSITOR),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&hint, 1);
+}
+
+static void vo_x11_check_net_wm_state_change(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ if (x11->parent)
+ return;
+
+ if (x11->wm_type & vo_wm_FULLSCREEN) {
+ int num_elems;
+ long *elems = x11_get_property(x11, x11->window, XA(x11, _NET_WM_STATE),
+ XA_ATOM, 32, &num_elems);
+ int is_fullscreen = 0, is_minimized = 0, is_maximized = 0;
+ if (elems) {
+ Atom fullscreen_prop = XA(x11, _NET_WM_STATE_FULLSCREEN);
+ Atom hidden = XA(x11, _NET_WM_STATE_HIDDEN);
+ Atom max_vert = XA(x11, _NET_WM_STATE_MAXIMIZED_VERT);
+ Atom max_horiz = XA(x11, _NET_WM_STATE_MAXIMIZED_HORZ);
+ for (int n = 0; n < num_elems; n++) {
+ if (elems[n] == fullscreen_prop)
+ is_fullscreen = 1;
+ if (elems[n] == hidden)
+ is_minimized = 1;
+ if (elems[n] == max_vert || elems[n] == max_horiz)
+ is_maximized = 1;
+ }
+ XFree(elems);
+ }
+
+ if (opts->window_maximized && !is_maximized && x11->geometry_change) {
+ x11->geometry_change = false;
+ vo_x11_config_vo_window(vo);
+ }
+
+ opts->window_minimized = is_minimized;
+ x11->hidden = is_minimized;
+ m_config_cache_write_opt(x11->opts_cache, &opts->window_minimized);
+ opts->window_maximized = is_maximized;
+ m_config_cache_write_opt(x11->opts_cache, &opts->window_maximized);
+
+ if ((x11->opts->fullscreen && !is_fullscreen) ||
+ (!x11->opts->fullscreen && is_fullscreen))
+ {
+ x11->opts->fullscreen = is_fullscreen;
+ x11->fs = is_fullscreen;
+ m_config_cache_write_opt(x11->opts_cache, &x11->opts->fullscreen);
+
+ if (!is_fullscreen && (x11->pos_changed_during_fs ||
+ x11->size_changed_during_fs))
+ {
+ vo_x11_move_resize(vo, x11->pos_changed_during_fs,
+ x11->size_changed_during_fs,
+ x11->nofsrc);
+ }
+
+ x11->size_changed_during_fs = false;
+ x11->pos_changed_during_fs = false;
+
+ vo_x11_update_composition_hint(vo);
+ }
+ }
+}
+
+static void vo_x11_check_net_wm_desktop_change(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->parent)
+ return;
+
+ long params[1] = {0};
+ if (x11_get_property_copy(x11, x11->window, XA(x11, _NET_WM_DESKTOP),
+ XA_CARDINAL, 32, params, sizeof(params)))
+ {
+ x11->opts->all_workspaces = params[0] == -1; // (gets sign-extended?)
+ m_config_cache_write_opt(x11->opts_cache, &x11->opts->all_workspaces);
+ }
+}
+
+// Releasing all keys on key-up or defocus is simpler and ensures no keys can
+// get "stuck".
+static void release_all_keys(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->no_autorepeat)
+ mp_input_put_key(x11->input_ctx, MP_INPUT_RELEASE_ALL);
+ x11->win_drag_button1_down = false;
+}
+
+void vo_x11_check_events(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ Display *display = vo->x11->display;
+ XEvent Event;
+
+ xscreensaver_heartbeat(vo->x11);
+
+ while (XPending(display)) {
+ XNextEvent(display, &Event);
+ MP_TRACE(x11, "XEvent: %d\n", Event.type);
+ switch (Event.type) {
+ case Expose:
+ x11->pending_vo_events |= VO_EVENT_EXPOSE;
+ break;
+ case ConfigureNotify:
+ if (x11->window == None)
+ break;
+ vo_x11_update_geometry(vo);
+ if (x11->parent && Event.xconfigure.window == x11->parent) {
+ MP_TRACE(x11, "adjusting embedded window position\n");
+ XMoveResizeWindow(x11->display, x11->window,
+ 0, 0, RC_W(x11->winrc), RC_H(x11->winrc));
+ }
+ break;
+ case KeyPress: {
+ char buf[100];
+ KeySym keySym = 0;
+ int modifiers = get_mods(Event.xkey.state);
+ if (x11->no_autorepeat)
+ modifiers |= MP_KEY_STATE_DOWN;
+ if (x11->xic) {
+ Status status;
+ int len = Xutf8LookupString(x11->xic, &Event.xkey, buf,
+ sizeof(buf), &keySym, &status);
+ int mpkey = vo_x11_lookupkey(keySym);
+ if (mpkey) {
+ mp_input_put_key(x11->input_ctx, mpkey | modifiers);
+ } else if (status == XLookupChars || status == XLookupBoth) {
+ struct bstr t = { buf, len };
+ mp_input_put_key_utf8(x11->input_ctx, modifiers, t);
+ }
+ } else {
+ XLookupString(&Event.xkey, buf, sizeof(buf), &keySym,
+ &x11->compose_status);
+ int mpkey = vo_x11_lookupkey(keySym);
+ if (mpkey)
+ mp_input_put_key(x11->input_ctx, mpkey | modifiers);
+ }
+ break;
+ }
+ case FocusIn:
+ x11->has_focus = true;
+ vo_update_cursor(vo);
+ x11->pending_vo_events |= VO_EVENT_FOCUS;
+ break;
+ case FocusOut:
+ release_all_keys(vo);
+ x11->has_focus = false;
+ vo_update_cursor(vo);
+ x11->pending_vo_events |= VO_EVENT_FOCUS;
+ break;
+ case KeyRelease:
+ release_all_keys(vo);
+ break;
+ case MotionNotify:
+ if (x11->win_drag_button1_down && !x11->fs &&
+ !mp_input_test_dragging(x11->input_ctx, Event.xmotion.x,
+ Event.xmotion.y))
+ {
+ mp_input_put_key(x11->input_ctx, MP_INPUT_RELEASE_ALL);
+ XUngrabPointer(x11->display, CurrentTime);
+
+ long params[5] = {
+ Event.xmotion.x_root, Event.xmotion.y_root,
+ 8, // _NET_WM_MOVERESIZE_MOVE
+ 1, // button 1
+ 1, // source indication: normal
+ };
+ x11_send_ewmh_msg(x11, "_NET_WM_MOVERESIZE", params);
+ } else {
+ mp_input_set_mouse_pos(x11->input_ctx, Event.xmotion.x,
+ Event.xmotion.y);
+ }
+ x11->win_drag_button1_down = false;
+ break;
+ case LeaveNotify:
+ if (Event.xcrossing.mode != NotifyNormal)
+ break;
+ x11->win_drag_button1_down = false;
+ mp_input_put_key(x11->input_ctx, MP_KEY_MOUSE_LEAVE);
+ break;
+ case EnterNotify:
+ if (Event.xcrossing.mode != NotifyNormal)
+ break;
+ mp_input_put_key(x11->input_ctx, MP_KEY_MOUSE_ENTER);
+ break;
+ case ButtonPress:
+ if (Event.xbutton.button - 1 >= MP_KEY_MOUSE_BTN_COUNT)
+ break;
+ if (Event.xbutton.button == 1)
+ x11->win_drag_button1_down = true;
+ mp_input_put_key(x11->input_ctx,
+ (MP_MBTN_BASE + Event.xbutton.button - 1) |
+ get_mods(Event.xbutton.state) | MP_KEY_STATE_DOWN);
+ long msg[4] = {XEMBED_REQUEST_FOCUS};
+ vo_x11_xembed_send_message(x11, msg);
+ break;
+ case ButtonRelease:
+ if (Event.xbutton.button - 1 >= MP_KEY_MOUSE_BTN_COUNT)
+ break;
+ if (Event.xbutton.button == 1)
+ x11->win_drag_button1_down = false;
+ mp_input_put_key(x11->input_ctx,
+ (MP_MBTN_BASE + Event.xbutton.button - 1) |
+ get_mods(Event.xbutton.state) | MP_KEY_STATE_UP);
+ break;
+ case MapNotify:
+ x11->window_hidden = false;
+ x11->pseudo_mapped = true;
+ x11->current_screen = -1;
+ vo_x11_update_geometry(vo);
+ break;
+ case DestroyNotify:
+ MP_WARN(x11, "Our window was destroyed, exiting\n");
+ mp_input_put_key(x11->input_ctx, MP_KEY_CLOSE_WIN);
+ x11->window = 0;
+ break;
+ case ClientMessage:
+ if (Event.xclient.message_type == XA(x11, WM_PROTOCOLS) &&
+ Event.xclient.data.l[0] == XA(x11, WM_DELETE_WINDOW))
+ mp_input_put_key(x11->input_ctx, MP_KEY_CLOSE_WIN);
+ vo_x11_dnd_handle_message(vo, &Event.xclient);
+ vo_x11_xembed_handle_message(vo, &Event.xclient);
+ break;
+ case SelectionNotify:
+ vo_x11_dnd_handle_selection(vo, &Event.xselection);
+ break;
+ case PropertyNotify:
+ if (Event.xproperty.atom == XA(x11, _NET_FRAME_EXTENTS) ||
+ Event.xproperty.atom == XA(x11, WM_STATE))
+ {
+ if (!x11->pseudo_mapped && !x11->parent) {
+ MP_VERBOSE(x11, "not waiting for MapNotify\n");
+ x11->pseudo_mapped = true;
+ }
+ } else if (Event.xproperty.atom == XA(x11, _NET_WM_STATE)) {
+ vo_x11_check_net_wm_state_change(vo);
+ } else if (Event.xproperty.atom == XA(x11, _NET_WM_DESKTOP)) {
+ vo_x11_check_net_wm_desktop_change(vo);
+ } else if (Event.xproperty.atom == x11->icc_profile_property) {
+ x11->pending_vo_events |= VO_EVENT_ICC_PROFILE_CHANGED;
+ }
+ break;
+ case GenericEvent: {
+ XGenericEventCookie *cookie = (XGenericEventCookie *)&Event.xcookie;
+ if (cookie->extension == x11->present_code && x11->use_present)
+ {
+ XGetEventData(x11->display, cookie);
+ if (cookie->evtype == PresentCompleteNotify) {
+ XPresentCompleteNotifyEvent *present_event;
+ present_event = (XPresentCompleteNotifyEvent *)cookie->data;
+ present_sync_update_values(x11->present,
+ present_event->ust * 1000,
+ present_event->msc);
+ }
+ }
+ XFreeEventData(x11->display, cookie);
+ break;
+ }
+ default:
+ if (Event.type == x11->ShmCompletionEvent) {
+ if (x11->ShmCompletionWaitCount > 0)
+ x11->ShmCompletionWaitCount--;
+ }
+ if (Event.type == x11->xrandr_event) {
+ xrandr_read(x11);
+ vo_x11_update_geometry(vo);
+ }
+ break;
+ }
+ }
+
+ update_vo_size(vo);
+}
+
+static void vo_x11_sizehint(struct vo *vo, struct mp_rect rc, bool override_pos)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ if (!x11->window || x11->parent)
+ return;
+
+ bool screen = opts->screen_id >= 0 || (opts->screen_name &&
+ opts->screen_name[0]);
+ bool fsscreen = opts->fsscreen_id >= 0 || (opts->fsscreen_name &&
+ opts->fsscreen_name[0]);
+ bool force_pos = opts->geometry.xy_valid || // explicitly forced by user
+ opts->force_window_position || // resize -> reset position
+ screen || fsscreen || // force onto screen area
+ opts->screen_name || // also force onto screen area
+ x11->parent || // force to fill parent
+ override_pos; // for fullscreen and such
+
+ XSizeHints *hint = XAllocSizeHints();
+ if (!hint)
+ return; // OOM
+
+ hint->flags |= PSize | (force_pos ? PPosition : 0);
+ hint->x = rc.x0;
+ hint->y = rc.y0;
+ hint->width = RC_W(rc);
+ hint->height = RC_H(rc);
+ hint->max_width = 0;
+ hint->max_height = 0;
+
+ if (opts->keepaspect && opts->keepaspect_window) {
+ hint->flags |= PAspect;
+ hint->min_aspect.x = hint->width;
+ hint->min_aspect.y = hint->height;
+ hint->max_aspect.x = hint->width;
+ hint->max_aspect.y = hint->height;
+ }
+
+ // Set minimum height/width to 4 to avoid off-by-one errors.
+ hint->flags |= PMinSize;
+ hint->min_width = hint->min_height = 4;
+
+ hint->flags |= PWinGravity;
+ hint->win_gravity = StaticGravity;
+
+ XSetWMNormalHints(x11->display, x11->window, hint);
+ XFree(hint);
+}
+
+static void vo_x11_move_resize(struct vo *vo, bool move, bool resize,
+ struct mp_rect rc)
+{
+ if (!vo->x11->window)
+ return;
+ int w = RC_W(rc), h = RC_H(rc);
+ XWindowChanges req = {.x = rc.x0, .y = rc.y0, .width = w, .height = h};
+ unsigned mask = (move ? CWX | CWY : 0) | (resize ? CWWidth | CWHeight : 0);
+ if (mask)
+ XConfigureWindow(vo->x11->display, vo->x11->window, mask, &req);
+ vo_x11_sizehint(vo, rc, false);
+}
+
+// set a X text property that expects a UTF8_STRING type
+static void vo_x11_set_property_utf8(struct vo *vo, Atom name, const char *t)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ XChangeProperty(x11->display, x11->window, name, XA(x11, UTF8_STRING), 8,
+ PropModeReplace, t, strlen(t));
+}
+
+// set a X text property that expects a STRING or COMPOUND_TEXT type
+static void vo_x11_set_property_string(struct vo *vo, Atom name, const char *t)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ XTextProperty prop = {0};
+
+ if (Xutf8TextListToTextProperty(x11->display, (char **)&t, 1,
+ XStdICCTextStyle, &prop) == Success)
+ {
+ XSetTextProperty(x11->display, x11->window, &prop, name);
+ } else {
+ // Strictly speaking this violates the ICCCM, but there's no way we
+ // can do this correctly.
+ vo_x11_set_property_utf8(vo, name, t);
+ }
+ XFree(prop.value);
+}
+
+static void vo_x11_update_window_title(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window || !x11->window_title)
+ return;
+
+ vo_x11_set_property_string(vo, XA_WM_NAME, x11->window_title);
+ vo_x11_set_property_string(vo, XA_WM_ICON_NAME, x11->window_title);
+
+ /* _NET_WM_NAME and _NET_WM_ICON_NAME must be sanitized to UTF-8. */
+ void *tmp = talloc_new(NULL);
+ struct bstr b_title = bstr_sanitize_utf8_latin1(tmp, bstr0(x11->window_title));
+ vo_x11_set_property_utf8(vo, XA(x11, _NET_WM_NAME), bstrto0(tmp, b_title));
+ vo_x11_set_property_utf8(vo, XA(x11, _NET_WM_ICON_NAME), bstrto0(tmp, b_title));
+ talloc_free(tmp);
+}
+
+static void vo_x11_xembed_update(struct vo_x11_state *x11, int flags)
+{
+ if (!x11->window || !x11->parent)
+ return;
+
+ long xembed_info[] = {XEMBED_VERSION, flags};
+ Atom name = XA(x11, _XEMBED_INFO);
+ XChangeProperty(x11->display, x11->window, name, name, 32,
+ PropModeReplace, (char *)xembed_info, 2);
+}
+
+static void vo_x11_xembed_handle_message(struct vo *vo, XClientMessageEvent *ce)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (!x11->window || !x11->parent || ce->message_type != XA(x11, _XEMBED))
+ return;
+
+ long msg = ce->data.l[1];
+ if (msg == XEMBED_EMBEDDED_NOTIFY)
+ MP_VERBOSE(x11, "Parent windows supports XEmbed.\n");
+}
+
+static void vo_x11_xembed_send_message(struct vo_x11_state *x11, long m[4])
+{
+ if (!x11->window || !x11->parent)
+ return;
+ XEvent ev = {.xclient = {
+ .type = ClientMessage,
+ .window = x11->parent,
+ .message_type = XA(x11, _XEMBED),
+ .format = 32,
+ .data = {.l = { CurrentTime, m[0], m[1], m[2], m[3] }},
+ } };
+ XSendEvent(x11->display, x11->parent, False, NoEventMask, &ev);
+}
+
+static void vo_x11_set_wm_icon(struct vo_x11_state *x11)
+{
+ int icon_size = 0;
+ long *icon = talloc_array(NULL, long, 0);
+
+ for (int n = 0; x11_icons[n].start; n++) {
+ struct mp_image *img =
+ load_image_png_buf(x11_icons[n].start, x11_icons[n].len, IMGFMT_RGBA);
+ if (!img)
+ continue;
+ int new_size = 2 + img->w * img->h;
+ MP_RESIZE_ARRAY(NULL, icon, icon_size + new_size);
+ long *cur = icon + icon_size;
+ icon_size += new_size;
+ *cur++ = img->w;
+ *cur++ = img->h;
+ for (int y = 0; y < img->h; y++) {
+ uint8_t *s = (uint8_t *)img->planes[0] + img->stride[0] * y;
+ for (int x = 0; x < img->w; x++) {
+ *cur++ = s[x * 4 + 0] | (s[x * 4 + 1] << 8) |
+ (s[x * 4 + 2] << 16) | ((unsigned)s[x * 4 + 3] << 24);
+ }
+ }
+ talloc_free(img);
+ }
+
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_ICON),
+ XA_CARDINAL, 32, PropModeReplace,
+ (unsigned char *)icon, icon_size);
+ talloc_free(icon);
+}
+
+static void vo_x11_create_window(struct vo *vo, XVisualInfo *vis,
+ struct mp_rect rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ assert(x11->window == None);
+ assert(!x11->xic);
+
+ XVisualInfo vinfo_storage;
+ if (!vis) {
+ vis = &vinfo_storage;
+ XWindowAttributes att;
+ XGetWindowAttributes(x11->display, x11->rootwin, &att);
+ XMatchVisualInfo(x11->display, x11->screen, att.depth, TrueColor, vis);
+ }
+
+ if (x11->colormap == None) {
+ x11->colormap = XCreateColormap(x11->display, x11->rootwin,
+ vis->visual, AllocNone);
+ }
+
+ unsigned long xswamask = CWBorderPixel | CWColormap;
+ XSetWindowAttributes xswa = {
+ .border_pixel = 0,
+ .colormap = x11->colormap,
+ };
+
+ Window parent = x11->parent;
+ if (!parent)
+ parent = x11->rootwin;
+
+ x11->window =
+ XCreateWindow(x11->display, parent, rc.x0, rc.y0, RC_W(rc), RC_H(rc), 0,
+ vis->depth, CopyFromParent, vis->visual, xswamask, &xswa);
+ Atom protos[1] = {XA(x11, WM_DELETE_WINDOW)};
+ XSetWMProtocols(x11->display, x11->window, protos, 1);
+
+ if (!XPresentQueryExtension(x11->display, &x11->present_code, NULL, NULL)) {
+ MP_VERBOSE(x11, "The XPresent extension is not supported.\n");
+ } else {
+ MP_VERBOSE(x11, "The XPresent extension was found.\n");
+ XPresentSelectInput(x11->display, x11->window, PresentCompleteNotifyMask);
+ }
+ xpresent_set(x11);
+
+ x11->mouse_cursor_set = false;
+ x11->mouse_cursor_visible = true;
+ vo_update_cursor(vo);
+
+ if (x11->xim) {
+ x11->xic = XCreateIC(x11->xim,
+ XNInputStyle, XIMPreeditNone | XIMStatusNone,
+ XNClientWindow, x11->window,
+ XNFocusWindow, x11->window,
+ NULL);
+ }
+
+ if (!x11->parent) {
+ vo_x11_update_composition_hint(vo);
+ vo_x11_set_wm_icon(x11);
+ vo_x11_dnd_init_window(vo);
+ vo_x11_set_property_utf8(vo, XA(x11, _GTK_THEME_VARIANT), "dark");
+ }
+ if (!x11->parent || x11->opts->x11_wid_title)
+ vo_x11_update_window_title(vo);
+ vo_x11_xembed_update(x11, 0);
+}
+
+static void vo_x11_map_window(struct vo *vo, struct mp_rect rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ vo_x11_move_resize(vo, true, true, rc);
+ vo_x11_decoration(vo, x11->opts->border);
+
+ if (x11->opts->fullscreen && (x11->wm_type & vo_wm_FULLSCREEN)) {
+ Atom state = XA(x11, _NET_WM_STATE_FULLSCREEN);
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_STATE), XA_ATOM,
+ 32, PropModeAppend, (unsigned char *)&state, 1);
+ x11->fs = 1;
+ // The "saved" positions are bogus, so reset them when leaving FS again.
+ x11->size_changed_during_fs = true;
+ x11->pos_changed_during_fs = true;
+ }
+
+ if (x11->opts->fsscreen_id != -1) {
+ long params[5] = {0};
+ if (x11->opts->fsscreen_id >= 0) {
+ for (int n = 0; n < 4; n++)
+ params[n] = x11->opts->fsscreen_id;
+ } else {
+ vo_x11_get_bounding_monitors(x11, &params[0]);
+ }
+ params[4] = 1; // source indication: normal
+ x11_send_ewmh_msg(x11, "_NET_WM_FULLSCREEN_MONITORS", params);
+ }
+
+ if (x11->opts->all_workspaces) {
+ if (x11->wm_type & vo_wm_STICKY) {
+ Atom state = XA(x11, _NET_WM_STATE_STICKY);
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_STATE), XA_ATOM,
+ 32, PropModeReplace, (unsigned char *)&state, 1);
+ } else {
+ long v = 0xFFFFFFFF;
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_DESKTOP),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&v, 1);
+ }
+ } else if (x11->opts->geometry.ws > 0) {
+ long v = x11->opts->geometry.ws - 1;
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_DESKTOP),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&v, 1);
+ }
+
+ vo_x11_update_composition_hint(vo);
+
+ // map window
+ int events = StructureNotifyMask | ExposureMask | PropertyChangeMask |
+ LeaveWindowMask | EnterWindowMask | FocusChangeMask;
+ if (mp_input_mouse_enabled(x11->input_ctx))
+ events |= PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
+ if (mp_input_vo_keyboard_enabled(x11->input_ctx))
+ events |= KeyPressMask | KeyReleaseMask;
+ vo_x11_selectinput_witherr(vo, x11->display, x11->window, events);
+ XMapWindow(x11->display, x11->window);
+
+ if (x11->opts->cursor_passthrough)
+ vo_x11_set_input_region(vo, true);
+
+ if (x11->opts->window_maximized) // don't override WM default on "no"
+ vo_x11_maximize(vo);
+ if (x11->opts->window_minimized) // don't override WM default on "no"
+ vo_x11_minimize(vo);
+
+ if (x11->opts->fullscreen && (x11->wm_type & vo_wm_FULLSCREEN))
+ x11_set_ewmh_state(x11, "_NET_WM_STATE_FULLSCREEN", 1);
+
+ vo_x11_xembed_update(x11, XEMBED_MAPPED);
+}
+
+static void vo_x11_highlevel_resize(struct vo *vo, struct mp_rect rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ bool reset_pos = opts->force_window_position;
+ if (reset_pos) {
+ x11->nofsrc = rc;
+ } else {
+ x11->nofsrc.x1 = x11->nofsrc.x0 + RC_W(rc);
+ x11->nofsrc.y1 = x11->nofsrc.y0 + RC_H(rc);
+ }
+
+ if (opts->fullscreen) {
+ x11->size_changed_during_fs = true;
+ x11->pos_changed_during_fs = reset_pos;
+ vo_x11_sizehint(vo, rc, false);
+ } else {
+ vo_x11_move_resize(vo, reset_pos, true, rc);
+ }
+}
+
+static void wait_until_mapped(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (!x11->pseudo_mapped)
+ x11_send_ewmh_msg(x11, "_NET_REQUEST_FRAME_EXTENTS", (long[5]){0});
+ while (!x11->pseudo_mapped && x11->window) {
+ XWindowAttributes att;
+ XGetWindowAttributes(x11->display, x11->window, &att);
+ if (att.map_state != IsUnmapped) {
+ x11->pseudo_mapped = true;
+ break;
+ }
+ XEvent unused;
+ XPeekEvent(x11->display, &unused);
+ vo_x11_check_events(vo);
+ }
+}
+
+// Create the X11 window. There is only 1, and it must be created before
+// vo_x11_config_vo_window() is called. vis can be NULL for default.
+bool vo_x11_create_vo_window(struct vo *vo, XVisualInfo *vis,
+ const char *classname)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ assert(!x11->window);
+
+ if (x11->parent) {
+ if (x11->parent == x11->rootwin) {
+ x11->window = x11->rootwin;
+ x11->pseudo_mapped = true;
+ XSelectInput(x11->display, x11->window, StructureNotifyMask);
+ } else {
+ XSelectInput(x11->display, x11->parent, StructureNotifyMask);
+ }
+ }
+ if (x11->window == None) {
+ vo_x11_create_window(vo, vis, (struct mp_rect){.x1 = 320, .y1 = 200 });
+ vo_x11_classhint(vo, x11->window, classname);
+ vo_x11_wm_hints(vo, x11->window);
+ x11->window_hidden = true;
+ }
+
+ return !!x11->window;
+}
+
+// Resize the window (e.g. new file, or video resolution change)
+void vo_x11_config_vo_window(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ assert(x11->window);
+
+ // Don't attempt to change autofit/geometry on maximized windows.
+ if (x11->geometry_change && opts->window_maximized)
+ return;
+
+ vo_x11_update_screeninfo(vo);
+
+ struct vo_win_geometry geo;
+ vo_calc_window_geometry2(vo, &x11->screenrc, x11->dpi_scale, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ struct mp_rect rc = geo.win;
+
+ if (x11->parent) {
+ vo_x11_update_geometry(vo);
+ rc = (struct mp_rect){0, 0, RC_W(x11->winrc), RC_H(x11->winrc)};
+ }
+
+ bool reset_size = (x11->old_dw != RC_W(rc) || x11->old_dh != RC_H(rc)) &&
+ (opts->auto_window_resize || x11->geometry_change);
+
+ x11->old_dw = RC_W(rc);
+ x11->old_dh = RC_H(rc);
+
+ if (x11->window_hidden) {
+ x11->nofsrc = rc;
+ vo_x11_map_window(vo, rc);
+ } else if (reset_size) {
+ vo_x11_highlevel_resize(vo, rc);
+ }
+
+ x11->geometry_change = false;
+
+ if (opts->ontop)
+ vo_x11_setlayer(vo, opts->ontop);
+
+ vo_x11_fullscreen(vo);
+
+ wait_until_mapped(vo);
+ vo_x11_update_geometry(vo);
+ update_vo_size(vo);
+ x11->pending_vo_events &= ~VO_EVENT_RESIZE; // implicitly done by the VO
+}
+
+static void vo_x11_sticky(struct vo *vo, bool sticky)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->wm_type & vo_wm_STICKY) {
+ x11_set_ewmh_state(x11, "_NET_WM_STATE_STICKY", sticky);
+ } else {
+ long params[5] = {0xFFFFFFFF, 1};
+ if (!sticky) {
+ x11_get_property_copy(x11, x11->rootwin,
+ XA(x11, _NET_CURRENT_DESKTOP),
+ XA_CARDINAL, 32, &params[0],
+ sizeof(params[0]));
+ }
+ x11_send_ewmh_msg(x11, "_NET_WM_DESKTOP", params);
+ }
+}
+
+static void vo_x11_setlayer(struct vo *vo, bool ontop)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->parent || !x11->window)
+ return;
+
+ if (x11->wm_type & (vo_wm_STAYS_ON_TOP | vo_wm_ABOVE)) {
+ char *state = "_NET_WM_STATE_ABOVE";
+
+ // Not in EWMH - but the old code preferred this (maybe it is "better")
+ if (x11->wm_type & vo_wm_STAYS_ON_TOP)
+ state = "_NET_WM_STATE_STAYS_ON_TOP";
+
+ x11_set_ewmh_state(x11, state, ontop);
+
+ MP_VERBOSE(x11, "NET style stay on top (%d). Using state %s.\n",
+ ontop, state);
+ } else if (x11->wm_type & vo_wm_LAYER) {
+ if (!x11->orig_layer) {
+ x11->orig_layer = WIN_LAYER_NORMAL;
+ x11_get_property_copy(x11, x11->window, XA(x11, _WIN_LAYER),
+ XA_CARDINAL, 32, &x11->orig_layer, sizeof(long));
+ MP_VERBOSE(x11, "original window layer is %ld.\n", x11->orig_layer);
+ }
+
+ long params[5] = {0};
+ // if not fullscreen, stay on default layer
+ params[0] = ontop ? WIN_LAYER_ABOVE_DOCK : x11->orig_layer;
+ params[1] = CurrentTime;
+ MP_VERBOSE(x11, "Layered style stay on top (layer %ld).\n", params[0]);
+ x11_send_ewmh_msg(x11, "_WIN_LAYER", params);
+ }
+}
+
+static bool rc_overlaps(struct mp_rect rc1, struct mp_rect rc2)
+{
+ return mp_rect_intersection(&rc1, &rc2); // changes the first argument
+}
+
+// update x11->winrc with current boundaries of vo->x11->window
+static void vo_x11_update_geometry(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ int x = 0, y = 0;
+ unsigned w, h, dummy_uint;
+ int dummy_int;
+ Window dummy_win;
+ Window win = x11->parent ? x11->parent : x11->window;
+ x11->winrc = (struct mp_rect){0, 0, 0, 0};
+ if (win) {
+ XGetGeometry(x11->display, win, &dummy_win, &dummy_int, &dummy_int,
+ &w, &h, &dummy_int, &dummy_uint);
+ if (w > INT_MAX || h > INT_MAX)
+ w = h = 0;
+ XTranslateCoordinates(x11->display, win, x11->rootwin, 0, 0,
+ &x, &y, &dummy_win);
+ x11->winrc = (struct mp_rect){x, y, x + w, y + h};
+ }
+ struct xrandr_display *disp = get_current_display(vo);
+ // Try to fallback to something reasonable if we have no disp yet
+ if (!disp) {
+ int screen = vo_x11_select_screen(vo);
+ if (screen > -1) {
+ disp = &x11->displays[screen];
+ } else if (x11->current_screen > - 1) {
+ disp = &x11->displays[x11->current_screen];
+ }
+ }
+ double fps = disp ? disp->fps : 0;
+ if (fps != x11->current_display_fps)
+ MP_VERBOSE(x11, "Current display FPS: %f\n", fps);
+ x11->current_display_fps = fps;
+ if (disp && x11->current_screen != disp->screen) {
+ x11->current_screen = disp->screen;
+ x11->pending_vo_events |= VO_EVENT_ICC_PROFILE_CHANGED;
+ }
+ x11->pending_vo_events |= VO_EVENT_WIN_STATE;
+}
+
+static void vo_x11_fullscreen(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ if (opts->fullscreen == x11->fs)
+ return;
+ x11->fs = opts->fullscreen; // x11->fs now contains the new state
+ if (x11->parent || !x11->window)
+ return;
+
+ // Save old state before entering fullscreen
+ if (x11->fs) {
+ vo_x11_update_geometry(vo);
+ x11->nofsrc = x11->winrc;
+ }
+
+ struct mp_rect rc = x11->nofsrc;
+
+ if (x11->wm_type & vo_wm_FULLSCREEN) {
+ x11_set_ewmh_state(x11, "_NET_WM_STATE_FULLSCREEN", x11->fs);
+ if (!x11->fs && (x11->pos_changed_during_fs ||
+ x11->size_changed_during_fs))
+ {
+ if (x11->screenrc.x0 == rc.x0 && x11->screenrc.x1 == rc.x1 &&
+ x11->screenrc.y0 == rc.y0 && x11->screenrc.y1 == rc.y1)
+ {
+ // Workaround for some WMs switching back to FS in this case.
+ MP_VERBOSE(x11, "avoiding triggering old-style fullscreen\n");
+ rc.x1 -= 1;
+ rc.y1 -= 1;
+ }
+ vo_x11_move_resize(vo, x11->pos_changed_during_fs,
+ x11->size_changed_during_fs, rc);
+ }
+ } else {
+ if (x11->fs) {
+ vo_x11_update_screeninfo(vo);
+ rc = x11->screenrc;
+ }
+
+ vo_x11_decoration(vo, opts->border && !x11->fs);
+ vo_x11_sizehint(vo, rc, true);
+
+ XMoveResizeWindow(x11->display, x11->window, rc.x0, rc.y0,
+ RC_W(rc), RC_H(rc));
+
+ vo_x11_setlayer(vo, x11->fs || opts->ontop);
+
+ XRaiseWindow(x11->display, x11->window);
+ XFlush(x11->display);
+ }
+
+ x11->size_changed_during_fs = false;
+ x11->pos_changed_during_fs = false;
+
+ vo_x11_update_composition_hint(vo);
+}
+
+static void vo_x11_maximize(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ long params[5] = {
+ x11->opts->window_maximized ? NET_WM_STATE_ADD : NET_WM_STATE_REMOVE,
+ XA(x11, _NET_WM_STATE_MAXIMIZED_VERT),
+ XA(x11, _NET_WM_STATE_MAXIMIZED_HORZ),
+ 1, // source indication: normal
+ };
+ x11_send_ewmh_msg(x11, "_NET_WM_STATE", params);
+}
+
+static void vo_x11_minimize(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->opts->window_minimized) {
+ XIconifyWindow(x11->display, x11->window, x11->screen);
+ } else {
+ long params[5] = {0};
+ x11_send_ewmh_msg(x11, "_NET_ACTIVE_WINDOW", params);
+ }
+}
+
+static void vo_x11_set_geometry(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window)
+ return;
+
+ x11->geometry_change = true;
+ vo_x11_config_vo_window(vo);
+}
+
+bool vo_x11_check_visible(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ bool render = !x11->hidden || opts->force_render ||
+ VS_IS_DISP(opts->video_sync);
+ return render;
+}
+
+static void vo_x11_set_input_region(struct vo *vo, bool passthrough)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (passthrough) {
+ XRectangle rect = {0, 0, 0, 0};
+ Region region = XCreateRegion();
+ XUnionRectWithRegion(&rect, region, region);
+ XShapeCombineRegion(x11->display, x11->window, ShapeInput, 0, 0,
+ region, ShapeSet);
+ XDestroyRegion(region);
+ } else {
+ XShapeCombineMask(x11->display, x11->window, ShapeInput, 0, 0,
+ 0, ShapeSet);
+ }
+}
+
+int vo_x11_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+ switch (request) {
+ case VOCTRL_CHECK_EVENTS:
+ vo_x11_check_events(vo);
+ *events |= x11->pending_vo_events;
+ x11->pending_vo_events = 0;
+ return VO_TRUE;
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(x11->opts_cache, &opt)) {
+ if (opt == &opts->fullscreen)
+ vo_x11_fullscreen(vo);
+ if (opt == &opts->ontop)
+ vo_x11_setlayer(vo, opts->ontop);
+ if (opt == &opts->border)
+ vo_x11_decoration(vo, opts->border);
+ if (opt == &opts->all_workspaces)
+ vo_x11_sticky(vo, opts->all_workspaces);
+ if (opt == &opts->window_minimized)
+ vo_x11_minimize(vo);
+ if (opt == &opts->window_maximized)
+ vo_x11_maximize(vo);
+ if (opt == &opts->cursor_passthrough)
+ vo_x11_set_input_region(vo, opts->cursor_passthrough);
+ if (opt == &opts->x11_present)
+ xpresent_set(x11);
+ if (opt == &opts->geometry || opt == &opts->autofit ||
+ opt == &opts->autofit_smaller || opt == &opts->autofit_larger)
+ {
+ vo_x11_set_geometry(vo);
+ }
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ if (!x11->window || x11->parent)
+ return VO_FALSE;
+ s[0] = (x11->fs ? RC_W(x11->nofsrc) : RC_W(x11->winrc)) / x11->dpi_scale;
+ s[1] = (x11->fs ? RC_H(x11->nofsrc) : RC_H(x11->winrc)) / x11->dpi_scale;
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ if (!x11->window || x11->parent)
+ return VO_FALSE;
+ int w = s[0] * x11->dpi_scale;
+ int h = s[1] * x11->dpi_scale;
+ struct mp_rect rc = x11->winrc;
+ rc.x1 = rc.x0 + w;
+ rc.y1 = rc.y0 + h;
+ if (x11->opts->window_maximized) {
+ x11->opts->window_maximized = false;
+ m_config_cache_write_opt(x11->opts_cache,
+ &x11->opts->window_maximized);
+ vo_x11_maximize(vo);
+ }
+ vo_x11_highlevel_resize(vo, rc);
+ if (!x11->fs) { // guess new window size, instead of waiting for X
+ x11->winrc.x1 = x11->winrc.x0 + w;
+ x11->winrc.y1 = x11->winrc.y0 + h;
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_FOCUSED: {
+ *(bool *)arg = x11->has_focus;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_NAMES: {
+ if (!x11->pseudo_mapped)
+ return VO_FALSE;
+ char **names = NULL;
+ int displays_spanned = 0;
+ for (int n = 0; n < x11->num_displays; n++) {
+ if (rc_overlaps(x11->displays[n].rc, x11->winrc))
+ MP_TARRAY_APPEND(NULL, names, displays_spanned,
+ talloc_strdup(NULL, x11->displays[n].name));
+ }
+ MP_TARRAY_APPEND(NULL, names, displays_spanned, NULL);
+ *(char ***)arg = names;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_ICC_PROFILE: {
+ if (!x11->pseudo_mapped)
+ return VO_NOTAVAIL;
+ int atom_id = x11->displays[x11->current_screen].atom_id;
+ char prop[80];
+ snprintf(prop, sizeof(prop), "_ICC_PROFILE");
+ if (atom_id > 0)
+ mp_snprintf_cat(prop, sizeof(prop), "_%d", atom_id);
+ x11->icc_profile_property = XAs(x11, prop);
+ int len;
+ MP_VERBOSE(x11, "Retrieving ICC profile for display: %d\n", x11->current_screen);
+ void *icc = x11_get_property(x11, x11->rootwin, x11->icc_profile_property,
+ XA_CARDINAL, 8, &len);
+ if (!icc)
+ return VO_FALSE;
+ *(bstr *)arg = bstrdup(NULL, (bstr){icc, len});
+ XFree(icc);
+ // Watch x11->icc_profile_property
+ XSelectInput(x11->display, x11->rootwin, PropertyChangeMask);
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ x11->mouse_cursor_visible = *(bool *)arg;
+ vo_update_cursor(vo);
+ return VO_TRUE;
+ case VOCTRL_KILL_SCREENSAVER:
+ set_screensaver(x11, false);
+ return VO_TRUE;
+ case VOCTRL_RESTORE_SCREENSAVER:
+ set_screensaver(x11, true);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ talloc_free(x11->window_title);
+ x11->window_title = talloc_strdup(x11, (char *)arg);
+ if (!x11->parent || x11->opts->x11_wid_title)
+ vo_x11_update_window_title(vo);
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_FPS: {
+ double fps = x11->current_display_fps;
+ if (fps <= 0)
+ break;
+ *(double *)arg = fps;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_RES: {
+ struct xrandr_display *disp = NULL;
+ if (x11->current_screen > -1)
+ disp = &x11->displays[x11->current_screen];
+ if (!x11->window || x11->parent || !disp)
+ return VO_NOTAVAIL;
+ ((int *)arg)[0] = mp_rect_w(disp->rc);
+ ((int *)arg)[1] = mp_rect_h(disp->rc);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_WINDOW_ID: {
+ if (!x11->window)
+ return VO_NOTAVAIL;
+ *(int64_t *)arg = x11->window;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_HIDPI_SCALE:
+ *(double *)arg = x11->dpi_scale;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+void vo_x11_present(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ XPresentNotifyMSC(x11->display, x11->window,
+ 0, 0, 1, 0);
+}
+
+void vo_x11_wakeup(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ (void)write(x11->wakeup_pipe[1], &(char){0}, 1);
+}
+
+void vo_x11_wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ struct pollfd fds[2] = {
+ { .fd = x11->event_fd, .events = POLLIN },
+ { .fd = x11->wakeup_pipe[0], .events = POLLIN },
+ };
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10));
+
+ mp_poll(fds, 2, timeout_ns);
+
+ if (fds[1].revents & POLLIN)
+ mp_flush_wakeup_pipe(x11->wakeup_pipe[0]);
+}
+
+static void xscreensaver_heartbeat(struct vo_x11_state *x11)
+{
+ double time = mp_time_sec();
+
+ if (x11->display && !x11->screensaver_enabled &&
+ (time - x11->screensaver_time_last) >= 10)
+ {
+ x11->screensaver_time_last = time;
+ XResetScreenSaver(x11->display);
+ }
+}
+
+static int xss_suspend(Display *mDisplay, Bool suspend)
+{
+ int event, error, major, minor;
+ if (XScreenSaverQueryExtension(mDisplay, &event, &error) != True ||
+ XScreenSaverQueryVersion(mDisplay, &major, &minor) != True)
+ return 0;
+ if (major < 1 || (major == 1 && minor < 1))
+ return 0;
+ XScreenSaverSuspend(mDisplay, suspend);
+ return 1;
+}
+
+static void set_screensaver(struct vo_x11_state *x11, bool enabled)
+{
+ Display *mDisplay = x11->display;
+ if (!mDisplay || x11->screensaver_enabled == enabled)
+ return;
+ MP_VERBOSE(x11, "%s screensaver.\n", enabled ? "Enabling" : "Disabling");
+ x11->screensaver_enabled = enabled;
+ if (xss_suspend(mDisplay, !enabled))
+ return;
+ int nothing;
+ if (DPMSQueryExtension(mDisplay, &nothing, &nothing)) {
+ BOOL onoff = 0;
+ CARD16 state;
+ DPMSInfo(mDisplay, &state, &onoff);
+ if (!x11->dpms_touched && enabled)
+ return; // enable DPMS only we we disabled it before
+ if (enabled != !!onoff) {
+ MP_VERBOSE(x11, "Setting DMPS: %s.\n", enabled ? "on" : "off");
+ if (enabled) {
+ DPMSEnable(mDisplay);
+ } else {
+ DPMSDisable(mDisplay);
+ x11->dpms_touched = true;
+ }
+ DPMSInfo(mDisplay, &state, &onoff);
+ if (enabled != !!onoff)
+ MP_WARN(x11, "DPMS state could not be set.\n");
+ }
+ }
+}
+
+static void vo_x11_selectinput_witherr(struct vo *vo,
+ Display *display,
+ Window w,
+ long event_mask)
+{
+ XSelectInput(display, w, NoEventMask);
+
+ // NOTE: this can raise BadAccess, which should be ignored by the X error
+ // handler; also see below
+ XSelectInput(display, w, event_mask);
+
+ // Test whether setting the event mask failed (with a BadAccess X error,
+ // although we don't know whether this really happened).
+ // This is needed for obscure situations like using --rootwin with a window
+ // manager active.
+ XWindowAttributes a;
+ if (XGetWindowAttributes(display, w, &a)) {
+ long bad = ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
+ if ((event_mask & bad) && (a.all_event_masks & bad) &&
+ ((a.your_event_mask & bad) != (event_mask & bad)))
+ {
+ MP_ERR(vo->x11, "X11 error: error during XSelectInput "
+ "call, trying without mouse events\n");
+ XSelectInput(display, w, event_mask & ~bad);
+ }
+ }
+}
+
+bool vo_x11_screen_is_composited(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ char buf[50];
+ snprintf(buf, sizeof(buf), "_NET_WM_CM_S%d", x11->screen);
+ Atom NET_WM_CM = XInternAtom(x11->display, buf, False);
+ return XGetSelectionOwner(x11->display, NET_WM_CM) != None;
+}
+
+// Return whether the given visual has alpha (when compositing is used).
+bool vo_x11_is_rgba_visual(XVisualInfo *v)
+{
+ // This is a heuristic at best. Note that normal 8 bit Visuals use
+ // a depth of 24, even if the pixels are padded to 32 bit. If the
+ // depth is higher than 24, the remaining bits must be alpha.
+ // Note: vinfo->bits_per_rgb appears to be useless (is always 8).
+ unsigned long mask = v->depth == sizeof(unsigned long) * 8 ?
+ (unsigned long)-1 : (1UL << v->depth) - 1;
+ return mask & ~(v->red_mask | v->green_mask | v->blue_mask);
+}
diff --git a/video/out/x11_common.h b/video/out/x11_common.h
new file mode 100644
index 0000000..62a96d7
--- /dev/null
+++ b/video/out/x11_common.h
@@ -0,0 +1,164 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_X11_COMMON_H
+#define MPLAYER_X11_COMMON_H
+
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include "common/common.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+struct vo;
+struct mp_log;
+
+#define MAX_DISPLAYS 32 // ought to be enough for everyone
+
+struct xrandr_display {
+ struct mp_rect rc;
+ double fps;
+ char *name;
+ bool overlaps;
+ int atom_id; // offset by location of primary
+ int screen;
+};
+
+struct vo_x11_state {
+ struct mp_log *log;
+ struct input_ctx *input_ctx;
+ struct m_config_cache *opts_cache;
+ struct mp_vo_opts *opts;
+ Display *display;
+ int event_fd;
+ int wakeup_pipe[2];
+ Window window;
+ Window rootwin;
+ Window parent; // embedded in this foreign window
+ int screen;
+ int display_is_local;
+ int ws_width;
+ int ws_height;
+ int dpi_scale;
+ struct mp_rect screenrc;
+ char *window_title;
+
+ struct xrandr_display displays[MAX_DISPLAYS];
+ int num_displays;
+ int current_screen;
+
+ int xrandr_event;
+ bool has_mesa;
+ bool has_nvidia;
+
+ bool screensaver_enabled;
+ bool dpms_touched;
+ double screensaver_time_last;
+
+ struct mp_present *present;
+ bool use_present;
+ int present_code;
+
+ XIM xim;
+ XIC xic;
+ bool no_autorepeat;
+
+ Colormap colormap;
+
+ int wm_type;
+ bool hidden; // _NET_WM_STATE_HIDDEN
+ bool window_hidden; // the window was mapped at least once
+ bool pseudo_mapped; // not necessarily mapped, but known window size
+ int fs; // whether we assume the window is in fullscreen mode
+
+ bool mouse_cursor_visible; // whether we want the cursor to be visible (only
+ // takes effect when the window is focused)
+ bool mouse_cursor_set; // whether the cursor is *currently* *hidden*
+ bool has_focus;
+ long orig_layer;
+
+ // Current actual window position (updated on window move/resize events).
+ struct mp_rect winrc;
+ double current_display_fps;
+
+ int pending_vo_events;
+
+ // last non-fullscreen extends (updated on fullscreen or reinitialization)
+ struct mp_rect nofsrc;
+
+ /* Keep track of original video width/height to determine when to
+ * resize window when reconfiguring. Resize window when video size
+ * changes, but don't force window size changes as long as video size
+ * stays the same (even if that size is different from the current
+ * window size after the user modified the latter). */
+ int old_dw, old_dh;
+ /* Video size changed during fullscreen when we couldn't tell the new
+ * size to the window manager. Must set window size when turning
+ * fullscreen off. */
+ bool size_changed_during_fs;
+ bool pos_changed_during_fs;
+
+ /* One of the autofit/geometry options changed at runtime. */
+ bool geometry_change;
+
+ XComposeStatus compose_status;
+
+ /* XShm stuff */
+ int ShmCompletionEvent;
+ /* Number of outstanding XShmPutImage requests */
+ /* Decremented when ShmCompletionEvent is received */
+ /* Increment it before XShmPutImage */
+ int ShmCompletionWaitCount;
+
+ /* drag and drop */
+ Atom dnd_requested_format;
+ Atom dnd_requested_action;
+ Window dnd_src_window;
+
+ /* dragging the window */
+ bool win_drag_button1_down;
+
+ Atom icc_profile_property;
+};
+
+bool vo_x11_init(struct vo *vo);
+void vo_x11_uninit(struct vo *vo);
+void vo_x11_check_events(struct vo *vo);
+bool vo_x11_screen_is_composited(struct vo *vo);
+bool vo_x11_create_vo_window(struct vo *vo, XVisualInfo *vis,
+ const char *classname);
+void vo_x11_config_vo_window(struct vo *vo);
+bool vo_x11_check_visible(struct vo *vo);
+int vo_x11_control(struct vo *vo, int *events, int request, void *arg);
+void vo_x11_present(struct vo *vo);
+void vo_x11_sync_swap(struct vo *vo);
+void vo_x11_wakeup(struct vo *vo);
+void vo_x11_wait_events(struct vo *vo, int64_t until_time_ns);
+
+void vo_x11_silence_xlib(int dir);
+
+bool vo_x11_is_rgba_visual(XVisualInfo *v);
+
+#endif /* MPLAYER_X11_COMMON_H */
diff --git a/video/repack.c b/video/repack.c
new file mode 100644
index 0000000..ce3703a
--- /dev/null
+++ b/video/repack.c
@@ -0,0 +1,1203 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include <libavutil/bswap.h>
+#include <libavutil/pixfmt.h>
+
+#include "common/common.h"
+#include "repack.h"
+#include "video/csputils.h"
+#include "video/fmt-conversion.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+
+enum repack_step_type {
+ REPACK_STEP_FLOAT,
+ REPACK_STEP_REPACK,
+ REPACK_STEP_ENDIAN,
+};
+
+struct repack_step {
+ enum repack_step_type type;
+ // 0=input, 1=output
+ struct mp_image *buf[2];
+ bool user_buf[2]; // user_buf[n]==true if buf[n] = user src/dst buffer
+ struct mp_imgfmt_desc fmt[2];
+ struct mp_image *tmp; // output buffer, if needed
+};
+
+struct mp_repack {
+ bool pack; // if false, this is for unpacking
+ int flags;
+ int imgfmt_user; // original mp format (unchanged endian)
+ int imgfmt_a; // original mp format (possibly packed format,
+ // swapped endian)
+ int imgfmt_b; // equivalent unpacked/planar format
+ struct mp_imgfmt_desc fmt_a;// ==imgfmt_a
+ struct mp_imgfmt_desc fmt_b;// ==imgfmt_b
+
+ void (*repack)(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w);
+
+ bool passthrough_y; // possible luma plane optimization for e.g. nv12
+ int endian_size; // endian swap; 0=none, 2/4=swap word size
+
+ // For packed_repack.
+ int components[4]; // b[n] = mp_image.planes[components[n]]
+ // pack: a is dst, b is src
+ // unpack: a is src, b is dst
+ void (*packed_repack_scanline)(void *a, void *b[], int w);
+
+ // Fringe RGB/YUV.
+ uint8_t comp_size;
+ uint8_t comp_map[6];
+ uint8_t comp_shifts[3];
+ uint8_t *comp_lut;
+ void (*repack_fringe_yuv)(void *dst, void *src[], int w, uint8_t *c);
+
+ // F32 repacking.
+ int f32_comp_size;
+ float f32_m[4], f32_o[4];
+ uint32_t f32_pmax[4];
+ enum mp_csp f32_csp_space;
+ enum mp_csp_levels f32_csp_levels;
+
+ // REPACK_STEP_REPACK: if true, need to copy this plane
+ bool copy_buf[4];
+
+ struct repack_step steps[4];
+ int num_steps;
+
+ bool configured;
+};
+
+// depth = number of LSB in use
+static int find_gbrp_format(int depth, int num_planes)
+{
+ if (num_planes != 3 && num_planes != 4)
+ return 0;
+ struct mp_regular_imgfmt desc = {
+ .component_type = MP_COMPONENT_TYPE_UINT,
+ .forced_csp = MP_CSP_RGB,
+ .component_size = depth > 8 ? 2 : 1,
+ .component_pad = depth - (depth > 8 ? 16 : 8),
+ .num_planes = num_planes,
+ .planes = { {1, {2}}, {1, {3}}, {1, {1}}, {1, {4}} },
+ };
+ return mp_find_regular_imgfmt(&desc);
+}
+
+// depth = number of LSB in use
+static int find_yuv_format(int depth, int num_planes)
+{
+ if (num_planes < 1 || num_planes > 4)
+ return 0;
+ struct mp_regular_imgfmt desc = {
+ .component_type = MP_COMPONENT_TYPE_UINT,
+ .component_size = depth > 8 ? 2 : 1,
+ .component_pad = depth - (depth > 8 ? 16 : 8),
+ .num_planes = num_planes,
+ .planes = { {1, {1}}, {1, {2}}, {1, {3}}, {1, {4}} },
+ };
+ if (num_planes == 2)
+ desc.planes[1].components[0] = 4;
+ return mp_find_regular_imgfmt(&desc);
+}
+
+// Copy one line on the plane p.
+static void copy_plane(struct mp_image *dst, int dst_x, int dst_y,
+ struct mp_image *src, int src_x, int src_y,
+ int w, int p)
+{
+ // Number of lines on this plane.
+ int h = (1 << dst->fmt.chroma_ys) - (1 << dst->fmt.ys[p]) + 1;
+ size_t size = mp_image_plane_bytes(dst, p, dst_x, w);
+
+ assert(dst->fmt.bpp[p] == src->fmt.bpp[p]);
+
+ for (int y = 0; y < h; y++) {
+ void *pd = mp_image_pixel_ptr_ny(dst, p, dst_x, dst_y + y);
+ void *ps = mp_image_pixel_ptr_ny(src, p, src_x, src_y + y);
+ memcpy(pd, ps, size);
+ }
+}
+
+// Swap endian for one line.
+static void swap_endian(struct mp_image *dst, int dst_x, int dst_y,
+ struct mp_image *src, int src_x, int src_y,
+ int w, int endian_size)
+{
+ assert(src->fmt.num_planes == dst->fmt.num_planes);
+
+ for (int p = 0; p < dst->fmt.num_planes; p++) {
+ int xs = dst->fmt.xs[p];
+ int bpp = dst->fmt.bpp[p] / 8;
+ int words_per_pixel = bpp / endian_size;
+ int num_words = ((w + (1 << xs) - 1) >> xs) * words_per_pixel;
+ // Number of lines on this plane.
+ int h = (1 << dst->fmt.chroma_ys) - (1 << dst->fmt.ys[p]) + 1;
+
+ assert(src->fmt.bpp[p] == bpp * 8);
+
+ for (int y = 0; y < h; y++) {
+ void *s = mp_image_pixel_ptr_ny(src, p, src_x, src_y + y);
+ void *d = mp_image_pixel_ptr_ny(dst, p, dst_x, dst_y + y);
+ switch (endian_size) {
+ case 2:
+ for (int x = 0; x < num_words; x++)
+ ((uint16_t *)d)[x] = av_bswap16(((uint16_t *)s)[x]);
+ break;
+ case 4:
+ for (int x = 0; x < num_words; x++)
+ ((uint32_t *)d)[x] = av_bswap32(((uint32_t *)s)[x]);
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+ }
+ }
+}
+
+// PA = PAck, copy planar input to single packed array
+// UN = UNpack, copy packed input to planar output
+// Naming convention:
+// pa_/un_ prefix to identify conversion direction.
+// Left (LSB, lowest byte address) -> Right (MSB, highest byte address).
+// (This is unusual; MSB to LSB is more commonly used to describe formats,
+// but our convention makes more sense for byte access in little endian.)
+// "c" identifies a color component.
+// "z" identifies known zero padding.
+// "x" identifies uninitialized padding.
+// A component is followed by its size in bits.
+// Size can be omitted for multiple uniform components (c8c8c8 == ccc8).
+// Unpackers will often use "x" for padding, because they ignore it, while
+// packers will use "z" because they write zero.
+
+#define PA_WORD_4(name, packed_t, plane_t, sh_c0, sh_c1, sh_c2, sh_c3) \
+ static void name(void *dst, void *src[], int w) { \
+ for (int x = 0; x < w; x++) { \
+ ((packed_t *)dst)[x] = \
+ ((packed_t)((plane_t *)src[0])[x] << (sh_c0)) | \
+ ((packed_t)((plane_t *)src[1])[x] << (sh_c1)) | \
+ ((packed_t)((plane_t *)src[2])[x] << (sh_c2)) | \
+ ((packed_t)((plane_t *)src[3])[x] << (sh_c3)); \
+ } \
+ }
+
+#define UN_WORD_4(name, packed_t, plane_t, sh_c0, sh_c1, sh_c2, sh_c3, mask)\
+ static void name(void *src, void *dst[], int w) { \
+ for (int x = 0; x < w; x++) { \
+ packed_t c = ((packed_t *)src)[x]; \
+ ((plane_t *)dst[0])[x] = (c >> (sh_c0)) & (mask); \
+ ((plane_t *)dst[1])[x] = (c >> (sh_c1)) & (mask); \
+ ((plane_t *)dst[2])[x] = (c >> (sh_c2)) & (mask); \
+ ((plane_t *)dst[3])[x] = (c >> (sh_c3)) & (mask); \
+ } \
+ }
+
+
+#define PA_WORD_3(name, packed_t, plane_t, sh_c0, sh_c1, sh_c2, pad) \
+ static void name(void *dst, void *src[], int w) { \
+ for (int x = 0; x < w; x++) { \
+ ((packed_t *)dst)[x] = (pad) | \
+ ((packed_t)((plane_t *)src[0])[x] << (sh_c0)) | \
+ ((packed_t)((plane_t *)src[1])[x] << (sh_c1)) | \
+ ((packed_t)((plane_t *)src[2])[x] << (sh_c2)); \
+ } \
+ }
+
+UN_WORD_4(un_cccc8, uint32_t, uint8_t, 0, 8, 16, 24, 0xFFu)
+PA_WORD_4(pa_cccc8, uint32_t, uint8_t, 0, 8, 16, 24)
+// Not sure if this is a good idea; there may be no alignment guarantee.
+UN_WORD_4(un_cccc16, uint64_t, uint16_t, 0, 16, 32, 48, 0xFFFFu)
+PA_WORD_4(pa_cccc16, uint64_t, uint16_t, 0, 16, 32, 48)
+
+#define UN_WORD_3(name, packed_t, plane_t, sh_c0, sh_c1, sh_c2, mask) \
+ static void name(void *src, void *dst[], int w) { \
+ for (int x = 0; x < w; x++) { \
+ packed_t c = ((packed_t *)src)[x]; \
+ ((plane_t *)dst[0])[x] = (c >> (sh_c0)) & (mask); \
+ ((plane_t *)dst[1])[x] = (c >> (sh_c1)) & (mask); \
+ ((plane_t *)dst[2])[x] = (c >> (sh_c2)) & (mask); \
+ } \
+ }
+
+UN_WORD_3(un_ccc8x8, uint32_t, uint8_t, 0, 8, 16, 0xFFu)
+PA_WORD_3(pa_ccc8z8, uint32_t, uint8_t, 0, 8, 16, 0)
+UN_WORD_3(un_x8ccc8, uint32_t, uint8_t, 8, 16, 24, 0xFFu)
+PA_WORD_3(pa_z8ccc8, uint32_t, uint8_t, 8, 16, 24, 0)
+UN_WORD_3(un_ccc10x2, uint32_t, uint16_t, 0, 10, 20, 0x3FFu)
+PA_WORD_3(pa_ccc10z2, uint32_t, uint16_t, 0, 10, 20, 0)
+UN_WORD_3(un_ccc16x16, uint64_t, uint16_t, 0, 16, 32, 0xFFFFu)
+PA_WORD_3(pa_ccc16z16, uint64_t, uint16_t, 0, 16, 32, 0)
+
+#define PA_WORD_2(name, packed_t, plane_t, sh_c0, sh_c1, pad) \
+ static void name(void *dst, void *src[], int w) { \
+ for (int x = 0; x < w; x++) { \
+ ((packed_t *)dst)[x] = (pad) | \
+ ((packed_t)((plane_t *)src[0])[x] << (sh_c0)) | \
+ ((packed_t)((plane_t *)src[1])[x] << (sh_c1)); \
+ } \
+ }
+
+#define UN_WORD_2(name, packed_t, plane_t, sh_c0, sh_c1, mask) \
+ static void name(void *src, void *dst[], int w) { \
+ for (int x = 0; x < w; x++) { \
+ packed_t c = ((packed_t *)src)[x]; \
+ ((plane_t *)dst[0])[x] = (c >> (sh_c0)) & (mask); \
+ ((plane_t *)dst[1])[x] = (c >> (sh_c1)) & (mask); \
+ } \
+ }
+
+UN_WORD_2(un_cc8, uint16_t, uint8_t, 0, 8, 0xFFu)
+PA_WORD_2(pa_cc8, uint16_t, uint8_t, 0, 8, 0)
+UN_WORD_2(un_cc16, uint32_t, uint16_t, 0, 16, 0xFFFFu)
+PA_WORD_2(pa_cc16, uint32_t, uint16_t, 0, 16, 0)
+
+#define PA_SEQ_3(name, comp_t) \
+ static void name(void *dst, void *src[], int w) { \
+ comp_t *r = dst; \
+ for (int x = 0; x < w; x++) { \
+ *r++ = ((comp_t *)src[0])[x]; \
+ *r++ = ((comp_t *)src[1])[x]; \
+ *r++ = ((comp_t *)src[2])[x]; \
+ } \
+ }
+
+#define UN_SEQ_3(name, comp_t) \
+ static void name(void *src, void *dst[], int w) { \
+ comp_t *r = src; \
+ for (int x = 0; x < w; x++) { \
+ ((comp_t *)dst[0])[x] = *r++; \
+ ((comp_t *)dst[1])[x] = *r++; \
+ ((comp_t *)dst[2])[x] = *r++; \
+ } \
+ }
+
+UN_SEQ_3(un_ccc8, uint8_t)
+PA_SEQ_3(pa_ccc8, uint8_t)
+UN_SEQ_3(un_ccc16, uint16_t)
+PA_SEQ_3(pa_ccc16, uint16_t)
+
+// "regular": single packed plane, all components have same width (except padding)
+struct regular_repacker {
+ int packed_width; // number of bits of the packed pixel
+ int component_width; // number of bits for a single component
+ int prepadding; // number of bits of LSB padding
+ int num_components; // number of components that can be accessed
+ void (*pa_scanline)(void *a, void *b[], int w);
+ void (*un_scanline)(void *a, void *b[], int w);
+};
+
+static const struct regular_repacker regular_repackers[] = {
+ {32, 8, 0, 3, pa_ccc8z8, un_ccc8x8},
+ {32, 8, 8, 3, pa_z8ccc8, un_x8ccc8},
+ {32, 8, 0, 4, pa_cccc8, un_cccc8},
+ {64, 16, 0, 4, pa_cccc16, un_cccc16},
+ {64, 16, 0, 3, pa_ccc16z16, un_ccc16x16},
+ {24, 8, 0, 3, pa_ccc8, un_ccc8},
+ {48, 16, 0, 3, pa_ccc16, un_ccc16},
+ {16, 8, 0, 2, pa_cc8, un_cc8},
+ {32, 16, 0, 2, pa_cc16, un_cc16},
+ {32, 10, 0, 3, pa_ccc10z2, un_ccc10x2},
+};
+
+static void packed_repack(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ uint32_t *pa = mp_image_pixel_ptr(a, 0, a_x, a_y);
+
+ void *pb[4] = {0};
+ for (int p = 0; p < b->num_planes; p++) {
+ int s = rp->components[p];
+ pb[p] = mp_image_pixel_ptr(b, s, b_x, b_y);
+ }
+
+ rp->packed_repack_scanline(pa, pb, w);
+}
+
+// Tries to set a packer/unpacker for component-wise byte aligned formats.
+static void setup_packed_packer(struct mp_repack *rp)
+{
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(rp->imgfmt_a);
+ if (!(desc.flags & MP_IMGFLAG_HAS_COMPS) ||
+ !(desc.flags & MP_IMGFLAG_TYPE_UINT) ||
+ !(desc.flags & MP_IMGFLAG_NE) ||
+ desc.num_planes != 1)
+ return;
+
+ int num_real_components = 0;
+ int components[4] = {0};
+ for (int n = 0; n < MP_NUM_COMPONENTS; n++) {
+ if (!desc.comps[n].size)
+ continue;
+ if (desc.comps[n].size != desc.comps[0].size ||
+ desc.comps[n].pad != desc.comps[0].pad ||
+ desc.comps[n].offset % desc.comps[0].size)
+ return;
+ int item = desc.comps[n].offset / desc.comps[0].size;
+ if (item >= 4)
+ return;
+ components[item] = n + 1;
+ num_real_components++;
+ }
+
+ int depth = desc.comps[0].size + MPMIN(0, desc.comps[0].pad);
+
+ static const int reorder_gbrp[] = {0, 3, 1, 2, 4};
+ static const int reorder_yuv[] = {0, 1, 2, 3, 4};
+ int planar_fmt = 0;
+ const int *reorder = NULL;
+ if (desc.flags & MP_IMGFLAG_COLOR_YUV) {
+ planar_fmt = find_yuv_format(depth, num_real_components);
+ reorder = reorder_yuv;
+ } else {
+ planar_fmt = find_gbrp_format(depth, num_real_components);
+ reorder = reorder_gbrp;
+ }
+ if (!planar_fmt)
+ return;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(regular_repackers); i++) {
+ const struct regular_repacker *pa = &regular_repackers[i];
+
+ // The following may assume little endian (because some repack backends
+ // use word access, while the metadata here uses byte access).
+
+ int prepad = components[0] ? 0 : 8;
+ int first_comp = components[0] ? 0 : 1;
+ void (*repack_cb)(void *pa, void *pb[], int w) =
+ rp->pack ? pa->pa_scanline : pa->un_scanline;
+
+ if (pa->packed_width != desc.bpp[0] ||
+ pa->component_width != depth ||
+ pa->num_components != num_real_components ||
+ pa->prepadding != prepad ||
+ !repack_cb)
+ continue;
+
+ rp->repack = packed_repack;
+ rp->packed_repack_scanline = repack_cb;
+ rp->imgfmt_b = planar_fmt;
+ for (int n = 0; n < num_real_components; n++) {
+ // Determine permutation that maps component order between the two
+ // formats, with has_alpha special case (see above).
+ int c = reorder[components[first_comp + n]];
+ rp->components[n] = c == 4 ? num_real_components - 1 : c - 1;
+ }
+ return;
+ }
+}
+
+#define PA_SHIFT_LUT8(name, packed_t) \
+ static void name(void *dst, void *src[], int w, uint8_t *lut, \
+ uint8_t s0, uint8_t s1, uint8_t s2) { \
+ for (int x = 0; x < w; x++) { \
+ ((packed_t *)dst)[x] = \
+ (lut[((uint8_t *)src[0])[x] + 256 * 0] << s0) | \
+ (lut[((uint8_t *)src[1])[x] + 256 * 1] << s1) | \
+ (lut[((uint8_t *)src[2])[x] + 256 * 2] << s2); \
+ } \
+ }
+
+
+#define UN_SHIFT_LUT8(name, packed_t) \
+ static void name(void *src, void *dst[], int w, uint8_t *lut, \
+ uint8_t s0, uint8_t s1, uint8_t s2) { \
+ for (int x = 0; x < w; x++) { \
+ packed_t c = ((packed_t *)src)[x]; \
+ ((uint8_t *)dst[0])[x] = lut[((c >> s0) & 0xFF) + 256 * 0]; \
+ ((uint8_t *)dst[1])[x] = lut[((c >> s1) & 0xFF) + 256 * 1]; \
+ ((uint8_t *)dst[2])[x] = lut[((c >> s2) & 0xFF) + 256 * 2]; \
+ } \
+ }
+
+PA_SHIFT_LUT8(pa_shift_lut8_8, uint8_t)
+PA_SHIFT_LUT8(pa_shift_lut8_16, uint16_t)
+UN_SHIFT_LUT8(un_shift_lut8_8, uint8_t)
+UN_SHIFT_LUT8(un_shift_lut8_16, uint16_t)
+
+static void fringe_rgb_repack(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ void *pa = mp_image_pixel_ptr(a, 0, a_x, a_y);
+
+ void *pb[4] = {0};
+ for (int p = 0; p < b->num_planes; p++) {
+ int s = rp->components[p];
+ pb[p] = mp_image_pixel_ptr(b, s, b_x, b_y);
+ }
+
+ assert(rp->comp_size == 1 || rp->comp_size == 2);
+
+ void (*repack)(void *pa, void *pb[], int w, uint8_t *lut,
+ uint8_t s0, uint8_t s1, uint8_t s2) = NULL;
+ if (rp->pack) {
+ repack = rp->comp_size == 1 ? pa_shift_lut8_8 : pa_shift_lut8_16;
+ } else {
+ repack = rp->comp_size == 1 ? un_shift_lut8_8 : un_shift_lut8_16;
+ }
+ repack(pa, pb, w, rp->comp_lut,
+ rp->comp_shifts[0], rp->comp_shifts[1], rp->comp_shifts[2]);
+}
+
+static void setup_fringe_rgb_packer(struct mp_repack *rp)
+{
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(rp->imgfmt_a);
+ if (!(desc.flags & MP_IMGFLAG_HAS_COMPS))
+ return;
+
+ if (desc.bpp[0] > 16 || (desc.bpp[0] % 8u) ||
+ mp_imgfmt_get_forced_csp(rp->imgfmt_a) != MP_CSP_RGB ||
+ desc.num_planes != 1 || desc.comps[3].size)
+ return;
+
+ int depth = desc.comps[0].size;
+ for (int n = 0; n < 3; n++) {
+ struct mp_imgfmt_comp_desc *c = &desc.comps[n];
+
+ if (c->size < 1 || c->size > 8 || c->pad)
+ return;
+
+ if (rp->flags & REPACK_CREATE_ROUND_DOWN) {
+ depth = MPMIN(depth, c->size);
+ } else {
+ depth = MPMAX(depth, c->size);
+ }
+ }
+ if (rp->flags & REPACK_CREATE_EXPAND_8BIT)
+ depth = 8;
+
+ rp->imgfmt_b = find_gbrp_format(depth, 3);
+ if (!rp->imgfmt_b)
+ return;
+ rp->comp_lut = talloc_array(rp, uint8_t, 256 * 3);
+ rp->repack = fringe_rgb_repack;
+ for (int n = 0; n < 3; n++)
+ rp->components[n] = ((int[]){3, 1, 2})[n] - 1;
+
+ for (int n = 0; n < 3; n++) {
+ int bits = desc.comps[n].size;
+ rp->comp_shifts[n] = desc.comps[n].offset;
+ if (rp->comp_lut) {
+ uint8_t *lut = rp->comp_lut + 256 * n;
+ uint8_t zmax = (1 << depth) - 1;
+ uint8_t cmax = (1 << bits) - 1;
+ for (int v = 0; v < 256; v++) {
+ if (rp->pack) {
+ lut[v] = (v * cmax + zmax / 2) / zmax;
+ } else {
+ lut[v] = (v & cmax) * zmax / cmax;
+ }
+ }
+ }
+ }
+
+ rp->comp_size = (desc.bpp[0] + 7) / 8;
+ assert(rp->comp_size == 1 || rp->comp_size == 2);
+
+ if (desc.endian_shift) {
+ assert(rp->comp_size == 2 && (1 << desc.endian_shift) == 2);
+ rp->endian_size = 2;
+ }
+}
+
+static void unpack_pal(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ uint8_t *src = mp_image_pixel_ptr(a, 0, a_x, a_y);
+ uint32_t *pal = (void *)a->planes[1];
+
+ uint8_t *dst[4] = {0};
+ for (int p = 0; p < b->num_planes; p++)
+ dst[p] = mp_image_pixel_ptr(b, p, b_x, b_y);
+
+ for (int x = 0; x < w; x++) {
+ uint32_t c = pal[src[x]];
+ dst[0][x] = (c >> 8) & 0xFF; // G
+ dst[1][x] = (c >> 0) & 0xFF; // B
+ dst[2][x] = (c >> 16) & 0xFF; // R
+ dst[3][x] = (c >> 24) & 0xFF; // A
+ }
+}
+
+static void bitmap_repack(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ uint8_t *pa = mp_image_pixel_ptr(a, 0, a_x, a_y);
+ uint8_t *pb = mp_image_pixel_ptr(b, 0, b_x, b_y);
+
+ if (rp->pack) {
+ for (unsigned x = 0; x < w; x += 8) {
+ uint8_t d = 0;
+ int max_b = MPMIN(8, w - x);
+ for (int bp = 0; bp < max_b; bp++)
+ d |= (rp->comp_lut[pb[x + bp]]) << (7 - bp);
+ pa[x / 8] = d;
+ }
+ } else {
+ for (unsigned x = 0; x < w; x += 8) {
+ uint8_t d = pa[x / 8];
+ int max_b = MPMIN(8, w - x);
+ for (int bp = 0; bp < max_b; bp++)
+ pb[x + bp] = rp->comp_lut[d & (1 << (7 - bp))];
+ }
+ }
+}
+
+static void setup_misc_packer(struct mp_repack *rp)
+{
+ if (rp->imgfmt_a == IMGFMT_PAL8 && !rp->pack) {
+ int grap_fmt = find_gbrp_format(8, 4);
+ if (!grap_fmt)
+ return;
+ rp->imgfmt_b = grap_fmt;
+ rp->repack = unpack_pal;
+ } else {
+ enum AVPixelFormat avfmt = imgfmt2pixfmt(rp->imgfmt_a);
+ if (avfmt == AV_PIX_FMT_MONOWHITE || avfmt == AV_PIX_FMT_MONOBLACK) {
+ rp->comp_lut = talloc_array(rp, uint8_t, 256);
+ rp->imgfmt_b = IMGFMT_Y1;
+ int max = 1;
+ if (rp->flags & REPACK_CREATE_EXPAND_8BIT) {
+ rp->imgfmt_b = IMGFMT_Y8;
+ max = 255;
+ }
+ bool inv = avfmt == AV_PIX_FMT_MONOWHITE;
+ for (int n = 0; n < 256; n++) {
+ rp->comp_lut[n] = rp->pack ? (inv ^ (n >= (max + 1) / 2))
+ : ((inv ^ !!n) ? max : 0);
+ }
+ rp->repack = bitmap_repack;
+ return;
+ }
+ }
+}
+
+#define PA_P422(name, comp_t) \
+ static void name(void *dst, void *src[], int w, uint8_t *c) { \
+ for (int x = 0; x < w; x += 2) { \
+ ((comp_t *)dst)[x * 2 + c[0]] = ((comp_t *)src[0])[x + 0]; \
+ ((comp_t *)dst)[x * 2 + c[1]] = ((comp_t *)src[0])[x + 1]; \
+ ((comp_t *)dst)[x * 2 + c[4]] = ((comp_t *)src[1])[x >> 1]; \
+ ((comp_t *)dst)[x * 2 + c[5]] = ((comp_t *)src[2])[x >> 1]; \
+ } \
+ }
+
+
+#define UN_P422(name, comp_t) \
+ static void name(void *src, void *dst[], int w, uint8_t *c) { \
+ for (int x = 0; x < w; x += 2) { \
+ ((comp_t *)dst[0])[x + 0] = ((comp_t *)src)[x * 2 + c[0]]; \
+ ((comp_t *)dst[0])[x + 1] = ((comp_t *)src)[x * 2 + c[1]]; \
+ ((comp_t *)dst[1])[x >> 1] = ((comp_t *)src)[x * 2 + c[4]]; \
+ ((comp_t *)dst[2])[x >> 1] = ((comp_t *)src)[x * 2 + c[5]]; \
+ } \
+ }
+
+PA_P422(pa_p422_8, uint8_t)
+PA_P422(pa_p422_16, uint16_t)
+UN_P422(un_p422_8, uint8_t)
+UN_P422(un_p422_16, uint16_t)
+
+static void pa_p411_8(void *dst, void *src[], int w, uint8_t *c)
+{
+ for (int x = 0; x < w; x += 4) {
+ ((uint8_t *)dst)[x / 4 * 6 + c[0]] = ((uint8_t *)src[0])[x + 0];
+ ((uint8_t *)dst)[x / 4 * 6 + c[1]] = ((uint8_t *)src[0])[x + 1];
+ ((uint8_t *)dst)[x / 4 * 6 + c[2]] = ((uint8_t *)src[0])[x + 2];
+ ((uint8_t *)dst)[x / 4 * 6 + c[3]] = ((uint8_t *)src[0])[x + 3];
+ ((uint8_t *)dst)[x / 4 * 6 + c[4]] = ((uint8_t *)src[1])[x >> 2];
+ ((uint8_t *)dst)[x / 4 * 6 + c[5]] = ((uint8_t *)src[2])[x >> 2];
+ }
+}
+
+
+static void un_p411_8(void *src, void *dst[], int w, uint8_t *c)
+{
+ for (int x = 0; x < w; x += 4) {
+ ((uint8_t *)dst[0])[x + 0] = ((uint8_t *)src)[x / 4 * 6 + c[0]];
+ ((uint8_t *)dst[0])[x + 1] = ((uint8_t *)src)[x / 4 * 6 + c[1]];
+ ((uint8_t *)dst[0])[x + 2] = ((uint8_t *)src)[x / 4 * 6 + c[2]];
+ ((uint8_t *)dst[0])[x + 3] = ((uint8_t *)src)[x / 4 * 6 + c[3]];
+ ((uint8_t *)dst[1])[x >> 2] = ((uint8_t *)src)[x / 4 * 6 + c[4]];
+ ((uint8_t *)dst[2])[x >> 2] = ((uint8_t *)src)[x / 4 * 6 + c[5]];
+ }
+}
+
+static void fringe_yuv_repack(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ void *pa = mp_image_pixel_ptr(a, 0, a_x, a_y);
+
+ void *pb[4] = {0};
+ for (int p = 0; p < b->num_planes; p++)
+ pb[p] = mp_image_pixel_ptr(b, p, b_x, b_y);
+
+ rp->repack_fringe_yuv(pa, pb, w, rp->comp_map);
+}
+
+static void setup_fringe_yuv_packer(struct mp_repack *rp)
+{
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(rp->imgfmt_a);
+ if (!(desc.flags & MP_IMGFLAG_PACKED_SS_YUV) ||
+ mp_imgfmt_desc_get_num_comps(&desc) != 3 ||
+ desc.align_x > 4)
+ return;
+
+ uint8_t y_loc[4];
+ if (!mp_imgfmt_get_packed_yuv_locations(desc.id, y_loc))
+ return;
+
+ for (int n = 0; n < MP_NUM_COMPONENTS; n++) {
+ if (!desc.comps[n].size)
+ continue;
+ if (desc.comps[n].size != desc.comps[0].size ||
+ desc.comps[n].pad < 0 ||
+ desc.comps[n].offset % desc.comps[0].size)
+ return;
+ if (n == 1 || n == 2) {
+ rp->comp_map[4 + (n - 1)] =
+ desc.comps[n].offset / desc.comps[0].size;
+ }
+ }
+ for (int n = 0; n < desc.align_x; n++) {
+ if (y_loc[n] % desc.comps[0].size)
+ return;
+ rp->comp_map[n] = y_loc[n] / desc.comps[0].size;
+ }
+
+ if (desc.comps[0].size == 8 && desc.align_x == 2) {
+ rp->repack_fringe_yuv = rp->pack ? pa_p422_8 : un_p422_8;
+ } else if (desc.comps[0].size == 16 && desc.align_x == 2) {
+ rp->repack_fringe_yuv = rp->pack ? pa_p422_16 : un_p422_16;
+ } else if (desc.comps[0].size == 8 && desc.align_x == 4) {
+ rp->repack_fringe_yuv = rp->pack ? pa_p411_8 : un_p411_8;
+ }
+
+ if (!rp->repack_fringe_yuv)
+ return;
+
+ struct mp_regular_imgfmt yuvfmt = {
+ .component_type = MP_COMPONENT_TYPE_UINT,
+ // NB: same problem with P010 and not clearing padding.
+ .component_size = desc.comps[0].size / 8u,
+ .num_planes = 3,
+ .planes = { {1, {1}}, {1, {2}}, {1, {3}} },
+ .chroma_xs = desc.chroma_xs,
+ .chroma_ys = 0,
+ };
+ rp->imgfmt_b = mp_find_regular_imgfmt(&yuvfmt);
+ rp->repack = fringe_yuv_repack;
+
+ if (desc.endian_shift) {
+ rp->endian_size = 1 << desc.endian_shift;
+ assert(rp->endian_size == 2);
+ }
+}
+
+static void repack_nv(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ int xs = a->fmt.chroma_xs;
+
+ uint32_t *pa = mp_image_pixel_ptr(a, 1, a_x, a_y);
+
+ void *pb[2];
+ for (int p = 0; p < 2; p++) {
+ int s = rp->components[p];
+ pb[p] = mp_image_pixel_ptr(b, s, b_x, b_y);
+ }
+
+ rp->packed_repack_scanline(pa, pb, (w + (1 << xs) - 1) >> xs);
+}
+
+static void setup_nv_packer(struct mp_repack *rp)
+{
+ struct mp_regular_imgfmt desc;
+ if (!mp_get_regular_imgfmt(&desc, rp->imgfmt_a))
+ return;
+
+ // Check for NV.
+ if (desc.num_planes != 2)
+ return;
+ if (desc.planes[0].num_components != 1 || desc.planes[0].components[0] != 1)
+ return;
+ if (desc.planes[1].num_components != 2)
+ return;
+ int cr0 = desc.planes[1].components[0];
+ int cr1 = desc.planes[1].components[1];
+ if (cr0 > cr1)
+ MPSWAP(int, cr0, cr1);
+ if (cr0 != 2 || cr1 != 3)
+ return;
+
+ // Construct equivalent planar format.
+ struct mp_regular_imgfmt desc2 = desc;
+ desc2.num_planes = 3;
+ desc2.planes[1].num_components = 1;
+ desc2.planes[1].components[0] = 2;
+ desc2.planes[2].num_components = 1;
+ desc2.planes[2].components[0] = 3;
+ // For P010. Strangely this concept exists only for the NV format.
+ if (desc2.component_pad > 0)
+ desc2.component_pad = 0;
+
+ int planar_fmt = mp_find_regular_imgfmt(&desc2);
+ if (!planar_fmt)
+ return;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(regular_repackers); i++) {
+ const struct regular_repacker *pa = &regular_repackers[i];
+
+ void (*repack_cb)(void *pa, void *pb[], int w) =
+ rp->pack ? pa->pa_scanline : pa->un_scanline;
+
+ if (pa->packed_width != desc.component_size * 2 * 8 ||
+ pa->component_width != desc.component_size * 8 ||
+ pa->num_components != 2 ||
+ pa->prepadding != 0 ||
+ !repack_cb)
+ continue;
+
+ rp->repack = repack_nv;
+ rp->passthrough_y = true;
+ rp->packed_repack_scanline = repack_cb;
+ rp->imgfmt_b = planar_fmt;
+ rp->components[0] = desc.planes[1].components[0] - 1;
+ rp->components[1] = desc.planes[1].components[1] - 1;
+ return;
+ }
+}
+
+#define PA_F32(name, packed_t) \
+ static void name(void *dst, float *src, int w, float m, float o, \
+ uint32_t p_max) { \
+ for (int x = 0; x < w; x++) { \
+ ((packed_t *)dst)[x] = \
+ MPCLAMP(lrint((src[x] + o) * m), 0, (packed_t)p_max); \
+ } \
+ }
+
+#define UN_F32(name, packed_t) \
+ static void name(void *src, float *dst, int w, float m, float o, \
+ uint32_t unused) { \
+ for (int x = 0; x < w; x++) \
+ dst[x] = ((packed_t *)src)[x] * m + o; \
+ }
+
+PA_F32(pa_f32_8, uint8_t)
+UN_F32(un_f32_8, uint8_t)
+PA_F32(pa_f32_16, uint16_t)
+UN_F32(un_f32_16, uint16_t)
+
+// In all this, float counts as "unpacked".
+static void repack_float(struct mp_repack *rp,
+ struct mp_image *a, int a_x, int a_y,
+ struct mp_image *b, int b_x, int b_y, int w)
+{
+ assert(rp->f32_comp_size == 1 || rp->f32_comp_size == 2);
+
+ void (*packer)(void *a, float *b, int w, float fm, float fb, uint32_t max)
+ = rp->pack ? (rp->f32_comp_size == 1 ? pa_f32_8 : pa_f32_16)
+ : (rp->f32_comp_size == 1 ? un_f32_8 : un_f32_16);
+
+ for (int p = 0; p < b->num_planes; p++) {
+ int h = (1 << b->fmt.chroma_ys) - (1 << b->fmt.ys[p]) + 1;
+ for (int y = 0; y < h; y++) {
+ void *pa = mp_image_pixel_ptr_ny(a, p, a_x, a_y + y);
+ void *pb = mp_image_pixel_ptr_ny(b, p, b_x, b_y + y);
+
+ packer(pa, pb, w >> b->fmt.xs[p], rp->f32_m[p], rp->f32_o[p],
+ rp->f32_pmax[p]);
+ }
+ }
+}
+
+static void update_repack_float(struct mp_repack *rp)
+{
+ if (!rp->f32_comp_size)
+ return;
+
+ // Image in input format.
+ struct mp_image *ui = rp->pack ? rp->steps[rp->num_steps - 1].buf[1]
+ : rp->steps[0].buf[0];
+ enum mp_csp csp = ui->params.color.space;
+ enum mp_csp_levels levels = ui->params.color.levels;
+ if (rp->f32_csp_space == csp && rp->f32_csp_levels == levels)
+ return;
+
+ // The fixed point format.
+ struct mp_regular_imgfmt desc = {0};
+ mp_get_regular_imgfmt(&desc, rp->imgfmt_b);
+ assert(desc.component_size);
+
+ int comp_bits = desc.component_size * 8 + MPMIN(desc.component_pad, 0);
+ for (int p = 0; p < desc.num_planes; p++) {
+ double m, o;
+ mp_get_csp_uint_mul(csp, levels, comp_bits, desc.planes[p].components[0],
+ &m, &o);
+ rp->f32_m[p] = rp->pack ? 1.0 / m : m;
+ rp->f32_o[p] = rp->pack ? -o : o;
+ rp->f32_pmax[p] = (1u << comp_bits) - 1;
+ }
+
+ rp->f32_csp_space = csp;
+ rp->f32_csp_levels = levels;
+}
+
+void repack_line(struct mp_repack *rp, int dst_x, int dst_y,
+ int src_x, int src_y, int w)
+{
+ assert(rp->configured);
+
+ struct repack_step *first = &rp->steps[0];
+ struct repack_step *last = &rp->steps[rp->num_steps - 1];
+
+ assert(dst_x >= 0 && dst_y >= 0 && src_x >= 0 && src_y >= 0 && w >= 0);
+ assert(dst_x + w <= MP_ALIGN_UP(last->buf[1]->w, last->fmt[1].align_x));
+ assert(src_x + w <= MP_ALIGN_UP(first->buf[0]->w, first->fmt[0].align_x));
+ assert(dst_y < last->buf[1]->h);
+ assert(src_y < first->buf[0]->h);
+ assert(!(dst_x & (last->fmt[1].align_x - 1)));
+ assert(!(src_x & (first->fmt[0].align_x - 1)));
+ assert(!(w & ((1 << first->fmt[0].chroma_xs) - 1)));
+ assert(!(dst_y & (last->fmt[1].align_y - 1)));
+ assert(!(src_y & (first->fmt[0].align_y - 1)));
+
+ for (int n = 0; n < rp->num_steps; n++) {
+ struct repack_step *rs = &rp->steps[n];
+
+ // When writing to temporary buffers, always write to the start (maybe
+ // helps with locality).
+ int sx = rs->user_buf[0] ? src_x : 0;
+ int sy = rs->user_buf[0] ? src_y : 0;
+ int dx = rs->user_buf[1] ? dst_x : 0;
+ int dy = rs->user_buf[1] ? dst_y : 0;
+
+ struct mp_image *buf_a = rs->buf[rp->pack];
+ struct mp_image *buf_b = rs->buf[!rp->pack];
+ int a_x = rp->pack ? dx : sx;
+ int a_y = rp->pack ? dy : sy;
+ int b_x = rp->pack ? sx : dx;
+ int b_y = rp->pack ? sy : dy;
+
+ switch (rs->type) {
+ case REPACK_STEP_REPACK: {
+ if (rp->repack)
+ rp->repack(rp, buf_a, a_x, a_y, buf_b, b_x, b_y, w);
+
+ for (int p = 0; p < rs->fmt[0].num_planes; p++) {
+ if (rp->copy_buf[p])
+ copy_plane(rs->buf[1], dx, dy, rs->buf[0], sx, sy, w, p);
+ }
+ break;
+ }
+ case REPACK_STEP_ENDIAN:
+ swap_endian(rs->buf[1], dx, dy, rs->buf[0], sx, sy, w,
+ rp->endian_size);
+ break;
+ case REPACK_STEP_FLOAT:
+ repack_float(rp, buf_a, a_x, a_y, buf_b, b_x, b_y, w);
+ break;
+ }
+ }
+}
+
+static bool setup_format_ne(struct mp_repack *rp)
+{
+ if (!rp->imgfmt_b)
+ setup_nv_packer(rp);
+ if (!rp->imgfmt_b)
+ setup_misc_packer(rp);
+ if (!rp->imgfmt_b)
+ setup_packed_packer(rp);
+ if (!rp->imgfmt_b)
+ setup_fringe_rgb_packer(rp);
+ if (!rp->imgfmt_b)
+ setup_fringe_yuv_packer(rp);
+ if (!rp->imgfmt_b)
+ rp->imgfmt_b = rp->imgfmt_a; // maybe it was planar after all
+
+ struct mp_regular_imgfmt desc;
+ if (!mp_get_regular_imgfmt(&desc, rp->imgfmt_b))
+ return false;
+
+ // no weird stuff
+ if (desc.num_planes > 4)
+ return false;
+
+ // Endian swapping.
+ if (rp->imgfmt_a != rp->imgfmt_user &&
+ rp->imgfmt_a == mp_find_other_endian(rp->imgfmt_user))
+ {
+ struct mp_imgfmt_desc desc_a = mp_imgfmt_get_desc(rp->imgfmt_a);
+ struct mp_imgfmt_desc desc_u = mp_imgfmt_get_desc(rp->imgfmt_user);
+ rp->endian_size = 1 << desc_u.endian_shift;
+ if (!desc_a.endian_shift && rp->endian_size != 2 && rp->endian_size != 4)
+ return false;
+ }
+
+ // Accept only true planar formats (with known components and no padding).
+ for (int n = 0; n < desc.num_planes; n++) {
+ if (desc.planes[n].num_components != 1)
+ return false;
+ int c = desc.planes[n].components[0];
+ if (c < 1 || c > 4)
+ return false;
+ }
+
+ rp->fmt_a = mp_imgfmt_get_desc(rp->imgfmt_a);
+ rp->fmt_b = mp_imgfmt_get_desc(rp->imgfmt_b);
+
+ // This is if we did a pack step.
+
+ if (rp->flags & REPACK_CREATE_PLANAR_F32) {
+ // imgfmt_b with float32 component type.
+ struct mp_regular_imgfmt fdesc = desc;
+ fdesc.component_type = MP_COMPONENT_TYPE_FLOAT;
+ fdesc.component_size = 4;
+ fdesc.component_pad = 0;
+ int ffmt = mp_find_regular_imgfmt(&fdesc);
+ if (!ffmt)
+ return false;
+ if (ffmt != rp->imgfmt_b) {
+ if (desc.component_type != MP_COMPONENT_TYPE_UINT ||
+ (desc.component_size != 1 && desc.component_size != 2))
+ return false;
+ rp->f32_comp_size = desc.component_size;
+ rp->f32_csp_space = MP_CSP_COUNT;
+ rp->f32_csp_levels = MP_CSP_LEVELS_COUNT;
+ rp->steps[rp->num_steps++] = (struct repack_step) {
+ .type = REPACK_STEP_FLOAT,
+ .fmt = {
+ mp_imgfmt_get_desc(ffmt),
+ rp->fmt_b,
+ },
+ };
+ }
+ }
+
+ rp->steps[rp->num_steps++] = (struct repack_step) {
+ .type = REPACK_STEP_REPACK,
+ .fmt = { rp->fmt_b, rp->fmt_a },
+ };
+
+ if (rp->endian_size) {
+ rp->steps[rp->num_steps++] = (struct repack_step) {
+ .type = REPACK_STEP_ENDIAN,
+ .fmt = {
+ rp->fmt_a,
+ mp_imgfmt_get_desc(rp->imgfmt_user),
+ },
+ };
+ }
+
+ // Reverse if unpack (to reflect actual data flow)
+ if (!rp->pack) {
+ for (int n = 0; n < rp->num_steps / 2; n++) {
+ MPSWAP(struct repack_step, rp->steps[n],
+ rp->steps[rp->num_steps - 1 - n]);
+ }
+ for (int n = 0; n < rp->num_steps; n++) {
+ struct repack_step *rs = &rp->steps[n];
+ MPSWAP(struct mp_imgfmt_desc, rs->fmt[0], rs->fmt[1]);
+ }
+ }
+
+ for (int n = 0; n < rp->num_steps - 1; n++)
+ assert(rp->steps[n].fmt[1].id == rp->steps[n + 1].fmt[0].id);
+
+ return true;
+}
+
+static void reset_params(struct mp_repack *rp)
+{
+ rp->num_steps = 0;
+ rp->imgfmt_b = 0;
+ rp->repack = NULL;
+ rp->passthrough_y = false;
+ rp->endian_size = 0;
+ rp->packed_repack_scanline = NULL;
+ rp->comp_size = 0;
+ talloc_free(rp->comp_lut);
+ rp->comp_lut = NULL;
+}
+
+static bool setup_format(struct mp_repack *rp)
+{
+ reset_params(rp);
+ rp->imgfmt_a = rp->imgfmt_user;
+ if (setup_format_ne(rp))
+ return true;
+ // Try reverse endian.
+ reset_params(rp);
+ rp->imgfmt_a = mp_find_other_endian(rp->imgfmt_user);
+ return rp->imgfmt_a && setup_format_ne(rp);
+}
+
+struct mp_repack *mp_repack_create_planar(int imgfmt, bool pack, int flags)
+{
+ struct mp_repack *rp = talloc_zero(NULL, struct mp_repack);
+ rp->imgfmt_user = imgfmt;
+ rp->pack = pack;
+ rp->flags = flags;
+
+ if (!setup_format(rp)) {
+ talloc_free(rp);
+ return NULL;
+ }
+
+ return rp;
+}
+
+int mp_repack_get_format_src(struct mp_repack *rp)
+{
+ return rp->steps[0].fmt[0].id;
+}
+
+int mp_repack_get_format_dst(struct mp_repack *rp)
+{
+ return rp->steps[rp->num_steps - 1].fmt[1].id;
+}
+
+int mp_repack_get_align_x(struct mp_repack *rp)
+{
+ // We really want the LCM between those, but since only one of them is
+ // packed (or they're the same format), and the chroma subsampling is the
+ // same for both, only the packed one matters.
+ return rp->fmt_a.align_x;
+}
+
+int mp_repack_get_align_y(struct mp_repack *rp)
+{
+ return rp->fmt_a.align_y; // should be the same for packed/planar formats
+}
+
+static void image_realloc(struct mp_image **img, int fmt, int w, int h)
+{
+ if (*img && (*img)->imgfmt == fmt && (*img)->w == w && (*img)->h == h)
+ return;
+ talloc_free(*img);
+ *img = mp_image_alloc(fmt, w, h);
+}
+
+bool repack_config_buffers(struct mp_repack *rp,
+ int dst_flags, struct mp_image *dst,
+ int src_flags, struct mp_image *src,
+ bool *enable_passthrough)
+{
+ struct repack_step *rs_first = &rp->steps[0];
+ struct repack_step *rs_last = &rp->steps[rp->num_steps - 1];
+
+ rp->configured = false;
+
+ assert(dst && src);
+
+ int buf_w = MPMAX(dst->w, src->w);
+
+ assert(dst->imgfmt == rs_last->fmt[1].id);
+ assert(src->imgfmt == rs_first->fmt[0].id);
+
+ // Chain/allocate buffers.
+
+ for (int n = 0; n < rp->num_steps; n++)
+ rp->steps[n].buf[0] = rp->steps[n].buf[1] = NULL;
+
+ rs_first->buf[0] = src;
+ rs_last->buf[1] = dst;
+
+ for (int n = 0; n < rp->num_steps; n++) {
+ struct repack_step *rs = &rp->steps[n];
+
+ if (!rs->buf[0]) {
+ assert(n > 0);
+ rs->buf[0] = rp->steps[n - 1].buf[1];
+ }
+
+ if (rs->buf[1])
+ continue;
+
+ // Note: since repack_line() can have different src/dst offsets, we
+ // can't do true in-place in general.
+ bool can_inplace = rs->type == REPACK_STEP_ENDIAN &&
+ rs->buf[0] != src && rs->buf[0] != dst;
+ if (can_inplace) {
+ rs->buf[1] = rs->buf[0];
+ continue;
+ }
+
+ if (rs != rs_last) {
+ struct repack_step *next = &rp->steps[n + 1];
+ if (next->buf[0]) {
+ rs->buf[1] = next->buf[0];
+ continue;
+ }
+ }
+
+ image_realloc(&rs->tmp, rs->fmt[1].id, buf_w, rs->fmt[1].align_y);
+ if (!rs->tmp)
+ return false;
+ talloc_steal(rp, rs->tmp);
+ rs->buf[1] = rs->tmp;
+ }
+
+ for (int n = 0; n < rp->num_steps; n++) {
+ struct repack_step *rs = &rp->steps[n];
+ rs->user_buf[0] = rs->buf[0] == src || rs->buf[0] == dst;
+ rs->user_buf[1] = rs->buf[1] == src || rs->buf[1] == dst;
+ }
+
+ // If repacking is the only operation. It's also responsible for simply
+ // copying src to dst if absolutely no filtering is done.
+ bool may_passthrough =
+ rp->num_steps == 1 && rp->steps[0].type == REPACK_STEP_REPACK;
+
+ for (int p = 0; p < rp->fmt_b.num_planes; p++) {
+ // (All repack callbacks copy, except nv12 does not copy luma.)
+ bool repack_copies_plane = rp->repack && !(rp->passthrough_y && p == 0);
+
+ bool can_pt = may_passthrough && !repack_copies_plane &&
+ enable_passthrough && enable_passthrough[p];
+
+ // Copy if needed, unless the repack callback does it anyway.
+ rp->copy_buf[p] = !repack_copies_plane && !can_pt;
+
+ if (enable_passthrough)
+ enable_passthrough[p] = can_pt && !rp->copy_buf[p];
+ }
+
+ if (enable_passthrough) {
+ for (int n = rp->fmt_b.num_planes; n < MP_MAX_PLANES; n++)
+ enable_passthrough[n] = false;
+ }
+
+ update_repack_float(rp);
+
+ rp->configured = true;
+
+ return true;
+}
diff --git a/video/repack.h b/video/repack.h
new file mode 100644
index 0000000..7afe7ed
--- /dev/null
+++ b/video/repack.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <stdbool.h>
+
+enum {
+ // This controls bheavior with different bit widths per component (like
+ // RGB565). If ROUND_DOWN is specified, the planar format will use the min.
+ // bit width of all components, otherwise the transformation is lossless.
+ REPACK_CREATE_ROUND_DOWN = (1 << 0),
+
+ // Expand some (not all) low bit depth fringe formats to 8 bit on unpack.
+ REPACK_CREATE_EXPAND_8BIT = (1 << 1),
+
+ // For mp_repack_create_planar(). If specified, the planar format uses a
+ // float 32 bit sample format. No range expansion is done.
+ REPACK_CREATE_PLANAR_F32 = (1 << 2),
+};
+
+struct mp_repack;
+struct mp_image;
+
+// Create a repacker between any format (imgfmt parameter) and an equivalent
+// planar format (that is native endian). If pack==true, imgfmt is the output,
+// otherwise it is the input. The respective other input/output is the planar
+// format. The planar format can be queried with mp_repack_get_format_*().
+// Note that some formats may change the "implied" colorspace (for example,
+// packed xyz unpacks as rgb).
+// If imgfmt is already planar, a passthrough repacker may be created.
+// imgfmt: src or dst format (usually packed, non-planar, etc.)
+// pack: true if imgfmt is dst, false if imgfmt is src
+// flags: any of REPACK_CREATE_* flags
+// returns: NULL on failure, otherwise free with talloc_free().
+struct mp_repack *mp_repack_create_planar(int imgfmt, bool pack, int flags);
+
+// Return input and output formats for which rp was created.
+int mp_repack_get_format_src(struct mp_repack *rp);
+int mp_repack_get_format_dst(struct mp_repack *rp);
+
+// Return pixel alignment. For x, this is a lowest pixel count at which there is
+// a byte boundary and a full chroma pixel (horizontal subsampling) on src/dst.
+// For y, this is the pixel height of the vertical subsampling.
+// Always returns a power of 2.
+int mp_repack_get_align_x(struct mp_repack *rp);
+int mp_repack_get_align_y(struct mp_repack *rp);
+
+// Repack a single line from dst to src, as set in repack_config_buffers().
+// For subsampled chroma formats, this copies as many luma/alpha rows as needed
+// for a complete line (e.g. 2 luma lines, 1 chroma line for 4:2:0).
+// dst_x, src_x, y must be aligned to the pixel alignment. w may be unaligned
+// if at the right crop-border of the image, but must be always aligned to
+// horiz. sub-sampling. y is subject to hslice.
+void repack_line(struct mp_repack *rp, int dst_x, int dst_y,
+ int src_x, int src_y, int w);
+
+// Configure with a source and target buffer. The rp instance will keep the
+// mp_image pointers and access them on repack_line() calls. Refcounting is
+// not respected - the caller needs to make sure dst is always writable.
+// The images can have different sizes (as repack_line() lets you use different
+// target coordinates for dst/src).
+// This also allocaters potentially required temporary buffers.
+// dst_flags: REPACK_BUF_* flags for dst
+// dst: where repack_line() writes to
+// src_flags: REPACK_BUF_* flags for src
+// src: where repack_line() reads from
+// enable_passthrough: if non-NULL, an bool array of size MP_MAX_PLANES indexed
+// by plane; a true entry requests disabling copying the
+// plane data to the dst plane. The function will write to
+// this array whether the plane can really be passed through
+// (i.e. will set array entries from true to false if pass-
+// through is not possible). It writes to all MP_MAX_PLANES
+// entries. If NULL, all entries are implicitly false.
+// returns: success (fails on OOM)
+bool repack_config_buffers(struct mp_repack *rp,
+ int dst_flags, struct mp_image *dst,
+ int src_flags, struct mp_image *src,
+ bool *enable_passthrough);
diff --git a/video/sws_utils.c b/video/sws_utils.c
new file mode 100644
index 0000000..5e9c358
--- /dev/null
+++ b/video/sws_utils.c
@@ -0,0 +1,496 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <libswscale/swscale.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/bswap.h>
+#include <libavutil/opt.h>
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
+#include <libavutil/pixdesc.h>
+#endif
+
+#include "config.h"
+
+#include "sws_utils.h"
+
+#include "common/common.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "video/mp_image.h"
+#include "video/img_format.h"
+#include "fmt-conversion.h"
+#include "csputils.h"
+#include "common/msg.h"
+#include "osdep/endian.h"
+
+#if HAVE_ZIMG
+#include "zimg.h"
+#endif
+
+//global sws_flags from the command line
+struct sws_opts {
+ int scaler;
+ float lum_gblur;
+ float chr_gblur;
+ int chr_vshift;
+ int chr_hshift;
+ float chr_sharpen;
+ float lum_sharpen;
+ bool fast;
+ bool bitexact;
+ bool zimg;
+};
+
+#define OPT_BASE_STRUCT struct sws_opts
+const struct m_sub_options sws_conf = {
+ .opts = (const m_option_t[]) {
+ {"scaler", OPT_CHOICE(scaler,
+ {"fast-bilinear", SWS_FAST_BILINEAR},
+ {"bilinear", SWS_BILINEAR},
+ {"bicubic", SWS_BICUBIC},
+ {"x", SWS_X},
+ {"point", SWS_POINT},
+ {"area", SWS_AREA},
+ {"bicublin", SWS_BICUBLIN},
+ {"gauss", SWS_GAUSS},
+ {"sinc", SWS_SINC},
+ {"lanczos", SWS_LANCZOS},
+ {"spline", SWS_SPLINE})},
+ {"lgb", OPT_FLOAT(lum_gblur), M_RANGE(0, 100.0)},
+ {"cgb", OPT_FLOAT(chr_gblur), M_RANGE(0, 100.0)},
+ {"cvs", OPT_INT(chr_vshift)},
+ {"chs", OPT_INT(chr_hshift)},
+ {"ls", OPT_FLOAT(lum_sharpen), M_RANGE(-100.0, 100.0)},
+ {"cs", OPT_FLOAT(chr_sharpen), M_RANGE(-100.0, 100.0)},
+ {"fast", OPT_BOOL(fast)},
+ {"bitexact", OPT_BOOL(bitexact)},
+ {"allow-zimg", OPT_BOOL(zimg)},
+ {0}
+ },
+ .size = sizeof(struct sws_opts),
+ .defaults = &(const struct sws_opts){
+ .scaler = SWS_LANCZOS,
+ .zimg = true,
+ },
+};
+
+// Highest quality, but also slowest.
+static const int mp_sws_hq_flags = SWS_FULL_CHR_H_INT | SWS_FULL_CHR_H_INP |
+ SWS_ACCURATE_RND;
+
+// Fast, lossy.
+const int mp_sws_fast_flags = SWS_BILINEAR;
+
+// Set ctx parameters to global command line flags.
+static void mp_sws_update_from_cmdline(struct mp_sws_context *ctx)
+{
+ m_config_cache_update(ctx->opts_cache);
+ struct sws_opts *opts = ctx->opts_cache->opts;
+
+ sws_freeFilter(ctx->src_filter);
+ ctx->src_filter = sws_getDefaultFilter(opts->lum_gblur, opts->chr_gblur,
+ opts->lum_sharpen, opts->chr_sharpen,
+ opts->chr_hshift, opts->chr_vshift, 0);
+ ctx->force_reload = true;
+
+ ctx->flags = SWS_PRINT_INFO;
+ ctx->flags |= opts->scaler;
+ if (!opts->fast)
+ ctx->flags |= mp_sws_hq_flags;
+ if (opts->bitexact)
+ ctx->flags |= SWS_BITEXACT;
+
+ ctx->allow_zimg = opts->zimg;
+}
+
+bool mp_sws_supported_format(int imgfmt)
+{
+ enum AVPixelFormat av_format = imgfmt2pixfmt(imgfmt);
+
+ return av_format != AV_PIX_FMT_NONE && sws_isSupportedInput(av_format)
+ && sws_isSupportedOutput(av_format);
+}
+
+#if HAVE_ZIMG
+static bool allow_zimg(struct mp_sws_context *ctx)
+{
+ return ctx->force_scaler == MP_SWS_ZIMG ||
+ (ctx->force_scaler == MP_SWS_AUTO && ctx->allow_zimg);
+}
+#endif
+
+static bool allow_sws(struct mp_sws_context *ctx)
+{
+ return ctx->force_scaler == MP_SWS_SWS || ctx->force_scaler == MP_SWS_AUTO;
+}
+
+bool mp_sws_supports_formats(struct mp_sws_context *ctx,
+ int imgfmt_out, int imgfmt_in)
+{
+#if HAVE_ZIMG
+ if (allow_zimg(ctx)) {
+ if (mp_zimg_supports_in_format(imgfmt_in) &&
+ mp_zimg_supports_out_format(imgfmt_out))
+ return true;
+ }
+#endif
+
+ return allow_sws(ctx) &&
+ sws_isSupportedInput(imgfmt2pixfmt(imgfmt_in)) &&
+ sws_isSupportedOutput(imgfmt2pixfmt(imgfmt_out));
+}
+
+static int mp_csp_to_sws_colorspace(enum mp_csp csp)
+{
+ // The SWS_CS_* macros are just convenience redefinitions of the
+ // AVCOL_SPC_* macros, inside swscale.h.
+ return mp_csp_to_avcol_spc(csp);
+}
+
+static bool cache_valid(struct mp_sws_context *ctx)
+{
+ struct mp_sws_context *old = ctx->cached;
+ if (ctx->force_reload)
+ return false;
+ return mp_image_params_equal(&ctx->src, &old->src) &&
+ mp_image_params_equal(&ctx->dst, &old->dst) &&
+ ctx->flags == old->flags &&
+ ctx->allow_zimg == old->allow_zimg &&
+ ctx->force_scaler == old->force_scaler &&
+ (!ctx->opts_cache || !m_config_cache_update(ctx->opts_cache));
+}
+
+static void free_mp_sws(void *p)
+{
+ struct mp_sws_context *ctx = p;
+ sws_freeContext(ctx->sws);
+ sws_freeFilter(ctx->src_filter);
+ sws_freeFilter(ctx->dst_filter);
+ TA_FREEP(&ctx->aligned_src);
+ TA_FREEP(&ctx->aligned_dst);
+}
+
+// You're supposed to set your scaling parameters on the returned context.
+// Free the context with talloc_free().
+struct mp_sws_context *mp_sws_alloc(void *talloc_ctx)
+{
+ struct mp_sws_context *ctx = talloc_ptrtype(talloc_ctx, ctx);
+ *ctx = (struct mp_sws_context) {
+ .log = mp_null_log,
+ .flags = SWS_BILINEAR,
+ .force_reload = true,
+ .params = {SWS_PARAM_DEFAULT, SWS_PARAM_DEFAULT},
+ .cached = talloc_zero(ctx, struct mp_sws_context),
+ };
+ talloc_set_destructor(ctx, free_mp_sws);
+
+#if HAVE_ZIMG
+ ctx->zimg = mp_zimg_alloc();
+ talloc_steal(ctx, ctx->zimg);
+#endif
+
+ return ctx;
+}
+
+// Enable auto-update of parameters from command line. Don't try to set custom
+// options (other than possibly .src/.dst), because they might be overwritten
+// if the user changes any options.
+void mp_sws_enable_cmdline_opts(struct mp_sws_context *ctx, struct mpv_global *g)
+{
+ // Should only ever be NULL for tests.
+ if (!g)
+ return;
+ if (ctx->opts_cache)
+ return;
+
+ ctx->opts_cache = m_config_cache_alloc(ctx, g, &sws_conf);
+ ctx->force_reload = true;
+ mp_sws_update_from_cmdline(ctx);
+
+#if HAVE_ZIMG
+ mp_zimg_enable_cmdline_opts(ctx->zimg, g);
+#endif
+}
+
+// Reinitialize (if needed) - return error code.
+// Optional, but possibly useful to avoid having to handle mp_sws_scale errors.
+int mp_sws_reinit(struct mp_sws_context *ctx)
+{
+ struct mp_image_params src = ctx->src;
+ struct mp_image_params dst = ctx->dst;
+
+ if (cache_valid(ctx))
+ return 0;
+
+ if (ctx->opts_cache)
+ mp_sws_update_from_cmdline(ctx);
+
+ sws_freeContext(ctx->sws);
+ ctx->sws = NULL;
+ ctx->zimg_ok = false;
+ TA_FREEP(&ctx->aligned_src);
+ TA_FREEP(&ctx->aligned_dst);
+
+#if HAVE_ZIMG
+ if (allow_zimg(ctx)) {
+ ctx->zimg->log = ctx->log;
+ ctx->zimg->src = src;
+ ctx->zimg->dst = dst;
+ if (ctx->zimg_opts)
+ ctx->zimg->opts = *ctx->zimg_opts;
+ if (mp_zimg_config(ctx->zimg)) {
+ ctx->zimg_ok = true;
+ MP_VERBOSE(ctx, "Using zimg.\n");
+ goto success;
+ }
+ MP_WARN(ctx, "Not using zimg, falling back to swscale.\n");
+ }
+#endif
+
+ if (!allow_sws(ctx)) {
+ MP_ERR(ctx, "No scaler.\n");
+ return -1;
+ }
+
+ ctx->sws = sws_alloc_context();
+ if (!ctx->sws)
+ return -1;
+
+ mp_image_params_guess_csp(&src); // sanitize colorspace/colorlevels
+ mp_image_params_guess_csp(&dst);
+
+ enum AVPixelFormat s_fmt = imgfmt2pixfmt(src.imgfmt);
+ if (s_fmt == AV_PIX_FMT_NONE || sws_isSupportedInput(s_fmt) < 1) {
+ MP_ERR(ctx, "Input image format %s not supported by libswscale.\n",
+ mp_imgfmt_to_name(src.imgfmt));
+ return -1;
+ }
+
+ enum AVPixelFormat d_fmt = imgfmt2pixfmt(dst.imgfmt);
+ if (d_fmt == AV_PIX_FMT_NONE || sws_isSupportedOutput(d_fmt) < 1) {
+ MP_ERR(ctx, "Output image format %s not supported by libswscale.\n",
+ mp_imgfmt_to_name(dst.imgfmt));
+ return -1;
+ }
+
+ int s_csp = mp_csp_to_sws_colorspace(src.color.space);
+ int s_range = src.color.levels == MP_CSP_LEVELS_PC;
+
+ int d_csp = mp_csp_to_sws_colorspace(dst.color.space);
+ int d_range = dst.color.levels == MP_CSP_LEVELS_PC;
+
+ av_opt_set_int(ctx->sws, "sws_flags", ctx->flags, 0);
+
+ av_opt_set_int(ctx->sws, "srcw", src.w, 0);
+ av_opt_set_int(ctx->sws, "srch", src.h, 0);
+ av_opt_set_int(ctx->sws, "src_format", s_fmt, 0);
+
+ av_opt_set_int(ctx->sws, "dstw", dst.w, 0);
+ av_opt_set_int(ctx->sws, "dsth", dst.h, 0);
+ av_opt_set_int(ctx->sws, "dst_format", d_fmt, 0);
+
+ av_opt_set_double(ctx->sws, "param0", ctx->params[0], 0);
+ av_opt_set_double(ctx->sws, "param1", ctx->params[1], 0);
+
+ int cr_src = mp_chroma_location_to_av(src.chroma_location);
+ int cr_dst = mp_chroma_location_to_av(dst.chroma_location);
+ int cr_xpos, cr_ypos;
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
+ if (av_chroma_location_enum_to_pos(&cr_xpos, &cr_ypos, cr_src) >= 0) {
+ av_opt_set_int(ctx->sws, "src_h_chr_pos", cr_xpos, 0);
+ av_opt_set_int(ctx->sws, "src_v_chr_pos", cr_ypos, 0);
+ }
+ if (av_chroma_location_enum_to_pos(&cr_xpos, &cr_ypos, cr_dst) >= 0) {
+ av_opt_set_int(ctx->sws, "dst_h_chr_pos", cr_xpos, 0);
+ av_opt_set_int(ctx->sws, "dst_v_chr_pos", cr_ypos, 0);
+ }
+#else
+ if (avcodec_enum_to_chroma_pos(&cr_xpos, &cr_ypos, cr_src) >= 0) {
+ av_opt_set_int(ctx->sws, "src_h_chr_pos", cr_xpos, 0);
+ av_opt_set_int(ctx->sws, "src_v_chr_pos", cr_ypos, 0);
+ }
+ if (avcodec_enum_to_chroma_pos(&cr_xpos, &cr_ypos, cr_dst) >= 0) {
+ av_opt_set_int(ctx->sws, "dst_h_chr_pos", cr_xpos, 0);
+ av_opt_set_int(ctx->sws, "dst_v_chr_pos", cr_ypos, 0);
+ }
+#endif
+
+ // This can fail even with normal operation, e.g. if a conversion path
+ // simply does not support these settings.
+ int r =
+ sws_setColorspaceDetails(ctx->sws, sws_getCoefficients(s_csp), s_range,
+ sws_getCoefficients(d_csp), d_range,
+ 0, 1 << 16, 1 << 16);
+ ctx->supports_csp = r >= 0;
+
+ if (sws_init_context(ctx->sws, ctx->src_filter, ctx->dst_filter) < 0)
+ return -1;
+
+#if HAVE_ZIMG
+success:
+#endif
+
+ ctx->force_reload = false;
+ *ctx->cached = *ctx;
+ return 1;
+}
+
+static struct mp_image *check_alignment(struct mp_log *log,
+ struct mp_image **alloc,
+ struct mp_image *img)
+{
+ // It's completely unclear which alignment libswscale wants (for performance)
+ // or requires (for avoiding crashes and memory corruption).
+ // Is it av_cpu_max_align()? Is it the hardcoded AVFrame "default" of 32
+ // in get_video_buffer()? Is it whatever avcodec_align_dimensions2()
+ // determines? It's like you can't win if you try to prevent libswscale from
+ // corrupting memory...
+ // So use 32, a value that has been experimentally determined to be safe,
+ // and which in most cases is not larger than decoder output. It is smaller
+ // or equal to what most image allocators in mpv/ffmpeg use.
+ size_t align = 32;
+ assert(align <= MP_IMAGE_BYTE_ALIGN); // or mp_image_alloc will not cut it
+
+ bool is_aligned = true;
+ for (int p = 0; p < img->num_planes; p++) {
+ is_aligned &= MP_IS_ALIGNED((uintptr_t)img->planes[p], align);
+ is_aligned &= MP_IS_ALIGNED(labs(img->stride[p]), align);
+ }
+
+ if (is_aligned)
+ return img;
+
+ if (!*alloc) {
+ mp_verbose(log, "unaligned libswscale parameter; using slow copy.\n");
+ *alloc = mp_image_alloc(img->imgfmt, img->w, img->h);
+ if (!*alloc)
+ return NULL;
+ }
+
+ mp_image_copy_attributes(*alloc, img);
+ return *alloc;
+}
+
+// Scale from src to dst - if src/dst have different parameters from previous
+// calls, the context is reinitialized. Return error code. (It can fail if
+// reinitialization was necessary, and swscale returned an error.)
+int mp_sws_scale(struct mp_sws_context *ctx, struct mp_image *dst,
+ struct mp_image *src)
+{
+ ctx->src = src->params;
+ ctx->dst = dst->params;
+
+ int r = mp_sws_reinit(ctx);
+ if (r < 0) {
+ MP_ERR(ctx, "libswscale initialization failed.\n");
+ return r;
+ }
+
+#if HAVE_ZIMG
+ if (ctx->zimg_ok)
+ return mp_zimg_convert(ctx->zimg, dst, src) ? 0 : -1;
+#endif
+
+ if (src->params.color.space == MP_CSP_XYZ && dst->params.color.space != MP_CSP_XYZ) {
+ // swsscale has hardcoded gamma 2.2 internally and 2.6 for XYZ
+ dst->params.color.gamma = MP_CSP_TRC_GAMMA22;
+ // and sRGB primaries...
+ dst->params.color.primaries = MP_CSP_PRIM_BT_709;
+ // it doesn't adjust white point though, but it is not worth to support
+ // this case. It would require custom prim with equal energy white point
+ // and sRGB primaries.
+ }
+
+ struct mp_image *a_src = check_alignment(ctx->log, &ctx->aligned_src, src);
+ struct mp_image *a_dst = check_alignment(ctx->log, &ctx->aligned_dst, dst);
+ if (!a_src || !a_dst) {
+ MP_ERR(ctx, "image allocation failed.\n");
+ return -1;
+ }
+
+ if (a_src != src)
+ mp_image_copy(a_src, src);
+
+ sws_scale(ctx->sws, (const uint8_t *const *) a_src->planes, a_src->stride,
+ 0, a_src->h, a_dst->planes, a_dst->stride);
+
+ if (a_dst != dst)
+ mp_image_copy(dst, a_dst);
+
+ return 0;
+}
+
+int mp_image_swscale(struct mp_image *dst, struct mp_image *src,
+ int my_sws_flags)
+{
+ struct mp_sws_context *ctx = mp_sws_alloc(NULL);
+ ctx->flags = my_sws_flags;
+ int res = mp_sws_scale(ctx, dst, src);
+ talloc_free(ctx);
+ return res;
+}
+
+int mp_image_sw_blur_scale(struct mp_image *dst, struct mp_image *src,
+ float gblur)
+{
+ struct mp_sws_context *ctx = mp_sws_alloc(NULL);
+ ctx->flags = SWS_LANCZOS | mp_sws_hq_flags;
+ ctx->src_filter = sws_getDefaultFilter(gblur, gblur, 0, 0, 0, 0, 0);
+ ctx->force_reload = true;
+ int res = mp_sws_scale(ctx, dst, src);
+ talloc_free(ctx);
+ return res;
+}
+
+static const int endian_swaps[][2] = {
+#if BYTE_ORDER == LITTLE_ENDIAN
+#if defined(AV_PIX_FMT_YA16) && defined(AV_PIX_FMT_RGBA64)
+ {AV_PIX_FMT_YA16BE, AV_PIX_FMT_YA16LE},
+ {AV_PIX_FMT_RGBA64BE, AV_PIX_FMT_RGBA64LE},
+ {AV_PIX_FMT_GRAY16BE, AV_PIX_FMT_GRAY16LE},
+ {AV_PIX_FMT_RGB48BE, AV_PIX_FMT_RGB48LE},
+#endif
+#endif
+ {AV_PIX_FMT_NONE, AV_PIX_FMT_NONE}
+};
+
+// Swap _some_ non-native endian formats to native. We do this specifically
+// for pixel formats used by PNG, to avoid going through libswscale, which
+// might reduce the effective bit depth in some cases.
+struct mp_image *mp_img_swap_to_native(struct mp_image *img)
+{
+ int avfmt = imgfmt2pixfmt(img->imgfmt);
+ int to = AV_PIX_FMT_NONE;
+ for (int n = 0; endian_swaps[n][0] != AV_PIX_FMT_NONE; n++) {
+ if (endian_swaps[n][0] == avfmt)
+ to = endian_swaps[n][1];
+ }
+ if (to == AV_PIX_FMT_NONE || !mp_image_make_writeable(img))
+ return img;
+ int elems = img->fmt.bpp[0] / 8 / 2 * img->w;
+ for (int y = 0; y < img->h; y++) {
+ uint16_t *p = (uint16_t *)(img->planes[0] + y * img->stride[0]);
+ for (int i = 0; i < elems; i++)
+ p[i] = av_be2ne16(p[i]);
+ }
+ mp_image_setfmt(img, pixfmt2imgfmt(to));
+ return img;
+}
+
+// vim: ts=4 sw=4 et tw=80
diff --git a/video/sws_utils.h b/video/sws_utils.h
new file mode 100644
index 0000000..24bec07
--- /dev/null
+++ b/video/sws_utils.h
@@ -0,0 +1,82 @@
+#ifndef MPLAYER_SWS_UTILS_H
+#define MPLAYER_SWS_UTILS_H
+
+#include <stdbool.h>
+
+#include "mp_image.h"
+
+struct mp_image;
+struct mpv_global;
+
+// libswscale currently requires 16 bytes alignment for row pointers and
+// strides. Otherwise, it will print warnings and use slow codepaths.
+// Guaranteed to be a power of 2 and > 1.
+#define SWS_MIN_BYTE_ALIGN MP_IMAGE_BYTE_ALIGN
+
+extern const int mp_sws_fast_flags;
+
+bool mp_sws_supported_format(int imgfmt);
+
+int mp_image_swscale(struct mp_image *dst, struct mp_image *src,
+ int my_sws_flags);
+
+int mp_image_sw_blur_scale(struct mp_image *dst, struct mp_image *src,
+ float gblur);
+
+enum mp_sws_scaler {
+ MP_SWS_AUTO = 0, // use command line
+ MP_SWS_SWS,
+ MP_SWS_ZIMG,
+};
+
+struct mp_sws_context {
+ // Can be set for verbose error printing.
+ struct mp_log *log;
+ // User configuration. These can be changed freely, at any time.
+ // mp_sws_scale() will handle the changes transparently.
+ int flags;
+ bool allow_zimg; // use zimg if available (ignores filters and all)
+ bool force_reload;
+ // These are also implicitly set by mp_sws_scale(), and thus optional.
+ // Setting them before that call makes sense when using mp_sws_reinit().
+ struct mp_image_params src, dst;
+
+ // This is unfortunately a hack: bypass command line choice
+ enum mp_sws_scaler force_scaler;
+
+ // If zimg is used. Need to manually invalidate cache (set force_reload).
+ // Conflicts with enabling command line opts.
+ struct zimg_opts *zimg_opts;
+
+ // Changing these requires setting force_reload=true.
+ // By default, they are NULL.
+ // Freeing the mp_sws_context will deallocate these if set.
+ struct SwsFilter *src_filter, *dst_filter;
+ double params[2];
+
+ // Cached context (if any)
+ struct SwsContext *sws;
+ bool supports_csp;
+
+ // Private.
+ struct m_config_cache *opts_cache;
+ struct mp_sws_context *cached; // contains parameters for which sws is valid
+ struct mp_zimg_context *zimg;
+ bool zimg_ok;
+ struct mp_image *aligned_src, *aligned_dst;
+};
+
+struct mp_sws_context *mp_sws_alloc(void *talloc_ctx);
+void mp_sws_enable_cmdline_opts(struct mp_sws_context *ctx, struct mpv_global *g);
+int mp_sws_reinit(struct mp_sws_context *ctx);
+int mp_sws_scale(struct mp_sws_context *ctx, struct mp_image *dst,
+ struct mp_image *src);
+
+bool mp_sws_supports_formats(struct mp_sws_context *ctx,
+ int imgfmt_out, int imgfmt_in);
+
+struct mp_image *mp_img_swap_to_native(struct mp_image *img);
+
+#endif /* MP_SWS_UTILS_H */
+
+// vim: ts=4 sw=4 et tw=80
diff --git a/video/vaapi.c b/video/vaapi.c
new file mode 100644
index 0000000..08248a7
--- /dev/null
+++ b/video/vaapi.c
@@ -0,0 +1,288 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "config.h"
+
+#include "vaapi.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "osdep/threads.h"
+#include "mp_image.h"
+#include "img_format.h"
+#include "mp_image_pool.h"
+#include "options/m_config.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+
+struct vaapi_opts {
+ char *path;
+};
+
+#define OPT_BASE_STRUCT struct vaapi_opts
+const struct m_sub_options vaapi_conf = {
+ .opts = (const struct m_option[]) {
+ {"device", OPT_STRING(path)},
+ {0},
+ },
+ .defaults = &(const struct vaapi_opts) {
+ .path = "/dev/dri/renderD128",
+ },
+ .size = sizeof(struct vaapi_opts),
+};
+
+int va_get_colorspace_flag(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_BT_601: return VA_SRC_BT601;
+ case MP_CSP_BT_709: return VA_SRC_BT709;
+ case MP_CSP_SMPTE_240M: return VA_SRC_SMPTE_240;
+ }
+ return 0;
+}
+
+static void va_message_callback(void *context, const char *msg, int mp_level)
+{
+ struct mp_vaapi_ctx *res = context;
+ mp_msg(res->log, mp_level, "libva: %s", msg);
+}
+
+static void va_error_callback(void *context, const char *msg)
+{
+ va_message_callback(context, msg, MSGL_ERR);
+}
+
+static void va_info_callback(void *context, const char *msg)
+{
+ va_message_callback(context, msg, MSGL_DEBUG);
+}
+
+static void free_device_ref(struct AVHWDeviceContext *hwctx)
+{
+ struct mp_vaapi_ctx *ctx = hwctx->user_opaque;
+
+ if (ctx->display)
+ vaTerminate(ctx->display);
+
+ if (ctx->destroy_native_ctx)
+ ctx->destroy_native_ctx(ctx->native_ctx);
+
+ talloc_free(ctx);
+}
+
+struct mp_vaapi_ctx *va_initialize(VADisplay *display, struct mp_log *plog,
+ bool probing)
+{
+ AVBufferRef *avref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
+ if (!avref)
+ return NULL;
+
+ AVHWDeviceContext *hwctx = (void *)avref->data;
+ AVVAAPIDeviceContext *vactx = hwctx->hwctx;
+
+ struct mp_vaapi_ctx *res = talloc_ptrtype(NULL, res);
+ *res = (struct mp_vaapi_ctx) {
+ .log = mp_log_new(res, plog, "/vaapi"),
+ .display = display,
+ .av_device_ref = avref,
+ .hwctx = {
+ .av_device_ref = avref,
+ },
+ };
+
+ hwctx->free = free_device_ref;
+ hwctx->user_opaque = res;
+
+ vaSetErrorCallback(display, va_error_callback, res);
+ vaSetInfoCallback(display, va_info_callback, res);
+
+ int major, minor;
+ int status = vaInitialize(display, &major, &minor);
+ if (status != VA_STATUS_SUCCESS) {
+ if (!probing)
+ MP_ERR(res, "Failed to initialize VAAPI: %s\n", vaErrorStr(status));
+ goto error;
+ }
+ MP_VERBOSE(res, "Initialized VAAPI: version %d.%d\n", major, minor);
+
+ vactx->display = res->display;
+
+ if (av_hwdevice_ctx_init(res->av_device_ref) < 0)
+ goto error;
+
+ return res;
+
+error:
+ res->display = NULL; // do not vaTerminate this
+ va_destroy(res);
+ return NULL;
+}
+
+// Undo va_initialize, and close the VADisplay.
+void va_destroy(struct mp_vaapi_ctx *ctx)
+{
+ if (!ctx)
+ return;
+
+ AVBufferRef *ref = ctx->av_device_ref;
+ av_buffer_unref(&ref); // frees ctx as well
+}
+
+VASurfaceID va_surface_id(struct mp_image *mpi)
+{
+ return mpi && mpi->imgfmt == IMGFMT_VAAPI ?
+ (VASurfaceID)(uintptr_t)mpi->planes[3] : VA_INVALID_ID;
+}
+
+static bool is_emulated(struct AVBufferRef *hw_device_ctx)
+{
+ AVHWDeviceContext *hwctx = (void *)hw_device_ctx->data;
+ AVVAAPIDeviceContext *vactx = hwctx->hwctx;
+
+ const char *s = vaQueryVendorString(vactx->display);
+ return s && strstr(s, "VDPAU backend");
+}
+
+
+bool va_guess_if_emulated(struct mp_vaapi_ctx *ctx)
+{
+ return is_emulated(ctx->av_device_ref);
+}
+
+struct va_native_display {
+ void (*create)(VADisplay **out_display, void **out_native_ctx,
+ const char *path);
+ void (*destroy)(void *native_ctx);
+};
+
+#if HAVE_VAAPI_X11
+#include <X11/Xlib.h>
+#include <va/va_x11.h>
+
+static void x11_destroy(void *native_ctx)
+{
+ XCloseDisplay(native_ctx);
+}
+
+static void x11_create(VADisplay **out_display, void **out_native_ctx,
+ const char *path)
+{
+ void *native_display = XOpenDisplay(NULL);
+ if (!native_display)
+ return;
+ *out_display = vaGetDisplay(native_display);
+ if (*out_display) {
+ *out_native_ctx = native_display;
+ } else {
+ XCloseDisplay(native_display);
+ }
+}
+
+static const struct va_native_display disp_x11 = {
+ .create = x11_create,
+ .destroy = x11_destroy,
+};
+#endif
+
+#if HAVE_VAAPI_DRM
+#include <unistd.h>
+#include <fcntl.h>
+#include <va/va_drm.h>
+
+struct va_native_display_drm {
+ int drm_fd;
+};
+
+static void drm_destroy(void *native_ctx)
+{
+ struct va_native_display_drm *ctx = native_ctx;
+ close(ctx->drm_fd);
+ talloc_free(ctx);
+}
+
+static void drm_create(VADisplay **out_display, void **out_native_ctx,
+ const char *path)
+{
+ int drm_fd = open(path, O_RDWR);
+ if (drm_fd < 0)
+ return;
+
+ struct va_native_display_drm *ctx = talloc_ptrtype(NULL, ctx);
+ ctx->drm_fd = drm_fd;
+ *out_display = vaGetDisplayDRM(drm_fd);
+ if (*out_display) {
+ *out_native_ctx = ctx;
+ return;
+ }
+
+ close(drm_fd);
+ talloc_free(ctx);
+}
+
+static const struct va_native_display disp_drm = {
+ .create = drm_create,
+ .destroy = drm_destroy,
+};
+#endif
+
+static const struct va_native_display *const native_displays[] = {
+#if HAVE_VAAPI_DRM
+ &disp_drm,
+#endif
+#if HAVE_VAAPI_X11
+ &disp_x11,
+#endif
+ NULL
+};
+
+static struct AVBufferRef *va_create_standalone(struct mpv_global *global,
+ struct mp_log *log, struct hwcontext_create_dev_params *params)
+{
+ struct AVBufferRef *ret = NULL;
+ struct vaapi_opts *opts = mp_get_config_group(NULL, global, &vaapi_conf);
+
+ for (int n = 0; native_displays[n]; n++) {
+ VADisplay *display = NULL;
+ void *native_ctx = NULL;
+ native_displays[n]->create(&display, &native_ctx, opts->path);
+ if (display) {
+ struct mp_vaapi_ctx *ctx =
+ va_initialize(display, log, params->probing);
+ if (!ctx) {
+ vaTerminate(display);
+ native_displays[n]->destroy(native_ctx);
+ goto end;
+ }
+ ctx->native_ctx = native_ctx;
+ ctx->destroy_native_ctx = native_displays[n]->destroy;
+ ret = ctx->hwctx.av_device_ref;
+ goto end;
+ }
+ }
+
+end:
+ talloc_free(opts);
+ return ret;
+}
+
+const struct hwcontext_fns hwcontext_fns_vaapi = {
+ .av_hwdevice_type = AV_HWDEVICE_TYPE_VAAPI,
+ .create_dev = va_create_standalone,
+ .is_emulated = is_emulated,
+};
diff --git a/video/vaapi.h b/video/vaapi.h
new file mode 100644
index 0000000..56235bc
--- /dev/null
+++ b/video/vaapi.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPV_VAAPI_H
+#define MPV_VAAPI_H
+
+#include <stdbool.h>
+#include <inttypes.h>
+#include <va/va.h>
+
+#include "mp_image.h"
+#include "hwdec.h"
+
+struct mp_vaapi_ctx {
+ struct mp_hwdec_ctx hwctx;
+ struct mp_log *log;
+ VADisplay display;
+ struct AVBufferRef *av_device_ref; // AVVAAPIDeviceContext*
+ // Internal, for va_create_standalone()
+ void *native_ctx;
+ void (*destroy_native_ctx)(void *native_ctx);
+};
+
+#define CHECK_VA_STATUS_LEVEL(ctx, msg, level) \
+ (status == VA_STATUS_SUCCESS ? true \
+ : (MP_MSG(ctx, level, "%s failed (%s)\n", msg, vaErrorStr(status)), false))
+
+#define CHECK_VA_STATUS(ctx, msg) \
+ CHECK_VA_STATUS_LEVEL(ctx, msg, MSGL_ERR)
+
+int va_get_colorspace_flag(enum mp_csp csp);
+
+struct mp_vaapi_ctx * va_initialize(VADisplay *display, struct mp_log *plog, bool probing);
+void va_destroy(struct mp_vaapi_ctx *ctx);
+
+VASurfaceID va_surface_id(struct mp_image *mpi);
+
+bool va_guess_if_emulated(struct mp_vaapi_ctx *ctx);
+
+#endif
diff --git a/video/vdpau.c b/video/vdpau.c
new file mode 100644
index 0000000..15985d6
--- /dev/null
+++ b/video/vdpau.c
@@ -0,0 +1,574 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vdpau.h>
+
+#include "vdpau.h"
+
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "video/out/x11_common.h"
+#include "img_format.h"
+#include "mp_image.h"
+#include "mp_image_pool.h"
+#include "vdpau_mixer.h"
+
+static void mark_vdpau_objects_uninitialized(struct mp_vdpau_ctx *ctx)
+{
+ for (int i = 0; i < MAX_VIDEO_SURFACES; i++) {
+ ctx->video_surfaces[i].surface = VDP_INVALID_HANDLE;
+ ctx->video_surfaces[i].osurface = VDP_INVALID_HANDLE;
+ ctx->video_surfaces[i].allocated = false;
+ }
+ ctx->vdp_device = VDP_INVALID_HANDLE;
+ ctx->preemption_obj = VDP_INVALID_HANDLE;
+}
+
+static void preemption_callback(VdpDevice device, void *context)
+{
+ struct mp_vdpau_ctx *ctx = context;
+
+ mp_mutex_lock(&ctx->preempt_lock);
+ ctx->is_preempted = true;
+ mp_mutex_unlock(&ctx->preempt_lock);
+}
+
+static int win_x11_init_vdpau_procs(struct mp_vdpau_ctx *ctx, bool probing)
+{
+ Display *x11 = ctx->x11;
+ VdpStatus vdp_st;
+
+ // Don't operate on ctx->vdp directly, so that even if init fails, ctx->vdp
+ // will have the function pointers from the previous successful init, and
+ // won't randomly make other code crash on calling NULL pointers.
+ struct vdp_functions vdp = {0};
+
+ if (!x11)
+ return -1;
+
+ struct vdp_function {
+ const int id;
+ int offset;
+ };
+
+ static const struct vdp_function vdp_func[] = {
+#define VDP_FUNCTION(_, macro_name, mp_name) {macro_name, offsetof(struct vdp_functions, mp_name)},
+#include "video/vdpau_functions.inc"
+#undef VDP_FUNCTION
+ {0, -1}
+ };
+
+ VdpGetProcAddress *get_proc_address;
+ vdp_st = vdp_device_create_x11(x11, DefaultScreen(x11), &ctx->vdp_device,
+ &get_proc_address);
+ if (vdp_st != VDP_STATUS_OK) {
+ if (ctx->is_preempted) {
+ MP_DBG(ctx, "Error calling vdp_device_create_x11 while preempted: %d\n",
+ vdp_st);
+ } else {
+ int lev = probing ? MSGL_V : MSGL_ERR;
+ mp_msg(ctx->log, lev, "Error when calling vdp_device_create_x11: %d\n",
+ vdp_st);
+ }
+ return -1;
+ }
+
+ for (const struct vdp_function *dsc = vdp_func; dsc->offset >= 0; dsc++) {
+ vdp_st = get_proc_address(ctx->vdp_device, dsc->id,
+ (void **)((char *)&vdp + dsc->offset));
+ if (vdp_st != VDP_STATUS_OK) {
+ MP_ERR(ctx, "Error when calling vdp_get_proc_address(function "
+ "id %d): %s\n", dsc->id,
+ vdp.get_error_string ? vdp.get_error_string(vdp_st) : "?");
+ return -1;
+ }
+ }
+
+ ctx->vdp = vdp;
+ ctx->get_proc_address = get_proc_address;
+
+ if (ctx->av_device_ref) {
+ AVHWDeviceContext *hwctx = (void *)ctx->av_device_ref->data;
+ AVVDPAUDeviceContext *vdctx = hwctx->hwctx;
+
+ vdctx->device = ctx->vdp_device;
+ vdctx->get_proc_address = ctx->get_proc_address;
+ }
+
+ vdp_st = vdp.output_surface_create(ctx->vdp_device, VDP_RGBA_FORMAT_B8G8R8A8,
+ 1, 1, &ctx->preemption_obj);
+ if (vdp_st != VDP_STATUS_OK) {
+ MP_ERR(ctx, "Could not create dummy object: %s",
+ vdp.get_error_string(vdp_st));
+ return -1;
+ }
+
+ vdp.preemption_callback_register(ctx->vdp_device, preemption_callback, ctx);
+ return 0;
+}
+
+static int handle_preemption(struct mp_vdpau_ctx *ctx)
+{
+ if (!ctx->is_preempted)
+ return 0;
+ mark_vdpau_objects_uninitialized(ctx);
+ if (!ctx->preemption_user_notified) {
+ MP_ERR(ctx, "Got display preemption notice! Will attempt to recover.\n");
+ ctx->preemption_user_notified = true;
+ }
+ /* Trying to initialize seems to be quite slow, so only try once a
+ * second to avoid using 100% CPU. */
+ if (ctx->last_preemption_retry_fail &&
+ mp_time_sec() - ctx->last_preemption_retry_fail < 1.0)
+ return -1;
+ if (win_x11_init_vdpau_procs(ctx, false) < 0) {
+ ctx->last_preemption_retry_fail = mp_time_sec();
+ return -1;
+ }
+ ctx->preemption_user_notified = false;
+ ctx->last_preemption_retry_fail = 0;
+ ctx->is_preempted = false;
+ ctx->preemption_counter++;
+ MP_INFO(ctx, "Recovered from display preemption.\n");
+ return 1;
+}
+
+// Check whether vdpau display preemption happened. The caller provides a
+// preemption counter, which contains the logical timestamp of the last
+// preemption handled by the caller. The counter can be 0 for init.
+// If counter is NULL, only ever return -1 or 1.
+// Return values:
+// -1: the display is currently preempted, and vdpau can't be used
+// 0: a preemption event happened, and the caller must recover
+// (*counter is updated, and a second call will report status ok)
+// 1: everything is fine, no preemption happened
+int mp_vdpau_handle_preemption(struct mp_vdpau_ctx *ctx, uint64_t *counter)
+{
+ int r = 1;
+ mp_mutex_lock(&ctx->preempt_lock);
+
+ const void *p[4] = {&(uint32_t){0}};
+ uint32_t stride[4] = {4};
+ VdpRect rc = {0};
+ ctx->vdp.output_surface_put_bits_native(ctx->preemption_obj, p, stride, &rc);
+
+ // First time init
+ if (counter && !*counter)
+ *counter = ctx->preemption_counter;
+
+ if (handle_preemption(ctx) < 0)
+ r = -1;
+
+ if (counter && r > 0 && *counter < ctx->preemption_counter) {
+ *counter = ctx->preemption_counter;
+ r = 0; // signal recovery after preemption
+ }
+
+ mp_mutex_unlock(&ctx->preempt_lock);
+ return r;
+}
+
+struct surface_ref {
+ struct mp_vdpau_ctx *ctx;
+ int index;
+};
+
+static void release_decoder_surface(void *ptr)
+{
+ struct surface_ref *r = ptr;
+ struct mp_vdpau_ctx *ctx = r->ctx;
+
+ mp_mutex_lock(&ctx->pool_lock);
+ assert(ctx->video_surfaces[r->index].in_use);
+ ctx->video_surfaces[r->index].in_use = false;
+ mp_mutex_unlock(&ctx->pool_lock);
+
+ talloc_free(r);
+}
+
+static struct mp_image *create_ref(struct mp_vdpau_ctx *ctx, int index)
+{
+ struct surface_entry *e = &ctx->video_surfaces[index];
+ assert(!e->in_use);
+ e->in_use = true;
+ e->age = ctx->age_counter++;
+ struct surface_ref *ref = talloc_ptrtype(NULL, ref);
+ *ref = (struct surface_ref){ctx, index};
+ struct mp_image *res =
+ mp_image_new_custom_ref(NULL, ref, release_decoder_surface);
+ if (res) {
+ mp_image_setfmt(res, e->rgb ? IMGFMT_VDPAU_OUTPUT : IMGFMT_VDPAU);
+ mp_image_set_size(res, e->w, e->h);
+ res->planes[0] = (void *)"dummy"; // must be non-NULL, otherwise arbitrary
+ res->planes[3] = (void *)(intptr_t)(e->rgb ? e->osurface : e->surface);
+ }
+ return res;
+}
+
+static struct mp_image *mp_vdpau_get_surface(struct mp_vdpau_ctx *ctx,
+ VdpChromaType chroma,
+ VdpRGBAFormat rgb_format,
+ bool rgb, int w, int h)
+{
+ struct vdp_functions *vdp = &ctx->vdp;
+ int surface_index = -1;
+ VdpStatus vdp_st;
+
+ if (rgb) {
+ chroma = (VdpChromaType)-1;
+ } else {
+ rgb_format = (VdpChromaType)-1;
+ }
+
+ mp_mutex_lock(&ctx->pool_lock);
+
+ // Destroy all unused surfaces that don't have matching parameters
+ for (int n = 0; n < MAX_VIDEO_SURFACES; n++) {
+ struct surface_entry *e = &ctx->video_surfaces[n];
+ if (!e->in_use && e->allocated) {
+ if (e->w != w || e->h != h || e->rgb != rgb ||
+ e->chroma != chroma || e->rgb_format != rgb_format)
+ {
+ if (e->rgb) {
+ vdp_st = vdp->output_surface_destroy(e->osurface);
+ } else {
+ vdp_st = vdp->video_surface_destroy(e->surface);
+ }
+ CHECK_VDP_WARNING(ctx, "Error when destroying surface");
+ e->surface = e->osurface = VDP_INVALID_HANDLE;
+ e->allocated = false;
+ }
+ }
+ }
+
+ // Try to find an existing unused surface
+ for (int n = 0; n < MAX_VIDEO_SURFACES; n++) {
+ struct surface_entry *e = &ctx->video_surfaces[n];
+ if (!e->in_use && e->allocated) {
+ assert(e->w == w && e->h == h);
+ assert(e->chroma == chroma);
+ assert(e->rgb_format == rgb_format);
+ assert(e->rgb == rgb);
+ if (surface_index >= 0) {
+ struct surface_entry *other = &ctx->video_surfaces[surface_index];
+ if (other->age < e->age)
+ continue;
+ }
+ surface_index = n;
+ }
+ }
+
+ if (surface_index >= 0)
+ goto done;
+
+ // Allocate new surface
+ for (int n = 0; n < MAX_VIDEO_SURFACES; n++) {
+ struct surface_entry *e = &ctx->video_surfaces[n];
+ if (!e->in_use) {
+ assert(e->surface == VDP_INVALID_HANDLE);
+ assert(e->osurface == VDP_INVALID_HANDLE);
+ assert(!e->allocated);
+ e->chroma = chroma;
+ e->rgb_format = rgb_format;
+ e->rgb = rgb;
+ e->w = w;
+ e->h = h;
+ if (mp_vdpau_handle_preemption(ctx, NULL) >= 0) {
+ if (rgb) {
+ vdp_st = vdp->output_surface_create(ctx->vdp_device, rgb_format,
+ w, h, &e->osurface);
+ e->allocated = e->osurface != VDP_INVALID_HANDLE;
+ } else {
+ vdp_st = vdp->video_surface_create(ctx->vdp_device, chroma,
+ w, h, &e->surface);
+ e->allocated = e->surface != VDP_INVALID_HANDLE;
+ }
+ CHECK_VDP_WARNING(ctx, "Error when allocating surface");
+ } else {
+ e->allocated = false;
+ e->osurface = VDP_INVALID_HANDLE;
+ e->surface = VDP_INVALID_HANDLE;
+ }
+ surface_index = n;
+ goto done;
+ }
+ }
+
+done: ;
+ struct mp_image *mpi = NULL;
+ if (surface_index >= 0)
+ mpi = create_ref(ctx, surface_index);
+
+ mp_mutex_unlock(&ctx->pool_lock);
+
+ if (!mpi)
+ MP_ERR(ctx, "no surfaces available in mp_vdpau_get_video_surface\n");
+ return mpi;
+}
+
+struct mp_image *mp_vdpau_get_video_surface(struct mp_vdpau_ctx *ctx,
+ VdpChromaType chroma, int w, int h)
+{
+ return mp_vdpau_get_surface(ctx, chroma, 0, false, w, h);
+}
+
+static void free_device_ref(struct AVHWDeviceContext *hwctx)
+{
+ struct mp_vdpau_ctx *ctx = hwctx->user_opaque;
+
+ struct vdp_functions *vdp = &ctx->vdp;
+ VdpStatus vdp_st;
+
+ for (int i = 0; i < MAX_VIDEO_SURFACES; i++) {
+ // can't hold references past context lifetime
+ assert(!ctx->video_surfaces[i].in_use);
+ if (ctx->video_surfaces[i].surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->video_surface_destroy(ctx->video_surfaces[i].surface);
+ CHECK_VDP_WARNING(ctx, "Error when calling vdp_video_surface_destroy");
+ }
+ if (ctx->video_surfaces[i].osurface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(ctx->video_surfaces[i].osurface);
+ CHECK_VDP_WARNING(ctx, "Error when calling vdp_output_surface_destroy");
+ }
+ }
+
+ if (ctx->preemption_obj != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(ctx->preemption_obj);
+ CHECK_VDP_WARNING(ctx, "Error when calling vdp_output_surface_destroy");
+ }
+
+ if (vdp->device_destroy && ctx->vdp_device != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->device_destroy(ctx->vdp_device);
+ CHECK_VDP_WARNING(ctx, "Error when calling vdp_device_destroy");
+ }
+
+ if (ctx->close_display)
+ XCloseDisplay(ctx->x11);
+
+ mp_mutex_destroy(&ctx->pool_lock);
+ mp_mutex_destroy(&ctx->preempt_lock);
+ talloc_free(ctx);
+}
+
+struct mp_vdpau_ctx *mp_vdpau_create_device_x11(struct mp_log *log, Display *x11,
+ bool probing)
+{
+ AVBufferRef *avref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VDPAU);
+ if (!avref)
+ return NULL;
+
+ AVHWDeviceContext *hwctx = (void *)avref->data;
+ AVVDPAUDeviceContext *vdctx = hwctx->hwctx;
+
+ struct mp_vdpau_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct mp_vdpau_ctx) {
+ .log = log,
+ .x11 = x11,
+ .preemption_counter = 1,
+ .av_device_ref = avref,
+ .hwctx = {
+ .av_device_ref = avref,
+ },
+ };
+ mp_mutex_init_type(&ctx->preempt_lock, MP_MUTEX_RECURSIVE);
+ mp_mutex_init(&ctx->pool_lock);
+
+ hwctx->free = free_device_ref;
+ hwctx->user_opaque = ctx;
+
+ mark_vdpau_objects_uninitialized(ctx);
+
+ if (win_x11_init_vdpau_procs(ctx, probing) < 0) {
+ mp_vdpau_destroy(ctx);
+ return NULL;
+ }
+
+ vdctx->device = ctx->vdp_device;
+ vdctx->get_proc_address = ctx->get_proc_address;
+
+ if (av_hwdevice_ctx_init(ctx->av_device_ref) < 0) {
+ mp_vdpau_destroy(ctx);
+ return NULL;
+ }
+
+ return ctx;
+}
+
+void mp_vdpau_destroy(struct mp_vdpau_ctx *ctx)
+{
+ if (!ctx)
+ return;
+
+ AVBufferRef *ref = ctx->av_device_ref;
+ av_buffer_unref(&ref); // frees ctx as well
+}
+
+bool mp_vdpau_get_format(int imgfmt, VdpChromaType *out_chroma_type,
+ VdpYCbCrFormat *out_pixel_format)
+{
+ VdpChromaType chroma = VDP_CHROMA_TYPE_420;
+ VdpYCbCrFormat ycbcr = (VdpYCbCrFormat)-1;
+
+ switch (imgfmt) {
+ case IMGFMT_420P:
+ ycbcr = VDP_YCBCR_FORMAT_YV12;
+ break;
+ case IMGFMT_NV12:
+ ycbcr = VDP_YCBCR_FORMAT_NV12;
+ break;
+ case IMGFMT_UYVY:
+ ycbcr = VDP_YCBCR_FORMAT_UYVY;
+ chroma = VDP_CHROMA_TYPE_422;
+ break;
+ case IMGFMT_VDPAU:
+ break;
+ default:
+ return false;
+ }
+
+ if (out_chroma_type)
+ *out_chroma_type = chroma;
+ if (out_pixel_format)
+ *out_pixel_format = ycbcr;
+ return true;
+}
+
+bool mp_vdpau_get_rgb_format(int imgfmt, VdpRGBAFormat *out_rgba_format)
+{
+ VdpRGBAFormat format = (VdpRGBAFormat)-1;
+
+ switch (imgfmt) {
+ case IMGFMT_BGRA:
+ format = VDP_RGBA_FORMAT_B8G8R8A8; break;
+ default:
+ return false;
+ }
+
+ if (out_rgba_format)
+ *out_rgba_format = format;
+ return true;
+}
+
+// Use mp_vdpau_get_video_surface, and upload mpi to it. Return NULL on failure.
+// If the image is already a vdpau video surface, just return a reference.
+struct mp_image *mp_vdpau_upload_video_surface(struct mp_vdpau_ctx *ctx,
+ struct mp_image *mpi)
+{
+ struct vdp_functions *vdp = &ctx->vdp;
+ VdpStatus vdp_st;
+
+ if (mpi->imgfmt == IMGFMT_VDPAU || mpi->imgfmt == IMGFMT_VDPAU_OUTPUT)
+ return mp_image_new_ref(mpi);
+
+ VdpChromaType chroma = (VdpChromaType)-1;
+ VdpYCbCrFormat ycbcr = (VdpYCbCrFormat)-1;
+ VdpRGBAFormat rgbafmt = (VdpRGBAFormat)-1;
+ bool rgb = !mp_vdpau_get_format(mpi->imgfmt, &chroma, &ycbcr);
+ if (rgb && !mp_vdpau_get_rgb_format(mpi->imgfmt, &rgbafmt))
+ return NULL;
+
+ struct mp_image *hwmpi =
+ mp_vdpau_get_surface(ctx, chroma, rgbafmt, rgb, mpi->w, mpi->h);
+ if (!hwmpi)
+ return NULL;
+
+ struct mp_image *src = mpi;
+ if (mpi->stride[0] < 0)
+ src = mp_image_new_copy(mpi); // unflips it when copying
+
+ if (hwmpi->imgfmt == IMGFMT_VDPAU) {
+ VdpVideoSurface surface = (intptr_t)hwmpi->planes[3];
+ const void *destdata[3] = {src->planes[0], src->planes[2], src->planes[1]};
+ if (src->imgfmt == IMGFMT_NV12)
+ destdata[1] = destdata[2];
+ vdp_st = vdp->video_surface_put_bits_y_cb_cr(surface,
+ ycbcr, destdata, src->stride);
+ } else {
+ VdpOutputSurface rgb_surface = (intptr_t)hwmpi->planes[3];
+ vdp_st = vdp->output_surface_put_bits_native(rgb_surface,
+ &(const void *){src->planes[0]},
+ &(uint32_t){src->stride[0]},
+ NULL);
+ }
+ CHECK_VDP_WARNING(ctx, "Error when uploading surface");
+
+ if (src != mpi)
+ talloc_free(src);
+
+ mp_image_copy_attributes(hwmpi, mpi);
+ return hwmpi;
+}
+
+bool mp_vdpau_guess_if_emulated(struct mp_vdpau_ctx *ctx)
+{
+ struct vdp_functions *vdp = &ctx->vdp;
+ VdpStatus vdp_st;
+ char const* info = NULL;
+ vdp_st = vdp->get_information_string(&info);
+ CHECK_VDP_WARNING(ctx, "Error when calling vdp_get_information_string");
+ return vdp_st == VDP_STATUS_OK && info && strstr(info, "VAAPI");
+}
+
+// (This clearly works only for contexts wrapped by our code.)
+struct mp_vdpau_ctx *mp_vdpau_get_ctx_from_av(AVBufferRef *hw_device_ctx)
+{
+ AVHWDeviceContext *hwctx = (void *)hw_device_ctx->data;
+
+ if (hwctx->free != free_device_ref)
+ return NULL; // not ours
+
+ return hwctx->user_opaque;
+}
+
+static bool is_emulated(struct AVBufferRef *hw_device_ctx)
+{
+ struct mp_vdpau_ctx *ctx = mp_vdpau_get_ctx_from_av(hw_device_ctx);
+ if (!ctx)
+ return false;
+
+ return mp_vdpau_guess_if_emulated(ctx);
+}
+
+static struct AVBufferRef *vdpau_create_standalone(struct mpv_global *global,
+ struct mp_log *log, struct hwcontext_create_dev_params *params)
+{
+ XInitThreads();
+
+ Display *display = XOpenDisplay(NULL);
+ if (!display)
+ return NULL;
+
+ struct mp_vdpau_ctx *vdp =
+ mp_vdpau_create_device_x11(log, display, params->probing);
+ if (!vdp) {
+ XCloseDisplay(display);
+ return NULL;
+ }
+
+ vdp->close_display = true;
+ return vdp->hwctx.av_device_ref;
+}
+
+const struct hwcontext_fns hwcontext_fns_vdpau = {
+ .av_hwdevice_type = AV_HWDEVICE_TYPE_VDPAU,
+ .create_dev = vdpau_create_standalone,
+ .is_emulated = is_emulated,
+};
diff --git a/video/vdpau.h b/video/vdpau.h
new file mode 100644
index 0000000..a30f478
--- /dev/null
+++ b/video/vdpau.h
@@ -0,0 +1,109 @@
+#ifndef MPV_VDPAU_H
+#define MPV_VDPAU_H
+
+#include <stdbool.h>
+#include <inttypes.h>
+
+#include <vdpau/vdpau.h>
+#include <vdpau/vdpau_x11.h>
+
+#include "common/msg.h"
+#include "hwdec.h"
+#include "osdep/threads.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define CHECK_VDP_ERROR_ST(ctx, message, statement) \
+ do { \
+ if (vdp_st != VDP_STATUS_OK) { \
+ MP_ERR(ctx, "%s: %s\n", message, vdp->get_error_string(vdp_st)); \
+ statement \
+ } \
+ } while (0)
+
+#define CHECK_VDP_ERROR(ctx, message) \
+ CHECK_VDP_ERROR_ST(ctx, message, return -1;)
+
+#define CHECK_VDP_ERROR_NORETURN(ctx, message) \
+ CHECK_VDP_ERROR_ST(ctx, message, ;)
+
+#define CHECK_VDP_WARNING(ctx, message) \
+ do { \
+ if (vdp_st != VDP_STATUS_OK) \
+ MP_WARN(ctx, "%s: %s\n", message, vdp->get_error_string(vdp_st)); \
+ } while (0)
+
+struct vdp_functions {
+#define VDP_FUNCTION(vdp_type, _, mp_name) vdp_type *mp_name;
+#include "video/vdpau_functions.inc"
+#undef VDP_FUNCTION
+};
+
+
+#define MAX_VIDEO_SURFACES 50
+
+// Shared state. Objects created from different VdpDevices are often (always?)
+// incompatible to each other, so all code must use a shared VdpDevice.
+struct mp_vdpau_ctx {
+ struct mp_log *log;
+ Display *x11;
+ bool close_display;
+
+ struct mp_hwdec_ctx hwctx;
+ struct AVBufferRef *av_device_ref;
+
+ // These are mostly immutable, except on preemption. We don't really care
+ // to synchronize the preemption case fully correctly, because it's an
+ // extremely obscure corner case, and basically a vdpau API design bug.
+ // What we do will sort-of work anyway (no memory errors are possible).
+ struct vdp_functions vdp;
+ VdpGetProcAddress *get_proc_address;
+ VdpDevice vdp_device;
+
+ mp_mutex preempt_lock;
+ bool is_preempted; // set to true during unavailability
+ uint64_t preemption_counter; // incremented after _restoring_
+ bool preemption_user_notified;
+ double last_preemption_retry_fail;
+ VdpOutputSurface preemption_obj; // dummy for reliable preempt. check
+
+ // Surface pool
+ mp_mutex pool_lock;
+ int64_t age_counter;
+ struct surface_entry {
+ VdpVideoSurface surface;
+ VdpOutputSurface osurface;
+ bool allocated;
+ int w, h;
+ VdpRGBAFormat rgb_format;
+ VdpChromaType chroma;
+ bool rgb;
+ bool in_use;
+ int64_t age;
+ } video_surfaces[MAX_VIDEO_SURFACES];
+};
+
+struct mp_vdpau_ctx *mp_vdpau_create_device_x11(struct mp_log *log, Display *x11,
+ bool probing);
+void mp_vdpau_destroy(struct mp_vdpau_ctx *ctx);
+
+int mp_vdpau_handle_preemption(struct mp_vdpau_ctx *ctx, uint64_t *counter);
+
+struct mp_image *mp_vdpau_get_video_surface(struct mp_vdpau_ctx *ctx,
+ VdpChromaType chroma, int w, int h);
+
+bool mp_vdpau_get_format(int imgfmt, VdpChromaType *out_chroma_type,
+ VdpYCbCrFormat *out_pixel_format);
+bool mp_vdpau_get_rgb_format(int imgfmt, VdpRGBAFormat *out_rgba_format);
+
+struct mp_image *mp_vdpau_upload_video_surface(struct mp_vdpau_ctx *ctx,
+ struct mp_image *mpi);
+
+struct mp_vdpau_ctx *mp_vdpau_get_ctx_from_av(struct AVBufferRef *hw_device_ctx);
+
+bool mp_vdpau_guess_if_emulated(struct mp_vdpau_ctx *ctx);
+
+#endif
diff --git a/video/vdpau_functions.inc b/video/vdpau_functions.inc
new file mode 100644
index 0000000..22c612c
--- /dev/null
+++ b/video/vdpau_functions.inc
@@ -0,0 +1,50 @@
+/* Lists the VDPAU functions used by MPV.
+ * First argument on each line is the VDPAU function type name,
+ * second is the macro name needed to get the function address,
+ * third is the name MPV uses for the function.
+ */
+
+VDP_FUNCTION(VdpGetErrorString, VDP_FUNC_ID_GET_ERROR_STRING, get_error_string)
+VDP_FUNCTION(VdpBitmapSurfaceCreate, VDP_FUNC_ID_BITMAP_SURFACE_CREATE, bitmap_surface_create)
+VDP_FUNCTION(VdpBitmapSurfaceDestroy, VDP_FUNC_ID_BITMAP_SURFACE_DESTROY, bitmap_surface_destroy)
+VDP_FUNCTION(VdpBitmapSurfacePutBitsNative, VDP_FUNC_ID_BITMAP_SURFACE_PUT_BITS_NATIVE, bitmap_surface_put_bits_native)
+VDP_FUNCTION(VdpBitmapSurfaceQueryCapabilities, VDP_FUNC_ID_BITMAP_SURFACE_QUERY_CAPABILITIES, bitmap_surface_query_capabilities)
+VDP_FUNCTION(VdpDecoderCreate, VDP_FUNC_ID_DECODER_CREATE, decoder_create)
+VDP_FUNCTION(VdpDecoderDestroy, VDP_FUNC_ID_DECODER_DESTROY, decoder_destroy)
+VDP_FUNCTION(VdpDecoderRender, VDP_FUNC_ID_DECODER_RENDER, decoder_render)
+VDP_FUNCTION(VdpDecoderQueryCapabilities, VDP_FUNC_ID_DECODER_QUERY_CAPABILITIES, decoder_query_capabilities)
+VDP_FUNCTION(VdpDeviceDestroy, VDP_FUNC_ID_DEVICE_DESTROY, device_destroy)
+VDP_FUNCTION(VdpGetInformationString, VDP_FUNC_ID_GET_INFORMATION_STRING, get_information_string)
+VDP_FUNCTION(VdpGenerateCSCMatrix, VDP_FUNC_ID_GENERATE_CSC_MATRIX, generate_csc_matrix)
+VDP_FUNCTION(VdpOutputSurfaceCreate, VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, output_surface_create)
+VDP_FUNCTION(VdpOutputSurfaceDestroy, VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, output_surface_destroy)
+VDP_FUNCTION(VdpOutputSurfaceGetBitsNative, VDP_FUNC_ID_OUTPUT_SURFACE_GET_BITS_NATIVE, output_surface_get_bits_native)
+VDP_FUNCTION(VdpOutputSurfacePutBitsIndexed, VDP_FUNC_ID_OUTPUT_SURFACE_PUT_BITS_INDEXED, output_surface_put_bits_indexed)
+VDP_FUNCTION(VdpOutputSurfacePutBitsNative, VDP_FUNC_ID_OUTPUT_SURFACE_PUT_BITS_NATIVE, output_surface_put_bits_native)
+VDP_FUNCTION(VdpOutputSurfaceRenderBitmapSurface, VDP_FUNC_ID_OUTPUT_SURFACE_RENDER_BITMAP_SURFACE, output_surface_render_bitmap_surface)
+VDP_FUNCTION(VdpOutputSurfaceRenderOutputSurface, VDP_FUNC_ID_OUTPUT_SURFACE_RENDER_OUTPUT_SURFACE, output_surface_render_output_surface)
+VDP_FUNCTION(VdpPreemptionCallbackRegister, VDP_FUNC_ID_PREEMPTION_CALLBACK_REGISTER, preemption_callback_register)
+VDP_FUNCTION(VdpPresentationQueueBlockUntilSurfaceIdle, VDP_FUNC_ID_PRESENTATION_QUEUE_BLOCK_UNTIL_SURFACE_IDLE, presentation_queue_block_until_surface_idle)
+VDP_FUNCTION(VdpPresentationQueueCreate, VDP_FUNC_ID_PRESENTATION_QUEUE_CREATE, presentation_queue_create)
+VDP_FUNCTION(VdpPresentationQueueDestroy, VDP_FUNC_ID_PRESENTATION_QUEUE_DESTROY, presentation_queue_destroy)
+VDP_FUNCTION(VdpPresentationQueueDisplay, VDP_FUNC_ID_PRESENTATION_QUEUE_DISPLAY, presentation_queue_display)
+VDP_FUNCTION(VdpPresentationQueueGetTime, VDP_FUNC_ID_PRESENTATION_QUEUE_GET_TIME, presentation_queue_get_time)
+VDP_FUNCTION(VdpPresentationQueueQuerySurfaceStatus, VDP_FUNC_ID_PRESENTATION_QUEUE_QUERY_SURFACE_STATUS, presentation_queue_query_surface_status)
+VDP_FUNCTION(VdpPresentationQueueSetBackgroundColor, VDP_FUNC_ID_PRESENTATION_QUEUE_SET_BACKGROUND_COLOR, presentation_queue_set_background_color)
+VDP_FUNCTION(VdpPresentationQueueGetBackgroundColor, VDP_FUNC_ID_PRESENTATION_QUEUE_GET_BACKGROUND_COLOR, presentation_queue_get_background_color)
+VDP_FUNCTION(VdpPresentationQueueTargetCreateX11, VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11, presentation_queue_target_create_x11)
+VDP_FUNCTION(VdpPresentationQueueTargetDestroy, VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_DESTROY, presentation_queue_target_destroy)
+VDP_FUNCTION(VdpVideoMixerCreate, VDP_FUNC_ID_VIDEO_MIXER_CREATE, video_mixer_create)
+VDP_FUNCTION(VdpVideoMixerDestroy, VDP_FUNC_ID_VIDEO_MIXER_DESTROY, video_mixer_destroy)
+VDP_FUNCTION(VdpVideoMixerQueryFeatureSupport, VDP_FUNC_ID_VIDEO_MIXER_QUERY_FEATURE_SUPPORT, video_mixer_query_feature_support)
+VDP_FUNCTION(VdpVideoMixerRender, VDP_FUNC_ID_VIDEO_MIXER_RENDER, video_mixer_render)
+VDP_FUNCTION(VdpVideoMixerSetAttributeValues, VDP_FUNC_ID_VIDEO_MIXER_SET_ATTRIBUTE_VALUES, video_mixer_set_attribute_values)
+VDP_FUNCTION(VdpVideoMixerSetFeatureEnables, VDP_FUNC_ID_VIDEO_MIXER_SET_FEATURE_ENABLES, video_mixer_set_feature_enables)
+VDP_FUNCTION(VdpVideoSurfaceCreate, VDP_FUNC_ID_VIDEO_SURFACE_CREATE, video_surface_create)
+VDP_FUNCTION(VdpVideoSurfaceDestroy, VDP_FUNC_ID_VIDEO_SURFACE_DESTROY, video_surface_destroy)
+VDP_FUNCTION(VdpVideoSurfacePutBitsYCbCr, VDP_FUNC_ID_VIDEO_SURFACE_PUT_BITS_Y_CB_CR, video_surface_put_bits_y_cb_cr)
+VDP_FUNCTION(VdpVideoSurfaceGetBitsYCbCr, VDP_FUNC_ID_VIDEO_SURFACE_GET_BITS_Y_CB_CR, video_surface_get_bits_y_cb_cr)
+VDP_FUNCTION(VdpVideoSurfaceGetParameters, VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, video_surface_get_parameters)
+VDP_FUNCTION(VdpVideoSurfaceQueryCapabilities, VDP_FUNC_ID_VIDEO_SURFACE_QUERY_CAPABILITIES, video_surface_query_capabilities)
+VDP_FUNCTION(VdpOutputSurfaceQueryCapabilities, VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, output_surface_query_capabilities)
+VDP_FUNCTION(VdpOutputSurfaceGetParameters, VDP_FUNC_ID_OUTPUT_SURFACE_GET_PARAMETERS, output_surface_get_parameters)
diff --git a/video/vdpau_mixer.c b/video/vdpau_mixer.c
new file mode 100644
index 0000000..b1aed70
--- /dev/null
+++ b/video/vdpau_mixer.c
@@ -0,0 +1,306 @@
+/*
+ * This file is part of mpv.
+ *
+ * Parts of video mixer creation code:
+ * Copyright (C) 2008 NVIDIA (Rajib Mahapatra <rmahapatra@nvidia.com>)
+ * Copyright (C) 2009 Uoti Urpala
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "vdpau_mixer.h"
+
+static void free_mixed_frame(void *arg)
+{
+ struct mp_vdpau_mixer_frame *frame = arg;
+ talloc_free(frame);
+}
+
+// This creates an image of format IMGFMT_VDPAU with a mp_vdpau_mixer_frame
+// struct. Use mp_vdpau_mixed_frame_get() to retrieve the struct and to
+// initialize it.
+// "base" is used only to set parameters, no image data is referenced.
+struct mp_image *mp_vdpau_mixed_frame_create(struct mp_image *base)
+{
+ assert(base->imgfmt == IMGFMT_VDPAU);
+
+ struct mp_vdpau_mixer_frame *frame =
+ talloc_zero(NULL, struct mp_vdpau_mixer_frame);
+ for (int n = 0; n < MP_VDP_HISTORY_FRAMES; n++)
+ frame->past[n] = frame->future[n] = VDP_INVALID_HANDLE;
+ frame->current = VDP_INVALID_HANDLE;
+ frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;
+
+ struct mp_image *mpi = mp_image_new_custom_ref(base, frame, free_mixed_frame);
+ if (mpi) {
+ mpi->planes[2] = (void *)frame;
+ mpi->planes[3] = (void *)(uintptr_t)VDP_INVALID_HANDLE;
+ }
+ return mpi;
+}
+
+struct mp_vdpau_mixer_frame *mp_vdpau_mixed_frame_get(struct mp_image *mpi)
+{
+ if (mpi->imgfmt != IMGFMT_VDPAU)
+ return NULL;
+ return (void *)mpi->planes[2];
+}
+
+struct mp_vdpau_mixer *mp_vdpau_mixer_create(struct mp_vdpau_ctx *vdp_ctx,
+ struct mp_log *log)
+{
+ struct mp_vdpau_mixer *mixer = talloc_ptrtype(NULL, mixer);
+ *mixer = (struct mp_vdpau_mixer){
+ .ctx = vdp_ctx,
+ .log = log,
+ .video_mixer = VDP_INVALID_HANDLE,
+ };
+ mp_vdpau_handle_preemption(mixer->ctx, &mixer->preemption_counter);
+ return mixer;
+}
+
+void mp_vdpau_mixer_destroy(struct mp_vdpau_mixer *mixer)
+{
+ struct vdp_functions *vdp = &mixer->ctx->vdp;
+ VdpStatus vdp_st;
+ if (mixer->video_mixer != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->video_mixer_destroy(mixer->video_mixer);
+ CHECK_VDP_WARNING(mixer, "Error when calling vdp_video_mixer_destroy");
+ }
+ talloc_free(mixer);
+}
+
+static bool opts_equal(const struct mp_vdpau_mixer_opts *a,
+ const struct mp_vdpau_mixer_opts *b)
+{
+ return a->deint == b->deint && a->chroma_deint == b->chroma_deint &&
+ a->pullup == b->pullup && a->hqscaling == b->hqscaling &&
+ a->sharpen == b->sharpen && a->denoise == b->denoise;
+}
+
+static int set_video_attribute(struct mp_vdpau_mixer *mixer,
+ VdpVideoMixerAttribute attr,
+ const void *value, char *attr_name)
+{
+ struct vdp_functions *vdp = &mixer->ctx->vdp;
+ VdpStatus vdp_st;
+
+ vdp_st = vdp->video_mixer_set_attribute_values(mixer->video_mixer, 1,
+ &attr, &value);
+ if (vdp_st != VDP_STATUS_OK) {
+ MP_ERR(mixer, "Error setting video mixer attribute %s: %s\n", attr_name,
+ vdp->get_error_string(vdp_st));
+ return -1;
+ }
+ return 0;
+}
+
+#define SET_VIDEO_ATTR(attr_name, attr_type, value) set_video_attribute(mixer, \
+ VDP_VIDEO_MIXER_ATTRIBUTE_ ## attr_name, &(attr_type){value},\
+ # attr_name)
+static int create_vdp_mixer(struct mp_vdpau_mixer *mixer,
+ VdpChromaType chroma_type, uint32_t w, uint32_t h)
+{
+ struct vdp_functions *vdp = &mixer->ctx->vdp;
+ VdpDevice vdp_device = mixer->ctx->vdp_device;
+ struct mp_vdpau_mixer_opts *opts = &mixer->opts;
+#define VDP_NUM_MIXER_PARAMETER 3
+#define MAX_NUM_FEATURES 6
+ int i;
+ VdpStatus vdp_st;
+
+ MP_VERBOSE(mixer, "Recreating vdpau video mixer.\n");
+
+ int feature_count = 0;
+ VdpVideoMixerFeature features[MAX_NUM_FEATURES];
+ VdpBool feature_enables[MAX_NUM_FEATURES];
+ static const VdpVideoMixerParameter parameters[VDP_NUM_MIXER_PARAMETER] = {
+ VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
+ VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
+ VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE,
+ };
+ const void *const parameter_values[VDP_NUM_MIXER_PARAMETER] = {
+ &(uint32_t){w},
+ &(uint32_t){h},
+ &(VdpChromaType){chroma_type},
+ };
+ if (opts->deint >= 3)
+ features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL;
+ if (opts->deint == 4)
+ features[feature_count++] =
+ VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL;
+ if (opts->pullup)
+ features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_INVERSE_TELECINE;
+ if (opts->denoise)
+ features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION;
+ if (opts->sharpen)
+ features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_SHARPNESS;
+ if (opts->hqscaling) {
+ VdpVideoMixerFeature hqscaling_feature =
+ VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1 + opts->hqscaling - 1;
+ VdpBool hqscaling_available;
+ vdp_st = vdp->video_mixer_query_feature_support(vdp_device,
+ hqscaling_feature,
+ &hqscaling_available);
+ CHECK_VDP_ERROR(mixer, "Error when calling video_mixer_query_feature_support");
+ if (hqscaling_available) {
+ features[feature_count++] = hqscaling_feature;
+ } else {
+ MP_ERR(mixer, "Your hardware or VDPAU library does not support "
+ "requested hqscaling.\n");
+ }
+ }
+
+ vdp_st = vdp->video_mixer_create(vdp_device, feature_count, features,
+ VDP_NUM_MIXER_PARAMETER,
+ parameters, parameter_values,
+ &mixer->video_mixer);
+ if (vdp_st != VDP_STATUS_OK)
+ mixer->video_mixer = VDP_INVALID_HANDLE;
+
+ CHECK_VDP_ERROR(mixer, "Error when calling vdp_video_mixer_create");
+
+ mixer->initialized = true;
+ mixer->current_chroma_type = chroma_type;
+ mixer->current_w = w;
+ mixer->current_h = h;
+
+ for (i = 0; i < feature_count; i++)
+ feature_enables[i] = VDP_TRUE;
+ if (feature_count) {
+ vdp_st = vdp->video_mixer_set_feature_enables(mixer->video_mixer,
+ feature_count, features,
+ feature_enables);
+ CHECK_VDP_WARNING(mixer, "Error calling vdp_video_mixer_set_feature_enables");
+ }
+ if (opts->denoise)
+ SET_VIDEO_ATTR(NOISE_REDUCTION_LEVEL, float, opts->denoise);
+ if (opts->sharpen)
+ SET_VIDEO_ATTR(SHARPNESS_LEVEL, float, opts->sharpen);
+ if (!opts->chroma_deint)
+ SET_VIDEO_ATTR(SKIP_CHROMA_DEINTERLACE, uint8_t, 1);
+
+ struct mp_cmat yuv2rgb;
+ VdpCSCMatrix matrix;
+
+ struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
+ mp_csp_set_image_params(&cparams, &mixer->image_params);
+ if (mixer->video_eq)
+ mp_csp_equalizer_state_get(mixer->video_eq, &cparams);
+ mp_get_csp_matrix(&cparams, &yuv2rgb);
+
+ for (int r = 0; r < 3; r++) {
+ for (int c = 0; c < 3; c++)
+ matrix[r][c] = yuv2rgb.m[r][c];
+ matrix[r][3] = yuv2rgb.c[r];
+ }
+
+ set_video_attribute(mixer, VDP_VIDEO_MIXER_ATTRIBUTE_CSC_MATRIX,
+ &matrix, "CSC matrix");
+
+ return 0;
+}
+
+// If opts is NULL, use the opts as implied by the video image.
+int mp_vdpau_mixer_render(struct mp_vdpau_mixer *mixer,
+ struct mp_vdpau_mixer_opts *opts,
+ VdpOutputSurface output, VdpRect *output_rect,
+ struct mp_image *video, VdpRect *video_rect)
+{
+ struct vdp_functions *vdp = &mixer->ctx->vdp;
+ VdpStatus vdp_st;
+ VdpRect fallback_rect = {0, 0, video->w, video->h};
+
+ if (!video_rect)
+ video_rect = &fallback_rect;
+
+ int pe = mp_vdpau_handle_preemption(mixer->ctx, &mixer->preemption_counter);
+ if (pe < 1) {
+ mixer->video_mixer = VDP_INVALID_HANDLE;
+ if (pe < 0)
+ return -1;
+ }
+
+ if (video->imgfmt == IMGFMT_VDPAU_OUTPUT) {
+ VdpOutputSurface surface = (uintptr_t)video->planes[3];
+ int flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_0;
+ vdp_st = vdp->output_surface_render_output_surface(output,
+ output_rect,
+ surface,
+ video_rect,
+ NULL, NULL, flags);
+ CHECK_VDP_WARNING(mixer, "Error when calling "
+ "vdp_output_surface_render_output_surface");
+ return 0;
+ }
+
+ if (video->imgfmt != IMGFMT_VDPAU)
+ return -1;
+
+ struct mp_vdpau_mixer_frame *frame = mp_vdpau_mixed_frame_get(video);
+ struct mp_vdpau_mixer_frame fallback = {{0}};
+ if (!frame) {
+ frame = &fallback;
+ frame->current = (uintptr_t)video->planes[3];
+ for (int n = 0; n < MP_VDP_HISTORY_FRAMES; n++)
+ frame->past[n] = frame->future[n] = VDP_INVALID_HANDLE;
+ frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;
+ }
+
+ if (!opts)
+ opts = &frame->opts;
+
+ if (mixer->video_mixer == VDP_INVALID_HANDLE)
+ mixer->initialized = false;
+
+ if (mixer->video_eq && mp_csp_equalizer_state_changed(mixer->video_eq))
+ mixer->initialized = false;
+
+ VdpChromaType s_chroma_type;
+ uint32_t s_w, s_h;
+
+ vdp_st = vdp->video_surface_get_parameters(frame->current, &s_chroma_type,
+ &s_w, &s_h);
+ CHECK_VDP_ERROR(mixer, "Error when calling vdp_video_surface_get_parameters");
+
+ if (!mixer->initialized || !opts_equal(opts, &mixer->opts) ||
+ !mp_image_params_equal(&video->params, &mixer->image_params) ||
+ mixer->current_w != s_w || mixer->current_h != s_h ||
+ mixer->current_chroma_type != s_chroma_type)
+ {
+ mixer->opts = *opts;
+ mixer->image_params = video->params;
+ if (mixer->video_mixer != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->video_mixer_destroy(mixer->video_mixer);
+ CHECK_VDP_WARNING(mixer, "Error when calling vdp_video_mixer_destroy");
+ }
+ mixer->video_mixer = VDP_INVALID_HANDLE;
+ mixer->initialized = false;
+ if (create_vdp_mixer(mixer, s_chroma_type, s_w, s_h) < 0)
+ return -1;
+ }
+
+ vdp_st = vdp->video_mixer_render(mixer->video_mixer, VDP_INVALID_HANDLE,
+ 0, frame->field,
+ MP_VDP_HISTORY_FRAMES, frame->past,
+ frame->current,
+ MP_VDP_HISTORY_FRAMES, frame->future,
+ video_rect,
+ output, NULL, output_rect,
+ 0, NULL);
+ CHECK_VDP_WARNING(mixer, "Error when calling vdp_video_mixer_render");
+ return 0;
+}
diff --git a/video/vdpau_mixer.h b/video/vdpau_mixer.h
new file mode 100644
index 0000000..4abe87e
--- /dev/null
+++ b/video/vdpau_mixer.h
@@ -0,0 +1,61 @@
+#ifndef MP_VDPAU_MIXER_H_
+#define MP_VDPAU_MIXER_H_
+
+#include <stdbool.h>
+
+#include "csputils.h"
+#include "mp_image.h"
+#include "vdpau.h"
+
+struct mp_vdpau_mixer_opts {
+ int deint;
+ bool chroma_deint;
+ bool pullup;
+ float denoise;
+ float sharpen;
+ int hqscaling;
+};
+
+#define MP_VDP_HISTORY_FRAMES 2
+
+struct mp_vdpau_mixer_frame {
+ // settings
+ struct mp_vdpau_mixer_opts opts;
+ // video data
+ VdpVideoMixerPictureStructure field;
+ VdpVideoSurface past[MP_VDP_HISTORY_FRAMES];
+ VdpVideoSurface current;
+ VdpVideoSurface future[MP_VDP_HISTORY_FRAMES];
+};
+
+struct mp_vdpau_mixer {
+ struct mp_log *log;
+ struct mp_vdpau_ctx *ctx;
+ uint64_t preemption_counter;
+ bool initialized;
+
+ struct mp_image_params image_params;
+ struct mp_vdpau_mixer_opts opts;
+
+ VdpChromaType current_chroma_type;
+ int current_w, current_h;
+
+ struct mp_csp_equalizer_state *video_eq;
+
+ VdpVideoMixer video_mixer;
+};
+
+struct mp_image *mp_vdpau_mixed_frame_create(struct mp_image *base);
+
+struct mp_vdpau_mixer_frame *mp_vdpau_mixed_frame_get(struct mp_image *mpi);
+
+struct mp_vdpau_mixer *mp_vdpau_mixer_create(struct mp_vdpau_ctx *vdp_ctx,
+ struct mp_log *log);
+void mp_vdpau_mixer_destroy(struct mp_vdpau_mixer *mixer);
+
+int mp_vdpau_mixer_render(struct mp_vdpau_mixer *mixer,
+ struct mp_vdpau_mixer_opts *opts,
+ VdpOutputSurface output, VdpRect *output_rect,
+ struct mp_image *video, VdpRect *video_rect);
+
+#endif
diff --git a/video/zimg.c b/video/zimg.c
new file mode 100644
index 0000000..5ff300c
--- /dev/null
+++ b/video/zimg.c
@@ -0,0 +1,730 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include <libavutil/cpu.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "csputils.h"
+#include "misc/thread_pool.h"
+#include "misc/thread_tools.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "repack.h"
+#include "video/fmt-conversion.h"
+#include "video/img_format.h"
+#include "zimg.h"
+#include "config.h"
+
+static_assert(MP_IMAGE_BYTE_ALIGN >= ZIMG_ALIGN, "");
+
+#define HAVE_ZIMG_ALPHA (ZIMG_API_VERSION >= ZIMG_MAKE_API_VERSION(2, 4))
+
+static const struct m_opt_choice_alternatives mp_zimg_scalers[] = {
+ {"point", ZIMG_RESIZE_POINT},
+ {"bilinear", ZIMG_RESIZE_BILINEAR},
+ {"bicubic", ZIMG_RESIZE_BICUBIC},
+ {"spline16", ZIMG_RESIZE_SPLINE16},
+ {"spline36", ZIMG_RESIZE_SPLINE36},
+ {"lanczos", ZIMG_RESIZE_LANCZOS},
+ {0}
+};
+
+const struct zimg_opts zimg_opts_defaults = {
+ .scaler = ZIMG_RESIZE_LANCZOS,
+ .scaler_params = {NAN, NAN},
+ .scaler_chroma_params = {NAN, NAN},
+ .scaler_chroma = ZIMG_RESIZE_BILINEAR,
+ .dither = ZIMG_DITHER_RANDOM,
+ .fast = true,
+};
+
+#define OPT_PARAM(var) OPT_DOUBLE(var), .flags = M_OPT_DEFAULT_NAN
+
+#define OPT_BASE_STRUCT struct zimg_opts
+const struct m_sub_options zimg_conf = {
+ .opts = (struct m_option[]) {
+ {"scaler", OPT_CHOICE_C(scaler, mp_zimg_scalers)},
+ {"scaler-param-a", OPT_PARAM(scaler_params[0])},
+ {"scaler-param-b", OPT_PARAM(scaler_params[1])},
+ {"scaler-chroma", OPT_CHOICE_C(scaler_chroma, mp_zimg_scalers)},
+ {"scaler-chroma-param-a", OPT_PARAM(scaler_chroma_params[0])},
+ {"scaler-chroma-param-b", OPT_PARAM(scaler_chroma_params[1])},
+ {"dither", OPT_CHOICE(dither,
+ {"no", ZIMG_DITHER_NONE},
+ {"ordered", ZIMG_DITHER_ORDERED},
+ {"random", ZIMG_DITHER_RANDOM},
+ {"error-diffusion", ZIMG_DITHER_ERROR_DIFFUSION})},
+ {"fast", OPT_BOOL(fast)},
+ {"threads", OPT_CHOICE(threads, {"auto", 0}), M_RANGE(1, 64)},
+ {0}
+ },
+ .size = sizeof(struct zimg_opts),
+ .defaults = &zimg_opts_defaults,
+};
+
+struct mp_zimg_state {
+ zimg_filter_graph *graph;
+ void *tmp;
+ void *tmp_alloc;
+ struct mp_zimg_repack *src;
+ struct mp_zimg_repack *dst;
+ int slice_y, slice_h; // y start position, height of target slice
+ double scale_y;
+ struct mp_waiter thread_waiter;
+};
+
+struct mp_zimg_repack {
+ bool pack; // if false, this is for unpacking
+ struct mp_image_params fmt; // original mp format (possibly packed format,
+ // swapped endian)
+ int zimgfmt; // zimg equivalent unpacked format
+ int num_planes; // number of planes involved
+ unsigned zmask[4]; // zmask[mp_index] = zimg mask (using mp index!)
+ int z_planes[4]; // z_planes[zimg_index] = mp_index (or -1)
+
+ struct mp_repack *repack; // converting to/from planar
+
+ // Temporary memory for slice-wise repacking. This may be set even if repack
+ // is not set (then it may be used to avoid alignment issues). This has
+ // about one slice worth of data.
+ struct mp_image *tmp;
+
+ // Temporary memory for zimg buffer.
+ zimg_image_buffer zbuf;
+ struct mp_image cropped_tmp;
+
+ int real_w, real_h; // aligned size
+};
+
+static void mp_zimg_update_from_cmdline(struct mp_zimg_context *ctx)
+{
+ m_config_cache_update(ctx->opts_cache);
+
+ struct zimg_opts *opts = ctx->opts_cache->opts;
+ ctx->opts = *opts;
+}
+
+static zimg_chroma_location_e mp_to_z_chroma(enum mp_chroma_location cl)
+{
+ switch (cl) {
+ case MP_CHROMA_TOPLEFT: return ZIMG_CHROMA_TOP_LEFT;
+ case MP_CHROMA_LEFT: return ZIMG_CHROMA_LEFT;
+ case MP_CHROMA_CENTER: return ZIMG_CHROMA_CENTER;
+ default: return ZIMG_CHROMA_LEFT;
+ }
+}
+
+static zimg_matrix_coefficients_e mp_to_z_matrix(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_BT_601: return ZIMG_MATRIX_BT470_BG;
+ case MP_CSP_BT_709: return ZIMG_MATRIX_BT709;
+ case MP_CSP_SMPTE_240M: return ZIMG_MATRIX_ST240_M;
+ case MP_CSP_BT_2020_NC: return ZIMG_MATRIX_BT2020_NCL;
+ case MP_CSP_BT_2020_C: return ZIMG_MATRIX_BT2020_CL;
+ case MP_CSP_RGB: return ZIMG_MATRIX_RGB;
+ case MP_CSP_XYZ: return ZIMG_MATRIX_RGB;
+ case MP_CSP_YCGCO: return ZIMG_MATRIX_YCGCO;
+ default: return ZIMG_MATRIX_BT709;
+ }
+}
+
+static zimg_transfer_characteristics_e mp_to_z_trc(enum mp_csp_trc trc)
+{
+ switch (trc) {
+ case MP_CSP_TRC_BT_1886: return ZIMG_TRANSFER_BT709;
+ case MP_CSP_TRC_SRGB: return ZIMG_TRANSFER_IEC_61966_2_1;
+ case MP_CSP_TRC_LINEAR: return ZIMG_TRANSFER_LINEAR;
+ case MP_CSP_TRC_GAMMA22: return ZIMG_TRANSFER_BT470_M;
+ case MP_CSP_TRC_GAMMA28: return ZIMG_TRANSFER_BT470_BG;
+ case MP_CSP_TRC_PQ: return ZIMG_TRANSFER_ST2084;
+ case MP_CSP_TRC_HLG: return ZIMG_TRANSFER_ARIB_B67;
+#if HAVE_ZIMG_ST428
+ case MP_CSP_TRC_ST428: return ZIMG_TRANSFER_ST428;
+#endif
+ case MP_CSP_TRC_GAMMA18: // ?
+ case MP_CSP_TRC_GAMMA20:
+ case MP_CSP_TRC_GAMMA24:
+ case MP_CSP_TRC_GAMMA26:
+ case MP_CSP_TRC_PRO_PHOTO:
+ case MP_CSP_TRC_V_LOG:
+ case MP_CSP_TRC_S_LOG1:
+ case MP_CSP_TRC_S_LOG2: // ?
+ default: return ZIMG_TRANSFER_BT709;
+ }
+}
+
+static zimg_color_primaries_e mp_to_z_prim(enum mp_csp_prim prim)
+{
+ switch (prim) {
+ case MP_CSP_PRIM_BT_601_525:return ZIMG_PRIMARIES_ST170_M;
+ case MP_CSP_PRIM_BT_601_625:return ZIMG_PRIMARIES_BT470_BG;
+ case MP_CSP_PRIM_BT_709: return ZIMG_PRIMARIES_BT709;
+ case MP_CSP_PRIM_BT_2020: return ZIMG_PRIMARIES_BT2020;
+ case MP_CSP_PRIM_BT_470M: return ZIMG_PRIMARIES_BT470_M;
+ case MP_CSP_PRIM_DCI_P3: return ZIMG_PRIMARIES_ST431_2;
+ case MP_CSP_PRIM_DISPLAY_P3:return ZIMG_PRIMARIES_ST432_1;
+ case MP_CSP_PRIM_EBU_3213: return ZIMG_PRIMARIES_EBU3213_E;
+ case MP_CSP_PRIM_FILM_C: return ZIMG_PRIMARIES_FILM;
+ case MP_CSP_PRIM_CIE_1931:
+ case MP_CSP_PRIM_APPLE: // ?
+ case MP_CSP_PRIM_ADOBE:
+ case MP_CSP_PRIM_PRO_PHOTO:
+ case MP_CSP_PRIM_V_GAMUT:
+ case MP_CSP_PRIM_S_GAMUT: // ?
+ case MP_CSP_PRIM_ACES_AP0:
+ case MP_CSP_PRIM_ACES_AP1:
+ default: return ZIMG_PRIMARIES_BT709;
+ }
+}
+
+static void destroy_zimg(struct mp_zimg_context *ctx)
+{
+ for (int n = 0; n < ctx->num_states; n++) {
+ struct mp_zimg_state *st = ctx->states[n];
+ talloc_free(st->tmp_alloc);
+ zimg_filter_graph_free(st->graph);
+ TA_FREEP(&st->src);
+ TA_FREEP(&st->dst);
+ talloc_free(st);
+ }
+ ctx->num_states = 0;
+}
+
+static void free_mp_zimg(void *p)
+{
+ struct mp_zimg_context *ctx = p;
+
+ destroy_zimg(ctx);
+ TA_FREEP(&ctx->tp);
+}
+
+struct mp_zimg_context *mp_zimg_alloc(void)
+{
+ struct mp_zimg_context *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct mp_zimg_context) {
+ .log = mp_null_log,
+ };
+ ctx->opts = *(struct zimg_opts *)zimg_conf.defaults;
+ talloc_set_destructor(ctx, free_mp_zimg);
+ return ctx;
+}
+
+void mp_zimg_enable_cmdline_opts(struct mp_zimg_context *ctx,
+ struct mpv_global *g)
+{
+ if (ctx->opts_cache)
+ return;
+
+ ctx->opts_cache = m_config_cache_alloc(ctx, g, &zimg_conf);
+ destroy_zimg(ctx); // force update
+ mp_zimg_update_from_cmdline(ctx); // first update
+}
+
+static int repack_entrypoint(void *user, unsigned i, unsigned x0, unsigned x1)
+{
+ struct mp_zimg_repack *r = user;
+
+ // If reading is not aligned, just read slightly more data.
+ if (!r->pack)
+ x0 &= ~(unsigned)(mp_repack_get_align_x(r->repack) - 1);
+
+ // mp_repack requirements and zimg guarantees.
+ assert(!(i & (mp_repack_get_align_y(r->repack) - 1)));
+ assert(!(x0 & (mp_repack_get_align_x(r->repack) - 1)));
+
+ unsigned i_src = i & (r->pack ? r->zmask[0] : ZIMG_BUFFER_MAX);
+ unsigned i_dst = i & (r->pack ? ZIMG_BUFFER_MAX : r->zmask[0]);
+
+ repack_line(r->repack, x0, i_dst, x0, i_src, x1 - x0);
+
+ return 0;
+}
+
+static bool wrap_buffer(struct mp_zimg_state *st, struct mp_zimg_repack *r,
+ struct mp_image *a_mpi)
+{
+ zimg_image_buffer *buf = &r->zbuf;
+ *buf = (zimg_image_buffer){ZIMG_API_VERSION};
+
+ struct mp_image *mpi = a_mpi;
+ if (r->pack) {
+ mpi = &r->cropped_tmp;
+ *mpi = *a_mpi;
+ int y1 = st->slice_y + st->slice_h;
+ // Due to subsampling we may assume the image to be bigger than it
+ // actually is (see real_h in setup_format).
+ if (mpi->h < y1) {
+ assert(y1 - mpi->h < 4);
+ mp_image_set_size(mpi, mpi->w, y1);
+ }
+ mp_image_crop(mpi, 0, st->slice_y, mpi->w, y1);
+ }
+
+ bool direct[MP_MAX_PLANES] = {0};
+
+ for (int p = 0; p < mpi->num_planes; p++) {
+ // If alignment is good, try to avoid copy.
+ direct[p] = !((uintptr_t)mpi->planes[p] % ZIMG_ALIGN) &&
+ !(mpi->stride[p] % ZIMG_ALIGN);
+ }
+
+ if (!repack_config_buffers(r->repack, 0, r->pack ? mpi : r->tmp,
+ 0, r->pack ? r->tmp : mpi, direct))
+ return false;
+
+ for (int n = 0; n < MP_ARRAY_SIZE(buf->plane); n++) {
+ // Note: this is really the only place we have to care about plane
+ // permutation (zimg_image_buffer may have a different plane order
+ // than the shadow mpi like r->tmp). We never use the zimg indexes
+ // in other places.
+ int mplane = r->z_planes[n];
+ if (mplane < 0)
+ continue;
+
+ struct mp_image *tmpi = direct[mplane] ? mpi : r->tmp;
+ buf->plane[n].data = tmpi->planes[mplane];
+ buf->plane[n].stride = tmpi->stride[mplane];
+ buf->plane[n].mask = direct[mplane] ? ZIMG_BUFFER_MAX : r->zmask[mplane];
+ }
+
+ return true;
+}
+
+// (ctx and st can be NULL for probing.)
+static bool setup_format(zimg_image_format *zfmt, struct mp_zimg_repack *r,
+ bool pack, struct mp_image_params *user_fmt,
+ struct mp_zimg_context *ctx,
+ struct mp_zimg_state *st)
+{
+ r->fmt = *user_fmt;
+ r->pack = pack;
+
+ zimg_image_format_default(zfmt, ZIMG_API_VERSION);
+
+ int rp_flags = 0;
+
+ // For e.g. RGB565, go to lowest depth on pack for less weird dithering.
+ if (r->pack) {
+ rp_flags |= REPACK_CREATE_ROUND_DOWN;
+ } else {
+ rp_flags |= REPACK_CREATE_EXPAND_8BIT;
+ }
+
+ r->repack = mp_repack_create_planar(r->fmt.imgfmt, r->pack, rp_flags);
+ if (!r->repack)
+ return false;
+
+ int align_x = mp_repack_get_align_x(r->repack);
+
+ r->zimgfmt = r->pack ? mp_repack_get_format_src(r->repack)
+ : mp_repack_get_format_dst(r->repack);
+
+ if (ctx) {
+ talloc_steal(r, r->repack);
+ } else {
+ TA_FREEP(&r->repack);
+ }
+
+ struct mp_image_params fmt = r->fmt;
+ mp_image_params_guess_csp(&fmt);
+
+ struct mp_regular_imgfmt desc;
+ if (!mp_get_regular_imgfmt(&desc, r->zimgfmt))
+ return false;
+
+ // Relies on zimg callbacks reading on 64 byte alignment.
+ if (!MP_IS_POWER_OF_2(align_x) || align_x > 64 / desc.component_size)
+ return false;
+
+ // no weird stuff
+ if (desc.num_planes > 4)
+ return false;
+
+ for (int n = 0; n < 4; n++)
+ r->z_planes[n] = -1;
+
+ for (int n = 0; n < desc.num_planes; n++) {
+ if (desc.planes[n].num_components != 1)
+ return false;
+ int c = desc.planes[n].components[0];
+ if (c < 1 || c > 4)
+ return false;
+ if (c < 4) {
+ // Unfortunately, ffmpeg prefers GBR order for planar RGB, while zimg
+ // is sane. This makes it necessary to determine and fix the order.
+ r->z_planes[c - 1] = n;
+ } else {
+ r->z_planes[3] = n; // alpha, always plane 4 in zimg
+
+#if HAVE_ZIMG_ALPHA
+ zfmt->alpha = fmt.alpha == MP_ALPHA_PREMUL
+ ? ZIMG_ALPHA_PREMULTIPLIED : ZIMG_ALPHA_STRAIGHT;
+#else
+ return false;
+#endif
+ }
+ }
+
+ r->num_planes = desc.num_planes;
+
+ // Take care of input/output size, including slicing.
+ // Note: formats with subsampled chroma may have odd width or height in
+ // mpv and FFmpeg. This is because the width/height is actually a cropping
+ // rectangle. Reconstruct the image allocation size and set the cropping.
+ zfmt->width = r->real_w = MP_ALIGN_UP(fmt.w, 1 << desc.chroma_xs);
+ zfmt->height = r->real_h = MP_ALIGN_UP(fmt.h, 1 << desc.chroma_ys);
+ if (st) {
+ if (r->pack) {
+ zfmt->height = r->real_h = st->slice_h =
+ MPMIN(st->slice_y + st->slice_h, r->real_h) - st->slice_y;
+
+ assert(MP_IS_ALIGNED(r->real_h, 1 << desc.chroma_ys));
+ } else {
+ // Relies on st->dst being initialized first.
+ struct mp_zimg_repack *dst = st->dst;
+
+ zfmt->active_region.width = dst->real_w * (double)fmt.w / dst->fmt.w;
+ zfmt->active_region.height = dst->real_h * st->scale_y;
+
+ zfmt->active_region.top = st->slice_y * st->scale_y;
+ }
+ }
+
+ zfmt->subsample_w = desc.chroma_xs;
+ zfmt->subsample_h = desc.chroma_ys;
+
+ zfmt->color_family = ZIMG_COLOR_YUV;
+ if (desc.num_planes <= 2) {
+ zfmt->color_family = ZIMG_COLOR_GREY;
+ } else if (fmt.color.space == MP_CSP_RGB || fmt.color.space == MP_CSP_XYZ) {
+ zfmt->color_family = ZIMG_COLOR_RGB;
+ }
+
+ if (desc.component_type == MP_COMPONENT_TYPE_UINT &&
+ desc.component_size == 1)
+ {
+ zfmt->pixel_type = ZIMG_PIXEL_BYTE;
+ } else if (desc.component_type == MP_COMPONENT_TYPE_UINT &&
+ desc.component_size == 2)
+ {
+ zfmt->pixel_type = ZIMG_PIXEL_WORD;
+ } else if (desc.component_type == MP_COMPONENT_TYPE_FLOAT &&
+ desc.component_size == 2)
+ {
+ zfmt->pixel_type = ZIMG_PIXEL_HALF;
+ } else if (desc.component_type == MP_COMPONENT_TYPE_FLOAT &&
+ desc.component_size == 4)
+ {
+ zfmt->pixel_type = ZIMG_PIXEL_FLOAT;
+ } else {
+ return false;
+ }
+
+ // (Formats like P010 are basically reported as P016.)
+ zfmt->depth = desc.component_size * 8 + MPMIN(0, desc.component_pad);
+
+ zfmt->pixel_range = fmt.color.levels == MP_CSP_LEVELS_PC ?
+ ZIMG_RANGE_FULL : ZIMG_RANGE_LIMITED;
+
+ zfmt->matrix_coefficients = mp_to_z_matrix(fmt.color.space);
+ zfmt->transfer_characteristics = mp_to_z_trc(fmt.color.gamma);
+ // For MP_CSP_XYZ only valid primaries are defined in ST 428-1
+ zfmt->color_primaries = fmt.color.space == MP_CSP_XYZ
+ ? ZIMG_PRIMARIES_ST428
+ : mp_to_z_prim(fmt.color.primaries);
+ zfmt->chroma_location = mp_to_z_chroma(fmt.chroma_location);
+
+ if (ctx && ctx->opts.fast) {
+ // mpv's default for RGB output slows down zimg significantly.
+ if (zfmt->transfer_characteristics == ZIMG_TRANSFER_IEC_61966_2_1 &&
+ zfmt->color_family == ZIMG_COLOR_RGB)
+ zfmt->transfer_characteristics = ZIMG_TRANSFER_BT709;
+ }
+
+ // mpv treats _some_ gray formats as RGB; zimg doesn't like this.
+ if (zfmt->color_family == ZIMG_COLOR_GREY &&
+ zfmt->matrix_coefficients == ZIMG_MATRIX_RGB)
+ zfmt->matrix_coefficients = ZIMG_MATRIX_BT470_BG;
+
+ return true;
+}
+
+static bool allocate_buffer(struct mp_zimg_state *st, struct mp_zimg_repack *r)
+{
+ unsigned lines = 0;
+ int err;
+ if (r->pack) {
+ err = zimg_filter_graph_get_output_buffering(st->graph, &lines);
+ } else {
+ err = zimg_filter_graph_get_input_buffering(st->graph, &lines);
+ }
+
+ if (err)
+ return false;
+
+ r->zmask[0] = zimg_select_buffer_mask(lines);
+
+ // Either ZIMG_BUFFER_MAX, or a power-of-2 slice buffer.
+ assert(r->zmask[0] == ZIMG_BUFFER_MAX || MP_IS_POWER_OF_2(r->zmask[0] + 1));
+
+ int h = r->zmask[0] == ZIMG_BUFFER_MAX ? r->real_h : r->zmask[0] + 1;
+ if (h >= r->real_h) {
+ h = r->real_h;
+ r->zmask[0] = ZIMG_BUFFER_MAX;
+ }
+
+ r->tmp = mp_image_alloc(r->zimgfmt, r->real_w, h);
+ talloc_steal(r, r->tmp);
+
+ if (!r->tmp)
+ return false;
+
+ // Note: although zimg doesn't require that the chroma plane's zmask is
+ // divided by the full size zmask, the repack callback requires it,
+ // since mp_repack can handle only proper slices.
+ for (int n = 1; n < r->tmp->fmt.num_planes; n++) {
+ r->zmask[n] = r->zmask[0];
+ if (r->zmask[0] != ZIMG_BUFFER_MAX)
+ r->zmask[n] = r->zmask[n] >> r->tmp->fmt.ys[n];
+ }
+
+ return true;
+}
+
+static bool mp_zimg_state_init(struct mp_zimg_context *ctx,
+ struct mp_zimg_state *st,
+ int slice_y, int slice_h)
+{
+ struct zimg_opts *opts = &ctx->opts;
+
+ st->src = talloc_zero(NULL, struct mp_zimg_repack);
+ st->dst = talloc_zero(NULL, struct mp_zimg_repack);
+
+ st->scale_y = ctx->src.h / (double)ctx->dst.h;
+ st->slice_y = slice_y;
+ st->slice_h = slice_h;
+
+ zimg_image_format src_fmt, dst_fmt;
+
+ // Note: do dst first, because src uses fields from dst.
+ if (!setup_format(&dst_fmt, st->dst, true, &ctx->dst, ctx, st) ||
+ !setup_format(&src_fmt, st->src, false, &ctx->src, ctx, st))
+ return false;
+
+ zimg_graph_builder_params params;
+ zimg_graph_builder_params_default(&params, ZIMG_API_VERSION);
+
+ params.resample_filter = opts->scaler;
+ params.filter_param_a = opts->scaler_params[0];
+ params.filter_param_b = opts->scaler_params[1];
+
+ params.resample_filter_uv = opts->scaler_chroma;
+ params.filter_param_a_uv = opts->scaler_chroma_params[0];
+ params.filter_param_b_uv = opts->scaler_chroma_params[1];
+
+ params.dither_type = opts->dither;
+
+ params.cpu_type = ZIMG_CPU_AUTO_64B;
+
+ if (opts->fast)
+ params.allow_approximate_gamma = 1;
+
+ // leave at default for SDR, which means 100 cd/m^2 for zimg
+ if (ctx->dst.color.hdr.max_luma > 0 && mp_trc_is_hdr(ctx->dst.color.gamma))
+ params.nominal_peak_luminance = ctx->dst.color.hdr.max_luma;
+
+ st->graph = zimg_filter_graph_build(&src_fmt, &dst_fmt, &params);
+ if (!st->graph) {
+ char err[128] = {0};
+ zimg_get_last_error(err, sizeof(err) - 1);
+ MP_ERR(ctx, "zimg_filter_graph_build: %s \n", err);
+ return false;
+ }
+
+ size_t tmp_size;
+ if (!zimg_filter_graph_get_tmp_size(st->graph, &tmp_size)) {
+ tmp_size = MP_ALIGN_UP(tmp_size, ZIMG_ALIGN) + ZIMG_ALIGN;
+ st->tmp_alloc = ta_alloc_size(NULL, tmp_size);
+ if (st->tmp_alloc)
+ st->tmp = (void *)MP_ALIGN_UP((uintptr_t)st->tmp_alloc, ZIMG_ALIGN);
+ }
+
+ if (!st->tmp_alloc)
+ return false;
+
+ if (!allocate_buffer(st, st->src) || !allocate_buffer(st, st->dst))
+ return false;
+
+ return true;
+}
+
+bool mp_zimg_config(struct mp_zimg_context *ctx)
+{
+ destroy_zimg(ctx);
+
+ if (ctx->opts_cache)
+ mp_zimg_update_from_cmdline(ctx);
+
+ int slices = ctx->opts.threads;
+ if (slices < 1)
+ slices = av_cpu_count();
+ slices = MPCLAMP(slices, 1, 64);
+
+ struct mp_imgfmt_desc dstfmt = mp_imgfmt_get_desc(ctx->dst.imgfmt);
+ if (!dstfmt.align_y)
+ goto fail;
+ int full_h = MP_ALIGN_UP(ctx->dst.h, dstfmt.align_y);
+ int slice_h = (full_h + slices - 1) / slices;
+ slice_h = MP_ALIGN_UP(slice_h, dstfmt.align_y);
+ slice_h = MP_ALIGN_UP(slice_h, 64); // for dithering and minimum slice size
+ slices = (full_h + slice_h - 1) / slice_h;
+
+ int threads = slices - 1;
+ if (threads != ctx->current_thread_count) {
+ // Just destroy and recreate all - dumb and costly, but rarely happens.
+ TA_FREEP(&ctx->tp);
+ ctx->current_thread_count = 0;
+ if (threads) {
+ MP_VERBOSE(ctx, "using %d threads for scaling\n", threads);
+ ctx->tp = mp_thread_pool_create(NULL, threads, threads, threads);
+ if (!ctx->tp)
+ goto fail;
+ ctx->current_thread_count = threads;
+ }
+ }
+
+ for (int n = 0; n < slices; n++) {
+ struct mp_zimg_state *st = talloc_zero(NULL, struct mp_zimg_state);
+ MP_TARRAY_APPEND(ctx, ctx->states, ctx->num_states, st);
+
+ if (!mp_zimg_state_init(ctx, st, n * slice_h, slice_h))
+ goto fail;
+ }
+
+ assert(ctx->num_states == slices);
+
+ return true;
+
+fail:
+ destroy_zimg(ctx);
+ return false;
+}
+
+bool mp_zimg_config_image_params(struct mp_zimg_context *ctx)
+{
+ if (ctx->num_states) {
+ // All states are the same, so checking only one of them is sufficient.
+ struct mp_zimg_state *st = ctx->states[0];
+ if (st->src && mp_image_params_equal(&ctx->src, &st->src->fmt) &&
+ st->dst && mp_image_params_equal(&ctx->dst, &st->dst->fmt) &&
+ (!ctx->opts_cache || !m_config_cache_update(ctx->opts_cache)) &&
+ st->graph)
+ return true;
+ }
+ return mp_zimg_config(ctx);
+}
+
+static void do_convert(struct mp_zimg_state *st)
+{
+ assert(st->graph);
+
+ // An annoyance.
+ zimg_image_buffer *zsrc = &st->src->zbuf;
+ zimg_image_buffer_const zsrc_c = {ZIMG_API_VERSION};
+ for (int n = 0; n < MP_ARRAY_SIZE(zsrc_c.plane); n++) {
+ zsrc_c.plane[n].data = zsrc->plane[n].data;
+ zsrc_c.plane[n].stride = zsrc->plane[n].stride;
+ zsrc_c.plane[n].mask = zsrc->plane[n].mask;
+ }
+
+ // (The API promises to succeed if no user callbacks fail, so no need
+ // to check the return value.)
+ zimg_filter_graph_process(st->graph, &zsrc_c, &st->dst->zbuf, st->tmp,
+ repack_entrypoint, st->src,
+ repack_entrypoint, st->dst);
+}
+
+static void do_convert_thread(void *ptr)
+{
+ struct mp_zimg_state *st = ptr;
+
+ do_convert(st);
+ mp_waiter_wakeup(&st->thread_waiter, 0);
+}
+
+bool mp_zimg_convert(struct mp_zimg_context *ctx, struct mp_image *dst,
+ struct mp_image *src)
+{
+ ctx->src = src->params;
+ ctx->dst = dst->params;
+
+ if (!mp_zimg_config_image_params(ctx)) {
+ MP_ERR(ctx, "zimg initialization failed.\n");
+ return false;
+ }
+
+ for (int n = 0; n < ctx->num_states; n++) {
+ struct mp_zimg_state *st = ctx->states[n];
+
+ if (!wrap_buffer(st, st->src, src) || !wrap_buffer(st, st->dst, dst)) {
+ MP_ERR(ctx, "zimg repacker initialization failed.\n");
+ return false;
+ }
+ }
+
+ for (int n = 1; n < ctx->num_states; n++) {
+ struct mp_zimg_state *st = ctx->states[n];
+
+ st->thread_waiter = (struct mp_waiter)MP_WAITER_INITIALIZER;
+
+ bool r = mp_thread_pool_run(ctx->tp, do_convert_thread, st);
+ // This is guaranteed by the API; and unrolling would be inconvenient.
+ assert(r);
+ }
+
+ do_convert(ctx->states[0]);
+
+ for (int n = 1; n < ctx->num_states; n++) {
+ struct mp_zimg_state *st = ctx->states[n];
+
+ mp_waiter_wait(&st->thread_waiter);
+ }
+
+ return true;
+}
+
+static bool supports_format(int imgfmt, bool out)
+{
+ struct mp_image_params fmt = {.imgfmt = imgfmt};
+ struct mp_zimg_repack t;
+ zimg_image_format zfmt;
+ return setup_format(&zfmt, &t, out, &fmt, NULL, NULL);
+}
+
+bool mp_zimg_supports_in_format(int imgfmt)
+{
+ return supports_format(imgfmt, false);
+}
+
+bool mp_zimg_supports_out_format(int imgfmt)
+{
+ return supports_format(imgfmt, true);
+}
diff --git a/video/zimg.h b/video/zimg.h
new file mode 100644
index 0000000..be018ca
--- /dev/null
+++ b/video/zimg.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <stdbool.h>
+
+#include <zimg.h>
+
+#include "mp_image.h"
+
+#define ZIMG_ALIGN 64
+
+struct mpv_global;
+
+bool mp_zimg_supports_in_format(int imgfmt);
+bool mp_zimg_supports_out_format(int imgfmt);
+
+struct zimg_opts {
+ int scaler;
+ double scaler_params[2];
+ int scaler_chroma;
+ double scaler_chroma_params[2];
+ int dither;
+ bool fast;
+ int threads;
+};
+
+extern const struct zimg_opts zimg_opts_defaults;
+
+struct mp_zimg_context {
+ // Can be set for verbose error printing.
+ struct mp_log *log;
+
+ // User configuration. Note: changing these requires calling mp_zimg_config()
+ // to update the filter graph. The first mp_zimg_convert() call (or if the
+ // image format changes) will do this automatically.
+ struct zimg_opts opts;
+
+ // Input/output parameters. Note: if these mismatch with the
+ // mp_zimg_convert() parameters, mp_zimg_config() will be called
+ // automatically.
+ struct mp_image_params src, dst;
+
+ // Cached zimg state (if any). Private, do not touch.
+ struct m_config_cache *opts_cache;
+ struct mp_zimg_state **states;
+ int num_states;
+ struct mp_thread_pool *tp;
+ int current_thread_count;
+};
+
+// Allocate a zimg context. Always succeeds. Returns a talloc pointer (use
+// talloc_free() to release it).
+struct mp_zimg_context *mp_zimg_alloc(void);
+
+// Enable auto-update of parameters from command line. Don't try to set custom
+// options (other than possibly .src/.dst), because they might be overwritten
+// if the user changes any options.
+void mp_zimg_enable_cmdline_opts(struct mp_zimg_context *ctx,
+ struct mpv_global *g);
+
+// Try to build the conversion chain using the parameters currently set in ctx.
+// If this succeeds, mp_zimg_convert() will always succeed (probably), as long
+// as the input has the same parameters.
+// Returns false on error.
+bool mp_zimg_config(struct mp_zimg_context *ctx);
+
+// Similar to mp_zimg_config(), but assume none of the user parameters changed,
+// except possibly .src and .dst. This essentially checks whether src/dst
+// changed, and if so, calls mp_zimg_config().
+bool mp_zimg_config_image_params(struct mp_zimg_context *ctx);
+
+// Convert/scale src to dst. On failure, the data in dst is not touched.
+bool mp_zimg_convert(struct mp_zimg_context *ctx, struct mp_image *dst,
+ struct mp_image *src);